Reorganize files for TS conversion

This commit is contained in:
Tangent 2023-08-04 23:54:52 -04:00
parent 56d7826aad
commit 8b4584eb48
6 changed files with 2 additions and 2 deletions

102
src/3x5.ts Normal file
View file

@ -0,0 +1,102 @@
/**
* @typedef {object} Card Basic unit of information, also an "actor" in the programming system
* @property {number} id Unique identifier
* @property {Record<string, string>} fields Key-value properties on the card
* @property {string} code Eventually: a markdown string containing code, but for now, just code
*/
/**
* @typedef {"action" | "render"} ScriptType "Mode" of the environment a script runs in; determines access to mutability features and such.
*
* "action": response to a UI action; allowed to modify card fields and access time and random numbers.
*
* "render": deterministic generation of display markup from card and workspace state; can only modify temporary variables.
*/
/**
* @typedef {object} Vm State for running a script in.
* @property {ScriptType} mode Mutability status
* @property {string} output Markup to render / output
*/
/**
* @param {Vm} state VM state
* @param {string} code Script to run
* @returns {string} Markup to render / output
*/
function renderCard(state, code) {
const script = Notcl.parse(code);
if (script[0]) {
state.output = JSON.stringify(script[1], null, 2);
} else {
state.output = script[1];
}
return state.output;
}
/**
* Global state: a single card
* @type {Card}
*/
let theCard = {
id: 100,
fields: {},
code: String.raw`
h1 "Hello, World!"
para [2 + 2]
block {
This is a paragraph of text, with one [b bold] word. Yes, this means there has to be some magic in text processing... <b>this</b> won't work.
}
block -red "Beware!"
para "All text should be quoted, it's clearer that way. & blockquotes already should contain paragraphs. (maybe normalize nested paragraphs)"
block {
First block
} {
Second block
Is this markdown-parsed?
[button "No we want to render UI" \\{noop}]
} {
Since we want escapes to work, these blocks [i will] be subject to substitutions.
}
# A comment
para {
line endings escaped\
one slash
not escaped if \\
two slashes
escaped with a slash if \\\
three slashes
not escaped with two slashes if \\\\
four slashes
escaped with two slashes if \\\\\
five slashes
not escaped with three slashes if \\\\\\
six slashes
}
`,
};
const state = document.createElement("pre");
const display = document.createElement("blockquote");
const debugDisplay = document.createElement("pre");
function render() {
const vm = {
mode: /** @type {ScriptType} */ ("render"),
output: "",
};
const html = renderCard(vm, theCard.code);
state.textContent = JSON.stringify(theCard, null, 2);
display.innerHTML = html;
debugDisplay.textContent = html;
}
render();
document.body.append(state, display, debugDisplay);

9
src/helpers.ts Normal file
View file

@ -0,0 +1,9 @@
const escapeDiv = document.createElement("div");
/**
* @param {string} text Potentially dangerous text
* @returns {string} Text safe to embed in HTML
**/
function escapeHtml(text) {
escapeDiv.textContent = text;
return escapeDiv.innerHTML;
}

115
src/notcl.ts Normal file
View file

