Specify width and syntax highlighting and indentation
This commit is contained in:
@@ -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 ? '' : <> </>}{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]
|
||||||
|
}
|
||||||
@@ -53,3 +53,10 @@ body {
|
|||||||
.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 }
|
||||||
@@ -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 || <> </>}</div>)
|
return (
|
||||||
|
<div key={`line_${i}`} className={i + 1 === activeLine ? 'highlight' : ''}>
|
||||||
|
{<HighlightedLine line={line}/> || <> </>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<div className='src'>{lineDivs}</div>
|
<div className='src'>{lineDivs}</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user