From c720cf14c6b7a19c173b217ec509c21bab361ecb Mon Sep 17 00:00:00 2001 From: HushBugger Date: Wed, 25 Mar 2026 17:59:39 +0100 Subject: [PATCH 1/4] Use `DOMContentLoaded` listener instead of trailing script tag --- static/script.js | 8 ++++---- templates/script_page.html | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/static/script.js b/static/script.js index 6125af81..7412aac8 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,6 +24,8 @@ highlightHash(); + window.addEventListener("hashchange", highlightHash); + const elements = [ ...document.getElementsByClassName("code-line"), ]; @@ -66,6 +68,4 @@ node.replaceWith(replacement); } } - - window.addEventListener("hashchange", highlightHash); -})(); +}); diff --git a/templates/script_page.html b/templates/script_page.html index e534cc22..95c90afc 100644 --- a/templates/script_page.html +++ b/templates/script_page.html @@ -37,7 +37,5 @@

{{ script_name }}

{{ footer }}

{% endif %} - - From 5beabdf1b3bd4a70b99e60a5059036630339784b Mon Sep 17 00:00:00 2001 From: HushBugger Date: Wed, 25 Mar 2026 21:06:06 +0100 Subject: [PATCH 2/4] Add pure-CSS fallback for selected line This works even if JS is disabled or the page is still loading. --- static/script.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } From 10fbdfc701f1f95ceae6a6c58e29ff0e531ab86b Mon Sep 17 00:00:00 2001 From: HushBugger Date: Wed, 25 Mar 2026 18:37:21 +0100 Subject: [PATCH 3/4] Escape dangerous HTML characters in GML code --- script.py | 10 +++++++++- templates/highlight/text.html | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/script.py b/script.py index 56bce8af..b9abc707 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 diff --git a/templates/highlight/text.html b/templates/highlight/text.html index bf6d77aa..781fcbd0 100644 --- a/templates/highlight/text.html +++ b/templates/highlight/text.html @@ -1 +1 @@ -
{{ parsed_text | safe }}
{{ before_var }}{{ variable }}{{ after_var }}
+
{{ parsed_text | safe }}
{{ before_var }}{{ variable | safe }}{{ after_var }}
From 3c317d923713d328029c8f9ec70240b274ec580e Mon Sep 17 00:00:00 2001 From: HushBugger Date: Wed, 25 Mar 2026 20:57:40 +0100 Subject: [PATCH 4/4] Optimize syntax highlighting on script pages --- script.py | 2 +- static/script-highlighter.js | 61 ++++++++++++++++++++++++++++ static/script.js | 66 ++++++++++++------------------- templates/highlight/alarm.html | 4 +- templates/highlight/function.html | 4 +- templates/highlight/text.html | 2 +- templates/index.html | 2 + templates/partials/head.html | 2 - templates/script_page.html | 2 +- 9 files changed, 95 insertions(+), 50 deletions(-) create mode 100644 static/script-highlighter.js diff --git a/script.py b/script.py index b9abc707..f8df625a 100755 --- a/script.py +++ b/script.py @@ -288,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.js b/static/script.js index 7412aac8..932139d6 100644 --- a/static/script.js +++ b/static/script.js @@ -26,46 +26,30 @@ window.addEventListener('DOMContentLoaded', function() { window.addEventListener("hashchange", 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)); - } - } - - 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); - } - } + 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 }}