Specify width and syntax highlighting and indentation

This commit is contained in:
2023-01-29 23:26:48 -06:00
parent 7e969f1469
commit 78d8457f75
5 changed files with 147 additions and 14 deletions
+118
View File
@@ -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(<span key={`token-${idx++}`}>{current}</span>) }
current = ''
}
// Push a recognized token with a classname
const pushToken = (css, tok) => {
pushCurrent()
tokens.push(<span className={css} key={`token-${idx++}`}>{tok}</span>)
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 ? '' : <>&nbsp;&nbsp;</>}{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]
}
+8 -1
View File
@@ -52,4 +52,11 @@ body {
.snippetEmulator .reset { color: lightcoral } .snippetEmulator .reset { color: lightcoral }
.snippetEmulator .build, .snippetEmulator .edit { color: dodgerblue } .snippetEmulator .build, .snippetEmulator .edit { color: dodgerblue }
.snippetEmulator .highlight { background-color: darkgoldenrod } .snippetEmulator .highlight { background-color: darkgoldenrod }
.snippetEmulator .src { overflow: scroll } .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 }
+12 -7
View File
@@ -1,8 +1,9 @@
import './snippet_emulator.css' import './snippet_emulator.css'
import React, {useState, useCallback, useEffect} from 'react' import React, { useState, useCallback, useEffect } from 'react'
import {WasmCPU, assemble_snippet, source_map, NovaForth} from '../pkg/vweb.js' 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: // Use this by giving it some source as a body:
// <SnippetEmulator> // <SnippetEmulator>
// {`.org 0x400 // {`.org 0x400
@@ -80,7 +81,7 @@ export default function({ children }) {
// On load, clean the source and build the stuff // On load, clean the source and build the stuff
useEffect(() => { useEffect(() => {
const lines = React.Children.toArray(children)[0].split('\n') 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) setSrc(cleaned)
rebuild(cleaned) rebuild(cleaned)
}, [children]) }, [children])
@@ -95,7 +96,7 @@ export default function({ children }) {
if (editing) { if (editing) {
return ( return (
<div className='snippetEmulator'> <div className='snippetEmulator' style={{ gridTemplateColumns: `${width}em` }}>
<textarea value={src} onChange={onChangeSrc}></textarea> <textarea value={src} onChange={onChangeSrc}></textarea>
<div className='message'>{message}</div> <div className='message'>{message}</div>
<div className='buttons'> <div className='buttons'>
@@ -105,7 +106,7 @@ export default function({ children }) {
) )
} else { } else {
return ( return (
<div className='snippetEmulator'> <div className='snippetEmulator' style={{ gridTemplateColumns: `${width}em` }}>
<SourceDisplay src={src} activeLine={activeLine}/> <SourceDisplay src={src} activeLine={activeLine}/>
<div className='message'>{message}</div> <div className='message'>{message}</div>
<div className='buttons'> <div className='buttons'>
@@ -124,7 +125,11 @@ function SourceDisplay({ src, activeLine }) {
const lines = src.split('\n') const lines = src.split('\n')
const lineDivs = lines.map((line, i) => { const lineDivs = lines.map((line, i) => {
// Loop indices are from 0, activeLine numbers are from 1 // Loop indices are from 0, activeLine numbers are from 1
return (<div key={`line_${i}`} className={i + 1 === activeLine ? 'highlight' : ''}>{line || <>&nbsp;</>}</div>) return (
<div key={`line_${i}`} className={i + 1 === activeLine ? 'highlight' : ''}>
{<HighlightedLine line={line}/> || <>&nbsp;</>}
</div>
)
}) })
return ( return (
<div className='src'>{lineDivs}</div> <div className='src'>{lineDivs}</div>
+7 -5
View File
@@ -15,13 +15,15 @@
Quisque tristique diam et purus sagittis, posuere pharetra orci egestas. Praesent imperdiet sapien vel fermentum 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. pulvinar. Nulla ac tortor laoreet, fringilla tellus nec, ullamcorper mi.
</p> </p>
<div class="snippet"> <div class="snippet" data-width="30">
<pre> <pre>
.org 0x400 .org 0x400
push 0 push 0 ; A comment
blah: add 1 blah:
jmp $-1 add 1
hlt jmp $-1
hlt
.db "A string; \"semicolon-y\"\0"
</pre> </pre>
</div> </div>
<p> <p>
+2 -1
View File
@@ -6,7 +6,8 @@ import init, { WasmCPU, assemble_snippet } from '../pkg/vweb.js'
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
init().then(() => { init().then(() => {
document.querySelectorAll('.snippet').forEach((el) => { 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))
}) })
}) })
}) })