sugar-high 0.2.0 → 0.3.0
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 +16 -1
- package/lib/index.mjs +85 -38
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped
|
|
5
5
|
|
|
6
|
-

|
|
7
7
|
|
|
8
8
|
### Usage
|
|
9
9
|
|
|
@@ -58,6 +58,21 @@ Then make your own theme with customized colors by token type and put in global
|
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### Features
|
|
62
|
+
|
|
63
|
+
#### Line number
|
|
64
|
+
|
|
65
|
+
Sugar high provide `.sh_line` class name for each line. To display line number, define the `.sh_line::before` element with CSS will enable line numbers automatically.
|
|
66
|
+
|
|
67
|
+
```css
|
|
68
|
+
.sh__line::before {
|
|
69
|
+
content: attr(data-line-number);
|
|
70
|
+
margin-right: 24px;
|
|
71
|
+
text-align: right;
|
|
72
|
+
color: #a4a4a4;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
61
76
|
### LICENSE
|
|
62
77
|
|
|
63
78
|
MIT
|
package/lib/index.mjs
CHANGED
|
@@ -71,6 +71,7 @@ const signs = new Set([
|
|
|
71
71
|
'[',
|
|
72
72
|
']',
|
|
73
73
|
'#',
|
|
74
|
+
'@',
|
|
74
75
|
'\\',
|
|
75
76
|
])
|
|
76
77
|
|
|
@@ -87,7 +88,7 @@ export const types = [
|
|
|
87
88
|
]
|
|
88
89
|
|
|
89
90
|
/**
|
|
90
|
-
*
|
|
91
|
+
*
|
|
91
92
|
* 0 - identifier
|
|
92
93
|
* 1 - keyword
|
|
93
94
|
* 2 - string
|
|
@@ -97,7 +98,7 @@ export const types = [
|
|
|
97
98
|
* 6 - break
|
|
98
99
|
* 7 - space
|
|
99
100
|
* 8 - jsx literals
|
|
100
|
-
*
|
|
101
|
+
*
|
|
101
102
|
*/
|
|
102
103
|
const [
|
|
103
104
|
T_IDENTIFIER,
|
|
@@ -122,16 +123,18 @@ function encode(str) {
|
|
|
122
123
|
.replace(/>/g, '>')
|
|
123
124
|
.replace(/"/g, '"')
|
|
124
125
|
.replace(/'/g, ''')
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isWord(chr) {
|
|
129
|
+
return /^[\w_]+$/.test(chr)
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
function isIdentifierChar(chr) {
|
|
130
|
-
return /^[
|
|
133
|
+
return /^[a-zA-Z_$]$/.test(chr)
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
function isIdentifier(str) {
|
|
134
|
-
return
|
|
137
|
+
return isIdentifierChar(str[0]) && (str.length === 1 || isWord(str.slice(1)))
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
function isStringQuotation(chr) {
|
|
@@ -147,8 +150,8 @@ function isRegexStart(str) {
|
|
|
147
150
|
return str[0] === '/' && !isCommentStart(str[0] + str[1])
|
|
148
151
|
}
|
|
149
152
|
|
|
150
|
-
/**
|
|
151
|
-
* @param {string} code
|
|
153
|
+
/**
|
|
154
|
+
* @param {string} code
|
|
152
155
|
* @return {Array<[number, string]>}
|
|
153
156
|
*/
|
|
154
157
|
export function tokenize(code) {
|
|
@@ -177,9 +180,9 @@ export function tokenize(code) {
|
|
|
177
180
|
} else if (token.split('').every(ch => signs.has(ch))) {
|
|
178
181
|
return T_SIGN
|
|
179
182
|
} else if (
|
|
180
|
-
!inJsxLiterals() &&
|
|
183
|
+
!inJsxLiterals() &&
|
|
181
184
|
(
|
|
182
|
-
|
|
185
|
+
isWord(chr0) &&
|
|
183
186
|
chr0 === chr0.toUpperCase() ||
|
|
184
187
|
token === 'null'
|
|
185
188
|
)
|
|
@@ -207,11 +210,11 @@ export function tokenize(code) {
|
|
|
207
210
|
const prev = code[i - 1]
|
|
208
211
|
const next = code[i + 1]
|
|
209
212
|
const c_n = curr + next // current and next
|
|
210
|
-
const p_c = prev + curr // previous and current
|
|
213
|
+
const p_c = prev + curr // previous and current
|
|
211
214
|
const isJsxLiterals = inJsxLiterals()
|
|
212
215
|
|
|
213
216
|
if (jsx.tag) {
|
|
214
|
-
const isOpenElementEnd = curr === '>'
|
|
217
|
+
const isOpenElementEnd = curr === '>'
|
|
215
218
|
const isCloseElementEnd = p_c === '/>'
|
|
216
219
|
jsx.child = !isCloseElementEnd && !isCloseElementEnd
|
|
217
220
|
jsx.tag = !(isOpenElementEnd || isCloseElementEnd)
|
|
@@ -228,13 +231,13 @@ export function tokenize(code) {
|
|
|
228
231
|
append()
|
|
229
232
|
const [lastType, lastToken] = last
|
|
230
233
|
// Special cases that are not considered as regex:
|
|
231
|
-
// * (
|
|
234
|
+
// * (expr1) / expr2: `()` before `/` operator is still in expression
|
|
232
235
|
// * <non comment start>/ expr: non comment start before `/` is not regex
|
|
233
236
|
if (
|
|
234
|
-
isRegexChar &&
|
|
235
|
-
lastType &&
|
|
237
|
+
isRegexChar &&
|
|
238
|
+
lastType &&
|
|
236
239
|
!(
|
|
237
|
-
(lastType === T_SIGN && !'()
|
|
240
|
+
(lastType === T_SIGN && !'()'.includes(lastToken)) ||
|
|
238
241
|
lastType === T_COMMENT
|
|
239
242
|
)
|
|
240
243
|
) {
|
|
@@ -243,35 +246,48 @@ export function tokenize(code) {
|
|
|
243
246
|
continue
|
|
244
247
|
}
|
|
245
248
|
const start = i++
|
|
246
|
-
|
|
247
|
-
|
|
249
|
+
const leftQuote = code[start]
|
|
250
|
+
let foundClose = false
|
|
251
|
+
|
|
252
|
+
// end of line of end of file
|
|
253
|
+
const isEof = () => i >= code.length
|
|
254
|
+
const isEol = () => isEof() || code[i] === '\n'
|
|
248
255
|
// string quotation
|
|
249
256
|
if (isQuotationChar) {
|
|
250
|
-
for (;
|
|
251
|
-
|
|
257
|
+
for (; !isEof(); i++) {
|
|
258
|
+
// handle single quotes
|
|
259
|
+
if ((leftQuote === "'" || leftQuote === '"') && code[i] === '\n') break
|
|
260
|
+
if (isStringQuotation(code[i]) && code[i] === leftQuote) {
|
|
261
|
+
foundClose = true
|
|
252
262
|
break
|
|
253
263
|
}
|
|
254
264
|
}
|
|
255
265
|
} else {
|
|
256
266
|
// regex
|
|
257
|
-
for (;
|
|
267
|
+
for (; !isEol(); i++) {
|
|
258
268
|
if (code[i] === '/' && code[i - 1] !== '\\') {
|
|
269
|
+
foundClose = true
|
|
259
270
|
// append regex flags
|
|
260
|
-
while (start !== i && /^[a-z]$/.test(code[i + 1]) &&
|
|
271
|
+
while (start !== i && /^[a-z]$/.test(code[i + 1]) && !isEol()) {
|
|
261
272
|
i++
|
|
262
|
-
}
|
|
273
|
+
}
|
|
263
274
|
break
|
|
264
275
|
}
|
|
265
276
|
}
|
|
266
277
|
}
|
|
267
|
-
if (start !== i) {
|
|
278
|
+
if (start !== i && foundClose) {
|
|
279
|
+
// If current line is fully closed with string quotes or regex slashes,
|
|
280
|
+
// add them to tokens
|
|
268
281
|
current = code.slice(start, i + 1)
|
|
269
282
|
append(T_STRING)
|
|
270
283
|
} else {
|
|
284
|
+
// If it doesn't match any of the above, just leave it as operator and move on
|
|
271
285
|
current = curr
|
|
272
286
|
append()
|
|
287
|
+
i = start
|
|
273
288
|
}
|
|
274
289
|
} else if (isCommentStart(c_n)) {
|
|
290
|
+
append()
|
|
275
291
|
const start = i
|
|
276
292
|
if (next === '/') {
|
|
277
293
|
for (; i < code.length && code[i] !== '\n'; i++);
|
|
@@ -297,7 +313,7 @@ export function tokenize(code) {
|
|
|
297
313
|
} else {
|
|
298
314
|
if (
|
|
299
315
|
(isJsxLiterals && !jsxBrackets.has(curr)) ||
|
|
300
|
-
|
|
316
|
+
isWord(curr) === isWord(current[current.length - 1]) &&
|
|
301
317
|
!signs.has(curr)
|
|
302
318
|
) {
|
|
303
319
|
current += curr
|
|
@@ -326,30 +342,61 @@ export function tokenize(code) {
|
|
|
326
342
|
return tokens
|
|
327
343
|
}
|
|
328
344
|
|
|
329
|
-
/**
|
|
330
|
-
* @param {Array<[number, string]>} tokens
|
|
345
|
+
/**
|
|
346
|
+
* @param {Array<[number, string]>} tokens
|
|
331
347
|
* @return {Array<string>}
|
|
332
348
|
*/
|
|
333
349
|
function generate(tokens) {
|
|
334
|
-
|
|
350
|
+
let lineNo = 1
|
|
351
|
+
const linesHtml = []
|
|
352
|
+
const createLine = (content) => `<span data-line-number="${lineNo++}" class="sh__line">${content}</span>`
|
|
353
|
+
|
|
354
|
+
function flushLine(tokens) {
|
|
355
|
+
linesHtml.push(createLine(
|
|
356
|
+
tokens.map(token => (
|
|
357
|
+
`<span class="sh__${types[token[0]]}">${encode(token[1])}</span>`
|
|
358
|
+
))
|
|
359
|
+
.join('')
|
|
360
|
+
))
|
|
361
|
+
}
|
|
362
|
+
const lineTokens = []
|
|
335
363
|
for (let i = 0; i < tokens.length; i++) {
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
364
|
+
const token = tokens[i]
|
|
365
|
+
const [type, value] = token
|
|
366
|
+
if (type !== T_BREAK) {
|
|
367
|
+
// Divide multi-line token into multi-line code
|
|
368
|
+
if (value.includes('\n')) {
|
|
369
|
+
const lines = value.split('\n')
|
|
370
|
+
for (let j = 0; j < lines.length; j++) {
|
|
371
|
+
lineTokens.push([type, lines[j]])
|
|
372
|
+
if (j < lines.length - 1) {
|
|
373
|
+
flushLine(lineTokens)
|
|
374
|
+
lineTokens.length = 0
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
lineTokens.push(token)
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
lineTokens.push([type, ''])
|
|
382
|
+
flushLine(lineTokens)
|
|
383
|
+
lineTokens.length = 0
|
|
384
|
+
}
|
|
342
385
|
}
|
|
343
|
-
|
|
386
|
+
|
|
387
|
+
if (lineTokens.length)
|
|
388
|
+
flushLine(lineTokens)
|
|
389
|
+
|
|
390
|
+
return linesHtml
|
|
344
391
|
}
|
|
345
392
|
|
|
346
393
|
/**
|
|
347
|
-
*
|
|
348
|
-
* @param {string} code
|
|
394
|
+
*
|
|
395
|
+
* @param {string} code
|
|
349
396
|
* @returns {string}
|
|
350
397
|
*/
|
|
351
398
|
export function highlight(code) {
|
|
352
399
|
const tokens = tokenize(code)
|
|
353
|
-
const output = generate(tokens).join('')
|
|
400
|
+
const output = generate(tokens).join('\n')
|
|
354
401
|
return output
|
|
355
402
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sugar-high",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": "./lib/index.mjs",
|
|
6
6
|
"description": "Super lightweight JSX syntax highlighter",
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"scripts": {
|
|
14
14
|
"test": "vitest run",
|
|
15
|
-
"dev": "
|
|
16
|
-
"build": "
|
|
15
|
+
"dev": "next dev docs",
|
|
16
|
+
"build": "next build docs"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"
|
|
20
|
-
"
|
|
19
|
+
"next": "12.1.4",
|
|
20
|
+
"react": "18.0.0",
|
|
21
|
+
"react-dom": "18.0.0",
|
|
22
|
+
"vitest": "~0.6.0"
|
|
21
23
|
}
|
|
22
24
|
}
|