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.
Files changed (3) hide show
  1. package/README.md +16 -1
  2. package/lib/index.mjs +85 -38
  3. 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
- ![img](https://repository-images.githubusercontent.com/453236442/9416c941-9c40-4dbe-a73e-0e0d21993802)
6
+ ![img](https://repository-images.githubusercontent.com/453236442/82cf2807-78f0-4009-bbdf-d7e753f73cf4)
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
- // matches space but not new line
126
- .replace(/[^\S\r\n]/g, ' ')
126
+ }
127
+
128
+ function isWord(chr) {
129
+ return /^[\w_]+$/.test(chr)
127
130
  }
128
131
 
129
132
  function isIdentifierChar(chr) {
130
- return /^[\w_$]$/.test(chr)
133
+ return /^[a-zA-Z_$]$/.test(chr)
131
134
  }
132
135
 
133
136
  function isIdentifier(str) {
134
- return /[a-zA-Z_$]/.test(str[0]) && (str.length === 1 || /^[\w_$]+$/.test(str.slice(1)))
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
- isIdentifierChar(chr0) &&
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
- // * (expr) / expr: `()[]` before `/` is still in expression
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 && !'()[]'.includes(lastToken)) ||
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
- const isInline = () => i < code.length && code[i] !== '\n'
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 (; isInline(); i++) {
251
- if (isStringQuotation(code[i])) {
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 (; isInline(); i++) {
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]) && isInline()) {
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
- isIdentifierChar(curr) === isIdentifierChar(current[current.length - 1]) &&
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
- const output = []
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 [type, token] = tokens[i]
337
- output.push(
338
- type === T_BREAK
339
- ? '<br>'
340
- : `<span class="sh__${types[type]}">${encode(token)}</span>`
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
- return output
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.2.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": "vite docs",
16
- "build": "vite build docs"
15
+ "dev": "next dev docs",
16
+ "build": "next build docs"
17
17
  },
18
18
  "devDependencies": {
19
- "vite": "^2.8.6",
20
- "vitest": "^0.5.9"
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
  }