diff --git a/script.py b/script.py index 56bce8af..f8df625a 100755 --- a/script.py +++ b/script.py @@ -35,7 +35,7 @@ def parse_text(text: str) -> str: text, ) # '&': newline - text = re.sub(r'(?', text) + text = re.sub(r'(?', text) # '%': close message ('%%' to close whole writer) text = re.sub( r'(? str: + # Escape dangerous HTML characters. + # This preserves strings like "THE LEGEND OF THIS WORLD.#" + line = re.sub( + r'(&|<)', + lambda matches: {'&': '&', '<': '<'}[matches[1]], + line, + ) + # Highlight localized strings line = re.sub( r'([A-Za-z0-9_]+loc\((?:\d+, )?)"((?:[^"\\]|\\.)+)(", "[a-z0-9_-]+")\)', # noqa: E501 @@ -280,7 +288,7 @@ def process_line( flags=re.IGNORECASE, ) - line = f"{line}" + line = f'{line}' return line diff --git a/static/script-highlighter.js b/static/script-highlighter.js new file mode 100644 index 00000000..c1be26be --- /dev/null +++ b/static/script-highlighter.js @@ -0,0 +1,61 @@ +importScripts( + "/static/highlight/highlight.min.js", + "/static/highlight/gml.min.js" +); + +// Process the innerHTML of `table.code`. See script.js. +// This code is brittle. If we overhaul the HTML structure it might break. +// It's also not very rigorous about matching tag names and classes. + +/** @param {MessageEvent} event */ +onmessage = function (event) { + let newHTML = ''; + let insideCode = false; + let preHighlightedDepth = 0; + + for (const chunk of event.data.split(/(<[^>]+>)/)) { + if (chunk.startsWith("<")) { + // We need to highlight text inside elements. + // We don't nest elements. + if (chunk.startsWith("") { + insideCode = false; + } + + // We don't want to highlight text inside . + // We do nest elements so we need to keep a count of how deep we are. + if (chunk.startsWith("") { + preHighlightedDepth--; + } + } + + newHTML += chunk; + continue; + } + + if (!insideCode || preHighlightedDepth !== 0) { + newHTML += chunk; + continue; + } + + if (chunk.trim() === "") { + newHTML += chunk; + continue; + } + + // highlight.js expects text input, not HTML input. + const decoded = chunk.replace( + /&|<|>/g, + (char) => ({"&": "&", "<": "<", ">": ">"})[char] + ); + newHTML += hljs.highlight(decoded, { language: "gml" }).value; + } + + postMessage(newHTML); +} diff --git a/static/script.css b/static/script.css index 7dd1e5eb..31df08b5 100644 --- a/static/script.css +++ b/static/script.css @@ -2,7 +2,7 @@ pre:not(.funcCode), code, .hljs { font-family: "JetBrains Mono"; } -.code .selected { +.code .selected, .code tr:has(:target) { background-color: var(--selected-line-background-color); } diff --git a/static/script.js b/static/script.js index 6125af81..932139d6 100644 --- a/static/script.js +++ b/static/script.js @@ -1,4 +1,4 @@ -(function() { +window.addEventListener('DOMContentLoaded', function() { 'use strict'; function highlightHash() { const selectedRow = document.querySelector('.code .selected'); @@ -24,48 +24,32 @@ highlightHash(); - const elements = [ - ...document.getElementsByClassName("code-line"), - ]; - - // Unfortunately a standard document tree walker doesn't work here - it misses quite a lot of nodes. - // ¯\_(ツ)_/¯ - - /** @param {Node} el */ - const getTextNodes = (el) => { - const nodes = []; - - for (const child of el.childNodes) { - if ( - child.nodeType == Node.ELEMENT_NODE && - child.classList.contains("highlighted") - ) continue; - - if (child.nodeType == Node.TEXT_NODE) { - nodes.push(child); - } else { - nodes.push(...getTextNodes(child)); - } - } + window.addEventListener("hashchange", highlightHash); - return nodes; + // We want to apply syntax highlighting. We have two problems. + // + // First, we have annotations that we want to preserve and not highlight. + // So we can't just let highlight.js go to town, we have to be careful. + // + // Second, the natural approach of calling replaceWith() on text nodes + // is extremely slow for large pages like gml_GlobalScript_scr_text, + // locking up the browser for ten seconds or more. Firefox spends >80% + // of its runtime just calling replaceWith(). + // + // So we process the HTML manually, as text, and set innerHTML a single + // time at the end. This also lets us use a web worker to highlight + // in the background without blocking the main thread. (You can't send + // DOM nodes to web workers.) + // + // See script-highlighter.js for the gory details. + + const table = document.querySelector("table.code"); + const worker = new Worker("/static/script-highlighter.js"); + /** @param {MessageEvent} event */ + worker.onmessage = function (event) { + table.innerHTML = event.data; + highlightHash(); + worker.terminate(); }; - - for (const el of elements) { - // Highlighting has to be done super carefully and manually like this, otherwise - // the annotations won't show up and get overwritten by highlight.js. - - for (const node of getTextNodes(el)) { - if (node.textContent.trim() == "") continue; - - const replacement = document.createElement("code"); - - replacement.classList.add("highlighted"); - replacement.innerHTML = hljs.highlight(node.textContent, { language: "gml" }).value; - - node.replaceWith(replacement); - } - } - - window.addEventListener("hashchange", highlightHash); -})(); + worker.postMessage(table.innerHTML); +}); diff --git a/templates/highlight/alarm.html b/templates/highlight/alarm.html index 35b09370..74e808b6 100644 --- a/templates/highlight/alarm.html +++ b/templates/highlight/alarm.html @@ -1,7 +1,7 @@ -{{ before_alarm }}{{ alarm_content }}
{{ content_rest }}
{% if script_content %}
{{ script_name }}.gml

{{ script_content | safe }}{{ function_name }}{% if script_content %}
{{ function_name }}

{{ script_content | safe }}