馃悕馃悕馃悕
at main 286 lines 8.2 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-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