馃悕馃悕馃悕
1
2$css(`/* css */
3 .highlight-container {
4 display: flex;
5 flex-direction: column;
6 height: 100%;
7 width: 100%;
8 background-color: var(--main-background);
9 color: var(--main-solid);
10 font-family: "Consolas", "Monaco", "Courier New", monospace;
11 }
12
13 .highlight-header {
14 height: 2rem;
15 }
16
17 .highlight-toolbar {
18 display: flex;
19 align-items: center;
20 gap: 1rem;
21 padding: 0.5rem 1rem;
22 background-color: var(--main-faded);
23 border-bottom: 1px solid var(--main-border);
24 font-size: 0.9rem;
25 }
26
27 .highlight-content {
28 flex: 1;
29 display: flex;
30 overflow: hidden;
31 position: relative;
32 word-wrap: break-word;
33 white-space: pre-wrap;
34 }
35
36 .highlight-editor {
37 flex: 1;
38 background-color: transparent;
39 color: transparent;
40 caret-color: var(--main-solid);
41 border: none;
42 padding: 1rem;
43 font-family: inherit;
44 font-size: 14px;
45 line-height: 1.4;
46 resize: none;
47 outline: none;
48 tab-size: 4;
49 overflow: scroll;
50 position: absolute;
51 top: 0;
52 left: 0;
53 width: 100%;
54 height: 100%;
55 z-index: 2;
56 }
57
58 .highlight-editor::selection { background-color: rgba(255, 255, 255, 0.2); }
59
60 .highlight-output {
61 flex: 1;
62 background-color: transparent;
63 padding: 1rem;
64 overflow: scroll;
65 font-size: 14px;
66 line-height: 1.4;
67 white-space: pre-wrap;
68 word-wrap: break-word;
69 position: absolute;
70 top: 0;
71 left: 0;
72 right: 0;
73 bottom: 0;
74 pointer-events: none;
75 z-index: 1;
76 }
77
78 /* Syntax highlighting styles */
79 .token-keyword { color: var(--code-keyword); font-weight: bold; }
80 .token-string { color: var(--code-string); }
81 .token-template-literal { color: var(--code-template-literal); }
82 .token-template-expression { color: var(--main-solid); background-color: var(--main-faded); }
83 .token-comment { color: var(--code-comment); font-style: italic; }
84 .token-number { color: var(--code-number); }
85 .token-operator { color: var(--code-operator); }
86 .token-punctuation { color: var(--code-punctuation); }
87 .token-function { color: var(--code-function); }
88 .token-property { color: var(--code-property); }
89 .token-bracket { color: var(--code-bracket); }
90 .token-builtin { color: var(--code-builtin); }
91 .token-regex { color: var(--code-regex); }
92 .token-identifier { color: var(--code-identifier); }
93 .token-whitespace { color: var(--code-whitespace); }
94 .token-unknown { color: var(--code-unknown); }
95`);
96
97const tokenizer_js = await import("/code/code/js_tokenizer.js");
98
99function highlight(code) {
100 const tokens = tokenizer_js.tokenize(code);
101 const fragment = document.createDocumentFragment();
102
103 tokens.forEach(token => {
104 const element = renderToken(token);
105 fragment.appendChild(element);
106 });
107
108 return fragment;
109}
110
111function renderCssToken(token) {
112 if (!token || typeof token.value !== "string") {
113 console.error(token);
114 }
115
116 const tokenTypeMap = {
117 "at-rule": "token-operator",
118 "property": "token-property",
119 "color": "token-color",
120 "number": "token-number",
121 "number-unit": "token-number",
122 "string": "token-string",
123 "url": "token-regex",
124 "function": "token-function",
125 "pseudo-class": "token-css-pseudo",
126 "pseudo-element": "token-css-pseudo",
127 "variable": "token-identifier",
128 "comment": "token-comment",
129 "important": "token-keyword",
130 "operator": "token-operator",
131 "delimiter": "token-punctuation",
132 "identifier": "token-identifier",
133 "unknown": "token-unknown"
134 };
135
136 const span = document.createElement("span");
137 span.textContent = token.value;
138
139 if (token.type === "punctuation") {
140 span.className = "{}[]()".includes(token.value) ? "token-bracket" : "token-punctuation";
141 } else {
142 span.className = tokenTypeMap[token.type] || `token-${token.type}`;
143 }
144
145 return span;
146}
147
148function renderToken(token) {
149 // Language-specific token rendering can be overridden
150 if (token.type === "template-expression") {
151 const span = document.createElement("span");
152 span.className = "token-template-expression";
153
154 // Add the opening ${
155 span.appendChild(document.createTextNode("${"));
156
157 // Extract and highlight the inner expression
158 const inner = token.value.slice(2, -1); // Remove ${ and }
159 const innerHighlighted = highlight(inner);
160 span.appendChild(innerHighlighted);
161
162 // Add the closing }
163 span.appendChild(document.createTextNode("}"));
164
165 return span;
166 }
167
168 if (token.type === "css-string") {
169 const span = document.createElement("span");
170 span.className = "token-string";
171
172 // Extract the CSS content and render it with CSS highlighting
173 const openQuote = token.value[0];
174 const cssMarker = "/* css */";
175 const markerStart = token.value.indexOf(cssMarker);
176 const cssStart = markerStart + cssMarker.length;
177 const cssEnd = token.value.lastIndexOf(openQuote);
178
179 if (markerStart !== -1 && cssEnd > cssStart) {
180 const prefix = token.value.slice(0, cssStart);
181 const suffix = token.value.slice(cssEnd);
182
183 span.appendChild(document.createTextNode(prefix));
184
185 // Render CSS tokens
186 const cssFragment = document.createDocumentFragment();
187 token.cssTokens.forEach(cssToken => {
188 cssFragment.appendChild(renderCssToken(cssToken));
189 });
190 span.appendChild(cssFragment);
191
192 span.appendChild(document.createTextNode(suffix));
193
194 return span;
195 }
196 // Fallback to regular string rendering if parsing fails
197 span.textContent = token.value;
198 return span;
199 }
200
201 const span = document.createElement("span");
202 span.textContent = token.value;
203
204 if (token.type === "punctuation" && "{}[]()".includes(token.value)) {
205 span.className = "token-bracket";
206 } else {
207 span.className = `token-${token.type}`;
208 }
209
210 return span;
211}
212
213export async function main(target, text="") {
214 const container = document.createElement("div");
215 container.className = "highlight-container";
216
217 const header = $div("highlight-header");
218
219 const content = document.createElement("div");
220 content.className = "highlight-content";
221
222 const editor = document.createElement("textarea");
223 editor.className = "highlight-editor";
224 editor.spellcheck = false;
225 editor.placeholder = "...";
226 editor.value = text;
227
228 const preformatted = document.createElement("pre");
229
230 const output = document.createElement("code");
231 output.className = "highlight-output";
232
233 container.$with(
234 header,
235 content.$with(
236 editor,
237 preformatted.$with(output)
238 )
239 );
240
241 function updateHighlight() {
242 const code = editor.value;
243 const highlighted = highlight(code);
244
245 // Clear existing content
246 output.innerHTML = "";
247
248 // Append the highlighted DOM fragment
249 output.appendChild(highlighted);
250
251 if (code.endsWith("\n")) {
252 // zero-width space to preserve trailing newline
253 output.appendChild(document.createTextNode("\u200B"));
254 }
255 }
256
257 editor.addEventListener("input", updateHighlight);
258 editor.addEventListener("scroll", () => {
259 output.scrollTop = editor.scrollTop;
260 output.scrollLeft = editor.scrollLeft;
261 });
262
263 editor.$ = { focusable: true, collapsible: false };
264
265 function exit() {
266 const target = container.parentNode;
267 container.remove();
268 $mod("layout/nothing", target);
269 }
270
271 container.$contextMenu = {
272 items: [
273 ["exit", exit ]
274 ]
275 };
276
277 // Initial highlight
278 updateHighlight();
279
280 target.appendChild(container);
281
282 return {
283 replace: true
284 };
285}
286