@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.
- package/LICENSE +201 -0
- package/README.md +156 -0
- package/dist/clusters.d.ts +34 -0
- package/dist/clusters.d.ts.map +1 -0
- package/dist/clusters.js +195 -0
- package/dist/clusters.js.map +1 -0
- package/dist/document.d.ts +190 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +308 -0
- package/dist/document.js.map +1 -0
- package/dist/element.d.ts +95 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +174 -0
- package/dist/element.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +122 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +297 -0
- package/dist/layout.js.map +1 -0
- package/dist/mount.d.ts +64 -0
- package/dist/mount.d.ts.map +1 -0
- package/dist/mount.js +402 -0
- package/dist/mount.js.map +1 -0
- package/dist/navigation.d.ts +56 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +121 -0
- package/dist/navigation.js.map +1 -0
- package/dist/parts.d.ts +89 -0
- package/dist/parts.d.ts.map +1 -0
- package/dist/parts.js +214 -0
- package/dist/parts.js.map +1 -0
- package/dist/projections/graph.d.ts +30 -0
- package/dist/projections/graph.d.ts.map +1 -0
- package/dist/projections/graph.js +644 -0
- package/dist/projections/graph.js.map +1 -0
- package/dist/projections/linear.d.ts +35 -0
- package/dist/projections/linear.d.ts.map +1 -0
- package/dist/projections/linear.js +157 -0
- package/dist/projections/linear.js.map +1 -0
- package/dist/projections/tree.d.ts +17 -0
- package/dist/projections/tree.d.ts.map +1 -0
- package/dist/projections/tree.js +209 -0
- package/dist/projections/tree.js.map +1 -0
- package/dist/render.d.ts +35 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +45 -0
- package/dist/render.js.map +1 -0
- package/dist/scene.d.ts +94 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +48 -0
- package/dist/scene.js.map +1 -0
- package/dist/styles.d.ts +17 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +389 -0
- package/dist/styles.js.map +1 -0
- package/dist/theme.d.ts +71 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +169 -0
- package/dist/theme.js.map +1 -0
- package/dist/vocabulary.d.ts +107 -0
- package/dist/vocabulary.d.ts.map +1 -0
- package/dist/vocabulary.js +98 -0
- package/dist/vocabulary.js.map +1 -0
- 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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"}
|
package/dist/document.js
ADDED
|
@@ -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"}
|