sugar-high 0.0.2 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/lib/index.mjs +327 -0
- package/package.json +5 -6
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Sugar High
|
|
2
|
+
### Introduction
|
|
3
|
+
|
|
4
|
+
Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped
|
|
5
|
+
|
|
6
|
+
> ⚠️ Still in experiment! Use it in production with caution!
|
|
7
|
+
|
|
8
|
+
### Usage
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install --save sugar-high
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { highlight } from 'sugar-high'
|
|
16
|
+
|
|
17
|
+
const codeHTML = highlight(code)
|
|
18
|
+
|
|
19
|
+
document.querySelector('pre > code').innerHTML = codeHTML
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Highlight with CSS
|
|
23
|
+
|
|
24
|
+
Then make your own theme with customized colors by token type and put in global CSS. The corresponding class names star with `sh__` prefix.
|
|
25
|
+
|
|
26
|
+
```css
|
|
27
|
+
/**
|
|
28
|
+
* Types that sugar-high have:
|
|
29
|
+
*
|
|
30
|
+
* identifier
|
|
31
|
+
* keyword
|
|
32
|
+
* string
|
|
33
|
+
* Class, number and null
|
|
34
|
+
* sign
|
|
35
|
+
* comment
|
|
36
|
+
*
|
|
37
|
+
*/
|
|
38
|
+
.sh__class {
|
|
39
|
+
color: #2d5e9d;
|
|
40
|
+
}
|
|
41
|
+
.sh__identifier {
|
|
42
|
+
color: #2d333b;
|
|
43
|
+
}
|
|
44
|
+
.sh__sign {
|
|
45
|
+
color: #8996a3;
|
|
46
|
+
}
|
|
47
|
+
.sh__string {
|
|
48
|
+
color: #00a99a;
|
|
49
|
+
}
|
|
50
|
+
.sh__keyword {
|
|
51
|
+
color: #f47067;
|
|
52
|
+
}
|
|
53
|
+
.sh__comment {
|
|
54
|
+
color: #a19595;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### LICENSE
|
|
60
|
+
|
|
61
|
+
MIT
|
|
62
|
+
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
const jsxBrackets = new Set(['<', '>', '{', '}', '[', ']'])
|
|
4
|
+
const keywords = new Set([
|
|
5
|
+
'for',
|
|
6
|
+
'while',
|
|
7
|
+
'if',
|
|
8
|
+
'else',
|
|
9
|
+
'return',
|
|
10
|
+
'function',
|
|
11
|
+
'var',
|
|
12
|
+
'let',
|
|
13
|
+
'const',
|
|
14
|
+
'true',
|
|
15
|
+
'false',
|
|
16
|
+
'undefined',
|
|
17
|
+
'this',
|
|
18
|
+
'new',
|
|
19
|
+
'delete',
|
|
20
|
+
'typeof',
|
|
21
|
+
'in',
|
|
22
|
+
'instanceof',
|
|
23
|
+
'void',
|
|
24
|
+
'break',
|
|
25
|
+
'continue',
|
|
26
|
+
'switch',
|
|
27
|
+
'case',
|
|
28
|
+
'default',
|
|
29
|
+
'throw',
|
|
30
|
+
'try',
|
|
31
|
+
'catch',
|
|
32
|
+
'finally',
|
|
33
|
+
'debugger',
|
|
34
|
+
'with',
|
|
35
|
+
'yield',
|
|
36
|
+
'async',
|
|
37
|
+
'await',
|
|
38
|
+
'class',
|
|
39
|
+
'extends',
|
|
40
|
+
'super',
|
|
41
|
+
'import',
|
|
42
|
+
'export',
|
|
43
|
+
'from',
|
|
44
|
+
'static',
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
const signs = new Set([
|
|
48
|
+
'+',
|
|
49
|
+
'-',
|
|
50
|
+
'*',
|
|
51
|
+
'/',
|
|
52
|
+
'%',
|
|
53
|
+
'=',
|
|
54
|
+
'!',
|
|
55
|
+
...jsxBrackets,
|
|
56
|
+
'&',
|
|
57
|
+
'|',
|
|
58
|
+
'^',
|
|
59
|
+
'~',
|
|
60
|
+
'!',
|
|
61
|
+
'?',
|
|
62
|
+
':',
|
|
63
|
+
'.',
|
|
64
|
+
',',
|
|
65
|
+
';',
|
|
66
|
+
`'`,
|
|
67
|
+
'"',
|
|
68
|
+
'.',
|
|
69
|
+
'(',
|
|
70
|
+
')',
|
|
71
|
+
'[',
|
|
72
|
+
']',
|
|
73
|
+
'#',
|
|
74
|
+
'\\',
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
const types = [
|
|
78
|
+
'identifier',
|
|
79
|
+
'keyword',
|
|
80
|
+
'string',
|
|
81
|
+
'class',
|
|
82
|
+
'sign',
|
|
83
|
+
'comment',
|
|
84
|
+
'break',
|
|
85
|
+
'space',
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
*
|
|
90
|
+
* 0 - identifier
|
|
91
|
+
* 1 - keyword
|
|
92
|
+
* 2 - string
|
|
93
|
+
* 3 - Class, number and null
|
|
94
|
+
* 4 - sign
|
|
95
|
+
* 5 - comment
|
|
96
|
+
* 6 - break
|
|
97
|
+
* 7 - space
|
|
98
|
+
*
|
|
99
|
+
*/
|
|
100
|
+
const [
|
|
101
|
+
T_IDENTIFIER,
|
|
102
|
+
T_KEYWORD,
|
|
103
|
+
T_STRING,
|
|
104
|
+
T_CLS_NUMBER,
|
|
105
|
+
T_SIGN,
|
|
106
|
+
T_COMMENT,
|
|
107
|
+
T_BREAK,
|
|
108
|
+
T_SPACE,
|
|
109
|
+
] = types.map((_, i) => i)
|
|
110
|
+
|
|
111
|
+
function isSpaces(str) {
|
|
112
|
+
return /^[^\S\r\n]+$/g.test(str)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function encode(str) {
|
|
116
|
+
return str
|
|
117
|
+
.replace(/&/g, '&')
|
|
118
|
+
.replace(/</g, '<')
|
|
119
|
+
.replace(/>/g, '>')
|
|
120
|
+
.replace(/"/g, '"')
|
|
121
|
+
.replace(/'/g, ''')
|
|
122
|
+
// matches space but not new line
|
|
123
|
+
.replace(/[^\S\r\n]/g, ' ')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isIdentifierChar(chr) {
|
|
127
|
+
return /[a-zA-Z0-9_$]/.test(chr)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isStringQuotation(chr) {
|
|
131
|
+
return chr === '"' || chr === "'" || chr === '`'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isCommentStart(str) {
|
|
135
|
+
str = str.slice(0, 2)
|
|
136
|
+
return str === '//' || str === '/*'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isRegexStart(str) {
|
|
140
|
+
return str[0] === '/' && !isCommentStart(str[0] + str[1])
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} code
|
|
145
|
+
* @return {Array<[number, string]>}
|
|
146
|
+
*/
|
|
147
|
+
export function tokenize(code) {
|
|
148
|
+
let current = ''
|
|
149
|
+
let type = -1
|
|
150
|
+
/** @type {Array<[number, string]>} */
|
|
151
|
+
const tokens = []
|
|
152
|
+
// string.type = 0 for string or string template
|
|
153
|
+
// string.type = 1 for regex
|
|
154
|
+
const string = { entered: false, type: 0 }
|
|
155
|
+
|
|
156
|
+
// comment.type = 0 for single line comments
|
|
157
|
+
// comment.type = 1 for multi-line comments
|
|
158
|
+
const comment = { entered: false, type: 0 }
|
|
159
|
+
|
|
160
|
+
// jsx.tag for entering open or closed tag
|
|
161
|
+
// jsx.child for entering children
|
|
162
|
+
// jsx.expr for entering {expression}
|
|
163
|
+
/** @type {{ tag: boolean; child: boolean; expr: boolean }} */
|
|
164
|
+
const jsx = { tag: false, child: false, expr: false }
|
|
165
|
+
|
|
166
|
+
function classify(token) {
|
|
167
|
+
if (isCommentStart(token[0] + token[1])) {
|
|
168
|
+
return T_COMMENT
|
|
169
|
+
} else if (keywords.has(token)) {
|
|
170
|
+
return T_KEYWORD
|
|
171
|
+
} else if (token === '\n') {
|
|
172
|
+
return T_BREAK
|
|
173
|
+
} else if (
|
|
174
|
+
(
|
|
175
|
+
// is quoted string
|
|
176
|
+
(isStringQuotation(token[0]) && !isStringQuotation(token[1])) ||
|
|
177
|
+
// is regex
|
|
178
|
+
(!jsx.tag && isRegexStart(token[0] + token[1]) && token[token.length - 1] === '/')
|
|
179
|
+
)
|
|
180
|
+
) {
|
|
181
|
+
return T_STRING
|
|
182
|
+
} else if (token === ' ') {
|
|
183
|
+
return T_SPACE
|
|
184
|
+
} else if (signs.has(token[0])) {
|
|
185
|
+
return T_SIGN
|
|
186
|
+
} else if (
|
|
187
|
+
token[0] === token[0].toUpperCase() ||
|
|
188
|
+
token === 'null'
|
|
189
|
+
) {
|
|
190
|
+
return T_CLS_NUMBER
|
|
191
|
+
} else {
|
|
192
|
+
return T_IDENTIFIER
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const append = () => {
|
|
197
|
+
if (current) {
|
|
198
|
+
type = classify(current)
|
|
199
|
+
tokens.push([type, current])
|
|
200
|
+
}
|
|
201
|
+
current = ''
|
|
202
|
+
}
|
|
203
|
+
for (let i = 0; i < code.length; i++) {
|
|
204
|
+
const curr = code[i]
|
|
205
|
+
const prev = code[i - 1]
|
|
206
|
+
const next = code[i + 1]
|
|
207
|
+
const c_n = curr + next // current and next
|
|
208
|
+
const p_c = prev + curr // previous and current
|
|
209
|
+
const isJsxLiterals = jsx.child && !jsx.expr
|
|
210
|
+
|
|
211
|
+
if (jsx.tag) {
|
|
212
|
+
const isOpenElementEnd = curr === '>'
|
|
213
|
+
const isCloseElementEnd = p_c === '/>'
|
|
214
|
+
jsx.child = !isCloseElementEnd && !isCloseElementEnd
|
|
215
|
+
jsx.tag = !(isOpenElementEnd || isCloseElementEnd)
|
|
216
|
+
}
|
|
217
|
+
// if it's not in a jsx tag declaration or a string, close child if next is jsx close tag
|
|
218
|
+
if (!jsx.tag && !string.entered && (curr === '<' && isIdentifierChar(next) || c_n === '</')) {
|
|
219
|
+
jsx.tag = true
|
|
220
|
+
jsx.child = false
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (jsx.child && curr === '{') {
|
|
224
|
+
jsx.expr = true
|
|
225
|
+
}
|
|
226
|
+
if (jsx.child && jsx.expr && curr === '}') {
|
|
227
|
+
jsx.expr = false
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
!string.entered &&
|
|
232
|
+
(isStringQuotation(curr) || !jsx.tag && isRegexStart(c_n))
|
|
233
|
+
) {
|
|
234
|
+
string.entered = true
|
|
235
|
+
string.type = isStringQuotation(curr) ? 0 : 1
|
|
236
|
+
append()
|
|
237
|
+
current = curr
|
|
238
|
+
} else if (string.entered) {
|
|
239
|
+
current += curr
|
|
240
|
+
if (string.type === 0 && isStringQuotation(curr)) {
|
|
241
|
+
string.entered = false
|
|
242
|
+
append()
|
|
243
|
+
} else if (string.type === 1 && prev !== '\\' && curr === '/') {
|
|
244
|
+
string.entered = false
|
|
245
|
+
append()
|
|
246
|
+
}
|
|
247
|
+
} else if (
|
|
248
|
+
!comment.entered &&
|
|
249
|
+
isCommentStart(c_n)
|
|
250
|
+
) {
|
|
251
|
+
comment.type = next === '/' ? 0 : 1
|
|
252
|
+
comment.entered = true
|
|
253
|
+
append()
|
|
254
|
+
current = c_n
|
|
255
|
+
i++
|
|
256
|
+
} else if (comment.entered) {
|
|
257
|
+
current += curr
|
|
258
|
+
if (comment.type === 0 && next === '\n') {
|
|
259
|
+
comment.entered = false
|
|
260
|
+
append()
|
|
261
|
+
} else if (comment.type === 1 && (c_n === '*/')) {
|
|
262
|
+
comment.entered = false
|
|
263
|
+
current += '/'
|
|
264
|
+
append()
|
|
265
|
+
i++
|
|
266
|
+
}
|
|
267
|
+
} else if (curr === ' ' || curr === '\n') {
|
|
268
|
+
if (
|
|
269
|
+
curr === ' ' &&
|
|
270
|
+
(
|
|
271
|
+
(isSpaces(current) || !current) ||
|
|
272
|
+
isJsxLiterals
|
|
273
|
+
)
|
|
274
|
+
) {
|
|
275
|
+
current += curr
|
|
276
|
+
} else {
|
|
277
|
+
append()
|
|
278
|
+
current = curr
|
|
279
|
+
append()
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
if (
|
|
283
|
+
(isJsxLiterals && !jsxBrackets.has(curr)) ||
|
|
284
|
+
isIdentifierChar(curr) === isIdentifierChar(current[current.length - 1]) &&
|
|
285
|
+
!signs.has(curr)
|
|
286
|
+
) {
|
|
287
|
+
current += curr
|
|
288
|
+
} else {
|
|
289
|
+
append()
|
|
290
|
+
current = curr
|
|
291
|
+
if (c_n === '</') {
|
|
292
|
+
current = c_n
|
|
293
|
+
append()
|
|
294
|
+
i++
|
|
295
|
+
}
|
|
296
|
+
else if (jsxBrackets.has(curr)) append()
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
append()
|
|
302
|
+
|
|
303
|
+
return tokens
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @param {Array<[number, string]>} tokens
|
|
308
|
+
* @return {Array<string>}
|
|
309
|
+
*/
|
|
310
|
+
function generate(tokens) {
|
|
311
|
+
const output = []
|
|
312
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
313
|
+
const [type, token] = tokens[i]
|
|
314
|
+
output.push(
|
|
315
|
+
type === T_BREAK
|
|
316
|
+
? '<br>'
|
|
317
|
+
: `<span class="sh__${types[type]}">${encode(token)}</span>`
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
return output
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function highlight(code) {
|
|
324
|
+
const tokens = tokenize(code)
|
|
325
|
+
const output = generate(tokens).join('')
|
|
326
|
+
return output
|
|
327
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sugar-high",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"exports":
|
|
6
|
-
|
|
7
|
-
},
|
|
8
|
-
"description": "Super lightweight JavaScript syntax highlighter",
|
|
5
|
+
"exports": "./lib/index.mjs",
|
|
6
|
+
"description": "Super lightweight JSX syntax highlighter",
|
|
9
7
|
"files": [
|
|
10
|
-
"
|
|
8
|
+
"lib",
|
|
9
|
+
"*.md"
|
|
11
10
|
],
|
|
12
11
|
"license": "MIT",
|
|
13
12
|
"scripts": {
|