@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 +1 -1
- package/constants.js +1 -0
- package/format.js +3 -2
- package/highlight-js.css +4 -0
- package/highlight.js +153 -50
- package/package.json +6 -3
- package/structure.css +3 -0
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
|
|
64
|
+
if (modes.js) return highlightJSRaw(code);
|
|
65
65
|
return escaped;
|
|
66
66
|
}
|
|
67
67
|
|
package/constants.js
CHANGED
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-]
|
|
18
|
+
.replace(/ ([\w-]+|@\[[^\]]+\])="(?:true|)"/g, (match, attr) =>
|
|
18
19
|
VALUE_ATTRS.has(attr) ? match : ` ${attr}`
|
|
19
20
|
)
|
|
20
|
-
.replace(/ ([\w-]
|
|
21
|
+
.replace(/ ([\w-]+|@\[[^\]]+\])="false"/g, (match, attr) =>
|
|
21
22
|
VALUE_ATTRS.has(attr) ? match : ''
|
|
22
23
|
)
|
|
23
24
|
|
package/highlight-js.css
CHANGED
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;))*?")|('(?:[^'\\]|\\.)*')|(`(?:[^`\\]|\\.)*`)|(
|
|
47
|
+
/(@\[([^\]]+)\])|("(?:[^&]|&(?!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+[^>]*)?>)|("(?:[^"\\]|\\.)*")|('(?:[^'\\]|\\.)*')|(`(?:[^`\\]|\\.)*`)|(
|
|
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 (
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
290
|
+
i++;
|
|
237
291
|
}
|
|
238
|
-
lastIndex = ruleRegex.lastIndex;
|
|
239
|
-
}
|
|
240
292
|
|
|
241
|
-
|
|
242
|
-
|
|
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, `<!--${highlightCommentVariables(content)}-->`),
|
|
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
|
-
/(<\/?)([a-zA-Z0-9\-]+)((?:\s+[a-zA-Z0-9\-:]
|
|
431
|
+
/(<\/?)([a-zA-Z0-9\-]+)((?:\s+(?:[a-zA-Z0-9\-:]+|@\[[^\]]+\])(?:=(?:"(?:(?!").)*?"|'[^']*'|[^\s>]+))?)*\s*)(\/?\s*>)/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\-:]
|
|
438
|
+
/([a-zA-Z0-9\-:]+|@\[[^\]]+\])(=(?:"((?:(?!").)*?)"|'([^']*)'|([^\s>]+)))?/gs;
|
|
337
439
|
let attrMatch,
|
|
338
440
|
last = 0;
|
|
339
441
|
|
|
340
442
|
while ((attrMatch = attrRegex.exec(attrs)) !== null) {
|
|
341
|
-
result +=
|
|
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
|
|
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.
|
|
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": "
|
|
25
|
-
"license": "
|
|
24
|
+
"author": "kkortes",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
26
29
|
}
|