Add local Prism highlighting for C64 BASIC and ACME assembly

This commit is contained in:
Christian Johannesen 2026-10-29 21:12:43 +01:00
parent 3dd4e077d2
commit 489918fbf8
7 changed files with 57 additions and 3 deletions

View file

@ -6,6 +6,10 @@ RUN npm ci --omit=dev && mkdir /app/screenshot-cache && chown node:node /app/scr
COPY server.mjs ./server.mjs
COPY screenshot-cache.mjs ./screenshot-cache.mjs
COPY public ./public
RUN mkdir -p public/vendor/prism \
&& cp node_modules/prismjs/components/prism-core.min.js public/vendor/prism/ \
&& cp node_modules/prismjs/components/prism-asm6502.min.js public/vendor/prism/ \
&& cp node_modules/prismjs/LICENSE public/vendor/prism/LICENSE
COPY . /programs
ENV PORT=8080

10
package-lock.json generated
View file

@ -6,6 +6,7 @@
"": {
"name": "c64-program-collection",
"dependencies": {
"prismjs": "1.30.0",
"sharp": "0.35.5"
}
},
@ -528,6 +529,15 @@
"node": ">=8"
}
},
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",

View file

@ -2,5 +2,5 @@
"name": "c64-program-collection",
"private": true,
"type": "module",
"dependencies": { "sharp": "0.35.5" }
"dependencies": { "sharp": "0.35.5", "prismjs": "1.30.0" }
}

View file

@ -207,7 +207,7 @@ async function showSource(program) {
try {
const response = await fetch(`/api/source?path=${encodeURIComponent(program.path)}`, { cache: 'no-store' });
if (!response.ok) throw new Error('The program source could not be loaded.');
sourceCode.querySelector('code').textContent = await response.text();
highlightSource(sourceCode.querySelector('code'), program, await response.text());
sourceStatus.hidden = true;
sourceCode.hidden = false;
} catch (error) {
@ -273,7 +273,7 @@ async function loadInlineSource(program) {
try {
const response = await fetch(`/api/source?path=${encodeURIComponent(program.path)}`, { cache: 'no-store' });
if (!response.ok) throw new Error('The program source could not be loaded.');
listing.querySelector('code').textContent = await response.text();
highlightSource(listing.querySelector('code'), program, await response.text());
message.hidden = true;
listing.hidden = false;
} catch (error) {

View file

@ -90,6 +90,9 @@
<p id="source-status" class="status">Loading source…</p>
<pre id="source-code" hidden><code></code></pre>
</dialog>
<script src="/vendor/prism/prism-core.min.js" data-manual></script>
<script src="/vendor/prism/prism-asm6502.min.js"></script>
<script src="/source-highlighting.js"></script>
<script src="/app.js"></script>
</body>
</html>

View file

@ -0,0 +1,28 @@
/* Prism grammars tailored to the sources in this collection. */
if (window.Prism) {
Prism.languages['c64-basic'] = {
'string': { pattern: /"[^"\r\n]*(?:"|(?=\r?$))/m, greedy: true },
'comment': { pattern: /\bREM[^\r\n]*/i, greedy: true },
'line-number': { pattern: /^\d+/m, alias: 'number' },
'keyword': /\b(?:CLOSE|CLR|CMD|CONT|DATA|DEF|DIM|END|FN|FOR|GET|GOSUB|GOTO|IF|INPUT|LET|LIST|LOAD|NEW|NEXT|ON|OPEN|POKE|PRINT|READ|RESTORE|RETURN|RUN|SAVE|STEP|STOP|SYS|THEN|TO|VERIFY|WAIT)\b/i,
'function': /\b(?:ABS|ASC|ATN|CHR\$|COS|EXP|FRE|INT|LEFT\$|LEN|LOG|MID\$|PEEK|POS|RIGHT\$|RND|SGN|SIN|SPC|SQR|STR\$|TAB|TAN|USR|VAL)(?=\s*\()/i,
'number': /(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:E[+-]?\d+)?/i,
'operator': /[+\-*/^=<>]|\b(?:AND|NOT|OR)\b/i,
'punctuation': /[,;:()]/
};
Prism.languages['acme-6502'] = Prism.languages.extend('asm6502', {
'directive': { pattern: /!\w+\b|\.\w+(?=\s)/, alias: 'property' },
'operator': /[+\-*/=<>#&|^~]/
});
Prism.languages.insertBefore('acme-6502', 'op-code', {
'label': { pattern: /^\s*[a-z_]\w*(?=:)/im, alias: 'function' }
});
}
function highlightSource(element, program, source) {
const language = program.language === 'machine-code' ? 'acme-6502' : 'c64-basic';
element.className = `language-${language}`;
// Assign text before highlighting, so source cannot inject HTML.
element.textContent = source;
if (window.Prism) Prism.highlightElement(element);
}

View file

@ -85,6 +85,15 @@ button:disabled { opacity: .5; cursor: wait; }
.instructions-panel h2, .program-source-panel h2 { margin: 0 0 .85rem; font-size: 1rem; letter-spacing: .08em; text-transform: uppercase; }
.program-source-panel pre { max-height: 600px; margin: 0; padding: 1rem; overflow: auto; border-radius: 6px; background: #0d0928; color: #d8d3f4; font: .85rem/1.55 ui-monospace, SFMono-Regular, Consolas, monospace; white-space: pre; tab-size: 4; }
.program-source-panel pre:focus-visible { outline: 2px solid var(--c64-light-blue); outline-offset: 2px; }
.program-source-panel code, .source-modal code { font: inherit; }
.token.comment { color: #aaa5b8; font-style: italic; }
.token.keyword { color: #c6b4ff; }
.token.string { color: #a6e3ad; }
.token.number { color: #f9c88b; }
.token.function { color: #8bd5ef; }
.token.property { color: #f5bde6; }
.token.variable { color: #eed49f; }
.token.operator, .token.punctuation { color: #cad3f5; }
.control-list { display: grid; gap: .7rem; }
.control-row { display: grid; grid-template-columns: minmax(150px, auto) 1fr; gap: 1rem; align-items: center; color: #d2cef0; }
.key-group { display: flex; flex-wrap: wrap; gap: .35rem; }