Sass 是一款強化 CSS 的輔助工具,它在 CSS 語法的基礎上增長了變量 (variables)、嵌套 (nested rules)、混合 (mixins)、導入 (inline imports) 等高級功能,這些拓展令 CSS 更增強大與優雅。使用 Sass 以及 Sass 的樣式庫(如 Compass)有助於更好地組織管理樣式文件,以及更高效地開發項目。css
// index.scss sass源碼 $redColor: red; $yellowBg: yellow; nav { height: 100px; border: 1px solid $redColor; } #content { height: 300px; p { margin: 10px; .selected { backgournd: $yellowBg; } } }
通過sass編譯後,生成的代碼結果以下:node
// sass編譯後的輸出的css代碼 nav { height: 100px; border: 1px solid red; } #content { height: 300px; } #content p { margin: 10px; } #content p .selected { backgournd: yellow; }
接下來咱們將實現一個簡單的Sass預處理,其基本功能包括:數組
Sass預處理器本質是一個編譯器,Sass的源文件是.scss文件,裏面的內容包含了Sass本身的語法,是沒法直接執行的,必須通過編譯轉換爲.css文件後才能執行,其編譯過程就是:sass
讀取sass源碼,而後對sass源碼進行詞法分析,生成一個一個的token;工具
而後對這些token進行語法分析,生成抽象語法樹(Abstract Syntax Tree,AST),解析成抽象語法樹後,就能夠很方便的拿到咱們須要的數據並進行相應的處理;測試
而後遍歷抽象語法樹,對抽象語法樹進行轉換,轉換成咱們須要的代碼輸出結構,方便輸出最終代碼,好比,由於Sass源碼採用了嵌套,因此咱們須要將選擇器變回鏈式結構;spa
雖然對抽象語法樹進行了相應的轉換,可是轉換後的結果仍然是對象的形式,因此咱們還須要進行代碼的生成,將對象形式轉換爲字符串形式輸出。code
① 詞法分析
詞法分析就是要找出源碼中包含的token,這個token也是一個對象,其中包含所屬的類型type、對應的值value(詞在源碼中對應的字符串內容)、當前token在源碼中的縮進值indent。其中type類型有變量定義、變量引用、選擇器、屬性、值。orm
{ type: "variableDef" | "variableRef" | "selector" | "property" | "value", // 當前詞所屬類型 value: string, // Sass源碼中對應的字符串內容 indent: number // 當前詞在Sass源碼中的縮進值 }
const sassSourceCode = ` `; // Sass的源碼 // 對Sass源碼以換行符進行分割 const lines = sassSourceCode.trim().split(/\n/);
// 遍歷每一行中的內容,將生成的token放到tokens數組中,最初爲[] lines.reduce((tokens, line) => { const spaces = line.match(/^\s+/) || [""]; // 匹配每行開頭的空格部分 const indent = spaces[0].length; // 拿到每行的縮進空格數量 const input = line.trim(); // 去除首尾空格 let words = input.split(/:/); // 用冒號進行分割,拿到每一行中的全部詞 }, []);
let firstWord = words.shift(); // 取出並刪除每行的第一個詞 const selectorReg = /([0-9a-zA-Z-.#\[\]]+)\s+\{$/; // 選擇器匹配正則 if (words.length === 0) { // 若是取出並刪除第一個詞後,words數組長度變爲爲0,說明該行只有一個詞,那麼這個詞就是選擇器 const result = firstWord.match(selectorReg); // 有多是 },冒號分割後words的長度也會變成0,因此須要進行正則匹配 if (result) { tokens.push({ // 將選擇器放到tokens中 type: "selector", value: result[1], indent }); } }
if (words.length === 0) { } else { // 變量定義、屬性、變量引用、值 let type = ""; if (/^\$/.test(firstWord)) { // 若是每行的第一個詞是以$開頭的,那麼這個詞就是一個變量定義 type = "variableDef"; // 那麼type就是變量定義,即variableDef } else { type = "property"; } tokens.push({ // 將變量定義或者屬性放到tokens中 type, value: firstWord, indent }); }
// 繼續取出words中剩餘的詞進行分析,剩下的詞多是值或者是變量引用兩種類型 while (firstWord = words.shift()) { // 取出下一個詞更新firstWord firstWord = firstWord.trim(); // 去除詞的首尾空格 const values = firstWord.split(/\s/); // 有些詞(1px solid red)可能包含多個值,因此須要用空格進行分割, 拿到全部的值 if (values.length > 1) { // 若是值有多個 words = values; // 將全部的值做爲words繼續遍歷 continue; } firstWord = firstWord.replace(/;/, ""); // 去除值中包含的分號 tokens.push({ // 將值或者變量引用加入到tokens中 type: /^\$/.test(firstWord) ? "variableRef" : "value", value: firstWord, indent: 0 }); }
通過一層一層遍歷,源碼中的全部詞都被解析成了token而且放到了tokens數組中,完整代碼以下:對象
/* * 將Sass源碼傳入進行詞法分析生成tokens數組 */ function tokenize(sassSourceCode) { return sassSourceCode.trim().split(/\n/).reduce((tokens, line) => { const spaces = line.match(/^\s+/) || [""]; // 匹配空格開頭的行 const indent = spaces[0].length; // 拿到每行的縮進空格數量 const input = line.trim(); // 去除首尾空格 let words = input.split(/:/); // 用冒號進行分割,拿到每一行中的全部詞 let firstWord = words.shift(); // 取出並刪除每行的第一個詞 const selectorReg = /([0-9a-zA-Z-.#\[\]]+)\s+\{$/; if (words.length === 0) { // 若是取出並刪除第一個詞後,words數組長度變爲爲0,說明該行只有一個詞,那麼這個詞就是選擇器 const result = firstWord.match(selectorReg); if (result) { tokens.push({ // 將選擇器放到tokens中 type: "selector", value: result[1], indent }); } } else { // 變量定義、變量引用、屬性、值 let type = ""; if (/^\$/.test(firstWord)) { // 若是每行的第一個詞是以$開頭的,那麼這個詞就是一個變量定義 type = "variableDef"; // 那麼type就是變量定義,即variableDef } else { // 若是每行的第一個次是非美圓符開頭,那麼就是屬性 type = "property"; } tokens.push({ // 將變量定義或者屬性放到tokens中 type, value: firstWord, indent }); // 繼續取出words中剩餘的詞進行分析,剩下的詞多是值或者是變量引用兩種類型 while (firstWord = words.shift()) { firstWord = firstWord.trim(); // 去除詞的首尾空格 const values = firstWord.split(/\s/); // 有些詞(1px solid red)可能包含多個值,因此須要用空格進行分割, 拿到全部的值 if (values.length > 1) { // 若是值有多個 words = values; // 將全部的值做爲words繼續遍歷 continue; } firstWord = firstWord.replace(/;/, ""); // 去除值中包含的分號 tokens.push({ // 將值或者變量引用加入到tokens中 type: /^\$/.test(firstWord) ? "variableRef" : "value", value: firstWord, indent: 0 }); } } return tokens; }, []); }
用上面的源碼測試一下詞法分析的結果以下:
[ { type: 'variableDef', value: '$redColor', indent: 0 }, { type: 'value', value: 'red', indent: 0 }, { type: 'variableDef', value: '$yellowBg', indent: 0 }, { type: 'value', value: 'yellow', indent: 0 }, { type: 'selector', value: 'nav', indent: 0 }, { type: 'property', value: 'height', indent: 4 }, { type: 'value', value: '100px', indent: 0 }, { type: 'property', value: 'border', indent: 4 }, { type: 'value', value: '1px', indent: 0 }, { type: 'value', value: 'solid', indent: 0 }, { type: 'variableRef', value: '$redColor', indent: 0 }, { type: 'selector', value: '#content', indent: 0 }, { type: 'property', value: 'height', indent: 4 }, { type: 'value', value: '300px', indent: 0 }, { type: 'selector', value: 'p', indent: 4 }, { type: 'property', value: 'margin', indent: 8 }, { type: 'value', value: '10px', indent: 0 }, { type: 'selector', value: '.selected', indent: 8 }, { type: 'property', value: 'backgournd', indent: 12 }, { type: 'variableRef', value: '$yellowBg', indent: 0 } ]
② 語法分析
語法分析就是對tokens進行遍歷,將其解析成一個樹形結構。整個樹有一個根節點,根節點下有children子節點數組,只有選擇器類型才能成爲一個節點,而且每個節點下有一個rules屬性用於存放當前節點的樣式規則,根節點以下:
const ast = { // 定義一個抽象語法樹AST對象,一開始只有根節點 type: "root", // 根節點 value: "root", children: [], rules: [], indent: -1 };
每一條規則也是一個對象,結構以下:
// 樣式規則 { property: "border", value: ["1px", "solid", "red"], indent: 8 }
function parse(tokens) { const ast = { // 定義一個抽象語法樹AST對象 type: "root", // 根節點 value: "root", children: [], rules: [], indent: -1 }; const path = [ast]; // 將抽象語法樹對象放到數組中,即當前解析路徑,最後一個元素爲父元素 let parentNode = ast; // 將當前根節點做爲父節點 // 遍歷全部的token while (token = tokens.shift()) { } return ast; }
const variableDict = {}; // 保存定義的變量字典 while (token = tokens.shift()) { if (token.type === "variableDef") { // 若是這個token是變量定義 if (tokens[0] && tokens[0].type === "value") { // 而且若是其下一個token的類型是值定義,那麼這兩個token就是變量的定義 const variableValueToken = tokens.shift(); // 取出包含變量值的token variableDict[token.value] = variableValueToken.value; // 將變量名和遍歷值放到vDict對象中 } continue; } }
if (token.type === "selector") { // 若是是選擇器 const selectorNode = { // 建立一個選擇器節點,而後填充children和rules便可 type: "selector", value: token.value, indent: token.indent, rules: [], children: [] } if (selectorNode.indent > parentNode.indent) { // 當前節點的縮進大於其父節點的縮進,說明當前選擇器節點是父節點的子節點 path.push(selectorNode); // 將當前選擇器節點加入到path中,路徑變長了,當前選擇器節點做爲父節點 parentNode.children.push(selectorNode); // 將當前選擇器對象添加到父節點的children數組中 parentNode = selectorNode; // 當前選擇器節點做爲父節點 } else { // 縮進比其父節點縮進小,說明是非其子節點,多是出現了同級的節點 parentNode = path.pop(); // 移除當前路徑的最後一個節點 while (token.indent <= parentNode.indent) { // 同級節點 parentNode = path.pop(); // 拿到其父節點的父節點 } // 找到父節點後,由於父節點已經從path中移除,因此還須要將父節點再次添加到path中 path.push(parentNode, selectorNode); parentNode.children.push(selectorNode); // 找到父節點後,將當前選擇器節點添加到父節點children中 parentNode = selectorNode; // 當前選擇器節點做爲父節點 } }
if (token.type === "property") { // 若是是屬性節點 if (token.indent > parentNode.indent) { // 若是該屬性的縮進大於父節點的縮進,說明是父節點選擇器的樣式 parentNode.rules.push({ // 將樣式添加到rules數組中 {property: "border", value:[]} property: token.value, value: [], indent: token.indent }); } else { // 非當前父節點選擇器的樣式 parentNode = path.pop(); // 取出並移除最後一個選擇器節點,拿到當前父節點 while (token.indent <= parentNode.indent) { // 與當前父節點的縮進比較,若是等於,說明與當前父節點同級,若是小於,則說明比當前父節點更上層 parentNode = path.pop(); // 比當前父節點層次相等或更高,取出當前父節點的父節點,再次循環判其父節點,直到比父節點的縮進大爲止 } // 拿到了其父節點 parentNode.rules.push({ // 將該樣式添加到其父選擇器節點中 property: token.value, value: [], indent: token.indent }); path.push(parentNode); // 因爲父節點已從path中移除,須要再次將父選擇器添加到path中 } continue; }
if (token.type === "value") { // 若是是值節點 // 拿到上一個選擇器節點的rules中的最後一個rule的value將值添加進去 parentNode.rules[parentNode.rules.length - 1].value.push(token.value); continue; } if (token.type === "variableRef") { // 若是是變量引用,從變量字典中取出值並添加到父節點樣式的value數組中 parentNode.rules[parentNode.rules.length - 1].value.push(variableDict[token.value]); continue; }
tokens通過一個個遍歷後,就按照上面的規則添加到了由根節點開始的樹結構上,完整代碼以下:
function parse(tokens) { const ast = { // 定義一個抽象語法樹AST對象 type: "root", // 根節點 value: "root", children: [], rules: [], indent: -1 }; const path = [ast]; // 將抽象語法樹對象放到數組中,即當前解析路徑,最後一個元素爲父元素 let parentNode = ast; // 將當前根節點做爲父節點 let token; const variableDict = {}; // 保存定義的變量字典 // 遍歷全部的token while (token = tokens.shift()) { if (token.type === "variableDef") { // 若是這個token是變量定義 if (tokens[0] && tokens[0].type === "value") { // 而且若是其下一個token的類型是值定義,那麼這兩個token就是變量的定義 const variableValueToken = tokens.shift(); // 取出包含變量值的token variableDict[token.value] = variableValueToken.value; // 將變量名和遍歷值放到vDict對象中 } continue; } if (token.type === "selector") { // 若是是選擇器 const selectorNode = { // 建立一個選擇器節點,而後填充children和rules便可 type: "selector", value: token.value, indent: token.indent, rules: [], children: [] } if (selectorNode.indent > parentNode.indent) { // 當前節點的縮進大於其父節點的縮進,說明當前選擇器節點是父節點的子節點 path.push(selectorNode); // 將當前選擇器節點加入到path中,路徑變長了,當前選擇器節點做爲父節點 parentNode.children.push(selectorNode); // 將當前選擇器對象添加到父節點的children數組中 parentNode = selectorNode; // 當前選擇器節點做爲父節點 } else { // 縮進比其父節點縮進小,說明是非其子節點,多是出現了同級的節點 parentNode = path.pop(); // 移除當前路徑的最後一個節點 while (token.indent <= parentNode.indent) { // 同級節點 parentNode = path.pop(); // 拿到其父節點的父節點 } // 找到父節點後,由於父節點已經從path中移除,因此還須要將父節點再次添加到path中 path.push(parentNode, selectorNode); parentNode.children.push(selectorNode); // 找到父節點後,將當前選擇器節點添加到父節點children中 parentNode = selectorNode; // 當前選擇器節點做爲父節點 } } if (token.type === "property") { // 若是是屬性節點 if (token.indent > parentNode.indent) { // 若是該屬性的縮進大於父節點的縮進,說明是父節點選擇器的樣式 parentNode.rules.push({ // 將樣式添加到rules數組中 {property: "border", value:[]} property: token.value, value: [], indent: token.indent }); } else { // 非當前父節點選擇器的樣式 parentNode = path.pop(); // 取出並移除最後一個選擇器節點,拿到當前父節點 while (token.indent <= parentNode.indent) { // 與當前父節點的縮進比較,若是等於,說明與當前父節點同級,若是小於,則說明比當前父節點更上層 parentNode = path.pop(); // 比當前父節點層次相等或更高,取出當前父節點的父節點,再次循環判其父節點,直到比父節點的縮進大爲止 } // 拿到了其父節點 parentNode.rules.push({ // 將該樣式添加到其父選擇器節點中 property: token.value, value: [], indent: token.indent }); path.push(parentNode); // 因爲父節點已從path中移除,須要再次將父選擇器添加到path中 } continue; } if (token.type === "value") { // 若是是值節點 // 拿到上一個選擇器節點的rules中的最後一個rule的value將值添加進去 parentNode.rules[parentNode.rules.length - 1].value.push(token.value); continue; } if (token.type === "variableRef") { // 若是是變量引用,從變量字典中取出值並添加到父節點樣式的value數組中 parentNode.rules[parentNode.rules.length - 1].value.push(variableDict[token.value]); continue; } } return ast; }
對上一步生成的tokens解析後的結果以下:
{ "type": "root", "value": "root", "children": [{ "type": "selector", "value": "nav", "indent": 0, "rules": [{ "property": "height", "value": ["100px"], "indent": 4 }, { "property": "border", "value": ["1px", "solid", "red"], "indent": 4 }], "children": [] }, { "type": "selector", "value": "#content", "indent": 0, "rules": [{ "property": "height", "value": ["300px"], "indent": 4 }], "children": [{ "type": "selector", "value": "p", "indent": 4, "rules": [{ "property": "margin", "value": ["10px"], "indent": 8 }], "children": [{ "type": "selector", "value": ".selected", "indent": 8, "rules": [{ "property": "backgournd", "value": ["yellow"], "indent": 12 }], "children": [] }] }] }], "rules": [], "indent": -1 }
③ 轉換
所謂轉換就是對抽象語法樹進行處理,將樹結構對象轉換成咱們最終須要的數據對象,根據上面Sass編譯後輸出的源碼,能夠發現咱們最終須要生成每一個選擇器下的樣式,而且這個選擇器是呈鏈式結構的,因此咱們須要遍歷抽象語法樹,找到每一個選擇器及其樣式,並記錄當前選擇器的父鏈,從新生成一個對象,以下:
// 根據這個對象咱們就能夠輸出一條樣式 #content p {margin: 10px} { selector: "#content p", // 鏈式結構的選擇器 rules:[{"property":"margin","value":"10px","indent":8}], // 鏈式選擇器最右邊選擇器的樣式,每條樣式包含屬性名和屬性值,以及該樣式的縮進值 indent: 4 // 鏈式選擇器最右邊選擇器的縮進值 }
咱們只須要傳入上面生成的抽象語法樹即根節點,而後進行遞歸遍歷其子節點,若是節點的type類型爲selector,咱們就須要進行處理,拿到當前選擇器下的全部樣式組成的rules數組和選擇器鏈一塊兒生成上面結構的對象做爲一條樣式並放到styles數組中便可。
function transform(ast) { const styles = []; // 存放要輸出的每一條樣式 function traverse(node, styles, selectorChain) { if (node.type === "selector") { // 若是是選擇器節點 selectorChain = [...selectorChain, node.value]; // 解析選擇器層級關係,拿到選擇器鏈 if (node.rules.length > 0) { styles.push({ selector: selectorChain.join(" "), rules: node.rules.reduce((rules, rule) => { // 遍歷其rules, 拿到當前選擇器下的全部樣式 rules.push({ // 拿到該樣式規則的屬性和屬性值並放到數組中 property: rule.property, value: rule.value.join(" "), indent: rule.indent }); return rules; }, []), indent: node.indent }); } } // 遍歷根節點的children數組 for (let i = 0; i < node.children.length; i++) { traverse(node.children[i], styles, selectorChain); } } traverse(ast, styles, []); return styles; }
用上面的抽象語法樹轉換後生成的styles數組以下:
[{ "selector": "nav", "rules": [{ "property": "height", "value": "100px", "indent": 4 }, { "property": "border", "value": "1px solid red", "indent": 4 }], "indent": 0 }, { "selector": "#content", "rules": [{ "property": "height", "value": "300px", "indent": 4 }], "indent": 0 }, { "selector": "#content p", "rules": [{ "property": "margin", "value": "10px", "indent": 8 }], "indent": 4 }, { "selector": "#content p .selected", "rules": [{ "property": "backgournd", "value": "yellow", "indent": 12 }], "indent": 8 }]
④ 代碼生成
上面通過轉換後仍然是對象的形式,因此咱們須要遍歷每一條樣式,對其rules數組中的每個rule的屬性和值用冒號拼接起來,而後將rules數組中的全部rule用換行符拼接起來生成樣式規則字符串,而後與選擇器一塊兒拼接成一條字符串形式的樣式便可。
function generate(styles) { return styles.map(style => { // 遍歷每一條樣式 const rules = style.rules.reduce((rules, rule) => { // 將當前樣式的全部rules合併起來 return rules += `\n${" ".repeat(rule.indent)}${rule.property}:${rule.value};`; }, ""); return `${" ".repeat(style.indent)}${style.selector} {${rules}}`; }).join("\n"); }