Skip to content

Commit 9db25c1

Browse files
HushBuggerKockaAdmiralac
authored andcommitted
Optimize syntax highlighting on script pages
1 parent 580330b commit 9db25c1

File tree

9 files changed

+95
-50
lines changed

9 files changed

+95
-50
lines changed

script.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ def process_line(
288288
flags=re.IGNORECASE,
289289
)
290290

291-
line = f"<code class='code-line language-gml'>{line}</code>"
291+
line = f'<code>{line}</code>'
292292

293293
return line
294294

static/script-highlighter.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
importScripts(
2+
"/static/highlight/highlight.min.js",
3+
"/static/highlight/gml.min.js"
4+
);
5+
6+
// Process the innerHTML of `table.code`. See script.js.
7+
// This code is brittle. If we overhaul the HTML structure it might break.
8+
// It's also not very rigorous about matching tag names and classes.
9+
10+
/** @param {MessageEvent<string>} event */
11+
onmessage = function (event) {
12+
let newHTML = '';
13+
let insideCode = false;
14+
let preHighlightedDepth = 0;
15+
16+
for (const chunk of event.data.split(/(<[^>]+>)/)) {
17+
if (chunk.startsWith("<")) {
18+
// We need to highlight text inside <code> elements.
19+
// We don't nest <code> elements.
20+
if (chunk.startsWith("<code")) {
21+
insideCode = true;
22+
} else if (chunk === "</code>") {
23+
insideCode = false;
24+
}
25+
26+
// We don't want to highlight text inside <span class="skip-highlight">.
27+
// We do nest <span> elements so we need to keep a count of how deep we are.
28+
if (chunk.startsWith("<span") && chunk.includes("skip-highlight")) {
29+
preHighlightedDepth = 1;
30+
} else if (preHighlightedDepth !== 0) {
31+
if (chunk.startsWith("<span")) {
32+
preHighlightedDepth++;
33+
} else if (chunk === "</span>") {
34+
preHighlightedDepth--;
35+
}
36+
}
37+
38+
newHTML += chunk;
39+
continue;
40+
}
41+
42+
if (!insideCode || preHighlightedDepth !== 0) {
43+
newHTML += chunk;
44+
continue;
45+
}
46+
47+
if (chunk.trim() === "") {
48+
newHTML += chunk;
49+
continue;
50+
}
51+
52+
// highlight.js expects text input, not HTML input.
53+
const decoded = chunk.replace(
54+
/&amp;|&lt;|&gt;/g,
55+
(char) => ({"&amp;": "&", "&lt;": "<", "&gt;": ">"})[char]
56+
);
57+
newHTML += hljs.highlight(decoded, { language: "gml" }).value;
58+
}
59+
60+
postMessage(newHTML);
61+
}

static/script.js

