@ape-egg/codie 0.1.2 → 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.
package/codie.js CHANGED
@@ -61,7 +61,7 @@ const highlight = (code, modes, options = {}) => {
61
61
  if (!modes.html) {
62
62
  if (modes.json) return highlightJSON(escaped);
63
63
  if (modes.css) return highlightCSS(escaped);
64
- if (modes.js) return highlightJS(escaped);
64
+ if (modes.js) return highlightJSRaw(code);
65
65
  return escaped;
66
66
  }
67
67
 
package/constants.js CHANGED
@@ -53,6 +53,7 @@ export const CLASS = {
53
53
  CSS_VALUE: 'codie-css-value',
54
54
 
55
55
  // Syntax tokens - JS
56
+ COMMAND: 'codie-command',
56
57
  JS_KEYWORD: 'codie-js-keyword',
57
58
  JS_IMPORT: 'codie-js-import',
58
59
  JS_STRING: 'codie-js-string',
package/format.js CHANGED
@@ -12,12 +12,13 @@ export const dedent = str => {
12
12
 
13
13
  // Clean up boolean attributes for non-value attributes:
14
14
  // attr="" → attr, attr="true" → attr, attr="false" → (removed)
15
+ // Vibe name bindings (@[expr]) count as attribute names too.
15
16
  export const cleanupBooleanAttrs = html =>
16
17
  html
17
- .replace(/ ([\w-]+)="(?:true|)"/g, (match, attr) =>
18
+ .replace(/ ([\w-]+|@\[[^\]]+\])="(?:true|)"/g, (match, attr) =>
18
19
  VALUE_ATTRS.has(attr) ? match : ` ${attr}`
19
20
  )
20
- .replace(/ ([\w-]+)="false"/g, (match, attr) =>
21
+ .replace(/ ([\w-]+|@\[[^\]]+\])="false"/g, (match, attr) =>
21
22
  VALUE_ATTRS.has(attr) ? match : ''
22
23
  )
23
24
 
package/highlight-js.css CHANGED
@@ -1,5 +1,9 @@
1
1
  /* Codie JavaScript syntax highlighting */
2
2
 
3
+ [codie] .codie-command {
4
+ color: var(--codie-js-ident);
5
+ }
6
+
3
7
  [codie] .codie-js-keyword {
4
8
  color: var(--codie-js-keyword);
5
9
  }
package/highlight.js CHANGED
@@ -44,7 +44,7 @@ const highlightCommentVariables = (content) => {
44
44
  export const highlightJS = (code, options = {}) => {
45
45
  const tokens = [];
46
46
  const tokenRegex =
47
- /(@\[([^\]]+)\])|("(?:[^&]|&(?!quot;))*?")|('(?:[^'\\]|\\.)*')|(`(?:[^`\\]|\\.)*`)|(\/\/[^\n]*)|(\/\*[\s\S]*?\*\/)|(\$\.([a-zA-Z_][a-zA-Z0-9_]*))|(\b\d+\.?\d*\b)|(\b[a-zA-Z_][a-zA-Z0-9_]*\b)|([^\s])/g;
47
+ /(@\[([^\]]+)\])|(&quot;(?:[^&]|&(?!quot;))*?&quot;)|('(?:[^'\\]|\\.)*')|(`(?:[^`\\]|\\.)*`)|((?<=^|\s)(?:\/\/|#)[^\n]*)|(\/\*[\s\S]*?\*\/)|(\$\.([a-zA-Z_][a-zA-Z0-9_]*))|(\b\d[\d_]*(?:\.[\d_]+)?\b)|(\b[a-zA-Z_][a-zA-Z0-9_]*\b)|([^\s])/g;
48
48
  let match,
49
49
  lastIndex = 0;
50
50
 
@@ -112,8 +112,10 @@ export const highlightJS = (code, options = {}) => {
112
112
  // JavaScript highlighter for raw (unescaped) content
113
113
  export const highlightJSRaw = (code) => {
114
114
  const tokens = [];
115
+ // A command line: a bare lowercase word then arguments, on one full row — no JS
116
+ // keyword lead, no assignment/call/statement characters, no trailing continuation \
115
117
  const tokenRegex =
116
- /(<\/?[a-zA-Z][a-zA-Z0-9-]*(?:\s+[^>]*)?>)|("(?:[^"\\]|\\.)*")|('(?:[^'\\]|\\.)*')|(`(?:[^`\\]|\\.)*`)|(\/\/[^\n]*)|(\/\*[\s\S]*?\*\/)|(\$\.([a-zA-Z_][a-zA-Z0-9_]*))|(\b\d+\.?\d*\b)|(\b[a-zA-Z_][a-zA-Z0-9_]*\b)|([^\s])/g;
118
+ /(^[ \t]*(?!(?:import|export|const|let|var|return|await|async|function|if|else|for|while|new|typeof|throw)\b)[a-z][\w.-]*[ \t]+[^\n=(){};]*[^\n\\=(){};])$|(<\/?[a-zA-Z][a-zA-Z0-9-]*(?:\s+[^>]*)?>)|("(?:[^"\\]|\\.)*")|('(?:[^'\\]|\\.)*')|(`(?:[^`\\]|\\.)*`)|((?<=^|\s)(?:\/\/|#)[^\n]*)|(\/\*[\s\S]*?\*\/)|(\$\.([a-zA-Z_][a-zA-Z0-9_]*))|(\b\d[\d_]*(?:\.[\d_]+)?\b)|(\b[a-zA-Z_][a-zA-Z0-9_]*\b)|([^\s])/gm;
117
119
  let match,
118
120
  lastIndex = 0;
119
121
 
@@ -122,6 +124,7 @@ export const highlightJSRaw = (code) => {
122
124
 
123
125
  const [
124
126
  full,
127
+ commandLine,
125
128
  htmlTag,
126
129
  dblStr,
127
130
  sglStr,
@@ -136,7 +139,9 @@ export const highlightJSRaw = (code) => {
136
139
  ] = match;
137
140
  const escaped = escapeHTML(full);
138
141
 
139
- if (htmlTag) {
142
+ if (commandLine) {
143
+ tokens.push(span(CLASS.COMMAND, escaped));
144
+ } else if (htmlTag) {
140
145
  const tagMatch = full.match(/^(<\/?)([a-zA-Z][a-zA-Z0-9-]*)([\s\S]*?)(>)$/);
141
146
  if (tagMatch) {
142
147
  const [, open, tagName, attrs, close] = tagMatch;
@@ -193,53 +198,102 @@ export const highlightJSRaw = (code) => {
193
198
  return tokens.join('');
194
199
  };
195
200
 
196
- // 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
197
203
  export const highlightCSS = (code, options = {}) => {
198
- let result = '';
199
- const ruleRegex = /(\/\*[\s\S]*?\*\/)|((?:[^{}\/]|\/(?!\*))+)(\{)([^}]*)(\})/g;
200
- let match,
201
- 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
+ }
202
243
 
203
- while ((match = ruleRegex.exec(code)) !== null) {
204
- 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
+ }
205
261
 
206
- 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
+ }
207
272
 
208
- if (comment) {
209
- result += span(CLASS.COMMENT, comment);
210
- } else {
211
- const ws = selector.match(/^(\s*)/)[1];
212
- result +=
213
- ws + span(CLASS.CSS_SELECTOR, selector.trim()) + ' ' + span(CLASS.PUNCTUATION, open);
214
-
215
- const propRegex = /([\w-]+)(\s*:\s*)([^;]+)(;?)/g;
216
- let propMatch,
217
- propLast = 0,
218
- propsResult = '';
219
-
220
- while ((propMatch = propRegex.exec(props)) !== null) {
221
- if (propMatch.index > propLast) propsResult += props.slice(propLast, propMatch.index);
222
- const [, prop, colon, value, semi] = propMatch;
223
- propsResult += span(CLASS.CSS_PROPERTY, prop) + span(CLASS.PUNCTUATION, colon);
224
- const highlighted = value.replace(/@\[([^\]]+)\]/g, (_, expr) => {
225
- const h = expr.replace(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g, (v) =>
226
- span(CLASS.VIBE_VARIABLE, v),
227
- );
228
- return `${span(CLASS.VIBE_COLOR, '@[')}${h}${span(CLASS.VIBE_COLOR, ']')}`;
229
- });
230
- propsResult += span(CLASS.CSS_VALUE, highlighted);
231
- if (semi) propsResult += span(CLASS.PUNCTUATION, semi);
232
- 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;
233
288
  }
234
- if (propLast < props.length) propsResult += props.slice(propLast);
235
289
 
236
- result += propsResult + span(CLASS.PUNCTUATION, close);
290
+ i++;
237
291
  }
238
- lastIndex = ruleRegex.lastIndex;
239
- }
240
292
 
241
- if (lastIndex < code.length) result += code.slice(lastIndex);
242
- return result;
293
+ return result + flush(text.slice(pending));
294
+ };
295
+
296
+ return block(code, false);
243
297
  };
