可自行修改或擴充 Emmet

VS Code 已經內建支援 Emmet,可當原生功能使用,但有時候 Emmet 產生的 HTML 或 CSS,並不符合需求,或者我們想擴充 Emmet 功能,這在 VS Code 有可能實現嗎 ?

Version


macOS Mojave 10.14.4
VS Code 1.33.1

Emmet !


customize000

Emmet 的 ! 會幫我們產生以上的 HTML snippet,除了包含 RWD 與支持 Edge 外,還特別設定langen,或許你會想要更精簡 HTML boilerplate,這該如何設定呢 ?

snippets.json


snippets.json

1
2
3
4
5
6
7
8
9
10
11
{
"html": {
"snippets": {
"doc": "html>(head>meta[charset=${charset}]+title{${1:Document}})+body"
}

}
,

"css": {
"snippets": {
}
}

}

VS Code 預設會在 ~/Library/Application Support/Code/User/snippets/ 建立各程式語言 user snippet,因此我們也將 Emmet 的 user snippet 建立在此目錄下。

VS Code 規定 Emmet 的 user snippet 檔名一定得是 snippets.json

第 1 行

1
2
3
4
5
6
7
8
9
10
{
"html": {
"snippets": {
}
}
,

"css": {
"snippets": {
}
}

}

snippets.jsonhtmlcss 兩部份,分別設定 HTML 與 CSS 的 user snippet。

  • 若要修改 Emmet 原本的 abbreviation,直接在 snippets 下新增同名的 abbreviation 即可,VS Code 會自動加以 override
  • 若要新增 Emmet 原本沒有的 abbreviation,一樣直接在 snippets 下新增即可

第 4 行

1
"doc": "html>(head>meta[charset=${charset}]+title{${1:Document}})+body"

新增 doc abbreviation,指定其由其他 abbreviation 組合而成,注意其中已經沒有 RWD、 Edge 與 en

Q:明明要修改 !,會什麼變成修改 doc 呢 ?

