@ape-egg/codie 0.1.7 → 0.1.9
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/README.md +15 -10
- package/codie.js +15 -56
- package/constants.js +14 -0
- package/editable.js +3 -35
- package/foldable.js +2 -2
- package/highlight.js +266 -107
- package/package.json +1 -1
- package/theme.css +7 -7
package/README.md
CHANGED
|
@@ -18,11 +18,11 @@ highlight + format +editable +live output, inspect,
|
|
|
18
18
|
|
|
19
19
|
### Core Features
|
|
20
20
|
|
|
21
|
-
- **Syntax Highlighting**: HTML, CSS, and
|
|
21
|
+
- **Syntax Highlighting**: HTML, CSS, JavaScript, JSON, shell and Python, detected from the code itself
|
|
22
22
|
- **Editable Mode**: Transform static code into a live editor with textarea overlay
|
|
23
23
|
- **Line Numbers**: Optional numbered rows for code reference
|
|
24
24
|
- **Its Own Element**: `<codie>` is the block; `<template codie>` holds HTML the browser must not parse
|
|
25
|
-
- **Language Modes**:
|
|
25
|
+
- **Language Modes**: Pin a language via attributes (`highlight-html`, `highlight-css`, `highlight-js`, `highlight-json`)
|
|
26
26
|
- **Dark/Light Themes**: Toggle with `[dark]` attribute
|
|
27
27
|
- **Runtime State Toggle**: Change features dynamically via `codieRef.editable`, `codieRef.numberedRows`
|
|
28
28
|
|
|
@@ -98,16 +98,19 @@ editor.onEdit = ({ formatted, raw }) => {
|
|
|
98
98
|
### Language-Specific Highlighting
|
|
99
99
|
|
|
100
100
|
```html
|
|
101
|
-
<!--
|
|
101
|
+
<!-- HTML, with its <script>, <style> and on* handlers -->
|
|
102
102
|
<template codie highlight-html>...</template>
|
|
103
103
|
|
|
104
|
-
<!--
|
|
104
|
+
<!-- CSS: stylesheets or bare declaration lists -->
|
|
105
105
|
<codie highlight-css>...</codie>
|
|
106
106
|
|
|
107
|
-
<!--
|
|
107
|
+
<!-- JavaScript -->
|
|
108
108
|
<codie highlight-js>...</codie>
|
|
109
109
|
|
|
110
|
-
<!--
|
|
110
|
+
<!-- JSON -->
|
|
111
|
+
<codie highlight-json>...</codie>
|
|
112
|
+
|
|
113
|
+
<!-- Detected from the code: HTML, CSS, JavaScript, JSON, shell, Python or plain text (default) -->
|
|
111
114
|
<codie>...</codie>
|
|
112
115
|
```
|
|
113
116
|
|
|
@@ -174,9 +177,11 @@ A `<codie>` element is an instance by its tag. The attributes below go on it, or
|
|
|
174
177
|
|
|
175
178
|
- `codie` - Marks an element that is not `<codie>` as an instance, `<template codie>` above all
|
|
176
179
|
- `dehydrate` - Skip initialization (for documentation)
|
|
177
|
-
- `highlight-html` -
|
|
178
|
-
- `highlight-css` -
|
|
179
|
-
- `highlight-js` -
|
|
180
|
+
- `highlight-html` - Highlight as HTML, including embedded `<script>`, `<style>` and `on*` handlers
|
|
181
|
+
- `highlight-css` - Highlight as CSS
|
|
182
|
+
- `highlight-js` - Highlight as JavaScript
|
|
183
|
+
- `highlight-json` - Highlight as JSON
|
|
184
|
+
- none of the above - Detect the language from the code (HTML, CSS, JavaScript, JSON, shell, Python or plain text)
|
|
180
185
|
- `dark` - Apply dark theme
|
|
181
186
|
|
|
182
187
|
### Runtime Attributes (applied by codie)
|
|
@@ -224,7 +229,7 @@ editor.onEdit = ({ formatted }) => {
|
|
|
224
229
|
|
|
225
230
|
### Implemented
|
|
226
231
|
|
|
227
|
-
- ✓ Syntax highlighting (HTML, CSS, JS)
|
|
232
|
+
- ✓ Syntax highlighting (HTML, CSS, JS, JSON, shell, Python) with language detection
|
|
228
233
|
- ✓ Editable mode with textarea overlay
|
|
229
234
|
- ✓ Line numbers
|
|
230
235
|
- ✓ Tab handling (insert spaces, multi-line indent, outdent)
|
package/codie.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// Codie - Modular Code Display & Editing
|
|
2
2
|
import { ATTR, CLASS } from './constants.js';
|
|
3
3
|
import {
|
|
4
|
+
highlight,
|
|
5
|
+
detectLanguage,
|
|
4
6
|
highlightHTML,
|
|
5
7
|
highlightJS,
|
|
6
8
|
highlightCSS,
|
|
7
|
-
highlightJSON,
|
|
8
9
|
highlightJSRaw,
|
|
9
|
-
highlightCSSOnly,
|
|
10
|
-
highlightJSOnly,
|
|
11
10
|
} from './highlight.js';
|
|
12
11
|
import {
|
|
13
12
|
escapeHTML,
|
|
@@ -21,59 +20,19 @@ import { initNumberRows, updateNumberRows } from './numberRows.js';
|
|
|
21
20
|
import { initEditable, destroyEditable } from './editable.js';
|
|
22
21
|
import { initFoldable, destroyFoldable } from './foldable.js';
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const hasJSON = el.hasAttribute(ATTR.HIGHLIGHT_JSON);
|
|
30
|
-
const explicit = hasHTML || hasCSS || hasJS || hasJSON;
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
html: !explicit || hasHTML,
|
|
34
|
-
css: !explicit || hasCSS,
|
|
35
|
-
js: !explicit || hasJS,
|
|
36
|
-
json: !explicit || hasJSON,
|
|
37
|
-
};
|
|
23
|
+
const LANGUAGE_ATTRS = {
|
|
24
|
+
html: ATTR.HIGHLIGHT_HTML,
|
|
25
|
+
json: ATTR.HIGHLIGHT_JSON,
|
|
26
|
+
css: ATTR.HIGHLIGHT_CSS,
|
|
27
|
+
js: ATTR.HIGHLIGHT_JS,
|
|
38
28
|
};
|
|
39
29
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const escaped = escapeHTML(code);
|
|
43
|
-
|
|
44
|
-
// Detect JSON by content: must start with { or [ AND contain quoted key patterns
|
|
45
|
-
const looksLikeJSON = /^\s*[{\[]/.test(code) && /"[^"]*"\s*:/.test(code);
|
|
46
|
-
|
|
47
|
-
// JSON takes priority over HTML when content looks like JSON
|
|
48
|
-
if (modes.json && looksLikeJSON) {
|
|
49
|
-
return highlightJSON(escaped);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (modes.html && !looksLikeJSON) {
|
|
53
|
-
return highlightHTML(escaped, {
|
|
54
|
-
...options,
|
|
55
|
-
highlightJS: modes.js,
|
|
56
|
-
highlightCSS: modes.css,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Standalone mode (no HTML): use direct highlighters, not the *Only variants
|
|
61
|
-
if (!modes.html) {
|
|
62
|
-
if (modes.json) return highlightJSON(escaped);
|
|
63
|
-
if (modes.css) return highlightCSS(escaped);
|
|
64
|
-
if (modes.js) return highlightJSRaw(code);
|
|
65
|
-
return escaped;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
let result = escaped;
|
|
69
|
-
if (modes.js) result = highlightJSOnly(result);
|
|
70
|
-
if (modes.css) result = highlightCSSOnly(result);
|
|
71
|
-
return result;
|
|
72
|
-
};
|
|
30
|
+
const languageOf = (el) =>
|
|
31
|
+
Object.keys(LANGUAGE_ATTRS).find((language) => el.hasAttribute(LANGUAGE_ATTRS[language]));
|
|
73
32
|
|
|
74
33
|
// Toggle a feature on the instance
|
|
75
34
|
const toggleFeature = (instance, feature, enabled) => {
|
|
76
|
-
const { el, display,
|
|
35
|
+
const { el, display, language, _code } = instance;
|
|
77
36
|
|
|
78
37
|
switch (feature) {
|
|
79
38
|
case 'numberedRows':
|
|
@@ -99,7 +58,7 @@ const toggleFeature = (instance, feature, enabled) => {
|
|
|
99
58
|
|
|
100
59
|
case 'foldable':
|
|
101
60
|
if (enabled && !el.hasAttribute(ATTR.FOLDABLE)) {
|
|
102
|
-
initFoldable(el, display,
|
|
61
|
+
initFoldable(el, display, language ?? detectLanguage(_code));
|
|
103
62
|
el.setAttribute(ATTR.FOLDABLE, '');
|
|
104
63
|
} else if (!enabled && el.hasAttribute(ATTR.FOLDABLE)) {
|
|
105
64
|
destroyFoldable(el, display);
|
|
@@ -135,7 +94,7 @@ const codie = (elOrConfig = {}) => {
|
|
|
135
94
|
return null;
|
|
136
95
|
}
|
|
137
96
|
|
|
138
|
-
const
|
|
97
|
+
const language = languageOf(el);
|
|
139
98
|
let raw = el.tagName === 'TEMPLATE' ? el.innerHTML : el.textContent;
|
|
140
99
|
|
|
141
100
|
// Decode HTML entities from template innerHTML
|
|
@@ -149,7 +108,7 @@ const codie = (elOrConfig = {}) => {
|
|
|
149
108
|
// Create display layer
|
|
150
109
|
const display = document.createElement('pre');
|
|
151
110
|
display.className = `${CLASS.CODE_LAYER} ${CLASS.DISPLAY}`;
|
|
152
|
-
display.innerHTML = highlight(code,
|
|
111
|
+
display.innerHTML = highlight(code, language, {
|
|
153
112
|
preserveWhitespace: !config.foldable,
|
|
154
113
|
});
|
|
155
114
|
|
|
@@ -161,7 +120,7 @@ const codie = (elOrConfig = {}) => {
|
|
|
161
120
|
const internal = {
|
|
162
121
|
el,
|
|
163
122
|
display,
|
|
164
|
-
|
|
123
|
+
language,
|
|
165
124
|
_code: code,
|
|
166
125
|
_config: { ...config },
|
|
167
126
|
textarea: null,
|
|
@@ -188,7 +147,7 @@ const codie = (elOrConfig = {}) => {
|
|
|
188
147
|
// Decode HTML entities when setting code (same as initialization)
|
|
189
148
|
const decoded = unescapeHTML(value);
|
|
190
149
|
target._code = decoded;
|
|
191
|
-
target.display.innerHTML = highlight(decoded, target.
|
|
150
|
+
target.display.innerHTML = highlight(decoded, target.language, {
|
|
192
151
|
preserveWhitespace: !target._foldable,
|
|
193
152
|
});
|
|
194
153
|
if (target.lineNumbers) {
|
package/constants.js
CHANGED
|
@@ -227,3 +227,17 @@ export const JS_IMPORT_KEYWORDS = new Set(['import', 'export', 'from'])
|
|
|
227
227
|
|
|
228
228
|
// JavaScript bracket characters
|
|
229
229
|
export const JS_BRACKETS = new Set(['{', '}', '(', ')', '[', ']'])
|
|
230
|
+
|
|
231
|
+
export const PY_KEYWORDS = new Set([
|
|
232
|
+
'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del',
|
|
233
|
+
'elif', 'else', 'except', 'finally', 'for', 'global', 'if', 'in', 'is', 'lambda',
|
|
234
|
+
'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield',
|
|
235
|
+
'None', 'True', 'False', 'self',
|
|
236
|
+
])
|
|
237
|
+
|
|
238
|
+
export const PY_IMPORT_KEYWORDS = new Set(['import', 'from'])
|
|
239
|
+
|
|
240
|
+
export const SHELL_KEYWORDS = new Set([
|
|
241
|
+
'if', 'then', 'else', 'elif', 'fi', 'for', 'in', 'do', 'done', 'case', 'esac',
|
|
242
|
+
'while', 'until', 'select', 'function',
|
|
243
|
+
])
|
package/editable.js
CHANGED
|
@@ -1,42 +1,10 @@
|
|
|
1
1
|
// Codie editable textarea layer
|
|
2
2
|
import { CLASS } from './constants.js'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { highlight } from './highlight.js'
|
|
4
|
+
import { formatDocument, normalizeInlineWhitespace, cleanupBooleanAttrs } from './format.js'
|
|
5
5
|
import { updateNumberRows } from './numberRows.js'
|
|
6
6
|
import keyboard from './keyboard.js'
|
|
7
7
|
|
|
8
|
-
// Highlight code based on modes — same dispatch as codie.js, so an edit
|
|
9
|
-
// re-renders through the same highlighter the initial display used
|
|
10
|
-
const highlight = (code, modes) => {
|
|
11
|
-
const escaped = escapeHTML(code)
|
|
12
|
-
|
|
13
|
-
const looksLikeJSON = /^\s*[{\[]/.test(code) && /"[^"]*"\s*:/.test(code)
|
|
14
|
-
|
|
15
|
-
if (modes.json && looksLikeJSON) {
|
|
16
|
-
return highlightJSON(escaped)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (modes.html && !looksLikeJSON) {
|
|
20
|
-
return highlightHTML(escaped, {
|
|
21
|
-
preserveWhitespace: true,
|
|
22
|
-
highlightJS: modes.js,
|
|
23
|
-
highlightCSS: modes.css,
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!modes.html) {
|
|
28
|
-
if (modes.json) return highlightJSON(escaped)
|
|
29
|
-
if (modes.css) return highlightCSS(escaped)
|
|
30
|
-
if (modes.js) return highlightJSRaw(code)
|
|
31
|
-
return escaped
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
let result = escaped
|
|
35
|
-
if (modes.js) result = highlightJSOnly(result)
|
|
36
|
-
if (modes.css) result = highlightCSSOnly(result)
|
|
37
|
-
return result
|
|
38
|
-
}
|
|
39
|
-
|
|
40
8
|
// Initialize editable textarea
|
|
41
9
|
export const initEditable = (el, instance, config) => {
|
|
42
10
|
const textarea = document.createElement('textarea')
|
|
@@ -59,7 +27,7 @@ export const initEditable = (el, instance, config) => {
|
|
|
59
27
|
const scrollLeft = el.scrollLeft
|
|
60
28
|
|
|
61
29
|
// Update display
|
|
62
|
-
instance.display.innerHTML = highlight(newCode, instance.
|
|
30
|
+
instance.display.innerHTML = highlight(newCode, instance.language, { preserveWhitespace: true })
|
|
63
31
|
|
|
64
32
|
// Restore scroll
|
|
65
33
|
el.scrollTop = scrollTop
|
package/foldable.js
CHANGED
|
@@ -124,9 +124,9 @@ const wrapWithFoldControls = (html, defaultOpenTags = ['html', 'body', 'head'])
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
// Initialize foldable on an element
|
|
127
|
-
export const initFoldable = (el, display,
|
|
127
|
+
export const initFoldable = (el, display, language) => {
|
|
128
128
|
// Only fold HTML content
|
|
129
|
-
if (
|
|
129
|
+
if (language !== 'html') return
|
|
130
130
|
|
|
131
131
|
const html = display.innerHTML
|
|
132
132
|
display.innerHTML = wrapWithFoldControls(html)
|
package/highlight.js
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
// Codie syntax highlighting engine
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import {
|
|
3
|
+
CLASS,
|
|
4
|
+
JS_KEYWORDS,
|
|
5
|
+
JS_IMPORT_KEYWORDS,
|
|
6
|
+
JS_BRACKETS,
|
|
7
|
+
PY_KEYWORDS,
|
|
8
|
+
PY_IMPORT_KEYWORDS,
|
|
9
|
+
SHELL_KEYWORDS,
|
|
10
|
+
} from './constants.js';
|
|
11
|
+
import { escapeHTML } from './format.js';
|
|
7
12
|
|
|
8
13
|
// Wrap in span with class
|
|
9
14
|
const span = (cls, content) => `<span class="${cls}">${content}</span>`;
|
|
10
15
|
|
|
16
|
+
const JS = { keywords: JS_KEYWORDS, imports: JS_IMPORT_KEYWORDS };
|
|
17
|
+
const PYTHON = { keywords: PY_KEYWORDS, imports: PY_IMPORT_KEYWORDS };
|
|
18
|
+
const CALL = /\s*\(/y;
|
|
19
|
+
|
|
20
|
+
const wordClass = (word, code, end, { keywords, imports }) => {
|
|
21
|
+
if (imports.has(word)) return CLASS.JS_IMPORT;
|
|
22
|
+
if (keywords.has(word)) return CLASS.JS_KEYWORD;
|
|
23
|
+
CALL.lastIndex = end;
|
|
24
|
+
return CALL.test(code) ? CLASS.JS_FUNCTION : CLASS.JS_IDENT;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const bracketClass = (punct) => (JS_BRACKETS.has(punct) ? CLASS.JS_IMPORT : CLASS.PUNCTUATION);
|
|
28
|
+
|
|
11
29
|
// Highlight @[expression] - vibe brackets + variable names
|
|
12
30
|
const highlightBindings = (text) =>
|
|
13
31
|
text.replace(/@\[([^\]]+)\]/g, (_, expr) => {
|
|
@@ -21,13 +39,6 @@ const highlightBindings = (text) =>
|
|
|
21
39
|
return `${span(CLASS.VIBE_COLOR, '@[')}${highlighted}${span(CLASS.VIBE_COLOR, ']')}`;
|
|
22
40
|
});
|
|
23
41
|
|
|
24
|
-
// Highlight $.variable - vibe state access
|
|
25
|
-
const highlightStateAccess = (text) =>
|
|
26
|
-
text.replace(
|
|
27
|
-
/\$\.([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
28
|
-
`${span(CLASS.VIBE_COLOR, '$.')}${span(CLASS.VIBE_VARIABLE, '$1')}`,
|
|
29
|
-
);
|
|
30
|
-
|
|
31
42
|
// Highlight variable names in Vibe comments
|
|
32
43
|
const highlightCommentVariables = (content) => {
|
|
33
44
|
let result = content.replace(
|
|
@@ -44,7 +55,7 @@ const highlightCommentVariables = (content) => {
|
|
|
44
55
|
export const highlightJS = (code, options = {}) => {
|
|
45
56
|
const tokens = [];
|
|
46
57
|
const tokenRegex =
|
|
47
|
-
/(@\[([^\]]+)\])|("(?:[^&]|&(?!quot;))*?")|('(?:[^'\\]|\\.)*')|(`(?:[^`\\]|\\.)*`)|((?<=^|\s)(?:\/\/|#)[^\n]*)|(\/\*[\s\S]*?\*\/)|(\$\.([a-zA-Z_][a-zA-Z0-9_]*))|(\b\d[\d_]*(?:\.[\d_]+)?\b)|(\
|
|
58
|
+
/(@\[([^\]]+)\])|("(?:[^&]|&(?!quot;))*?")|('(?:[^'\\]|\\.)*')|(`(?:[^`\\]|\\.)*`)|((?<=^|\s)(?:\/\/|#)[^\n]*)|(\/\*[\s\S]*?\*\/)|(\$\.([a-zA-Z_][a-zA-Z0-9_]*))|(\b\d[\d_]*(?:\.[\d_]+)?\b)|((?<![\w$])[a-zA-Z_$][\w$]*)|([^\s])/g;
|
|
48
59
|
let match,
|
|
49
60
|
lastIndex = 0;
|
|
50
61
|
|
|
@@ -90,15 +101,9 @@ export const highlightJS = (code, options = {}) => {
|
|
|
90
101
|
} else if (number) {
|
|
91
102
|
tokens.push(span(CLASS.JS_NUMBER, full));
|
|
92
103
|
} else if (word) {
|
|
93
|
-
|
|
94
|
-
else if (JS_KEYWORDS.has(word)) tokens.push(span(CLASS.JS_KEYWORD, full));
|
|
95
|
-
else if (code.slice(tokenRegex.lastIndex).match(/^\s*\(/))
|
|
96
|
-
tokens.push(span(CLASS.JS_FUNCTION, full));
|
|
97
|
-
else tokens.push(span(CLASS.JS_IDENT, full));
|
|
104
|
+
tokens.push(span(wordClass(word, code, tokenRegex.lastIndex, JS), full));
|
|
98
105
|
} else if (punct) {
|
|
99
|
-
tokens.push(
|
|
100
|
-
JS_BRACKETS.has(punct) ? span(CLASS.JS_IMPORT, full) : span(CLASS.PUNCTUATION, full),
|
|
101
|
-
);
|
|
106
|
+
tokens.push(span(bracketClass(punct), full));
|
|
102
107
|
} else {
|
|
103
108
|
tokens.push(full);
|
|
104
109
|
}
|
|
@@ -115,7 +120,7 @@ export const highlightJSRaw = (code) => {
|
|
|
115
120
|
// A command line: a bare lowercase word then arguments, on one full row — no JS
|
|
116
121
|
// keyword lead, no assignment/call/statement characters, no trailing continuation \
|
|
117
122
|
const tokenRegex =
|
|
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)|(\
|
|
123
|
+
/(^[ \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)|((?<![\w$])[a-zA-Z_$][\w$]*)|([^\s])/gm;
|
|
119
124
|
let match,
|
|
120
125
|
lastIndex = 0;
|
|
121
126
|
|
|
@@ -179,15 +184,9 @@ export const highlightJSRaw = (code) => {
|
|
|
179
184
|
} else if (number) {
|
|
180
185
|
tokens.push(span(CLASS.JS_NUMBER, escaped));
|
|
181
186
|
} else if (word) {
|
|
182
|
-
|
|
183
|
-
else if (JS_KEYWORDS.has(word)) tokens.push(span(CLASS.JS_KEYWORD, escaped));
|
|
184
|
-
else if (code.slice(tokenRegex.lastIndex).match(/^\s*\(/))
|
|
185
|
-
tokens.push(span(CLASS.JS_FUNCTION, escaped));
|
|
186
|
-
else tokens.push(span(CLASS.JS_IDENT, escaped));
|
|
187
|
+
tokens.push(span(wordClass(word, code, tokenRegex.lastIndex, JS), escaped));
|
|
187
188
|
} else if (punct) {
|
|
188
|
-
tokens.push(
|
|
189
|
-
JS_BRACKETS.has(punct) ? span(CLASS.JS_IMPORT, escaped) : span(CLASS.PUNCTUATION, escaped),
|
|
190
|
-
);
|
|
189
|
+
tokens.push(span(bracketClass(punct), escaped));
|
|
191
190
|
} else {
|
|
192
191
|
tokens.push(escaped);
|
|
193
192
|
}
|
|
@@ -201,6 +200,9 @@ export const highlightJSRaw = (code) => {
|
|
|
201
200
|
// CSS highlighter - walks brace depth so nested rules (@supports, @media, and
|
|
202
201
|
// & selectors) tokenize at every level, not just the innermost block
|
|
203
202
|
export const highlightCSS = (code, options = {}) => {
|
|
203
|
+
const customProperties = (text) =>
|
|
204
|
+
text.replace(/--[\w-]+/g, (name) => span(CLASS.CSS_PROPERTY, name));
|
|
205
|
+
|
|
204
206
|
const declarations = (text) => {
|
|
205
207
|
const propRegex = /([\w-]+)(\s*:\s*)([^;]+)(;?)/g;
|
|
206
208
|
let match,
|
|
@@ -208,7 +210,7 @@ export const highlightCSS = (code, options = {}) => {
|
|
|
208
210
|
result = '';
|
|
209
211
|
|
|
210
212
|
while ((match = propRegex.exec(text)) !== null) {
|
|
211
|
-
if (match.index > last) result += text.slice(last, match.index);
|
|
213
|
+
if (match.index > last) result += customProperties(text.slice(last, match.index));
|
|
212
214
|
const [, prop, colon, value, semi] = match;
|
|
213
215
|
result += span(CLASS.CSS_PROPERTY, prop) + span(CLASS.PUNCTUATION, colon);
|
|
214
216
|
const highlighted = value.replace(/@\[([^\]]+)\]/g, (_, expr) => {
|
|
@@ -221,7 +223,7 @@ export const highlightCSS = (code, options = {}) => {
|
|
|
221
223
|
if (semi) result += span(CLASS.PUNCTUATION, semi);
|
|
222
224
|
last = propRegex.lastIndex;
|
|
223
225
|
}
|
|
224
|
-
if (last < text.length) result += text.slice(last);
|
|
226
|
+
if (last < text.length) result += customProperties(text.slice(last));
|
|
225
227
|
return result;
|
|
226
228
|
};
|
|
227
229
|
|
|
@@ -244,8 +246,7 @@ export const highlightCSS = (code, options = {}) => {
|
|
|
244
246
|
return depth ? -1 : i - 1;
|
|
245
247
|
};
|
|
246
248
|
|
|
247
|
-
const block = (text
|
|
248
|
-
const flush = (chunk) => (inRule ? declarations(chunk) : chunk);
|
|
249
|
+
const block = (text) => {
|
|
249
250
|
let result = '',
|
|
250
251
|
i = 0,
|
|
251
252
|
pending = 0;
|
|
@@ -254,7 +255,7 @@ export const highlightCSS = (code, options = {}) => {
|
|
|
254
255
|
if (text.startsWith('/*', i)) {
|
|
255
256
|
const end = text.indexOf('*/', i + 2);
|
|
256
257
|
const stop = end === -1 ? text.length : end + 2;
|
|
257
|
-
result +=
|
|
258
|
+
result += declarations(text.slice(pending, i)) + span(CLASS.COMMENT, text.slice(i, stop));
|
|
258
259
|
i = pending = stop;
|
|
259
260
|
continue;
|
|
260
261
|
}
|
|
@@ -272,15 +273,14 @@ export const highlightCSS = (code, options = {}) => {
|
|
|
272
273
|
|
|
273
274
|
if (text[i] === '{') {
|
|
274
275
|
const close = closingBrace(text, i);
|
|
275
|
-
const
|
|
276
|
-
const ws = prelude.match(/^(\s*)/)[1];
|
|
277
|
-
const name = prelude.trim();
|
|
276
|
+
const [, ws, name, gap] = text.slice(pending, i).match(/^(\s*)([\s\S]*?)(\s*)$/);
|
|
278
277
|
|
|
279
278
|
result +=
|
|
280
279
|
ws +
|
|
281
|
-
(name ? span(CLASS.CSS_SELECTOR, name)
|
|
280
|
+
(name ? span(CLASS.CSS_SELECTOR, name) : '') +
|
|
281
|
+
gap +
|
|
282
282
|
span(CLASS.PUNCTUATION, '{') +
|
|
283
|
-
block(text.slice(i + 1, close === -1 ? text.length : close)
|
|
283
|
+
block(text.slice(i + 1, close === -1 ? text.length : close)) +
|
|
284
284
|
(close === -1 ? '' : span(CLASS.PUNCTUATION, '}'));
|
|
285
285
|
|
|
286
286
|
i = pending = close === -1 ? text.length : close + 1;
|
|
@@ -290,10 +290,10 @@ export const highlightCSS = (code, options = {}) => {
|
|
|
290
290
|
i++;
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
return result +
|
|
293
|
+
return result + declarations(text.slice(pending));
|
|
294
294
|
};
|
|
295
295
|
|
|
296
|
-
return block(code
|
|
296
|
+
return block(code);
|
|
297
297
|
};
|
|
298
298
|
|
|
299
299
|
// JSON highlighter (receives HTML-escaped code)
|
|
@@ -341,34 +341,22 @@ export const highlightJSON = (code) => {
|
|
|
341
341
|
return tokens.join('');
|
|
342
342
|
};
|
|
343
343
|
|
|
344
|
-
// CSS-only highlighter - only highlights content inside <style> tags
|
|
345
|
-
export const highlightCSSOnly = (html, options = {}) => {
|
|
346
|
-
// Find and highlight only <style> tag contents
|
|
347
|
-
return html.replace(
|
|
348
|
-
/(<style[^&]*>)([\s\S]*?)(<\/style>)/gi,
|
|
349
|
-
(_, open, content, close) => open + highlightCSS(content) + close,
|
|
350
|
-
);
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
// JS-only highlighter - only highlights content inside <script> tags
|
|
354
|
-
export const highlightJSOnly = (html, options = {}) => {
|
|
355
|
-
// Find and highlight only <script> tag contents
|
|
356
|
-
return html.replace(
|
|
357
|
-
/(<script[^&]*>)([\s\S]*?)(<\/script>)/gi,
|
|
358
|
-
(_, open, content, close) => open + highlightJS(content) + close,
|
|
359
|
-
);
|
|
360
|
-
};
|
|
361
|
-
|
|
362
344
|
// Bare CSS rules sitting outside any markup, handed to highlightCSS one whole
|
|
363
345
|
// brace-balanced block at a time. Markup inside a candidate disqualifies it —
|
|
364
346
|
// that is HTML, already highlighted.
|
|
365
347
|
const bareCSS = (text) => {
|
|
366
348
|
let result = '',
|
|
367
349
|
i = 0,
|
|
368
|
-
done = 0
|
|
350
|
+
done = 0,
|
|
351
|
+
nesting = 0;
|
|
369
352
|
|
|
370
353
|
while (i < text.length) {
|
|
371
|
-
if (text[i]
|
|
354
|
+
if (text[i] === '<') {
|
|
355
|
+
nesting += text.startsWith('</', i) ? -1 : 1;
|
|
356
|
+
i = text.indexOf('>', i) + 1;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (text[i] !== '{' || nesting) {
|
|
372
360
|
i++;
|
|
373
361
|
continue;
|
|
374
362
|
}
|
|
@@ -408,10 +396,7 @@ const bareCSS = (text) => {
|
|
|
408
396
|
};
|
|
409
397
|
|
|
410
398
|
// HTML highlighter - main entry point
|
|
411
|
-
export const highlightHTML = (
|
|
412
|
-
html,
|
|
413
|
-
{ preserveWhitespace = false, highlightJS: doJS = true, highlightCSS: doCSS = true } = {},
|
|
414
|
-
) => {
|
|
399
|
+
export const highlightHTML = (html, { preserveWhitespace = false } = {}) => {
|
|
415
400
|
let highlighted = html;
|
|
416
401
|
|
|
417
402
|
// Format Vibe comments if not preserving whitespace
|
|
@@ -428,7 +413,7 @@ export const highlightHTML = (
|
|
|
428
413
|
|
|
429
414
|
// HTML tags with attributes (attribute names can be @[...] name bindings)
|
|
430
415
|
highlighted = highlighted.replace(
|
|
431
|
-
/(<
|
|
416
|
+
/(<[\/!]?)([a-zA-Z][a-zA-Z0-9\-]*)((?:\s+(?:[a-zA-Z0-9\-:]+|@\[[^\]]+\])(?:=(?:"(?:(?!").)*?"|'[^']*'|[^\s>]+))?)*\s*)(\/?\s*>)/gs,
|
|
432
417
|
(fullMatch, open, tag, attrs, close) => {
|
|
433
418
|
let result = span(CLASS.PUNCTUATION, open) + span(CLASS.TAG, tag);
|
|
434
419
|
|
|
@@ -454,7 +439,7 @@ export const highlightHTML = (
|
|
|
454
439
|
|
|
455
440
|
if (quote) result += span(CLASS.PUNCTUATION, quote);
|
|
456
441
|
if (name.startsWith('on')) {
|
|
457
|
-
result +=
|
|
442
|
+
result += highlightJS(value);
|
|
458
443
|
} else {
|
|
459
444
|
result += span(CLASS.ATTR_VALUE, highlightBindings(value));
|
|
460
445
|
}
|
|
@@ -473,48 +458,44 @@ export const highlightHTML = (
|
|
|
473
458
|
},
|
|
474
459
|
);
|
|
475
460
|
|
|
476
|
-
// JavaScript in <script> tags
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
);
|
|
461
|
+
// JavaScript in <script> tags
|
|
462
|
+
highlighted = highlighted.replace(
|
|
463
|
+
new RegExp(
|
|
464
|
+
`(<span class="${CLASS.PUNCTUATION}"><<\\/span><span class="${CLASS.TAG}">script<\\/span>[\\s\\S]*?<span class="${CLASS.PUNCTUATION}">><\\/span>)([\\s\\S]*?)(<span class="${CLASS.PUNCTUATION}"><\\/<\\/span><span class="${CLASS.TAG}">script<\\/span>)`,
|
|
465
|
+
'gi',
|
|
466
|
+
),
|
|
467
|
+
(match, open, content, close) =>
|
|
468
|
+
content.includes(`class="${CLASS.JS_KEYWORD}"`)
|
|
469
|
+
? match
|
|
470
|
+
: open + highlightJS(content) + close,
|
|
471
|
+
);
|
|
488
472
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
473
|
+
// JavaScript in <code lang="js">
|
|
474
|
+
highlighted = highlighted.replace(
|
|
475
|
+
new RegExp(
|
|
476
|
+
`(<span class="${CLASS.PUNCTUATION}"><<\\/span><span class="${CLASS.TAG}">code<\\/span>\\s*<span class="${CLASS.ATTR_NAME}">lang<\\/span><span class="${CLASS.PUNCTUATION}">=<\\/span><span class="${CLASS.PUNCTUATION}">"<\\/span><span class="${CLASS.ATTR_VALUE}">js<\\/span><span class="${CLASS.PUNCTUATION}">"<\\/span><span class="${CLASS.PUNCTUATION}">><\\/span>)([\\s\\S]*?)(<span class="${CLASS.PUNCTUATION}"><\\/<\\/span><span class="${CLASS.TAG}">code<\\/span>)`,
|
|
477
|
+
'gi',
|
|
478
|
+
),
|
|
479
|
+
(match, open, content, close) =>
|
|
480
|
+
content.includes(`class="${CLASS.JS_KEYWORD}"`)
|
|
481
|
+
? match
|
|
482
|
+
: open + highlightJS(content) + close,
|
|
483
|
+
);
|
|
501
484
|
|
|
502
|
-
// CSS in <style> tags
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
(
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
);
|
|
485
|
+
// CSS in <style> tags
|
|
486
|
+
highlighted = highlighted.replace(
|
|
487
|
+
new RegExp(
|
|
488
|
+
`(<span class="${CLASS.PUNCTUATION}"><<\\/span><span class="${CLASS.TAG}">style<\\/span>[\\s\\S]*?<span class="${CLASS.PUNCTUATION}">><\\/span>)([\\s\\S]*?)(<span class="${CLASS.PUNCTUATION}"><\\/<\\/span><span class="${CLASS.TAG}">style<\\/span>)`,
|
|
489
|
+
'gi',
|
|
490
|
+
),
|
|
491
|
+
(match, open, content, close) =>
|
|
492
|
+
content.includes(`class="${CLASS.CSS_SELECTOR}"`)
|
|
493
|
+
? match
|
|
494
|
+
: open + highlightCSS(content) + close,
|
|
495
|
+
);
|
|
514
496
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
497
|
+
// CSS rules outside <style> tags (bare CSS blocks in mixed HTML+CSS snippets)
|
|
498
|
+
highlighted = bareCSS(highlighted);
|
|
518
499
|
|
|
519
500
|
// Remaining @[...] patterns
|
|
520
501
|
highlighted = highlighted.replace(
|
|
@@ -530,3 +511,181 @@ export const highlightHTML = (
|
|
|
530
511
|
|
|
531
512
|
return highlighted;
|
|
532
513
|
};
|
|
514
|
+
|
|
515
|
+
export const highlightPython = (code) =>
|
|
516
|
+
code.replace(
|
|
517
|
+
/(\b[rRbBuUfF]{1,2})?("""[\s\S]*?"""|'''[\s\S]*?'''|"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*')|(#[^\n]*)|(\b\d[\d_]*(?:\.[\d_]*)?(?:[eE][+-]?\d+)?\b)|(\b[a-zA-Z_]\w*\b)|([^\s\w])/g,
|
|
518
|
+
(full, prefix, string, comment, number, word, punct, offset) => {
|
|
519
|
+
const escaped = escapeHTML(full);
|
|
520
|
+
if (string) return span(CLASS.JS_STRING, escaped);
|
|
521
|
+
if (comment) return span(CLASS.COMMENT, escaped);
|
|
522
|
+
if (number) return span(CLASS.JS_NUMBER, escaped);
|
|
523
|
+
if (word) return span(wordClass(word, code, offset + full.length, PYTHON), escaped);
|
|
524
|
+
return span(bracketClass(punct), escaped);
|
|
525
|
+
},
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
export const highlightShell = (code) => {
|
|
529
|
+
let i = 0;
|
|
530
|
+
|
|
531
|
+
const take = (regex) => {
|
|
532
|
+
regex.lastIndex = i;
|
|
533
|
+
const match = regex.exec(code);
|
|
534
|
+
if (match) i = regex.lastIndex;
|
|
535
|
+
return match?.[0];
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const expansion = () => {
|
|
539
|
+
if (code.startsWith('$(', i)) {
|
|
540
|
+
i += 2;
|
|
541
|
+
const inner = commands(')');
|
|
542
|
+
return span(CLASS.PUNCTUATION, '$(') + inner + (take(/\)/y) ? span(CLASS.PUNCTUATION, ')') : '');
|
|
543
|
+
}
|
|
544
|
+
const variable = take(/\$(?:\{[^}\n]*\}|[A-Za-z_]\w*|[\d@*#?$!-])/y);
|
|
545
|
+
return variable && span(CLASS.JS_IDENT, escapeHTML(variable));
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const doubleQuoted = () => {
|
|
549
|
+
let result = '',
|
|
550
|
+
text = code[i++];
|
|
551
|
+
const flush = () => {
|
|
552
|
+
if (text) result += span(CLASS.JS_STRING, escapeHTML(text));
|
|
553
|
+
text = '';
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
while (i < code.length && code[i] !== '"') {
|
|
557
|
+
const piece = code[i] === '$' && expansion();
|
|
558
|
+
if (piece) {
|
|
559
|
+
flush();
|
|
560
|
+
result += piece;
|
|
561
|
+
} else if (code[i] === '\\') {
|
|
562
|
+
text += code.slice(i, i + 2);
|
|
563
|
+
i += 2;
|
|
564
|
+
} else {
|
|
565
|
+
text += code[i++];
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
text += take(/"/y) ?? '';
|
|
569
|
+
flush();
|
|
570
|
+
return result;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const commands = (stop) => {
|
|
574
|
+
let result = '',
|
|
575
|
+
command = true,
|
|
576
|
+
header = null,
|
|
577
|
+
pattern = false;
|
|
578
|
+
|
|
579
|
+
while (i < code.length && !(code[i] === stop && !pattern)) {
|
|
580
|
+
const char = code[i];
|
|
581
|
+
const wordStart = !i || /[\s;&|()`]/.test(code[i - 1]);
|
|
582
|
+
let redirect;
|
|
583
|
+
|
|
584
|
+
if (char === '\n') {
|
|
585
|
+
result += code[i++];
|
|
586
|
+
command = !pattern;
|
|
587
|
+
} else if (/\s/.test(char)) {
|
|
588
|
+
result += take(/[^\S\n]+/y);
|
|
589
|
+
} else if (char === '\\') {
|
|
590
|
+
result += escapeHTML(code.slice(i, i + 2));
|
|
591
|
+
i += 2;
|
|
592
|
+
} else if (char === '#' && wordStart) {
|
|
593
|
+
result += span(CLASS.COMMENT, escapeHTML(take(/[^\n]*/y)));
|
|
594
|
+
} else if (char === "'") {
|
|
595
|
+
result += span(CLASS.JS_STRING, escapeHTML(take(/'[^']*'?/y)));
|
|
596
|
+
if (wordStart) command = false;
|
|
597
|
+
} else if (char === '"') {
|
|
598
|
+
result += doubleQuoted();
|
|
599
|
+
if (wordStart) command = false;
|
|
600
|
+
} else if (char === '$') {
|
|
601
|
+
result += expansion() || code[i++];
|
|
602
|
+
if (wordStart) command = false;
|
|
603
|
+
} else if (char === '`') {
|
|
604
|
+
i++;
|
|
605
|
+
result += span(CLASS.PUNCTUATION, '`') + commands('`');
|
|
606
|
+
if (take(/`/y)) result += span(CLASS.PUNCTUATION, '`');
|
|
607
|
+
} else if (char === '(') {
|
|
608
|
+
i++;
|
|
609
|
+
result += span(CLASS.PUNCTUATION, '(') + commands(')');
|
|
610
|
+
if (take(/\)/y)) result += span(CLASS.PUNCTUATION, ')');
|
|
611
|
+
command = false;
|
|
612
|
+
} else if (char === ')') {
|
|
613
|
+
result += span(CLASS.PUNCTUATION, code[i++]);
|
|
614
|
+
command = pattern;
|
|
615
|
+
pattern = false;
|
|
616
|
+
} else if (/[;&|]/.test(char)) {
|
|
617
|
+
const operator = take(/;;|\|\||&&|[;&|]/y);
|
|
618
|
+
result += span(CLASS.PUNCTUATION, operator);
|
|
619
|
+
if (operator === ';;') pattern = true;
|
|
620
|
+
command = !pattern;
|
|
621
|
+
} else if ((redirect = take(/\d*(?:>>|[<>]&\d*-?|>|<)/y))) {
|
|
622
|
+
result += span(CLASS.PUNCTUATION, escapeHTML(redirect));
|
|
623
|
+
} else {
|
|
624
|
+
const word = take(/[^\s'"$`;&|()<>\\]+/y);
|
|
625
|
+
if (pattern && word === 'esac') {
|
|
626
|
+
result += span(CLASS.JS_KEYWORD, word);
|
|
627
|
+
pattern = false;
|
|
628
|
+
command = false;
|
|
629
|
+
} else if (!wordStart || pattern) {
|
|
630
|
+
result += escapeHTML(word);
|
|
631
|
+
} else if (header && word === 'in') {
|
|
632
|
+
result += span(CLASS.JS_KEYWORD, word);
|
|
633
|
+
pattern = header === 'case';
|
|
634
|
+
header = null;
|
|
635
|
+
} else if (['[', '[[', ']', ']]', '{', '}', '!'].includes(word)) {
|
|
636
|
+
result += span(CLASS.PUNCTUATION, word);
|
|
637
|
+
command = ['{', '}', '!'].includes(word) || (!word.startsWith('[') && command);
|
|
638
|
+
} else if (!command) {
|
|
639
|
+
result += escapeHTML(word);
|
|
640
|
+
} else if (SHELL_KEYWORDS.has(word)) {
|
|
641
|
+
result += span(CLASS.JS_KEYWORD, word);
|
|
642
|
+
header = ['for', 'case', 'select'].includes(word) ? word : null;
|
|
643
|
+
command = !header && !['fi', 'done', 'esac'].includes(word);
|
|
644
|
+
} else if (/^[A-Za-z_]\w*=/.test(word)) {
|
|
645
|
+
const [name, value] = word.split(/=(.*)/s);
|
|
646
|
+
result += span(CLASS.JS_IDENT, name) + span(CLASS.PUNCTUATION, '=') + escapeHTML(value);
|
|
647
|
+
} else {
|
|
648
|
+
result += span(CLASS.JS_FUNCTION, escapeHTML(word));
|
|
649
|
+
command = false;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return result;
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
return commands();
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const LANGUAGE_SIGNALS = {
|
|
661
|
+
python:
|
|
662
|
+
/^\s*(?:from\s+[\w.]+\s+import\s|import\s+[\w.]+(?:\s+as\s+\w+)?(?:\s*,\s*[\w.]+)*\s*$|def\s+\w+\s*\(|class\s+\w+.*:\s*$|(?:if|elif|while|for|with|except)\b.*:\s*(?:#.*)?$|(?:else|try|finally):|print\(|raise\s)/,
|
|
663
|
+
shell:
|
|
664
|
+
/^(?!.*:\s*$)(?!\s*(?:from|import)\s)(?!\s*[\w.[\]]+\s*[-+*/]?=\s)\s*(?:[a-z][\w.-]*(?=[^\n]*\s(?:-{1,2}\w|[\w@~.-]*[./][\w./-]|\$|\||&&|\d?>|["']|\\$))(?:\s+\S+)+|[A-Za-z_]\w*=(?:[^\s,]*|"[^"]*"|'[^']*'|\$\([^)]*\)?)(?:;|\s*$|\s+[a-z])|(?:if|then|fi|for|do|done|case|esac|while|echo|printf|export)\b|\[\s|\((?:cd|[a-z]+\s))/,
|
|
665
|
+
css: /^\s*(?:-{0,2}[a-zA-Z][\w-]*\s*:[^;{]*;|--[\w-]+\s*(?:,|$)|(?:[^{};()=[\]'"]|\[[^\]]*\]|:[\w-]+\([^)]*\))+\{(?:[^{}]*\})?\s*(?:\/\*.*)?$)/,
|
|
666
|
+
js: /^\s*(?:(?:const|let|var)\s+[\w{[$]|(?:async\s+)?function\b|import\b.*\bfrom\s*['"]|import\s*['"{*]|export\s|return\b.*;|\/\/|(?:window|document|console)\.|\$\.\w)|=>|[^;];\s*$/,
|
|
667
|
+
html: /^\s*<[!/a-zA-Z]|<\/[a-zA-Z][\w-]*>|@\[/,
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
export const detectLanguage = (code) => {
|
|
671
|
+
if (/^\s*(?:[{[][\s\S]*"[^"\n]*"\s*:|"[^"\n]*"\s*:)/.test(code)) return 'json';
|
|
672
|
+
if (/^\s*<[!/a-zA-Z]/.test(code)) return 'html';
|
|
673
|
+
|
|
674
|
+
const lines = code.split('\n');
|
|
675
|
+
return Object.entries(LANGUAGE_SIGNALS)
|
|
676
|
+
.map(([language, signal]) => [language, lines.filter((line) => signal.test(line)).length])
|
|
677
|
+
.reduce((best, entry) => (entry[1] > best[1] ? entry : best), ['text', 0])[0];
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const HIGHLIGHTERS = {
|
|
681
|
+
html: (code, options) => highlightHTML(escapeHTML(code), options),
|
|
682
|
+
json: (code) => highlightJSON(escapeHTML(code)),
|
|
683
|
+
css: (code) => highlightCSS(escapeHTML(code)),
|
|
684
|
+
js: highlightJSRaw,
|
|
685
|
+
python: highlightPython,
|
|
686
|
+
shell: highlightShell,
|
|
687
|
+
text: escapeHTML,
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
export const highlight = (code, language, options) =>
|
|
691
|
+
HIGHLIGHTERS[language ?? detectLanguage(code)](code, options);
|
package/package.json
CHANGED
package/theme.css
CHANGED
|
@@ -50,12 +50,12 @@
|
|
|
50
50
|
--codie-highlight: oklch(0.7 0.1 250 / 0.2);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/* Dark theme - VSCode Dark+ */
|
|
53
|
+
/* Dark theme - VSCode Dark+ tokens on Stylecheat's dark surface */
|
|
54
54
|
.dark :where(codie, [codie]),
|
|
55
55
|
[dark] :where(codie, [codie]),
|
|
56
56
|
:where(codie, [codie])[dark] {
|
|
57
|
-
--codie-bg: #
|
|
58
|
-
--codie-text: #
|
|
57
|
+
--codie-bg: #15181d;
|
|
58
|
+
--codie-text: #dfe6f0;
|
|
59
59
|
--codie-caret: #fff;
|
|
60
60
|
--codie-selection: oklch(0.5 0.1 250 / 0.4);
|
|
61
61
|
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
--codie-tag: #569cd6;
|
|
64
64
|
--codie-attr-name: #9cdcfe;
|
|
65
65
|
--codie-attr-value: #ce9178;
|
|
66
|
-
--codie-punct: #
|
|
66
|
+
--codie-punct: #8a94a6;
|
|
67
67
|
--codie-comment: #6a9955;
|
|
68
68
|
|
|
69
69
|
/* CSS tokens */
|
|
@@ -90,11 +90,11 @@
|
|
|
90
90
|
--codie-vibe: #db2777;
|
|
91
91
|
|
|
92
92
|
/* Line numbers */
|
|
93
|
-
--codie-line-number: oklch(0.45 0
|
|
94
|
-
--codie-line-number-active: oklch(0.75 0
|
|
93
|
+
--codie-line-number: oklch(0.45 0.02 264);
|
|
94
|
+
--codie-line-number-active: oklch(0.75 0.02 264);
|
|
95
95
|
|
|
96
96
|
/* Folding */
|
|
97
|
-
--codie-fold-icon: oklch(0.45 0
|
|
97
|
+
--codie-fold-icon: oklch(0.45 0.02 264);
|
|
98
98
|
--codie-fold-hover: oklch(0.4 0.1 250 / 0.3);
|
|
99
99
|
|
|
100
100
|
/* Line highlight */
|