From b05cdab771001228ec3cdda7158529da67e52342 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sat, 27 Jan 2024 23:57:09 -0500 Subject: [PATCH 01/13] Simplify tick.ts typing --- lib/tick.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/tick.ts b/lib/tick.ts index a4b8c2b..d158357 100644 --- a/lib/tick.ts +++ b/lib/tick.ts @@ -1,13 +1,9 @@ -import { Cancel, Source } from "./source"; +import { Source } from "./source"; export type PhysicsTick = ["physics"]; export type RenderTick = ["render", number]; export function tick(fps: number): Source<PhysicsTick | RenderTick> { - function tickSource(): PhysicsTick; - function tickSource( - callback: (tick: PhysicsTick | RenderTick) => void - ): Cancel; function tickSource(callback?: (tick: PhysicsTick | RenderTick) => void) { if (callback) { let lastPhysicsTick: number = new Date().getTime(); @@ -32,5 +28,5 @@ export function tick(fps: number): Source<PhysicsTick | RenderTick> { } } - return tickSource; + return tickSource as Source<PhysicsTick | RenderTick>; } From b04ba4e62249b8c38c4f9c93b002936e068f7287 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sat, 27 Jan 2024 23:57:36 -0500 Subject: [PATCH 02/13] WIP KeyControl event source --- lib/keys.ts | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 lib/keys.ts diff --git a/lib/keys.ts b/lib/keys.ts new file mode 100644 index 0000000..c3b335f --- /dev/null +++ b/lib/keys.ts @@ -0,0 +1,67 @@ +import { Source } from "./source"; + +export type KeyName = "up" | "down" | "left" | "right" | "a" | "b" | "menu"; + +const KEY_NAMES: Record<string, KeyName> = { + // compact keys (WASD+ZXC) + KeyZ: "a", + KeyX: "b", + KeyC: "menu", + KeyW: "up", + KeyS: "down", + KeyA: "left", + KeyD: "right", + // full-board keys (arrows+space/shift/enter) + Space: "a", + ShiftLeft: "b", + ShiftRight: "b", + Enter: "menu", + ArrowUp: "up", + ArrowDown: "down", + ArrowLeft: "left", + ArrowRight: "right", +}; + +/** A keypress/release event for an abstract button, or else ["focus", "release"] if for some reason future release events might not be registered. */ +export type KeyEvent = [KeyName | "focus", "press" | "release"]; + +export function keyControl(source: HTMLElement): Source<KeyEvent> { + const tabIndex = source.getAttribute("tabIndex"); + source.setAttribute( + "tabindex", + tabIndex == "" || tabIndex == null ? "-1" : tabIndex + ); + + return ((callback?: (keyEvent: KeyEvent) => void) => { + if (callback) { + const handle = (evt: KeyboardEvent, action: "press" | "release") => { + const keyName = KEY_NAMES[evt.code]; + if (keyName != undefined) { + evt.preventDefault(); + evt.stopPropagation(); + + callback([keyName, action]); + } + }; + const keyUp = (evt: KeyboardEvent) => handle(evt, "release"); + const keyDown = (evt: KeyboardEvent) => handle(evt, "press"); + const focus = () => callback(["focus", "press"]); + const blur = () => callback(["focus", "release"]); + + source.addEventListener("keyup", keyUp, false); + source.addEventListener("keydown", keyDown, false); + source.addEventListener("focus", focus, false); + source.addEventListener("blur", blur, false); + source.focus({ focusVisible: true } as FocusOptions); + + return () => { + source.removeEventListener("keyup", keyUp, false); + source.removeEventListener("keydown", keyDown, false); + source.removeEventListener("focus", focus, false); + source.removeEventListener("blur", blur, false); + }; + } else { + return ["blur", "release"]; + } + }) as Source<KeyEvent>; +} From 497731f62b299517f3831c13f42836d689d065c7 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sat, 3 Feb 2024 15:36:29 -0500 Subject: [PATCH 03/13] Skeleton for pixelflood --- pixelflood.html | 11 +++++++++++ pixelflood.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 pixelflood.html create mode 100644 pixelflood.ts diff --git a/pixelflood.html b/pixelflood.html new file mode 100644 index 0000000..a9d9d7f --- /dev/null +++ b/pixelflood.html @@ -0,0 +1,11 @@ +<html> + <head> + <title>Pixelflood Art</title> + </head> + <body> + <script src="js/pixelflood.js"></script> + <script> + document.body.append(...PixelfloodApplet()); + </script> + </body> +</html> diff --git a/pixelflood.ts b/pixelflood.ts new file mode 100644 index 0000000..ba288a7 --- /dev/null +++ b/pixelflood.ts @@ -0,0 +1,45 @@ +// TODO choose generator +// TODO get/put image data in render/tick loop +// TODO rediscover RGB/HSV pixelflood techniques + +import { canvas2d, h } from "./lib/html"; + +interface Controls { + seed: HTMLInputElement; +} + +function PixelfloodApplet() { + const [canvas, cx] = canvas2d({}); + const [controls, controlUi] = ControlUi(); + + return [canvas, ...controlUi]; +} + +function numInput(init: number) { + return h("input", { type: "number", valueAsNumber: init }); +} + +function ControlUi(): [Controls, HTMLElement[]] { + let seed, width, height; + + const html = [ + h("h2", {}, "Controls"), + h( + "div", + {}, + h( + "label", + {}, + "Width: ", + (width = numInput(128)), + "Height: ", + (height = numInput(128)) + ) + ), + h("div", {}, h("label", {}, "Random Seed: ", (seed = numInput(128)))), + ]; + + return [{ seed }, html]; +} + +Object.assign(globalThis, { PixelfloodApplet }); From a2df9a4142194e00c36e4bd7a1ad3f40cd3fe5f3 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sun, 7 Jul 2024 17:28:10 -0400 Subject: [PATCH 04/13] Use vscode folder format --- .vscode/settings.json | 3 +++ .vscode/tasks.json | 17 +++++++++++++++++ js_sketch.code-workspace | 27 --------------------------- 3 files changed, 20 insertions(+), 27 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json delete mode 100644 js_sketch.code-workspace diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ad92582 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..4261f3e --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Serve Esbuild", + "detail": "Run a local server serving html files & transpiling TS -> JS", + "command": "npm", + "type": "process", + "args": ["run", "serve"], + "problemMatcher": ["$esbuild-watch"], + "presentation": { + "reveal": "always", + "panel": "dedicated" + } + } + ] +} diff --git a/js_sketch.code-workspace b/js_sketch.code-workspace deleted file mode 100644 index 85ed6cf..0000000 --- a/js_sketch.code-workspace +++ /dev/null @@ -1,27 +0,0 @@ -{ - "folders": [ - { - "path": "." - } - ], - "settings": { - "editor.formatOnSave": true - }, - "tasks": { - "version": "2.0.0", - "tasks": [ - { - "label": "Serve Esbuild", - "detail": "Run a local server serving html files & transpiling TS -> JS", - "command": "npm", - "type": "process", - "args": ["run", "serve"], - "problemMatcher": ["$esbuild-watch"], - "presentation": { - "reveal": "always", - "panel": "dedicated" - } - } - ] - } -} From e90727a06c787f1e117c7fee1dbb89462556051a Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sun, 7 Jul 2024 21:00:20 -0400 Subject: [PATCH 05/13] Stub out Idv parser --- debug.html | 1 + debug.ts | 3 ++- debug/idv.ts | 43 +++++++++++++++++++++++++++++++ lib/idv.ts | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 debug/idv.ts create mode 100644 lib/idv.ts diff --git a/debug.html b/debug.html index c79ae55..7019993 100644 --- a/debug.html +++ b/debug.html @@ -6,6 +6,7 @@ <script src="js/debug.js"></script> <script> document.body.append(...TickDebug()); + document.body.append(...IdvDebug()); </script> </body> </html> diff --git a/debug.ts b/debug.ts index 19908a0..b181e09 100644 --- a/debug.ts +++ b/debug.ts @@ -1,3 +1,4 @@ import { TickDebug } from "./debug/tick"; +import { IdvDebug } from "./debug/idv"; -Object.assign(globalThis, { TickDebug }); +Object.assign(globalThis, { TickDebug, IdvDebug }); diff --git a/debug/idv.ts b/debug/idv.ts new file mode 100644 index 0000000..43dbd7e --- /dev/null +++ b/debug/idv.ts @@ -0,0 +1,43 @@ +import { h } from "../lib/html"; +import { Idv, StringFromDocument } from "../lib/idv"; + +export function IdvDebug() { + const textarea = h( + "textarea", + { + cols: 80, + rows: 40, + oninput(ev) { + parse(); + }, + }, + `# Idv Testing Ground +Uid: 0 +Shell: tclsh +Group: users +Group: sudo + ` + ); + + const pre = h("pre"); + + function parse() { + try { + const idv = Idv.parse(textarea.value); + + pre.textContent = JSON.stringify( + { + shell: idv.getProperty("Shell", String, StringFromDocument), + groups: idv.getList("Group", String, StringFromDocument), + }, + null, + 2 + ); + } catch (e) { + pre.textContent = String(e); + } + } + parse(); + + return [textarea, pre]; +} diff --git a/lib/idv.ts b/lib/idv.ts new file mode 100644 index 0000000..2521e82 --- /dev/null +++ b/lib/idv.ts @@ -0,0 +1,72 @@ +export type DistinguisherParser<T> = (distinguisher: string) => T; +export type DocumentParser<T> = (document: string[]) => T; + +const LEADING_WHITESPACE = /^([ \t]+)/; +const ENTRY = /^(.+?):\s*(.*)/; + +export class Idv { + collections: Record<string, undefined | [string, string[]][]> = {}; + + public static parse(input: string): Idv { + const lines = input.split("\n").map((line) => line.trimEnd()); + return Idv.parseLines(lines); + } + static parseLines(input: string[]): Idv { + const idv = new Idv(); + let currentDocument: string[] = []; + + input.forEach((line) => { + const indent = LEADING_WHITESPACE.exec(line)?.[1]; + if (indent) { + // TODO + } else if (line == "") { + // TODO + } else if (line[0] == "#") { + // skip + } else { + const matches = ENTRY.exec(line); + if (matches) { + const [, collection, distinguisher] = matches; + + if (idv.collections[collection] == undefined) { + idv.collections[collection] = []; + } + + currentDocument = []; + idv.collections[collection].push([distinguisher, currentDocument]); + } else { + throw new Error("Failed to parse a property"); + } + } + }); + + return idv; + } + + public getProperty<T>( + name: string, + parseDistinguisher: DistinguisherParser<T>, + parseDocument: DocumentParser<T> + ): T | null { + const firstEntry = this.collections[name]?.[0]; + return firstEntry && firstEntry[1].length > 0 + ? parseDocument(firstEntry[1]) + : firstEntry?.[0] + ? parseDistinguisher(firstEntry[0]) + : null; + } + + public getList<T>( + name: string, + parseDistinguisher: DistinguisherParser<T>, + parseDocument: DocumentParser<T> + ): T[] { + return (this.collections[name] ?? []).map(([distinguisher, document]) => + document.length > 0 + ? parseDocument(document) + : parseDistinguisher(distinguisher) + ); + } +} + +export const StringFromDocument = (lines: string[]) => lines.join("\n"); From 2f9bb73107078b231316f056b107058ac19180f1 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Wed, 10 Jul 2024 00:17:33 -0400 Subject: [PATCH 06/13] parse indented documents --- debug/idv.ts | 6 +++++- lib/idv.ts | 36 +++++++++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/debug/idv.ts b/debug/idv.ts index 43dbd7e..b54354c 100644 --- a/debug/idv.ts +++ b/debug/idv.ts @@ -16,7 +16,11 @@ Uid: 0 Shell: tclsh Group: users Group: sudo - ` +Banner: + +------------------+ + |Welcome to Debian!| + +------------------+ +#` ); const pre = h("pre"); diff --git a/lib/idv.ts b/lib/idv.ts index 2521e82..a656145 100644 --- a/lib/idv.ts +++ b/lib/idv.ts @@ -13,14 +13,30 @@ export class Idv { } static parseLines(input: string[]): Idv { const idv = new Idv(); - let currentDocument: string[] = []; + let currentDocument: string[] | undefined = undefined; + let currentIndent: string | undefined = undefined; + let bufferedBlankLines: string[] = []; input.forEach((line) => { const indent = LEADING_WHITESPACE.exec(line)?.[1]; if (indent) { - // TODO + if (currentDocument == undefined) { + throw new Error("Indented document found before an entry"); + } + if (currentIndent == undefined) { + currentIndent = indent; + } + if (line.startsWith(currentIndent)) { + currentDocument.push(...bufferedBlankLines); + bufferedBlankLines = []; + currentDocument.push(line.substring(currentIndent.length)); + } else { + throw new Error( + "Inconsistent indentation- line indented less than the first line of its document" + ); + } } else if (line == "") { - // TODO + bufferedBlankLines.push(""); } else if (line[0] == "#") { // skip } else { @@ -33,6 +49,7 @@ export class Idv { } currentDocument = []; + currentIndent = undefined; idv.collections[collection].push([distinguisher, currentDocument]); } else { throw new Error("Failed to parse a property"); @@ -47,13 +64,14 @@ export class Idv { name: string, parseDistinguisher: DistinguisherParser<T>, parseDocument: DocumentParser<T> - ): T | null { + ): T | undefined { const firstEntry = this.collections[name]?.[0]; - return firstEntry && firstEntry[1].length > 0 - ? parseDocument(firstEntry[1]) - : firstEntry?.[0] - ? parseDistinguisher(firstEntry[0]) - : null; + return ( + firstEntry && + (firstEntry[1].length > 0 + ? parseDocument(firstEntry[1]) + : parseDistinguisher(firstEntry[0])) + ); } public getList<T>( From 525c6fa954572de3b8766fa7b34f3bb3b614add0 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Wed, 10 Jul 2024 00:31:33 -0400 Subject: [PATCH 07/13] impl map helper --- debug/idv.ts | 38 +++++++++++++++++++++++++------------- lib/idv.ts | 12 ++++++++++++ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/debug/idv.ts b/debug/idv.ts index b54354c..de17339 100644 --- a/debug/idv.ts +++ b/debug/idv.ts @@ -12,15 +12,21 @@ export function IdvDebug() { }, }, `# Idv Testing Ground -Uid: 0 -Shell: tclsh -Group: users -Group: sudo -Banner: - +------------------+ - |Welcome to Debian!| - +------------------+ -#` +User: tangent128 + Uid: 10000 + Shell: tclsh + Group: users + Group: sudo + Banner: + +------------------+ + |Welcome to Debian!| + +------------------+ + +User: tirga + Uid: 10101 + Shell: bash + Group: users +` ); const pre = h("pre"); @@ -30,10 +36,7 @@ Banner: const idv = Idv.parse(textarea.value); pre.textContent = JSON.stringify( - { - shell: idv.getProperty("Shell", String, StringFromDocument), - groups: idv.getList("Group", String, StringFromDocument), - }, + idv.getMap("User", UserFromDocument), null, 2 ); @@ -45,3 +48,12 @@ Banner: return [textarea, pre]; } + +const UserFromDocument = (lines: string[]) => { + const idv = Idv.parseLines(lines); + return { + shell: idv.getProperty("Shell", String, StringFromDocument), + groups: idv.getList("Group", String, StringFromDocument), + banner: idv.getProperty("Banner", String, StringFromDocument), + }; +}; diff --git a/lib/idv.ts b/lib/idv.ts index a656145..ddccc28 100644 --- a/lib/idv.ts +++ b/lib/idv.ts @@ -85,6 +85,18 @@ export class Idv { : parseDistinguisher(distinguisher) ); } + + public getMap<T>( + name: string, + parseDocument: DocumentParser<T> + ): Record<string, T> { + return Object.fromEntries( + (this.collections[name] ?? []).map(([distinguisher, document]) => [ + distinguisher, + parseDocument(document), + ]) + ); + } } export const StringFromDocument = (lines: string[]) => lines.join("\n"); From dc19a5ed9e1a7e707db5427b6f31298bb2460199 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sun, 14 Jul 2024 21:33:01 -0400 Subject: [PATCH 08/13] Refactor Idv to support merging documents --- lib/idv.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/idv.ts b/lib/idv.ts index ddccc28..4db02a6 100644 --- a/lib/idv.ts +++ b/lib/idv.ts @@ -8,11 +8,18 @@ export class Idv { collections: Record<string, undefined | [string, string[]][]> = {}; public static parse(input: string): Idv { - const lines = input.split("\n").map((line) => line.trimEnd()); - return Idv.parseLines(lines); - } - static parseLines(input: string[]): Idv { const idv = new Idv(); + return idv.import(input); + } + public static parseLines(input: string[]): Idv { + const idv = new Idv(); + return idv.importLines(input); + } + public import(input: string): Idv { + const lines = input.split("\n").map((line) => line.trimEnd()); + return this.importLines(lines); + } + public importLines(input: string[]): Idv { let currentDocument: string[] | undefined = undefined; let currentIndent: string | undefined = undefined; let bufferedBlankLines: string[] = []; @@ -44,20 +51,20 @@ export class Idv { if (matches) { const [, collection, distinguisher] = matches; - if (idv.collections[collection] == undefined) { - idv.collections[collection] = []; + if (this.collections[collection] == undefined) { + this.collections[collection] = []; } currentDocument = []; currentIndent = undefined; - idv.collections[collection].push([distinguisher, currentDocument]); + this.collections[collection].push([distinguisher, currentDocument]); } else { throw new Error("Failed to parse a property"); } } }); - return idv; + return this; } public getProperty<T>( From 63c41f07cbe3b8249aa41f7b4cf5c0ed3c88eafe Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sun, 14 Jul 2024 22:19:26 -0400 Subject: [PATCH 09/13] Implement MergedMap pattern --- debug/idv.ts | 5 ++--- lib/idv.ts | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/debug/idv.ts b/debug/idv.ts index de17339..7583dbe 100644 --- a/debug/idv.ts +++ b/debug/idv.ts @@ -36,7 +36,7 @@ User: tirga const idv = Idv.parse(textarea.value); pre.textContent = JSON.stringify( - idv.getMap("User", UserFromDocument), + idv.getMergedMap("User", UserFromDocument), null, 2 ); @@ -49,8 +49,7 @@ User: tirga return [textarea, pre]; } -const UserFromDocument = (lines: string[]) => { - const idv = Idv.parseLines(lines); +const UserFromDocument = (idv: Idv) => { return { shell: idv.getProperty("Shell", String, StringFromDocument), groups: idv.getList("Group", String, StringFromDocument), diff --git a/lib/idv.ts b/lib/idv.ts index 4db02a6..b0c753a 100644 --- a/lib/idv.ts +++ b/lib/idv.ts @@ -1,5 +1,6 @@ export type DistinguisherParser<T> = (distinguisher: string) => T; export type DocumentParser<T> = (document: string[]) => T; +export type IdvConverter<T> = (idv: Idv) => T; const LEADING_WHITESPACE = /^([ \t]+)/; const ENTRY = /^(.+?):\s*(.*)/; @@ -104,6 +105,30 @@ export class Idv { ]) ); } + + public getMergedMap<T>( + name: string, + convertIdv: IdvConverter<T> + ): Record<string, T> { + const idvMap: Map<string, Idv> = new Map(); + + (this.collections[name] ?? []).forEach(([distinguisher, document]) => { + let idv = idvMap.get(distinguisher); + if (idv == undefined) { + idvMap.set(distinguisher, (idv = new Idv())); + } + + idv.importLines(document); + }); + + const result: Record<string, T> = {}; + + for (const [distinguisher, idv] of idvMap.entries()) { + result[distinguisher] = convertIdv(idv); + } + + return result; + } } export const StringFromDocument = (lines: string[]) => lines.join("\n"); From 6c6c9386be94cccb10b23b431003817972ccb0b6 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Fri, 19 Jul 2024 23:41:11 -0400 Subject: [PATCH 10/13] Change signature for Property pattern helpers --- debug/idv.ts | 8 ++++---- lib/idv.ts | 29 +++++++++-------------------- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/debug/idv.ts b/debug/idv.ts index 7583dbe..f5447bb 100644 --- a/debug/idv.ts +++ b/debug/idv.ts @@ -1,5 +1,5 @@ import { h } from "../lib/html"; -import { Idv, StringFromDocument } from "../lib/idv"; +import { Idv, StringProperty } from "../lib/idv"; export function IdvDebug() { const textarea = h( @@ -51,8 +51,8 @@ User: tirga const UserFromDocument = (idv: Idv) => { return { - shell: idv.getProperty("Shell", String, StringFromDocument), - groups: idv.getList("Group", String, StringFromDocument), - banner: idv.getProperty("Banner", String, StringFromDocument), + shell: idv.getProperty("Shell", StringProperty), + groups: idv.getList("Group", StringProperty), + banner: idv.getProperty("Banner", StringProperty), }; }; diff --git a/lib/idv.ts b/lib/idv.ts index b0c753a..c8339f5 100644 --- a/lib/idv.ts +++ b/lib/idv.ts @@ -1,5 +1,6 @@ export type DistinguisherParser<T> = (distinguisher: string) => T; export type DocumentParser<T> = (document: string[]) => T; +export type UnionParser<T> = (distinguisher: string, document: string[]) => T; export type IdvConverter<T> = (idv: Idv) => T; const LEADING_WHITESPACE = /^([ \t]+)/; @@ -58,6 +59,7 @@ export class Idv { currentDocument = []; currentIndent = undefined; + // TODO: implement backslash escaping in the distinguisher this.collections[collection].push([distinguisher, currentDocument]); } else { throw new Error("Failed to parse a property"); @@ -68,29 +70,14 @@ export class Idv { return this; } - public getProperty<T>( - name: string, - parseDistinguisher: DistinguisherParser<T>, - parseDocument: DocumentParser<T> - ): T | undefined { + public getProperty<T>(name: string, parse: UnionParser<T>): T | undefined { const firstEntry = this.collections[name]?.[0]; - return ( - firstEntry && - (firstEntry[1].length > 0 - ? parseDocument(firstEntry[1]) - : parseDistinguisher(firstEntry[0])) - ); + return firstEntry && parse(firstEntry[0], firstEntry[1]); } - public getList<T>( - name: string, - parseDistinguisher: DistinguisherParser<T>, - parseDocument: DocumentParser<T> - ): T[] { + public getList<T>(name: string, parse: UnionParser<T>): T[] { return (this.collections[name] ?? []).map(([distinguisher, document]) => - document.length > 0 - ? parseDocument(document) - : parseDistinguisher(distinguisher) + parse(distinguisher, document) ); } @@ -131,4 +118,6 @@ export class Idv { } } -export const StringFromDocument = (lines: string[]) => lines.join("\n"); +// TODO: implement backslash-escaping in the document? +export const StringProperty = (distinguisher: string, lines: string[]) => + lines.length == 0 ? distinguisher : lines.join("\n"); From af07866ac0af4acbc69eb898aa94d8195b79c6a3 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sat, 20 Jul 2024 00:08:32 -0400 Subject: [PATCH 11/13] refactor IDV parser to use map why not --- lib/idv.ts | 18 ++++++++++-------- tsconfig.json | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/idv.ts b/lib/idv.ts index c8339f5..5cf7d2b 100644 --- a/lib/idv.ts +++ b/lib/idv.ts @@ -7,7 +7,7 @@ const LEADING_WHITESPACE = /^([ \t]+)/; const ENTRY = /^(.+?):\s*(.*)/; export class Idv { - collections: Record<string, undefined | [string, string[]][]> = {}; + collections: Map<string, undefined | [string, string[]][]> = new Map(); public static parse(input: string): Idv { const idv = new Idv(); @@ -53,14 +53,16 @@ export class Idv { if (matches) { const [, collection, distinguisher] = matches; - if (this.collections[collection] == undefined) { - this.collections[collection] = []; + if (!this.collections.has(collection)) { + this.collections.set(collection, []); } currentDocument = []; currentIndent = undefined; // TODO: implement backslash escaping in the distinguisher - this.collections[collection].push([distinguisher, currentDocument]); + this.collections + .get(collection)! + .push([distinguisher, currentDocument]); } else { throw new Error("Failed to parse a property"); } @@ -71,12 +73,12 @@ export class Idv { } public getProperty<T>(name: string, parse: UnionParser<T>): T | undefined { - const firstEntry = this.collections[name]?.[0]; + const firstEntry = this.collections.get(name)?.[0]; return firstEntry && parse(firstEntry[0], firstEntry[1]); } public getList<T>(name: string, parse: UnionParser<T>): T[] { - return (this.collections[name] ?? []).map(([distinguisher, document]) => + return (this.collections.get(name) ?? []).map(([distinguisher, document]) => parse(distinguisher, document) ); } @@ -86,7 +88,7 @@ export class Idv { parseDocument: DocumentParser<T> ): Record<string, T> { return Object.fromEntries( - (this.collections[name] ?? []).map(([distinguisher, document]) => [ + (this.collections.get(name) ?? []).map(([distinguisher, document]) => [ distinguisher, parseDocument(document), ]) @@ -99,7 +101,7 @@ export class Idv { ): Record<string, T> { const idvMap: Map<string, Idv> = new Map(); - (this.collections[name] ?? []).forEach(([distinguisher, document]) => { + (this.collections.get(name) ?? []).forEach(([distinguisher, document]) => { let idv = idvMap.get(distinguisher); if (idv == undefined) { idvMap.set(distinguisher, (idv = new Idv())); diff --git a/tsconfig.json b/tsconfig.json index 41fc1da..d1ec214 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "isolatedModules": true, "lib": ["ES2019", "DOM"], "strict": true, + "target": "ES2020", "noEmit": true } } From 763bfbc8cf20ad50169b599800cee5b8dd995684 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Sat, 20 Jul 2024 00:52:21 -0400 Subject: [PATCH 12/13] Record first set of IDV spec thoughts --- idv.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 idv.md diff --git a/idv.md b/idv.md new file mode 100644 index 0000000..904a32d --- /dev/null +++ b/idv.md @@ -0,0 +1,83 @@ +# The Indented Document Values Format + +## Overview + +The Indented Document Values (IDV) format is a text-based, whitespace-sensitive serialization format. + +IDV is designed to prioritize human readability and writability by minimizing visual noise- there are no sigils, quotes, or brackets, only colons, indentation, and (when necessary) backslash escapes. + +As a tradeoff, IDV is not a self-describing data format- you have to know what type of data an IDV document represents at the time you parse it. + +### Example + +> TODO: need something both concise and nontrivial. LDAP user data is certainly an option + +## Syntax + +IDV is a line-oriented format. Before any other parsing is done, the input is split into lines, and any trailing whitespace on a line (including line separators) is ignored. + +> TODO: possible redraft: sequence of comments, entry headers, and documents, defined by line types (blank, comment, entry header, indented) + +The lines of an IDV document represent a single flat list of Comments and Entries. + +A **Comment** is any line whose first character is a `#` character. Comment lines are for human use and are ignored by the parser. + +``` +# This line is ignored +``` + +An **Entry**'s first line is unindented and contains the name of a **Category**, up to the first `:` character, followed by a **Distinguisher**. All following lines with indentation, if any, are the entry's **Document**: + +``` +Collection: distinguisher + Indented + document + + with a blank line +``` + +1. The Category and Distinguisher are both trimmed of surrounding whitespace before being interpreted, but internal whitespace is left intact. +1. Backslash unescaping is performed on the Category and Distinguisher. +1. The Distinguisher may contain literal colons; these are treated as regular characters and carry no special meaning. +1. The first line of a Document defines the document's indentation- subsequent lines can be indented deeper, but no line may be indented _less_ than the first line. +1. It is ambiguous whether blank lines are part of a document or just aesthetic spacing for Entries; to resolve this, blank lines before and after a Document are ignored, but internal blank lines are considered part of the Document. +1. Backslash unescaping is **not** performed on the Document. However, backslashes may be processed later, when the document is interpreted. + +## Data Model + +> TODO: tuples, can be interpreted according to patterns + +## Patterns + +### Primitive Property + +> TODO: one of distinguisher | document non-empty, parsing based on expected type + +### Object Property + +> TODO: distinguisher ignored, document is IDV + +### Union Property + +> TODO: distinguisher determines how the document is parsed + +### List + +> TODO: property specified multiple times + +### Map + +> TODO: distinguisher defines key, document parsed for value + +### Property Map + +> TODO: Category defines key, parsed as property for value + +### Merged Map + +## See Also + +> TODO: +> +> - yaml +> - dpkg control files From 924d8ccf48277629a3544e415f610f2354417196 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight <tangent128@gmail.com> Date: Tue, 6 Aug 2024 21:19:45 -0400 Subject: [PATCH 13/13] Revisions to IDV spec --- idv.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 18 deletions(-) diff --git a/idv.md b/idv.md index 904a32d..9ad7de1 100644 --- a/idv.md +++ b/idv.md @@ -2,23 +2,36 @@ ## Overview -The Indented Document Values (IDV) format is a text-based, whitespace-sensitive serialization format. +The Indented Document Values (IDV) format is a meta-syntax for machine-readable textual data. IDV is designed to prioritize human readability and writability by minimizing visual noise- there are no sigils, quotes, or brackets, only colons, indentation, and (when necessary) backslash escapes. -As a tradeoff, IDV is not a self-describing data format- you have to know what type of data an IDV document represents at the time you parse it. +As a tradeoff, IDV is not a self-describing data format- while it can be used for defining a serialization or configuration format, systems using it need to layer their own semantics on top of it. ### Example -> TODO: need something both concise and nontrivial. LDAP user data is certainly an option +``` +Person: Alice + Uid: 1000 + Phone: 555-1234 + Group: users + Group: sudo + Banner: + ============================ + This is my ASCII art login message + ============================ + +Person: Bob + Uid: 1001 + Phone: 555-5656 + Group: users +``` ## Syntax IDV is a line-oriented format. Before any other parsing is done, the input is split into lines, and any trailing whitespace on a line (including line separators) is ignored. -> TODO: possible redraft: sequence of comments, entry headers, and documents, defined by line types (blank, comment, entry header, indented) - -The lines of an IDV document represent a single flat list of Comments and Entries. +### Comments A **Comment** is any line whose first character is a `#` character. Comment lines are for human use and are ignored by the parser. @@ -26,26 +39,98 @@ A **Comment** is any line whose first character is a `#` character. Comment line # This line is ignored ``` -An **Entry**'s first line is unindented and contains the name of a **Category**, up to the first `:` character, followed by a **Distinguisher**. All following lines with indentation, if any, are the entry's **Document**: +### Blank Lines + +A **Blank Line** is any line that only contains whitespace. Because trailing whitespace is always trimmed, all Blank Lines are indistinguishable from each other. + +Blank Lines are ignored unless they are part of a Document. (see below) + +### Entries + +An **Entry** is composed of one or more lines: + +#### Tags + +Each entry begins with a **Tag**, terminated by a colon (`:`). A Tag can contain any characters except leading or trailing whitespace, newlines, and colons: ``` -Collection: distinguisher - Indented - document - - with a blank line +Tag: ``` -1. The Category and Distinguisher are both trimmed of surrounding whitespace before being interpreted, but internal whitespace is left intact. -1. Backslash unescaping is performed on the Category and Distinguisher. +#### Distinguishers + +Optionally, a Distinguisher can follow the Tag on the same line. A Distinguisher can contain any characters except leading or trailing whitespace, and newlines: + +``` +Tag: distinguisher +``` + +#### Escapes + +Within Tags and Distinguishers, backslash escapes may be used to represent non-permitted or inconvenient characters: + +``` +Tag With \: And Spaces: + +Tag: \ distinguisher with leading whitespace and\nA newline +``` + +| Escape sequence | Replacement | +| --------------- | ----------------- | +| \\_\<space>_ | A literal space | +| \\n | A newline | +| \\: | A colon (`:`) | +| \\\\ | A backslash (`\`) | + +> TODO: additional escapes? ie, hex or unicode? + +#### Documents + +After the first line of an entry, any indented lines make up the **Document** portion of the entry: + +``` +Tag: distinguisher + First Line + Second Line + Third Line +``` + +The first line of a Document defines the Document's indentation- subsequent lines can be indented deeper, but no line may be indented _less_ than the first line. This indentation is removed from the beginning of each line when determining the Document's value. + +Blank Lines can not carry indentation information. To resolve this ambiguity, Documents may not begin or end with Blank Lines- such lines are ignored. Blank Lines that occur _between_ indented lines _are_ considered part of the Document. + +``` +Tag: + + The above blank line is ignored. + The below blank line is part of the Document. + + The below blank line is ignored. + +Tag: + Other stuff +``` + +Backslash escapes are _not_ processed within a Document. However, backslashes may be processed later, by higher-layered semantics. + +In many cases the Document will contain recursive IDV data, and the rules above are designed to play nicely with this case- but it is up to the concrete format to decide how to parse the Document. It could just as easily contain free text, XML, or a base64 blob. + +#### Disambiguations: + +1. The Tag and Distinguisher are both trimmed of surrounding whitespace before being interpreted, but internal whitespace is left intact. 1. The Distinguisher may contain literal colons; these are treated as regular characters and carry no special meaning. -1. The first line of a Document defines the document's indentation- subsequent lines can be indented deeper, but no line may be indented _less_ than the first line. -1. It is ambiguous whether blank lines are part of a document or just aesthetic spacing for Entries; to resolve this, blank lines before and after a Document are ignored, but internal blank lines are considered part of the Document. -1. Backslash unescaping is **not** performed on the Document. However, backslashes may be processed later, when the document is interpreted. ## Data Model -> TODO: tuples, can be interpreted according to patterns +Applying minimal interpretation, IDV data can be represented as a list of Entries. + +An Entry can be represented as a 3-tuple of: + +1. a string (the Tag) +2. a string (the optional Distinguisher) +3. a list of strings (the lines of the Document) + +How Entries are interpreted by the appication is not specified, but see below for some suggested patterns that should line up with things people usually want to do. ## Patterns