html.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
{
"a": "a[href]",
"a:blank": "a[href='http://${0}' target='_blank' rel='noopener noreferrer']",
"a:link": "a[href='http://${0}']",
"a:mail": "a[href='mailto:${0}']",
"a:tel": "a[href='tel:+${0}']",
"abbr": "abbr[title]",
"acr|acronym": "acronym[title]",
"base": "base[href]/",
"basefont": "basefont/",
"br": "br/",
"frame": "frame/",
"hr": "hr/",
"bdo": "bdo[dir]",
"bdo:r": "bdo[dir=rtl]",
"bdo:l": "bdo[dir=ltr]",
"col": "col/",
"link": "link[rel=stylesheet href]/",
"link:css": "link[href='${1:style}.css']",
"link:print": "link[href='${1:print}.css' media=print]",
"link:favicon": "link[rel='shortcut icon' type=image/x-icon href='${1:favicon.ico}']",
"link:mf|link:manifest": "link[rel='manifest' href='${1:manifest.json}']",
"link:touch": "link[rel=apple-touch-icon href='${1:favicon.png}']",
"link:rss": "link[rel=alternate type=application/rss+xml title=RSS href='${1:rss.xml}']",
"link:atom": "link[rel=alternate type=application/atom+xml title=Atom href='${1:atom.xml}']",
"link:im|link:import": "link[rel=import href='${1:component}.html']",
"meta": "meta/",
"meta:utf": "meta[http-equiv=Content-Type content='text/html;charset=UTF-8']",
"meta:vp": "meta[name=viewport content='width=${1:device-width}, initial-scale=${2:1.0}']",
"meta:compat": "meta[http-equiv=X-UA-Compatible content='${1:IE=7}']",
"meta:edge": "meta:compat[content='${1:ie=edge}']",
"meta:redirect": "meta[http-equiv=refresh content='0; url=${1:http://example.com}']",
"style": "style",
"script": "script[!src]",
"script:src": "script[src]",
"img": "img[src alt]/",
"img:s|img:srcset": "img[srcset src alt]",
"img:z|img:sizes": "img[sizes srcset src alt]",
"picture": "picture",
"src|source": "source/",
"src:sc|source:src": "source[src type]",
"src:s|source:srcset": "source[srcset]",
"src:t|source:type": "source[srcset type='${1:image/}']",
"src:z|source:sizes": "source[sizes srcset]",
"src:m|source:media": "source[media='(${1:min-width: })' srcset]",
"src:mt|source:media:type": "source:media[type='${2:image/}']",
"src:mz|source:media:sizes": "source:media[sizes srcset]",
"src:zt|source:sizes:type": "source[sizes srcset type='${1:image/}']",
"iframe": "iframe[src frameborder=0]",
"embed": "embed[src type]/",
"object": "object[data type]",
"param": "param[name value]/",
"map": "map[name]",
"area": "area[shape coords href alt]/",
"area:d": "area[shape=default]",
"area:c": "area[shape=circle]",
"area:r": "area[shape=rect]",
"area:p": "area[shape=poly]",
"form": "form[action]",
"form:get": "form[method=get]",
"form:post": "form[method=post]",
"label": "label[for]",
"input": "input[type=${1:text}]/",
"inp": "input[name=${1} id=${1}]",
"input:h|input:hidden": "input[type=hidden name]",
"input:t|input:text": "inp[type=text]",
"input:search": "inp[type=search]",
"input:email": "inp[type=email]",
"input:url": "inp[type=url]",
"input:p|input:password": "inp[type=password]",
"input:datetime": "inp[type=datetime]",
"input:date": "inp[type=date]",
"input:datetime-local": "inp[type=datetime-local]",
"input:month": "inp[type=month]",
"input:week": "inp[type=week]",
"input:time": "inp[type=time]",
"input:tel": "inp[type=tel]",
"input:number": "inp[type=number]",
"input:color": "inp[type=color]",
"input:c|input:checkbox": "inp[type=checkbox]",
"input:r|input:radio": "inp[type=radio]",
"input:range": "inp[type=range]",
"input:f|input:file": "inp[type=file]",
"input:s|input:submit": "input[type=submit value]",
"input:i|input:image": "input[type=image src alt]",
"input:b|input:button": "input[type=button value]",
"input:reset": "input:button[type=reset]",
"isindex": "isindex/",
"select": "select[name=${1} id=${1}]",
"select:d|select:disabled": "select[disabled.]",
"opt|option": "option[value]",
"textarea": "textarea[name=${1} id=${1} cols=${2:30} rows=${3:10}]",
"marquee": "marquee[behavior direction]",
"menu:c|menu:context": "menu[type=context]",
"menu:t|menu:toolbar": "menu[type=toolbar]",
"video": "video[src]",
"audio": "audio[src]",
"html:xml": "html[xmlns=http://www.w3.org/1999/xhtml]",
"keygen": "keygen/",
"command": "command/",
"btn:s|button:s|button:submit" : "button[type=submit]",
"btn:r|button:r|button:reset" : "button[type=reset]",
"btn:d|button:d|button:disabled" : "button[disabled.]",
"fst:d|fset:d|fieldset:d|fieldset:disabled" : "fieldset[disabled.]",
"bq": "blockquote",
"fig": "figure",
"figc": "figcaption",
"pic": "picture",
"ifr": "iframe",
"emb": "embed",
"obj": "object",
"cap": "caption",
"colg": "colgroup",
"fst": "fieldset",
"btn": "button",
"optg": "optgroup",
"tarea": "textarea",
"leg": "legend",
"sect": "section",
"art": "article",
"hdr": "header",
"ftr": "footer",
"adr": "address",
"dlg": "dialog",
"str": "strong",
"prog": "progress",
"mn": "main",
"tem": "template",
"fset": "fieldset",
"datag": "datagrid",
"datal": "datalist",
"kg": "keygen",
"out": "output",
"det": "details",
"cmd": "command",
"ri:d|ri:dpr": "img:s",
"ri:v|ri:viewport": "img:z",
"ri:a|ri:art": "pic>src:m+img",
"ri:t|ri:type": "pic>src:t+img",
"!!!": "{<!DOCTYPE html>}",
"doc": "html[lang=${lang}]>(head>meta[charset=${charset}]+meta:vp+meta:edge+title{${1:Document}})+body",
"!|html:5": "!!!+doc",
"c": "{<!-- ${0} -->}",
"cc:ie": "{<!--[if IE]>${0}<![endif]-->}",
"cc:noie": "{<!--[if !IE]><!-->${0}<!--<![endif]-->}"
}

在 Emmet 的官方 GitHub,可以發現 Emmet 的所有 HTML abbreviation,秘密就在 html.json 這個檔案。

140 行

1
2
3
"!!!": "{<!DOCTYPE html>}",
"doc": "html[lang=${lang}]>(head>meta[charset=${charset}]+meta:vp+meta:edge+title{${1:Document}})+body",
"!|html:5": "!!!+doc",

當我們輸入 !html:5 時,將由 !!!doc 組合而成。

doc 我們可以看到 meta:vp+meta:edge,這就是會產生 RWD 與 Edge 部分,還可看到 html[lang=${lang}],因此我們要做的就是 override 掉 doc,將 meta:vp+meta:edge[lang=${lang}] 拿掉。

Preference


customize001

Code -> Preferences

  1. 輸入 emmet
  2. Emmet: Extension Path 輸入 ~/Library/Application Support/Code/User/snippets/,這是我們剛剛建立 snippets.json 的路徑

customize002

重新啟動 VS Code,輸入 !,就會發現 HTML 已經被我們改掉了。

Conclusion


  • snippets.json 巧門一開,能做的事情就很多了,除了能修改原本 Emmet 的 abbreviation 外,還可以新增自己的 abbreviation
  • 由 Emmet 官方的 html.json 可以發現,Emmet 也是大量組合其他 abbreviation 而來,這正是運用 Function Composition 的實例

Reference


VS Code, Using custom Emmet snippets
Emmet, html.json
Emmet, css.json
CK’s Notepad, VS Code 自訂 Emmet 範本

2019-04-15