@go-go-golems/pbui-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/builder.d.ts +74 -0
- package/builder.js +191 -0
- package/command.d.ts +91 -0
- package/command.js +64 -0
- package/docline.d.ts +5 -0
- package/docline.js +43 -0
- package/engine.d.ts +177 -0
- package/engine.js +790 -0
- package/index.d.ts +10 -0
- package/index.js +10 -0
- package/invocation.d.ts +34 -0
- package/invocation.js +71 -0
- package/package.json +25 -0
- package/ptype.d.ts +40 -0
- package/ptype.js +103 -0
- package/registry.d.ts +40 -0
- package/registry.js +143 -0
- package/transcript-text.d.ts +3 -0
- package/transcript-text.js +31 -0
- package/transcript.d.ts +16 -0
- package/transcript.js +49 -0
- package/types.d.ts +83 -0
- package/types.js +29 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./ptype.js";
|
|
3
|
+
export * from "./registry.js";
|
|
4
|
+
export * from "./command.js";
|
|
5
|
+
export * from "./transcript.js";
|
|
6
|
+
export * from "./engine.js";
|
|
7
|
+
export * from "./docline.js";
|
|
8
|
+
export * from "./transcript-text.js";
|
|
9
|
+
export * from "./builder.js";
|
|
10
|
+
export * from "./invocation.js";
|
package/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./ptype.js";
|
|
3
|
+
export * from "./registry.js";
|
|
4
|
+
export * from "./command.js";
|
|
5
|
+
export * from "./transcript.js";
|
|
6
|
+
export * from "./engine.js";
|
|
7
|
+
export * from "./docline.js";
|
|
8
|
+
export * from "./transcript-text.js";
|
|
9
|
+
export * from "./builder.js";
|
|
10
|
+
export * from "./invocation.js";
|
package/invocation.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ArgValues, Unsubscribe } from "./types.js";
|
|
2
|
+
export type InvocationStatus = "executing" | "completed" | "failed" | "undone";
|
|
3
|
+
export interface CommandInvocation {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
/** args as collected — refs, so records survive world GC */
|
|
7
|
+
argValues: ArgValues;
|
|
8
|
+
status: InvocationStatus;
|
|
9
|
+
error?: string;
|
|
10
|
+
/** present iff the command opted into undo and completed */
|
|
11
|
+
undo?: () => void | Promise<void>;
|
|
12
|
+
/** monotonic ordering */
|
|
13
|
+
seq: number;
|
|
14
|
+
/** transcript echo line this invocation belongs to, if any */
|
|
15
|
+
echoLineId?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class InvocationLog {
|
|
18
|
+
private cap;
|
|
19
|
+
private records;
|
|
20
|
+
private listeners;
|
|
21
|
+
constructor(cap?: number);
|
|
22
|
+
record(name: string, argValues: ArgValues, echoLineId?: string): CommandInvocation;
|
|
23
|
+
private patch;
|
|
24
|
+
complete(id: string, undo?: () => void | Promise<void>): void;
|
|
25
|
+
fail(id: string, error: string): void;
|
|
26
|
+
markUndone(id: string): void;
|
|
27
|
+
byId(id: string): CommandInvocation | undefined;
|
|
28
|
+
byEchoLine(lineId: string): CommandInvocation | undefined;
|
|
29
|
+
/** the only invocation Undo will touch — linear undo (D4) */
|
|
30
|
+
lastUndoable(): CommandInvocation | undefined;
|
|
31
|
+
list(): CommandInvocation[];
|
|
32
|
+
subscribe(fn: () => void): Unsubscribe;
|
|
33
|
+
private emit;
|
|
34
|
+
}
|
package/invocation.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* Command invocation records (CLIM-JSX-004 §7) — the thesis's "command
|
|
2
|
+
* application": every executed command becomes a first-class record with
|
|
3
|
+
* lifecycle state, presentable and (when the command opted in) undoable.
|
|
4
|
+
*
|
|
5
|
+
* Core stays clock-free (decision D5): `seq` orders records; apps map to
|
|
6
|
+
* wall time at display if they want it. Undo is linear-only (decision D4).
|
|
7
|
+
*/
|
|
8
|
+
let nextSeq = 1;
|
|
9
|
+
export class InvocationLog {
|
|
10
|
+
cap;
|
|
11
|
+
records = [];
|
|
12
|
+
listeners = new Set();
|
|
13
|
+
constructor(cap = 100) {
|
|
14
|
+
this.cap = cap;
|
|
15
|
+
}
|
|
16
|
+
record(name, argValues, echoLineId) {
|
|
17
|
+
const seq = nextSeq++;
|
|
18
|
+
const inv = {
|
|
19
|
+
id: `inv-${seq}`,
|
|
20
|
+
name,
|
|
21
|
+
argValues,
|
|
22
|
+
status: "executing",
|
|
23
|
+
seq,
|
|
24
|
+
echoLineId,
|
|
25
|
+
};
|
|
26
|
+
this.records = [...this.records, inv];
|
|
27
|
+
if (this.records.length > this.cap)
|
|
28
|
+
this.records = this.records.slice(this.records.length - this.cap);
|
|
29
|
+
this.emit();
|
|
30
|
+
return inv;
|
|
31
|
+
}
|
|
32
|
+
patch(id, p) {
|
|
33
|
+
this.records = this.records.map((r) => (r.id === id ? { ...r, ...p } : r));
|
|
34
|
+
this.emit();
|
|
35
|
+
}
|
|
36
|
+
complete(id, undo) {
|
|
37
|
+
this.patch(id, { status: "completed", undo });
|
|
38
|
+
}
|
|
39
|
+
fail(id, error) {
|
|
40
|
+
this.patch(id, { status: "failed", error });
|
|
41
|
+
}
|
|
42
|
+
markUndone(id) {
|
|
43
|
+
this.patch(id, { status: "undone", undo: undefined });
|
|
44
|
+
}
|
|
45
|
+
byId(id) {
|
|
46
|
+
return this.records.find((r) => r.id === id);
|
|
47
|
+
}
|
|
48
|
+
byEchoLine(lineId) {
|
|
49
|
+
return this.records.find((r) => r.echoLineId === lineId);
|
|
50
|
+
}
|
|
51
|
+
/** the only invocation Undo will touch — linear undo (D4) */
|
|
52
|
+
lastUndoable() {
|
|
53
|
+
for (let i = this.records.length - 1; i >= 0; i--) {
|
|
54
|
+
const r = this.records[i];
|
|
55
|
+
if (r.status === "completed" && r.undo)
|
|
56
|
+
return r;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
list() {
|
|
61
|
+
return this.records;
|
|
62
|
+
}
|
|
63
|
+
subscribe(fn) {
|
|
64
|
+
this.listeners.add(fn);
|
|
65
|
+
return () => this.listeners.delete(fn);
|
|
66
|
+
}
|
|
67
|
+
emit() {
|
|
68
|
+
for (const fn of this.listeners)
|
|
69
|
+
fn();
|
|
70
|
+
}
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@go-go-golems/pbui-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PBUI core package.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Manuel Odendahl",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/go-go-golems/react-pbui.git",
|
|
11
|
+
"directory": "packages/core"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/go-go-golems/react-pbui/tree/main/packages/core#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/go-go-golems/react-pbui/issues"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"main": "./index.js",
|
|
21
|
+
"types": "./index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./index.js"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/ptype.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ObjectRef, PartLike } from "./types.js";
|
|
2
|
+
export type ParseResult<T = unknown> = {
|
|
3
|
+
ok: true;
|
|
4
|
+
value: T;
|
|
5
|
+
ref: ObjectRef;
|
|
6
|
+
label: string;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
err: string;
|
|
10
|
+
};
|
|
11
|
+
export interface PTypeSpec<T = unknown, W = unknown> {
|
|
12
|
+
name: string;
|
|
13
|
+
/** direct supertypes; omitted = child of "any" */
|
|
14
|
+
supertypes?: string[];
|
|
15
|
+
/** printed representation, e.g. `#<TASK T-3>` */
|
|
16
|
+
print?: (obj: T) => string;
|
|
17
|
+
/** keyboard half of accept: parse typed text against the world */
|
|
18
|
+
parse?: (text: string, world: W) => ParseResult<T>;
|
|
19
|
+
/** rich Describe output; falls back to the printed representation */
|
|
20
|
+
describe?: (obj: T, world: W) => PartLike[];
|
|
21
|
+
/** command started on plain left-click outside an input context */
|
|
22
|
+
defaultCommand?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface PType<T = unknown, W = unknown> extends PTypeSpec<T, W> {
|
|
25
|
+
supertypes: string[];
|
|
26
|
+
}
|
|
27
|
+
export declare class PTypes<W = unknown> {
|
|
28
|
+
private byName;
|
|
29
|
+
define<T>(spec: PTypeSpec<T, W>): PType<T, W>;
|
|
30
|
+
get(name: string): PType<unknown, W> | undefined;
|
|
31
|
+
/** is `t` a subtype of `want`? Everything is a subtype of "any". */
|
|
32
|
+
subtypep(t: string, want: string): boolean;
|
|
33
|
+
/** ["milestone","task","any"] — first parent chain, for lattice display */
|
|
34
|
+
latticePath(t: string): string[];
|
|
35
|
+
/** "MILESTONE ⊂ TASK ⊂ ANY" — the context-menu title convention */
|
|
36
|
+
latticeLabel(t: string): string;
|
|
37
|
+
print(type: string, obj: unknown, label?: string): string;
|
|
38
|
+
}
|
|
39
|
+
/** Register the "number" and "string" argument ptypes used by typed input. */
|
|
40
|
+
export declare function defineBuiltinPtypes<W>(ptypes: PTypes<W>): void;
|
package/ptype.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/* Presentation types (ptypes): a named lattice with per-type codecs.
|
|
2
|
+
*
|
|
3
|
+
* Types form a parent lattice ("milestone" ⊂ "task" ⊂ ...); "any" is the
|
|
4
|
+
* implicit top. Each type may carry a printer, a keyboard parser (the two
|
|
5
|
+
* halves of the round-trip codec, aitr-794.md:856-880), a describer, and
|
|
6
|
+
* the name of its default left-click command.
|
|
7
|
+
*/
|
|
8
|
+
export class PTypes {
|
|
9
|
+
byName = new Map();
|
|
10
|
+
define(spec) {
|
|
11
|
+
if (spec.name === "any")
|
|
12
|
+
throw new Error(`"any" is reserved`);
|
|
13
|
+
const t = { ...spec, supertypes: spec.supertypes ?? [] };
|
|
14
|
+
for (const s of t.supertypes) {
|
|
15
|
+
if (s !== "any" && !this.byName.has(s))
|
|
16
|
+
throw new Error(`unknown supertype "${s}" of "${spec.name}"`);
|
|
17
|
+
}
|
|
18
|
+
// reject cycles (supertypes must already exist, so cycles are impossible
|
|
19
|
+
// by construction, but guard against self-reference)
|
|
20
|
+
if (t.supertypes.includes(spec.name))
|
|
21
|
+
throw new Error(`ptype "${spec.name}" cannot be its own supertype`);
|
|
22
|
+
this.byName.set(spec.name, t);
|
|
23
|
+
return t;
|
|
24
|
+
}
|
|
25
|
+
get(name) {
|
|
26
|
+
return this.byName.get(name);
|
|
27
|
+
}
|
|
28
|
+
/** is `t` a subtype of `want`? Everything is a subtype of "any". */
|
|
29
|
+
subtypep(t, want) {
|
|
30
|
+
if (want === "any" || t === want)
|
|
31
|
+
return true;
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const stack = [t];
|
|
34
|
+
while (stack.length) {
|
|
35
|
+
const cur = stack.pop();
|
|
36
|
+
if (cur === want)
|
|
37
|
+
return true;
|
|
38
|
+
if (seen.has(cur))
|
|
39
|
+
continue;
|
|
40
|
+
seen.add(cur);
|
|
41
|
+
const def = this.byName.get(cur);
|
|
42
|
+
if (def)
|
|
43
|
+
stack.push(...def.supertypes);
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
/** ["milestone","task","any"] — first parent chain, for lattice display */
|
|
48
|
+
latticePath(t) {
|
|
49
|
+
const chain = [t];
|
|
50
|
+
let cur = this.byName.get(t);
|
|
51
|
+
while (cur && cur.supertypes.length > 0) {
|
|
52
|
+
const parent = cur.supertypes[0];
|
|
53
|
+
chain.push(parent);
|
|
54
|
+
cur = this.byName.get(parent);
|
|
55
|
+
}
|
|
56
|
+
if (chain[chain.length - 1] !== "any")
|
|
57
|
+
chain.push("any");
|
|
58
|
+
return chain;
|
|
59
|
+
}
|
|
60
|
+
/** "MILESTONE ⊂ TASK ⊂ ANY" — the context-menu title convention */
|
|
61
|
+
latticeLabel(t) {
|
|
62
|
+
return this.latticePath(t)
|
|
63
|
+
.map((x) => x.toUpperCase())
|
|
64
|
+
.join(" ⊂ ");
|
|
65
|
+
}
|
|
66
|
+
print(type, obj, label) {
|
|
67
|
+
const def = this.byName.get(type);
|
|
68
|
+
if (def?.print && obj !== undefined) {
|
|
69
|
+
try {
|
|
70
|
+
return def.print(obj);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// fall through to the generic form on printer errors
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return `#<${type.toUpperCase()}${label ? " " + label : ""}>`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/* ------------------------- built-in argument ptypes ----------------------- */
|
|
80
|
+
import { valueRef } from "./types.js";
|
|
81
|
+
/** Register the "number" and "string" argument ptypes used by typed input. */
|
|
82
|
+
export function defineBuiltinPtypes(ptypes) {
|
|
83
|
+
ptypes.define({
|
|
84
|
+
name: "number",
|
|
85
|
+
print: (n) => String(n),
|
|
86
|
+
parse: (text) => {
|
|
87
|
+
const n = Number(text.trim());
|
|
88
|
+
if (text.trim() === "" || Number.isNaN(n))
|
|
89
|
+
return { ok: false, err: `${text.trim() || "??"} is not a valid NUMBER` };
|
|
90
|
+
return { ok: true, value: n, ref: valueRef(n), label: String(n) };
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
ptypes.define({
|
|
94
|
+
name: "string",
|
|
95
|
+
print: (s) => JSON.stringify(s),
|
|
96
|
+
parse: (text) => {
|
|
97
|
+
const s = text.trim();
|
|
98
|
+
if (!s)
|
|
99
|
+
return { ok: false, err: "empty STRING" };
|
|
100
|
+
return { ok: true, value: s, ref: valueRef(s), label: s };
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
package/registry.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ObjectRef, PresentationRecord, PresId, Rect, Unsubscribe } from "./types.js";
|
|
2
|
+
export type RegistryEvent = {
|
|
3
|
+
kind: "register";
|
|
4
|
+
rec: PresentationRecord;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "update";
|
|
7
|
+
rec: PresentationRecord;
|
|
8
|
+
} | {
|
|
9
|
+
kind: "unregister";
|
|
10
|
+
id: PresId;
|
|
11
|
+
};
|
|
12
|
+
export declare function refKey(r: ObjectRef): string;
|
|
13
|
+
export declare class PresentationRegistry {
|
|
14
|
+
private recs;
|
|
15
|
+
private listeners;
|
|
16
|
+
private presListeners;
|
|
17
|
+
private presVersions;
|
|
18
|
+
private byRefIdx;
|
|
19
|
+
/** bump one presentation's flag version and wake its subscribers */
|
|
20
|
+
notifyPres(id: PresId): void;
|
|
21
|
+
/** accept transitions legitimately change everyone's flags (D3) */
|
|
22
|
+
notifyAllPres(): void;
|
|
23
|
+
subscribePres(id: PresId, fn: () => void): Unsubscribe;
|
|
24
|
+
presVersion(id: PresId): number;
|
|
25
|
+
register(rec: Omit<PresentationRecord, "id">): PresId;
|
|
26
|
+
private indexRef;
|
|
27
|
+
private unindexRef;
|
|
28
|
+
update(id: PresId, patch: Partial<Omit<PresentationRecord, "id">>): void;
|
|
29
|
+
unregister(id: PresId): void;
|
|
30
|
+
get(id: PresId): PresentationRecord | undefined;
|
|
31
|
+
all(): PresentationRecord[];
|
|
32
|
+
/** every presentation of the given object — cross-pane highlighting */
|
|
33
|
+
byRef(ref: ObjectRef): PresentationRecord[];
|
|
34
|
+
byType(type: string): PresentationRecord[];
|
|
35
|
+
/** smallest hit-testable presentation containing the point */
|
|
36
|
+
at(x: number, y: number): PresentationRecord | undefined;
|
|
37
|
+
subscribe(fn: (ev: RegistryEvent) => void): Unsubscribe;
|
|
38
|
+
private emit;
|
|
39
|
+
}
|
|
40
|
+
export type { Rect };
|
package/registry.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/* The PresentationRegistry — the thesis's "presentation data base" (D2).
|
|
2
|
+
*
|
|
3
|
+
* A subscribable store of every presentation currently on screen (or in the
|
|
4
|
+
* transcript). Presenters register on mount and unregister on unmount;
|
|
5
|
+
* consumers query by object ref, by ptype, or by screen point.
|
|
6
|
+
*/
|
|
7
|
+
let nextId = 1;
|
|
8
|
+
export function refKey(r) {
|
|
9
|
+
return "value" in r ? `v:${String(r.value)}` : `${r.kind}:${r.id}`;
|
|
10
|
+
}
|
|
11
|
+
export class PresentationRegistry {
|
|
12
|
+
recs = new Map();
|
|
13
|
+
listeners = new Set();
|
|
14
|
+
/* per-presentation invalidation channel (CLIM-JSX-005 §6.2): hover-paced
|
|
15
|
+
* flag changes notify exactly the affected presentations */
|
|
16
|
+
presListeners = new Map();
|
|
17
|
+
presVersions = new Map();
|
|
18
|
+
/* refKey -> ids index so byRef is O(presentations-of-that-object) */
|
|
19
|
+
byRefIdx = new Map();
|
|
20
|
+
/** bump one presentation's flag version and wake its subscribers */
|
|
21
|
+
notifyPres(id) {
|
|
22
|
+
this.presVersions.set(id, (this.presVersions.get(id) ?? 0) + 1);
|
|
23
|
+
const set = this.presListeners.get(id);
|
|
24
|
+
if (set)
|
|
25
|
+
for (const fn of set)
|
|
26
|
+
fn();
|
|
27
|
+
}
|
|
28
|
+
/** accept transitions legitimately change everyone's flags (D3) */
|
|
29
|
+
notifyAllPres() {
|
|
30
|
+
for (const id of this.recs.keys())
|
|
31
|
+
this.notifyPres(id);
|
|
32
|
+
}
|
|
33
|
+
subscribePres(id, fn) {
|
|
34
|
+
let set = this.presListeners.get(id);
|
|
35
|
+
if (!set) {
|
|
36
|
+
set = new Set();
|
|
37
|
+
this.presListeners.set(id, set);
|
|
38
|
+
}
|
|
39
|
+
set.add(fn);
|
|
40
|
+
return () => {
|
|
41
|
+
set.delete(fn);
|
|
42
|
+
if (set.size === 0)
|
|
43
|
+
this.presListeners.delete(id);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
presVersion(id) {
|
|
47
|
+
return this.presVersions.get(id) ?? 0;
|
|
48
|
+
}
|
|
49
|
+
register(rec) {
|
|
50
|
+
const id = `p${nextId++}`;
|
|
51
|
+
const full = { ...rec, id };
|
|
52
|
+
this.recs.set(id, full);
|
|
53
|
+
this.indexRef(full);
|
|
54
|
+
this.emit({ kind: "register", rec: full });
|
|
55
|
+
return id;
|
|
56
|
+
}
|
|
57
|
+
indexRef(rec) {
|
|
58
|
+
const k = refKey(rec.ref);
|
|
59
|
+
let set = this.byRefIdx.get(k);
|
|
60
|
+
if (!set) {
|
|
61
|
+
set = new Set();
|
|
62
|
+
this.byRefIdx.set(k, set);
|
|
63
|
+
}
|
|
64
|
+
set.add(rec.id);
|
|
65
|
+
}
|
|
66
|
+
unindexRef(rec) {
|
|
67
|
+
const k = refKey(rec.ref);
|
|
68
|
+
const set = this.byRefIdx.get(k);
|
|
69
|
+
if (set) {
|
|
70
|
+
set.delete(rec.id);
|
|
71
|
+
if (set.size === 0)
|
|
72
|
+
this.byRefIdx.delete(k);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
update(id, patch) {
|
|
76
|
+
const cur = this.recs.get(id);
|
|
77
|
+
if (!cur)
|
|
78
|
+
return;
|
|
79
|
+
const next = { ...cur, ...patch };
|
|
80
|
+
this.unindexRef(cur);
|
|
81
|
+
this.recs.set(id, next);
|
|
82
|
+
this.indexRef(next);
|
|
83
|
+
this.emit({ kind: "update", rec: next });
|
|
84
|
+
}
|
|
85
|
+
unregister(id) {
|
|
86
|
+
const cur = this.recs.get(id);
|
|
87
|
+
if (!cur)
|
|
88
|
+
return;
|
|
89
|
+
this.recs.delete(id);
|
|
90
|
+
this.unindexRef(cur);
|
|
91
|
+
this.presListeners.delete(id);
|
|
92
|
+
this.presVersions.delete(id);
|
|
93
|
+
this.emit({ kind: "unregister", id });
|
|
94
|
+
}
|
|
95
|
+
get(id) {
|
|
96
|
+
return this.recs.get(id);
|
|
97
|
+
}
|
|
98
|
+
all() {
|
|
99
|
+
return [...this.recs.values()];
|
|
100
|
+
}
|
|
101
|
+
/** every presentation of the given object — cross-pane highlighting */
|
|
102
|
+
byRef(ref) {
|
|
103
|
+
const ids = this.byRefIdx.get(refKey(ref));
|
|
104
|
+
if (!ids)
|
|
105
|
+
return [];
|
|
106
|
+
const out = [];
|
|
107
|
+
for (const id of ids) {
|
|
108
|
+
const r = this.recs.get(id);
|
|
109
|
+
if (r)
|
|
110
|
+
out.push(r);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
byType(type) {
|
|
115
|
+
return this.all().filter((r) => r.type === type);
|
|
116
|
+
}
|
|
117
|
+
/** smallest hit-testable presentation containing the point */
|
|
118
|
+
at(x, y) {
|
|
119
|
+
let best;
|
|
120
|
+
let bestArea = Infinity;
|
|
121
|
+
for (const r of this.recs.values()) {
|
|
122
|
+
const b = r.bounds?.();
|
|
123
|
+
if (!b)
|
|
124
|
+
continue;
|
|
125
|
+
if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
|
|
126
|
+
const area = b.w * b.h;
|
|
127
|
+
if (area < bestArea) {
|
|
128
|
+
bestArea = area;
|
|
129
|
+
best = r;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return best;
|
|
134
|
+
}
|
|
135
|
+
subscribe(fn) {
|
|
136
|
+
this.listeners.add(fn);
|
|
137
|
+
return () => this.listeners.delete(fn);
|
|
138
|
+
}
|
|
139
|
+
emit(ev) {
|
|
140
|
+
for (const fn of this.listeners)
|
|
141
|
+
fn(ev);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* Canonical text rendering of transcript records.
|
|
2
|
+
*
|
|
3
|
+
* This is the format the golden-transcript tests pin: the echo grammar is
|
|
4
|
+
* a specification, and refactors must not move a character of it.
|
|
5
|
+
*
|
|
6
|
+
* [echo] Command: Compare Sites (site-a) {site SITE-ALPHA}
|
|
7
|
+
* [out] Compared **SITE-ALPHA** with ...
|
|
8
|
+
* [err] ...
|
|
9
|
+
*
|
|
10
|
+
* pres parts render as {type label}, bold as **text**.
|
|
11
|
+
*/
|
|
12
|
+
export function renderRecord(rec) {
|
|
13
|
+
const body = rec.parts
|
|
14
|
+
.map((p) => {
|
|
15
|
+
switch (p.t) {
|
|
16
|
+
case "text":
|
|
17
|
+
return p.s;
|
|
18
|
+
case "bold":
|
|
19
|
+
return `**${p.s}**`;
|
|
20
|
+
case "err":
|
|
21
|
+
return p.s;
|
|
22
|
+
case "pres":
|
|
23
|
+
return `{${p.type} ${p.label}}`;
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
.join("");
|
|
27
|
+
return `[${rec.kind.padEnd(4)}] ${body}`;
|
|
28
|
+
}
|
|
29
|
+
export function renderTranscript(records) {
|
|
30
|
+
return records.map(renderRecord).join("\n") + "\n";
|
|
31
|
+
}
|
package/transcript.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { OutputKind, OutputRecord, PartLike, Unsubscribe } from "./types.js";
|
|
2
|
+
export declare class Transcript {
|
|
3
|
+
private cap;
|
|
4
|
+
private records;
|
|
5
|
+
private listeners;
|
|
6
|
+
constructor(cap?: number);
|
|
7
|
+
print(kind: OutputKind, ...parts: PartLike[]): OutputRecord;
|
|
8
|
+
out(...parts: PartLike[]): OutputRecord;
|
|
9
|
+
echo(...parts: PartLike[]): OutputRecord;
|
|
10
|
+
err(...parts: PartLike[]): OutputRecord;
|
|
11
|
+
clear(): void;
|
|
12
|
+
/** stable snapshot for useSyncExternalStore */
|
|
13
|
+
lines(): OutputRecord[];
|
|
14
|
+
subscribe(fn: () => void): Unsubscribe;
|
|
15
|
+
private emit;
|
|
16
|
+
}
|
package/transcript.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* The transcript: a capped, subscribable list of output records whose
|
|
2
|
+
* object parts remain live presentations (D7). */
|
|
3
|
+
import { toPart } from "./types.js";
|
|
4
|
+
let nextLineId = 1;
|
|
5
|
+
export class Transcript {
|
|
6
|
+
cap;
|
|
7
|
+
records = [];
|
|
8
|
+
listeners = new Set();
|
|
9
|
+
constructor(cap = 300) {
|
|
10
|
+
this.cap = cap;
|
|
11
|
+
}
|
|
12
|
+
print(kind, ...parts) {
|
|
13
|
+
const rec = {
|
|
14
|
+
id: `l${nextLineId++}`,
|
|
15
|
+
kind,
|
|
16
|
+
parts: parts.map(toPart),
|
|
17
|
+
};
|
|
18
|
+
this.records = [...this.records, rec];
|
|
19
|
+
if (this.records.length > this.cap)
|
|
20
|
+
this.records = this.records.slice(this.records.length - this.cap);
|
|
21
|
+
this.emit();
|
|
22
|
+
return rec;
|
|
23
|
+
}
|
|
24
|
+
out(...parts) {
|
|
25
|
+
return this.print("out", ...parts);
|
|
26
|
+
}
|
|
27
|
+
echo(...parts) {
|
|
28
|
+
return this.print("echo", ...parts);
|
|
29
|
+
}
|
|
30
|
+
err(...parts) {
|
|
31
|
+
return this.print("err", ...parts);
|
|
32
|
+
}
|
|
33
|
+
clear() {
|
|
34
|
+
this.records = [];
|
|
35
|
+
this.emit();
|
|
36
|
+
}
|
|
37
|
+
/** stable snapshot for useSyncExternalStore */
|
|
38
|
+
lines() {
|
|
39
|
+
return this.records;
|
|
40
|
+
}
|
|
41
|
+
subscribe(fn) {
|
|
42
|
+
this.listeners.add(fn);
|
|
43
|
+
return () => this.listeners.delete(fn);
|
|
44
|
+
}
|
|
45
|
+
emit() {
|
|
46
|
+
for (const fn of this.listeners)
|
|
47
|
+
fn();
|
|
48
|
+
}
|
|
49
|
+
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Reference to a domain object. `kind` names the object family, `id` its key.
|
|
2
|
+
* Immediate values (numbers, strings, enum choices) use kind "value". */
|
|
3
|
+
export type ObjectRef = {
|
|
4
|
+
kind: string;
|
|
5
|
+
id: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "value";
|
|
8
|
+
value: unknown;
|
|
9
|
+
};
|
|
10
|
+
export declare function valueRef(value: unknown): ObjectRef;
|
|
11
|
+
export declare function refEquals(a: ObjectRef, b: ObjectRef): boolean;
|
|
12
|
+
/** Apps supply a resolver from refs to live domain objects.
|
|
13
|
+
* `undefined` means the object no longer exists (stale presentation). */
|
|
14
|
+
export interface Resolver {
|
|
15
|
+
resolve(ref: ObjectRef): unknown | undefined;
|
|
16
|
+
}
|
|
17
|
+
export interface Rect {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
w: number;
|
|
21
|
+
h: number;
|
|
22
|
+
}
|
|
23
|
+
export type PresId = string;
|
|
24
|
+
/** A presentation: a typed, on-screen (or in-transcript) handle to a domain
|
|
25
|
+
* object. This is the record stored in the PresentationRegistry — the
|
|
26
|
+
* thesis's "presentation data base". */
|
|
27
|
+
export interface PresentationRecord {
|
|
28
|
+
id: PresId;
|
|
29
|
+
/** ptype name */
|
|
30
|
+
type: string;
|
|
31
|
+
ref: ObjectRef;
|
|
32
|
+
/** display label used in echoes, menus and the mouse-doc line */
|
|
33
|
+
label: string;
|
|
34
|
+
paneId?: string;
|
|
35
|
+
parentId?: PresId;
|
|
36
|
+
/** participation during a foreign input context (CLIM-JSX-005 §5):
|
|
37
|
+
* gated (default) — dimmed and swallowed; active — stays interactive,
|
|
38
|
+
* left-click may run a duringAccept command without aborting the
|
|
39
|
+
* context; fallthrough — gesture-transparent (canvas overlays) */
|
|
40
|
+
mode?: "gated" | "active" | "fallthrough";
|
|
41
|
+
/** lazily measured screen bounds; null/undefined = not hit-testable */
|
|
42
|
+
bounds?: () => Rect | null;
|
|
43
|
+
}
|
|
44
|
+
/** The subset of a presentation needed to supply/echo an argument.
|
|
45
|
+
* Argument values collected by the accept loop are stored in this shape,
|
|
46
|
+
* whether they came from a click, the keyboard, or a menu. */
|
|
47
|
+
export interface ArgValue {
|
|
48
|
+
type: string;
|
|
49
|
+
ref: ObjectRef;
|
|
50
|
+
label: string;
|
|
51
|
+
}
|
|
52
|
+
export type ArgValues = Record<string, ArgValue>;
|
|
53
|
+
export type OutputPart = {
|
|
54
|
+
t: "text";
|
|
55
|
+
s: string;
|
|
56
|
+
} | {
|
|
57
|
+
t: "bold";
|
|
58
|
+
s: string;
|
|
59
|
+
} | {
|
|
60
|
+
t: "err";
|
|
61
|
+
s: string;
|
|
62
|
+
}
|
|
63
|
+
/** a live presentation embedded in transcript output — stays sensitive */
|
|
64
|
+
| {
|
|
65
|
+
t: "pres";
|
|
66
|
+
type: string;
|
|
67
|
+
ref: ObjectRef;
|
|
68
|
+
label: string;
|
|
69
|
+
};
|
|
70
|
+
export type OutputKind = "out" | "echo" | "err";
|
|
71
|
+
export interface OutputRecord {
|
|
72
|
+
id: string;
|
|
73
|
+
kind: OutputKind;
|
|
74
|
+
parts: OutputPart[];
|
|
75
|
+
}
|
|
76
|
+
/** Loose input accepted by print helpers: strings become text parts. */
|
|
77
|
+
export type PartLike = OutputPart | string;
|
|
78
|
+
export declare function toPart(p: PartLike): OutputPart;
|
|
79
|
+
export declare const S: (s: string) => OutputPart;
|
|
80
|
+
export declare const B: (s: string) => OutputPart;
|
|
81
|
+
export declare const E: (s: string) => OutputPart;
|
|
82
|
+
export declare const P: (type: string, ref: ObjectRef, label: string) => OutputPart;
|
|
83
|
+
export type Unsubscribe = () => void;
|