馃悕馃悕馃悕
at dev 7.9 kB view raw
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