More API cleanup in preparation for a DSL.

This commit is contained in:
Joshua Bemenderfer 2023-01-30 20:14:31 -05:00
parent 67e7811772
commit aebe488dad
4 changed files with 133 additions and 122 deletions

View File

@ -40,52 +40,43 @@ const lines = [
` Further comments below. As I will now demonstrate, there is no simple`, ` Further comments below. As I will now demonstrate, there is no simple`,
` even if embedded`, ` even if embedded`,
` way of dealing with this problem.`, ` way of dealing with this problem.`,
``
] ]
// Schema
// name tail
// version tail
// license tail
// exports object
// #any object
// import tail
// require tail
// scripts object
// #any tail
// devDependencies
// #any tail
// author object
// name tail
// email tail
// #text
async function main() { async function main() {
const { toArrays } = useDocument(createStringReader(lines)) const { toLineArray } = useDocument(createStringReader(lines))
const resultArrays = await toArrays() console.dir(await toLineArray(), { depth: null })
console.dir(resultArrays, { depth: null })
const { tail, each, match, buildObject } = useDocument(createStringReader(lines)) const { head, tail, each, match, toObject } = useDocument(createStringReader(lines))
const structure = { const structure = await toObject({
name: null, 'name': true,
version: null, 'version': () => tail().trim(),
license: null, 'license': () => tail().trim(),
exports: null, 'exports': () => toObject({
scripts: null, '#any': () => toObject({ import: true, require: true })
devDependencies: null, }),
author: null 'scripts': () => toObject({ '#any': true }),
} 'devDependencies': () => toObject(),
'author': () => toObject({ name: true, email: true, '#text': true })
await each(async () => {
if (match('name')) structure.name = tail().trim()
if (match('version')) structure.version = tail().trim()
if (match('license')) structure.license = tail().trim()
// FIXME: Order of operations causes other parts to break if this doesn't run first?!
if (match('exports')) structure.exports = await buildObject([], async () => {
const section = { import: null, require: null }
await each(() => {
if (match('import')) section.import = tail().trim()
if (match('require')) section.require = tail().trim()
if (section.import && section.require) return true
})
return section
})
if (match('scripts')) structure.scripts = await buildObject()
if (match('devDependencies')) structure.devDependencies = await buildObject()
if (match('author')) structure.author = await buildObject(['name', 'email', '#text'])
return structure.name &&
structure.version &&
structure.license &&
structure.exports &&
structure.scripts &&
structure.devDependencies &&
structure.author
}) })
console.dir(structure, { depth: null }) console.dir(structure, { depth: null })

View File

@ -1 +1 @@
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const h=require("./parser.cjs");function T(p,b=" "){let c=0;const t=h.createLineData("",b);async function s(){c===0&&(c=1);const e=await p.next();return e===null?(c=2,!0):(t.line=e,h.parseLine(t),!1)}const a=()=>t.level,u=()=>t.line.slice(t.offsetHead),o=()=>t.line.slice(t.offsetHead,t.offsetTail),f=()=>t.line.slice(t.offsetTail),v=e=>e===o();async function d(e){const n=c===0?-1:a();for(;!(await s()||a()<=n||await e()););}async function l(e=-1,n=[u()]){var i;return e===-1&&(e=t.level+1),await s()||t.level<e?n:(n.push(((i=t.line)==null?void 0:i.slice(e))||""),l(e,n))}async function w(e=[],n){const r={};return await d(async()=>{const i=o();if(!!i){if(!e.length||e.includes(i)){r[i]=n?await n():f().trim();return}if(e&&!e.includes(i)&&e.includes("#text")){r["#text"]=await l(a());return}}}),r}async function m(){const e=[["root",[]]];for(;!await s();){const n=a(),r=n+1,i=e[n];if(!i)continue;e.length=r;const x=e[r]=[u(),[]];i[1].push(x)}return e[0]}return{state:c,next:s,line:u,head:o,tail:f,level:a,match:v,each:d,blockAsText:l,buildObject:w,toArrays:m}}exports.useDocument=T; "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const d=require("./parser.cjs");function D(v,b=" "){let a=0;const n=d.createLineData("",b);async function u(){switch(a){case 0:a=1;break;case 2:return a=1,!1}const e=await v.next();return e===null?(a=3,!0):(n.line=e,d.parseLine(n),!1)}const y=()=>a=2,r=()=>n.level,s=()=>n.line.slice(n.offsetHead),c=()=>n.line.slice(n.offsetHead,n.offsetTail),l=()=>n.line.slice(n.offsetTail),j=e=>e===c();async function w(e){const t=a===0?-1:r();for(;;){if(await u())return;if(r()<=t)return y();if(await e())return}}async function f(e=-1,t=[s()]){var i;return e===-1&&(e=r()+1),await u()?t:r()<e?(y(),t):(t.push(((i=n.line)==null?void 0:i.slice(e))||""),f(e,t))}async function x(e){const t={};return await w(async()=>{const i=c();if(!i)return;const o=e?e[i]||e["#any"]:null;if(e?o===!0?t[i]=l().trim():o?t[i]=await o():e!=null&&e["#text"]&&(t["#text"]=await f(r())):t[i]=l().trim(),e&&Object.keys(e).every(p=>t[p]!==void 0))return!0}),t}async function T(){const e=[["root",[]]];for(;!await u();){const t=r(),i=t+1,o=e[t];if(!o)continue;e.length=i;const p=e[i]=[s(),[]];o[1].push(p)}return e[0]}return{next:u,line:s,head:c,tail:l,level:r,match:j,each:w,blockAsText:f,toObject:x,toLineArray:T}}exports.useDocument=D;

View File

@ -1,64 +1,70 @@
import { parseLine as v, createLineData as T } from "./parser.js"; import { parseLine as j, createLineData as D } from "./parser.js";
function y(h, p = " ") { function k(x, d = " ") {
let r = 0; let o = 0;
const t = T("", p); const n = D("", d);
async function s() { async function c() {
r === 0 && (r = 1); switch (o) {
const e = await h.next(); case 0:
return e === null ? (r = 2, !0) : (t.line = e, v(t), !1); o = 1;
break;
case 2:
return o = 1, !1;
}
const e = await x.next();
return e === null ? (o = 3, !0) : (n.line = e, j(n), !1);
} }
const a = () => t.level, o = () => t.line.slice(t.offsetHead), u = () => t.line.slice(t.offsetHead, t.offsetTail), l = () => t.line.slice(t.offsetTail), w = (e) => e === u(); const w = () => o = 2, r = () => n.level, u = () => n.line.slice(n.offsetHead), s = () => n.line.slice(n.offsetHead, n.offsetTail), f = () => n.line.slice(n.offsetTail), v = (e) => e === s();
async function d(e) { async function y(e) {
const n = r === 0 ? -1 : a(); const t = o === 0 ? -1 : r();
for (; !(await s() || a() <= n || await e()); ) for (; ; ) {
; if (await c())
return;
if (r() <= t)
return w();
if (await e())
return;
}
} }
async function f(e = -1, n = [o()]) { async function l(e = -1, t = [u()]) {
var i; var i;
return e === -1 && (e = t.level + 1), await s() || t.level < e ? n : (n.push(((i = t.line) == null ? void 0 : i.slice(e)) || ""), f(e, n)); return e === -1 && (e = r() + 1), await c() ? t : r() < e ? (w(), t) : (t.push(((i = n.line) == null ? void 0 : i.slice(e)) || ""), l(e, t));
} }
async function x(e = [], n) { async function T(e) {
const c = {}; const t = {};
return await d(async () => { return await y(async () => {
const i = u(); const i = s();
if (!!i) { if (!i)
if (!e.length || e.includes(i)) { return;
c[i] = n ? await n() : l().trim(); const a = e ? e[i] || e["#any"] : null;
return; if (e ? a === !0 ? t[i] = f().trim() : a ? t[i] = await a() : e != null && e["#text"] && (t["#text"] = await l(r())) : t[i] = f().trim(), e && Object.keys(e).every((p) => t[p] !== void 0))
} return !0;
if (e && !e.includes(i) && e.includes("#text")) { }), t;
c["#text"] = await f(a());
return;
}
}
}), c;
} }
async function b() { async function b() {
const e = [["root", []]]; const e = [["root", []]];
for (; !await s(); ) { for (; !await c(); ) {
const n = a(), c = n + 1, i = e[n]; const t = r(), i = t + 1, a = e[t];
if (!i) if (!a)
continue; continue;
e.length = c; e.length = i;
const m = e[c] = [o(), []]; const p = e[i] = [u(), []];
i[1].push(m); a[1].push(p);
} }
return e[0]; return e[0];
} }
return { return {
state: r, next: c,
next: s, line: u,
line: o, head: s,
head: u, tail: f,
tail: l, level: r,
level: a, match: v,
match: w, each: y,
each: d, blockAsText: l,
blockAsText: f, toObject: T,
buildObject: x, toLineArray: b
toArrays: b
}; };
} }
export { export {
y as useDocument k as useDocument
}; };

View File

@ -4,11 +4,11 @@ import { createLineData, parseLine } from './parser'
enum STATE { enum STATE {
READY = 0, READY = 0,
STARTED = 1, STARTED = 1,
ENDED = 2 PAUSED = 2,
ENDED = 3
} }
type Document = { type Document = {
state: STATE,
next: () => Promise<boolean> next: () => Promise<boolean>
level: () => number, level: () => number,
line: () => string, line: () => string,
@ -17,18 +17,27 @@ type Document = {
match: (matchHead: string) => boolean, match: (matchHead: string) => boolean,
each: (handler: Function) => void, each: (handler: Function) => void,
blockAsText: (startLevel: number, lines?: string[]) => Promise<Array<string>>, blockAsText: (startLevel: number, lines?: string[]) => Promise<Array<string>>,
buildObject: (allowKeys: Array<string>, processValue?: () => any) => any, toObject: (matchers?: { [key: string]: Function|boolean }) => { [key: string]: any },
toArrays(): Promise<LineArray> toLineArray(): Promise<LineArray>
} }
type LineArray = [string, Array<LineArray>]; type LineArray = [string, Array<LineArray>]
export function useDocument (reader: Reader, indent: string = ' '): Document { export function useDocument (reader: Reader, indent: string = ' '): Document {
let state = STATE.READY let state = STATE.READY
const lineData = createLineData('', indent) const lineData = createLineData('', indent)
async function next() { async function next() {
if (state === STATE.READY) state = STATE.STARTED switch (state) {
// The initial state change allows us to do some special-case handling for the initial state of lineData. TODO: Should lineData have a special inital state?
case STATE.READY:
state = STATE.STARTED
break
// If we are currently in the "paused" state, repeat the same line instead of reading the next one.
case STATE.PAUSED:
state = STATE.STARTED
return false
}
const line = await reader.next() const line = await reader.next()
if (line === null) { if (line === null) {
@ -41,6 +50,10 @@ export function useDocument (reader: Reader, indent: string = ' '): Document {
return false return false
} }
// If we pause, the next call to next() will repeat the current line.
// Allows a child loop to look forward, determine that the next line will be outside its purview,
// and return control to the calling loop transparently without additional logic.
const pause = () => state = STATE.PAUSED
const level = () => lineData.level const level = () => lineData.level
const line = () => lineData.line.slice(lineData.offsetHead) const line = () => lineData.line.slice(lineData.offsetHead)
const head = () => lineData.line.slice(lineData.offsetHead, lineData.offsetTail) const head = () => lineData.line.slice(lineData.offsetHead, lineData.offsetTail)
@ -53,52 +66,54 @@ export function useDocument (reader: Reader, indent: string = ' '): Document {
const startLevel = state === STATE.READY ? -1 : level() const startLevel = state === STATE.READY ? -1 : level()
while(true) { while(true) {
if (await next()) break if (await next()) return
if (level() <= startLevel) break // If we've reached the next block outside the level of this one, "pause", so that the next time "next" is called, we repeat the same line.
if (await handler()) break if (level() <= startLevel) return pause()
// If the handler returns true, exit.
if (await handler()) return
} }
} }
async function blockAsText (startLevel: number = -1, blockLines: string[] = [line()]): Promise<Array<string>> { async function blockAsText (startLevel: number = -1, blockLines: string[] = [line()]): Promise<Array<string>> {
if (startLevel === -1) startLevel = lineData.level + 1 if (startLevel === -1) startLevel = level() + 1
const ended = await next() if (await next()) return blockLines
if (ended) return blockLines if (level() < startLevel) { pause(); return blockLines }
if (lineData.level < startLevel) return blockLines
blockLines.push(lineData.line?.slice(startLevel) || '') blockLines.push(lineData.line?.slice(startLevel) || '')
return blockAsText(startLevel, blockLines) return blockAsText(startLevel, blockLines)
} }
async function buildObject(allowKeys: Array<string> = [], processValue?: () => any) { async function toObject (matchers?: { [key: string]: Function|boolean }) {
const obj: any = {} const obj: { [key: string]: any } = {}
await each(async () => { await each(async () => {
const currHead = head() const currHead = head()
if (!currHead) return if (!currHead) return
// Set the object key matching the current head if it is allowed, or if no allow list is specified.
if (!allowKeys.length || allowKeys.includes(currHead)) {
// Default to using {head: tail} as the key-value pair if no value handler is specified.
obj[currHead] = processValue ? await processValue() : tail().trim()
return
}
// Parse unspecified text into an array of lines and save to #text key. const propertyMatcher = matchers ? matchers[currHead] || matchers['#any'] : null
// TODO: Rework this. I don't like it at all. // Set the object key matching the current head to tail() if no matchers are specified.
if (allowKeys && !allowKeys.includes(currHead) && allowKeys.includes('#text')) { if (!matchers) obj[currHead] = tail().trim()
obj['#text'] = await blockAsText(level()) // Or if matchers[currHead] is `true`, or if #any is true.
return else if (propertyMatcher === true) obj[currHead] = tail().trim()
} // If matchers[currHead] or matchers[#any] is a function, set object key to its output.
else if (propertyMatcher) obj[currHead] = await propertyMatcher()
// If we get to this point and matchers[#text] is set, parse all remaining block contents as text.
// TODO: I still don't like this.
else if (matchers?.['#text']) obj['#text'] = await blockAsText(level())
// Bail early as soon as we know all keys have been matched.
if (matchers && Object.keys(matchers).every(k => obj[k] !== undefined)) return true
}) })
return obj return obj
} }
async function toArrays(): Promise<LineArray> { async function toLineArray (): Promise<LineArray> {
const levelTracker: Array<LineArray> = [['root', []]] const levelTracker: Array<LineArray> = [['root', []]]
// Simple parser that produces canonical array structure for blocks. // Simple parser that produces canonical array structure for blocks.
while (true) { while (true) {
// If next() returns true we've ended the // If next() returns true we've ended the
if (await next()) break; if (await next()) break
const parentLevel = level() const parentLevel = level()
const scopeLevel = parentLevel + 1 const scopeLevel = parentLevel + 1
// Determine parent for this scope. // Determine parent for this scope.
@ -116,7 +131,6 @@ export function useDocument (reader: Reader, indent: string = ' '): Document {
} }
return { return {
state,
next, next,
line, line,
head, head,
@ -125,7 +139,7 @@ export function useDocument (reader: Reader, indent: string = ' '): Document {
match, match,
each, each,
blockAsText, blockAsText,
buildObject, toObject,
toArrays, toLineArray,
} }
} }