244
298
 
245
299
  // JSON highlighter (receives HTML-escaped code)
@@ -305,6 +359,54 @@ export const highlightJSOnly = (html, options = {}) => {
305
359
  );
306
360
  };
307
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
+
308
410
  // HTML highlighter - main entry point
309
411
  export const highlightHTML = (
310
412
  html,
@@ -324,21 +426,25 @@ export const highlightHTML = (
324
426
  span(CLASS.COMMENT, `&lt;!--${highlightCommentVariables(content)}--&gt;`),
325
427
  );
326
428
 
327
- // HTML tags with attributes
429
+ // HTML tags with attributes (attribute names can be @[...] name bindings)
328
430
  highlighted = highlighted.replace(
329
- /(&lt;\/?)([a-zA-Z0-9\-]+)((?:\s+[a-zA-Z0-9\-:]+(?:=(?:&quot;(?:(?!&quot;).)*?&quot;|'[^']*'|[^\s&gt;]+))?)*\s*)(\/?\s*&gt;)/gs,
431
+ /(&lt;\/?)([a-zA-Z0-9\-]+)((?:\s+(?:[a-zA-Z0-9\-:]+|@\[[^\]]+\])(?:=(?:&quot;(?:(?!&quot;).)*?&quot;|'[^']*'|[^\s&gt;]+))?)*\s*)(\/?\s*&gt;)/gs,
330
432
  (fullMatch, open, tag, attrs, close) => {
331
433
  let result = span(CLASS.PUNCTUATION, open) + span(CLASS.TAG, tag);
332
434
 
333
435
  if (attrs) {
334
436
  if (attrs.trim()) {
335
437
  const attrRegex =
336
- /([a-zA-Z0-9\-:]+)(=(?:&quot;((?:(?!&quot;).)*?)&quot;|'([^']*)'|([^\s&gt;]+)))?/gs;
438
+ /([a-zA-Z0-9\-:]+|@\[[^\]]+\])(=(?:&quot;((?:(?!&quot;).)*?)&quot;|'([^']*)'|([^\s&gt;]+)))?/gs;
337
439
  let attrMatch,
338
440
  last = 0;
339
441
 
340
442
  while ((attrMatch = attrRegex.exec(attrs)) !== null) {
341
- result += attrs.substring(last, attrMatch.index) + span(CLASS.ATTR_NAME, attrMatch[1]);
443
+ result +=
444
+ attrs.substring(last, attrMatch.index) +
445
+ (attrMatch[1].startsWith('@[')
446
+ ? highlightBindings(attrMatch[1])
447
+ : span(CLASS.ATTR_NAME, attrMatch[1]));
342
448
 
343
449
  if (attrMatch[2]) {
344
450
  result += span(CLASS.PUNCTUATION, '=');
@@ -407,10 +513,7 @@ export const highlightHTML = (
407
513
  );
408
514
 
409
515
  // CSS rules outside <style> tags (bare CSS blocks in mixed HTML+CSS snippets)
410
- highlighted = highlighted.replace(
411
- /([^<>{}]+\{[^<>{}]*\})/g,
412
- (match) => /[\w-]+\s*:\s*/.test(match) ? highlightCSS(match) : match,
413
- );
516
+ highlighted = bareCSS(highlighted);
414
517
  }
415
518
 
416
519
  // Remaining @[...] patterns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ape-egg/codie",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Modular code display and editing package",
5
5
  "type": "module",
6
6
  "main": "codie.js",
@@ -21,6 +21,9 @@
21
21
  "syntax-highlighting",
22
22
  "code-display"
23
23
  ],
24
- "author": "kortes",
25
- "license": "MIT"
24
+ "author": "kkortes",
25
+ "license": "ISC",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ }
26
29
  }
package/structure.css CHANGED
@@ -50,6 +50,9 @@
50
50
  min-height: 100%;
51
51
  border: none;
52
52
  outline: none;
53
+ box-shadow: none;
54
+ border-radius: 0;
55
+ background: none;
53
56
  resize: none;
54
57
  overflow: hidden;
55
58
  color: transparent;