Lines changed: 25 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -26,46 +26,30 @@ window.addEventListener('DOMContentLoaded', function() {
2626

2727
window.addEventListener("hashchange", highlightHash);
2828

29-
const elements = [
30-
...document.getElementsByClassName("code-line"),
31-
];
32-
33-
// Unfortunately a standard document tree walker doesn't work here - it misses quite a lot of nodes.
34-
// ¯\_(ツ)_/¯
35-
36-
/** @param {Node} el */
37-
const getTextNodes = (el) => {
38-
const nodes = [];
39-
40-
for (const child of el.childNodes) {
41-
if (
42-
child.nodeType == Node.ELEMENT_NODE &&
43-
child.classList.contains("highlighted")
44-
) continue;
45-
46-
if (child.nodeType == Node.TEXT_NODE) {
47-
nodes.push(child);
48-
} else {
49-
nodes.push(...getTextNodes(child));
50-
}
51-
}
52-
53-
return nodes;
29+
// We want to apply syntax highlighting. We have two problems.
30+
//
31+
// First, we have annotations that we want to preserve and not highlight.
32+
// So we can't just let highlight.js go to town, we have to be careful.
33+
//
34+
// Second, the natural approach of calling replaceWith() on text nodes
35+
// is extremely slow for large pages like gml_GlobalScript_scr_text,
36+
// locking up the browser for ten seconds or more. Firefox spends >80%
37+
// of its runtime just calling replaceWith().
38+
//
39+
// So we process the HTML manually, as text, and set innerHTML a single
40+
// time at the end. This also lets us use a web worker to highlight
41+
// in the background without blocking the main thread. (You can't send
42+
// DOM nodes to web workers.)
43+
//
44+
// See script-highlighter.js for the gory details.
45+
46+
const table = document.querySelector("table.code");
47+
const worker = new Worker("/static/script-highlighter.js");
48+
/** @param {MessageEvent<string>} event */
49+
worker.onmessage = function (event) {
50+
table.innerHTML = event.data;
51+
highlightHash();
52+
worker.terminate();
5453
};
55-
56-
for (const el of elements) {
57-
// Highlighting has to be done super carefully and manually like this, otherwise
58-
// the annotations won't show up and get overwritten by highlight.js.
59-
60-
for (const node of getTextNodes(el)) {
61-
if (node.textContent.trim() == "") continue;
62-
63-
const replacement = document.createElement("code");
64-
65-
replacement.classList.add("highlighted");
66-
replacement.innerHTML = hljs.highlight(node.textContent, { language: "gml" }).value;
67-
68-
node.replaceWith(replacement);
69-
}
70-
}
54+
worker.postMessage(table.innerHTML);
7155
});

templates/highlight/alarm.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<span class="alarmC"><span class="alarmU">{{ before_alarm }}<a href="{{ script_name }}.html" class="alarm">{{ alarm_content }}</a><pre class="code-line inline-code">{{ content_rest }}</pre></span><!--
1+
<span class="alarmC"><span class="alarmU">{{ before_alarm }}<a href="{{ script_name }}.html" class="alarm">{{ alarm_content }}</a><pre class="inline-code">{{ content_rest }}</pre></span><!--
22
-->{% if script_content %}<!--
33
--><span class="alarmA"></span><!--
4-
--><pre class="alarmT language-gml"><!--
4+
--><pre class="alarmT"><!--
55
--><strong><a href="{{ script_name }}.html">{{ script_name }}.gml</a></strong><!--
66
--><br><br><!--
77
-->{{ script_content | safe }}<!--

templates/highlight/function.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
<pre class="inline-code code-line">{{ before_function }}</pre><!--
1+
<pre class="inline-code">{{ before_function }}</pre><!--
22
--><span class="funcC"><!--
33
--><a class="func {% if script_name == 'gml_GlobalScript_scr_debug' %}debug{% endif %}" href="{{ script_name }}.html">{{ function_name }}</a><!--
44
-->{% if script_content %}<!--
5-
--><pre class='funcCode language-gml'><!--
5+
--><pre class='funcCode'><!--
66
--><strong><a href='{{ script_name }}.html'>{{ function_name }}</a></strong><!--
77
--><br><br><!--
88
-->{{ script_content | safe }}<!--

templates/highlight/text.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<div class="langtext"><span class="highlighted">{{ parsed_text | safe }}</span><div class="langvar">{{ before_var }}{{ variable | safe }}{{ after_var }}</div></div>
1+
<div class="langtext"><span class="skip-highlight">{{ parsed_text | safe }}</span><div class="langvar">{{ before_var }}{{ variable | safe }}{{ after_var }}</div></div>

templates/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<title>{{ game }} script viewer</title>
55
{% include 'partials/head.html' %}
66
<link rel="stylesheet" href="/static/index.css" />
7+
<script src="/static/highlight/highlight.min.js"></script>
8+
<script src="/static/highlight/gml.min.js"></script>
79
<script src="/static/search.js"></script>
810
</head>
911
<body>

templates/partials/head.html

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,5 @@
22
<meta name="viewport" content="width=device-width, initial-scale=1">
33
<meta name="robots" content="noindex">
44
<link rel="stylesheet" href="/static/highlight/github-dark.min.css" />
5-
<script src="/static/highlight/highlight.min.js" type="text/javascript"></script>
6-
<script src="/static/highlight/gml.min.js" type="text/javascript"></script>
75
<link rel="stylesheet" href="/static/fonts.css" />
86
<link rel="stylesheet" href="/static/main.css" />

templates/script_page.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ <h2>{{ script_name }}</h2>
2323
</td>
2424

2525
<td>
26-
<pre class="code-line language-gml">{{ line | safe }}</pre>
26+
<pre>{{ line | safe }}</pre>
2727
</td>
2828
</tr>
2929
{% endfor %}

0 commit comments

Comments
 (0)