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.
Files changed (3) hide show
  1. package/README.md +62 -0
  2. package/lib/index.mjs +327 -0
  3. 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, '&amp;')
118
+ .replace(/</g, '&lt;')
119
+ .replace(/>/g, '&gt;')
120
+ .replace(/"/g, '&quot;')
121
+ .replace(/'/g, '&#039;')
122
+ // matches space but not new line
123
+ .replace(/[^\S\r\n]/g, '&nbsp;')
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.2",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
- "exports": {
6
- "import": "./lib/index.mjs"
7
- },
8
- "description": "Super lightweight JavaScript syntax highlighter",
5
+ "exports": "./lib/index.mjs",
6
+ "description": "Super lightweight JSX syntax highlighter",
9
7
  "files": [
10
- "./lib"
8
+ "lib",
9
+ "*.md"
11
10
  ],
12
11
  "license": "MIT",
13
12
  "scripts": {