@ape-egg/codie 0.1.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.
- package/README.md +232 -0
- package/codie.css +7 -0
- package/codie.js +252 -0
- package/constants.js +228 -0
- package/editable.js +196 -0
- package/foldable.js +185 -0
- package/format.js +195 -0
- package/highlight-css.css +14 -0
- package/highlight-html.css +41 -0
- package/highlight-js.css +25 -0
- package/highlight-json.css +21 -0
- package/highlight.js +423 -0
- package/keyboard.js +61 -0
- package/numberRows.js +51 -0
- package/package.json +29 -0
- package/structure.css +194 -0
- package/theme.css +102 -0
package/highlight.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// Codie syntax highlighting engine
|
|
2
|
+
import { CLASS, JS_KEYWORDS, JS_IMPORT_KEYWORDS, JS_BRACKETS } from './constants.js';
|
|
3
|
+
|
|
4
|
+
// HTML entity escaping
|
|
5
|
+
const escapeHTML = (str) =>
|
|
6
|
+
str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
7
|
+
|
|
8
|
+
// Wrap in span with class
|
|
9
|
+
const span = (cls, content) => `<span class="${cls}">${content}</span>`;
|
|
10
|
+
|
|
11
|
+
// Highlight @[expression] - vibe brackets + variable names
|
|
12
|
+
const highlightBindings = (text) =>
|
|
13
|
+
text.replace(/@\[([^\]]+)\]/g, (_, expr) => {
|
|
14
|
+
const highlighted = expr.replace(
|
|
15
|
+
/("[^&]*(?:&(?!quot;)[^&]*)*")|('[^']*')|("[^"]*")|(\b[a-zA-Z_][a-zA-Z0-9_]*\b)/g,
|
|
16
|
+
(m, escaped, single, double, ident) => {
|
|
17
|
+
if (escaped || single || double) return m;
|
|
18
|
+
return span(CLASS.VIBE_VARIABLE, ident);
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
return `${span(CLASS.VIBE_COLOR, '@[')}${highlighted}${span(CLASS.VIBE_COLOR, ']')}`;
|
|
22
|
+
});
|
|
23
|
+
|
|
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
|
+
// Highlight variable names in Vibe comments
|
|
32
|
+
const highlightCommentVariables = (content) => {
|
|
33
|
+
let result = content.replace(
|
|
34
|
+
/^(\s*(?:if|each)\s+)([a-zA-Z_][a-zA-Z0-9_.]*)/,
|
|
35
|
+
(_, prefix, varName) => `${prefix}${span(CLASS.VIBE_VARIABLE, varName)}`,
|
|
36
|
+
);
|
|
37
|
+
return result.replace(
|
|
38
|
+
/(\s+as\s+)([a-zA-Z_][a-zA-Z0-9_]*)/,
|
|
39
|
+
(_, as, alias) => `${as}${span(CLASS.VIBE_VARIABLE, alias)}`,
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// JavaScript highlighter for escaped HTML content
|
|
44
|
+
export const highlightJS = (code, options = {}) => {
|
|
45
|
+
const tokens = [];
|
|
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;
|
|
48
|
+
let match,
|
|
49
|
+
lastIndex = 0;
|
|
50
|
+
|
|
51
|
+
while ((match = tokenRegex.exec(code)) !== null) {
|
|
52
|
+
if (match.index > lastIndex) tokens.push(code.slice(lastIndex, match.index));
|
|
53
|
+
|
|
54
|
+
const [
|
|
55
|
+
full,
|
|
56
|
+
vibeBinding,
|
|
57
|
+
vibeExpr,
|
|
58
|
+
dblStr,
|
|
59
|
+
sglStr,
|
|
60
|
+
tmplStr,
|
|
61
|
+
lineComment,
|
|
62
|
+
blockComment,
|
|
63
|
+
stateAccess,
|
|
64
|
+
stateVar,
|
|
65
|
+
number,
|
|
66
|
+
word,
|
|
67
|
+
punct,
|
|
68
|
+
] = match;
|
|
69
|
+
|
|
70
|
+
if (vibeBinding) {
|
|
71
|
+
const highlighted = vibeExpr.replace(
|
|
72
|
+
/('[^']*')|("[^"]*")|(\b[a-zA-Z_][a-zA-Z0-9_]*\b)/g,
|
|
73
|
+
(m, s1, s2, ident) => (s1 || s2 ? m : span(CLASS.VIBE_VARIABLE, ident)),
|
|
74
|
+
);
|
|
75
|
+
tokens.push(`${span(CLASS.VIBE_COLOR, '@[')}${highlighted}${span(CLASS.VIBE_COLOR, ']')}`);
|
|
76
|
+
} else if (dblStr || sglStr || tmplStr) {
|
|
77
|
+
const str = dblStr || sglStr || tmplStr;
|
|
78
|
+
const highlighted = str.replace(/@\[([^\]]+)\]/g, (_, expr) => {
|
|
79
|
+
const h = expr.replace(
|
|
80
|
+
/('[^']*')|("[^"]*")|(\b[a-zA-Z_][a-zA-Z0-9_]*\b)/g,
|
|
81
|
+
(m, s1, s2, ident) => (s1 || s2 ? m : span(CLASS.VIBE_VARIABLE, ident)),
|
|
82
|
+
);
|
|
83
|
+
return `${span(CLASS.VIBE_COLOR, '@[')}${h}${span(CLASS.VIBE_COLOR, ']')}`;
|
|
84
|
+
});
|
|
85
|
+
tokens.push(span(CLASS.JS_STRING, highlighted));
|
|
86
|
+
} else if (lineComment || blockComment) {
|
|
87
|
+
tokens.push(span(CLASS.COMMENT, full));
|
|
88
|
+
} else if (stateAccess) {
|
|
89
|
+
tokens.push(`${span(CLASS.VIBE_COLOR, '$.')}${span(CLASS.VIBE_VARIABLE, stateVar)}`);
|
|
90
|
+
} else if (number) {
|
|
91
|
+
tokens.push(span(CLASS.JS_NUMBER, full));
|
|
92
|
+
} else if (word) {
|
|
93
|
+
if (JS_IMPORT_KEYWORDS.has(word)) tokens.push(span(CLASS.JS_IMPORT, full));
|
|
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));
|
|
98
|
+
} else if (punct) {
|
|
99
|
+
tokens.push(
|
|
100
|
+
JS_BRACKETS.has(punct) ? span(CLASS.JS_IMPORT, full) : span(CLASS.PUNCTUATION, full),
|
|
101
|
+
);
|
|
102
|
+
} else {
|
|
103
|
+
tokens.push(full);
|
|
104
|
+
}
|
|
105
|
+
lastIndex = tokenRegex.lastIndex;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (lastIndex < code.length) tokens.push(code.slice(lastIndex));
|
|
109
|
+
return tokens.join('');
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// JavaScript highlighter for raw (unescaped) content
|
|
113
|
+
export const highlightJSRaw = (code) => {
|
|
114
|
+
const tokens = [];
|
|
115
|
+
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;
|
|
117
|
+
let match,
|
|
118
|
+
lastIndex = 0;
|
|
119
|
+
|
|
120
|
+
while ((match = tokenRegex.exec(code)) !== null) {
|
|
121
|
+
if (match.index > lastIndex) tokens.push(escapeHTML(code.slice(lastIndex, match.index)));
|
|
122
|
+
|
|
123
|
+
const [
|
|
124
|
+
full,
|
|
125
|
+
htmlTag,
|
|
126
|
+
dblStr,
|
|
127
|
+
sglStr,
|
|
128
|
+
tmplStr,
|
|
129
|
+
lineComment,
|
|
130
|
+
blockComment,
|
|
131
|
+
stateAccess,
|
|
132
|
+
stateVar,
|
|
133
|
+
number,
|
|
134
|
+
word,
|
|
135
|
+
punct,
|
|
136
|
+
] = match;
|
|
137
|
+
const escaped = escapeHTML(full);
|
|
138
|
+
|
|
139
|
+
if (htmlTag) {
|
|
140
|
+
const tagMatch = full.match(/^(<\/?)([a-zA-Z][a-zA-Z0-9-]*)([\s\S]*?)(>)$/);
|
|
141
|
+
if (tagMatch) {
|
|
142
|
+
const [, open, tagName, attrs, close] = tagMatch;
|
|
143
|
+
let result =
|
|
144
|
+
span(CLASS.PUNCTUATION, escapeHTML(open)) + span(CLASS.TAG, escapeHTML(tagName));
|
|
145
|
+
if (attrs) {
|
|
146
|
+
result += attrs.replace(
|
|
147
|
+
/([a-zA-Z][a-zA-Z0-9-:]*)(=)?(["'])?([^"']*)?(\3)?/g,
|
|
148
|
+
(m, name, eq, q1, val, q2) => {
|
|
149
|
+
if (!name) return escapeHTML(m);
|
|
150
|
+
let r = span(CLASS.ATTR_NAME, escapeHTML(name));
|
|
151
|
+
if (eq) {
|
|
152
|
+
r += span(CLASS.PUNCTUATION, '=');
|
|
153
|
+
if (q1) r += span(CLASS.PUNCTUATION, escapeHTML(q1));
|
|
154
|
+
if (val) r += span(CLASS.ATTR_VALUE, escapeHTML(val));
|
|
155
|
+
if (q2) r += span(CLASS.PUNCTUATION, escapeHTML(q2));
|
|
156
|
+
}
|
|
157
|
+
return r;
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
result += span(CLASS.PUNCTUATION, escapeHTML(close));
|
|
162
|
+
tokens.push(result);
|
|
163
|
+
} else {
|
|
164
|
+
tokens.push(escaped);
|
|
165
|
+
}
|
|
166
|
+
} else if (dblStr || sglStr || tmplStr) {
|
|
167
|
+
tokens.push(span(CLASS.JS_STRING, escaped));
|
|
168
|
+
} else if (lineComment || blockComment) {
|
|
169
|
+
tokens.push(span(CLASS.COMMENT, escaped));
|
|
170
|
+
} else if (stateAccess) {
|
|
171
|
+
tokens.push(
|
|
172
|
+
`${span(CLASS.VIBE_COLOR, '$.')}${span(CLASS.VIBE_VARIABLE, escapeHTML(stateVar))}`,
|
|
173
|
+
);
|
|
174
|
+
} else if (number) {
|
|
175
|
+
tokens.push(span(CLASS.JS_NUMBER, escaped));
|
|
176
|
+
} else if (word) {
|
|
177
|
+
if (JS_IMPORT_KEYWORDS.has(word)) tokens.push(span(CLASS.JS_IMPORT, escaped));
|
|
178
|
+
else if (JS_KEYWORDS.has(word)) tokens.push(span(CLASS.JS_KEYWORD, escaped));
|
|
179
|
+
else if (code.slice(tokenRegex.lastIndex).match(/^\s*\(/))
|
|
180
|
+
tokens.push(span(CLASS.JS_FUNCTION, escaped));
|
|
181
|
+
else tokens.push(span(CLASS.JS_IDENT, escaped));
|
|
182
|
+
} else if (punct) {
|
|
183
|
+
tokens.push(
|
|
184
|
+
JS_BRACKETS.has(punct) ? span(CLASS.JS_IMPORT, escaped) : span(CLASS.PUNCTUATION, escaped),
|
|
185
|
+
);
|
|
186
|
+
} else {
|
|
187
|
+
tokens.push(escaped);
|
|
188
|
+
}
|
|
189
|
+
lastIndex = tokenRegex.lastIndex;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (lastIndex < code.length) tokens.push(escapeHTML(code.slice(lastIndex)));
|
|
193
|
+
return tokens.join('');
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// CSS highlighter
|
|
197
|
+
export const highlightCSS = (code, options = {}) => {
|
|
198
|
+
let result = '';
|
|
199
|
+
const ruleRegex = /(\/\*[\s\S]*?\*\/)|([^{}]+)(\{)([^}]*)(\})/g;
|
|
200
|
+
let match,
|
|
201
|
+
lastIndex = 0;
|
|
202
|
+
|
|
203
|
+
while ((match = ruleRegex.exec(code)) !== null) {
|
|
204
|
+
if (match.index > lastIndex) result += code.slice(lastIndex, match.index);
|
|
205
|
+
|
|
206
|
+
const [, comment, selector, open, props, close] = match;
|
|
207
|
+
|
|
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;
|
|
233
|
+
}
|
|
234
|
+
if (propLast < props.length) propsResult += props.slice(propLast);
|
|
235
|
+
|
|
236
|
+
result += propsResult + span(CLASS.PUNCTUATION, close);
|
|
237
|
+
}
|
|
238
|
+
lastIndex = ruleRegex.lastIndex;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (lastIndex < code.length) result += code.slice(lastIndex);
|
|
242
|
+
return result;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// JSON highlighter (receives HTML-escaped code)
|
|
246
|
+
export const highlightJSON = (code) => {
|
|
247
|
+
const tokens = [];
|
|
248
|
+
// Match HTML-escaped strings: "..." instead of "..."
|
|
249
|
+
const tokenRegex =
|
|
250
|
+
/(\/\/[^\n]*)|(\/\*[\s\S]*?\*\/)|("(?:(?!")[^\\]|\\.)*?")(\s*:)|("(?:(?!")[^\\]|\\.)*?")|(true|false|null)\b|(-?\d+\.?\d*(?:[eE][+-]?\d+)?)|([{}[\],:])/g;
|
|
251
|
+
let match,
|
|
252
|
+
lastIndex = 0;
|
|
253
|
+
|
|
254
|
+
while ((match = tokenRegex.exec(code)) !== null) {
|
|
255
|
+
if (match.index > lastIndex) tokens.push(code.slice(lastIndex, match.index));
|
|
256
|
+
|
|
257
|
+
const [full, lineComment, blockComment, keyString, colon, valueString, literal, number, punct] =
|
|
258
|
+
match;
|
|
259
|
+
|
|
260
|
+
if (lineComment || blockComment) {
|
|
261
|
+
tokens.push(span(CLASS.COMMENT, full));
|
|
262
|
+
} else if (keyString) {
|
|
263
|
+
// Property name (key before colon)
|
|
264
|
+
tokens.push(span(CLASS.JSON_KEY, keyString));
|
|
265
|
+
if (colon) tokens.push(span(CLASS.PUNCTUATION, colon.trim()));
|
|
266
|
+
} else if (valueString) {
|
|
267
|
+
// String value
|
|
268
|
+
tokens.push(span(CLASS.JSON_STRING, valueString));
|
|
269
|
+
} else if (literal) {
|
|
270
|
+
// true, false, null
|
|
271
|
+
if (literal === 'null') {
|
|
272
|
+
tokens.push(span(CLASS.JSON_NULL, literal));
|
|
273
|
+
} else {
|
|
274
|
+
tokens.push(span(CLASS.JSON_BOOLEAN, literal));
|
|
275
|
+
}
|
|
276
|
+
} else if (number) {
|
|
277
|
+
tokens.push(span(CLASS.JSON_NUMBER, number));
|
|
278
|
+
} else if (punct) {
|
|
279
|
+
tokens.push(span(CLASS.PUNCTUATION, punct));
|
|
280
|
+
} else {
|
|
281
|
+
tokens.push(full);
|
|
282
|
+
}
|
|
283
|
+
lastIndex = tokenRegex.lastIndex;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (lastIndex < code.length) tokens.push(code.slice(lastIndex));
|
|
287
|
+
return tokens.join('');
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// CSS-only highlighter - only highlights content inside <style> tags
|
|
291
|
+
export const highlightCSSOnly = (html, options = {}) => {
|
|
292
|
+
// Find and highlight only <style> tag contents
|
|
293
|
+
return html.replace(
|
|
294
|
+
/(<style[^&]*>)([\s\S]*?)(<\/style>)/gi,
|
|
295
|
+
(_, open, content, close) => open + highlightCSS(content) + close,
|
|
296
|
+
);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// JS-only highlighter - only highlights content inside <script> tags
|
|
300
|
+
export const highlightJSOnly = (html, options = {}) => {
|
|
301
|
+
// Find and highlight only <script> tag contents
|
|
302
|
+
return html.replace(
|
|
303
|
+
/(<script[^&]*>)([\s\S]*?)(<\/script>)/gi,
|
|
304
|
+
(_, open, content, close) => open + highlightJS(content) + close,
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// HTML highlighter - main entry point
|
|
309
|
+
export const highlightHTML = (
|
|
310
|
+
html,
|
|
311
|
+
{ preserveWhitespace = false, highlightJS: doJS = true, highlightCSS: doCSS = true } = {},
|
|
312
|
+
) => {
|
|
313
|
+
let highlighted = html;
|
|
314
|
+
|
|
315
|
+
// Format Vibe comments if not preserving whitespace
|
|
316
|
+
if (!preserveWhitespace) {
|
|
317
|
+
highlighted = highlighted
|
|
318
|
+
.replace(/([^\s])[ \t]*(<!--\s*(?:if|else|\/if|each|\/each)\b)/g, '$1\n $2')
|
|
319
|
+
.replace(/(<!--\s*(?:if|else|each)\b[^-]*-->)[ \t]*(\S)/g, '$1\n $2');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// HTML comments
|
|
323
|
+
highlighted = highlighted.replace(/<!--([\s\S]*?)-->/g, (_, content) =>
|
|
324
|
+
span(CLASS.COMMENT, `<!--${highlightCommentVariables(content)}-->`),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// HTML tags with attributes
|
|
328
|
+
highlighted = highlighted.replace(
|
|
329
|
+
/(<\/?)([a-zA-Z0-9\-]+)((?:\s+[a-zA-Z0-9\-:]+(?:=(?:"(?:(?!").)*?"|'[^']*'|[^\s>]+))?)*\s*)(\/?\s*>)/gs,
|
|
330
|
+
(fullMatch, open, tag, attrs, close) => {
|
|
331
|
+
let result = span(CLASS.PUNCTUATION, open) + span(CLASS.TAG, tag);
|
|
332
|
+
|
|
333
|
+
if (attrs) {
|
|
334
|
+
if (attrs.trim()) {
|
|
335
|
+
const attrRegex =
|
|
336
|
+
/([a-zA-Z0-9\-:]+)(=(?:"((?:(?!").)*?)"|'([^']*)'|([^\s>]+)))?/gs;
|
|
337
|
+
let attrMatch,
|
|
338
|
+
last = 0;
|
|
339
|
+
|
|
340
|
+
while ((attrMatch = attrRegex.exec(attrs)) !== null) {
|
|
341
|
+
result += attrs.substring(last, attrMatch.index) + span(CLASS.ATTR_NAME, attrMatch[1]);
|
|
342
|
+
|
|
343
|
+
if (attrMatch[2]) {
|
|
344
|
+
result += span(CLASS.PUNCTUATION, '=');
|
|
345
|
+
const name = attrMatch[1].toLowerCase();
|
|
346
|
+
const value = attrMatch[3] ?? attrMatch[4] ?? attrMatch[5] ?? '';
|
|
347
|
+
const quote = attrMatch[3] !== undefined ? '"' : attrMatch[4] ? "'" : '';
|
|
348
|
+
|
|
349
|
+
if (quote) result += span(CLASS.PUNCTUATION, quote);
|
|
350
|
+
if (name.startsWith('on')) {
|
|
351
|
+
result += span(CLASS.VIBE_COLOR, highlightStateAccess(value));
|
|
352
|
+
} else {
|
|
353
|
+
result += span(CLASS.ATTR_VALUE, highlightBindings(value));
|
|
354
|
+
}
|
|
355
|
+
if (quote) result += span(CLASS.PUNCTUATION, quote);
|
|
356
|
+
}
|
|
357
|
+
last = attrRegex.lastIndex;
|
|
358
|
+
}
|
|
359
|
+
const remaining = attrs.substring(last);
|
|
360
|
+
if (remaining) result += remaining;
|
|
361
|
+
} else {
|
|
362
|
+
result += attrs;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return result + span(CLASS.PUNCTUATION, close.trim());
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// JavaScript in <script> tags (if enabled)
|
|
371
|
+
if (doJS) {
|
|
372
|
+
highlighted = highlighted.replace(
|
|
373
|
+
new RegExp(
|
|
374
|
+
`(<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>)`,
|
|
375
|
+
'gi',
|
|
376
|
+
),
|
|
377
|
+
(match, open, content, close) =>
|
|
378
|
+
content.includes(`class="${CLASS.JS_KEYWORD}"`)
|
|
379
|
+
? match
|
|
380
|
+
: open + highlightJS(content) + close,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// JavaScript in <code lang="js">
|
|
384
|
+
highlighted = highlighted.replace(
|
|
385
|
+
new RegExp(
|
|
386
|
+
`(<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>)`,
|
|
387
|
+
'gi',
|
|
388
|
+
),
|
|
389
|
+
(match, open, content, close) =>
|
|
390
|
+
content.includes(`class="${CLASS.JS_KEYWORD}"`)
|
|
391
|
+
? match
|
|
392
|
+
: open + highlightJS(content) + close,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// CSS in <style> tags (if enabled)
|
|
397
|
+
if (doCSS) {
|
|
398
|
+
highlighted = highlighted.replace(
|
|
399
|
+
new RegExp(
|
|
400
|
+
`(<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>)`,
|
|
401
|
+
'gi',
|
|
402
|
+
),
|
|
403
|
+
(match, open, content, close) =>
|
|
404
|
+
content.includes(`class="${CLASS.CSS_SELECTOR}"`)
|
|
405
|
+
? match
|
|
406
|
+
: open + highlightCSS(content) + close,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Remaining @[...] patterns
|
|
411
|
+
highlighted = highlighted.replace(
|
|
412
|
+
new RegExp(`(?<!${CLASS.VIBE_COLOR}">)(?<!${CLASS.JS_STRING}">)@\\[([^\\]]+)\\]`, 'g'),
|
|
413
|
+
(_, expr) => {
|
|
414
|
+
const h = expr.replace(
|
|
415
|
+
/("[^&]*(?:&(?!quot;)[^&]*)*")|('[^']*')|("[^"]*")|(\b[a-zA-Z_][a-zA-Z0-9_]*\b)/g,
|
|
416
|
+
(m, e, s1, s2, ident) => (e || s1 || s2 ? m : span(CLASS.VIBE_VARIABLE, ident)),
|
|
417
|
+
);
|
|
418
|
+
return `${span(CLASS.VIBE_COLOR, '@[')}${h}${span(CLASS.VIBE_COLOR, ']')}`;
|
|
419
|
+
},
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
return highlighted;
|
|
423
|
+
};
|
package/keyboard.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keyboard.js - Key command handler for Codie editor
|
|
3
|
+
*
|
|
4
|
+
* Simple API for registering keyboard shortcuts.
|
|
5
|
+
* Normalizes platform differences (Cmd on Mac, Ctrl elsewhere).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalize key combinations to a consistent string format
|
|
10
|
+
* @param {KeyboardEvent} e - The keyboard event
|
|
11
|
+
* @returns {string} - Normalized key combo like "Cmd+s" or "Cmd+Shift+f"
|
|
12
|
+
*/
|
|
13
|
+
function normalizeKeyCombo(e) {
|
|
14
|
+
const parts = []
|
|
15
|
+
|
|
16
|
+
// Use "Cmd" for both Command (Mac) and Control (Windows/Linux) for consistency
|
|
17
|
+
if (e.metaKey || e.ctrlKey) parts.push('Cmd')
|
|
18
|
+
if (e.altKey) parts.push('Alt')
|
|
19
|
+
if (e.shiftKey) parts.push('Shift')
|
|
20
|
+
|
|
21
|
+
// Add the actual key (lowercase for consistency)
|
|
22
|
+
const key = e.key.toLowerCase()
|
|
23
|
+
if (key !== 'meta' && key !== 'control' && key !== 'alt' && key !== 'shift') {
|
|
24
|
+
parts.push(key)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return parts.join('+')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Attach keyboard shortcuts to a textarea
|
|
32
|
+
* @param {HTMLTextAreaElement} textarea - The textarea element to attach to
|
|
33
|
+
* @param {Object} commands - Map of key combos to handler functions
|
|
34
|
+
* @returns {Function} - Cleanup function to remove the listener
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* keyboard(textarea, {
|
|
38
|
+
* 'Cmd+s': (e, textarea) => {
|
|
39
|
+
* e.preventDefault()
|
|
40
|
+
* formatCode(textarea)
|
|
41
|
+
* },
|
|
42
|
+
* 'Cmd+/': (e, textarea) => {
|
|
43
|
+
* toggleComment(textarea)
|
|
44
|
+
* }
|
|
45
|
+
* })
|
|
46
|
+
*/
|
|
47
|
+
export default function keyboard(textarea, commands) {
|
|
48
|
+
const handler = (e) => {
|
|
49
|
+
const combo = normalizeKeyCombo(e)
|
|
50
|
+
const command = commands[combo]
|
|
51
|
+
|
|
52
|
+
if (command) {
|
|
53
|
+
command(e, textarea)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
textarea.addEventListener('keydown', handler)
|
|
58
|
+
|
|
59
|
+
// Return cleanup function
|
|
60
|
+
return () => textarea.removeEventListener('keydown', handler)
|
|
61
|
+
}
|
package/numberRows.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Codie line numbers
|
|
2
|
+
import { CLASS } from './constants.js'
|
|
3
|
+
|
|
4
|
+
// Count lines in code
|
|
5
|
+
const countLines = code => code.split('\n').length
|
|
6
|
+
|
|
7
|
+
// Generate line numbers HTML
|
|
8
|
+
const generateNumbers = count => {
|
|
9
|
+
let html = ''
|
|
10
|
+
for (let i = 1; i <= count; i++) {
|
|
11
|
+
html += `<span class="${CLASS.LINE}">${i}</span>\n`
|
|
12
|
+
}
|
|
13
|
+
return html.trimEnd()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Initialize line numbers for an element
|
|
17
|
+
export const initNumberRows = (el, code) => {
|
|
18
|
+
const lineCount = countLines(code)
|
|
19
|
+
const container = document.createElement('div')
|
|
20
|
+
container.className = CLASS.LINE_NUMBERS
|
|
21
|
+
container.innerHTML = generateNumbers(lineCount)
|
|
22
|
+
|
|
23
|
+
// Insert at beginning
|
|
24
|
+
el.insertBefore(container, el.firstChild)
|
|
25
|
+
|
|
26
|
+
return container
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Update line numbers when code changes
|
|
30
|
+
export const updateNumberRows = (container, code) => {
|
|
31
|
+
const newCount = countLines(code)
|
|
32
|
+
const currentCount = container.children.length
|
|
33
|
+
|
|
34
|
+
if (newCount === currentCount) return
|
|
35
|
+
|
|
36
|
+
container.innerHTML = generateNumbers(newCount)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Highlight active line number
|
|
40
|
+
export const highlightLineNumber = (container, lineIndex) => {
|
|
41
|
+
// Clear previous highlight
|
|
42
|
+
container.querySelectorAll(`.${CLASS.LINE_HIGHLIGHT}`).forEach(el => {
|
|
43
|
+
el.classList.remove(CLASS.LINE_HIGHLIGHT)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Add highlight to target line
|
|
47
|
+
const lines = container.querySelectorAll(`.${CLASS.LINE}`)
|
|
48
|
+
if (lines[lineIndex]) {
|
|
49
|
+
lines[lineIndex].classList.add(CLASS.LINE_HIGHLIGHT)
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ape-egg/codie",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Modular code display and editing package",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "codie.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./codie.js",
|
|
9
|
+
"./codie.css": "./codie.css",
|
|
10
|
+
"./highlight": "./highlight.js",
|
|
11
|
+
"./format": "./format.js",
|
|
12
|
+
"./constants": "./constants.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"*.js",
|
|
16
|
+
"*.css"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"code",
|
|
20
|
+
"editor",
|
|
21
|
+
"syntax-highlighting",
|
|
22
|
+
"code-display"
|
|
23
|
+
],
|
|
24
|
+
"author": "Kim Korte",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
29
|
+
}
|