sugar-high 0.2.1 → 0.3.1

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 +17 -2
  2. package/lib/index.mjs +95 -45
  3. package/package.json +4 -4
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
- ![img](https://repository-images.githubusercontent.com/453236442/82cf2807-78f0-4009-bbdf-d7e753f73cf4)
6
+ ![img](https://repository-images.githubusercontent.com/453236442/94994be5-72fa-479f-8b87-2dc14fe40329)
7
7
 
8
8
  ### Usage
9
9
 
@@ -54,7 +54,22 @@ Then make your own theme with customized colors by token type and put in global
54
54
  color: #a19595;
55
55
  }
56
56
  .sh__jsxliterals {
57
- color: #03066e;
57
+ color: #6266d1;
58
+ }
59
+ ```
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;
58
73
  }
59
74
  ```
60
75
 
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,14 +123,19 @@ function encode(str) {
122
123
  .replace(/>/g, '>')
123
124
  .replace(/"/g, '"')
124
125
  .replace(/'/g, ''')
125
- // matches space but not new line
126
- .replace(/[^\S\r\n]/g, ' ')
127
126
  }
128
127
 
129
128
  function isWord(chr) {
130
129
  return /^[\w_]+$/.test(chr)
131
130
  }
132
131
 
132
+ function isCls(str) {
133
+ const chr0 = str[0]
134
+ return isWord(chr0) &&
135
+ chr0 === chr0.toUpperCase() ||
136
+ str === 'null'
137
+ }
138
+
133
139
  function isIdentifierChar(chr) {
134
140
  return /^[a-zA-Z_$]$/.test(chr)
135
141
  }
@@ -151,8 +157,8 @@ function isRegexStart(str) {
151
157
  return str[0] === '/' && !isCommentStart(str[0] + str[1])
152
158
  }
153
159
 
154
- /**
155
- * @param {string} code
160
+ /**
161
+ * @param {string} code
156
162
  * @return {Array<[number, string]>}
157
163
  */
158
164
  export function tokenize(code) {
@@ -162,16 +168,22 @@ export function tokenize(code) {
162
168
  /** @type {Array<[number, string]>} */
163
169
  const tokens = []
164
170
 
165
- // jsx.tag for entering open or closed tag
171
+ // jsx.tag for entering open or closed tags
172
+ // jsx.close for is the closing sign of open or close tags
166
173
  // jsx.child for entering children
167
174
  // jsx.expr for entering {expression}
168
- /** @type {{ tag: boolean; child: boolean; expr: boolean }} */
169
- const jsx = { tag: false, child: false, expr: false }
175
+ /** @type {{ tag: boolean; close: boolean; child: boolean; expr: boolean }} */
176
+ const jsx = { tag: false, close: false, child: false, expr: false }
170
177
 
171
- const inJsxLiterals = () => !jsx.tag && jsx.child && !jsx.expr
178
+ const inJsxTag = () => jsx.tag || jsx.close
179
+ const inJsxLiterals = () => !inJsxTag() && jsx.child && !jsx.expr
172
180
 
181
+ /**
182
+ *
183
+ * @param {string} token
184
+ * @returns {number}
185
+ */
173
186
  function classify(token) {
174
- const chr0 = token[0]
175
187
  if (keywords.has(token)) {
176
188
  return T_KEYWORD
177
189
  } else if (token === '\n') {
@@ -180,17 +192,12 @@ export function tokenize(code) {
180
192
  return T_SPACE
181
193
  } else if (token.split('').every(ch => signs.has(ch))) {
182
194
  return T_SIGN
183
- } else if (
184
- !inJsxLiterals() &&
185
- (
186
- isWord(chr0) &&
187
- chr0 === chr0.toUpperCase() ||
188
- token === 'null'
189
- )
190
- ) {
191
- return T_CLS_NUMBER
195
+ } else if (isCls(token)) {
196
+ return inJsxLiterals() ? T_JSX_LITERALS : inJsxTag() ? T_IDENTIFIER : T_CLS_NUMBER
192
197
  } else {
193
- return isIdentifier(token) ? T_IDENTIFIER : inJsxLiterals() ? T_JSX_LITERALS : T_STRING
198
+ return inJsxLiterals()
199
+ ? T_JSX_LITERALS
200
+ : isIdentifier(token) ? T_IDENTIFIER : T_STRING
194
201
  }
195
202
  }
196
203
 
@@ -210,15 +217,19 @@ export function tokenize(code) {
210
217
  const curr = code[i]
211
218
  const prev = code[i - 1]
212
219
  const next = code[i + 1]
220
+ const p_c = prev + curr // previous and current
213
221
  const c_n = curr + next // current and next
214
- const p_c = prev + curr // previous and current
215
222
  const isJsxLiterals = inJsxLiterals()
216
223
 
224
+ if (jsx.close && (last[1] === '>' || last[1] === '/>')) {
225
+ jsx.close = false
226
+ }
217
227
  if (jsx.tag) {
218
- const isOpenElementEnd = curr === '>'
228
+ const isOpenElementEnd = curr === '>'
219
229
  const isCloseElementEnd = p_c === '/>'
220
- jsx.child = !isCloseElementEnd && !isCloseElementEnd
230
+ jsx.child = !isCloseElementEnd
221
231
  jsx.tag = !(isOpenElementEnd || isCloseElementEnd)
232
+ jsx.close = isOpenElementEnd || isCloseElementEnd
222
233
  }
223
234
  // if it's not in a jsx tag declaration or a string, close child if next is jsx close tag
224
235
  if (!jsx.tag && (curr === '<' && isIdentifierChar(next) || c_n === '</')) {
@@ -235,10 +246,10 @@ export function tokenize(code) {
235
246
  // * (expr1) / expr2: `()` before `/` operator is still in expression
236
247
  // * <non comment start>/ expr: non comment start before `/` is not regex
237
248
  if (
238
- isRegexChar &&
239
- lastType &&
249
+ isRegexChar &&
250
+ lastType &&
240
251
  !(
241
- (lastType === T_SIGN && !'()'.includes(lastToken)) ||
252
+ (lastType === T_SIGN && !'()'.includes(lastToken)) ||
242
253
  lastType === T_COMMENT
243
254
  )
244
255
  ) {
@@ -247,14 +258,18 @@ export function tokenize(code) {
247
258
  continue
248
259
  }
249
260
  const start = i++
261
+ const leftQuote = code[start]
250
262
  let foundClose = false
251
-
263
+
252
264
  // end of line of end of file
253
- const isEol = () => i >= code.length || code[i] === '\n'
265
+ const isEof = () => i >= code.length
266
+ const isEol = () => isEof() || code[i] === '\n'
254
267
  // string quotation
255
268
  if (isQuotationChar) {
256
- for (; !isEol(); i++) {
257
- if (isStringQuotation(code[i]) && code[i] === code[start]) {
269
+ for (; !isEof(); i++) {
270
+ // handle single quotes
271
+ if ((leftQuote === "'" || leftQuote === '"') && code[i] === '\n') break
272
+ if (isStringQuotation(code[i]) && code[i] === leftQuote) {
258
273
  foundClose = true
259
274
  break
260
275
  }
@@ -267,7 +282,7 @@ export function tokenize(code) {
267
282
  // append regex flags
268
283
  while (start !== i && /^[a-z]$/.test(code[i + 1]) && !isEol()) {
269
284
  i++
270
- };
285
+ }
271
286
  break
272
287
  }
273
288
  }
@@ -284,6 +299,7 @@ export function tokenize(code) {
284
299
  i = start
285
300
  }
286
301
  } else if (isCommentStart(c_n)) {
302
+ append()
287
303
  const start = i
288
304
  if (next === '/') {
289
305
  for (; i < code.length && code[i] !== '\n'; i++);
@@ -313,6 +329,9 @@ export function tokenize(code) {
313
329
  !signs.has(curr)
314
330
  ) {
315
331
  current += curr
332
+ if (next === '<') {
333
+ append();
334
+ }
316
335
  } else {
317
336
  append()
318
337
  current = curr
@@ -338,30 +357,61 @@ export function tokenize(code) {
338
357
  return tokens
339
358
  }
340
359
 
341
- /**
342
- * @param {Array<[number, string]>} tokens
360
+ /**
361
+ * @param {Array<[number, string]>} tokens
343
362
  * @return {Array<string>}
344
363
  */
345
364
  function generate(tokens) {
346
- const output = []
365
+ let lineNo = 1
366
+ const linesHtml = []
367
+ const createLine = (content) => `<span data-line-number="${lineNo++}" class="sh__line">${content}</span>`
368
+
369
+ function flushLine(tokens) {
370
+ linesHtml.push(createLine(
371
+ tokens.map(token => (
372
+ `<span class="sh__${types[token[0]]}">${encode(token[1])}</span>`
373
+ ))
374
+ .join('')
375
+ ))
376
+ }
377
+ const lineTokens = []
347
378
  for (let i = 0; i < tokens.length; i++) {
348
- const [type, token] = tokens[i]
349
- output.push(
350
- type === T_BREAK
351
- ? '<br>'
352
- : `<span class="sh__${types[type]}">${encode(token)}</span>`
353
- )
379
+ const token = tokens[i]
380
+ const [type, value] = token
381
+ if (type !== T_BREAK) {
382
+ // Divide multi-line token into multi-line code
383
+ if (value.includes('\n')) {
384
+ const lines = value.split('\n')
385
+ for (let j = 0; j < lines.length; j++) {
386
+ lineTokens.push([type, lines[j]])
387
+ if (j < lines.length - 1) {
388
+ flushLine(lineTokens)
389
+ lineTokens.length = 0
390
+ }
391
+ }
392
+ } else {
393
+ lineTokens.push(token)
394
+ }
395
+ } else {
396
+ lineTokens.push([type, ''])
397
+ flushLine(lineTokens)
398
+ lineTokens.length = 0
399
+ }
354
400
  }
355
- return output
401
+
402
+ if (lineTokens.length)
403
+ flushLine(lineTokens)
404
+
405
+ return linesHtml
356
406
  }
357
407
 
358
408
  /**
359
- *
360
- * @param {string} code
409
+ *
410
+ * @param {string} code
361
411
  * @returns {string}
362
412
  */
363
413
  export function highlight(code) {
364
414
  const tokens = tokenize(code)
365
- const output = generate(tokens).join('')
415
+ const output = generate(tokens).join('\n')
366
416
  return output
367
417
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sugar-high",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "exports": "./lib/index.mjs",
6
6
  "description": "Super lightweight JSX syntax highlighter",
@@ -16,9 +16,9 @@
16
16
  "build": "next build docs"
17
17
  },
18
18
  "devDependencies": {
19
- "next": "12.1.1-canary.15",
20
- "react": "18.0.0-rc.2",
21
- "react-dom": "18.0.0-rc.2",
19
+ "next": "12.1.4",
20
+ "react": "18.0.0",
21
+ "react-dom": "18.0.0",
22
22
  "vitest": "~0.6.0"
23
23
  }
24
24
  }