sugar-high 0.0.1 → 0.0.5

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 ADDED
@@ -0,0 +1,61 @@
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 and put in global CSS
25
+
26
+ ```css
27
+ /*
28
+ * 0 - comment
29
+ * 1 - keyword
30
+ * 2 - break
31
+ * 3 - string
32
+ * 4 - space
33
+ * 5 - sign
34
+ * 6 - identifier
35
+ * 7 - Class, number and null
36
+ */
37
+ .sh__7 {
38
+ color: #3476cb;
39
+ }
40
+ .sh__6 {
41
+ color: #2d333b;
42
+ }
43
+ .sh__5 {
44
+ color: #818c9b;
45
+ font-weight: bold;
46
+ }
47
+ .sh__3 {
48
+ color: #00a99a;
49
+ }
50
+ .sh__1 {
51
+ color: #f47067;
52
+ }
53
+ .sh__0 {
54
+ color: #818c9b;
55
+ }
56
+ ```
57
+
58
+ ### LICENSE
59
+
60
+ MIT
61
+
package/lib/index.mjs ADDED
@@ -0,0 +1,315 @@
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
+ * 7 - Class, number and null
86
+ *
87
+ */
88
+ const [
89
+ T_COMMENT,
90
+ T_KEYWORD,
91
+ T_BREAK,
92
+ T_STRING,
93
+ T_SPACE,
94
+ T_SIGN,
95
+ T_IDENTIFIER,
96
+ T_CLS_NUMBER,
97
+ ] = Array(8).fill().map((_, i) => i)
98
+
99
+ function isSpaces(str) {
100
+ return /^[^\S\r\n]+$/g.test(str)
101
+ }
102
+
103
+ function encode(str) {
104
+ return str
105
+ .replace(/&/g, '&amp;')
106
+ .replace(/</g, '&lt;')
107
+ .replace(/>/g, '&gt;')
108
+ .replace(/"/g, '&quot;')
109
+ .replace(/'/g, '&#039;')
110
+ // matches space but not new line
111
+ .replace(/[^\S\r\n]/g, '&nbsp;')
112
+ }
113
+
114
+ function isIdentifierChar(chr) {
115
+ return /[a-zA-Z0-9_$]/.test(chr)
116
+ }
117
+
118
+ function isStringQuotation(chr) {
119
+ return chr === '"' || chr === "'" || chr === '`'
120
+ }
121
+
122
+ function isCommentStart(str) {
123
+ str = str.slice(0, 2)
124
+ return str === '//' || str === '/*'
125
+ }
126
+
127
+ function isRegexStart(str) {
128
+ return str[0] === '/' && !isCommentStart(str[0] + str[1])
129
+ }
130
+
131
+ /**
132
+ * @param {string} code
133
+ * @return {Array<[number, string]>}
134
+ */
135
+ export function tokenize(code) {
136
+ let current = ''
137
+ let type = -1
138
+ /** @type {Array<[number, string]>} */
139
+ const tokens = []
140
+ // string.type = 0 for string or string template
141
+ // string.type = 1 for regex
142
+ const string = { entered: false, type: 0 }
143
+
144
+ // comment.type = 0 for single line comments
145
+ // comment.type = 1 for multi-line comments
146
+ const comment = { entered: false, type: 0 }
147
+
148
+ // jsx.tag for entering open or closed tag
149
+ // jsx.child for entering children
150
+ // jsx.expr for entering {expression}
151
+ /** @type {{ tag: boolean; child: boolean; expr: boolean }} */
152
+ const jsx = { tag: false, child: false, expr: false }
153
+
154
+ function classify(token) {
155
+ if (isCommentStart(token[0] + token[1])) {
156
+ return T_COMMENT
157
+ } else if (keywords.has(token)) {
158
+ return T_KEYWORD
159
+ } else if (token === '\n') {
160
+ return T_BREAK
161
+ } else if (
162
+ (
163
+ // is quoted string
164
+ (isStringQuotation(token[0]) && !isStringQuotation(token[1])) ||
165
+ // is regex
166
+ (!jsx.tag && isRegexStart(token[0] + token[1]) && token[token.length - 1] === '/')
167
+ )
168
+ ) {
169
+ return T_STRING
170
+ } else if (token === ' ') {
171
+ return T_SPACE
172
+ } else if (signs.has(token[0])) {
173
+ return T_SIGN
174
+ } else if (
175
+ token[0] === token[0].toUpperCase() ||
176
+ token === 'null'
177
+ ) {
178
+ return T_CLS_NUMBER
179
+ } else {
180
+ return T_IDENTIFIER
181
+ }
182
+ }
183
+
184
+ const append = () => {
185
+ if (current) {
186
+ type = classify(current)
187
+ tokens.push([type, current])
188
+ }
189
+ current = ''
190
+ }
191
+ for (let i = 0; i < code.length; i++) {
192
+ const curr = code[i]
193
+ const prev = code[i - 1]
194
+ const next = code[i + 1]
195
+ const c_n = curr + next // current and next
196
+ const p_c = prev + curr // previous and current
197
+ const isJsxLiterals = jsx.child && !jsx.expr
198
+
199
+ if (jsx.tag) {
200
+ const isOpenElementEnd = curr === '>'
201
+ const isCloseElementEnd = p_c === '/>'
202
+ jsx.child = !isCloseElementEnd && !isCloseElementEnd
203
+ jsx.tag = !(isOpenElementEnd || isCloseElementEnd)
204
+ }
205
+ // if it's not in a jsx tag declaration or a string, close child if next is jsx close tag
206
+ if (!jsx.tag && !string.entered && (curr === '<' && isIdentifierChar(next) || c_n === '</')) {
207
+ jsx.tag = true
208
+ jsx.child = false
209
+ }
210
+
211
+ if (jsx.child && curr === '{') {
212
+ jsx.expr = true
213
+ }
214
+ if (jsx.child && jsx.expr && curr === '}') {
215
+ jsx.expr = false
216
+ }
217
+
218
+ if (
219
+ !string.entered &&
220
+ (isStringQuotation(curr) || !jsx.tag && isRegexStart(c_n))
221
+ ) {
222
+ string.entered = true
223
+ string.type = isStringQuotation(curr) ? 0 : 1
224
+ append()
225
+ current = curr
226
+ } else if (string.entered) {
227
+ current += curr
228
+ if (string.type === 0 && isStringQuotation(curr)) {
229
+ string.entered = false
230
+ append()
231
+ } else if (string.type === 1 && prev !== '\\' && curr === '/') {
232
+ string.entered = false
233
+ append()
234
+ }
235
+ } else if (
236
+ !comment.entered &&
237
+ isCommentStart(c_n)
238
+ ) {
239
+ comment.type = next === '/' ? 0 : 1
240
+ comment.entered = true
241
+ append()
242
+ current = c_n
243
+ i++
244
+ } else if (comment.entered) {
245
+ current += curr
246
+ if (comment.type === 0 && next === '\n') {
247
+ comment.entered = false
248
+ append()
249
+ } else if (comment.type === 1 && (c_n === '*/')) {
250
+ comment.entered = false
251
+ current += '/'
252
+ append()
253
+ i++
254
+ }
255
+ } else if (curr === ' ' || curr === '\n') {
256
+ if (
257
+ curr === ' ' &&
258
+ (
259
+ (isSpaces(current) || !current) ||
260
+ isJsxLiterals
261
+ )
262
+ ) {
263
+ current += curr
264
+ } else {
265
+ append()
266
+ current = curr
267
+ append()
268
+ }
269
+ } else {
270
+ if (
271
+ (isJsxLiterals && !jsxBrackets.has(curr)) ||
272
+ isIdentifierChar(curr) === isIdentifierChar(current[current.length - 1]) &&
273
+ !signs.has(curr)
274
+ ) {
275
+ current += curr
276
+ } else {
277
+ append()
278
+ current = curr
279
+ if (c_n === '</') {
280
+ current = c_n
281
+ append()
282
+ i++
283
+ }
284
+ else if (jsxBrackets.has(curr)) append()
285
+ }
286
+ }
287
+ }
288
+
289
+ append()
290
+
291
+ return tokens
292
+ }
293
+
294
+ /**
295
+ * @param {Array<[number, string]>} tokens
296
+ * @return {Array<string>}
297
+ */
298
+ function generate(tokens) {
299
+ const output = []
300
+ for (let i = 0; i < tokens.length; i++) {
301
+ const [type, token] = tokens[i]
302
+ output.push(
303
+ type === T_BREAK
304
+ ? '<br>'
305
+ : `<span class="sh__${type}">${encode(token)}</span>`
306
+ )
307
+ }
308
+ return output
309
+ }
310
+
311
+ export function highlight(code) {
312
+ const tokens = tokenize(code)
313
+ const output = generate(tokens).join('')
314
+ return output
315
+ }
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "sugar-high",
3
- "version": "0.0.1",
4
- "exports": "./lib/index.js",
5
- "description": "Super lightweight JavaScript syntax highlighter",
3
+ "version": "0.0.5",
4
+ "type": "module",
5
+ "exports": "./lib/index.mjs",
6
+ "description": "Super lightweight JSX syntax highlighter",
7
+ "files": [
8
+ "lib",
9
+ "*.md"
10
+ ],
6
11
  "license": "MIT",
7
12
  "scripts": {
8
13
  "dev": "vite docs",
package/docs/index.html DELETED
@@ -1,72 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>sugar high</title>
8
- <style>
9
- html {
10
- font-family: "Inter",-apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif
11
- }
12
- body {
13
- max-width: 1120px;
14
- margin: auto;
15
- padding: 0 10px;
16
- }
17
- #code {
18
- font-family: Consolas, Monaco, monospace;
19
- font-size: 16px;
20
- display: block;
21
- width: 100%;
22
- min-height: 100px;
23
- background-color: #fff;
24
- }
25
-
26
- #output {
27
- font-family: Consolas, Monaco, monospace;
28
- font-size: 16px;
29
- display: block;
30
- min-height: 100px;
31
- }
32
-
33
- .identifier {
34
- color: #2d333b;
35
- }
36
- .sign {
37
- color: #818c9b;
38
- font-weight: bold;
39
- }
40
- .string {
41
- color: #00a99a;
42
- }
43
- .keyword {
44
- color: #f47067;
45
- }
46
-
47
- .flex {
48
- display: flex;
49
- }
50
- .flex-1 {
51
- flex: 1 0;
52
- margin: 8px;
53
- padding: 12px 10px 12px 10px;
54
- background-color: #f9f9f9;
55
- border: 1px solid #d0d0d0;
56
- border-radius: 4px;
57
- }
58
- .comment {
59
- color: #818c9b;
60
- }
61
- </style>
62
- </head>
63
- <body>
64
- <h1>Sugar High</h1>
65
- <div class="flex">
66
- <textarea class="flex-1" id="code"></textarea>
67
-
68
- <pre class="flex-1"><code id="output"></code></pre>
69
- </div>
70
- <script type="module" src="./main.js"></script>
71
- </body>
72
- </html>
package/docs/main.js DELETED
@@ -1,50 +0,0 @@
1
- import { tokenize, generate } from '../lib'
2
-
3
- // console interactive API
4
- window.tokenize = tokenize
5
- window.generate = generate
6
-
7
- const codeInput = document.getElementById('code')
8
- const codeOutput = document.getElementById('output')
9
-
10
- codeInput.addEventListener('input', () => {
11
- update()
12
- })
13
-
14
- codeInput.value = `
15
- // hello-world.js
16
- import { planet } from '../space'
17
-
18
- class SuperArray extends Array {
19
- static core = planet
20
-
21
- constructor(...args) {
22
- super(...args)
23
- }
24
-
25
- bump() {
26
- return this.map(x => x + 1)
27
- }
28
- }
29
-
30
- /**
31
- * @param {string} name
32
- * @return {void}
33
- */
34
- function hello(name) {
35
- console.log('hello', name)
36
- }
37
-
38
- `.trim()
39
-
40
- function update() {
41
- const code = codeInput.value?.trim() || ''
42
- const tokens = tokenize(code)
43
- const output = generate(tokens).join('')
44
-
45
- console.log(tokens)
46
-
47
- codeOutput.innerHTML = output
48
- }
49
-
50
- update()
package/lib/index.js DELETED
@@ -1,203 +0,0 @@
1
- // @ts-check
2
-
3
- const keywords = new Set([
4
- 'for',
5
- 'while',
6
- 'if',
7
- 'else',
8
- 'return',
9
- 'function',
10
- 'var',
11
- 'let',
12
- 'const',
13
- 'true',
14
- 'false',
15
- 'null',
16
- 'undefined',
17
- 'NaN',
18
- 'Infinity',
19
- 'this',
20
- 'new',
21
- 'delete',
22
- 'typeof',
23
- 'in',
24
- 'instanceof',
25
- 'void',
26
- 'break',
27
- 'continue',
28
- 'switch',
29
- 'case',
30
- 'default',
31
- 'throw',
32
- 'try',
33
- 'catch',
34
- 'finally',
35
- 'debugger',
36
- 'with',
37
- 'yield',
38
- 'async',
39
- 'await',
40
- 'class',
41
- 'extends',
42
- 'super',
43
- 'import',
44
- 'export',
45
- 'from',
46
- 'static',
47
- ])
48
-
49
- const signs = new Set([
50
- '+',
51
- '-',
52
- '*',
53
- '/',
54
- '%',
55
- '=',
56
- '!',
57
- '>',
58
- '<',
59
- '&',
60
- '|',
61
- '^',
62
- '~',
63
- '!',
64
- '?',
65
- ':',
66
- '.',
67
- ',',
68
- `'`,
69
- '"',
70
- '.',
71
- '{',
72
- '}',
73
- '(',
74
- ')',
75
- '[',
76
- ']',
77
- '#',
78
- '\\',
79
- ])
80
-
81
- function encode(str) {
82
- return str
83
- .replace(/&/g, "&amp;")
84
- .replace(/</g, "&lt;")
85
- .replace(/>/g, "&gt;")
86
- .replace(/"/g, "&quot;")
87
- .replace(/'/g, "&#039;")
88
- }
89
-
90
- function isIdentifierChar(chr) {
91
- return /[a-zA-Z0-9_$]/.test(chr)
92
- }
93
-
94
- function isQuotationMark(chr) {
95
- return chr === '"' || chr === "'" || chr === '`'
96
- }
97
-
98
- /**
99
- * @param {string} code
100
- * @return {Array<string>}
101
- */
102
- export function tokenize(code) {
103
- let current = ''
104
- const tokens = []
105
- const string = { entered: false }
106
- const comment = { entered: false, type: 0 }
107
- for (let i = 0; i < code.length; i++) {
108
- if (!string.entered && isQuotationMark(code[i])) {
109
- string.entered = true
110
- if (current) tokens.push(current)
111
- current = code[i]
112
- } else if (string.entered) {
113
- current += code[i]
114
- if (isQuotationMark(code[i])) {
115
- string.entered = false
116
- tokens.push(current)
117
- current = ''
118
- }
119
- } else if (
120
- !comment.entered &&
121
- code[i] === '/' &&
122
- (code[i + 1] === '/' || code[i + 1] === '*')
123
- ) {
124
- comment.type = code[i + 1] === '/' ? 0 : 1
125
- comment.entered = true
126
- if (current) tokens.push(current)
127
- current = code[i]
128
- } else if (comment.entered) {
129
- current += code[i]
130
- if (comment.type === 0 && code[i + 1] === '\n') {
131
- comment.entered = false
132
- tokens.push(current)
133
- current = ''
134
- } else if (comment.type === 1 && code[i] === '*' && code[i + 1] === '/') {
135
- comment.entered = false
136
- tokens.push(current)
137
- current = ''
138
- tokens.push(code[i])
139
- tokens.push(code[i + 1])
140
- i++
141
- }
142
- } else if (code[i] === ' ' || code[i] === '\n') {
143
- if (current.length > 0) {
144
- tokens.push(current)
145
- current = ''
146
- }
147
- tokens.push(code[i])
148
- } else {
149
- if (isIdentifierChar(code[i]) !== isIdentifierChar(current[current.length - 1])) {
150
- if (current) tokens.push(current)
151
- current = code[i]
152
- } else {
153
- current += code[i]
154
- }
155
- }
156
- }
157
- if (current) {
158
- tokens.push(current)
159
- current = ''
160
- }
161
-
162
- return tokens
163
- }
164
-
165
-
166
- function classify(token) {
167
- if (token[0] === '/' && (token[1] === '*' || token[1] === '/')) {
168
- return 'comment'
169
- } else if (keywords.has(token)) {
170
- return 'keyword'
171
- } else if (token === '\n') {
172
- return 'linebreak'
173
- } else if (isQuotationMark(token[0]) && !isQuotationMark(token[1])) {
174
- return 'string'
175
- } else if (token === ' ') {
176
- return 'space'
177
- } else if (signs.has(token[0])) {
178
- return 'sign'
179
- } else {
180
- return 'identifier'
181
- }
182
- }
183
-
184
- /**
185
- * @param {Array<string>} tokens
186
- * @return {Array<string>}
187
- */
188
- export function generate(tokens) {
189
- let output = []
190
- for (let i = 0; i < tokens.length; i++) {
191
- const token = tokens[i]
192
- const type = classify(token)
193
- if (type === 'linebreak') {
194
- output.push('<br>')
195
- } else if (type === 'space') {
196
- output.push(`<span class="space">&nbsp;</span>`)
197
- } else {
198
- output.push(`<span class="${type}">${encode(token)}</span>`)
199
- }
200
- }
201
- return output
202
- }
203
-