Compare commits
13 commits
063dd636bb
...
924d8ccf48
Author | SHA1 | Date | |
---|---|---|---|
924d8ccf48 | |||
763bfbc8cf | |||
af07866ac0 | |||
6c6c9386be | |||
63c41f07cb | |||
dc19a5ed9e | |||
525c6fa954 | |||
2f9bb73107 | |||
e90727a06c | |||
a2df9a4142 | |||
497731f62b | |||
b04ba4e622 | |||
b05cdab771 |
13 changed files with 500 additions and 34 deletions
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
17
.vscode/tasks.json
vendored
Normal file
17
.vscode/tasks.json
vendored
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
<script src="js/debug.js"></script>
|
||||
<script>
|
||||
document.body.append(...TickDebug());
|
||||
document.body.append(...IdvDebug());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
3
debug.ts
3
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 });
|
||||
|
|
58
debug/idv.ts
Normal file
58
debug/idv.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { h } from "../lib/html";
|
||||
import { Idv, StringProperty } from "../lib/idv";
|
||||
|
||||
export function IdvDebug() {
|
||||
const textarea = h(
|
||||
"textarea",
|
||||
{
|
||||
cols: 80,
|
||||
rows: 40,
|
||||
oninput(ev) {
|
||||
parse();
|
||||
},
|
||||
},
|
||||
`# Idv Testing Ground
|
||||
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");
|
||||
|
||||
function parse() {
|
||||
try {
|
||||
const idv = Idv.parse(textarea.value);
|
||||
|
||||
pre.textContent = JSON.stringify(
|
||||
idv.getMergedMap("User", UserFromDocument),
|
||||
null,
|
||||
2
|
||||
);
|
||||
} catch (e) {
|
||||
pre.textContent = String(e);
|
||||
}
|
||||
}
|
||||
parse();
|
||||
|
||||
return [textarea, pre];
|
||||
}
|
||||
|
||||
const UserFromDocument = (idv: Idv) => {
|
||||
return {
|
||||
shell: idv.getProperty("Shell", StringProperty),
|
||||
groups: idv.getList("Group", StringProperty),
|
||||
banner: idv.getProperty("Banner", StringProperty),
|
||||
};
|
||||
};
|
168
idv.md
Normal file
168
idv.md
Normal file
|
@ -0,0 +1,168 @@
|
|||
# The Indented Document Values Format
|
||||
|
||||
## Overview
|
||||
|
||||
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- 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
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
### Comments
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```
|
||||
Tag:
|
||||
```
|
||||
|
||||
#### 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.
|
||||
|
||||
## Data Model
|
||||
|
||||
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
|
||||
|
||||
### 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
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
125
lib/idv.ts
Normal file
125
lib/idv.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
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]+)/;
|
||||
const ENTRY = /^(.+?):\s*(.*)/;
|
||||
|
||||
export class Idv {
|
||||
collections: Map<string, undefined | [string, string[]][]> = new Map();
|
||||
|
||||
public static parse(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[] = [];
|
||||
|
||||
input.forEach((line) => {
|
||||
const indent = LEADING_WHITESPACE.exec(line)?.[1];
|
||||
if (indent) {
|
||||
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 == "") {
|
||||
bufferedBlankLines.push("");
|
||||
} else if (line[0] == "#") {
|
||||
// skip
|
||||
} else {
|
||||
const matches = ENTRY.exec(line);
|
||||
if (matches) {
|
||||
const [, collection, distinguisher] = matches;
|
||||
|
||||
if (!this.collections.has(collection)) {
|
||||
this.collections.set(collection, []);
|
||||
}
|
||||
|
||||
currentDocument = [];
|
||||
currentIndent = undefined;
|
||||
// TODO: implement backslash escaping in the distinguisher
|
||||
this.collections
|
||||
.get(collection)!
|
||||
.push([distinguisher, currentDocument]);
|
||||
} else {
|
||||
throw new Error("Failed to parse a property");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public getProperty<T>(name: string, parse: UnionParser<T>): T | undefined {
|
||||
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.get(name) ?? []).map(([distinguisher, document]) =>
|
||||
parse(distinguisher, document)
|
||||
);
|
||||
}
|
||||
|
||||
public getMap<T>(
|
||||
name: string,
|
||||
parseDocument: DocumentParser<T>
|
||||
): Record<string, T> {
|
||||
return Object.fromEntries(
|
||||
(this.collections.get(name) ?? []).map(([distinguisher, document]) => [
|
||||
distinguisher,
|
||||
parseDocument(document),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
public getMergedMap<T>(
|
||||
name: string,
|
||||
convertIdv: IdvConverter<T>
|
||||
): Record<string, T> {
|
||||
const idvMap: Map<string, Idv> = new Map();
|
||||
|
||||
(this.collections.get(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;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: implement backslash-escaping in the document?
|
||||
export const StringProperty = (distinguisher: string, lines: string[]) =>
|
||||
lines.length == 0 ? distinguisher : lines.join("\n");
|
67
lib/keys.ts
Normal file
67
lib/keys.ts
Normal file
|
@ -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>;
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
|
|
11
pixelflood.html
Normal file
11
pixelflood.html
Normal file
|
@ -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>
|
45
pixelflood.ts
Normal file
45
pixelflood.ts
Normal file
|
@ -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 });
|
|
@ -3,6 +3,7 @@
|
|||
"isolatedModules": true,
|
||||
"lib": ["ES2019", "DOM"],
|
||||
"strict": true,
|
||||
"target": "ES2020",
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue