diff --git a/vweb/src/highlighted_line.jsx b/vweb/src/highlighted_line.jsx new file mode 100644 index 0000000..42c91f7 --- /dev/null +++ b/vweb/src/highlighted_line.jsx @@ -0,0 +1,118 @@ +import React from 'react' + +const keywords = { + '.db': 'directive', + '.org': 'directive', + '.equ': 'directive', + '#if': 'macro', + '#unless': 'macro', + '#while': 'macro', + '#until': 'macro', + '#do': 'macro', + '#else': 'macro', + '#end': 'macro' +} + +export default function({ line }) { + let remainder = line.trim(), // The part of the line we haven't processed + current = '', // The characters that aren't part of a recognized token that we're building up + idx = 0, // A counter for unique keys + label_directive = false // Whether this line has a label definition or directive on it + const tokens = [] // The list of spans we're building up + + // Push a span containing the "current" string + const pushCurrent = () => { + if (current !== '') { tokens.push({current}) } + current = '' + } + + // Push a recognized token with a classname + const pushToken = (css, tok) => { + pushCurrent() + tokens.push({tok}) + remainder = remainder.slice(tok.length) + } + + // Consume things from the remainder of the line. The general idea here is that we look at the start of the str + // and see if it's a recognizable token. If not, add a single char to `current` and keep going. If so, then push + // `current`, push that token, and keep going. + // This works but only just: everything we highlight (directives, macros, label _declarations,_ numbers, strings, + // and comments) is recognizable that way and can't be a substring of anything else. We could not, for example, + // recognize label _usages_ this way because we can't tell from the first character that it is one + while (remainder !== '') { + let any = false // Whether any special thing matched + + if (remainder[0] === ';') { // If it's a comment, consume the rest + pushToken('comment', remainder) + remainder = '' + any = true + } + + if (remainder[0] === '"') { // Is it a string? + let [str, newRemainder] = readQuotedString(remainder) + pushToken('string', str) + remainder = newRemainder + any = true + } + + // Try to match number patterns + const num = remainder.match(/^(0x[0-9a-fA-F]+)/) || + remainder.match(/^(0b[10]+)/) || + remainder.match(/^(-?\d+)/) + if (num && num[1]) { // Is it a number? + pushToken('number', num[1]) + any = true + } + + // Any sort of keyword? + Object.keys(keywords).forEach((keyword) => { + if (remainder.startsWith(keyword)) { + pushToken(keywords[keyword], keyword) + label_directive = true + any = true + } + }) + + // Labels! + const lbl = remainder.match(/^([a-zA-Z\d_$]+:)/) + if (lbl && lbl[1]) { + pushToken('label', lbl[1]) + any = true + label_directive = true + } + + if (!any) { // Nothing better matched, eat a char and keep going + current += remainder[0] + remainder = remainder.slice(1) + } + } + + pushCurrent() + + return <>{label_directive ? '' : <>  }{tokens} +} + +// A cheap way to indent lines: if the first token is a macro, directive, or label declaration, indent it two spaces +export function indent(str) { + str = str.trim() + if (str.match(/^#[a-z]+/) || + str.match(/^\.[a-z]+/) || + str.match(/^[a-zA-Z\d]+:/)) { + return str + } else { + return ' ' + str + } +} + +// If the start of this line is a quoted string, return that + the remainder (in an escape-aware way). Otherwise +// just return empty string + the entire line +function readQuotedString(line) { + if (line[0] === '"') { + for (let n = 1; n < line.length; n++) { + if (line[n] === '"' && line[n-1] !== '\\') { + return [line.slice(0, n+1), line.slice(n+1)] + } + } + } + return ['', line] +} \ No newline at end of file diff --git a/vweb/src/snippet_emulator.css b/vweb/src/snippet_emulator.css index a04bafb..105c88c 100644 --- a/vweb/src/snippet_emulator.css +++ b/vweb/src/snippet_emulator.css @@ -52,4 +52,11 @@ body { .snippetEmulator .reset { color: lightcoral } .snippetEmulator .build, .snippetEmulator .edit { color: dodgerblue } .snippetEmulator .highlight { background-color: darkgoldenrod } -.snippetEmulator .src { overflow: scroll } \ No newline at end of file +.snippetEmulator .src { overflow: scroll } + +.snippetEmulator .src .directive { color: darkcyan } +.snippetEmulator .src .macro { color: mediumpurple } +.snippetEmulator .src .number { color: lightskyblue } +.snippetEmulator .src .string { color: darkseagreen } +.snippetEmulator .src .label { color: chocolate } +.snippetEmulator .src .comment { color: grey } \ No newline at end of file diff --git a/vweb/src/snippet_emulator.jsx b/vweb/src/snippet_emulator.jsx index 393433e..3aaf09d 100644 --- a/vweb/src/snippet_emulator.jsx +++ b/vweb/src/snippet_emulator.jsx @@ -1,8 +1,9 @@ import './snippet_emulator.css' -import React, {useState, useCallback, useEffect} from 'react' -import {WasmCPU, assemble_snippet, source_map, NovaForth} from '../pkg/vweb.js' +import React, { useState, useCallback, useEffect } from 'react' +import { WasmCPU, assemble_snippet, source_map } from '../pkg/vweb.js' +import HighlightedLine, { indent } from './highlighted_line' -export default function({ children }) { +export default function({ children, width = 20 }) { // Use this by giving it some source as a body: // // {`.org 0x400 @@ -80,7 +81,7 @@ export default function({ children }) { // On load, clean the source and build the stuff useEffect(() => { const lines = React.Children.toArray(children)[0].split('\n') - const cleaned = lines.map(l => l.trim()).join('\n') + const cleaned = lines.map(indent).join('\n') setSrc(cleaned) rebuild(cleaned) }, [children]) @@ -95,7 +96,7 @@ export default function({ children }) { if (editing) { return ( -
+
{message}
@@ -105,7 +106,7 @@ export default function({ children }) { ) } else { return ( -
+
{message}
@@ -124,7 +125,11 @@ function SourceDisplay({ src, activeLine }) { const lines = src.split('\n') const lineDivs = lines.map((line, i) => { // Loop indices are from 0, activeLine numbers are from 1 - return (
{line || <> }
) + return ( +
+ { || <> } +
+ ) }) return (
{lineDivs}
diff --git a/vweb/src/snippet_emulator_demo.html b/vweb/src/snippet_emulator_demo.html index 06c9ccc..109a7ba 100644 --- a/vweb/src/snippet_emulator_demo.html +++ b/vweb/src/snippet_emulator_demo.html @@ -15,13 +15,15 @@ Quisque tristique diam et purus sagittis, posuere pharetra orci egestas. Praesent imperdiet sapien vel fermentum pulvinar. Nulla ac tortor laoreet, fringilla tellus nec, ullamcorper mi.

-
+
         .org 0x400
-        push 0
-        blah: add 1
-        jmp $-1
-        hlt
+            push 0 ; A comment
+        blah:
+            add 1
+            jmp $-1
+            hlt
+        .db "A string; \"semicolon-y\"\0"
         

diff --git a/vweb/src/snippet_emulator_demo.jsx b/vweb/src/snippet_emulator_demo.jsx index f35b922..3669628 100644 --- a/vweb/src/snippet_emulator_demo.jsx +++ b/vweb/src/snippet_emulator_demo.jsx @@ -6,7 +6,8 @@ import init, { WasmCPU, assemble_snippet } from '../pkg/vweb.js' document.addEventListener('DOMContentLoaded', () => { init().then(() => { document.querySelectorAll('.snippet').forEach((el) => { - createRoot(el).render(React.createElement(SnippetEmulator, { children: el.innerText })) + const props = { children: el.innerText, width: el.getAttribute('data-width') } + createRoot(el).render(React.createElement(SnippetEmulator, props)) }) }) }) \ No newline at end of file