@issuegraph/viewer 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.
Files changed (67) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +156 -0
  3. package/dist/clusters.d.ts +34 -0
  4. package/dist/clusters.d.ts.map +1 -0
  5. package/dist/clusters.js +195 -0
  6. package/dist/clusters.js.map +1 -0
  7. package/dist/document.d.ts +190 -0
  8. package/dist/document.d.ts.map +1 -0
  9. package/dist/document.js +308 -0
  10. package/dist/document.js.map +1 -0
  11. package/dist/element.d.ts +95 -0
  12. package/dist/element.d.ts.map +1 -0
  13. package/dist/element.js +174 -0
  14. package/dist/element.js.map +1 -0
  15. package/dist/index.d.ts +40 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +33 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/layout.d.ts +122 -0
  20. package/dist/layout.d.ts.map +1 -0
  21. package/dist/layout.js +297 -0
  22. package/dist/layout.js.map +1 -0
  23. package/dist/mount.d.ts +64 -0
  24. package/dist/mount.d.ts.map +1 -0
  25. package/dist/mount.js +402 -0
  26. package/dist/mount.js.map +1 -0
  27. package/dist/navigation.d.ts +56 -0
  28. package/dist/navigation.d.ts.map +1 -0
  29. package/dist/navigation.js +121 -0
  30. package/dist/navigation.js.map +1 -0
  31. package/dist/parts.d.ts +89 -0
  32. package/dist/parts.d.ts.map +1 -0
  33. package/dist/parts.js +214 -0
  34. package/dist/parts.js.map +1 -0
  35. package/dist/projections/graph.d.ts +30 -0
  36. package/dist/projections/graph.d.ts.map +1 -0
  37. package/dist/projections/graph.js +644 -0
  38. package/dist/projections/graph.js.map +1 -0
  39. package/dist/projections/linear.d.ts +35 -0
  40. package/dist/projections/linear.d.ts.map +1 -0
  41. package/dist/projections/linear.js +157 -0
  42. package/dist/projections/linear.js.map +1 -0
  43. package/dist/projections/tree.d.ts +17 -0
  44. package/dist/projections/tree.d.ts.map +1 -0
  45. package/dist/projections/tree.js +209 -0
  46. package/dist/projections/tree.js.map +1 -0
  47. package/dist/render.d.ts +35 -0
  48. package/dist/render.d.ts.map +1 -0
  49. package/dist/render.js +45 -0
  50. package/dist/render.js.map +1 -0
  51. package/dist/scene.d.ts +94 -0
  52. package/dist/scene.d.ts.map +1 -0
  53. package/dist/scene.js +48 -0
  54. package/dist/scene.js.map +1 -0
  55. package/dist/styles.d.ts +17 -0
  56. package/dist/styles.d.ts.map +1 -0
  57. package/dist/styles.js +389 -0
  58. package/dist/styles.js.map +1 -0
  59. package/dist/theme.d.ts +71 -0
  60. package/dist/theme.d.ts.map +1 -0
  61. package/dist/theme.js +169 -0
  62. package/dist/theme.js.map +1 -0
  63. package/dist/vocabulary.d.ts +107 -0
  64. package/dist/vocabulary.d.ts.map +1 -0
  65. package/dist/vocabulary.js +98 -0
  66. package/dist/vocabulary.js.map +1 -0
  67. package/package.json +56 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * The viewer's input: one normalised document, and nothing else.