@ -0,0 +1,115 @@
/**
* @typedef {Notcl.Command[]} Notcl.Script
* @typedef {Notcl.Word[]} Notcl.Command
* @typedef {object} Notcl.Word
* @property {string} text
*/
var Notcl = (() => {
const { AtLeast, Choose, End, Regex, Sequence, Use } = Peg;
const InterCommandWhitespace = Regex(/\s+/y).expects("whitespace");
const Comment = Regex(/#[^\n]*/y)
.expects("#")
.map(() => []);
const PreCommand = AtLeast(0, InterCommandWhitespace);
const PreWordWhitespace = Regex(/[^\S\n;]+/y).expects("whitespace");
const BasicWord = Regex(/(?!\{)[^\s;]+/y)
.map(([word]) => ({ text: word }))
.expects("BASIC_WORD");
// WIP, need to be able to escape braces correctly
/** @type {Peg.Pattern<string>} */
const Brace = Sequence(
Regex(/\{/y).expects("{"),
AtLeast(
0,
Choose(
Use(() => Brace)
.expects("{")
.map((text) => `{${text}}`),
Regex(/[^{}]+/y)
.expects("text")
.map(([text]) => text)
)
),
Regex(/\}/y).expects("}")
).map(([_left, fragments, _right]) => fragments.join(""));
const Word = Choose(
BasicWord,
Brace.map((text) => ({ text }))
);
const CommandTerminator = Regex(/[\n;]/y)
.expects("NEWLINE | ;")
.map(() => true);
/** @type {Peg.Pattern<Notcl.Command>} */
const Command = Sequence(
Word,
AtLeast(
0,
Sequence(PreWordWhitespace, Word).map(([, word]) => word)
),
AtLeast(0, PreWordWhitespace)
).map(([word, moreWords]) => [word].concat(moreWords));
/** @type {Peg.Pattern<Notcl.Script>} */
const Script = Sequence(
AtLeast(
0,
Choose(
PreWordWhitespace.map(() => []),
CommandTerminator.map(() => []),
Sequence(Comment, Choose(CommandTerminator, End())).map(() => []),
Sequence(Command, Choose(CommandTerminator, End())).map(
([words]) => words
)
)
),
End()
).map(([commands]) => commands.filter((command) => command.length > 0));
const ERROR_CONTEXT = /(?<=([^\n]{0,50}))([^\n]{0,50})/y;
return {
/**
* Parse out a Notcl script into an easier-to-interpret representation.
* No script is actually executed yet.
*
* @param {string} code to parse
* @returns {[true, Notcl.Script] | [false, string]} parsed list of commands, or error message on failure
*/
parse(code) {
/* Preprocess */
// fold line endings
code = code.replace(/(?<!\\)((\\\\)*)\\\n/g, "$1");
/* Parse */
const [commands, errorPos, expected] = Script(code, 0);
if (commands) {
return [true, commands[0]];
} else {
ERROR_CONTEXT.lastIndex = errorPos;
const [, before, after] = /** @type {RegExpExecArray} */ (
ERROR_CONTEXT.exec(code)
);
return [
false,
`<pre>Error at position ${errorPos}
${escapeHtml(before + "" + after)}
${"-".repeat(before.length)}^
Expected: ${escapeHtml(expected)}</pre>`,
];
}
},
};
})();

204
src/peg.ts Normal file
View file

@ -0,0 +1,204 @@
/**
* A Pattern is a function that matches against a string starting at a given index.
*
* If it matches successfully, it returns some captured value, and the index following the match.
*
* On success or failure, it returns the furthest point the pattern could make sense of, and a description of what was expected next at that point.
*
* For simple patterns, the "furthest point" may just be the following index; however, some more complex patterns might succeed,
* but consume less input than they would have been able to if some other expected symbol was found. Reporting
* the furthest a pattern could hypothetically have gotten can help generate better error messages if no valid parse tree is found.
*
* @template T
* @callback Peg.PatternCall
* @param {string} source - the string being parsed
* @param {number} index - the index in the string to begin matching from
* @returns {[[T, number] | null, number, string]} - [successValue, furthest symbol attempted, expected pattern]
*/
/**
* @template T
* @typedef {object} Peg.PatternExt
* @property {<U>(map: (value: T) => U) => Peg.Pattern<U>} map Creates a pattern that wraps another pattern, transforming the returned value on a match
* @property {string} expectLabel A human-readable annotation describing the pattern for error messages
* @property {(label: string) => Peg.Pattern<T>} expects Adds a human-readable annotation describing the pattern
*/
/**
* @template T
* @typedef {Peg.PatternCall<T> & Peg.PatternExt<T>} Peg.Pattern
*/
var Peg = window.Peg ?? {};
/**
* Makes a pattern from a function, adding helper methods.
*
* @template T
* @param {(source: string, index: number) => ([[T, number] | null, number, string])} matchFunc
* @returns {Peg.Pattern<T>}
*/
Peg.WrapPattern = function (matchFunc) {
const pattern = /** @type {Peg.Pattern<T>} */ (matchFunc);
pattern.map = function (map) {
return Peg.WrapPattern(function (source, index) {
const [value, furthest, expected] = pattern(source, index);
return [value ? [map(value[0]), value[1]] : null, furthest, expected];
}).expects(pattern.expectLabel);
};
pattern.expectLabel = pattern.name;
pattern.expects = (label) => {
pattern.expectLabel = label;
return pattern;
};
return pattern;
};
/**
* Proxies to a pattern retrieved from an accessor function.
*
* Allows using a pattern recursively in its own definition, by returning the value of the const assigned to.
*
* @template T
* @param {() => Peg.Pattern<T>} getPattern
* @returns {Peg.Pattern<T>}
*/
Peg.Use = function (getPattern) {
return Peg.WrapPattern(function (source, index) {
return getPattern()(source, index);
}).expects(String(getPattern));
};
/**
* Creates a pattern matching a regex & returning any captures. The regex needs to be sticky (using the //y modifier)
* @param {RegExp} regex
* @return {Peg.Pattern<RegExpExecArray>}
*/
Peg.Regex = function (regex) {
/** @type {Peg.Pattern<RegExpExecArray>} */
const pattern = Peg.WrapPattern(function (source, index) {
regex.lastIndex = index;
const matches = regex.exec(source);
return matches
? [[matches, regex.lastIndex], -1, pattern.expectLabel]
: [null, index, pattern.expectLabel];
}).expects(regex.source);
return pattern;
};
/**
* Creates a pattern that tries the given patterns, in order, until it finds one that matches at the current index.
* @template T
* @param {...Peg.Pattern<T>} patterns
* @return {Peg.Pattern<T>}
*/
Peg.Choose = function (...patterns) {
const genericExpected = patterns
.map((pattern) => pattern.expectLabel)
.join(" | ");
return Peg.WrapPattern(function (source, index) {
let furthestFound = index;
let furthestExpected = genericExpected;
for (const pattern of patterns) {
const [value, furthest, expected] = pattern(source, index);
if (value) {
return [value, furthest, expected];
} else if (furthest > furthestFound) {
furthestFound = furthest;
furthestExpected = expected;
} else if (furthest == furthestFound) {
furthestExpected = furthestExpected + " | " + expected;
}
}
return [null, furthestFound, furthestExpected];
}).expects(genericExpected);
};
/**
* Creates a pattern that concatenates the given patterns, returning a tuple of their captured values.
*
* For example, if A matches "a" and captures 1, while B matches "b" and captures null,
* then `Sequence(A,B)` will match "ab" and capture [1, null]
* @template {unknown[]} T
* @param {{[K in keyof T]: Peg.Pattern<T[K]>}} patterns
* @return {Peg.Pattern<T>}
*/
Peg.Sequence = function (...patterns) {
const genericExpected = patterns[0]?.expectLabel ?? "(nothing)";
return Peg.WrapPattern(function (source, index) {
const values = /** @type {T} */ (/** @type {unknown} */ ([]));
let furthestFound = index;
let furthestExpected = genericExpected;
for (const pattern of patterns) {
const [value, furthest, expected] = pattern(source, index);
if (furthest > furthestFound) {
furthestFound = furthest;
furthestExpected = expected;
} else if (furthest == furthestFound) {
furthestExpected = furthestExpected + " | " + expected;
}
if (value == null) {
return [null, furthestFound, furthestExpected];
}
values.push(value[0]);
index = value[1];
}
return [[values, index], furthestFound, furthestExpected];
}).expects(genericExpected);
};
/**
* Creates a pattern that matches consecutive runs of the given pattern, returning an array of all captures.
*
* The match only succeeds if the run is at least {@link min} instances long.
*
* If the given pattern does not consume input, the matching will be terminated to prevent an eternal loop.
*
* Note that if the minimum run is zero, this pattern will always succeed, but might not consume any input.
* @template {unknown} T
* @param {number} min
* @param {Peg.Pattern<T>} pattern
* @return {Peg.Pattern<T[]>}
*/
Peg.AtLeast = function (min, pattern) {
return Peg.WrapPattern(function (source, index) {
const values = /** @type {T[]} */ ([]);
let furthestFound = index;
let furthestExpected = pattern.expectLabel;
do {
const [value, furthest, expected] = pattern(source, index);
if (furthest > furthestFound) {
furthestFound = furthest;
furthestExpected = expected;
}
if (value == null) {
break;
}
values.push(value[0]);
if (index == value[1]) {
break;
}
index = value[1];
} while (true);
if (values.length >= min) {
return [[values, index], furthestFound, furthestExpected];
} else {
return [null, furthestFound, furthestExpected];
}
}).expects(pattern.expectLabel);
};
/**
* Creates a pattern that matches the end of input
* @return {Peg.Pattern<true>}
*/
Peg.End = () => {
/** @type {Peg.Pattern<true>} */
const end = Peg.WrapPattern(function End(source, index) {
return [
source.length == index ? [/** @type {true} */ (true), index] : null,
index,
end.expectLabel,
];
}).expects("<eof>");
return end;
};