Compare commits

...

13 commits

13 changed files with 500 additions and 34 deletions

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

17
.vscode/tasks.json vendored Normal file
View 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"
}
}
]
}

View file

@ -6,6 +6,7 @@
<script src="js/debug.js"></script>
<script>
document.body.append(...TickDebug());
document.body.append(...IdvDebug());
</script>
</body>
</html>

View file

@ -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
View 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
View 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

View file

@ -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
View 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
View 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>;
}

View file

@ -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
View 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
View 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 });

View file

@ -3,6 +3,7 @@
"isolatedModules": true,
"lib": ["ES2019", "DOM"],
"strict": true,
"target": "ES2020",
"noEmit": true
}
}