figdown 0.1.8 → 0.2.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/.claude-plugin/plugin.json +1 -1
- package/dist/figdown.js +255 -78
- package/dist/figdown.mjs +255 -78
- package/examples/evpn-fabric.svg +1 -1
- package/examples/showcase/arp-resolution.svg +1 -1
- package/examples/showcase/ethernet-frame.svg +1 -1
- package/examples/showcase/l2-forwarding-logic.svg +9 -9
- package/examples/showcase/tcp-handshake.svg +1 -1
- package/examples/showcase/tcp-header.svg +1 -1
- package/examples/showcase/tcp-state-machine.svg +77 -55
- package/guide/expressing.md +3 -3
- package/guide/layout.md +16 -16
- package/guide/showcase.md +62 -56
- package/package.json +2 -2
- package/skill/figdown/SKILL.md +26 -0
- package/skill/figdown/figdown.html +307 -99
- package/skill/figdown/reference/experimental/flowchart.md +59 -18
- package/skill/figdown/reference/experimental/statechart.md +92 -0
- package/skill/figdown/reference/reading.md +13 -1
- package/skill/figdown/reference/scene.md +18 -3
package/dist/figdown.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// figdown.js — FigDown embeddable library (0.
|
|
1
|
+
// figdown.js — FigDown embeddable library (0.2.0)
|
|
2
2
|
// GENERATED FILE, DO NOT EDIT. Built from editor/figdown.html.
|
|
3
3
|
// Regenerate with: node tools/make-lib.js
|
|
4
4
|
(function (root, factory) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
}
|
|
11
11
|
}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
12
12
|
'use strict';
|
|
13
|
-
var VERSION = "0.
|
|
13
|
+
var VERSION = "0.2.0";
|
|
14
14
|
|
|
15
15
|
// ---- engine (extracted verbatim from editor/figdown.html) ----
|
|
16
16
|
var __engine = (function () {
|
|
@@ -24,13 +24,30 @@ const SHAPES = ['box','rounded','circle','ellipse','diamond','cylinder'];
|
|
|
24
24
|
// input to that promise, and under core §13 a 0.x renderer may differ from
|
|
25
25
|
// the next — which makes the recorded version the only thing that can
|
|
26
26
|
// explain a diff between two renderings of one source.
|
|
27
|
-
const FIGDOWN_VERSION = '0.
|
|
27
|
+
const FIGDOWN_VERSION = '0.2.0';
|
|
28
|
+
// `STATECHART-GENRE-SCOPE`: the language number moved for the first time. The dev
|
|
29
|
+
// counter does NOT reset (core §13.0.4 — `N` counts source states of the
|
|
30
|
+
// engine and only ever increases), so 0.1 is followed by
|
|
31
|
+
// 0.2, not by 0.2.
|
|
32
|
+
//
|
|
33
|
+
// LANGUAGE VERSIONS THIS ENGINE ACCEPTS (core §13.7 second bullet: an engine
|
|
34
|
+
// MUST state this, and stating the release version alone does not satisfy
|
|
35
|
+
// it). Declared here, in ONE place, so the header check and the documented
|
|
36
|
+
// interface cannot drift:
|
|
37
|
+
const LANG_VERSIONS = ['0.1', '0.2'];
|
|
38
|
+
// Genres per declared language version. `Y` never removes (core §13.0), so
|
|
39
|
+
// each row is a superset of the one above it, and `figdown 0.1 <anything>`
|
|
40
|
+
// resolves against exactly the list it resolved against before `STATECHART-GENRE-SCOPE`.
|
|
41
|
+
const GENRES_BY_VERSION = {
|
|
42
|
+
'0.1': ['block','topology','flowchart','bitfield','table','timing'],
|
|
43
|
+
'0.2': ['block','topology','flowchart','bitfield','table','timing','statechart']
|
|
44
|
+
};
|
|
28
45
|
// Retired shape VALUES keep a named diagnostic (PROCESS §5(d)), the same way
|
|
29
46
|
// retired option keys do: `cloud` was the one value that named a domain
|
|
30
47
|
// (the internet cloud) in an enum the language keeps purely geometric
|
|
31
48
|
// (`SHAPE-ENUM-VOCABULARY`/`EXTERNAL-EDGE-ENDPOINTS`), so it was removed rather than demoted.
|
|
32
49
|
const RETIRED_SHAPES = {
|
|
33
|
-
cloud: 'shape=cloud has been retired: use shape=ellipse and put the meaning in the label or a class= (shapes are pure geometry, `SHAPE-ENUM-VOCABULARY`) (MIGRATIONS)'
|
|
50
|
+
cloud: 'shape=cloud has been retired: use shape=ellipse and put the meaning in the label or a class= (shapes are pure geometry, `SHAPE-ENUM-VOCABULARY`) (MIGRATIONS 0.1)'
|
|
34
51
|
};
|
|
35
52
|
// Colors are CSS hex (#rgb / #rrggbb) or CSS named colors (spec §1) — the
|
|
36
53
|
// 147 CSS/SVG color keywords (lowercase) plus `transparent`. Anything else
|
|
@@ -285,9 +302,17 @@ const DIRECTIVE_OPTS={
|
|
|
285
302
|
process:['shape','fill','stroke','style','class','in','plane','width','height'],
|
|
286
303
|
decision:['shape','fill','stroke','style','class','in','plane','width','height'],
|
|
287
304
|
terminator:['shape','fill','stroke','style','class','in','plane','width','height'],
|
|
305
|
+
// `GENRE-NODE-SPELLING`: `state` IS `node` under `statechart` — a rename, not a
|
|
306
|
+
// new directive, so it takes `node`'s keys exactly and nothing more.
|
|
307
|
+
state:['shape','fill','stroke','style','class','in','plane','width','height'],
|
|
288
308
|
group:['fill','stroke','style','gap','class','plane'],
|
|
289
309
|
external:['plane'],
|
|
290
310
|
edge:['style','class','fill','stroke','plane','label','taillabel','headlabel'],
|
|
311
|
+
// `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`: same rename argument — the connector's option set is one set
|
|
312
|
+
// under three spellings, listed three times only because the tables are
|
|
313
|
+
// keyed by the surface word an author actually wrote.
|
|
314
|
+
flowline:['style','class','fill','stroke','plane','label','taillabel','headlabel'],
|
|
315
|
+
transition:['style','class','fill','stroke','plane','label','taillabel','headlabel'],
|
|
291
316
|
plane:['z','z-index'], flow:[], rank:[],
|
|
292
317
|
bundle:['fill','stroke','style','plane'],
|
|
293
318
|
threshold:['in','at','offset','fill','stroke','style','plane'],
|
|
@@ -370,10 +395,10 @@ const ENUM_OPT_KEYS=['shape','style','numbering','extend','type'];
|
|
|
370
395
|
// `color=` sets the TEXT — and once `color=` is live again the engine can no
|
|
371
396
|
// longer diagnose a pre-0.1 document at all.
|
|
372
397
|
const RETIRED_OPT_KEYS={
|
|
373
|
-
w:'w= has been renamed: use width= (SVG, CSS, DOT, mxGraph and D2 all spell it in full — no standard abbreviates it) (MIGRATIONS)',
|
|
374
|
-
h:'h= has been renamed: use height= (SVG, CSS, DOT, mxGraph and D2 all spell it in full — no standard abbreviates it) (MIGRATIONS)',
|
|
375
|
-
dir:'dir= has been renamed: use extend= (HTML\'s dir= is text writing direction; this one says which way the band extends from its anchor) (MIGRATIONS)',
|
|
376
|
-
text:'text= has been retired: v0.1 has NO label-colour key — the label colour is DERIVED from the fill it sits on (core §5), and the owner-level key that could replace it would colour an edge\'s [tail]/[mid]/[head] labels identically, which is the wrong shape (core §9 `ANNOTATION-LOCATOR-SPLIT`). Delete the key; if the distinction was knowledge, write it in the label or a class= meaning (§5, `PRESENTATION-AS-MEANING-CARRIER`) (MIGRATIONS)',
|
|
398
|
+
w:'w= has been renamed: use width= (SVG, CSS, DOT, mxGraph and D2 all spell it in full — no standard abbreviates it) (MIGRATIONS 0.1)',
|
|
399
|
+
h:'h= has been renamed: use height= (SVG, CSS, DOT, mxGraph and D2 all spell it in full — no standard abbreviates it) (MIGRATIONS 0.1)',
|
|
400
|
+
dir:'dir= has been renamed: use extend= (HTML\'s dir= is text writing direction; this one says which way the band extends from its anchor) (MIGRATIONS 0.1)',
|
|
401
|
+
text:'text= has been retired: v0.1 has NO label-colour key — the label colour is DERIVED from the fill it sits on (core §5), and the owner-level key that could replace it would colour an edge\'s [tail]/[mid]/[head] labels identically, which is the wrong shape (core §9 `ANNOTATION-LOCATOR-SPLIT`). Delete the key; if the distinction was knowledge, write it in the label or a class= meaning (§5, `PRESENTATION-AS-MEANING-CARRIER`) (MIGRATIONS 0.1)',
|
|
377
402
|
// `COLOUR-KEY-STATUS`. This is the ONLY key in the language whose diagnostic
|
|
378
403
|
// must name two eras and refuse to choose between them: the same six
|
|
379
404
|
// characters meant the FILL before this release and the LABEL after
|
|
@@ -392,10 +417,10 @@ const RETIRED_OPT_KEYS={
|
|
|
392
417
|
// those observables, says plainly when a file has neither, and cites the
|
|
393
418
|
// release only as a MIGRATIONS lookup. Same rule as the migration tool's
|
|
394
419
|
// `color=` family, which reads the same evidence to decide its refusals.
|
|
395
|
-
color:'color= has been retired: the same six characters set the box FILL in one era of this language and the LABEL colour in another, and this line does not say which — which is why the key is gone rather than renamed. READ IT OFF THE REST OF THE DOCUMENT. A file that also writes fill= cannot be from the FILL era (the two keys never coexisted), so its color= was a LABEL colour: delete it and let the derived default apply (core §5). A file still writing the spellings that were retired before the LABEL era (w= h= unit= via= dir= kind= layer= boundary wrap optional) cannot be from that era, so its color= was a FILL: write fill= instead. A file with NEITHER carries no evidence at all, and the two readings then differ only in what was DRAWN — as a FILL the value painted the box interior, as a LABEL colour it painted only the text. If the colour carried meaning, put that meaning in the label or a class= (§5, `PRESENTATION-AS-MEANING-CARRIER`). tools/migrate-figdown.js reads this evidence for you and REFUSES the wrong --color-means=fill|text (MIGRATIONS)',
|
|
420
|
+
color:'color= has been retired: the same six characters set the box FILL in one era of this language and the LABEL colour in another, and this line does not say which — which is why the key is gone rather than renamed. READ IT OFF THE REST OF THE DOCUMENT. A file that also writes fill= cannot be from the FILL era (the two keys never coexisted), so its color= was a LABEL colour: delete it and let the derived default apply (core §5). A file still writing the spellings that were retired before the LABEL era (w= h= unit= via= dir= kind= layer= boundary wrap optional) cannot be from that era, so its color= was a FILL: write fill= instead. A file with NEITHER carries no evidence at all, and the two readings then differ only in what was DRAWN — as a FILL the value painted the box interior, as a LABEL colour it painted only the text. If the colour carried meaning, put that meaning in the label or a class= (§5, `PRESENTATION-AS-MEANING-CARRIER`). tools/migrate-figdown.js reads this evidence for you and REFUSES the wrong --color-means=fill|text (MIGRATIONS 0.1)',
|
|
396
421
|
kind:'kind= has been renamed: on a node use shape= (geometric; the label text carries the device semantics — MIGRATIONS 0.1), on a chart use type= (Vega, Chart.js and ECharts all spell the chart-type key "type" — MIGRATIONS 0.1). One spelling was retired on node and live on plot at the same time, inside one namespace; 0.1 closed that.',
|
|
397
|
-
layer:'layer= has been renamed: use plane= (mxGraph makes a layer a containment parent that establishes coordinates; Inkscape layers can carry a transform; OGC WMS layers carry an SRS; CSS @layer is cascade priority. None of those is what this key does, and SVG has no layer at all) (MIGRATIONS)',
|
|
398
|
-
labels:'labels= has been renamed: use data= (WaveDrom\'s own key for exactly this is `data`, "an array of signal labels" — one per value cell of the lane) (MIGRATIONS)',
|
|
422
|
+
layer:'layer= has been renamed: use plane= (mxGraph makes a layer a containment parent that establishes coordinates; Inkscape layers can carry a transform; OGC WMS layers carry an SRS; CSS @layer is cascade priority. None of those is what this key does, and SVG has no layer at all) (MIGRATIONS 0.1)',
|
|
423
|
+
labels:'labels= has been renamed: use data= (WaveDrom\'s own key for exactly this is `data`, "an array of signal labels" — one per value cell of the lane) (MIGRATIONS 0.1)',
|
|
399
424
|
// 0.1 (`EDGE-GEOMETRY-CONSTRUCTS`). These six keys end in a WITHDRAWAL, not a rename, so
|
|
400
425
|
// their messages have a shape no earlier retirement in this table has: they
|
|
401
426
|
// name no replacement spelling, because there is none. `via=`/`src=`/`dst=`
|
|
@@ -409,28 +434,28 @@ const RETIRED_OPT_KEYS={
|
|
|
409
434
|
tailport:'tailport= has been WITHDRAWN with the `path` directive (`EDGE-GEOMETRY-CONSTRUCTS`): the construct is removed from the language, not renamed, so there is no spelling to migrate to. Attachment to a named site addressed by semantic role IS inside the stable prior-art intersection; FigDown\'s realisation was not (a fraction on the EDGE is mxGraph-only, and written-order attachment has zero prior art in any surveyed system). Restoring it needs an edge-identity construct first. Delete the line; the edge draws under auto layout. The decision and its evidence: MIGRATIONS 0.1, core §9 `EDGE-IDENTITY-AND-GEOMETRY`, decisions/registry.md',
|
|
410
435
|
headport:'headport= has been WITHDRAWN with the `path` directive (`EDGE-GEOMETRY-CONSTRUCTS`): the construct is removed from the language, not renamed, so there is no spelling to migrate to. Attachment to a named site addressed by semantic role IS inside the stable prior-art intersection; FigDown\'s realisation was not (a fraction on the EDGE is mxGraph-only, and written-order attachment has zero prior art in any surveyed system). Restoring it needs an edge-identity construct first. Delete the line; the edge draws under auto layout. The decision and its evidence: MIGRATIONS 0.1, core §9 `EDGE-IDENTITY-AND-GEOMETRY`, decisions/registry.md',
|
|
411
436
|
routing:'routing= has been WITHDRAWN with the `path` directive (`EDGE-GEOMETRY-CONSTRUCTS`): the construct is removed from the language, not renamed, so there is no spelling to migrate to. The per-edge routing SCOPE was inside the stable prior-art intersection and is deliberately lost with its host line — an override needs an edge to address, and FigDown has no edge-identity construct. Delete the line. The decision and its evidence: MIGRATIONS 0.1, core §9 `EDGE-IDENTITY-AND-GEOMETRY`, decisions/registry.md',
|
|
412
|
-
unit:'unit= has been renamed: use word= (RFC 2360 §3.1: "a sequence of long words in network byte order, with each word horizontal on the page"; RFC 791 §3.1 measures the header in "32 bit words". Mermaid names the identical setting bitsPerRow — semantically right, camelCase barred. `unit=32` also inverts count-vs-unit, reading as "the unit is 32", and C\'s "unit" is the addressable storage unit, not the row width) (MIGRATIONS)',
|
|
413
|
-
z:'z= has been renamed: use z-index= (CSS spells the stacking concept z-index, and RULE 4.2 takes the standard spelling in full; a single-letter key is structurally risky next to the closed timing lane alphabet, `LANE-ALPHABET-KEY-RESERVATION`) (MIGRATIONS)',
|
|
437
|
+
unit:'unit= has been renamed: use word= (RFC 2360 §3.1: "a sequence of long words in network byte order, with each word horizontal on the page"; RFC 791 §3.1 measures the header in "32 bit words". Mermaid names the identical setting bitsPerRow — semantically right, camelCase barred. `unit=32` also inverts count-vs-unit, reading as "the unit is 32", and C\'s "unit" is the addressable storage unit, not the row width) (MIGRATIONS 0.1)',
|
|
438
|
+
z:'z= has been renamed: use z-index= (CSS spells the stacking concept z-index, and RULE 4.2 takes the standard spelling in full; a single-letter key is structurally risky next to the closed timing lane alphabet, `LANE-ALPHABET-KEY-RESERVATION`) (MIGRATIONS 0.1)',
|
|
414
439
|
// `DESCRIPTION-KEY-SPELLING`. The spelling leaves the LANGUAGE, so the message fires
|
|
415
440
|
// wherever it appears (the `w=`/`h=`/`unit=` placement test, RULE 6.2).
|
|
416
|
-
note:'note= has been renamed: use description= (IEEE 1685-2022 spells this channel `description`; SystemRDL\'s `desc` is barred by RULE 4.2 as an abbreviation. The rename is defensive: `ANNOTATION-LOCATOR-SPLIT` files `note` as the highest-demand v0.2 annotation construct — ~66 figure-identities, 20 independent reinventions — and that one will be a DRAWN callout, so a never-drawing `note=` beside an always-drawing `note` would be one spelling with two opposite behaviours) (MIGRATIONS)',
|
|
417
|
-
level:'level= has been DELETED, not renamed: it drew a reference plane through a 3-D bar chart, has zero uses in either downstream corpus and zero 3-D bar charts to draw it on, was the only construct whose caption the ENGINE wrote rather than the author, and its parseFloat grammar uniquely accepted 1e3 where every other number in the language is \\d+(\\.\\d+)? — delete the key (MIGRATIONS)'
|
|
441
|
+
note:'note= has been renamed: use description= (IEEE 1685-2022 spells this channel `description`; SystemRDL\'s `desc` is barred by RULE 4.2 as an abbreviation. The rename is defensive: `ANNOTATION-LOCATOR-SPLIT` files `note` as the highest-demand v0.2 annotation construct — ~66 figure-identities, 20 independent reinventions — and that one will be a DRAWN callout, so a never-drawing `note=` beside an always-drawing `note` would be one spelling with two opposite behaviours) (MIGRATIONS 0.1)',
|
|
442
|
+
level:'level= has been DELETED, not renamed: it drew a reference plane through a 3-D bar chart, has zero uses in either downstream corpus and zero 3-D bar charts to draw it on, was the only construct whose caption the ENGINE wrote rather than the author, and its parseFloat grammar uniquely accepted 1e3 where every other number in the language is \\d+(\\.\\d+)? — delete the key (MIGRATIONS 0.1)'
|
|
418
443
|
};
|
|
419
444
|
// `PLANE-KEYWORD-SPELLING`: the keyword `plane`/`plane=` was spelled `layer`/`layer=`.
|
|
420
|
-
const RETIRED_LAYER='layer has been renamed: use plane (in mxGraph — the geometry model FigDown adopted — a layer is a CONTAINMENT PARENT that establishes coordinates, so layer=overlay reads as "reparent and re-origin this element", which FigDown does not do; Inkscape layers are <g> and may carry a transform, OGC WMS layers each carry an SRS, and CSS @layer is cascade priority with no visual meaning. SVG has no layer concept at all. `plane` is claimed by no standard for a conflicting meaning and removes the layout/layer near-miss) (MIGRATIONS)';
|
|
445
|
+
const RETIRED_LAYER='layer has been renamed: use plane (in mxGraph — the geometry model FigDown adopted — a layer is a CONTAINMENT PARENT that establishes coordinates, so layer=overlay reads as "reparent and re-origin this element", which FigDown does not do; Inkscape layers are <g> and may carry a transform, OGC WMS layers each carry an SRS, and CSS @layer is cascade priority with no visual meaning. SVG has no layer concept at all. `plane` is claimed by no standard for a conflicting meaning and removes the layout/layer near-miss) (MIGRATIONS 0.1)';
|
|
421
446
|
// `THRESHOLD-KEYWORD-SPELLING`: the scene keyword `guide` became `threshold`.
|
|
422
|
-
const RETIRED_GUIDE='guide has been renamed: use threshold (in Illustrator, Inkscape, Figma and draw.io a "guide" is an author-only construction line that is NEVER rendered, while FigDown\'s is drawn output — an INVERTED name, which `UNSAFE-DEFAULT-ELIMINATION` rates worse than an unfamiliar one, and no counter-example was found where "guide" names rendered output. `guide` was also a FigDown coinage, and `SIZE-AND-DIRECTION-KEY-NAMING` makes coining a last resort; `threshold` comes whole from Grafana, whose "Show thresholds" render option offers "As lines", "As filled regions" and "As filled regions and lines" — FigDown\'s marker + region pair, split the same way — with IETF RED/AQM as the secondary source (RFC 2309: "Two RED parameters, minth (minimum threshold) and maxth (maximum threshold)"; RFC 7567: "an AQM algorithm configured with a threshold"). 78% of the measured corpus marks are thresholds; target/mean/reference marks: 0) (MIGRATIONS)';
|
|
447
|
+
const RETIRED_GUIDE='guide has been renamed: use threshold (in Illustrator, Inkscape, Figma and draw.io a "guide" is an author-only construction line that is NEVER rendered, while FigDown\'s is drawn output — an INVERTED name, which `UNSAFE-DEFAULT-ELIMINATION` rates worse than an unfamiliar one, and no counter-example was found where "guide" names rendered output. `guide` was also a FigDown coinage, and `SIZE-AND-DIRECTION-KEY-NAMING` makes coining a last resort; `threshold` comes whole from Grafana, whose "Show thresholds" render option offers "As lines", "As filled regions" and "As filled regions and lines" — FigDown\'s marker + region pair, split the same way — with IETF RED/AQM as the secondary source (RFC 2309: "Two RED parameters, minth (minimum threshold) and maxth (maximum threshold)"; RFC 7567: "an AQM algorithm configured with a threshold"). 78% of the measured corpus marks are thresholds; target/mean/reference marks: 0) (MIGRATIONS 0.1)';
|
|
423
448
|
// `EXTERNAL-ENDPOINT-NAMING`: the scene keyword `boundary` became `external`.
|
|
424
|
-
const RETIRED_BOUNDARY='boundary has been renamed: use external (it declares an external I/O endpoint — the spec\'s own words — while UML\'s «boundary» is an INTERNAL interface object, C4\'s System_Boundary is a dashed grouping container FigDown already spells `group`, and BPMN\'s Boundary Event is a third meaning) (MIGRATIONS)';
|
|
449
|
+
const RETIRED_BOUNDARY='boundary has been renamed: use external (it declares an external I/O endpoint — the spec\'s own words — while UML\'s «boundary» is an INTERNAL interface object, C4\'s System_Boundary is a dashed grouping container FigDown already spells `group`, and BPMN\'s Boundary Event is a third meaning) (MIGRATIONS 0.1)';
|
|
425
450
|
// `ROW-BREAK-NAMING`: the `bitfield` child keyword `wrap` became `break`.
|
|
426
|
-
const RETIRED_WRAP='wrap has been renamed: use break (in CSS and typography `wrap` is AUTOMATIC reflow — a mode — while this directive is an EXPLICIT row break, an event; CSS Fragmentation calls it "a forced break … explicitly indicated by the … author", HTML spells it `br`) (MIGRATIONS)';
|
|
451
|
+
const RETIRED_WRAP='wrap has been renamed: use break (in CSS and typography `wrap` is AUTOMATIC reflow — a mode — while this directive is an EXPLICIT row break, an event; CSS Fragmentation calls it "a forced break … explicitly indicated by the … author", HTML spells it `br`) (MIGRATIONS 0.1)';
|
|
427
452
|
// `PRESENCE-FLAG-SPELLING`: the 0.1 rename `optional` -> `conditional` (`PRESENCE-FLAG-SPELLING`)
|
|
428
453
|
// is REVERTED. `conditional` has zero attestation as a wire-format field
|
|
429
454
|
// marker and zero uses in the downstream corpus, while "optional" appears in
|
|
430
455
|
// 34 downstream field LABELS — authors wrote the word in the label precisely
|
|
431
456
|
// because the keyword no longer said it. The MODEL key moves with the surface
|
|
432
457
|
// (`NORMATIVE-SEMANTIC-MODEL`): it is `optional` again on both sides.
|
|
433
|
-
const RETIRED_FIELD_CONDITIONAL='the field flag "conditional" has been retired: write present="<the condition>" (or present="" if the condition is not stated). "conditional" was attested as a wire-format field marker nowhere — zero hits in RFC 2784, ASN.1 X.680, draft-mcquistin-augmented-ascii-diagrams, SystemRDL, IP-XACT, Kaitai Struct and protobuf — and the flag it briefly replaced, "optional", is retired too: an option key carrying the CONDITION says what a bare flag could not (MIGRATIONS)';
|
|
458
|
+
const RETIRED_FIELD_CONDITIONAL='the field flag "conditional" has been retired: write present="<the condition>" (or present="" if the condition is not stated). "conditional" was attested as a wire-format field marker nowhere — zero hits in RFC 2784, ASN.1 X.680, draft-mcquistin-augmented-ascii-diagrams, SystemRDL, IP-XACT, Kaitai Struct and protobuf — and the flag it briefly replaced, "optional", is retired too: an option key carrying the CONDITION says what a bare flag could not (MIGRATIONS 0.1)';
|
|
434
459
|
// `PRESENCE-CONDITION-EXPRESSION`: the positional flag `optional` becomes the option key
|
|
435
460
|
// `present=`, whose VALUE is the presence condition. A bare flag could say
|
|
436
461
|
// only THAT a field is conditional; every RFC that draws one also states WHY
|
|
@@ -439,7 +464,7 @@ const RETIRED_FIELD_CONDITIONAL='the field flag "conditional" has been retired:
|
|
|
439
464
|
// prose the model may not read. `present` is the attested spelling: X.680
|
|
440
465
|
// PRESENT, IP-XACT isPresent, SystemRDL ispresent, RFC 2784 "present only
|
|
441
466
|
// if", draft-mcquistin "present only when".
|
|
442
|
-
const RETIRED_FIELD_OPTIONAL='the field flag "optional" has been retired and replaced by an option key that carries the CONDITION: write present="<the condition>" (e.g. field "Checksum" 16 present="C = 1"), or present="" when the condition is not stated. The bare flag could say only THAT the field was conditional, so the condition had to live in note= — invisible to the human reading the figure, and prose the model may not parse (`BITFIELD-CONDITIONAL-OFFSETS`). present= DRAWS: the field stays dashed and a stated condition becomes a caption under the block (MIGRATIONS)';
|
|
467
|
+
const RETIRED_FIELD_OPTIONAL='the field flag "optional" has been retired and replaced by an option key that carries the CONDITION: write present="<the condition>" (e.g. field "Checksum" 16 present="C = 1"), or present="" when the condition is not stated. The bare flag could say only THAT the field was conditional, so the condition had to live in note= — invisible to the human reading the figure, and prose the model may not parse (`BITFIELD-CONDITIONAL-OFFSETS`). present= DRAWS: the field stays dashed and a stated condition becomes a caption under the block (MIGRATIONS 0.1)';
|
|
443
468
|
// `TIMING-GENRE-NAMING`: the EXPERIMENTAL genre `wave` became `timing`, both as
|
|
444
469
|
// the header genre token and as the block opener. The old name was WaveDrom's
|
|
445
470
|
// MEMBER KEY, not its figure name: in WaveJSON `signal` is the root object and
|
|
@@ -456,8 +481,8 @@ const RETIRED_FIELD_OPTIONAL='the field flag "optional" has been retired and rep
|
|
|
456
481
|
// to know that the keys survive unchanged and only their carrier moved.
|
|
457
482
|
// RULE 6.2 placement: the spelling left the LANGUAGE, so this fires wherever
|
|
458
483
|
// it appears at line start, in every genre, ahead of the `GENRE-KEYWORD-ALLOWLIST` allowlist.
|
|
459
|
-
const RETIRED_SIZE='size has been retired: its keys moved onto pin — write pin <id> width=<px> height=<px> (one directive carries an element\'s whole declared geometry: at= places it, width=/height= extend it; all three keys are optional and a pin with none of them declares nothing) (MIGRATIONS)';
|
|
460
|
-
const RETIRED_WAVE='wave has been renamed: use timing (in WaveJSON `signal` is the root object and `wave` is a PROPERTY of one signal — its lane activity string — so `wave` named a member key, not a figure kind; WaveDrom\'s own name for the figure is "Digital Timing Diagram", "timing diagram" is the datasheet/JEDEC term for it, and UML 2.5.1\'s Timing Diagram is the same concept. The rename frees `wave` for the lane) (MIGRATIONS)';
|
|
484
|
+
const RETIRED_SIZE='size has been retired: its keys moved onto pin — write pin <id> width=<px> height=<px> (one directive carries an element\'s whole declared geometry: at= places it, width=/height= extend it; all three keys are optional and a pin with none of them declares nothing) (MIGRATIONS 0.1)';
|
|
485
|
+
const RETIRED_WAVE='wave has been renamed: use timing (in WaveJSON `signal` is the root object and `wave` is a PROPERTY of one signal — its lane activity string — so `wave` named a member key, not a figure kind; WaveDrom\'s own name for the figure is "Digital Timing Diagram", "timing diagram" is the datasheet/JEDEC term for it, and UML 2.5.1\'s Timing Diagram is the same concept. The rename frees `wave` for the lane) (MIGRATIONS 0.1)';
|
|
461
486
|
// `EDGE-GEOMETRY-CONSTRUCTS`: `path` and `routing` are WITHDRAWN from the language.
|
|
462
487
|
// These two diagnostics are a NEW SHAPE for this table. Every retirement
|
|
463
488
|
// before them named a replacement spelling — `size` named `pin`, `guide` named
|
|
@@ -475,15 +500,15 @@ const WITHDRAWN_WHERE=' The decision and its evidence: MIGRATIONS 0.1, core §9
|
|
|
475
500
|
const RETIRED_PATH='path has been WITHDRAWN from the language (`EDGE-GEOMETRY-CONSTRUCTS`) — removed, not renamed, so there is no replacement spelling. A prior-art study of Visio, draw.io/mxGraph, Graphviz and ELK found author waypoints OUTSIDE the stable intersection: only 2 of the 4 model them, and those 2 disagree on what happens when an endpoint moves. The dock realisation was outside it too — written-order attachment has zero prior art in any surveyed system. Delete the line: the edge draws under auto layout, and `rank`, `flow`, declaration order and `pin` are the content-zone means of shaping it.'+WITHDRAWN_WHERE;
|
|
476
501
|
const RETIRED_ROUTING='routing has been WITHDRAWN from the language (`EDGE-GEOMETRY-CONSTRUCTS`) — removed, not renamed, so there is no replacement spelling. Two routing modes and two scopes ARE inside the stable prior-art intersection, so the need is recognised and its shape is known; what is missing is the evidence and the implementation (6 of the 8 in-repo `routing=orthogonal` writings were provable no-ops, and downstream adoption was zero), and the per-edge scope cannot be restored without an edge-identity construct FigDown does not have. Delete the line; the edges draw straight.'+WITHDRAWN_WHERE;
|
|
477
502
|
// `TIMING-LANE-ALPHABET`: the timing lane digits `2`-`9` left the closed alphabet.
|
|
478
|
-
const RETIRED_LANE_DIGIT='timing lane digits 2-9 have been retired: write "=" for a data cell and name it in data= (WaveDrom defines 2..9 as "value with color N" and "=" as "value (default color 2)" — the same brick with a palette index, while FigDown drew the digit character itself as the box label and consumed no data entry, so the two readings of one lane differed silently) (MIGRATIONS)';
|
|
503
|
+
const RETIRED_LANE_DIGIT='timing lane digits 2-9 have been retired: write "=" for a data cell and name it in data= (WaveDrom defines 2..9 as "value with color N" and "=" as "value (default color 2)" — the same brick with a palette index, while FigDown drew the digit character itself as the box label and consumed no data entry, so the two readings of one lane differed silently) (MIGRATIONS 0.1)';
|
|
479
504
|
// 0.1 (§8.4): `edge`, `threshold` and `bundle` have NO interior, so
|
|
480
505
|
// `fill=` and `stroke=` named the SAME channel and `stroke=` won silently —
|
|
481
506
|
// two keys for one channel, resolved by an undocumented precedence that
|
|
482
507
|
// produced a legal, wrong figure whenever both were written (16 lines in
|
|
483
508
|
// this repository, 3 of them writing both on one line). Same defect shape as
|
|
484
509
|
// the retired `color=`; same cure, a named diagnostic.
|
|
485
|
-
const NO_INTERIOR=new Set(['edge','threshold','bundle']);
|
|
486
|
-
const FILL_NO_INTERIOR=k=>k+' has no interior, so fill= and stroke= name the same channel (stroke= won silently) — write stroke= (MIGRATIONS)';
|
|
510
|
+
const NO_INTERIOR=new Set(['edge','flowline','transition','threshold','bundle']);
|
|
511
|
+
const FILL_NO_INTERIOR=k=>k+' has no interior, so fill= and stroke= name the same channel (stroke= won silently) — write stroke= (MIGRATIONS 0.1)';
|
|
487
512
|
// `STYLE-KEY-SCOPE`: `style=` left these three directives (it stays live on
|
|
488
513
|
// node/group/edge/class/bundle/threshold/band). The generic
|
|
489
514
|
// "<directive> does not take style=" would be true but would not say why, and
|
|
@@ -494,8 +519,8 @@ const FILL_NO_INTERIOR=k=>k+' has no interior, so fill= and stroke= name the sam
|
|
|
494
519
|
// are closed here the way every other one-channel-two-keys collision in this
|
|
495
520
|
// language was closed — a named line error, not a precedence rule. A
|
|
496
521
|
// precedence rule is what `STYLE-KEY-SCOPE` had just finished removing from `field`.
|
|
497
|
-
const CELL_HL_ON_CELL='highlight is a ROW mark and takes the single-valued row form (cell <row> highlight) — on a cell address it was SILENTLY DISCARDED and never reached the model, while the cell fill drew. A row tint and a cell fill paint the same channel, so writing both for one cell has no honest resolution: tint the row (cell <row> highlight) or paint the cell (cell (<row>,<col>) fill=…/class=…), not both (MIGRATIONS)';
|
|
498
|
-
const CELL_HL_ROW_CONFLICT=(r,c)=>'cell ('+r+','+c+') resolves to a fill on row '+r+', which is highlighted — the cell fill overrides the row tint, so the model says "row '+r+' is highlighted" while the drawing shows only part of the row tinted (`PRESENTATION-AS-MEANING-CARRIER`: presentation may render meaning, never delete it). Drop the row highlight, or move the cell fill to a row that carries none (MIGRATIONS)';
|
|
522
|
+
const CELL_HL_ON_CELL='highlight is a ROW mark and takes the single-valued row form (cell <row> highlight) — on a cell address it was SILENTLY DISCARDED and never reached the model, while the cell fill drew. A row tint and a cell fill paint the same channel, so writing both for one cell has no honest resolution: tint the row (cell <row> highlight) or paint the cell (cell (<row>,<col>) fill=…/class=…), not both (MIGRATIONS 0.1)';
|
|
523
|
+
const CELL_HL_ROW_CONFLICT=(r,c)=>'cell ('+r+','+c+') resolves to a fill on row '+r+', which is highlighted — the cell fill overrides the row tint, so the model says "row '+r+' is highlighted" while the drawing shows only part of the row tinted (`PRESENTATION-AS-MEANING-CARRIER`: presentation may render meaning, never delete it). Drop the row highlight, or move the cell fill to a row that carries none (MIGRATIONS 0.1)';
|
|
499
524
|
const NO_ITEM_STYLE=new Set(['field','cell','signal']);
|
|
500
525
|
const STYLE_NO_ITEM=k=>k+' does not take style= — '+(k==='field'
|
|
501
526
|
? 'on a field the dash IS conditional presence (`present=`, spelled `optional` until this release), and style=solid erased it while the model still recorded the field as conditionally present (`PRESENTATION-AS-MEANING-CARRIER`: presentation may render meaning, never be its only carrier)'
|
|
@@ -507,7 +532,7 @@ const STYLE_NO_ITEM=k=>k+' does not take style= — '+(k==='field'
|
|
|
507
532
|
// human, which is the same defect `STYLE-KEY-SCOPE` exists to close. The two channels
|
|
508
533
|
// that DRAW are the name/label and a `class` meaning (which also earns a
|
|
509
534
|
// legend entry).
|
|
510
|
-
+'. Delete the key; if the distinction is knowledge, write it in the name/label or in a class= meaning — both of which DRAW; description= is documentation prose and produces no ink beyond a tooltip (MIGRATIONS)';
|
|
535
|
+
+'. Delete the key; if the distinction is knowledge, write it in the name/label or in a class= meaning — both of which DRAW; description= is documentation prose and produces no ink beyond a tooltip (MIGRATIONS 0.1)';
|
|
511
536
|
function splitOpts(toks, laneMode){
|
|
512
537
|
// a repeated option key on one line is a line error, never
|
|
513
538
|
// silent last-wins. `dup` names the first key that appeared twice.
|
|
@@ -557,7 +582,7 @@ const ID_RE=/^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
|
557
582
|
// `a`+`x--b`) and the greedy member regex silently committed to the first,
|
|
558
583
|
// making the second unreachable and undiagnosed — RULE 6.3 says malformed
|
|
559
584
|
// input is an error, never a guess. Zero ids in either corpus contain `--`.
|
|
560
|
-
const DD_ID='"--" is not allowed inside an id — it is the link operator (edge a -- b, bundle t1 a--b); write a single "-" or "_" (MIGRATIONS)';
|
|
585
|
+
const DD_ID='"--" is not allowed inside an id — it is the link operator (edge a -- b, bundle t1 a--b); write a single "-" or "_" (MIGRATIONS 0.1)';
|
|
561
586
|
// `QUOTED-IDS`: ONE wording for every id position. It covers both
|
|
562
587
|
// halves of the defect it closes — a needlessly quoted legal id
|
|
563
588
|
// (`node "a"`, silently accepted before) and a quoted token that is not a
|
|
@@ -576,7 +601,7 @@ const DD_ID='"--" is not allowed inside an id — it is the link operator (edge
|
|
|
576
601
|
// old failure named the wrong thing (`node a Cache miss` reported
|
|
577
602
|
// `unexpected argument "miss"` — a surplus argument, when the defect was a
|
|
578
603
|
// missing quote).
|
|
579
|
-
const Q_WHY='whitespace also separates positionals, so a bare token cannot express a phrase (MIGRATIONS)';
|
|
604
|
+
const Q_WHY='whitespace also separates positionals, so a bare token cannot express a phrase (MIGRATIONS 0.1)';
|
|
580
605
|
const ID_RULE='ids are bare and match [A-Za-z_][A-Za-z0-9_-]* — text with spaces or punctuation belongs in the label: node <id> "your text"';
|
|
581
606
|
// isId: the whole id test, used at every id position in the language.
|
|
582
607
|
const isId=v=>typeof v==='string'&&ID_RE.test(v)&&!v.includes('--');
|
|
@@ -764,7 +789,9 @@ function parseIndexRange(v){
|
|
|
764
789
|
const CORE_KW=['figdown','title','layout'];
|
|
765
790
|
const LAYOUT_KW=['pin']; // `LAYOUT-ZONE-NAMESPACE`, NORMATIVE — the whole namespace
|
|
766
791
|
const GENRE_FREE_KW=CORE_KW.concat(LAYOUT_KW);
|
|
767
|
-
|
|
792
|
+
// `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`: the NODE and CONNECTOR spellings are per genre, so
|
|
793
|
+
// they are NOT in the shared list — every scene genre concats its own two.
|
|
794
|
+
const SCENE_KW_TOP=['group','external','class','flow','rank'];
|
|
768
795
|
const SCENE_EXP_KW=['threshold','band','bundle','plane'];
|
|
769
796
|
const SCENE_HOST_KW=GENRE_FREE_KW.concat(SCENE_KW_TOP, SCENE_EXP_KW, ['bitfield','table','timing','chart']);
|
|
770
797
|
// `FLOWCHART-ROLE-KEYWORDS`: the flowchart ROLE vocabulary — the FIRST exercise of
|
|
@@ -780,10 +807,88 @@ const FLOWCHART_ROLE_KW=['process','decision','terminator'];
|
|
|
780
807
|
// only its default drawing, and `shape=` on the same line overrides the
|
|
781
808
|
// drawing without touching the role (§12.7).
|
|
782
809
|
const ROLE_SHAPE={process:'box',decision:'diamond',terminator:'rounded'};
|
|
810
|
+
|
|
811
|
+
// `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`: PER-GENRE NODE AND CONNECTOR SPELLINGS.
|
|
812
|
+
// Each scene genre takes the word its own domain uses for the line between
|
|
813
|
+
// two things, and for the thing itself. The criterion is THE TERM THE DOMAIN
|
|
814
|
+
// ACTUALLY USES, not the standard's exact orthography (see the vocabulary
|
|
815
|
+
// rows: `flowline` carries the same ISO-clause verification debt
|
|
816
|
+
// `terminator` does, recorded rather than claimed away).
|
|
817
|
+
//
|
|
818
|
+
// block / topology node edge (DOT, unchanged)
|
|
819
|
+
// flowchart node flowline (ISO 5807)
|
|
820
|
+
// statechart state transition (OMG UML 2.5.1 §14)
|
|
821
|
+
//
|
|
822
|
+
// The ASYMMETRY is deliberate and is the whole of `GENRE-CONNECTOR-SPELLING` vs `GENRE-NODE-SPELLING`: `node`
|
|
823
|
+
// STAYS in `flowchart`, because there a stage can have a role THE SOURCE
|
|
824
|
+
// DOES NOT STATE, and `node` is the only spelling for that — forcing
|
|
825
|
+
// `process` would make the transcriber assert what the source never said.
|
|
826
|
+
// `statechart` has exactly ONE kind of node, so nothing can be left unstated
|
|
827
|
+
// and `state` loses nothing. `BARE-NODE-MEANING` CORRECTS the reason this
|
|
828
|
+
// comment used to give ("ISO has ~ten symbol kinds and we carry three"): a
|
|
829
|
+
// symbol this genre cannot spell is a COVERAGE GAP in FigDown, not a state
|
|
830
|
+
// of the figure, and `node` is not its spelling — see
|
|
831
|
+
// the project’s working record for the coverage ledger.
|
|
832
|
+
const GENRE_NODE_KW={block:'node',topology:'node',flowchart:'node',statechart:'state'};
|
|
833
|
+
const GENRE_CONNECTOR_KW={block:'edge',topology:'edge',flowchart:'flowline',statechart:'transition'};
|
|
834
|
+
const NODE_SPELLINGS=new Set(['node','state']);
|
|
835
|
+
const CONNECTOR_SPELLINGS=new Set(['edge','flowline','transition']);
|
|
836
|
+
// `KEYWORD-RENAME-SCOPE`: the flowchart rename is GATED BY THE DECLARED LANGUAGE
|
|
837
|
+
// VERSION, because `GENRE-CONNECTOR-SPELLING` applied it to `figdown 0.1` and that BROKE documents
|
|
838
|
+
// legal at v0.1.8 — `figdown 0.1 flowchart` + `edge` stopped parsing, with
|
|
839
|
+
// nothing recording the break as a decision. Under core §13.0 only X removes,
|
|
840
|
+
// so 0.1 keeps its spelling:
|
|
841
|
+
//
|
|
842
|
+
// figdown 0.1 flowchart `edge` legal, `flowline` is a VERSION error
|
|
843
|
+
// figdown 0.2 flowchart `flowline` legal, `edge` is the WRONG_WORD error
|
|
844
|
+
//
|
|
845
|
+
// Two spellings inside ONE version is what the no-alias rule (`IDENTITY-ASSERTION`)
|
|
846
|
+
// forbids; two spellings across VERSIONS is ordinary language evolution, and
|
|
847
|
+
// each version accepts exactly one. `statechart` needs no gate of its own —
|
|
848
|
+
// the GENRE requires 0.2 (GENRES_BY_VERSION), so `state`/`transition` cannot
|
|
849
|
+
// be reached from a 0.1 document at all.
|
|
850
|
+
const GENRE_CONNECTOR_KW_AT={
|
|
851
|
+
'0.1':{block:'edge',topology:'edge',flowchart:'edge'},
|
|
852
|
+
'0.2':GENRE_CONNECTOR_KW
|
|
853
|
+
};
|
|
854
|
+
const connectorKwAt=(genre,ver)=>
|
|
855
|
+
((GENRE_CONNECTOR_KW_AT[ver]||GENRE_CONNECTOR_KW)[genre])||undefined;
|
|
856
|
+
// The version a connector spelling FIRST becomes legal in, so the diagnostic
|
|
857
|
+
// can name it the way the genre gate already names `figdown 0.2`.
|
|
858
|
+
const CONNECTOR_MIN_VERSION={flowline:'0.2',transition:'0.2'};
|
|
859
|
+
const WRONG_VERSION_WORD=(surf,want,genre,need,have)=>
|
|
860
|
+
'"'+surf+'" requires figdown '+need+' (this document declares '+have+'): '+
|
|
861
|
+
'under figdown '+have+' genre '+genre+' spells this "'+want+'". The rename is '+
|
|
862
|
+
'gated by the language version — a figdown '+have+' document keeps the spelling it '+
|
|
863
|
+
'was written with (core §13.0: only a MAJOR version removes) — so raise the header '+
|
|
864
|
+
'to figdown '+need+' or write "'+want+'" (MIGRATIONS 0.2)';
|
|
865
|
+
// Why each genre's word is its word — quoted in the diagnostic, because
|
|
866
|
+
// "not allowed in genre X" tells an author nothing about what to write.
|
|
867
|
+
const WORD_WHY={
|
|
868
|
+
edge:'a block or topology figure is a graph, and `edge` is the graph word (DOT)',
|
|
869
|
+
flowline:'the connecting line in a flowchart is a FLOWLINE — the term ISO 5807 uses for it',
|
|
870
|
+
transition:'the connecting line in a statechart is a TRANSITION — the term UML 2.5.1 §14 uses for it',
|
|
871
|
+
node:'this genre has more kinds of thing than it has words for, so `node` is the general one',
|
|
872
|
+
state:'a statechart has exactly ONE kind of node and it is a STATE (UML 2.5.1 §14)'
|
|
873
|
+
};
|
|
874
|
+
// The named diagnostic `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING` owe: it says WHICH word this genre uses and
|
|
875
|
+
// WHY, and it names the migration, because every connector line in a
|
|
876
|
+
// reclassified document has to be rewritten (the cost `GENRE-CONNECTOR-SPELLING` accepted).
|
|
877
|
+
const WRONG_WORD=(surf,want,genre)=>
|
|
878
|
+
'"'+surf+'" is not the word genre '+genre+' uses for this — write "'+want+'": '+WORD_WHY[want]+
|
|
879
|
+
'. Each scene genre takes the term its own domain uses (block/topology `node` `edge`, flowchart `node` `flowline`, statechart `state` `transition`) — run tools/migrate-figdown.js to rewrite it (MIGRATIONS 0.2)';
|
|
783
880
|
const GENRE_KW={
|
|
784
|
-
block:new Set(SCENE_HOST_KW),
|
|
785
|
-
topology:new Set(SCENE_HOST_KW),
|
|
786
|
-
flowchart:new Set(SCENE_HOST_KW.concat(FLOWCHART_ROLE_KW)),
|
|
881
|
+
block:new Set(SCENE_HOST_KW.concat(['node','edge'])),
|
|
882
|
+
topology:new Set(SCENE_HOST_KW.concat(['node','edge'])),
|
|
883
|
+
flowchart:new Set(SCENE_HOST_KW.concat(['node','flowline'], FLOWCHART_ROLE_KW)),
|
|
884
|
+
// `STATECHART-GENRE-SCOPE`: `statechart` added no keyword of its own — it was the
|
|
885
|
+
// scene host set and nothing else. `GENRE-NODE-SPELLING` gives it its two: the
|
|
886
|
+
// scene host set with `state` and `transition` in the slots `node` and
|
|
887
|
+
// `edge` occupy elsewhere. It still does NOT inherit `process`/`decision`/
|
|
888
|
+
// `terminator`: those are flowchart's words (`GENRE-NAMESPACE` `GENRE-VOCABULARY-OBLIGATION`), and a `decision` in a
|
|
889
|
+
// statechart is a category error, not a shorthand. The allowlist is what
|
|
890
|
+
// makes that a line error with no extra code.
|
|
891
|
+
statechart:new Set(SCENE_HOST_KW.concat(['state','transition'])),
|
|
787
892
|
bitfield:new Set(GENRE_FREE_KW.concat(['class','bitfield'])),
|
|
788
893
|
// chart is experimental and attaches to a table id in the same document
|
|
789
894
|
table:new Set(GENRE_FREE_KW.concat(['class','table','chart'])),
|
|
@@ -886,8 +991,13 @@ function parseOne(text){
|
|
|
886
991
|
// operator into halves: -[x]- -[x]-> <-[x]- <-[x]->. Bracket content:
|
|
887
992
|
// balanced brackets nest verbatim ([flags[3:0]] just works); ["..."] takes
|
|
888
993
|
// the standard quoted-string escapes for unbalanced brackets / \n.
|
|
889
|
-
|
|
890
|
-
|
|
994
|
+
// `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`: `kw` is the SURFACE spelling the author wrote —
|
|
995
|
+
// `edge`, `flowline` or `transition`. One scanner, three words: every
|
|
996
|
+
// message names the word on the line, and nothing downstream of here knows
|
|
997
|
+
// the difference (the model records a connector, not a spelling).
|
|
998
|
+
function parseEdgeLine(s,n,kw){
|
|
999
|
+
kw=kw||'edge';
|
|
1000
|
+
let i=kw.length; // past the connector keyword
|
|
891
1001
|
const ws=()=>{ while(i<s.length&&/\s/.test(s[i])) i++; };
|
|
892
1002
|
// `LINK-OPERATOR-IN-IDS`: a hyphen is an id character only when it is NOT
|
|
893
1003
|
// followed by a second one, because `--` is the link operator. This is
|
|
@@ -935,7 +1045,7 @@ function parseOne(text){
|
|
|
935
1045
|
ws();
|
|
936
1046
|
if(s[i]==='"'){ err(n,ID_RULE); return; }
|
|
937
1047
|
const a=readId();
|
|
938
|
-
if(!a){ err(n, idHere()?ID_RULE:'
|
|
1048
|
+
if(!a){ err(n, idHere()?ID_RULE:kw+' needs <id> ->|<-|--|<-> <id>'); return; }
|
|
939
1049
|
if(idHere()){ err(n,ID_RULE); return; }
|
|
940
1050
|
ws(); let tail=null;
|
|
941
1051
|
if(s[i]==='['){ const r=readLbl(); if(r.error){ err(n,r.error); return; } tail=r.v; }
|
|
@@ -943,7 +1053,7 @@ function parseOne(text){
|
|
|
943
1053
|
let lh=null;
|
|
944
1054
|
if(s.startsWith('<-',i)){ lh='<-'; i+=2; }
|
|
945
1055
|
else if(s[i]==='-'){ lh='-'; i++; }
|
|
946
|
-
else { err(n,'
|
|
1056
|
+
else { err(n,kw+' needs an operator: -> <- -- <-> (a [mid] label splits it: -[x]->)'); return; }
|
|
947
1057
|
let mid=null, op=null;
|
|
948
1058
|
if(s[i]==='['){
|
|
949
1059
|
const r=readLbl(); if(r.error){ err(n,r.error); return; } mid=r.v;
|
|
@@ -955,14 +1065,14 @@ function parseOne(text){
|
|
|
955
1065
|
} else {
|
|
956
1066
|
if(s[i]==='-'){ op='--'; i++; }
|
|
957
1067
|
else if(s[i]==='>'){ op='->'; i++; }
|
|
958
|
-
else { err(n,'
|
|
1068
|
+
else { err(n,kw+' needs an operator: -> <- -- <->'); return; }
|
|
959
1069
|
}
|
|
960
1070
|
ws(); let head=null;
|
|
961
1071
|
if(s[i]==='['){ const r=readLbl(); if(r.error){ err(n,r.error); return; } head=r.v; }
|
|
962
1072
|
ws();
|
|
963
1073
|
if(s[i]==='"'){ err(n,ID_RULE); return; }
|
|
964
1074
|
const b=readId();
|
|
965
|
-
if(!b){ err(n, idHere()?ID_RULE:'
|
|
1075
|
+
if(!b){ err(n, idHere()?ID_RULE:kw+' needs a target id after the operator'); return; }
|
|
966
1076
|
if(idHere()){ err(n,ID_RULE); return; }
|
|
967
1077
|
const tk2=tokenize(s.slice(i).trim());
|
|
968
1078
|
if(tk2.error){ err(n,tk2.error); return; }
|
|
@@ -976,10 +1086,10 @@ function parseOne(text){
|
|
|
976
1086
|
for(const rk in RETIRED_OPT_KEYS)
|
|
977
1087
|
if(o2[rk]!==undefined){ err(n,RETIRED_OPT_KEYS[rk]); return; }
|
|
978
1088
|
for(const k in o2)
|
|
979
|
-
if(!DIRECTIVE_OPTS.
|
|
1089
|
+
if(!DIRECTIVE_OPTS[kw].includes(k)){ err(n,kw+' does not take '+k+'='); return; }
|
|
980
1090
|
for(const k of ['label','taillabel','headlabel'])
|
|
981
|
-
if(o2[k]!==undefined){ err(n,k+'= is retired — write the label inline:
|
|
982
|
-
if(o2.fill!==undefined){ err(n,FILL_NO_INTERIOR(
|
|
1091
|
+
if(o2[k]!==undefined){ err(n,k+'= is retired — write the label inline: '+kw+' A [tail] -[mid]-> [head] B (MIGRATIONS 0.1)'); return; }
|
|
1092
|
+
if(o2.fill!==undefined){ err(n,FILL_NO_INTERIOR(kw)); return; }
|
|
983
1093
|
for(const k of ['fill','stroke'])
|
|
984
1094
|
if(o2[k]!==undefined && !isColor(o2[k])){ err(n,'unknown color "'+o2[k]+'" (#hex or CSS color name)'); return; }
|
|
985
1095
|
// `RULE-POSITION-ENUMERATION`: `edge` was the ONE id position in the language that
|
|
@@ -1081,17 +1191,41 @@ function parseOne(text){
|
|
|
1081
1191
|
if(findReservedSemi(raw)>=0){ err(n,RESERVED_SEMI); continue; }
|
|
1082
1192
|
// edge lines carry inline [labels] with free text — dedicated scanner,
|
|
1083
1193
|
// not the generic tokenizer
|
|
1084
|
-
|
|
1194
|
+
// `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`: all THREE connector spellings are scanned here.
|
|
1195
|
+
// The wrong one for the genre must reach the named diagnostic below, so
|
|
1196
|
+
// the dispatch cannot be narrowed to the genre's own word — a `flowline`
|
|
1197
|
+
// under `block` would then fall through to `unrecognized line`, which is
|
|
1198
|
+
// exactly the answer these rulings owe an author better than.
|
|
1199
|
+
const mConn=/^(edge|flowline|transition)(\s|$)/.exec(raw.trim());
|
|
1200
|
+
if(mConn){
|
|
1201
|
+
const ckw=mConn[1];
|
|
1085
1202
|
if(firstContent){ firstContent=false; err(n,'first line must be "figdown 0.1 <genre>"'); }
|
|
1086
1203
|
cur=null;
|
|
1087
|
-
// `CONTENT-LAYOUT-ZONE-SPLIT`:
|
|
1204
|
+
// `CONTENT-LAYOUT-ZONE-SPLIT`: a connector is a semantic directive, and it is dispatched here —
|
|
1088
1205
|
// before the generic layout-zone gate below — so it needs its own copy
|
|
1089
1206
|
// of that gate, or it is the one semantic line that escapes the zone.
|
|
1090
|
-
if(sawLayout){ err(n,'"
|
|
1091
|
-
// `GENRE-KEYWORD-ALLOWLIST`:
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1207
|
+
if(sawLayout){ err(n,'"'+ckw+'" is a semantic directive — it must appear before the layout zone (`CONTENT-LAYOUT-ZONE-SPLIT`)'); continue; }
|
|
1208
|
+
// `GENRE-KEYWORD-ALLOWLIST`: a connector is scene vocabulary only — and in a scene genre it is
|
|
1209
|
+
// THAT genre's connector word (`GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`), not any of the three.
|
|
1210
|
+
// `KEYWORD-RENAME-SCOPE`: the word this genre uses is read AT THE DECLARED VERSION, not
|
|
1211
|
+
// at the newest one. A genre with no connector at all (bitfield, table,
|
|
1212
|
+
// timing) still gets the allowlist message.
|
|
1213
|
+
if(sawHeader && doc.genre && GENRE_KW[doc.genre]){
|
|
1214
|
+
const want=connectorKwAt(doc.genre, doc.version);
|
|
1215
|
+
if(!want){ err(n,'"'+ckw+'" is not allowed in genre '+doc.genre); continue; }
|
|
1216
|
+
if(ckw!==want){
|
|
1217
|
+
const need=CONNECTOR_MIN_VERSION[ckw];
|
|
1218
|
+
// The author wrote a word this genre really does use — just not in
|
|
1219
|
+
// the version they declared. That is a different mistake from the
|
|
1220
|
+
// wrong domain's word, and it has a different one-step fix.
|
|
1221
|
+
if(need && doc.version && need>doc.version &&
|
|
1222
|
+
GENRE_CONNECTOR_KW[doc.genre]===ckw)
|
|
1223
|
+
err(n, WRONG_VERSION_WORD(ckw,want,doc.genre,need,doc.version));
|
|
1224
|
+
else
|
|
1225
|
+
err(n, WRONG_WORD(ckw,want,doc.genre));
|
|
1226
|
+
continue; }
|
|
1227
|
+
}
|
|
1228
|
+
parseEdgeLine(raw.trim(),n,ckw);
|
|
1095
1229
|
continue;
|
|
1096
1230
|
}
|
|
1097
1231
|
const tk=tokenize(raw.trim());
|
|
@@ -1203,25 +1337,53 @@ function parseOne(text){
|
|
|
1203
1337
|
// line. So the quoting check runs first and suppresses the value
|
|
1204
1338
|
// check for that token, exactly as `badOpts` does for `shape=`.
|
|
1205
1339
|
const hq1=!!posq[1], hq2=!!posq[2];
|
|
1340
|
+
// `STATECHART-GENRE-SCOPE`: the language version is now a SET, not a constant, so every
|
|
1341
|
+
// message that echoes a corrected header echoes the version the
|
|
1342
|
+
// author actually declared — an author on `0.2` must not be shown a
|
|
1343
|
+
// `0.1` example. An unrecognised version falls back to `0.1` for the
|
|
1344
|
+
// purpose of these examples only; it has already errored on its own.
|
|
1345
|
+
const verOK=!hq1 && LANG_VERSIONS.includes(pos[1]);
|
|
1346
|
+
const ver=verOK?pos[1]:'0.1';
|
|
1206
1347
|
if(hq1) err(n,ENUM_BARE('figdown '+pos[1]+' <genre>'));
|
|
1207
|
-
else if(hq2) err(n,ENUM_BARE('figdown
|
|
1208
|
-
if(!hq1 && pos[1]
|
|
1209
|
-
const GENRES=['block','topology','flowchart','bitfield','table','timing'];
|
|
1348
|
+
else if(hq2) err(n,ENUM_BARE('figdown '+ver+' '+pos[2]));
|
|
1349
|
+
if(!hq1 && !LANG_VERSIONS.includes(pos[1])) err(n,'unsupported version "'+(pos[1]||'')+'" (expected '+LANG_VERSIONS.join(' or ')+')');
|
|
1210
1350
|
// 0.1: the genre token is REQUIRED. `bitfield`/`table`/`timing`
|
|
1211
1351
|
// documents declare their kind in their content, but `block`,
|
|
1212
1352
|
// `topology` and `flowchart` share the SAME vocabulary
|
|
1213
1353
|
// (node/edge/group) and differ only in default flow — so the header
|
|
1214
1354
|
// is the ONLY place such a document states which kind of figure it
|
|
1215
1355
|
// is, and omitting it destroys the distinction with no recoverable
|
|
1216
|
-
// fallback. The message lists the
|
|
1217
|
-
// agent fixes it in one step
|
|
1218
|
-
|
|
1356
|
+
// fallback. The message lists the legal values so an authoring
|
|
1357
|
+
// agent fixes it in one step — the values legal AT THE DECLARED
|
|
1358
|
+
// VERSION, since listing `statechart` to a `figdown 0.1` author would
|
|
1359
|
+
// name a genre that document may not use.
|
|
1360
|
+
const GENRES=GENRES_BY_VERSION[ver];
|
|
1361
|
+
const GLIST='('+GENRES.join('|')+')';
|
|
1362
|
+
if(pos[2]===undefined) err(n,'figdown header requires a genre '+GLIST);
|
|
1219
1363
|
else if(hq2){ /* the quoting error above is this token's one error */ }
|
|
1220
1364
|
else if(pos[2]==='wave') err(n,RETIRED_WAVE);
|
|
1221
|
-
else if(!GENRES.includes(pos[2]))
|
|
1365
|
+
else if(!GENRES.includes(pos[2])){
|
|
1366
|
+
// A genre that exists but only LATER is its own diagnostic, not
|
|
1367
|
+
// `unknown genre`. core §13.7 forbids guessing a version, and the
|
|
1368
|
+
// author's one-step fix is to raise the header — which the message
|
|
1369
|
+
// SPELLS OUT, because "unknown genre statechart" would send them
|
|
1370
|
+
// looking for a typo that is not there. The trailing `— write: …`
|
|
1371
|
+
// is the same shape WRONG_VERSION_WORD ends with: name the fix, do
|
|
1372
|
+
// not leave the author to derive it.
|
|
1373
|
+
const later=Object.keys(GENRES_BY_VERSION).sort()
|
|
1374
|
+
.find(v=>v>ver && GENRES_BY_VERSION[v].includes(pos[2]));
|
|
1375
|
+
if(later) err(n,'genre "'+pos[2]+'" requires figdown '+later+' (this document declares '+ver+') — write: figdown '+later+' '+pos[2]);
|
|
1376
|
+
else err(n,'unknown genre "'+pos[2]+'" '+GLIST);
|
|
1377
|
+
}
|
|
1222
1378
|
else{ doc.genre=pos[2];
|
|
1379
|
+
// The DECLARED language version is part of the model (core §12.5
|
|
1380
|
+
// `header.version`): a reader must be able to tell what contract
|
|
1381
|
+
// the author wrote against without re-reading the source.
|
|
1382
|
+
doc.version=ver;
|
|
1223
1383
|
// genre defaults (`GENRE-NAMESPACE`/`DEFAULT-VALUE-SELECTION`): flowchart figures flow down —
|
|
1224
|
-
// the census-dominant direction; an explicit flow
|
|
1384
|
+
// the census-dominant direction; an explicit `flow` directive overrides.
|
|
1385
|
+
// `statechart` takes the SCENE default (`right`) and adds no rule
|
|
1386
|
+
// of its own — `STATECHART-GENRE-SCOPE` lands a dispatch point, not a layout.
|
|
1225
1387
|
if(pos[2]==='flowchart') doc.flow='down'; }
|
|
1226
1388
|
// §1: a directive line carrying positional arguments its grammar does
|
|
1227
1389
|
// not accept MUST be rejected — the header is a directive like any other
|
|
@@ -1265,19 +1427,19 @@ function parseOne(text){
|
|
|
1265
1427
|
// the first entries in it that name NO replacement. `route`'s own message
|
|
1266
1428
|
// has to change with them: it pointed at `path`, which no longer exists,
|
|
1267
1429
|
// so it now states the whole chain and ends where the others end.
|
|
1268
|
-
if(kw==='line'){ err(n,'line has been renamed: use threshold (a labelled reference value drawn across the target'+"'"+'s box; "line" now only names a source line number, and the 0.1 replacement `guide` was itself retired) (MIGRATIONS)'); continue; }
|
|
1430
|
+
if(kw==='line'){ err(n,'line has been renamed: use threshold (a labelled reference value drawn across the target'+"'"+'s box; "line" now only names a source line number, and the 0.1 replacement `guide` was itself retired) (MIGRATIONS 0.1)'); continue; }
|
|
1269
1431
|
if(kw==='fill'){ err(n,'fill has been renamed: use band (a range band; the KEYWORD is retired — fill= is the presentation option key)'); continue; }
|
|
1270
1432
|
if(kw==='route'){ err(n,'route has been WITHDRAWN: it was renamed path, and path was withdrawn from the language (`EDGE-GEOMETRY-CONSTRUCTS`). There is no replacement spelling. Delete the line: the edge draws under auto layout.'+WITHDRAWN_WHERE); continue; }
|
|
1271
1433
|
if(kw==='path'){ err(n,RETIRED_PATH); continue; }
|
|
1272
1434
|
if(kw==='routing'){ err(n,RETIRED_ROUTING); continue; }
|
|
1273
|
-
if(kw==='render'){ err(n,'render has been renamed: use layout (the zone takes only pin — geometry, not presentation) (MIGRATIONS)'); continue; }
|
|
1435
|
+
if(kw==='render'){ err(n,'render has been renamed: use layout (the zone takes only pin — geometry, not presentation) (MIGRATIONS 0.1)'); continue; }
|
|
1274
1436
|
if(kw==='wrap'){ err(n,RETIRED_WRAP); continue; }
|
|
1275
1437
|
if(kw==='boundary'){ err(n,RETIRED_BOUNDARY); continue; }
|
|
1276
1438
|
if(kw==='layer'){ err(n,RETIRED_LAYER); continue; }
|
|
1277
1439
|
if(kw==='guide'){ err(n,RETIRED_GUIDE); continue; }
|
|
1278
1440
|
if(kw==='wave'){ err(n,RETIRED_WAVE); continue; }
|
|
1279
1441
|
if(kw==='size'){ err(n,RETIRED_SIZE); continue; }
|
|
1280
|
-
if(kw==='plot'){ err(n,'plot has been renamed: use chart (plot reads as an imperative — the reason render was retired — while every other block opener is a noun; ECharts, Chart.js and Mermaid all name the object a chart) (MIGRATIONS)'); continue; }
|
|
1442
|
+
if(kw==='plot'){ err(n,'plot has been renamed: use chart (plot reads as an imperative — the reason render was retired — while every other block opener is a noun; ECharts, Chart.js and Mermaid all name the object a chart) (MIGRATIONS 0.1)'); continue; }
|
|
1281
1443
|
|
|
1282
1444
|
// typed-block children
|
|
1283
1445
|
if(cur && ['field','break','cell','width','signal','gap'].includes(kw)){
|
|
@@ -1355,7 +1517,7 @@ function parseOne(text){
|
|
|
1355
1517
|
if(opts.index!==undefined){
|
|
1356
1518
|
err(n,'index= is not available on the compact field form — the range would apply LINE-wide, saying that every item repeats over the same indices. Write the repeated element in the classic form on its own line: field "<name>" <width> index=0..7'); continue; }
|
|
1357
1519
|
if(items0.length>1){
|
|
1358
|
-
err(n,'field: the item list is ONE comma-delimited token — unexpected argument "'+items0[1]+'" (write field a:1,b:2 with no space after the comma; quote a name that contains whitespace: "Long Name":16) (MIGRATIONS)'); continue; }
|
|
1520
|
+
err(n,'field: the item list is ONE comma-delimited token — unexpected argument "'+items0[1]+'" (write field a:1,b:2 with no space after the comma; quote a name that contains whitespace: "Long Name":16) (MIGRATIONS 0.1)'); continue; }
|
|
1359
1521
|
let bad=null; const parsed=[];
|
|
1360
1522
|
for(const el of splitList(posT[1],0)){
|
|
1361
1523
|
const it=el.v; if(!it) continue;
|
|
@@ -1420,7 +1582,7 @@ function parseOne(text){
|
|
|
1420
1582
|
// `POSITIONAL-LIST-SPELLING`: comma form only; the space form is retired.
|
|
1421
1583
|
const wtoks=pos.slice(1);
|
|
1422
1584
|
if(wtoks.length>1){
|
|
1423
|
-
err(n,'width takes ONE comma-delimited token: write width '+joinListForm(wtoks)+' — the space form is retired (MIGRATIONS)'); continue; }
|
|
1585
|
+
err(n,'width takes ONE comma-delimited token: write width '+joinListForm(wtoks)+' — the space form is retired (MIGRATIONS 0.1)'); continue; }
|
|
1424
1586
|
const vals=wtoks.length?splitList(posT[1],0).map(e=>e.v):[];
|
|
1425
1587
|
if(!vals.length){ err(n,'width needs one value per column (auto | <px> | <n>%)'); continue; }
|
|
1426
1588
|
let badw=null;
|
|
@@ -1449,7 +1611,7 @@ function parseOne(text){
|
|
|
1449
1611
|
// stays bare, exactly as `threshold offset=50%` does.
|
|
1450
1612
|
const cadr=pos[1]||'';
|
|
1451
1613
|
if(/^h?\d+,\d+$/.test(cadr)){
|
|
1452
|
-
err(n,'cell address is now a paren point: cell ('+cadr+') — a bare comma pair is a list of two numbers, not an address (MIGRATIONS)'); continue; }
|
|
1614
|
+
err(n,'cell address is now a paren point: cell ('+cadr+') — a bare comma pair is a list of two numbers, not an address (MIGRATIONS 0.1)'); continue; }
|
|
1453
1615
|
const rc=/^\((h?)(\d+),(\d+)\)$/.exec(cadr)||/^(h?)(\d+)$/.exec(cadr);
|
|
1454
1616
|
// `RULE-POSITION-ENUMERATION`: `highlight` is a bare keyword FLAG
|
|
1455
1617
|
// (vocabulary-sources.tsv `cell.highlight`), so RULE 2.4 governs it
|
|
@@ -1544,7 +1706,16 @@ function parseOne(text){
|
|
|
1544
1706
|
// must be in the header genre allowlist. Child keywords still use the
|
|
1545
1707
|
// "needs a bitfield/table/timing above" path when they appear with no cur.
|
|
1546
1708
|
if(sawHeader && doc.genre && GENRE_KW[doc.genre] && !CHILD_KW.has(kw) && !GENRE_KW[doc.genre].has(kw)){
|
|
1547
|
-
|
|
1709
|
+
// `GENRE-NODE-SPELLING`: a word this genre SPELLS DIFFERENTLY is not an
|
|
1710
|
+
// unknown word, and "not allowed in genre statechart" would send an
|
|
1711
|
+
// author looking for a construct they cannot have instead of the one
|
|
1712
|
+
// they already wrote. The named diagnostic says which word and why.
|
|
1713
|
+
// (`edge`/`flowline`/`transition` never reach here — they are
|
|
1714
|
+
// dispatched by their own scanner above.)
|
|
1715
|
+
if(NODE_SPELLINGS.has(kw) && GENRE_NODE_KW[doc.genre])
|
|
1716
|
+
err(n, WRONG_WORD(kw, GENRE_NODE_KW[doc.genre], doc.genre));
|
|
1717
|
+
else
|
|
1718
|
+
err(n,'"'+kw+'" is not allowed in genre '+doc.genre);
|
|
1548
1719
|
continue;
|
|
1549
1720
|
}
|
|
1550
1721
|
if(badOpts(kw)) continue;
|
|
@@ -1570,7 +1741,7 @@ function parseOne(text){
|
|
|
1570
1741
|
// meaning: the token is a normal quoted string and the generic
|
|
1571
1742
|
// tokenizer above has already resolved its escapes.
|
|
1572
1743
|
const t0=tk.toks[1];
|
|
1573
|
-
if(!t0||!t0.q){ err(n,'title needs a quoted string: title "<text>" (MIGRATIONS)'); break; }
|
|
1744
|
+
if(!t0||!t0.q){ err(n,'title needs a quoted string: title "<text>" (MIGRATIONS 0.1)'); break; }
|
|
1574
1745
|
if(tk.toks.length>2){ err(n,'unexpected argument "'+tk.toks[2].v+'"'); break; }
|
|
1575
1746
|
doc.title=t0.v; sawTitle=true; break;
|
|
1576
1747
|
}
|
|
@@ -1632,9 +1803,15 @@ function parseOne(text){
|
|
|
1632
1803
|
// being wrong is harmless, and a flowchart node may be a datastore, an
|
|
1633
1804
|
// annotation or a state, so `role:"process"` by default would let the
|
|
1634
1805
|
// model assert a falsehood the figure cannot be inspected to catch.
|
|
1806
|
+
// `GENRE-NODE-SPELLING`: `state` joins the same case. It is `node` renamed
|
|
1807
|
+
// for one genre, not a role — a statechart node has exactly one kind,
|
|
1808
|
+
// so there is no role to record and `role` stays null, exactly as a
|
|
1809
|
+
// bare `node` does. The DRAWING is unchanged too (`shape=box` default),
|
|
1810
|
+
// which is what let the five corpus figures migrate byte-identically.
|
|
1635
1811
|
case 'process': case 'decision': case 'terminator':
|
|
1812
|
+
case 'state':
|
|
1636
1813
|
case 'node': {
|
|
1637
|
-
const role=kw==='node'?null:kw;
|
|
1814
|
+
const role=(kw==='node'||kw==='state')?null:kw;
|
|
1638
1815
|
const id=pos[1];
|
|
1639
1816
|
{ const e=idErr(id,posq[1],kw+' needs an id'); if(e){ err(n,e); break; } }
|
|
1640
1817
|
if(dupId(id)){ err(n,'duplicate id "'+id+'"'); break; }
|
|
@@ -1703,7 +1880,7 @@ function parseOne(text){
|
|
|
1703
1880
|
break;
|
|
1704
1881
|
}
|
|
1705
1882
|
case 'flow': {
|
|
1706
|
-
if(sawFlow){ err(n,'duplicate flow
|
|
1883
|
+
if(sawFlow){ err(n,'duplicate flow directive'); break; }
|
|
1707
1884
|
// `RULE-POSITION-ENUMERATION`: `flow.direction` is an enum position — bare.
|
|
1708
1885
|
if(posq[1]){ err(n,ENUM_BARE('flow '+pos[1])); break; }
|
|
1709
1886
|
if(!['right','down','left','up'].includes(pos[1])){ err(n,'flow needs right|down|left|up'); break; }
|
|
@@ -1736,7 +1913,7 @@ function parseOne(text){
|
|
|
1736
1913
|
// could not even detect a half-converted line.
|
|
1737
1914
|
const rtoks=pos.slice(1), rT=posT.slice(1);
|
|
1738
1915
|
if(rtoks.length>1){
|
|
1739
|
-
err(n,'rank takes ONE comma-delimited token: write rank '+joinListForm(rtoks)+' — the space form is retired (MIGRATIONS)'); break; }
|
|
1916
|
+
err(n,'rank takes ONE comma-delimited token: write rank '+joinListForm(rtoks)+' — the space form is retired (MIGRATIONS 0.1)'); break; }
|
|
1740
1917
|
const rels=rtoks.length?splitList(rT[0],0):[];
|
|
1741
1918
|
let badr=null;
|
|
1742
1919
|
for(const e of rels){
|
|
@@ -1776,7 +1953,7 @@ function parseOne(text){
|
|
|
1776
1953
|
// is now detectable — a second positional token after the label is
|
|
1777
1954
|
// the half-converted line the old tolerance could not report.
|
|
1778
1955
|
if(restT.length>1){
|
|
1779
|
-
err(n,'bundle members take ONE comma-delimited token: write bundle '+id+(tlabel===null?'':' "'+tlabel+'"')+' '+joinListForm(restT.map(t=>t.v))+' — the space form is retired (MIGRATIONS)'); break; }
|
|
1956
|
+
err(n,'bundle members take ONE comma-delimited token: write bundle '+id+(tlabel===null?'':' "'+tlabel+'"')+' '+joinListForm(restT.map(t=>t.v))+' — the space form is retired (MIGRATIONS 0.1)'); break; }
|
|
1780
1957
|
const pairs=[]; let badp=null;
|
|
1781
1958
|
outerB:
|
|
1782
1959
|
for(const t of restT){
|
|
@@ -1826,7 +2003,7 @@ function parseOne(text){
|
|
|
1826
2003
|
// percentage" `offset` (on <stop>), which is the same source the
|
|
1827
2004
|
// paint keys come from, so nothing is invented.
|
|
1828
2005
|
if(opts.at!==undefined){
|
|
1829
|
-
err(n,'threshold at= has been renamed: use offset=<0..100>% (at= is the pin POINT in canvas px; SVG spells a position along an extent "offset") (MIGRATIONS)'); break; }
|
|
2006
|
+
err(n,'threshold at= has been renamed: use offset=<0..100>% (at= is the pin POINT in canvas px; SVG spells a position along an extent "offset") (MIGRATIONS 0.1)'); break; }
|
|
1830
2007
|
const m=/^(\d+(?:\.\d+)?)%$/.exec(opts.offset||''); // % is mandatory (`BARE-FRACTION-VALUES`)
|
|
1831
2008
|
if(!m||+m[1]<0||+m[1]>100){ err(n,'threshold needs offset=<0..100>% (with the % sign)'); break; }
|
|
1832
2009
|
// §5: `stroke=` is the marker colour, `color=` the label colour,
|
|
@@ -1877,7 +2054,7 @@ function parseOne(text){
|
|
|
1877
2054
|
// reads the same way. `OPTION-POSITION-PARSING` quotedness is `pos`-aligned, so an option
|
|
1878
2055
|
// may still precede it.
|
|
1879
2056
|
const flabel=posq[1]?pos[1]:null;
|
|
1880
|
-
if(flabel===null){ err(n,'band needs a quoted "<label>" first: band "<name>" <a>..<b>% in=<node-or-group-id> (a band with no label asserts nothing a reader may keep — fill= is presentation, and §5 forbids meaning riding on colour alone) (MIGRATIONS)'); break; }
|
|
2057
|
+
if(flabel===null){ err(n,'band needs a quoted "<label>" first: band "<name>" <a>..<b>% in=<node-or-group-id> (a band with no label asserts nothing a reader may keep — fill= is presentation, and §5 forbids meaning riding on colour alone) (MIGRATIONS 0.1)'); break; }
|
|
1881
2058
|
if(!opts['in']){ err(n,'band needs in=<node-or-group-id>'); break; }
|
|
1882
2059
|
// 0.1: the `%` is MANDATORY, matching `threshold offset=` (`BARE-FRACTION-VALUES`).
|
|
1883
2060
|
// `band 15`, `band 15-35` and `band 15%-35` all parsed before (the
|
|
@@ -1892,7 +2069,7 @@ function parseOne(text){
|
|
|
1892
2069
|
// cited by `bitfield`. `band` is EXPERIMENTAL (`CONSTRUCT-STATUS-TIERS`), so no
|
|
1893
2070
|
// compatibility promise is owed; the diagnostic is owed anyway.
|
|
1894
2071
|
if(/^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)%$/.test(pos[2]||'')){
|
|
1895
|
-
err(n,'the hyphen range "'+pos[2]+'" is no longer the spelling: write band "'+flabel+'" '+String(pos[2]).replace('-','..')+' in=… — FigDown has ONE range grammar, "..", and a hyphen between two numbers reads as subtraction (MIGRATIONS)'); break; }
|
|
2072
|
+
err(n,'the hyphen range "'+pos[2]+'" is no longer the spelling: write band "'+flabel+'" '+String(pos[2]).replace('-','..')+' in=… — FigDown has ONE range grammar, "..", and a hyphen between two numbers reads as subtraction (MIGRATIONS 0.1)'); break; }
|
|
1896
2073
|
const m=/^(\d+(?:\.\d+)?)%$|^(\d+(?:\.\d+)?)\.\.(\d+(?:\.\d+)?)%$/.exec(pos[2]||'');
|
|
1897
2074
|
if(!m){ err(n,'band needs a range with the % sign: band "<label>" <pct>% or band "<label>" <a>..<b>%'); break; }
|
|
1898
2075
|
if(pos.length>3){ err(n,'unexpected argument "'+pos[3]+'"'); break; }
|
|
@@ -1925,7 +2102,7 @@ function parseOne(text){
|
|
|
1925
2102
|
if(!id){ err(n,PIN_SHAPE); break; }
|
|
1926
2103
|
// 0.1 (RULE 1.1a): the pair is PARENTHESISED — at=(x,y).
|
|
1927
2104
|
if(at!==undefined&&/^-?\d+(?:\.\d+)?,-?\d+(?:\.\d+)?$/.test(at)){
|
|
1928
|
-
err(n,'pin at= now takes a paren point: at=('+at+') — a bare comma pair is a list of two numbers, not a point (MIGRATIONS)'); break; }
|
|
2105
|
+
err(n,'pin at= now takes a paren point: at=('+at+') — a bare comma pair is a list of two numbers, not a point (MIGRATIONS 0.1)'); break; }
|
|
1929
2106
|
const m=at===undefined?null:/^\((-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)\)$/.exec(at);
|
|
1930
2107
|
// A WRITTEN `at=` that does not parse is its own error, never the
|
|
1931
2108
|
// whole-line shape message: the author declared a point and got the
|
|
@@ -2134,9 +2311,9 @@ function parseOne(text){
|
|
|
2134
2311
|
if(!c||c.stroke!==undefined) continue;
|
|
2135
2312
|
if(c.fill===undefined&&c.style!==undefined) continue;
|
|
2136
2313
|
if(c.fill!==undefined)
|
|
2137
|
-
errs.push('Line '+e.line+': class "'+cid+'" sets fill= but no stroke=, and an edge has no interior — add stroke= to the class (it paints the edge; fill= keeps painting members that have an interior) (MIGRATIONS)');
|
|
2314
|
+
errs.push('Line '+e.line+': class "'+cid+'" sets fill= but no stroke=, and an edge has no interior — add stroke= to the class (it paints the edge; fill= keeps painting members that have an interior) (MIGRATIONS 0.1)');
|
|
2138
2315
|
else
|
|
2139
|
-
errs.push('Line '+e.line+': class "'+cid+'" declares no channel an edge has — add stroke= (an edge has only stroke= and style=: no interior, and v0.1 has no label-colour key). Without one the edge takes the default colour and the class shows nothing in the legend (MIGRATIONS)');
|
|
2316
|
+
errs.push('Line '+e.line+': class "'+cid+'" declares no channel an edge has — add stroke= (an edge has only stroke= and style=: no interior, and v0.1 has no label-colour key). Without one the edge takes the default colour and the class shows nothing in the legend (MIGRATIONS 0.1)');
|
|
2140
2317
|
}
|
|
2141
2318
|
}
|
|
2142
2319
|
{ // class references must resolve (closed grammar)
|
|
@@ -2313,7 +2490,7 @@ function findReservedSemi(s){
|
|
|
2313
2490
|
}
|
|
2314
2491
|
return -1;
|
|
2315
2492
|
}
|
|
2316
|
-
const RESERVED_SEMI='";" is reserved for a future statement separator and has no meaning in v0.1 — write one directive per line; a literal ";" belongs inside a quoted string, an [edge label] or a comment (MIGRATIONS)';
|
|
2493
|
+
const RESERVED_SEMI='";" is reserved for a future statement separator and has no meaning in v0.1 — write one directive per line; a literal ";" belongs inside a quoted string, an [edge label] or a comment (MIGRATIONS 0.1)';
|
|
2317
2494
|
|
|
2318
2495
|
function findComment(s){
|
|
2319
2496
|
// '#' starts a comment only at line start or after whitespace,
|