@ape-egg/codie 0.1.3 → 0.1.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 (2) hide show
  1. package/highlight.js +137 -43
  2. package/package.json +1 -1
package/highlight.js CHANGED
@@ -198,53 +198,102 @@ export const highlightJSRaw = (code) => {
198
198
  return tokens.join('');
199
199
  };
200
200
 
201
- // CSS highlighter
201
+ // CSS highlighter - walks brace depth so nested rules (@supports, @media, and
202
+ // & selectors) tokenize at every level, not just the innermost block
202
203
  export const highlightCSS = (code, options = {}) => {
203
- let result = '';
204
- const ruleRegex = /(\/\*[\s\S]*?\*\/)|((?:[^{}\/]|\/(?!\*))+)(\{)([^}]*)(\})/g;
205
- let match,
206
- lastIndex = 0;
204
+ const declarations = (text) => {
205
+ const propRegex = /([\w-]+)(\s*:\s*)([^;]+)(;?)/g;
206
+ let match,
207
+ last = 0,
208
+ result = '';
209
+
210
+ while ((match = propRegex.exec(text)) !== null) {
211
+ if (match.index > last) result += text.slice(last, match.index);
212
+ const [, prop, colon, value, semi] = match;
213
+ result += span(CLASS.CSS_PROPERTY, prop) + span(CLASS.PUNCTUATION, colon);
214
+ const highlighted = value.replace(/@\[([^\]]+)\]/g, (_, expr) => {
215
+ const h = expr.replace(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g, (v) =>
216
+ span(CLASS.VIBE_VARIABLE, v),
217
+ );
218
+ return `${span(CLASS.VIBE_COLOR, '@[')}${h}${span(CLASS.VIBE_COLOR, ']')}`;
219
+ });
220
+ result += span(CLASS.CSS_VALUE, highlighted);
221
+ if (semi) result += span(CLASS.PUNCTUATION, semi);
222
+ last = propRegex.lastIndex;
223
+ }
224
+ if (last < text.length) result += text.slice(last);
225
+ return result;
226
+ };
227
+
228
+ // Index of the } closing the block opened at `open`, or -1 when unterminated
229
+ const closingBrace = (text, open) => {
230
+ let depth = 1,
231
+ i = open + 1;
232
+
233
+ while (i < text.length && depth) {
234
+ if (text.startsWith('/*', i)) {
235
+ const end = text.indexOf('*/', i + 2);
236
+ i = end === -1 ? text.length : end + 2;
237
+ continue;
238
+ }
239
+ if (text[i] === '{') depth++;
240
+ else if (text[i] === '}') depth--;
241
+ i++;
242
+ }
207
243
 
208
- while ((match = ruleRegex.exec(code)) !== null) {
209
- if (match.index > lastIndex) result += code.slice(lastIndex, match.index);
244
+ return depth ? -1 : i - 1;
245
+ };
246
+
247
+ const block = (text, inRule) => {
248
+ const flush = (chunk) => (inRule ? declarations(chunk) : chunk);
249
+ let result = '',
250
+ i = 0,
251
+ pending = 0;
252
+
253
+ while (i < text.length) {
254
+ if (text.startsWith('/*', i)) {
255
+ const end = text.indexOf('*/', i + 2);
256
+ const stop = end === -1 ? text.length : end + 2;
257
+ result += flush(text.slice(pending, i)) + span(CLASS.COMMENT, text.slice(i, stop));
258
+ i = pending = stop;
259
+ continue;
260
+ }
210
261
 
211
- const [, comment, selector, open, props, close] = match;
262
+ // Statement at-rules (@import, @layer a, b;) close on ; with no block
263
+ if (text[i] === ';' && text.slice(pending, i).trim().startsWith('@')) {
264
+ const statement = text.slice(pending, i);
265
+ result +=
266
+ statement.match(/^(\s*)/)[1] +
267
+ span(CLASS.CSS_SELECTOR, statement.trim()) +
268
+ span(CLASS.PUNCTUATION, ';');
269
+ i = pending = i + 1;
270
+ continue;
271
+ }
212
272
 
213
- if (comment) {
214
- result += span(CLASS.COMMENT, comment);
215
- } else {
216
- const ws = selector.match(/^(\s*)/)[1];
217
- result +=
218
- ws + span(CLASS.CSS_SELECTOR, selector.trim()) + ' ' + span(CLASS.PUNCTUATION, open);
219
-
220
- const propRegex = /([\w-]+)(\s*:\s*)([^;]+)(;?)/g;
221
- let propMatch,
222
- propLast = 0,
223
- propsResult = '';
224
-
225
- while ((propMatch = propRegex.exec(props)) !== null) {
226
- if (propMatch.index > propLast) propsResult += props.slice(propLast, propMatch.index);
227
- const [, prop, colon, value, semi] = propMatch;
228
- propsResult += span(CLASS.CSS_PROPERTY, prop) + span(CLASS.PUNCTUATION, colon);
229
- const highlighted = value.replace(/@\[([^\]]+)\]/g, (_, expr) => {
230
- const h = expr.replace(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g, (v) =>
231
- span(CLASS.VIBE_VARIABLE, v),
232
- );
233
- return `${span(CLASS.VIBE_COLOR, '@[')}${h}${span(CLASS.VIBE_COLOR, ']')}`;
234
- });
235
- propsResult += span(CLASS.CSS_VALUE, highlighted);
236
- if (semi) propsResult += span(CLASS.PUNCTUATION, semi);
237
- propLast = propRegex.lastIndex;
273
+ if (text[i] === '{') {
274
+ const close = closingBrace(text, i);
275
+ const prelude = text.slice(pending, i);
276
+ const ws = prelude.match(/^(\s*)/)[1];
277
+ const name = prelude.trim();
278
+
279
+ result +=
280
+ ws +
281
+ (name ? span(CLASS.CSS_SELECTOR, name) + ' ' : '') +
282
+ span(CLASS.PUNCTUATION, '{') +
283
+ block(text.slice(i + 1, close === -1 ? text.length : close), true) +
284
+ (close === -1 ? '' : span(CLASS.PUNCTUATION, '}'));
285
+
286
+ i = pending = close === -1 ? text.length : close + 1;
287
+ continue;
238
288
  }
239
- if (propLast < props.length) propsResult += props.slice(propLast);
240
289
 
241
- result += propsResult + span(CLASS.PUNCTUATION, close);
290
+ i++;
242
291
  }
243
- lastIndex = ruleRegex.lastIndex;
244
- }
245
292
 
246
- if (lastIndex < code.length) result += code.slice(lastIndex);
247
- return result;
293
+ return result + flush(text.slice(pending));
294
+ };
295
+
296
+ return block(code, false);
248
297
  };