3
+ *
4
+ * THIS IS A PLAIN, JSON-SAFE SHAPE ON PURPOSE. `@issuegraph/derive` describes
5
+ * its own return value as "an in-process value, not a payload" — it carries a
6
+ * live closure and two `Map`s, so it cannot cross a serialization boundary. A
7
+ * viewer that took it directly would be unusable from a server-rendered host, a
8
+ * worker, or anything that receives its data over a wire, and it would couple
9
+ * the innermost rendering layer to a package it is not allowed to need.
10
+ *
11
+ * So the shape here is a PROJECTION of that value rather than the value: a host
12
+ * that uses `deriveIssueOrder` maps it across in a few lines, and a host that
13
+ * ranks its backlog some other way supplies the same fields from its own
14
+ * source. The viewer derives no order, resolves no reference, and reads no
15
+ * tracker — everything it draws was given to it.
16
+ *
17
+ * `normalizeDocument` is the one entry point. It never throws: a document
18
+ * assembled by hand is untrusted input, not a contract, and a renderer that
19
+ * dies on a dangling edge is worse than one that draws what it can and says
20
+ * what it dropped.
21
+ */
22
+ import { type EdgeField } from '@issuegraph/core';
23
+ /**
24
+ * How an issue's rank came about, in the three forms the format and a host's
25
+ * own configuration can produce. The viewer FORMATS provenance; it never
26
+ * decides it — which is why every arm carries its own text rather than a code
27
+ * the viewer would have to interpret.
28
+ */
29
+ export type RankProvenance =
30
+ /** A host ordering query matched this issue. `label` is the query as written. */
31
+ {
32
+ readonly kind: 'matched-query';
33
+ readonly index: number;
34
+ readonly label: string;
35
+ }
36
+ /** No query matched; the issue sits in its declared priority tier. */
37
+ | {
38
+ readonly kind: 'declared-tier';
39
+ readonly priority: number;
40
+ }
41
+ /**
42
+ * Effective priority promoted it. `notation` is the spec's own `P3 -> 0`
43
+ * form and `promotedBy` names the open dependents the urgency arrived
44
+ * through — both supplied, because the viewer cannot recompute either.
45
+ */
46
+ | {
47
+ readonly kind: 'promotion';
48
+ readonly notation: string;
49
+ readonly promotedBy: readonly string[];
50
+ };
51
+ /** Where a hold came from. The two families never share a treatment. */
52
+ export type HoldFamily =
53
+ /**
54
+ * The graph itself holds it — a `blocked-by` that is still open, or a
55
+ * serialize component someone else is in. These render INLINE at the rank
56
+ * the work would have taken, because "why isn't my P1 running" has to be
57
+ * answerable in place.
58
+ */
59
+ 'graph'
60
+ /**
61
+ * The runner or the tracker holds it — claimed, parked, an external gate.
62
+ * These are not facts about the work, so they earn no rank slot and collapse
63
+ * into a footer group.
64
+ */
65
+ | 'tracker';
66
+ /** One reason a slot is held, and which family it belongs to. */
67
+ export interface ViewerHold {
68
+ readonly family: HoldFamily;
69
+ /** Human-readable, host-authored. The viewer renders it verbatim. */
70
+ readonly reason: string;
71
+ }
72
+ /** One issue the document knows about. */
73
+ export interface ViewerIssue {
74
+ /** The model's node key: `"12"`, or `"owner/repo#12"` when qualified. */
75
+ readonly key: string;
76
+ /** Rendered as the row's leading text. Escaped at render; never parsed. */
77
+ readonly title: string;
78
+ /**
79
+ * The deep-link target for the `owner/repo#N` chip. OPTIONAL BY DESIGN: the
80
+ * viewer renders no link it was not given, because inventing one means
81
+ * knowing a tracker's URL shape, which is precisely the knowledge this layer
82
+ * must not carry.
83
+ */
84
+ readonly url?: string | undefined;
85
+ readonly open: boolean;
86
+ /** Declared priority, as the host resolved it. */
87
+ readonly priority: number;
88
+ /** Absent when the host has no provenance to state. */
89
+ readonly provenance?: RankProvenance | undefined;
90
+ }
91
+ /** One relationship, exactly as the format declares it. */
92
+ export interface ViewerEdge {
93
+ readonly field: EdgeField;
94
+ readonly from: string;
95
+ readonly to: string;
96
+ }
97
+ /**
98
+ * One position in the order. A together unit is ONE slot with several members,
99
+ * not one slot per member — the same rule `@issuegraph/derive` applies, carried
100
+ * across so the two cannot disagree about what a position is.
101
+ */
102
+ export interface ViewerSlot {
103
+ /**
104
+ * The 1-based rank, or `null` when the slot is held. A held slot renders `—`
105
+ * rather than a number: it has no position in the sequence, and printing one
106
+ * would claim work is queued that nothing can start.
107
+ */
108
+ readonly rank: number | null;
109
+ /** The member that placed the slot — the detail surface's subject. */
110
+ readonly lead: string;
111
+ /** Every member, in the order the host supplied. */
112
+ readonly members: readonly string[];
113
+ readonly ready: boolean;
114
+ /** Empty when ready. Each entry names one failed readiness condition. */
115
+ readonly holds: readonly ViewerHold[];
116
+ /**
117
+ * The rank this slot becomes ready after, when the host knows it. Drives the
118
+ * hollow readiness station; `null` renders a filled one when ready and a
119
+ * dashed one when held.
120
+ */
121
+ readonly readyAfterRank?: number | null | undefined;
122
+ }
123
+ /** An issue the order deliberately never works. */
124
+ export interface ViewerExclusion {
125
+ readonly key: string;
126
+ /** The issue this one defers to. */
127
+ readonly canonical: string;
128
+ readonly reason: 'duplicate-of';
129
+ }
130
+ /** The order, as the host derived it. */
131
+ export interface ViewerOrder {
132
+ /**
133
+ * Every slot in derivation order. A HELD slot keeps its position — it is
134
+ * never moved to the end — so its rank column reads `—` exactly where the
135
+ * work would have sat.
136
+ */
137
+ readonly slots: readonly ViewerSlot[];
138
+ readonly excluded: readonly ViewerExclusion[];
139
+ }
140
+ /** Everything the viewer draws. */
141
+ export interface ViewerDocument {
142
+ readonly issues: readonly ViewerIssue[];
143
+ readonly edges: readonly ViewerEdge[];
144
+ readonly order: ViewerOrder;
145
+ }
146
+ /**
147
+ * A normalised document plus an index over it. The index exists so no
148
+ * projection re-scans the issue list per row, and so every projection resolves
149
+ * a key the same way.
150
+ */
151
+ export interface NormalizedDocument {
152
+ readonly issues: readonly ViewerIssue[];
153
+ readonly edges: readonly ViewerEdge[];
154
+ readonly order: ViewerOrder;
155
+ /** Key -> issue. Every edge and slot member below is present here. */
156
+ readonly byKey: ReadonlyMap<string, ViewerIssue>;
157
+ /** Key -> the edges touching it, in document order. */
158
+ readonly edgesOf: ReadonlyMap<string, readonly ViewerEdge[]>;
159
+ /** Keys that appear in no slot and on no edge. */
160
+ readonly isolated: readonly string[];
161
+ /**
162
+ * Key -> the `decomposed-from` origin it declares that this document does not
163
+ * carry.
164
+ *
165
+ * KEPT RATHER THAN DROPPED WITH THE EDGE. The edge itself cannot be drawn —
166
+ * a node outside the set has no bounds — but the tree projection has to be
167
+ * able to say "this is a root because its origin is not here" instead of
168
+ * "this is a root". An absence rendered as a value licenses a false
169
+ * conclusion, and the false conclusion here is that an issue has no
170
+ * provenance when in fact its provenance was not supplied.
171
+ */
172
+ readonly outOfSetOrigins: ReadonlyMap<string, string>;
173
+ }
174
+ export interface NormalizeResult {
175
+ readonly document: NormalizedDocument;
176
+ /**
177
+ * What was dropped and why. A host surfaces these; the viewer does not draw
178
+ * them, because a diagnostic is a fact about the DATA and this layer renders
179
+ * the work order.
180
+ */
181
+ readonly diagnostics: readonly string[];
182
+ }
183
+ /**
184
+ * Read a document into the shape every projection consumes.
185
+ *
186
+ * Deterministic and total: the same input always produces the same output, and
187
+ * no input produces a throw. Call it once per render and pass the result down.
188
+ */
189
+ export declare function normalizeDocument(input: ViewerDocument): NormalizeResult;
190
+ //# sourceMappingURL=document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,308 @@
1
+ /**
2
+ * The viewer's input: one normalised document, and nothing else.
3
+ *
4
+ * THIS IS A PLAIN, JSON-SAFE SHAPE ON PURPOSE. `@issuegraph/derive` describes
5
+ * its own return value as "an in-process value, not a payload" — it carries a
6
+ * live closure and two `Map`s, so it cannot cross a serialization boundary. A
7
+ * viewer that took it directly would be unusable from a server-rendered host, a
8
+ * worker, or anything that receives its data over a wire, and it would couple
9
+ * the innermost rendering layer to a package it is not allowed to need.
10
+ *
11
+ * So the shape here is a PROJECTION of that value rather than the value: a host
12
+ * that uses `deriveIssueOrder` maps it across in a few lines, and a host that
13
+ * ranks its backlog some other way supplies the same fields from its own
14
+ * source. The viewer derives no order, resolves no reference, and reads no
15
+ * tracker — everything it draws was given to it.
16
+ *
17
+ * `normalizeDocument` is the one entry point. It never throws: a document
18
+ * assembled by hand is untrusted input, not a contract, and a renderer that
19
+ * dies on a dangling edge is worse than one that draws what it can and says
20
+ * what it dropped.
21
+ */
22
+ import { EDGE_CARDINALITY, isEdgeField, isSymmetricEdgeField, } from '@issuegraph/core';
23
+ /**
24
+ * The schemes a deep link may carry.
25
+ *
26
+ * A DOCUMENT IS UNTRUSTED INPUT — this module says so at the top, and a `url`
27
+ * is the one field that becomes an executable surface when it reaches the DOM.
28
+ * `javascript:` and `data:` hrefs run in the host's origin, so escaping the
29
+ * attribute is not enough: the value has to be refused. An ALLOWLIST rather
30
+ * than a denylist, because the scheme space is open and a miss here publishes
31
+ * exactly the bug the check exists to stop.
32
+ *
33
+ * A relative URL is allowed: it cannot name a scheme, and a host embedding the
34
+ * viewer under its own routes has a legitimate reason to pass one.
35
+ */
36
+ const LINKABLE_SCHEMES = new Set(['http:', 'https:', 'mailto:']);
37
+ function isLinkable(url) {
38
+ // CANONICALIZE THE WAY A URL PARSER DOES, THEN READ THE SCHEME. Reading the
39
+ // raw string is not enough: the URL parser REMOVES every ASCII tab, newline
40
+ // and carriage return from ANYWHERE in the input before it reads anything,
41
+ // and then strips leading C0 controls and spaces. So `java<TAB>script:alert(1)`
42
+ // reaches the browser as `javascript:` while a raw scan finds no scheme at
43
+ // all, reads the value as relative, and links it.
44
+ //
45
+ // The test therefore has to canonicalize first. Stripping only the leading
46
+ // controls — which is what this did — left the whole embedded-control class
47
+ // open, which is the same fail-open the allowlist exists to close.
48
+ const canonical = url.replace(/[\t\n\r]/g, '').replace(/^[\x00-\x20]+/, '');
49
+ // A scheme is `ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) ":"` (RFC 3986 3.1).
50
+ // No match means the value is relative, which carries no scheme to abuse.
51
+ const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):/.exec(canonical);
52
+ if (scheme === null)
53
+ return true;
54
+ return LINKABLE_SCHEMES.has(`${scheme[1].toLowerCase()}:`);
55
+ }
56
+ function indexIssues(issues, diagnostics) {
57
+ const byKey = new Map();
58
+ const kept = [];
59
+ for (const issue of issues) {
60
+ if (issue.key === '') {
61
+ diagnostics.push('issue with an empty key dropped');
62
+ continue;
63
+ }
64
+ if (byKey.has(issue.key)) {
65
+ diagnostics.push(`duplicate issue key ${issue.key}: the first occurrence is kept`);
66
+ continue;
67
+ }
68
+ let kept_issue = issue;
69
+ if (issue.url !== undefined && issue.url !== '' && !isLinkable(issue.url)) {
70
+ diagnostics.push(`${issue.key} declares a url whose scheme the viewer will not link; the deep link was dropped`);
71
+ const { url: _dropped, ...rest } = issue;
72
+ kept_issue = rest;
73
+ }
74
+ byKey.set(issue.key, kept_issue);
75
+ kept.push(kept_issue);
76
+ }
77
+ return { kept, byKey };
78
+ }
79
+ /**
80
+ * An edge survives only when the format recognises its field AND both ends are
81
+ * issues this document carries.
82
+ *
83
+ * DROPPING A DANGLING EDGE IS THE POINT, not a shortcut. Every projection
84
+ * derives an endpoint from a node's computed bounds, and a node that does not
85
+ * exist has none — so an edge kept here would either be drawn at coordinates
86
+ * nobody computed or crash the layout. Dropping it and SAYING SO leaves the
87
+ * absence visible to the host, which is the only party that can resolve it.
88
+ */
89
+ function indexEdges(edges, byKey, slotOf, diagnostics) {
90
+ const kept = [];
91
+ const edgesOf = new Map();
92
+ const outOfSetOrigins = new Map();
93
+ // SINGLE-CARDINALITY IS ASKED OF THE FORMAT, NOT LISTED HERE. `EDGE_CARDINALITY`
94
+ // names four single-reference fields and this guard used to enforce exactly
95
+ // one of them, so a document declaring two `duplicate-of` (or `serialize-with`,
96
+ // or `together-with`) edges from one issue kept both: two badges for one
97
+ // relationship and two graph paths, where the format promises one fact and
98
+ // this reader promises a diagnostic. Reading the constant means the fifth
99
+ // single field, whenever the spec adds one, is covered the day it lands.
100
+ // THE RULE BELONGS HERE, where the edges are read — not later, per projection.
101
+ // A second origin that resolved while the first did not left the tree nesting
102
+ // an issue under one parent while printing that it came from another, with
103
+ // neither the diagnostic nor the first-origin rule the projection promises.
104
+ // First declared wins, whether or not it resolves.
105
+ // KEYED ON (from, field), WHICH IS WHAT MAKES IT SAFE FOR THE SYMMETRIC FIELDS.
106
+ // `A together-with B` and `B together-with A` are one undirected fact written
107
+ // the way the format asks, and each endpoint declared ONCE — so keying on the
108
+ // endpoint PAIR would reject the reverse declaration as a repeat, and keying on
109
+ // the target would reject a legitimate three-member group in which B and C both
110
+ // point at A. Each issue gets one declaration; who points at it is not its
111
+ // budget to spend.
112
+ const claimed = new Set();
113
+ // Every edge's identity, symmetric or directed — see the branch below for what
114
+ // each key collapses.
115
+ const edgeSeen = new Set();
116
+ for (const edge of edges) {
117
+ if (!isEdgeField(edge.field)) {
118
+ diagnostics.push(`edge ${edge.from} -> ${edge.to} names an unknown field and was dropped`);
119
+ continue;
120
+ }
121
+ // BEFORE THE SYMMETRIC DEDUPE BELOW, AND THAT ORDER IS THE CORRECTNESS. The
122
+ // dedupe drops a symmetric edge's reverse declaration as a repeat of one
123
+ // fact — so counted after it, `B together-with A` would spend nothing, and a
124
+ // `B together-with C` arriving later would read as B's first declaration
125
+ // when it is really its second. Counting first means every declaration an
126
+ // author WROTE is counted, whatever the reader later collapses or drops.
127
+ if (EDGE_CARDINALITY[edge.field] === 'single') {
128
+ const claim = `${edge.field}\u0000${edge.from}`;
129
+ if (claimed.has(claim)) {
130
+ diagnostics.push(`${edge.from} declares more than one ${edge.field}; the format allows one, so only the first is kept and ${edge.to} was dropped`);
131
+ continue;
132
+ }
133
+ claimed.add(claim);
134
+ }
135
+ const missing = !byKey.has(edge.from) ? edge.from : !byKey.has(edge.to) ? edge.to : null;
136
+ if (missing !== null) {
137
+ // The edge cannot be drawn, but a missing PROVENANCE origin is worth
138
+ // remembering: the tree projection has to distinguish "no origin" from
139
+ // "an origin this document was not given".
140
+ if (edge.field === 'decomposed-from' && missing === edge.to && byKey.has(edge.from)) {
141
+ if (!outOfSetOrigins.has(edge.from))
142
+ outOfSetOrigins.set(edge.from, edge.to);
143
+ }
144
+ diagnostics.push(`${edge.field} edge ${edge.from} -> ${edge.to} names ${missing}, which this document does not carry, and was dropped`);
145
+ continue;
146
+ }
147
+ if (edge.from === edge.to) {
148
+ diagnostics.push(`${edge.field} self-edge on ${edge.from} was dropped`);
149
+ continue;
150
+ }
151
+ // A `together-with` THE ORDER DOES NOT GROUP CANNOT BE DRAWN AT ALL. It is
152
+ // rendered as an enclosure around one slot's members rather than as an arc,
153
+ // and the enclosures come from the slot table — so an edge whose endpoints
154
+ // sit in different slots, or in none, produced no mark anywhere while still
155
+ // counting toward the relationship total the legend reports. Measured: a
156
+ // document with one such edge said "2 relationships" and drew one.
157
+ // DROPPED AND REPORTED RATHER THAN DRAWN SOME OTHER WAY. The alternative is a
158
+ // fallback connector, which invents a second visual form for one relationship
159
+ // and leaves the reader to reconcile them. The input is inconsistent — an
160
+ // author declared a grouping the order does not carry — and saying so is what
161
+ // this reader does with every other undrawable edge above.
162
+ // THE COUNT IS WHY IT MATTERS: it is taken from the kept edges, so dropping
163
+ // here makes the total the legend prints match what the canvas contains.
164
+ if (edge.field === 'together-with') {
165
+ const home = slotOf.get(edge.from);
166
+ if (home === undefined || home !== slotOf.get(edge.to)) {
167
+ diagnostics.push(`together-with edge ${edge.from} -> ${edge.to} is not carried by any one order slot, so nothing could draw it, and it was dropped`);
168
+ continue;
169
+ }
170
+ }
171
+ // A SYMMETRIC FIELD IS ONE UNDIRECTED FACT, so both endpoints declaring it
172
+ // is the NORMAL way to write it down, not a malformed document — and the
173
+ // reader that keeps both renders the one relationship twice: two badges,
174
+ // two paths, and a component edge count nobody can reconcile with what is
175
+ // drawn. Canonicalizing the pair collapses the reverse declaration AND a
176
+ // repeat of the same direction, because the identity of an undirected edge
177
+ // is its endpoint SET.
178
+ // NO DIAGNOSTIC. Unlike the drops above, nothing here is wrong with the
179
+ // document: `A serialize-with B` plus `B serialize-with A` is exactly what
180
+ // the format asks an author to write, so reporting it would train readers
181
+ // to ignore the diagnostics that do mean something.
182
+ // ONE FACT IS DRAWN ONCE, WHICHEVER DIRECTION THE FIELD HAS. The symmetric
183
+ // arm canonicalizes the endpoint PAIR, because `A serialize-with B` and
184
+ // `B serialize-with A` are one undirected fact. A DIRECTED field has no such
185
+ // equivalence — `A blocked-by B` and `B blocked-by A` are two different
186
+ // claims — but an EXACT repeat of one of them is still one relationship
187
+ // written twice, and this reader kept both: two identical paths, two badges,
188
+ // and a blocking count in the refusal summary inflated past what the canvas
189
+ // contains. Measured: `A blocked-by B` listed twice survived as two edges.
190
+ // THE KEY IS WHAT DIFFERS, not the rule. Symmetric collapses the reverse;
191
+ // directed keeps it and collapses only the exact triple — so the distinct
192
+ // `B -> A` edge is preserved, which is the half that must not be lost.
193
+ // NO DIAGNOSTIC, for the reason the symmetric arm already gives: a repeat is
194
+ // the format's normal redundancy rather than a malformed document, and
195
+ // reporting it would train readers to ignore the drops that do mean
196
+ // something.
197
+ const identity = isSymmetricEdgeField(edge.field)
198
+ ? edge.from < edge.to
199
+ ? `${edge.field}\u0000${edge.from}\u0000${edge.to}`
200
+ : `${edge.field}\u0000${edge.to}\u0000${edge.from}`
201
+ : `${edge.field}\u0000${edge.from}\u0000${edge.to}`;
202
+ if (edgeSeen.has(identity))
203
+ continue;
204
+ edgeSeen.add(identity);
205
+ kept.push(edge);
206
+ for (const end of [edge.from, edge.to]) {
207
+ const existing = edgesOf.get(end);
208
+ if (existing === undefined)
209
+ edgesOf.set(end, [edge]);
210
+ else
211
+ existing.push(edge);
212
+ }
213
+ }
214
+ return { kept, edgesOf, outOfSetOrigins };
215
+ }
216
+ /**
217
+ * A slot survives only when at least one member is an issue the document
218
+ * carries; unknown members are dropped from it, with a diagnostic each.
219
+ */
220
+ function normalizeSlots(slots, byKey, diagnostics) {
221
+ const kept = [];
222
+ // ONE ISSUE HOLDS ONE PLACE IN THE ORDER. A hand-built document can name the
223
+ // same issue in two slots, and keeping both publishes the key twice: two rows
224
+ // render `tabindex="0"` for one focused key, while `indexOf` and the mount
225
+ // index can only ever address the first — so roving focus breaks and the
226
+ // later row is unreachable. The first placement wins, as everywhere else here.
227
+ const placed = new Set();
228
+ for (const slot of slots) {
229
+ const members = slot.members.filter((member) => {
230
+ if (!byKey.has(member)) {
231
+ diagnostics.push(`slot member ${member} is not an issue in this document and was dropped`);
232
+ return false;
233
+ }
234
+ if (placed.has(member)) {
235
+ diagnostics.push(`${member} is already placed in an earlier slot; the later placement was dropped`);
236
+ return false;
237
+ }
238
+ placed.add(member);
239
+ return true;
240
+ });
241
+ if (members.length === 0) {
242
+ diagnostics.push(`slot led by ${slot.lead} has no known member and was dropped`);
243
+ continue;
244
+ }
245
+ const lead = members.includes(slot.lead) ? slot.lead : members[0];
246
+ if (lead !== slot.lead) {
247
+ diagnostics.push(`slot lead ${slot.lead} is not among its members; ${lead} leads instead`);
248
+ }
249
+ kept.push(Object.freeze({ ...slot, lead, members: Object.freeze(members) }));
250
+ }
251
+ return { kept, placed };
252
+ }
253
+ /**
254
+ * Read a document into the shape every projection consumes.
255
+ *
256
+ * Deterministic and total: the same input always produces the same output, and
257
+ * no input produces a throw. Call it once per render and pass the result down.
258
+ */
259
+ export function normalizeDocument(input) {
260
+ const diagnostics = [];
261
+ const { kept: issues, byKey } = indexIssues(input.issues, diagnostics);
262
+ // SLOTS BEFORE EDGES, because one edge rule needs to know them. `together-with`
263
+ // is drawn as an ENCLOSURE around a slot's members rather than as an arc, so an
264
+ // edge whose endpoints are not in one slot has nothing to draw it — and
265
+ // `indexEdges` is where an undrawable edge is dropped and reported. Nothing in
266
+ // `normalizeSlots` reads the edges, so the two are free to swap.
267
+ const { kept: slots, placed } = normalizeSlots(input.order.slots, byKey, diagnostics);
268
+ // WHICH SLOT OWNS EACH KEY, which is the whole question the rule asks.
269
+ const slotOf = new Map();
270
+ for (const slot of slots)
271
+ for (const member of slot.members)
272
+ slotOf.set(member, slot.lead);
273
+ const { kept: edges, edgesOf, outOfSetOrigins } = indexEdges(input.edges, byKey, slotOf, diagnostics);
274
+ // AN EXCLUSION IS A POSITION TOO. The rule is one issue, one position — and
275
+ // it has to cover this field as well as the slots, because the projections
276
+ // publish both into one focus order. A key in a slot AND in `excluded`, or
277
+ // twice in `excluded`, rendered two keyed rows: `ArrowDown` from the first
278
+ // resolved to the same key and returned `none`, so nothing after it was
279
+ // reachable.
280
+ const excluded = input.order.excluded.filter((exclusion) => {
281
+ if (!byKey.has(exclusion.key)) {
282
+ diagnostics.push(`excluded ${exclusion.key} is not an issue in this document and was dropped`);
283
+ return false;
284
+ }
285
+ if (placed.has(exclusion.key)) {
286
+ diagnostics.push(`${exclusion.key} already holds a position in the order; the exclusion was dropped`);
287
+ return false;
288
+ }
289
+ placed.add(exclusion.key);
290
+ return true;
291
+ });
292
+ const isolated = issues
293
+ .filter((issue) => !placed.has(issue.key) && (edgesOf.get(issue.key) ?? []).length === 0)
294
+ .map((issue) => issue.key);
295
+ return Object.freeze({
296
+ document: Object.freeze({
297
+ issues: Object.freeze(issues),
298
+ edges: Object.freeze(edges),
299
+ order: Object.freeze({ slots: Object.freeze(slots), excluded: Object.freeze(excluded) }),
300
+ byKey,
301
+ edgesOf,
302
+ isolated: Object.freeze(isolated),
303
+ outOfSetOrigins,
304
+ }),
305
+ diagnostics: Object.freeze(diagnostics),
306
+ });
307
+ }
308
+ //# sourceMappingURL=document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.js","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * One description of rendered structure, and the two renderers that consume it.
3
+ *
4
+ * A projection returns an {@link ElementSpec} tree. {@link renderMarkup} turns
5
+ * it into a string — server rendering, and every test in this package that
6
+ * asserts what is drawn. {@link materialize} builds the same tree out of real
7
+ * nodes for {@link ../mount.ts mountViewer}.
8
+ *
9
+ * THE POINT IS THAT THEY CANNOT DISAGREE. The obvious alternative — build
10
+ * markup, then set `innerHTML` — makes the string path and the node path one
11
+ * implementation but leaves the package unable to render without a parser; the
12
+ * other obvious alternative, writing the DOM path separately, makes them two
13
+ * implementations that drift. One spec with two total walks over it is the only
14
+ * arrangement where a test on the markup is evidence about the nodes.
15
+ *
16
+ * Nothing here reaches a global. {@link materialize} takes the document it
17
+ * builds into, which is what keeps this module importable on a runtime that has
18
+ * no DOM at all.
19
+ */
20
+ /** Attribute values a spec may carry. `false`, `null` and `undefined` omit it. */
21
+ export type AttrValue = string | number | boolean | null | undefined;
22
+ export interface ElementSpec {
23
+ readonly tag: string;
24
+ /** `svg` puts this element and its subtree in the SVG namespace. */
25
+ readonly ns?: 'svg' | undefined;
26
+ readonly attrs?: Readonly<Record<string, AttrValue>> | undefined;
27
+ readonly children?: readonly SpecChild[] | undefined;
28
+ }
29
+ export type SpecChild = ElementSpec | string;
30
+ export declare const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
31
+ /**
32
+ * Render a spec tree to markup.
33
+ *
34
+ * Pure and deterministic: attribute order follows the spec's own key order, so
35
+ * the same tree always produces the same bytes. That is what makes a
36
+ * two-themes-one-markup assertion possible.
37
+ *
38
+ * ITERATIVE, LIKE EVERY OTHER WALK IN THIS PACKAGE. A recursive renderer costs
39
+ * one call frame per level of nesting, and the tree projection's depth is the
40
+ * host's `decomposed-from` chain — which nothing bounds. `renderViewer`
41
+ * documents itself as TOTAL, so a document that makes it throw falsifies the
42
+ * claim whatever the depth was. This was the third stack-depth finding on this
43
+ * package; making the traversal iterative removes the class rather than
44
+ * capping the input, which would have invented a product rule the design does
45
+ * not have.
46
+ */
47
+ export declare function renderMarkup(spec: ElementSpec, inheritedNs?: 'svg' | undefined): string;
48
+ /**
49
+ * The slice of `Document` this package builds with.
50
+ *
51
+ * Declared structurally rather than as `Document` so a host can pass any
52
+ * implementation that answers these five calls — and so this module stays
53
+ * honest about how little of the DOM it actually needs.
54
+ */
55
+ export interface SpecDocument {
56
+ createElement(tag: string): SpecElement;
57
+ createElementNS(namespace: string, tag: string): SpecElement;
58
+ createTextNode(text: string): SpecNode;
59
+ }
60
+ export interface SpecNode {
61
+ readonly nodeType?: number;
62
+ }
63
+ export interface SpecElement extends SpecNode {
64
+ setAttribute(name: string, value: string): void;
65
+ appendChild(child: SpecNode): void;
66
+ /**
67
+ * Optional because building a tree never needs it — only the shell's roving
68
+ * tab stop does, and an implementation that cannot move focus should be able
69
+ * to say so by omitting it rather than by throwing.
70
+ */
71
+ focus?(): void;
72
+ }
73
+ export interface MaterializeOptions {
74
+ readonly ns?: 'svg' | undefined;
75
+ /**
76
+ * Called for every element as it is built. `mount` uses it to index the
77
+ * elements carrying a key, which is cheaper and less brittle than walking the
78
+ * finished tree back down — and it keeps this module to ONE traversal, so the
79
+ * markup and the node paths cannot diverge in how they descend.
80
+ */
81
+ readonly onElement?: ((spec: ElementSpec, element: SpecElement) => void) | undefined;
82
+ }
83
+ /**
84
+ * Build a spec tree as nodes in the supplied document.
85
+ *
86
+ * Attributes follow `renderMarkup`'s rules exactly — same omissions, same
87
+ * number coercion, same bare-attribute handling for `true` — because the two
88
+ * walks are only useful as one grammar.
89
+ */
90
+ export declare function materialize(doc: SpecDocument, spec: ElementSpec, options?: MaterializeOptions): SpecElement;
91
+ /** A spec, with `undefined` children dropped — the common conditional-child case. */
92
+ export declare function element(tag: string, attrs: Readonly<Record<string, AttrValue>>, children?: readonly (SpecChild | null | undefined | false)[]): ElementSpec;
93
+ /** The same, in the SVG namespace. */
94
+ export declare function svg(tag: string, attrs: Readonly<Record<string, AttrValue>>, children?: readonly (SpecChild | null | undefined | false)[]): ElementSpec;
95
+ //# sourceMappingURL=element.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../src/element.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,kFAAkF;AAClF,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;AAErE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,GAAG,SAAS,CAAC;IACjE,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,CAAC;CACtD;AAED,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC;AAE7C,eAAO,MAAM,aAAa,+BAA+B,CAAC;AAiD1D;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAyCvF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;IACxC,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC;IAC7D,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;CACxC;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,WAAY,SAAQ,QAAQ;IAC3C,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAC;IACnC;;;;OAIG;IACH,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CACtF;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,WAAW,EACjB,OAAO,GAAE,kBAAuB,GAC/B,WAAW,CAwDb;AAED,qFAAqF;AACrF,wBAAgB,OAAO,CACrB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,EAC1C,QAAQ,GAAE,SAAS,CAAC,SAAS,GAAG,IAAI,GAAG,SAAS,GAAG,KAAK,CAAC,EAAO,GAC/D,WAAW,CAQb;AAED,sCAAsC;AACtC,wBAAgB,GAAG,CACjB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,EAC1C,QAAQ,GAAE,SAAS,CAAC,SAAS,GAAG,IAAI,GAAG,SAAS,GAAG,KAAK,CAAC,EAAO,GAC/D,WAAW,CAEb"}