[day-23] 我們說明了 SCSS 的規則 & 一些關鍵的字元
今天我們仿造 [Day12] - HTML 的 Tokenizer 的方法 來實作一個 SCSS 的 tokenizer
預計輸入 - app.scss
@import "_color.scss"; // 會將 _color.scss 的內容全部輸出
@import "_button.scss"; // 會將 _button.scss 的內容全部輸出
// @use "_text.scss"; // 一定要放在 @forward 之前,且只會輸出有在目標檔案中有使用到的部分,可設定 Namespace
// @forward "_space.scss"; // 只會輸出有在目標檔案中有使用到的部分
@debug "Debugging";
@warn "This is a warning";
// Nesting
nav {
ul {
margin: 0;
padding: 0;
list-style: none;
}
li { display: inline-block; }
a {
display: block;
padding: 6px 12px;
text-decoration: none;
}
// parent selector
[dir=rtl] & {
margin-left: 0;
margin-right: 10px;
}
}
// Placeholder Selectors
%toolbelt {
box-sizing: border-box;
border-top: 1px rgba(#000, .12) solid;
padding: 16px 0;
width: 100%;
&:hover { border: 2px rgba(#000, .5) solid; }
}
.action-buttons {
@extend %toolbelt;
color: #4285f4;
}
.reset-buttons {
@extend %toolbelt;
color: #cddc39;
}
預期輸出 Tokens
const tokens = [
{
"type": "keyword",
"name": "import",
"value": "\"_color.scss\""
},
{
"type": "keyword",
"name": "import",
"value": "\"_button.scss\""
}
]
// 讀取檔案內容
const fs = require('fs');
const str = fs.readFileSync('sample.html', 'utf8');
// 切分成字元陣列
const charList = str.split('');
const STATUS = {
INITIAL: 0,
IN_SINGLE_COMMENT: 1, // 開始於 // 遇到 \n 回到 initial
IN_MULTI_COMMENT: 2, // 開始於 /* 遇到 */ 回到 initial
IN_VAR_NAME: 3, // initial 開始於 $ 遇到 : 變成 IN_VAR_VALUE
IN_VAR_VALUE: 4, // 開始於 : 遇到 ; 變成 INITIAL
IN_KEYWORD: 5, // 開始於 @ 遇到 空格 變成 IN_KEYWORD_VALUE
IN_KEYWORD_VALUE: 6, // 遇到 ; 變成 INITIAL / 遇到 { 變成 IN_BODY
IN_BODY: 7, // 開始於 { 遇到 } 變成 INITIAL
}
resetCollect = () => this.collected = '';
collectChar = char => this.collected += char;
setCurrStatus = status => this.CURR_STATUS = status;
isLastChar = () => this.charList.length === 1;
addToken = token => this.tokens.push(token);
4.1 - INITIAL 的狀態變化
handle_INITIAL() {
const current = this.getCurr();
if (current === `/`) {
const next = this.getNext();
// 遇到 `//`,切換狀態成 IN_SINGLE_COMMENT
if (next === `/`) {
this.setCurrStatus(ScssTokenizer.STATUS.IN_SINGLE_COMMENT);
this.removeFirstN(2);
return;
}
// 遇到 `/*`,切換狀態成 IN_MULTI_COMMENT
if (next === `*`) {
this.setCurrStatus(ScssTokenizer.STATUS.IN_MULTI_COMMENT);
this.removeFirstN(2);
return;
}
}
if (current === `$` || current === `#`) {
this.setCurrStatus(ScssTokenizer.STATUS.IN_VAR_NAME);
this.removeCurr();
return;
}
if (current === `@`) {
this.setCurrStatus(ScssTokenizer.STATUS.IN_KEYWORD);
this.removeCurr();
return;
}
if (current === `{`) {
this.setCurrStatus(ScssTokenizer.STATUS.IN_BODY);
this.addToken({type: 'css', selector: this.collected});
this.resetCollect();
this.removeCurr();
return;
}
if (!['\r', '\n', ' '].includes(current)) this.collectChar(current);
this.removeCurr();
}
4.2 - IN_SINGLE_COMMENT 的狀態變化
handle_IN_SINGLE_COMMENT() {
const current = this.getCurr();
if (current === '\n') {
this.setCurrStatus(ScssTokenizer.STATUS.INITIAL);
this.addToken({type: 'comment', value: this.collected});
this.removeCurr();
this.resetCollect();
this.removeCurr();
return;
}
this.collectChar(current);
this.removeCurr();
}
4.3 - IN_MULTI_COMMENT 的狀態變化
handle_IN_MULTI_COMMENT() {
const current = this.getCurr();
if (current === '*' && this.lookahead(1) === '/') {
this.setCurrStatus(ScssTokenizer.STATUS.INITIAL);
this.addToken({type: 'comment', value: this.collected});
this.removeCurr();
this.resetCollect();
this.removeCurr();
return;
}
this.collectChar(current);
this.removeCurr();
}
...等
將上面的區塊做整合,就可以得到 完整程式碼 scssTokenizer.js