249
298
 
250
299
  // JSON highlighter (receives HTML-escaped code)
@@ -310,6 +359,54 @@ export const highlightJSOnly = (html, options = {}) => {
310
359
  );
311
360
  };
312
361
 
362
+ // Bare CSS rules sitting outside any markup, handed to highlightCSS one whole
363
+ // brace-balanced block at a time. Markup inside a candidate disqualifies it —
364
+ // that is HTML, already highlighted.
365
+ const bareCSS = (text) => {
366
+ let result = '',
367
+ i = 0,
368
+ done = 0;
369
+
370
+ while (i < text.length) {
371
+ if (text[i] !== '{') {
372
+ i++;
373
+ continue;
374
+ }
375
+
376
+ let start = i;
377
+ while (start > done && !'<>{}'.includes(text[start - 1])) start--;
378
+
379
+ let depth = 0,
380
+ j = i,
381
+ close = -1;
382
+
383
+ while (j < text.length) {
384
+ const c = text[j];
385
+ if (c === '<' || c === '>') break;
386
+ if (c === '{') depth++;
387
+ else if (c === '}' && !--depth) {
388
+ close = j;
389
+ break;
390
+ }
391
+ j++;
392
+ }
393
+
394
+ if (start === i || close === -1) {
395
+ i++;
396
+ continue;
397
+ }
398
+
399
+ const candidate = text.slice(start, close + 1);
400
+ if (/[\w-]+\s*:\s*/.test(candidate)) {
401
+ result += text.slice(done, start) + highlightCSS(candidate);
402
+ done = close + 1;
403
+ }
404
+ i = close + 1;
405
+ }
406
+
407
+ return result + text.slice(done);
408
+ };
409
+
313
410
  // HTML highlighter - main entry point
314
411
  export const highlightHTML = (
315
412
  html,
@@ -416,10 +513,7 @@ export const highlightHTML = (
416
513
  );
417
514
 
418
515
  // CSS rules outside <style> tags (bare CSS blocks in mixed HTML+CSS snippets)
419
- highlighted = highlighted.replace(
420
- /([^<>{}]+\{[^<>{}]*\})/g,
421
- (match) => /[\w-]+\s*:\s*/.test(match) ? highlightCSS(match) : match,
422
- );
516
+ highlighted = bareCSS(highlighted);
423
517
  }
424
518
 
425
519
  // Remaining @[...] patterns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ape-egg/codie",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Modular code display and editing package",
5
5
  "type": "module",
6
6
  "main": "codie.js",