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