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.
Files changed (3) hide show
  1. package/README.md +23 -0
  2. package/lib/index.mjs +314 -0
  3. 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, '&amp;')
105
+ .replace(/</g, '&lt;')
106
+ .replace(/>/g, '&gt;')
107
+ .replace(/"/g, '&quot;')
108
+ .replace(/'/g, '&#039;')
109
+ // matches space but not new line
110
+ .replace(/[^\S\r\n]/g, '&nbsp;')
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",
3
+ "version": "0.0.4",
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": {