sugar-high 0.0.3 → 0.0.4
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 +23 -0
- package/lib/index.mjs +314 -0
- package/package.json +5 -6
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Sugar High
|
|
2
|
+
> Super lightweight JSX syntax highlighter
|
|
3
|
+
|
|
4
|
+
### Install
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm install --save sugar-high
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
### Usage
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { highlight } from 'sugar-high'
|
|
14
|
+
|
|
15
|
+
const codeHTML = highlight(code)
|
|
16
|
+
|
|
17
|
+
document.querySelector('pre > code').innerHTML = codeHTML
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### LICENSE
|
|
21
|
+
|
|
22
|
+
MIT
|
|
23
|
+
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
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
|
+
*
|
|
78
|
+
* 0 - comment
|
|
79
|
+
* 1 - keyword
|
|
80
|
+
* 2 - break
|
|
81
|
+
* 3 - string
|
|
82
|
+
* 4 - space
|
|
83
|
+
* 5 - sign
|
|
84
|
+
* 6 - identifier
|
|
85
|
+
*
|
|
86
|
+
*/
|
|
87
|
+
const [
|
|
88
|
+
T_COMMENT,
|
|
89
|
+
T_KEYWORD,
|
|
90
|
+
T_BREAK,
|
|
91
|
+
T_STRING,
|
|
92
|
+
T_SPACE,
|
|
93
|
+
T_SIGN,
|
|
94
|
+
T_IDENTIFIER,
|
|
95
|
+
T_CLS_NUMBER,
|
|
96
|
+
] = Array(8).fill().map((_, i) => i)
|
|
97
|
+
|
|
98
|
+
function isSpaces(str) {
|
|
99
|
+
return /^[^\S\r\n]+$/g.test(str)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function encode(str) {
|
|
103
|
+
return str
|
|
104
|
+
.replace(/&/g, '&')
|
|
105
|
+
.replace(/</g, '<')
|
|
106
|
+
.replace(/>/g, '>')
|
|
107
|
+
.replace(/"/g, '"')
|
|
108
|
+
.replace(/'/g, ''')
|
|
109
|
+
// matches space but not new line
|
|
110
|
+
.replace(/[^\S\r\n]/g, ' ')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isIdentifierChar(chr) {
|
|
114
|
+
return /[a-zA-Z0-9_$]/.test(chr)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isStringQuotation(chr) {
|
|
118
|
+
return chr === '"' || chr === "'" || chr === '`'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isCommentStart(str) {
|
|
122
|
+
str = str.slice(0, 2)
|
|
123
|
+
return str === '//' || str === '/*'
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isRegexStart(str) {
|
|
127
|
+
return str[0] === '/' && !isCommentStart(str[0] + str[1])
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {string} code
|
|
132
|
+
* @return {Array<[number, string]>}
|
|
133
|
+
*/
|
|
134
|
+
export function tokenize(code) {
|
|
135
|
+
let current = ''
|
|
136
|
+
let type = -1
|
|
137
|
+
/** @type {Array<[number, string]>} */
|
|
138
|
+
const tokens = []
|
|
139
|
+
// string.type = 0 for string or string template
|
|
140
|
+
// string.type = 1 for regex
|
|
141
|
+
const string = { entered: false, type: 0 }
|
|
142
|
+
|
|
143
|
+
// comment.type = 0 for single line comments
|
|
144
|
+
// comment.type = 1 for multi-line comments
|
|
145
|
+
const comment = { entered: false, type: 0 }
|
|
146
|
+
|
|
147
|
+
// jsx.tag for entering open or closed tag
|
|
148
|
+
// jsx.child for entering children
|
|
149
|
+
// jsx.expr for entering {expression}
|
|
150
|
+
/** @type {{ tag: boolean; child: boolean; expr: boolean }} */
|
|
151
|
+
const jsx = { tag: false, child: false, expr: false }
|
|
152
|
+
|
|
153
|
+
function classify(token) {
|
|
154
|
+
if (isCommentStart(token[0] + token[1])) {
|
|
155
|
+
return T_COMMENT
|
|
156
|
+
} else if (keywords.has(token)) {
|
|
157
|
+
return T_KEYWORD
|
|
158
|
+
} else if (token === '\n') {
|
|
159
|
+
return T_BREAK
|
|
160
|
+
} else if (
|
|
161
|
+
(
|
|
162
|
+
// is quoted string
|
|
163
|
+
(isStringQuotation(token[0]) && !isStringQuotation(token[1])) ||
|
|
164
|
+
// is regex
|
|
165
|
+
(!jsx.tag && isRegexStart(token[0] + token[1]) && token[token.length - 1] === '/')
|
|
166
|
+
)
|
|
167
|
+
) {
|
|
168
|
+
return T_STRING
|
|
169
|
+
} else if (token === ' ') {
|
|
170
|
+
return T_SPACE
|
|
171
|
+
} else if (signs.has(token[0])) {
|
|
172
|
+
return T_SIGN
|
|
173
|
+
} else if (
|
|
174
|
+
token[0] === token[0].toUpperCase() ||
|
|
175
|
+
token === 'null'
|
|
176
|
+
) {
|
|
177
|
+
return T_CLS_NUMBER
|
|
178
|
+
} else {
|
|
179
|
+
return T_IDENTIFIER
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const append = () => {
|
|
184
|
+
if (current) {
|
|
185
|
+
type = classify(current)
|
|
186
|
+
tokens.push([type, current])
|
|
187
|
+
}
|
|
188
|
+
current = ''
|
|
189
|
+
}
|
|
190
|
+
for (let i = 0; i < code.length; i++) {
|
|
191
|
+
const curr = code[i]
|
|
192
|
+
const prev = code[i - 1]
|
|
193
|
+
const next = code[i + 1]
|
|
194
|
+
const c_n = curr + next // current and next
|
|
195
|
+
const p_c = prev + curr // previous and current
|
|
196
|
+
const isJsxLiterals = jsx.child && !jsx.expr
|
|
197
|
+
|
|
198
|
+
if (jsx.tag) {
|
|
199
|
+
const isOpenElementEnd = curr === '>'
|
|
200
|
+
const isCloseElementEnd = p_c === '/>'
|
|
201
|
+
jsx.child = !isCloseElementEnd && !isCloseElementEnd
|
|
202
|
+
jsx.tag = !(isOpenElementEnd || isCloseElementEnd)
|
|
203
|
+
}
|
|
204
|
+
// if it's not in a jsx tag declaration or a string, close child if next is jsx close tag
|
|
205
|
+
if (!jsx.tag && !string.entered && (curr === '<' && isIdentifierChar(next) || c_n === '</')) {
|
|
206
|
+
jsx.tag = true
|
|
207
|
+
jsx.child = false
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (jsx.child && curr === '{') {
|
|
211
|
+
jsx.expr = true
|
|
212
|
+
}
|
|
213
|
+
if (jsx.child && jsx.expr && curr === '}') {
|
|
214
|
+
jsx.expr = false
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
!string.entered &&
|
|
219
|
+
(isStringQuotation(curr) || !jsx.tag && isRegexStart(c_n))
|
|
220
|
+
) {
|
|
221
|
+
string.entered = true
|
|
222
|
+
string.type = isStringQuotation(curr) ? 0 : 1
|
|
223
|
+
append()
|
|
224
|
+
current = curr
|
|
225
|
+
} else if (string.entered) {
|
|
226
|
+
current += curr
|
|
227
|
+
if (string.type === 0 && isStringQuotation(curr)) {
|
|
228
|
+
string.entered = false
|
|
229
|
+
append()
|
|
230
|
+
} else if (string.type === 1 && prev !== '\\' && curr === '/') {
|
|
231
|
+
string.entered = false
|
|
232
|
+
append()
|
|
233
|
+
}
|
|
234
|
+
} else if (
|
|
235
|
+
!comment.entered &&
|
|
236
|
+
isCommentStart(c_n)
|
|
237
|
+
) {
|
|
238
|
+
comment.type = next === '/' ? 0 : 1
|
|
239
|
+
comment.entered = true
|
|
240
|
+
append()
|
|
241
|
+
current = c_n
|
|
242
|
+
i++
|
|
243
|
+
} else if (comment.entered) {
|
|
244
|
+
current += curr
|
|
245
|
+
if (comment.type === 0 && next === '\n') {
|
|
246
|
+
comment.entered = false
|
|
247
|
+
append()
|
|
248
|
+
} else if (comment.type === 1 && (c_n === '*/')) {
|
|
249
|
+
comment.entered = false
|
|
250
|
+
current += '/'
|
|
251
|
+
append()
|
|
252
|
+
i++
|
|
253
|
+
}
|
|
254
|
+
} else if (curr === ' ' || curr === '\n') {
|
|
255
|
+
if (
|
|
256
|
+
curr === ' ' &&
|
|
257
|
+
(
|
|
258
|
+
(isSpaces(current) || !current) ||
|
|
259
|
+
isJsxLiterals
|
|
260
|
+
)
|
|
261
|
+
) {
|
|
262
|
+
current += curr
|
|
263
|
+
} else {
|
|
264
|
+
append()
|
|
265
|
+
current = curr
|
|
266
|
+
append()
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
if (
|
|
270
|
+
(isJsxLiterals && !jsxBrackets.has(curr)) ||
|
|
271
|
+
isIdentifierChar(curr) === isIdentifierChar(current[current.length - 1]) &&
|
|
272
|
+
!signs.has(curr)
|
|
273
|
+
) {
|
|
274
|
+
current += curr
|
|
275
|
+
} else {
|
|
276
|
+
append()
|
|
277
|
+
current = curr
|
|
278
|
+
if (c_n === '</') {
|
|
279
|
+
current = c_n
|
|
280
|
+
append()
|
|
281
|
+
i++
|
|
282
|
+
}
|
|
283
|
+
else if (jsxBrackets.has(curr)) append()
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
append()
|
|
289
|
+
|
|
290
|
+
return tokens
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @param {Array<[number, string]>} tokens
|
|
295
|
+
* @return {Array<string>}
|
|
296
|
+
*/
|
|
297
|
+
function generate(tokens) {
|
|
298
|
+
const output = []
|
|
299
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
300
|
+
const [type, token] = tokens[i]
|
|
301
|
+
output.push(
|
|
302
|
+
type === T_BREAK
|
|
303
|
+
? '<br>'
|
|
304
|
+
: `<span class="sh__${type}">${encode(token)}</span>`
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
return output
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function highlight(code) {
|
|
311
|
+
const tokens = tokenize(code)
|
|
312
|
+
const output = generate(tokens).join('')
|
|
313
|
+
return output
|
|
314
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sugar-high",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
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": {
|