figdown 0.1.8 → 0.3.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 +971 -169
- package/dist/figdown.mjs +971 -169
- package/examples/evpn-fabric.svg +3 -4
- 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 +30 -18
- package/guide/layout.md +19 -19
- package/guide/showcase.md +62 -56
- package/package.json +2 -2
- package/skill/figdown/SKILL.md +30 -4
- package/skill/figdown/figdown.html +1030 -194
- package/skill/figdown/reference/experimental/block.md +60 -0
- package/skill/figdown/reference/experimental/chart.md +32 -0
- package/skill/figdown/reference/experimental/flowchart.md +148 -20
- package/skill/figdown/reference/experimental/statechart.md +138 -0
- package/skill/figdown/reference/experimental/timing.md +1 -1
- package/skill/figdown/reference/experimental/topology.md +148 -22
- package/skill/figdown/reference/reading.md +25 -1
- package/skill/figdown/reference/scene.md +78 -10
- package/skill/figdown/reference/experimental/constructs.md +0 -90
package/dist/figdown.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// figdown.js — FigDown embeddable library (0.
|
|
1
|
+
// figdown.js — FigDown embeddable library (0.3.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.3.0";
|
|
14
14
|
|
|
15
15
|
// ---- engine (extracted verbatim from editor/figdown.html) ----
|
|
16
16
|
var __engine = (function () {
|
|
@@ -24,13 +24,56 @@ 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.3.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
|
+
// `DRAWN-ANNOTATION-FORM`: `figdown 0.3` joins the set. `note=` is a NEW OPTION KEY,
|
|
38
|
+
// and core §13.0 makes a new key a `Y` change and not a `Z` one — "`Z`: Bug
|
|
39
|
+
// fixes only. No new features. The language does not move." Shipping `note=`
|
|
40
|
+
// under `v0.2.z` would make `figdown 0.2` name two different languages: the one
|
|
41
|
+
// `v0.2.0` published and the one with `note=`. So the language number moves.
|
|
42
|
+
const LANG_VERSIONS = ['0.1', '0.2', '0.3'];
|
|
43
|
+
// Genres per declared language version. `Y` never removes (core §13.0), so
|
|
44
|
+
// each row is a superset of the one above it, and `figdown 0.1 <anything>`
|
|
45
|
+
// resolves against exactly the list it resolved against before `STATECHART-GENRE-SCOPE`.
|
|
46
|
+
const GENRES_BY_VERSION = {
|
|
47
|
+
'0.1': ['block','topology','flowchart','bitfield','table','timing'],
|
|
48
|
+
'0.2': ['block','topology','flowchart','bitfield','table','timing','statechart'],
|
|
49
|
+
'0.3': ['block','topology','flowchart','bitfield','table','timing','statechart']
|
|
50
|
+
};
|
|
51
|
+
// The version an OPTION KEY first becomes legal in — the `CONNECTOR_MIN_VERSION`
|
|
52
|
+
// device, applied to the option namespace. `DRAWN-ANNOTATION-FORM`: `note=` is gated on the
|
|
53
|
+
// declared version, and the gate's reason is specific to THIS key rather than
|
|
54
|
+
// generic to new keys. `note=` has a PRIOR MEANING on the record: it was the
|
|
55
|
+
// retired spelling of `description=` (`DESCRIPTION-KEY-SPELLING`) and its retirement
|
|
56
|
+
// diagnostic actively told authors to write `description=` for a tooltip.
|
|
57
|
+
// Accepting it silently under a `figdown 0.2` header would repaint a document
|
|
58
|
+
// whose author meant a never-drawn tooltip as one that puts ink on the page —
|
|
59
|
+
// core §13.0.1's named hazard, "a figure that looks right and means something
|
|
60
|
+
// else". A key that had never been spelled before would carry no such risk.
|
|
61
|
+
const OPT_MIN_VERSION={note:'0.3'};
|
|
62
|
+
// True when the document's declared version is older than the key's own.
|
|
63
|
+
// A document with no parsable header has already been diagnosed on line 1, so
|
|
64
|
+
// an absent version never gates a second time.
|
|
65
|
+
const belowOptVersion=(key,ver)=>{
|
|
66
|
+
const need=OPT_MIN_VERSION[key];
|
|
67
|
+
if(!need||!ver) return false;
|
|
68
|
+
const i=LANG_VERSIONS.indexOf(ver), j=LANG_VERSIONS.indexOf(need);
|
|
69
|
+
return i>=0 && j>=0 && i<j;
|
|
70
|
+
};
|
|
28
71
|
// Retired shape VALUES keep a named diagnostic (PROCESS §5(d)), the same way
|
|
29
72
|
// retired option keys do: `cloud` was the one value that named a domain
|
|
30
73
|
// (the internet cloud) in an enum the language keeps purely geometric
|
|
31
74
|
// (`SHAPE-ENUM-VOCABULARY`/`EXTERNAL-EDGE-ENDPOINTS`), so it was removed rather than demoted.
|
|
32
75
|
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)'
|
|
76
|
+
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
77
|
};
|
|
35
78
|
// Colors are CSS hex (#rgb / #rrggbb) or CSS named colors (spec §1) — the
|
|
36
79
|
// 147 CSS/SVG color keywords (lowercase) plus `transparent`. Anything else
|
|
@@ -208,7 +251,7 @@ function splitList(t,off){
|
|
|
208
251
|
// - a key=value token with an unregistered key is an "unknown option"
|
|
209
252
|
// line error (`UNKNOWN-OPTION-DEGRADATION`) — except inside timing `signal` lanes, where bare
|
|
210
253
|
// tokens may contain '=' and stay positional (laneMode).
|
|
211
|
-
// `fill` was registered here until
|
|
254
|
+
// `fill` was registered here until 0.1 solely to power a retired
|
|
212
255
|
// migration diagnostic on the old `line` directive; it left the registry with
|
|
213
256
|
// the `fill` → `band` KEYWORD rename, and 0.1 gave the word back to the
|
|
214
257
|
// option-key namespace as the primary presentation key (`color=` → `fill=`).
|
|
@@ -223,11 +266,11 @@ function splitList(t,off){
|
|
|
223
266
|
// time the language gains no replacement — v0.1 has no author-facing label
|
|
224
267
|
// colour at all (the default is derived, `LABEL-COLOUR-SOURCE`; the owner-level key that could
|
|
225
268
|
// be added today is the wrong shape, core §9 `ANNOTATION-LOCATOR-SPLIT`). It stays registered so
|
|
226
|
-
// the message can name BOTH eras: a `color=` written
|
|
227
|
-
// the FILL, one written meant the LABEL, and only a human
|
|
269
|
+
// the message can name BOTH eras: a `color=` written in one era meant
|
|
270
|
+
// the FILL, one written in another meant the LABEL, and only a human
|
|
228
271
|
// knows which document this is. `text` and `z` stay registered
|
|
229
272
|
// as RETIRED keys so each rename gets a named diagnostic. `offset` replaces
|
|
230
|
-
// `threshold at=` (the directive was spelled `guide` until
|
|
273
|
+
// `threshold at=` (the directive was spelled `guide` until 0.1);
|
|
231
274
|
// `at` stays live on `pin`.
|
|
232
275
|
// 0.1: `level` stays registered as a RETIRED key — the construct was
|
|
233
276
|
// DELETED (`CHART-LEVEL-KEY`), and a registered-but-retired key is the only way the
|
|
@@ -258,7 +301,7 @@ const OPT_KEYS=new Set(['kind','type','shape','fill','color','stroke','text','in
|
|
|
258
301
|
// - `external` is NEVER drawn (`EXTERNAL-EDGE-ENDPOINTS`) — no fill, no border, no dash; only its
|
|
259
302
|
// label exists, so it takes `text=` (plus `plane=`, organizational exactly
|
|
260
303
|
// as on a node);
|
|
261
|
-
// - `band` carried NO label channel at all until
|
|
304
|
+
// - `band` carried NO label channel at all until 0.1 (`BAND-LABEL-STATUS`); it now
|
|
262
305
|
// takes a mandatory quoted label, so `color=` applies to it like any
|
|
263
306
|
// other labelled element;
|
|
264
307
|
// - typed blocks (`bitfield`/`table`/`timing`) stack in document order OUTSIDE
|
|
@@ -277,21 +320,48 @@ const OPT_KEYS=new Set(['kind','type','shape','fill','color','stroke','text','in
|
|
|
277
320
|
// the key existed only because there was no label to colour.
|
|
278
321
|
const DIRECTIVE_OPTS={
|
|
279
322
|
figdown:[],
|
|
280
|
-
|
|
323
|
+
// `DRAWN-ANNOTATION-FORM`: `title` gains its FIRST option key. It took one
|
|
324
|
+
// positional string and nothing else until now, and the key
|
|
325
|
+
// it gains carries the figure-level annotation — 14% of the measured demand,
|
|
326
|
+
// 10 instances that name no single element ("Total: 2 blocks × 8 ways × 1k
|
|
327
|
+
// sets = 16,384 entries", a four-signal legend, a TODO about the figure).
|
|
328
|
+
// The figure HAS a declaration line, so attachment-by-position reaches it and
|
|
329
|
+
// no standalone keyword is needed. `UNIVERSAL-CORE-KEYWORDS` fixes what `title` MEANS across genres;
|
|
330
|
+
// it is not a bar on the directive taking options, so the key exists in every
|
|
331
|
+
// genre at once, which is correct — every genre has figures.
|
|
332
|
+
title:['note'],
|
|
333
|
+
node:['shape','fill','stroke','style','class','in','width','height','note'],
|
|
281
334
|
// `FLOWCHART-ROLE-KEYWORDS`: the three flowchart role keywords take EXACTLY the
|
|
282
335
|
// option keys `node` takes — they ARE nodes, with a role recorded. Listing
|
|
283
336
|
// `width`/`height` mirrors `node` so the same "use a pin line" diagnostic
|
|
284
337
|
// fires rather than a bare `unknown option`.
|
|
285
|
-
process:['shape','fill','stroke','style','class','in','
|
|
286
|
-
decision:['shape','fill','stroke','style','class','in','
|
|
287
|
-
terminator:['shape','fill','stroke','style','class','in','
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
338
|
+
process:['shape','fill','stroke','style','class','in','width','height','note'],
|
|
339
|
+
decision:['shape','fill','stroke','style','class','in','width','height','note'],
|
|
340
|
+
terminator:['shape','fill','stroke','style','class','in','width','height','note'],
|
|
341
|
+
// `GENRE-NODE-SPELLING`: `state` IS `node` under `statechart` — a rename, not a
|
|
342
|
+
// new directive, so it takes `node`'s keys exactly and nothing more.
|
|
343
|
+
state:['shape','fill','stroke','style','class','in','width','height','note'],
|
|
344
|
+
group:['fill','stroke','style','gap','class','note'],
|
|
345
|
+
// `PAINT-ORDER-CONSTRUCT`: `external` now takes NO option key at all. `plane=`
|
|
346
|
+
// was its only one — it is never drawn (`EXTERNAL-EDGE-ENDPOINTS`), so it has no fill, no border
|
|
347
|
+
// and no dash to set — and the withdrawal of `plane=` empties the row. An
|
|
348
|
+
// empty array is the declaration: every key falls through to the generic
|
|
349
|
+
// `external does not take <k>=`.
|
|
350
|
+
external:[],
|
|
351
|
+
edge:['style','class','fill','stroke','label','taillabel','headlabel','note'],
|
|
352
|
+
// `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`: same rename argument — the connector's option set is one set
|
|
353
|
+
// under three spellings, listed three times only because the tables are
|
|
354
|
+
// keyed by the surface word an author actually wrote.
|
|
355
|
+
flowline:['style','class','fill','stroke','label','taillabel','headlabel','note'],
|
|
356
|
+
transition:['style','class','fill','stroke','label','taillabel','headlabel','note'],
|
|
357
|
+
// `PAINT-ORDER-CONSTRUCT`: the `plane` row is GONE, not emptied — the keyword is
|
|
358
|
+
// withdrawn from the language, so it has no acceptor row at all, the shape
|
|
359
|
+
// `path`/`routing` left behind. `z-index=` goes with it: it
|
|
360
|
+
// was legal on `plane` and on nothing else.
|
|
361
|
+
flow:[], rank:[],
|
|
362
|
+
bundle:['fill','stroke','style'],
|
|
363
|
+
threshold:['in','at','offset','fill','stroke','style'],
|
|
364
|
+
band:['in','extend','fill','stroke','style','from','to'],
|
|
295
365
|
// `ELEMENT-GEOMETRY-DIRECTIVE`: `size` merged into `pin`. ONE directive carries an
|
|
296
366
|
// element's whole DECLARED geometry — `at=` places it, `width=`/`height=`
|
|
297
367
|
// extend it — and one model object records it. All three keys are optional
|
|
@@ -302,7 +372,7 @@ const DIRECTIVE_OPTS={
|
|
|
302
372
|
// derives its geometry from its content).
|
|
303
373
|
pin:['at','width','height'],
|
|
304
374
|
layout:[],
|
|
305
|
-
'class':['fill','stroke','style'
|
|
375
|
+
'class':['fill','stroke','style'],
|
|
306
376
|
// 0.1: `class=` is NOT accepted on the typed-block OPENERS. The
|
|
307
377
|
// normative registry (core §10) lists its acceptors as node/group/edge/
|
|
308
378
|
// field/cell — the block openers were an engine-only extra with 0 uses in
|
|
@@ -326,7 +396,7 @@ const DIRECTIVE_OPTS={
|
|
|
326
396
|
// the field is ONE ELEMENT of a repeated run and gives the run's index
|
|
327
397
|
// range; the engine derives the elision row and the index labels from it,
|
|
328
398
|
// exactly as it derives the dash and the caption from `present=`.
|
|
329
|
-
field:['fill','stroke','class','description','present','index'], 'break':[],
|
|
399
|
+
field:['fill','stroke','class','description','present','index','note'], 'break':[],
|
|
330
400
|
cell:['fill','stroke','class'], width:[],
|
|
331
401
|
signal:['data','fill','stroke'], gap:[]
|
|
332
402
|
};
|
|
@@ -370,14 +440,14 @@ const ENUM_OPT_KEYS=['shape','style','numbering','extend','type'];
|
|
|
370
440
|
// `color=` sets the TEXT — and once `color=` is live again the engine can no
|
|
371
441
|
// longer diagnose a pre-0.1 document at all.
|
|
372
442
|
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)',
|
|
443
|
+
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)',
|
|
444
|
+
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)',
|
|
445
|
+
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)',
|
|
446
|
+
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
447
|
// `COLOUR-KEY-STATUS`. This is the ONLY key in the language whose diagnostic
|
|
378
448
|
// must name two eras and refuse to choose between them: the same six
|
|
379
|
-
// characters meant the FILL
|
|
380
|
-
//
|
|
449
|
+
// characters meant the FILL in one era and the LABEL
|
|
450
|
+
// in another, and no engine can tell the two source files apart. Retiring
|
|
381
451
|
// the key is what makes the difference DIAGNOSABLE at all — while it was
|
|
382
452
|
// live, a pre-0.1 document parsed and drew a legal, wrong figure in
|
|
383
453
|
// silence.
|
|
@@ -392,10 +462,19 @@ const RETIRED_OPT_KEYS={
|
|
|
392
462
|
// those observables, says plainly when a file has neither, and cites the
|
|
393
463
|
// release only as a MIGRATIONS lookup. Same rule as the migration tool's
|
|
394
464
|
// `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)',
|
|
465
|
+
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
466
|
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
|
|
398
|
-
|
|
467
|
+
// `PAINT-ORDER-CONSTRUCT`: `layer=` was renamed `plane=`, and
|
|
468
|
+
// `plane=` has since been WITHDRAWN, so this message can no longer end at
|
|
469
|
+
// the rename — the `route`→`path` precedent, where a message
|
|
470
|
+
// pointing at a spelling that no longer exists had to state the whole chain.
|
|
471
|
+
layer:'layer= has been WITHDRAWN: it was renamed plane=, and plane= was withdrawn with the `plane` keyword (`PAINT-ORDER-CONSTRUCT`). There is no replacement spelling. Delete the key: everything paints in one plane, in document order. If the element is on a distinct logical layer of the SUBJECT — an overlay, a control plane — say so with a class= whose label states it, which is where that meaning belongs (core §5, `PRESENTATION-AS-MEANING-CARRIER`) (MIGRATIONS 0.3)',
|
|
472
|
+
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)',
|
|
473
|
+
// `PAINT-ORDER-CONSTRUCT`. `plane=` referenced a declared `plane`; with the
|
|
474
|
+
// keyword withdrawn from every genre the key would keep exactly ONE legal
|
|
475
|
+
// value — the implicit `base` — so it is withdrawn with it rather than left
|
|
476
|
+
// as a key that can only ever restate the default.
|
|
477
|
+
plane:'plane= has been WITHDRAWN with the `plane` keyword (`PAINT-ORDER-CONSTRUCT`): the construct is removed from the language, not renamed, so there is no spelling to migrate to. `plane=` named a declared plane, and with no way to declare one the key had a single legal value — `base`, the implicit plane every element is already on. Delete the key. What it did was PAINT ORDER, and paint order is document order: a later line paints on top. The measurement: stripping `plane` and `plane=` from examples/evpn-fabric.fd left the drawn SVG byte-identical but for one `data-edge` index, because the overlay meaning was carried by `class=overlay` throughout — which is where a logical layer of the SUBJECT belongs (core §5, `PRESENTATION-AS-MEANING-CARRIER`) (MIGRATIONS 0.3)',
|
|
399
478
|
// 0.1 (`EDGE-GEOMETRY-CONSTRUCTS`). These six keys end in a WITHDRAWAL, not a rename, so
|
|
400
479
|
// their messages have a shape no earlier retirement in this table has: they
|
|
401
480
|
// name no replacement spelling, because there is none. `via=`/`src=`/`dst=`
|
|
@@ -409,28 +488,94 @@ const RETIRED_OPT_KEYS={
|
|
|
409
488
|
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
489
|
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
490
|
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
|
-
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
491
|
+
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)',
|
|
492
|
+
// `PAINT-ORDER-CONSTRUCT`: `z-index=` itself. Its ONLY acceptor was `plane`, so
|
|
493
|
+
// with the keyword withdrawn the key has no directive left to sit on. Left
|
|
494
|
+
// in OPT_KEYS with no acceptor row it would have produced `<directive> does
|
|
495
|
+
// not take z-index=` — true, but it tells an author holding a 0.2 document
|
|
496
|
+
// that they picked the wrong host, when in fact there is no host. RULE 6.2
|
|
497
|
+
// placement: the spelling left the LANGUAGE, so it is reported wherever it
|
|
498
|
+
// appears.
|
|
499
|
+
'z-index':'z-index= has been WITHDRAWN with the `plane` keyword (`PAINT-ORDER-CONSTRUCT`): it was legal on `plane` and on nothing else, so it left with its only acceptor. There is no replacement spelling and no other directive to move it to. Delete the key: paint order is document order, and a later line paints on top (MIGRATIONS 0.3)',
|
|
500
|
+
z:'z= has been WITHDRAWN: it was renamed z-index=, and z-index= was withdrawn with the `plane` keyword (`PAINT-ORDER-CONSTRUCT`) — it was legal on `plane` and on nothing else. There is no replacement spelling. Delete the key: paint order is document order, a later line paints on top (MIGRATIONS 0.3)',
|
|
501
|
+
// `note` was HERE (`DESCRIPTION-KEY-SPELLING`) until this release (`DRAWN-ANNOTATION-FORM`), and its
|
|
502
|
+
// row is gone because the key is LIVE again — SYNTAX-STYLE RULE 4.9
|
|
503
|
+
// obligation 3 forbids leaving the retirement message standing past the
|
|
504
|
+
// revival, on the ground that a message telling an author to write
|
|
505
|
+
// `description=` where `note=` is now the correct key is worse than no
|
|
506
|
+
// message: it is the language actively misinforming its user. What replaces
|
|
507
|
+
// it is not silence but two NARROWER messages — NOTE_VERSION for a document
|
|
508
|
+
// that declares a language version older than the key, and NOTE_ON_FIELD for
|
|
509
|
+
// the one directive that keeps refusing it. See OPT_MIN_VERSION above.
|
|
510
|
+
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
511
|
};
|
|
512
|
+
// `DRAWN-ANNOTATION-FORM`. The two messages that REPLACE the `note=` retirement
|
|
513
|
+
// diagnostic. SYNTAX-STYLE RULE 4.9 obligation 3 requires the retirement
|
|
514
|
+
// message to be reversed in the same release that revives the spelling, and
|
|
515
|
+
// "reversed" does not mean "deleted": each of the two situations the old
|
|
516
|
+
// message used to cover keeps a named diagnostic of its own.
|
|
517
|
+
//
|
|
518
|
+
// (a) The document declares a language version older than the key. The gate is
|
|
519
|
+
// `KEYWORD-RENAME-SCOPE`'s device — name the version, offer the one-step fix — and its
|
|
520
|
+
// reason is stated in the message because `note=`'s prior meaning is what
|
|
521
|
+
// makes the gate necessary rather than merely tidy.
|
|
522
|
+
const NOTE_VERSION=(have)=>
|
|
523
|
+
'note= requires figdown 0.3 (this document declares '+have+'): under figdown '+
|
|
524
|
+
have+' the spelling is still the RETIRED one that meant description=, and an '+
|
|
525
|
+
'engine that accepted it here would repaint a tooltip as ink — a figure that '+
|
|
526
|
+
'looks right and means something else (core §13.0.1). note= is the DRAWN '+
|
|
527
|
+
'annotation: an explanation the human reader must SEE. Raise the header to '+
|
|
528
|
+
'figdown 0.3, or write description= if you meant prose only a machine reads '+
|
|
529
|
+
'(MIGRATIONS 0.3)';
|
|
530
|
+
// (b) The directive is `field`, which refuses the key at EVERY version. The
|
|
531
|
+
// bitfield genre already has `description=` for machine-facing prose, and
|
|
532
|
+
// no measured figure needs a DRAWN per-field aside — granting a directive
|
|
533
|
+
// both keys with no evidence spends the distinction before anyone needs it.
|
|
534
|
+
// The message states the distinction rather than naming a replacement,
|
|
535
|
+
// because `description=` is not a replacement: it reaches a different
|
|
536
|
+
// reader.
|
|
537
|
+
const NOTE_ON_FIELD=
|
|
538
|
+
'note= draws and is not accepted on field; use description= for prose a '+
|
|
539
|
+
'machine reads. The two keys divide by AUDIENCE, not by length: description= '+
|
|
540
|
+
'reaches the reading agent as an SVG <title> and puts no ink on the page, '+
|
|
541
|
+
'while note= is an explanation the human must see. A field\'s presence '+
|
|
542
|
+
'condition is present=, not either of them (MIGRATIONS 0.3)';
|
|
419
543
|
// `PLANE-KEYWORD-SPELLING`: the keyword `plane`/`plane=` was spelled `layer`/`layer=`.
|
|
420
|
-
|
|
544
|
+
// `PAINT-ORDER-CONSTRUCT`: `plane` is WITHDRAWN, so `layer`'s message states the
|
|
545
|
+
// whole chain and ends where `route`'s does — the precedent,
|
|
546
|
+
// when `path` was withdrawn out from under the spelling `route` pointed at.
|
|
547
|
+
// `PAINT-ORDER-CONSTRUCT`: `plane` is WITHDRAWN from the language. The construct
|
|
548
|
+
// left because every genre that could write it lost it at once, and for two
|
|
549
|
+
// different reasons that happen to converge:
|
|
550
|
+
// - `block` and `flowchart` had ZERO authored uses. Every authored use in
|
|
551
|
+
// the tree was a `topology` document.
|
|
552
|
+
// - `topology` had two, and they are the worst domain collision measured in
|
|
553
|
+
// the language: in networking a PLANE is the control / data / management
|
|
554
|
+
// partition of a device — one of the first distinctions the field teaches
|
|
555
|
+
// — and `topology` is precisely the genre network engineers author in.
|
|
556
|
+
// `examples/evpn-fabric.fd` showed the trap already closed: it wrote
|
|
557
|
+
// `plane overlay "VXLAN tunnels" z-index=2`, where `overlay` is itself a
|
|
558
|
+
// networking term, so the line read as a network-architectural assertion
|
|
559
|
+
// and was in fact a paint order.
|
|
560
|
+
// What replaced it was already there. Stripping both writings from that file
|
|
561
|
+
// left the drawn SVG byte-identical apart from one `data-edge` index, because
|
|
562
|
+
// `class=overlay` carried the meaning the whole time.
|
|
563
|
+
const WITHDRAWN_PLANE_WHERE=' The decision and its evidence: MIGRATIONS 0.3, decisions/registry.md.';
|
|
564
|
+
const RETIRED_PLANE='plane has been WITHDRAWN from the language (`PAINT-ORDER-CONSTRUCT`) — removed, not renamed, so there is no replacement spelling. It declared a DRAWING LAYER (a z-order), and in the genre that actually used it "plane" means the control / data / management partition of a network device, so the one word said the wrong thing to exactly the readers who write the figure. Delete the line and delete every plane= that referenced it: paint order is document order, a later line paints on top. If the elements form a logical layer of the SUBJECT, that is a class= whose label states it (core §5, `PRESENTATION-AS-MEANING-CARRIER`) — which is what the two authored uses were already doing alongside it.'+WITHDRAWN_PLANE_WHERE;
|
|
565
|
+
const RETIRED_LAYER='layer has been WITHDRAWN: it was renamed plane, and plane was withdrawn from the language (`PAINT-ORDER-CONSTRUCT`). There is no replacement spelling. Delete the line: everything paints in one plane and paint order is document order (a later line paints on top). A logical layer of the SUBJECT — an overlay, a control plane — is a class= whose label says so (core §5, `PRESENTATION-AS-MEANING-CARRIER`).'+WITHDRAWN_PLANE_WHERE;
|
|
421
566
|
// `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)';
|
|
567
|
+
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
568
|
// `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)';
|
|
569
|
+
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
570
|
// `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)';
|
|
571
|
+
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
572
|
// `PRESENCE-FLAG-SPELLING`: the 0.1 rename `optional` -> `conditional` (`PRESENCE-FLAG-SPELLING`)
|
|
428
573
|
// is REVERTED. `conditional` has zero attestation as a wire-format field
|
|
429
574
|
// marker and zero uses in the downstream corpus, while "optional" appears in
|
|
430
575
|
// 34 downstream field LABELS — authors wrote the word in the label precisely
|
|
431
576
|
// because the keyword no longer said it. The MODEL key moves with the surface
|
|
432
577
|
// (`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)';
|
|
578
|
+
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
579
|
// `PRESENCE-CONDITION-EXPRESSION`: the positional flag `optional` becomes the option key
|
|
435
580
|
// `present=`, whose VALUE is the presence condition. A bare flag could say
|
|
436
581
|
// only THAT a field is conditional; every RFC that draws one also states WHY
|
|
@@ -439,7 +584,7 @@ const RETIRED_FIELD_CONDITIONAL='the field flag "conditional" has been retired:
|
|
|
439
584
|
// prose the model may not read. `present` is the attested spelling: X.680
|
|
440
585
|
// PRESENT, IP-XACT isPresent, SystemRDL ispresent, RFC 2784 "present only
|
|
441
586
|
// 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)';
|
|
587
|
+
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 the field\'s documentation prose — the key spelled note= at the time and description= — where it was invisible to the human reading the figure, and prose the model may not parse (`BITFIELD-CONDITIONAL-OFFSETS`). (Today\'s note= is a different key: it is the DRAWN annotation revived, it is refused on field, and it is not where a presence condition belongs either.) present= DRAWS: the field stays dashed and a stated condition becomes a caption under the block (MIGRATIONS 0.1)';
|
|
443
588
|
// `TIMING-GENRE-NAMING`: the EXPERIMENTAL genre `wave` became `timing`, both as
|
|
444
589
|
// the header genre token and as the block opener. The old name was WaveDrom's
|
|
445
590
|
// MEMBER KEY, not its figure name: in WaveJSON `signal` is the root object and
|
|
@@ -456,8 +601,8 @@ const RETIRED_FIELD_OPTIONAL='the field flag "optional" has been retired and rep
|
|
|
456
601
|
// to know that the keys survive unchanged and only their carrier moved.
|
|
457
602
|
// RULE 6.2 placement: the spelling left the LANGUAGE, so this fires wherever
|
|
458
603
|
// 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)';
|
|
604
|
+
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)';
|
|
605
|
+
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
606
|
// `EDGE-GEOMETRY-CONSTRUCTS`: `path` and `routing` are WITHDRAWN from the language.
|
|
462
607
|
// These two diagnostics are a NEW SHAPE for this table. Every retirement
|
|
463
608
|
// before them named a replacement spelling — `size` named `pin`, `guide` named
|
|
@@ -471,19 +616,19 @@ const RETIRED_WAVE='wave has been renamed: use timing (in WaveJSON `signal` is t
|
|
|
471
616
|
// not "use X"), says what an author should do instead (delete the line and let
|
|
472
617
|
// auto layout draw it, with the content-zone means named), and points at where
|
|
473
618
|
// the decision is RECORDED so the reasoning is one lookup away.
|
|
474
|
-
const WITHDRAWN_WHERE=' The decision and its evidence: MIGRATIONS 0.1, core §9 `EDGE-IDENTITY-AND-GEOMETRY`, decisions/registry.md';
|
|
619
|
+
const WITHDRAWN_WHERE=' The decision and its evidence: MIGRATIONS 0.1, core §9 `EDGE-IDENTITY-AND-GEOMETRY`, decisions/registry.md.';
|
|
475
620
|
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
621
|
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
622
|
// `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)';
|
|
623
|
+
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
624
|
// 0.1 (§8.4): `edge`, `threshold` and `bundle` have NO interior, so
|
|
480
625
|
// `fill=` and `stroke=` named the SAME channel and `stroke=` won silently —
|
|
481
626
|
// two keys for one channel, resolved by an undocumented precedence that
|
|
482
627
|
// produced a legal, wrong figure whenever both were written (16 lines in
|
|
483
628
|
// this repository, 3 of them writing both on one line). Same defect shape as
|
|
484
629
|
// 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)';
|
|
630
|
+
const NO_INTERIOR=new Set(['edge','flowline','transition','threshold','bundle']);
|
|
631
|
+
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
632
|
// `STYLE-KEY-SCOPE`: `style=` left these three directives (it stays live on
|
|
488
633
|
// node/group/edge/class/bundle/threshold/band). The generic
|
|
489
634
|
// "<directive> does not take style=" would be true but would not say why, and
|
|
@@ -494,11 +639,11 @@ const FILL_NO_INTERIOR=k=>k+' has no interior, so fill= and stroke= name the sam
|
|
|
494
639
|
// are closed here the way every other one-channel-two-keys collision in this
|
|
495
640
|
// language was closed — a named line error, not a precedence rule. A
|
|
496
641
|
// 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)';
|
|
642
|
+
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)';
|
|
643
|
+
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
644
|
const NO_ITEM_STYLE=new Set(['field','cell','signal']);
|
|
500
645
|
const STYLE_NO_ITEM=k=>k+' does not take style= — '+(k==='field'
|
|
501
|
-
? 'on a field the dash IS conditional presence (`present=`, spelled `optional` until
|
|
646
|
+
? 'on a field the dash IS conditional presence (`present=`, spelled `optional` until 0.1), 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)'
|
|
502
647
|
: 'a dash on one '+k+' carried no meaning the block does not already carry, and 0 documents outside this repository wrote it')
|
|
503
648
|
// `DESCRIPTION-KEY-SPELLING` corrected the second half of this message. It used to
|
|
504
649
|
// offer `note=` as a place to put knowledge, which was wrong twice over:
|
|
@@ -507,7 +652,7 @@ const STYLE_NO_ITEM=k=>k+' does not take style= — '+(k==='field'
|
|
|
507
652
|
// human, which is the same defect `STYLE-KEY-SCOPE` exists to close. The two channels
|
|
508
653
|
// that DRAW are the name/label and a `class` meaning (which also earns a
|
|
509
654
|
// 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)';
|
|
655
|
+
+'. 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
656
|
function splitOpts(toks, laneMode){
|
|
512
657
|
// a repeated option key on one line is a line error, never
|
|
513
658
|
// silent last-wins. `dup` names the first key that appeared twice.
|
|
@@ -557,7 +702,7 @@ const ID_RE=/^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
|
557
702
|
// `a`+`x--b`) and the greedy member regex silently committed to the first,
|
|
558
703
|
// making the second unreachable and undiagnosed — RULE 6.3 says malformed
|
|
559
704
|
// 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)';
|
|
705
|
+
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
706
|
// `QUOTED-IDS`: ONE wording for every id position. It covers both
|
|
562
707
|
// halves of the defect it closes — a needlessly quoted legal id
|
|
563
708
|
// (`node "a"`, silently accepted before) and a quoted token that is not a
|
|
@@ -576,7 +721,7 @@ const DD_ID='"--" is not allowed inside an id — it is the link operator (edge
|
|
|
576
721
|
// old failure named the wrong thing (`node a Cache miss` reported
|
|
577
722
|
// `unexpected argument "miss"` — a surplus argument, when the defect was a
|
|
578
723
|
// missing quote).
|
|
579
|
-
const Q_WHY='whitespace also separates positionals, so a bare token cannot express a phrase (MIGRATIONS)';
|
|
724
|
+
const Q_WHY='whitespace also separates positionals, so a bare token cannot express a phrase (MIGRATIONS 0.1)';
|
|
580
725
|
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
726
|
// isId: the whole id test, used at every id position in the language.
|
|
582
727
|
const isId=v=>typeof v==='string'&&ID_RE.test(v)&&!v.includes('--');
|
|
@@ -764,9 +909,55 @@ function parseIndexRange(v){
|
|
|
764
909
|
const CORE_KW=['figdown','title','layout'];
|
|
765
910
|
const LAYOUT_KW=['pin']; // `LAYOUT-ZONE-NAMESPACE`, NORMATIVE — the whole namespace
|
|
766
911
|
const GENRE_FREE_KW=CORE_KW.concat(LAYOUT_KW);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
912
|
+
// `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`: the NODE and CONNECTOR spellings are per genre, so
|
|
913
|
+
// they are NOT in the shared list — every scene genre concats its own two.
|
|
914
|
+
//
|
|
915
|
+
// `SUBJECT-VOCABULARY-SCOPE`: SUBJECT VOCABULARY IS PER GENRE, AND THERE IS NO SHARED
|
|
916
|
+
// LIST OF IT. `SCENE_KW_TOP` and `SCENE_EXP_KW` are gone. They held the words
|
|
917
|
+
// that say what a figure is OF — `group`, `external`, `threshold`, `band`,
|
|
918
|
+
// `bundle`, `plane` — in one array concatenated into four genres, which is
|
|
919
|
+
// the same defect core §3's "scene keywords" sentence recorded: an
|
|
920
|
+
// INTERSECTION written down as if it were a namespace. Under `GENRE-VOCABULARY-OBLIGATION` a genre owns
|
|
921
|
+
// its words, so each scene genre now names its own subject vocabulary in its
|
|
922
|
+
// own array below. Two arrays agreeing is TWO DECLARATIONS that agree today,
|
|
923
|
+
// never one declaration inherited, and either may be withdrawn, renamed or
|
|
924
|
+
// constrained without touching the other.
|
|
925
|
+
//
|
|
926
|
+
// What stays shared, and why that is not a contradiction:
|
|
927
|
+
// - `class` is STYLING declaration and `flow`/`rank` are LAYOUT INTENT.
|
|
928
|
+
// None of the three describes a referent, so no genre's domain holds a
|
|
929
|
+
// competing meaning for them and no genre can independently earn or lose
|
|
930
|
+
// one. They are nearer `LAYOUT-ZONE-NAMESPACE`'s genre-independent layout namespace than `GENRE-VOCABULARY-OBLIGATION`.
|
|
931
|
+
// - `bitfield`/`table`/`timing`/`chart` are `GENRE-COMPOSITION` REGION OPENERS: composition,
|
|
932
|
+
// not subject vocabulary. The region's own namespace is the nested
|
|
933
|
+
// genre's.
|
|
934
|
+
const SCENE_STYLE_KW=['class','flow','rank'];
|
|
935
|
+
const SCENE_REGION_KW=['bitfield','table','timing','chart'];
|
|
936
|
+
const SCENE_HOST_KW=GENRE_FREE_KW.concat(SCENE_STYLE_KW, SCENE_REGION_KW);
|
|
937
|
+
// --- Each scene genre's OWN subject vocabulary. One array per genre. ---
|
|
938
|
+
// `block` (NORMATIVE): `group` and `external` are normative; `threshold` and
|
|
939
|
+
// `band` are EXPERIMENTAL and are the `GENRE-EARNING-THRESHOLD` INTERIM scalar-marker pair, held here
|
|
940
|
+
// deliberately unfrozen so the future scalar-marker genre can name them once
|
|
941
|
+
// WITH a scale. They are not renamed now — a rename would hand that genre a
|
|
942
|
+
// retired word.
|
|
943
|
+
const BLOCK_SUBJECT_KW=['group','external','threshold','band'];
|
|
944
|
+
// `topology` (EXPERIMENTAL): `bundle` is the one construct whose domain
|
|
945
|
+
// reading and drawn reading are the same reading — a LAG (IEEE 802.1AX), an
|
|
946
|
+
// ECMP set, an EVPN Ethernet Segment. `group`/`external` keep their block
|
|
947
|
+
// spellings because every networking synonym is more taken (`zone` DNS and
|
|
948
|
+
// firewall, `cluster` RFC 4456, `domain` RFC 7926, `area` OSPF, `site` EVPN)
|
|
949
|
+
// and their collisions are SOFT — the picture contradicts the wrong reading.
|
|
950
|
+
const TOPOLOGY_SUBJECT_KW=['group','external','bundle'];
|
|
951
|
+
// `flowchart` (EXPERIMENTAL): `external` only — the off-page terminus, ISO
|
|
952
|
+
// 5807 §9.4.2 *Terminator* being ISO's word for the concept and already this
|
|
953
|
+
// genre's live keyword, so the spelling stays. `group` had one occurrence in
|
|
954
|
+
// the whole tree and it was this genre's own reference figure.
|
|
955
|
+
const FLOWCHART_SUBJECT_KW=['external'];
|
|
956
|
+
// `statechart` (EXPERIMENTAL): NONE, and the empty array is the declaration.
|
|
957
|
+
// Three authored statechart figures, all transcribed from RFCs, reach for
|
|
958
|
+
// none of the six; `external` is additionally UML 2.5.1 §14's own
|
|
959
|
+
// `TransitionKind` literal and is reserved for it (`RESERVED-SPELLINGS`).
|
|
960
|
+
const STATECHART_SUBJECT_KW=[];
|
|
770
961
|
// `FLOWCHART-ROLE-KEYWORDS`: the flowchart ROLE vocabulary — the FIRST exercise of
|
|
771
962
|
// `GENRE-NAMESPACE` `GENRE-VOCABULARY-OBLIGATION` ("a genre owns its words"). These three are legal ONLY under
|
|
772
963
|
// `figdown 0.1 flowchart`; `GENRE-NAMESPACE`'s allowlist is what makes `decision x` a line
|
|
@@ -780,10 +971,163 @@ const FLOWCHART_ROLE_KW=['process','decision','terminator'];
|
|
|
780
971
|
// only its default drawing, and `shape=` on the same line overrides the
|
|
781
972
|
// drawing without touching the role (§12.7).
|
|
782
973
|
const ROLE_SHAPE={process:'box',decision:'diamond',terminator:'rounded'};
|
|
974
|
+
|
|
975
|
+
// `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`: PER-GENRE NODE AND CONNECTOR SPELLINGS.
|
|
976
|
+
// Each scene genre takes the word its own domain uses for the line between
|
|
977
|
+
// two things, and for the thing itself. The criterion is THE TERM THE DOMAIN
|
|
978
|
+
// ACTUALLY USES, not the standard's exact orthography (see the vocabulary
|
|
979
|
+
// rows: `flowline` carries the same ISO-clause verification debt
|
|
980
|
+
// `terminator` does, recorded rather than claimed away).
|
|
981
|
+
//
|
|
982
|
+
// block / topology node edge (DOT, unchanged)
|
|
983
|
+
// flowchart node flowline (ISO 5807)
|
|
984
|
+
// statechart state transition (OMG UML 2.5.1 §14)
|
|
985
|
+
//
|
|
986
|
+
// The ASYMMETRY is deliberate and is the whole of `GENRE-CONNECTOR-SPELLING` vs `GENRE-NODE-SPELLING`: `node`
|
|
987
|
+
// STAYS in `flowchart`, because there a stage can have a role THE SOURCE
|
|
988
|
+
// DOES NOT STATE, and `node` is the only spelling for that — forcing
|
|
989
|
+
// `process` would make the transcriber assert what the source never said.
|
|
990
|
+
// `statechart` has exactly ONE kind of node, so nothing can be left unstated
|
|
991
|
+
// and `state` loses nothing. `BARE-NODE-MEANING` CORRECTS the reason this
|
|
992
|
+
// comment used to give ("ISO has ~ten symbol kinds and we carry three"): a
|
|
993
|
+
// symbol this genre cannot spell is a COVERAGE GAP in FigDown, not a state
|
|
994
|
+
// of the figure, and `node` is not its spelling — see
|
|
995
|
+
// the project’s working record for the coverage ledger.
|
|
996
|
+
const GENRE_NODE_KW={block:'node',topology:'node',flowchart:'node',statechart:'state'};
|
|
997
|
+
const GENRE_CONNECTOR_KW={block:'edge',topology:'edge',flowchart:'flowline',statechart:'transition'};
|
|
998
|
+
const NODE_SPELLINGS=new Set(['node','state']);
|
|
999
|
+
const CONNECTOR_SPELLINGS=new Set(['edge','flowline','transition']);
|
|
1000
|
+
// `KEYWORD-RENAME-SCOPE`: the flowchart rename is GATED BY THE DECLARED LANGUAGE
|
|
1001
|
+
// VERSION, because `GENRE-CONNECTOR-SPELLING` applied it to `figdown 0.1` and that BROKE documents
|
|
1002
|
+
// legal at v0.1.8 — `figdown 0.1 flowchart` + `edge` stopped parsing, with
|
|
1003
|
+
// nothing recording the break as a decision. Under core §13.0 only X removes,
|
|
1004
|
+
// so 0.1 keeps its spelling:
|
|
1005
|
+
//
|
|
1006
|
+
// figdown 0.1 flowchart `edge` legal, `flowline` is a VERSION error
|
|
1007
|
+
// figdown 0.2 flowchart `flowline` legal, `edge` is the WRONG_WORD error
|
|
1008
|
+
//
|
|
1009
|
+
// Two spellings inside ONE version is what the no-alias rule (`IDENTITY-ASSERTION`)
|
|
1010
|
+
// forbids; two spellings across VERSIONS is ordinary language evolution, and
|
|
1011
|
+
// each version accepts exactly one. `statechart` needs no gate of its own —
|
|
1012
|
+
// the GENRE requires 0.2 (GENRES_BY_VERSION), so `state`/`transition` cannot
|
|
1013
|
+
// be reached from a 0.1 document at all.
|
|
1014
|
+
const GENRE_CONNECTOR_KW_AT={
|
|
1015
|
+
'0.1':{block:'edge',topology:'edge',flowchart:'edge'},
|
|
1016
|
+
'0.2':GENRE_CONNECTOR_KW
|
|
1017
|
+
};
|
|
1018
|
+
const connectorKwAt=(genre,ver)=>
|
|
1019
|
+
((GENRE_CONNECTOR_KW_AT[ver]||GENRE_CONNECTOR_KW)[genre])||undefined;
|
|
1020
|
+
// The version a connector spelling FIRST becomes legal in, so the diagnostic
|
|
1021
|
+
// can name it the way the genre gate already names `figdown 0.2`.
|
|
1022
|
+
const CONNECTOR_MIN_VERSION={flowline:'0.2',transition:'0.2'};
|
|
1023
|
+
const WRONG_VERSION_WORD=(surf,want,genre,need,have)=>
|
|
1024
|
+
'"'+surf+'" requires figdown '+need+' (this document declares '+have+'): '+
|
|
1025
|
+
'under figdown '+have+' genre '+genre+' spells this "'+want+'". The rename is '+
|
|
1026
|
+
'gated by the language version — a figdown '+have+' document keeps the spelling it '+
|
|
1027
|
+
'was written with (core §13.0: only a MAJOR version removes) — so raise the header '+
|
|
1028
|
+
'to figdown '+need+' or write "'+want+'" (MIGRATIONS 0.2)';
|
|
1029
|
+
// Why each genre's word is its word — quoted in the diagnostic, because
|
|
1030
|
+
// "not allowed in genre X" tells an author nothing about what to write.
|
|
1031
|
+
const WORD_WHY={
|
|
1032
|
+
edge:'a block or topology figure is a graph, and `edge` is the graph word (DOT)',
|
|
1033
|
+
flowline:'the connecting line in a flowchart is a FLOWLINE — the term the flowchart domain commonly uses for the symbol ISO 5807 §9.3.1 names "Line"',
|
|
1034
|
+
transition:'the connecting line in a statechart is a TRANSITION — the term UML 2.5.1 §14 uses for it',
|
|
1035
|
+
node:'this genre has more kinds of thing than it has words for, so `node` is the general one',
|
|
1036
|
+
state:'a statechart has exactly ONE kind of node and it is a STATE (UML 2.5.1 §14)'
|
|
1037
|
+
};
|
|
1038
|
+
// The named diagnostic `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING` owe: it says WHICH word this genre uses and
|
|
1039
|
+
// WHY, and it names the migration, because every connector line in a
|
|
1040
|
+
// reclassified document has to be rewritten (the cost `GENRE-CONNECTOR-SPELLING` accepted).
|
|
1041
|
+
const WRONG_WORD=(surf,want,genre)=>
|
|
1042
|
+
'"'+surf+'" is not the word genre '+genre+' uses for this — write "'+want+'": '+WORD_WHY[want]+
|
|
1043
|
+
'. 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)';
|
|
1044
|
+
// `SCENE-KEYWORD-MEMBERSHIP`: a word WITHDRAWN FROM ONE GENRE is not an unknown word,
|
|
1045
|
+
// and `"threshold" is not allowed in genre topology` would send an author
|
|
1046
|
+
// looking for a typo. Each cell below was legal until this release and states
|
|
1047
|
+
// WHY that genre no longer declares it — the ruling's own ground, per cell,
|
|
1048
|
+
// because the grounds differ and a single sentence could not carry them.
|
|
1049
|
+
// Every one of these withdrawals was FREE: `topology`, `flowchart` and
|
|
1050
|
+
// `statechart` are EXPERIMENTAL genres outside the compatibility promise, and
|
|
1051
|
+
// in `block` the two withdrawn words were EXPERIMENTAL keywords (`EDGE-GEOMETRY-CONSTRUCTS` precedent:
|
|
1052
|
+
// experimental withdrawal, no gate, no rewrite owed).
|
|
1053
|
+
const WITHDREW_AT=' (withdrawn, `SCENE-KEYWORD-MEMBERSHIP`; MIGRATIONS 0.3)';
|
|
1054
|
+
const GENRE_WITHDRAWN={
|
|
1055
|
+
block:{
|
|
1056
|
+
bundle:'`bundle` is now declared by `topology` only. It had ZERO authored uses under `block` — every authored link bundle in the corpus is a topology document — and the construct is defined by a REFERENT that only that genre has: a LAG (IEEE 802.1AX), an ECMP set, an EVPN Ethernet Segment. Under `block` it was a ring around parallel edges with nothing to name.'
|
|
1057
|
+
},
|
|
1058
|
+
topology:{
|
|
1059
|
+
threshold:'`threshold` is now declared by `block` only. It had ZERO occurrences under `topology` in the whole corpus, and in this genre\'s domain a threshold is a QUEUE DEPTH WITH A NUMERIC VALUE (RFC 2309 minth/maxth, RFC 7567) — while FigDown\'s takes no value= and its offset= is a fraction of the target\'s rendered extent, not a quantity. Author the figure as `block`, where the `GENRE-EARNING-THRESHOLD` scalar-marker evidence lives.',
|
|
1060
|
+
band:'`band` is now declared by `block` only. Its two occurrences under `topology` were both conformance fixtures, never a figure anyone needed, and in this genre a BAND is a frequency band — radio, wireless, microwave, optical transport — which is exactly the kind of figure a topology document draws. Author the figure as `block`.'
|
|
1061
|
+
},
|
|
1062
|
+
flowchart:{
|
|
1063
|
+
group:'`flowchart` no longer declares `group`. Its one occurrence in the corpus was this genre\'s own reference figure, which exists to demonstrate every form of every keyword — so citing it as evidence of need is circular. There were no authored uses.',
|
|
1064
|
+
threshold:'`flowchart` no longer declares `threshold`. Zero occurrences, and the construct does not apply: a threshold is a labelled reference value drawn at a percentage of the target\'s RENDERED EXTENT, and a process box\'s extent is an artifact of its label length, so the line asserts nothing a reader can read.',
|
|
1065
|
+
band:'`flowchart` no longer declares `band`. Zero occurrences, and a band is a RANGE over that same meaningless extent.',
|
|
1066
|
+
bundle:'`flowchart` no longer declares `bundle`. Zero occurrences, and parallel flowlines between the same two stages are different CONDITIONS; drawing a ring round them hides what the figure is for.'
|
|
1067
|
+
},
|
|
1068
|
+
statechart:{
|
|
1069
|
+
group:'`statechart` declares NO subject vocabulary at all. UML\'s grouping construct is the COMPOSITE STATE and its REGIONS, and under the single-source-vocabulary rule a statechart that needed grouping should take UML 2.5.1 §14\'s word for it — declared in this genre\'s own document — rather than inherit another genre\'s.',
|
|
1070
|
+
external:'`statechart` declares NO subject vocabulary at all, and `external` is additionally RESERVED here: UML 2.5.1 §14 defines TransitionKind as `external | internal | local`, so in this genre\'s own source standard "external" already names A TRANSITION THAT EXITS AND RE-ENTERS ITS SOURCE STATE. FigDown\'s `external` means an endpoint outside the figure that is never drawn — same word, same genre, same standard, unrelated meanings.',
|
|
1071
|
+
threshold:'`statechart` declares NO subject vocabulary at all. A state has no extent that means anything — its box is sized by its label — so a reference value drawn 60% down it asserts nothing.',
|
|
1072
|
+
band:'`statechart` declares NO subject vocabulary at all, and a band is a range over that same meaningless extent.',
|
|
1073
|
+
bundle:'`statechart` declares NO subject vocabulary at all, and here `bundle` is an ANTI-FEATURE: two transitions between the same pair of states are two different TRIGGERS, and the trigger is the whole content of the arc. Bundling them draws away exactly what the figure is for.'
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
const WITHDRAWN_FROM_GENRE=(kw,genre)=>
|
|
1077
|
+
'"'+kw+'" is not allowed in genre '+genre+' — it was WITHDRAWN from this genre, not misspelled: '+
|
|
1078
|
+
GENRE_WITHDRAWN[genre][kw]+
|
|
1079
|
+
' Subject vocabulary is per genre (core §3, `GENRE-VOCABULARY-OBLIGATION`): a spelling accepted by several genres is several '+
|
|
1080
|
+
'independent declarations, and this genre\'s was withdrawn without touching any other\'s.'+WITHDREW_AT;
|
|
1081
|
+
// `MEMBERSHIP-KEY-ACCEPTANCE`: THE OPTION-KEY HALF OF `SCENE-KEYWORD-MEMBERSHIP`. A per-genre withdrawal can
|
|
1082
|
+
// strand an option KEY as easily as it strands a keyword: `in=` states
|
|
1083
|
+
// membership and its ONLY value domain is the id of a containing `group`, so
|
|
1084
|
+
// once `SCENE-KEYWORD-MEMBERSHIP` stopped `flowchart` and `statechart` from declaring a `group` the
|
|
1085
|
+
// key stayed accepted with nothing it could name. The measured symptom was
|
|
1086
|
+
// `process a "A" in=g` answering `unknown group "g"` with NO spelling that
|
|
1087
|
+
// succeeds — a dangling reference every author reaches by writing the key at
|
|
1088
|
+
// all. The other acceptors of `in=` in the language are `threshold` and
|
|
1089
|
+
// `band`, whose domain `MARKER-TARGET-KINDS` widened to REGION ids; neither is a keyword of
|
|
1090
|
+
// either genre since `SCENE-KEYWORD-MEMBERSHIP`, and the widening never reached `node`, so a
|
|
1091
|
+
// `flowchart` document that declares `table q` still answers `unknown group
|
|
1092
|
+
// "q"` for `node a "A" in=q`. Nothing in either genre was left un-stranded,
|
|
1093
|
+
// which is why the withdrawal is by KEY here and not directive by directive.
|
|
1094
|
+
//
|
|
1095
|
+
// The grounds differ per genre and are stated per cell, as `SCENE-KEYWORD-MEMBERSHIP`'s are:
|
|
1096
|
+
// `flowchart`'s is that every value is a dead end, `statechart`'s is that the
|
|
1097
|
+
// spelling is RESERVED for a different domain. Both genres are EXPERIMENTAL,
|
|
1098
|
+
// so both withdrawals are free — the `EDGE-GEOMETRY-CONSTRUCTS` precedent, no gate and no rewrite
|
|
1099
|
+
// owed — which is also what makes re-adding `in=` to `statechart` later with
|
|
1100
|
+
// a `state`-id domain cost nothing.
|
|
1101
|
+
const WITHDREW_OPT_AT=' (withdrawn, `MEMBERSHIP-KEY-ACCEPTANCE`; MIGRATIONS 0.3)';
|
|
1102
|
+
const GENRE_WITHDRAWN_OPT={
|
|
1103
|
+
flowchart:{
|
|
1104
|
+
in:'`flowchart` no longer accepts `in=`. Its only value domain was the id of a containing `group`, and this genre has not declared `group` (`SCENE-KEYWORD-MEMBERSHIP`) — so EVERY value was a dead end: `in=x` answered `unknown group "x"` and no spelling succeeded. An unknown-option error that names the reason beats a dangling reference no author can satisfy. What expresses membership TODAY is `class=`: declare `class ingress "Ingress phase"` and write `class=ingress` on each stage — it earns a legend entry and applies to every member at once. Containment in a flowchart is an OPEN question and the construct the need is waiting on is a swimlane, not a box (spec/genres/experimental/flowchart.md, What is excluded).'
|
|
1105
|
+
},
|
|
1106
|
+
statechart:{
|
|
1107
|
+
in:'`statechart` no longer accepts `in=`, and the spelling is RESERVED rather than merely dropped. This genre declares NO subject vocabulary (`SUBJECT-VOCABULARY-SCOPE`), so the `group` id that was `in=`\'s only value domain cannot exist here and every value was a dead end. The reason for withdrawing rather than leaving it is the RESERVATION: `in=`\'s future domain in this genre is a STATE id — UML 2.5.1 §14.2.3.4 composite states would arrive as nesting on `state`, through this exact key — so a live key with a group-id domain taught the WRONG model using the very spelling reserved for the right one. Delete the key: a category shared by several states is a `class=` meaning. `in=` is expected back with a state-id domain when composite-state nesting is earned (`MEMBERSHIP-KEY-ACCEPTANCE`).'
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
const WITHDRAWN_OPT_FROM_GENRE=(key,genre)=>
|
|
1111
|
+
key+'= is not allowed in genre '+genre+' — it was WITHDRAWN from this genre, not misspelled: '+
|
|
1112
|
+
GENRE_WITHDRAWN_OPT[genre][key]+
|
|
1113
|
+
' An option key is per genre for the same reason a keyword is (core §3, `GENRE-VOCABULARY-OBLIGATION`): the key is accepted '+
|
|
1114
|
+
'by the directive AND by the genre, and this genre\'s acceptance was withdrawn without touching any other\'s.'+
|
|
1115
|
+
WITHDREW_OPT_AT;
|
|
783
1116
|
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)),
|
|
1117
|
+
block:new Set(SCENE_HOST_KW.concat(BLOCK_SUBJECT_KW, ['node','edge'])),
|
|
1118
|
+
topology:new Set(SCENE_HOST_KW.concat(TOPOLOGY_SUBJECT_KW, ['node','edge'])),
|
|
1119
|
+
flowchart:new Set(SCENE_HOST_KW.concat(FLOWCHART_SUBJECT_KW, ['node','flowline'], FLOWCHART_ROLE_KW)),
|
|
1120
|
+
// `STATECHART-GENRE-SCOPE`: `statechart` added no keyword of its own — it was the
|
|
1121
|
+
// scene host set and nothing else. `GENRE-NODE-SPELLING` gives it its two: the
|
|
1122
|
+
// scene host set with `state` and `transition` in the slots `node` and
|
|
1123
|
+
// `edge` occupy elsewhere. It still does NOT inherit `process`/`decision`/
|
|
1124
|
+
// `terminator`: those are flowchart's words (`GENRE-NAMESPACE` `GENRE-VOCABULARY-OBLIGATION`), and a `decision` in a
|
|
1125
|
+
// statechart is a category error, not a shorthand. The allowlist is what
|
|
1126
|
+
// makes that a line error with no extra code.
|
|
1127
|
+
// `SUBJECT-VOCABULARY-SCOPE`: its subject vocabulary is the empty array above, so
|
|
1128
|
+
// `state` + `transition` + core + layout + styling + region openers is now
|
|
1129
|
+
// the WHOLE of what a statechart document may write at top level.
|
|
1130
|
+
statechart:new Set(SCENE_HOST_KW.concat(STATECHART_SUBJECT_KW, ['state','transition'])),
|
|
787
1131
|
bitfield:new Set(GENRE_FREE_KW.concat(['class','bitfield'])),
|
|
788
1132
|
// chart is experimental and attaches to a table id in the same document
|
|
789
1133
|
table:new Set(GENRE_FREE_KW.concat(['class','table','chart'])),
|
|
@@ -846,7 +1190,10 @@ function parseOne(text){
|
|
|
846
1190
|
// `EMPTY-LABEL-STATE`: `title` and a plane label start ABSENT (null), never as an empty
|
|
847
1191
|
// string — an author who writes `title ""` has made a distinction the model
|
|
848
1192
|
// must keep, and the implicit `base` plane wrote no label at all.
|
|
849
|
-
|
|
1193
|
+
// `DRAWN-ANNOTATION-FORM`: `note` sits beside `title` and starts ABSENT (null), on
|
|
1194
|
+
// `EMPTY-LABEL-STATE`'s rule for `title` itself — an author who writes `note=""` has made
|
|
1195
|
+
// a distinction the model must keep.
|
|
1196
|
+
const doc={title:null,note:null,nodes:[],groups:[],edges:[],planes:[{id:'base',label:null,z:0}],
|
|
850
1197
|
flow:'right',ranks:[],pins:{},blocks:[],trunks:[],thresholds:[],bands:[],
|
|
851
1198
|
classes:[],boundaries:[]};
|
|
852
1199
|
const nodeIds=new Set(), groupIds=new Set(), planeIds=new Set(['base']), classIds=new Set(),
|
|
@@ -886,8 +1233,13 @@ function parseOne(text){
|
|
|
886
1233
|
// operator into halves: -[x]- -[x]-> <-[x]- <-[x]->. Bracket content:
|
|
887
1234
|
// balanced brackets nest verbatim ([flags[3:0]] just works); ["..."] takes
|
|
888
1235
|
// the standard quoted-string escapes for unbalanced brackets / \n.
|
|
889
|
-
|
|
890
|
-
|
|
1236
|
+
// `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`: `kw` is the SURFACE spelling the author wrote —
|
|
1237
|
+
// `edge`, `flowline` or `transition`. One scanner, three words: every
|
|
1238
|
+
// message names the word on the line, and nothing downstream of here knows
|
|
1239
|
+
// the difference (the model records a connector, not a spelling).
|
|
1240
|
+
function parseEdgeLine(s,n,kw){
|
|
1241
|
+
kw=kw||'edge';
|
|
1242
|
+
let i=kw.length; // past the connector keyword
|
|
891
1243
|
const ws=()=>{ while(i<s.length&&/\s/.test(s[i])) i++; };
|
|
892
1244
|
// `LINK-OPERATOR-IN-IDS`: a hyphen is an id character only when it is NOT
|
|
893
1245
|
// followed by a second one, because `--` is the link operator. This is
|
|
@@ -935,7 +1287,7 @@ function parseOne(text){
|
|
|
935
1287
|
ws();
|
|
936
1288
|
if(s[i]==='"'){ err(n,ID_RULE); return; }
|
|
937
1289
|
const a=readId();
|
|
938
|
-
if(!a){ err(n, idHere()?ID_RULE:'
|
|
1290
|
+
if(!a){ err(n, idHere()?ID_RULE:kw+' needs <id> ->|<-|--|<-> <id>'); return; }
|
|
939
1291
|
if(idHere()){ err(n,ID_RULE); return; }
|
|
940
1292
|
ws(); let tail=null;
|
|
941
1293
|
if(s[i]==='['){ const r=readLbl(); if(r.error){ err(n,r.error); return; } tail=r.v; }
|
|
@@ -943,7 +1295,7 @@ function parseOne(text){
|
|
|
943
1295
|
let lh=null;
|
|
944
1296
|
if(s.startsWith('<-',i)){ lh='<-'; i+=2; }
|
|
945
1297
|
else if(s[i]==='-'){ lh='-'; i++; }
|
|
946
|
-
else { err(n,'
|
|
1298
|
+
else { err(n,kw+' needs an operator: -> <- -- <-> (a [mid] label splits it: -[x]->)'); return; }
|
|
947
1299
|
let mid=null, op=null;
|
|
948
1300
|
if(s[i]==='['){
|
|
949
1301
|
const r=readLbl(); if(r.error){ err(n,r.error); return; } mid=r.v;
|
|
@@ -955,14 +1307,14 @@ function parseOne(text){
|
|
|
955
1307
|
} else {
|
|
956
1308
|
if(s[i]==='-'){ op='--'; i++; }
|
|
957
1309
|
else if(s[i]==='>'){ op='->'; i++; }
|
|
958
|
-
else { err(n,'
|
|
1310
|
+
else { err(n,kw+' needs an operator: -> <- -- <->'); return; }
|
|
959
1311
|
}
|
|
960
1312
|
ws(); let head=null;
|
|
961
1313
|
if(s[i]==='['){ const r=readLbl(); if(r.error){ err(n,r.error); return; } head=r.v; }
|
|
962
1314
|
ws();
|
|
963
1315
|
if(s[i]==='"'){ err(n,ID_RULE); return; }
|
|
964
1316
|
const b=readId();
|
|
965
|
-
if(!b){ err(n, idHere()?ID_RULE:'
|
|
1317
|
+
if(!b){ err(n, idHere()?ID_RULE:kw+' needs a target id after the operator'); return; }
|
|
966
1318
|
if(idHere()){ err(n,ID_RULE); return; }
|
|
967
1319
|
const tk2=tokenize(s.slice(i).trim());
|
|
968
1320
|
if(tk2.error){ err(n,tk2.error); return; }
|
|
@@ -976,10 +1328,10 @@ function parseOne(text){
|
|
|
976
1328
|
for(const rk in RETIRED_OPT_KEYS)
|
|
977
1329
|
if(o2[rk]!==undefined){ err(n,RETIRED_OPT_KEYS[rk]); return; }
|
|
978
1330
|
for(const k in o2)
|
|
979
|
-
if(!DIRECTIVE_OPTS.
|
|
1331
|
+
if(!DIRECTIVE_OPTS[kw].includes(k)){ err(n,kw+' does not take '+k+'='); return; }
|
|
980
1332
|
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(
|
|
1333
|
+
if(o2[k]!==undefined){ err(n,k+'= is retired — write the label inline: '+kw+' A [tail] -[mid]-> [head] B (MIGRATIONS 0.1)'); return; }
|
|
1334
|
+
if(o2.fill!==undefined){ err(n,FILL_NO_INTERIOR(kw)); return; }
|
|
983
1335
|
for(const k of ['fill','stroke'])
|
|
984
1336
|
if(o2[k]!==undefined && !isColor(o2[k])){ err(n,'unknown color "'+o2[k]+'" (#hex or CSS color name)'); return; }
|
|
985
1337
|
// `RULE-POSITION-ENUMERATION`: `edge` was the ONE id position in the language that
|
|
@@ -994,6 +1346,16 @@ function parseOne(text){
|
|
|
994
1346
|
const e=idErr(o2.plane, optHasQ(oT2,'plane'), null);
|
|
995
1347
|
if(e){ err(n,e); return; }
|
|
996
1348
|
}
|
|
1349
|
+
// `DRAWN-ANNOTATION-FORM`: the connector's copy of the `note=` version gate and
|
|
1350
|
+
// of the `QUOTING-RULES` quoted-prose rule. A connector is the acceptor the ruling
|
|
1351
|
+
// called decisive — an edge has no id, so an attribute is the ONLY form
|
|
1352
|
+
// that can reach it — and this scanner has to carry every language-wide
|
|
1353
|
+
// check itself or the one construct that most needs the key is the one
|
|
1354
|
+
// construct where the key is unchecked.
|
|
1355
|
+
if(o2.note!==undefined){
|
|
1356
|
+
if(belowOptVersion('note',doc.version)){ err(n,NOTE_VERSION(doc.version)); return; }
|
|
1357
|
+
if(!optQ(oT2,'note')){ err(n,'note= must be quoted: note="'+o2.note+'" — '+Q_WHY); return; }
|
|
1358
|
+
}
|
|
997
1359
|
// `RULE-POSITION-ENUMERATION`: and the enum half of RULE 2.4, for the one enum key `edge` takes.
|
|
998
1360
|
// Checked before the value, exactly as `badOpts` does it.
|
|
999
1361
|
if(o2.style!==undefined && optHasQ(oT2,'style')){ err(n,ENUM_BARE('style='+o2.style)); return; }
|
|
@@ -1008,7 +1370,7 @@ function parseOne(text){
|
|
|
1008
1370
|
// and `fill=` name the same channel (`stroke=` wins when both are
|
|
1009
1371
|
// written); `text=` colours the [tail]/[mid]/[head] labels.
|
|
1010
1372
|
doc.edges.push({a,b,op,tail,mid,head,style:o2.style,cls:ecls,
|
|
1011
|
-
stroke:o2.stroke,
|
|
1373
|
+
stroke:o2.stroke,note:o2.note,
|
|
1012
1374
|
plane:o2.plane||'base',line:n});
|
|
1013
1375
|
}
|
|
1014
1376
|
|
|
@@ -1081,17 +1443,41 @@ function parseOne(text){
|
|
|
1081
1443
|
if(findReservedSemi(raw)>=0){ err(n,RESERVED_SEMI); continue; }
|
|
1082
1444
|
// edge lines carry inline [labels] with free text — dedicated scanner,
|
|
1083
1445
|
// not the generic tokenizer
|
|
1084
|
-
|
|
1446
|
+
// `GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`: all THREE connector spellings are scanned here.
|
|
1447
|
+
// The wrong one for the genre must reach the named diagnostic below, so
|
|
1448
|
+
// the dispatch cannot be narrowed to the genre's own word — a `flowline`
|
|
1449
|
+
// under `block` would then fall through to `unrecognized line`, which is
|
|
1450
|
+
// exactly the answer these rulings owe an author better than.
|
|
1451
|
+
const mConn=/^(edge|flowline|transition)(\s|$)/.exec(raw.trim());
|
|
1452
|
+
if(mConn){
|
|
1453
|
+
const ckw=mConn[1];
|
|
1085
1454
|
if(firstContent){ firstContent=false; err(n,'first line must be "figdown 0.1 <genre>"'); }
|
|
1086
1455
|
cur=null;
|
|
1087
|
-
// `CONTENT-LAYOUT-ZONE-SPLIT`:
|
|
1456
|
+
// `CONTENT-LAYOUT-ZONE-SPLIT`: a connector is a semantic directive, and it is dispatched here —
|
|
1088
1457
|
// before the generic layout-zone gate below — so it needs its own copy
|
|
1089
1458
|
// 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
|
-
|
|
1459
|
+
if(sawLayout){ err(n,'"'+ckw+'" is a semantic directive — it must appear before the layout zone (`CONTENT-LAYOUT-ZONE-SPLIT`)'); continue; }
|
|
1460
|
+
// `GENRE-KEYWORD-ALLOWLIST`: a connector is scene vocabulary only — and in a scene genre it is
|
|
1461
|
+
// THAT genre's connector word (`GENRE-CONNECTOR-SPELLING`/`GENRE-NODE-SPELLING`), not any of the three.
|
|
1462
|
+
// `KEYWORD-RENAME-SCOPE`: the word this genre uses is read AT THE DECLARED VERSION, not
|
|
1463
|
+
// at the newest one. A genre with no connector at all (bitfield, table,
|
|
1464
|
+
// timing) still gets the allowlist message.
|
|
1465
|
+
if(sawHeader && doc.genre && GENRE_KW[doc.genre]){
|
|
1466
|
+
const want=connectorKwAt(doc.genre, doc.version);
|
|
1467
|
+
if(!want){ err(n,'"'+ckw+'" is not allowed in genre '+doc.genre); continue; }
|
|
1468
|
+
if(ckw!==want){
|
|
1469
|
+
const need=CONNECTOR_MIN_VERSION[ckw];
|
|
1470
|
+
// The author wrote a word this genre really does use — just not in
|
|
1471
|
+
// the version they declared. That is a different mistake from the
|
|
1472
|
+
// wrong domain's word, and it has a different one-step fix.
|
|
1473
|
+
if(need && doc.version && need>doc.version &&
|
|
1474
|
+
GENRE_CONNECTOR_KW[doc.genre]===ckw)
|
|
1475
|
+
err(n, WRONG_VERSION_WORD(ckw,want,doc.genre,need,doc.version));
|
|
1476
|
+
else
|
|
1477
|
+
err(n, WRONG_WORD(ckw,want,doc.genre));
|
|
1478
|
+
continue; }
|
|
1479
|
+
}
|
|
1480
|
+
parseEdgeLine(raw.trim(),n,ckw);
|
|
1095
1481
|
continue;
|
|
1096
1482
|
}
|
|
1097
1483
|
const tk=tokenize(raw.trim());
|
|
@@ -1110,6 +1496,19 @@ function parseOne(text){
|
|
|
1110
1496
|
// same-line repeated option key (last-wins was silent data loss)
|
|
1111
1497
|
if(dup){ err(n,'duplicate option "'+dup+'=" on one line'); bad=true; }
|
|
1112
1498
|
for(const u of unk){ err(n,'unknown option "'+u+'="'); bad=true; }
|
|
1499
|
+
// `MEMBERSHIP-KEY-ACCEPTANCE`: the PER-GENRE option-key withdrawal, checked here —
|
|
1500
|
+
// after `unknown option`, so a key the LANGUAGE does not have keeps its
|
|
1501
|
+
// own answer, and before every value check, so a withdrawn key is never
|
|
1502
|
+
// told what its value would have meant. `gwHit` suppresses the id-value
|
|
1503
|
+
// rule below for the same key: ONE token, ONE error, the convention
|
|
1504
|
+
// `enumQ` already follows. The line is abandoned by the caller
|
|
1505
|
+
// (`if(badOpts(kw)) continue;`), so no cascade reaches the resolver and
|
|
1506
|
+
// the author never sees the `unknown group "…"` this ruling removes.
|
|
1507
|
+
const gwOpt=(doc.genre&&GENRE_WITHDRAWN_OPT[doc.genre])||null;
|
|
1508
|
+
const gwHit=new Set();
|
|
1509
|
+
if(gwOpt) for(const o in opts)
|
|
1510
|
+
if(gwOpt[o]!==undefined && allowed.includes(o)){
|
|
1511
|
+
err(n,WITHDRAWN_OPT_FROM_GENRE(o,doc.genre)); gwHit.add(o); bad=true; }
|
|
1113
1512
|
// Retired spelling: `color=` → `fill=`. Fires only where
|
|
1114
1513
|
// the key was accepted; on a directive that never took it the existing
|
|
1115
1514
|
// `<directive> does not take color=` is still the right answer.
|
|
@@ -1119,6 +1518,19 @@ function parseOne(text){
|
|
|
1119
1518
|
// spelling left the language rather than moving between directives.
|
|
1120
1519
|
for(const rk in RETIRED_OPT_KEYS)
|
|
1121
1520
|
if(opts[rk]!==undefined){ err(n,RETIRED_OPT_KEYS[rk]); bad=true; }
|
|
1521
|
+
// `DRAWN-ANNOTATION-FORM`: the two `note=` refusals, in the order that gives
|
|
1522
|
+
// ONE error per line. `field` is checked first and unconditionally,
|
|
1523
|
+
// because it refuses the key at every version — telling a `figdown 0.2`
|
|
1524
|
+
// bitfield author to raise their header would send them to a version
|
|
1525
|
+
// that still refuses them. Every directive that does NOT list `note` in
|
|
1526
|
+
// its row falls through to the generic `<directive> does not take note=`
|
|
1527
|
+
// below, which is the right answer for `external`, `threshold`, `band`,
|
|
1528
|
+
// `bundle`, `plane`, `class` and `cell`: the key is in OPT_KEYS, so none
|
|
1529
|
+
// of them can report `unknown option` for a spelling the language has.
|
|
1530
|
+
if(opts.note!==undefined && allowed.includes('note')){
|
|
1531
|
+
if(k==='field'){ err(n,NOTE_ON_FIELD); bad=true; }
|
|
1532
|
+
else if(belowOptVersion('note',doc.version)){ err(n,NOTE_VERSION(doc.version)); bad=true; }
|
|
1533
|
+
}
|
|
1122
1534
|
// `RULE-POSITION-ENUMERATION`: RULE 2.4's enum half on the OPTION keys. One loop
|
|
1123
1535
|
// for every enum-valued key, the same device the id-valued keys below
|
|
1124
1536
|
// use — so a key that gains an enum grammar later is covered by
|
|
@@ -1167,7 +1579,11 @@ function parseOne(text){
|
|
|
1167
1579
|
// until then) and `present=` (`PRESENCE-CONDITION-EXPRESSION`). `present=""` is legal and is the
|
|
1168
1580
|
// "conditional, condition not stated" form — an EMPTY quoted value,
|
|
1169
1581
|
// not an unquoted one, so the same rule admits it.
|
|
1170
|
-
|
|
1582
|
+
// `DRAWN-ANNOTATION-FORM`: THREE since `note=` revived. It is prose in the
|
|
1583
|
+
// same sense, and it revives with the value shape it retired with —
|
|
1584
|
+
// which is RULE 4.9 obligation 2 satisfied in the parser rather than
|
|
1585
|
+
// only on paper.
|
|
1586
|
+
for(const sk of ['description','present','note'])
|
|
1171
1587
|
if(opts[sk]!==undefined && allowed.includes(sk) && !optQ(optT,sk)){
|
|
1172
1588
|
err(n,sk+'= must be quoted: '+sk+'="'+opts[sk]+'" — '+Q_WHY); bad=true; }
|
|
1173
1589
|
// `QUOTED-IDS`: `in=` and `plane=` are ID-VALUED options, so the
|
|
@@ -1175,7 +1591,7 @@ function parseOne(text){
|
|
|
1175
1591
|
// keeps its directive-specific message (`threshold needs in=…`); a written
|
|
1176
1592
|
// one that is quoted or not a legal id gets the ID RULE.
|
|
1177
1593
|
for(const k of ['in','plane'])
|
|
1178
|
-
if(opts[k]!==undefined && allowed.includes(k)){
|
|
1594
|
+
if(opts[k]!==undefined && allowed.includes(k) && !gwHit.has(k)){
|
|
1179
1595
|
const e=idErr(opts[k], optHasQ(optT,k), null);
|
|
1180
1596
|
if(e){ err(n,e); bad=true; }
|
|
1181
1597
|
}
|
|
@@ -1203,25 +1619,53 @@ function parseOne(text){
|
|
|
1203
1619
|
// line. So the quoting check runs first and suppresses the value
|
|
1204
1620
|
// check for that token, exactly as `badOpts` does for `shape=`.
|
|
1205
1621
|
const hq1=!!posq[1], hq2=!!posq[2];
|
|
1622
|
+
// `STATECHART-GENRE-SCOPE`: the language version is now a SET, not a constant, so every
|
|
1623
|
+
// message that echoes a corrected header echoes the version the
|
|
1624
|
+
// author actually declared — an author on `0.2` must not be shown a
|
|
1625
|
+
// `0.1` example. An unrecognised version falls back to `0.1` for the
|
|
1626
|
+
// purpose of these examples only; it has already errored on its own.
|
|
1627
|
+
const verOK=!hq1 && LANG_VERSIONS.includes(pos[1]);
|
|
1628
|
+
const ver=verOK?pos[1]:'0.1';
|
|
1206
1629
|
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'];
|
|
1630
|
+
else if(hq2) err(n,ENUM_BARE('figdown '+ver+' '+pos[2]));
|
|
1631
|
+
if(!hq1 && !LANG_VERSIONS.includes(pos[1])) err(n,'unsupported version "'+(pos[1]||'')+'" (expected '+LANG_VERSIONS.join(' or ')+')');
|
|
1210
1632
|
// 0.1: the genre token is REQUIRED. `bitfield`/`table`/`timing`
|
|
1211
1633
|
// documents declare their kind in their content, but `block`,
|
|
1212
1634
|
// `topology` and `flowchart` share the SAME vocabulary
|
|
1213
1635
|
// (node/edge/group) and differ only in default flow — so the header
|
|
1214
1636
|
// is the ONLY place such a document states which kind of figure it
|
|
1215
1637
|
// is, and omitting it destroys the distinction with no recoverable
|
|
1216
|
-
// fallback. The message lists the
|
|
1217
|
-
// agent fixes it in one step
|
|
1218
|
-
|
|
1638
|
+
// fallback. The message lists the legal values so an authoring
|
|
1639
|
+
// agent fixes it in one step — the values legal AT THE DECLARED
|
|
1640
|
+
// VERSION, since listing `statechart` to a `figdown 0.1` author would
|
|
1641
|
+
// name a genre that document may not use.
|
|
1642
|
+
const GENRES=GENRES_BY_VERSION[ver];
|
|
1643
|
+
const GLIST='('+GENRES.join('|')+')';
|
|
1644
|
+
if(pos[2]===undefined) err(n,'figdown header requires a genre '+GLIST);
|
|
1219
1645
|
else if(hq2){ /* the quoting error above is this token's one error */ }
|
|
1220
1646
|
else if(pos[2]==='wave') err(n,RETIRED_WAVE);
|
|
1221
|
-
else if(!GENRES.includes(pos[2]))
|
|
1647
|
+
else if(!GENRES.includes(pos[2])){
|
|
1648
|
+
// A genre that exists but only LATER is its own diagnostic, not
|
|
1649
|
+
// `unknown genre`. core §13.7 forbids guessing a version, and the
|
|
1650
|
+
// author's one-step fix is to raise the header — which the message
|
|
1651
|
+
// SPELLS OUT, because "unknown genre statechart" would send them
|
|
1652
|
+
// looking for a typo that is not there. The trailing `— write: …`
|
|
1653
|
+
// is the same shape WRONG_VERSION_WORD ends with: name the fix, do
|
|
1654
|
+
// not leave the author to derive it.
|
|
1655
|
+
const later=Object.keys(GENRES_BY_VERSION).sort()
|
|
1656
|
+
.find(v=>v>ver && GENRES_BY_VERSION[v].includes(pos[2]));
|
|
1657
|
+
if(later) err(n,'genre "'+pos[2]+'" requires figdown '+later+' (this document declares '+ver+') — write: figdown '+later+' '+pos[2]);
|
|
1658
|
+
else err(n,'unknown genre "'+pos[2]+'" '+GLIST);
|
|
1659
|
+
}
|
|
1222
1660
|
else{ doc.genre=pos[2];
|
|
1661
|
+
// The DECLARED language version is part of the model (core §12.5
|
|
1662
|
+
// `header.version`): a reader must be able to tell what contract
|
|
1663
|
+
// the author wrote against without re-reading the source.
|
|
1664
|
+
doc.version=ver;
|
|
1223
1665
|
// genre defaults (`GENRE-NAMESPACE`/`DEFAULT-VALUE-SELECTION`): flowchart figures flow down —
|
|
1224
|
-
// the census-dominant direction; an explicit flow
|
|
1666
|
+
// the census-dominant direction; an explicit `flow` directive overrides.
|
|
1667
|
+
// `statechart` takes the SCENE default (`right`) and adds no rule
|
|
1668
|
+
// of its own — `STATECHART-GENRE-SCOPE` lands a dispatch point, not a layout.
|
|
1225
1669
|
if(pos[2]==='flowchart') doc.flow='down'; }
|
|
1226
1670
|
// §1: a directive line carrying positional arguments its grammar does
|
|
1227
1671
|
// not accept MUST be rejected — the header is a directive like any other
|
|
@@ -1256,7 +1700,7 @@ function parseOne(text){
|
|
|
1256
1700
|
// noun in a topology figure (`SHAPE-ENUM-VOCABULARY`: no domain nouns in the presentation
|
|
1257
1701
|
// vocabulary) while the directive means geometric waypoints;
|
|
1258
1702
|
// - `render` was a verb naming a zone that admits only geometry
|
|
1259
|
-
// (`pin` — and, until
|
|
1703
|
+
// (`pin` — and, until 0.1, `path` and `routing`), and it
|
|
1260
1704
|
// collided with the renderer
|
|
1261
1705
|
// and the render options of §7. `layout` is the cross-tool word for
|
|
1262
1706
|
// this half of a diagram language, and the zone it opens carried the
|
|
@@ -1265,19 +1709,25 @@ function parseOne(text){
|
|
|
1265
1709
|
// the first entries in it that name NO replacement. `route`'s own message
|
|
1266
1710
|
// has to change with them: it pointed at `path`, which no longer exists,
|
|
1267
1711
|
// 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; }
|
|
1712
|
+
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
1713
|
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
1714
|
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
1715
|
if(kw==='path'){ err(n,RETIRED_PATH); continue; }
|
|
1272
1716
|
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; }
|
|
1717
|
+
if(kw==='render'){ err(n,'render has been renamed: use layout (the zone takes only pin — geometry, not presentation) (MIGRATIONS 0.1)'); continue; }
|
|
1274
1718
|
if(kw==='wrap'){ err(n,RETIRED_WRAP); continue; }
|
|
1275
1719
|
if(kw==='boundary'){ err(n,RETIRED_BOUNDARY); continue; }
|
|
1276
1720
|
if(kw==='layer'){ err(n,RETIRED_LAYER); continue; }
|
|
1721
|
+
// `PAINT-ORDER-CONSTRUCT`: `plane` joins this block. RULE 6.2 placement — the
|
|
1722
|
+
// spelling left the LANGUAGE, not one genre, so it fires wherever it
|
|
1723
|
+
// appears at line start, in every genre, AHEAD of the `GENRE-KEYWORD-ALLOWLIST` allowlist. A
|
|
1724
|
+
// `plane` line under `bitfield` gets the withdrawal, not "not allowed in
|
|
1725
|
+
// genre bitfield", which would be true and useless.
|
|
1726
|
+
if(kw==='plane'){ err(n,RETIRED_PLANE); continue; }
|
|
1277
1727
|
if(kw==='guide'){ err(n,RETIRED_GUIDE); continue; }
|
|
1278
1728
|
if(kw==='wave'){ err(n,RETIRED_WAVE); continue; }
|
|
1279
1729
|
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; }
|
|
1730
|
+
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
1731
|
|
|
1282
1732
|
// typed-block children
|
|
1283
1733
|
if(cur && ['field','break','cell','width','signal','gap'].includes(kw)){
|
|
@@ -1288,7 +1738,7 @@ function parseOne(text){
|
|
|
1288
1738
|
// per-field options. Classic form: field <name> <width> [options].
|
|
1289
1739
|
// Classic form: field <name> <width-in-bits|*> [fill=] [description=]
|
|
1290
1740
|
// [present=]
|
|
1291
|
-
// Conditional presence was a POSITIONAL FLAG until
|
|
1741
|
+
// Conditional presence was a POSITIONAL FLAG until 0.1:
|
|
1292
1742
|
// `optional` (…0.1), `conditional` (0.1…0.1),
|
|
1293
1743
|
// `optional` again (`PRESENCE-FLAG-SPELLING`). `PRESENCE-CONDITION-EXPRESSION` replaces the flag with
|
|
1294
1744
|
// `present=`, an option key whose value is the presence CONDITION as
|
|
@@ -1355,7 +1805,7 @@ function parseOne(text){
|
|
|
1355
1805
|
if(opts.index!==undefined){
|
|
1356
1806
|
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
1807
|
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; }
|
|
1808
|
+
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
1809
|
let bad=null; const parsed=[];
|
|
1360
1810
|
for(const el of splitList(posT[1],0)){
|
|
1361
1811
|
const it=el.v; if(!it) continue;
|
|
@@ -1375,7 +1825,7 @@ function parseOne(text){
|
|
|
1375
1825
|
// the overflow.
|
|
1376
1826
|
if(m[2]!=='*' && +m[2]>cur.word){
|
|
1377
1827
|
// The suggestion spells the CLASSIC form, and the classic name is
|
|
1378
|
-
// QUOTED (`QUOTING-RULES`). Until
|
|
1828
|
+
// QUOTED (`QUOTING-RULES`). Until 0.1 this string said
|
|
1379
1829
|
// `write "field P 64"` — a second line error, so a user who
|
|
1380
1830
|
// followed the diagnostic was told off twice.
|
|
1381
1831
|
bad='"'+nm+':'+m[2]+'" is wider than word='+cur.word+' — a compact item must fit one row; write it in the classic form to span rows: field "'+nm+'" '+m[2]; break; }
|
|
@@ -1401,7 +1851,7 @@ function parseOne(text){
|
|
|
1401
1851
|
// `break` ends the row after the fields declared since the block
|
|
1402
1852
|
// opened (or since the previous break). With none there is nothing to
|
|
1403
1853
|
// break — genre doc: "break with no preceding field in the current row".
|
|
1404
|
-
// Spelled `wrap` until
|
|
1854
|
+
// Spelled `wrap` until 0.1 (`ROW-BREAK-NAMING`): in CSS/typography `wrap` is
|
|
1405
1855
|
// AUTOMATIC reflow — a mode — while this is an EXPLICIT break, an
|
|
1406
1856
|
// event; CSS Fragmentation calls exactly this "a forced break …
|
|
1407
1857
|
// explicitly indicated by the … author" and HTML spells it `br`.
|
|
@@ -1420,7 +1870,7 @@ function parseOne(text){
|
|
|
1420
1870
|
// `POSITIONAL-LIST-SPELLING`: comma form only; the space form is retired.
|
|
1421
1871
|
const wtoks=pos.slice(1);
|
|
1422
1872
|
if(wtoks.length>1){
|
|
1423
|
-
err(n,'width takes ONE comma-delimited token: write width '+joinListForm(wtoks)+' — the space form is retired (MIGRATIONS)'); continue; }
|
|
1873
|
+
err(n,'width takes ONE comma-delimited token: write width '+joinListForm(wtoks)+' — the space form is retired (MIGRATIONS 0.1)'); continue; }
|
|
1424
1874
|
const vals=wtoks.length?splitList(posT[1],0).map(e=>e.v):[];
|
|
1425
1875
|
if(!vals.length){ err(n,'width needs one value per column (auto | <px> | <n>%)'); continue; }
|
|
1426
1876
|
let badw=null;
|
|
@@ -1449,7 +1899,7 @@ function parseOne(text){
|
|
|
1449
1899
|
// stays bare, exactly as `threshold offset=50%` does.
|
|
1450
1900
|
const cadr=pos[1]||'';
|
|
1451
1901
|
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; }
|
|
1902
|
+
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
1903
|
const rc=/^\((h?)(\d+),(\d+)\)$/.exec(cadr)||/^(h?)(\d+)$/.exec(cadr);
|
|
1454
1904
|
// `RULE-POSITION-ENUMERATION`: `highlight` is a bare keyword FLAG
|
|
1455
1905
|
// (vocabulary-sources.tsv `cell.highlight`), so RULE 2.4 governs it
|
|
@@ -1500,7 +1950,7 @@ function parseOne(text){
|
|
|
1500
1950
|
// / `TYPED-BLOCK-SILENT-FALLBACK`: data= is ABSENCE vs presence. An empty value, empty
|
|
1501
1951
|
// members (a,b), or a count that does not match the lane's `=`
|
|
1502
1952
|
// cells are all line errors — never silent drop or shift.
|
|
1503
|
-
// Spelled `labels=` until
|
|
1953
|
+
// Spelled `labels=` until 0.1 (`SIGNAL-DATA-KEY-SPELLING`): WaveDrom's own key is
|
|
1504
1954
|
// `data`, "an array of signal labels" naming every value cell, and
|
|
1505
1955
|
// after the `2`-`9` retirement (`TIMING-LANE-ALPHABET`) the two scopes coincide exactly.
|
|
1506
1956
|
let labels;
|
|
@@ -1544,7 +1994,21 @@ function parseOne(text){
|
|
|
1544
1994
|
// must be in the header genre allowlist. Child keywords still use the
|
|
1545
1995
|
// "needs a bitfield/table/timing above" path when they appear with no cur.
|
|
1546
1996
|
if(sawHeader && doc.genre && GENRE_KW[doc.genre] && !CHILD_KW.has(kw) && !GENRE_KW[doc.genre].has(kw)){
|
|
1547
|
-
|
|
1997
|
+
// `GENRE-NODE-SPELLING`: a word this genre SPELLS DIFFERENTLY is not an
|
|
1998
|
+
// unknown word, and "not allowed in genre statechart" would send an
|
|
1999
|
+
// author looking for a construct they cannot have instead of the one
|
|
2000
|
+
// they already wrote. The named diagnostic says which word and why.
|
|
2001
|
+
// (`edge`/`flowline`/`transition` never reach here — they are
|
|
2002
|
+
// dispatched by their own scanner above.)
|
|
2003
|
+
if(NODE_SPELLINGS.has(kw) && GENRE_NODE_KW[doc.genre])
|
|
2004
|
+
err(n, WRONG_WORD(kw, GENRE_NODE_KW[doc.genre], doc.genre));
|
|
2005
|
+
// `SCENE-KEYWORD-MEMBERSHIP`: same argument one step further. A word this genre
|
|
2006
|
+
// WITHDREW is not an unknown word either, and the author holding it
|
|
2007
|
+
// needs the ground, not a spellcheck.
|
|
2008
|
+
else if(GENRE_WITHDRAWN[doc.genre] && GENRE_WITHDRAWN[doc.genre][kw])
|
|
2009
|
+
err(n, WITHDRAWN_FROM_GENRE(kw, doc.genre));
|
|
2010
|
+
else
|
|
2011
|
+
err(n,'"'+kw+'" is not allowed in genre '+doc.genre);
|
|
1548
2012
|
continue;
|
|
1549
2013
|
}
|
|
1550
2014
|
if(badOpts(kw)) continue;
|
|
@@ -1569,10 +2033,23 @@ function parseOne(text){
|
|
|
1569
2033
|
// different values for the same visible text. One form, one
|
|
1570
2034
|
// meaning: the token is a normal quoted string and the generic
|
|
1571
2035
|
// tokenizer above has already resolved its escapes.
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
2036
|
+
// `DRAWN-ANNOTATION-FORM`: read the POSITIONALS, not the raw token stream.
|
|
2037
|
+
// `title` took no options until this release, so `tk.toks[1]` and
|
|
2038
|
+
// `tk.toks.length>2` were the same thing as `pos[1]` and
|
|
2039
|
+
// `pos.length>2`. They stop being the same thing the moment the line
|
|
2040
|
+
// may carry `note=`, and testing the raw stream would report the
|
|
2041
|
+
// annotation as a surplus positional. This is `OPTION-POSITION-PARSING`'s lesson (`bundle`
|
|
2042
|
+
// and `threshold` read `posq` for exactly this reason) applied to the
|
|
2043
|
+
// one directive that had never needed it.
|
|
2044
|
+
const t0v=pos[1], t0q=posq[1];
|
|
2045
|
+
if(t0v===undefined||!t0q){ err(n,'title needs a quoted string: title "<text>" (MIGRATIONS 0.1)'); break; }
|
|
2046
|
+
if(pos.length>2){ err(n,'unexpected argument "'+pos[2]+'"'); break; }
|
|
2047
|
+
doc.title=t0v; sawTitle=true;
|
|
2048
|
+
// The figure-level note lives on the document, not on an element —
|
|
2049
|
+
// there is no element for it to live on, which is the whole reason
|
|
2050
|
+
// `title` is an acceptor.
|
|
2051
|
+
if(opts.note!==undefined) doc.note=opts.note;
|
|
2052
|
+
break;
|
|
1576
2053
|
}
|
|
1577
2054
|
case 'class': {
|
|
1578
2055
|
// semantic class (`CATEGORICAL-MEANING-MAPPING`): meaning + presentation defaults declared
|
|
@@ -1595,30 +2072,11 @@ function parseOne(text){
|
|
|
1595
2072
|
if(pos.length>3){ err(n,'unexpected argument "'+pos[3]+'"'); break; }
|
|
1596
2073
|
classIds.add(id);
|
|
1597
2074
|
// `plane=` on a class is the members' default plane (an element's own
|
|
1598
|
-
// plane
|
|
2075
|
+
// `PAINT-ORDER-CONSTRUCT`: `plane=` is withdrawn, so a class carries the
|
|
2076
|
+
// FOUR §5 attributes it can still set. The paint-order attribute is
|
|
2077
|
+
// gone from the language, not merely off this directive.
|
|
1599
2078
|
doc.classes.push({id,label:pos[2],fill:opts.fill,stroke:opts.stroke,
|
|
1600
|
-
style:opts.style,
|
|
1601
|
-
break;
|
|
1602
|
-
}
|
|
1603
|
-
case 'plane': {
|
|
1604
|
-
const id=pos[1];
|
|
1605
|
-
{ const e=idErr(id,posq[1],'plane needs an id'); if(e){ err(n,e); break; } }
|
|
1606
|
-
if(planeIds.has(id)){ err(n,'duplicate plane id "'+id+'"'); break; }
|
|
1607
|
-
if(pos[2]!==undefined&&!posq[2]){ err(n,'plane label must be quoted: plane '+id+' "'+pos[2]+'" — '+Q_WHY); break; }
|
|
1608
|
-
if(pos.length>3){ err(n,'unexpected argument "'+pos[3]+'"'); break; }
|
|
1609
|
-
// 0.1: `z=` -> `z-index=` (CSS's own spelling for the
|
|
1610
|
-
// stacking concept, taken in full per RULE 4.2). The retired `z=`
|
|
1611
|
-
// is caught language-wide in RETIRED_OPT_KEYS before this runs.
|
|
1612
|
-
let z=doc.planes.length;
|
|
1613
|
-
const zi=opts['z-index'];
|
|
1614
|
-
if(zi!==undefined){
|
|
1615
|
-
if(!/^-?\d+$/.test(zi)){ err(n,'z-index must be a number'); break; }
|
|
1616
|
-
z=parseInt(zi,10);
|
|
1617
|
-
}
|
|
1618
|
-
planeIds.add(id);
|
|
1619
|
-
// `EMPTY-LABEL-STATE`: absent is absent, `""` is a written value — same
|
|
1620
|
-
// non-collapsing form as node/group/bundle and the typed blocks.
|
|
1621
|
-
doc.planes.push({id,label:pos[2]!==undefined?pos[2]:null,z});
|
|
2079
|
+
style:opts.style,line:n});
|
|
1622
2080
|
break;
|
|
1623
2081
|
}
|
|
1624
2082
|
// `FLOWCHART-ROLE-KEYWORDS`: `process` / `decision` / `terminator` DESUGAR to
|
|
@@ -1632,9 +2090,15 @@ function parseOne(text){
|
|
|
1632
2090
|
// being wrong is harmless, and a flowchart node may be a datastore, an
|
|
1633
2091
|
// annotation or a state, so `role:"process"` by default would let the
|
|
1634
2092
|
// model assert a falsehood the figure cannot be inspected to catch.
|
|
2093
|
+
// `GENRE-NODE-SPELLING`: `state` joins the same case. It is `node` renamed
|
|
2094
|
+
// for one genre, not a role — a statechart node has exactly one kind,
|
|
2095
|
+
// so there is no role to record and `role` stays null, exactly as a
|
|
2096
|
+
// bare `node` does. The DRAWING is unchanged too (`shape=box` default),
|
|
2097
|
+
// which is what let the five corpus figures migrate byte-identically.
|
|
1635
2098
|
case 'process': case 'decision': case 'terminator':
|
|
2099
|
+
case 'state':
|
|
1636
2100
|
case 'node': {
|
|
1637
|
-
const role=kw==='node'?null:kw;
|
|
2101
|
+
const role=(kw==='node'||kw==='state')?null:kw;
|
|
1638
2102
|
const id=pos[1];
|
|
1639
2103
|
{ const e=idErr(id,posq[1],kw+' needs an id'); if(e){ err(n,e); break; } }
|
|
1640
2104
|
if(dupId(id)){ err(n,'duplicate id "'+id+'"'); break; }
|
|
@@ -1655,6 +2119,7 @@ function parseOne(text){
|
|
|
1655
2119
|
// Display falls back to the id in render(), so the figure is unchanged.
|
|
1656
2120
|
doc.nodes.push({id,label:pos[2]!==undefined?pos[2]:null,shape,role,fill:opts.fill,stroke:opts.stroke,
|
|
1657
2121
|
style:opts.style,cls:parseClassList(opts['class'],optList(optT,'class')).ids,
|
|
2122
|
+
note:opts.note,
|
|
1658
2123
|
group:opts['in']||null,plane:opts.plane||'base',line:n});
|
|
1659
2124
|
break;
|
|
1660
2125
|
}
|
|
@@ -1674,6 +2139,7 @@ function parseOne(text){
|
|
|
1674
2139
|
}
|
|
1675
2140
|
doc.groups.push({id,label:pos[2]!==undefined?pos[2]:null,fill:opts.fill,stroke:opts.stroke,
|
|
1676
2141
|
style:opts.style,gap:ggap,cls:parseClassList(opts['class'],optList(optT,'class')).ids,
|
|
2142
|
+
note:opts.note,
|
|
1677
2143
|
plane:opts.plane||null,line:n});
|
|
1678
2144
|
break;
|
|
1679
2145
|
}
|
|
@@ -1685,7 +2151,7 @@ function parseOne(text){
|
|
|
1685
2151
|
// Shares the node/group/block id namespace. Of the §5 attributes it
|
|
1686
2152
|
// can carry only the two that need no drawn shape: `text=` (the label
|
|
1687
2153
|
// colour) and `plane=` (organizational, exactly as on a node).
|
|
1688
|
-
// Spelled `boundary` until
|
|
2154
|
+
// Spelled `boundary` until 0.1 (`EXTERNAL-ENDPOINT-NAMING`): three standards claim that
|
|
1689
2155
|
// word for the OPPOSITE meaning (UML ECB «boundary» is an internal
|
|
1690
2156
|
// interface object, C4 System_Boundary is a dashed grouping container,
|
|
1691
2157
|
// BPMN's is an event), and this spec's own prose had already stopped
|
|
@@ -1703,7 +2169,7 @@ function parseOne(text){
|
|
|
1703
2169
|
break;
|
|
1704
2170
|
}
|
|
1705
2171
|
case 'flow': {
|
|
1706
|
-
if(sawFlow){ err(n,'duplicate flow
|
|
2172
|
+
if(sawFlow){ err(n,'duplicate flow directive'); break; }
|
|
1707
2173
|
// `RULE-POSITION-ENUMERATION`: `flow.direction` is an enum position — bare.
|
|
1708
2174
|
if(posq[1]){ err(n,ENUM_BARE('flow '+pos[1])); break; }
|
|
1709
2175
|
if(!['right','down','left','up'].includes(pos[1])){ err(n,'flow needs right|down|left|up'); break; }
|
|
@@ -1736,7 +2202,7 @@ function parseOne(text){
|
|
|
1736
2202
|
// could not even detect a half-converted line.
|
|
1737
2203
|
const rtoks=pos.slice(1), rT=posT.slice(1);
|
|
1738
2204
|
if(rtoks.length>1){
|
|
1739
|
-
err(n,'rank takes ONE comma-delimited token: write rank '+joinListForm(rtoks)+' — the space form is retired (MIGRATIONS)'); break; }
|
|
2205
|
+
err(n,'rank takes ONE comma-delimited token: write rank '+joinListForm(rtoks)+' — the space form is retired (MIGRATIONS 0.1)'); break; }
|
|
1740
2206
|
const rels=rtoks.length?splitList(rT[0],0):[];
|
|
1741
2207
|
let badr=null;
|
|
1742
2208
|
for(const e of rels){
|
|
@@ -1776,7 +2242,7 @@ function parseOne(text){
|
|
|
1776
2242
|
// is now detectable — a second positional token after the label is
|
|
1777
2243
|
// the half-converted line the old tolerance could not report.
|
|
1778
2244
|
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; }
|
|
2245
|
+
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
2246
|
const pairs=[]; let badp=null;
|
|
1781
2247
|
outerB:
|
|
1782
2248
|
for(const t of restT){
|
|
@@ -1826,7 +2292,7 @@ function parseOne(text){
|
|
|
1826
2292
|
// percentage" `offset` (on <stop>), which is the same source the
|
|
1827
2293
|
// paint keys come from, so nothing is invented.
|
|
1828
2294
|
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; }
|
|
2295
|
+
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
2296
|
const m=/^(\d+(?:\.\d+)?)%$/.exec(opts.offset||''); // % is mandatory (`BARE-FRACTION-VALUES`)
|
|
1831
2297
|
if(!m||+m[1]<0||+m[1]>100){ err(n,'threshold needs offset=<0..100>% (with the % sign)'); break; }
|
|
1832
2298
|
// §5: `stroke=` is the marker colour, `color=` the label colour,
|
|
@@ -1840,7 +2306,7 @@ function parseOne(text){
|
|
|
1840
2306
|
case 'chart': {
|
|
1841
2307
|
// chart family: chart <table-id> [type=bar3d]
|
|
1842
2308
|
// rows -> X, columns -> Y, numeric cells -> Z (the table IS the data)
|
|
1843
|
-
// Spelled `plot` with `kind=bars3d` until
|
|
2309
|
+
// Spelled `plot` with `kind=bars3d` until 0.1 (`CHART-BLOCK-NAMING`).
|
|
1844
2310
|
// 0.1 (`CHART-LEVEL-KEY`): `level=` is DELETED. Zero uses corpus-wide, zero
|
|
1845
2311
|
// 3-D bar charts, zero requests; one in-repo example and two fixtures.
|
|
1846
2312
|
// It was the only construct whose caption the ENGINE wrote rather than
|
|
@@ -1877,11 +2343,11 @@ function parseOne(text){
|
|
|
1877
2343
|
// reads the same way. `OPTION-POSITION-PARSING` quotedness is `pos`-aligned, so an option
|
|
1878
2344
|
// may still precede it.
|
|
1879
2345
|
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; }
|
|
2346
|
+
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
2347
|
if(!opts['in']){ err(n,'band needs in=<node-or-group-id>'); break; }
|
|
1882
2348
|
// 0.1: the `%` is MANDATORY, matching `threshold offset=` (`BARE-FRACTION-VALUES`).
|
|
1883
2349
|
// `band 15`, `band 15-35` and `band 15%-35` all parsed before (the
|
|
1884
|
-
// separator was a hyphen until
|
|
2350
|
+
// separator was a hyphen until 0.1); one concept in one
|
|
1885
2351
|
// document must not have two value grammars (RULE 4.4).
|
|
1886
2352
|
// `RANGE-SPELLING`: the separator is `..`, and the HYPHEN form it
|
|
1887
2353
|
// replaces gets its own named diagnostic. `15-35%` reads as
|
|
@@ -1892,7 +2358,7 @@ function parseOne(text){
|
|
|
1892
2358
|
// cited by `bitfield`. `band` is EXPERIMENTAL (`CONSTRUCT-STATUS-TIERS`), so no
|
|
1893
2359
|
// compatibility promise is owed; the diagnostic is owed anyway.
|
|
1894
2360
|
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; }
|
|
2361
|
+
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
2362
|
const m=/^(\d+(?:\.\d+)?)%$|^(\d+(?:\.\d+)?)\.\.(\d+(?:\.\d+)?)%$/.exec(pos[2]||'');
|
|
1897
2363
|
if(!m){ err(n,'band needs a range with the % sign: band "<label>" <pct>% or band "<label>" <a>..<b>%'); break; }
|
|
1898
2364
|
if(pos.length>3){ err(n,'unexpected argument "'+pos[3]+'"'); break; }
|
|
@@ -1925,7 +2391,7 @@ function parseOne(text){
|
|
|
1925
2391
|
if(!id){ err(n,PIN_SHAPE); break; }
|
|
1926
2392
|
// 0.1 (RULE 1.1a): the pair is PARENTHESISED — at=(x,y).
|
|
1927
2393
|
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; }
|
|
2394
|
+
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
2395
|
const m=at===undefined?null:/^\((-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)\)$/.exec(at);
|
|
1930
2396
|
// A WRITTEN `at=` that does not parse is its own error, never the
|
|
1931
2397
|
// whole-line shape message: the author declared a point and got the
|
|
@@ -1979,7 +2445,7 @@ function parseOne(text){
|
|
|
1979
2445
|
// the cell border colour, `text=` the block caption colour.
|
|
1980
2446
|
// `TYPED-BLOCK-SILENT-FALLBACK`: word= empty or non-integer was a silent fallback/truncation
|
|
1981
2447
|
// (word= → 32, word=8.5 → 8). Positive integer only; absence → 32.
|
|
1982
|
-
// Spelled `unit=` until
|
|
2448
|
+
// Spelled `unit=` until 0.1 (`BITS-PER-ROW-KEY-NAMING`).
|
|
1983
2449
|
let word=32;
|
|
1984
2450
|
if(opts.word!==undefined){
|
|
1985
2451
|
if(opts.word===''||!/^\d+$/.test(opts.word)||+opts.word<1){
|
|
@@ -2056,11 +2522,39 @@ function parseOne(text){
|
|
|
2056
2522
|
}
|
|
2057
2523
|
for(const r of doc.ranks) for(const id of r.ids)
|
|
2058
2524
|
if(!nodeIds.has(id)) errs.push('Line '+r.line+': unknown node "'+id+'" in rank');
|
|
2525
|
+
// `MARKER-TARGET-KINDS`: `in=` on `threshold`/`band` also resolves a REGION id —
|
|
2526
|
+
// a `bitfield`, `table` or `timing` block. This is a WIDENING of the value
|
|
2527
|
+
// domain, not a third sense of `in=` and not a new spelling: the relation is
|
|
2528
|
+
// sense 2 verbatim, *the element this one is drawn across*, and what changes
|
|
2529
|
+
// is only which declared ids the resolver will bind.
|
|
2530
|
+
//
|
|
2531
|
+
// It is UNGATED, and that is argued rather than assumed. A region-targeted
|
|
2532
|
+
// threshold did not merely mean something else before this release — it did
|
|
2533
|
+
// not PARSE. `threshold "Max" in=q offset=50%` over a `table q` answered
|
|
2534
|
+
// `unknown target "q" for threshold`, the same error a nonexistent id gets,
|
|
2535
|
+
// because this set was hard-coded to nodes and groups while `table <id>`
|
|
2536
|
+
// makes the id mandatory and `chart <table-id>` already consumes it from
|
|
2537
|
+
// another directive. So no `figdown 0.1` or `figdown 0.2` document changes
|
|
2538
|
+
// meaning and none becomes invalid; the only documents affected are ones
|
|
2539
|
+
// that produced no figure at all. Core §13.0.1's hazard — "a figure that
|
|
2540
|
+
// looks right and means something else" — needs two readings to choose
|
|
2541
|
+
// between, and here the alternative reading was an error message. That is
|
|
2542
|
+
// exactly why `note=` IS gated a few hundred lines up and this is not: the
|
|
2543
|
+
// key had a prior meaning, this had none. Nothing is added to the option
|
|
2544
|
+
// registry, so a reader of `figdown 0.2` consulting core §10 finds the same
|
|
2545
|
+
// 45 rows either way.
|
|
2546
|
+
//
|
|
2547
|
+
// The two WRED figures this unblocks are the whole of the measured demand
|
|
2548
|
+
// (core §9 `ANNOTATION-LOCATOR-SPLIT`). The locator COORDINATE grammar — `in=q(3)`, addressing a
|
|
2549
|
+
// row inside the region — is designed and deliberately NOT built: it has no
|
|
2550
|
+
// shipping consumer, and RULE 4.7 argues against spending a grammar before
|
|
2551
|
+
// one exists.
|
|
2552
|
+
const regionTarget=id=>blockIds.has(id);
|
|
2059
2553
|
for(const gl of doc.thresholds)
|
|
2060
|
-
if(!groupIds.has(gl.target)&&!nodeIds.has(gl.target))
|
|
2554
|
+
if(!groupIds.has(gl.target)&&!nodeIds.has(gl.target)&&!regionTarget(gl.target))
|
|
2061
2555
|
errs.push('Line '+gl.line+': unknown target "'+gl.target+'" for threshold');
|
|
2062
2556
|
for(const f of doc.bands)
|
|
2063
|
-
if(!groupIds.has(f.target)&&!nodeIds.has(f.target))
|
|
2557
|
+
if(!groupIds.has(f.target)&&!nodeIds.has(f.target)&&!regionTarget(f.target))
|
|
2064
2558
|
errs.push('Line '+f.line+': unknown target "'+f.target+'" for band');
|
|
2065
2559
|
for(const t of doc.trunks) for(const [a,b] of t.pairs){
|
|
2066
2560
|
if((!nodeIds.has(a)&&!boundaryIds.has(a))||(!nodeIds.has(b)&&!boundaryIds.has(b))){ errs.push('Line '+t.line+': unknown endpoint in "'+a+'--'+b+'"'); continue; }
|
|
@@ -2119,7 +2613,7 @@ function parseOne(text){
|
|
|
2119
2613
|
// b class=p` was accepted, drew a #555 line, and rendered a legend swatch
|
|
2120
2614
|
// that showed nothing, so the class's meaning was invisible in its own
|
|
2121
2615
|
// derived legend. With `color=` retired (`COLOUR-KEY-STATUS`) the remaining shape of the
|
|
2122
|
-
// hole is a class carrying only `style
|
|
2616
|
+
// hole is a class carrying only `style=`, or nothing at
|
|
2123
2617
|
// all: the edge silently takes the default colour and the author who
|
|
2124
2618
|
// declared a class to CLASSIFY the edge gets no colour and no warning.
|
|
2125
2619
|
// Both halves are the same rule — a class an edge joins must declare at
|
|
@@ -2134,9 +2628,9 @@ function parseOne(text){
|
|
|
2134
2628
|
if(!c||c.stroke!==undefined) continue;
|
|
2135
2629
|
if(c.fill===undefined&&c.style!==undefined) continue;
|
|
2136
2630
|
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)');
|
|
2631
|
+
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
2632
|
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)');
|
|
2633
|
+
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
2634
|
}
|
|
2141
2635
|
}
|
|
2142
2636
|
{ // class references must resolve (closed grammar)
|
|
@@ -2289,7 +2783,7 @@ function stackSectionSvgs(results){
|
|
|
2289
2783
|
// became the node's LABEL. It is now a line error wherever it is part of the
|
|
2290
2784
|
// GRAMMAR — that is, everywhere except the FOUR verbatim regions (this
|
|
2291
2785
|
// function handles three of them; the pipe row is the caller's, see below).
|
|
2292
|
-
// The count read "three" until
|
|
2786
|
+
// The count read "three" until 0.1, listing four:
|
|
2293
2787
|
// - inside a quoted string ("…;…"),
|
|
2294
2788
|
// - inside a comment (already stripped before this runs),
|
|
2295
2789
|
// - inside an edge label (edge a -[packet arrives; TMR != 0]-> b),
|
|
@@ -2313,7 +2807,7 @@ function findReservedSemi(s){
|
|
|
2313
2807
|
}
|
|
2314
2808
|
return -1;
|
|
2315
2809
|
}
|
|
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)';
|
|
2810
|
+
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
2811
|
|
|
2318
2812
|
function findComment(s){
|
|
2319
2813
|
// '#' starts a comment only at line start or after whitespace,
|
|
@@ -2435,6 +2929,92 @@ function cwMax(s){ return Math.max(...String(s).split('\n').map(cw)); }
|
|
|
2435
2929
|
// §5 style= → SVG dash pattern. `def` is the construct's conventional
|
|
2436
2930
|
// default (the bundle ring and the threshold line are dashed by convention);
|
|
2437
2931
|
// an explicit style= always wins.
|
|
2932
|
+
// ── the note box (`DRAWN-ANNOTATION-FORM`) ───────────────────────────────────────
|
|
2933
|
+
// The drawn annotation's whole appearance lives in these three functions,
|
|
2934
|
+
// because `DOMAIN-CONVENTION-DIRECTIVES` gives the engine the drawing convention outright: `note=` takes
|
|
2935
|
+
// no `at=`, no `side=`, no colour and no size, so there is exactly one look and
|
|
2936
|
+
// it is decided here rather than by an author.
|
|
2937
|
+
//
|
|
2938
|
+
// The look is the UML note symbol — a rectangle with a folded top-right corner
|
|
2939
|
+
// — which is the notation of the metaclass the SPELLING is borrowed from
|
|
2940
|
+
// (UML 2.5.1's `Comment`; RULE 4.1 takes the standard's word, and taking its
|
|
2941
|
+
// glyph with it is what lets a reader recognise the box as an aside without a
|
|
2942
|
+
// legend entry). It is deliberately unlike a `node`: no rounded corners, a
|
|
2943
|
+
// paler wash, smaller type, and a corner no node shape has.
|
|
2944
|
+
const NOTE_FS=10, NOTE_PAD=6, NOTE_FOLD=9, NOTE_MAXCH=30;
|
|
2945
|
+
const NOTE_FILL='#fdfaf0', NOTE_STROKE='#c9c4b2', NOTE_INK='#5c584c';
|
|
2946
|
+
// The leader is deliberately DARKER than the box outline. Drawn in the box's
|
|
2947
|
+
// own stroke it was legible in the SVG and invisible on the page at 1x — a
|
|
2948
|
+
// leader nobody can see is a leader that is not there, and the note then reads
|
|
2949
|
+
// as annotating whatever it happens to sit above. Checked by eye, not by a
|
|
2950
|
+
// contrast number: the box is a surface and may recede, the leader is a
|
|
2951
|
+
// statement of attachment and may not.
|
|
2952
|
+
const NOTE_LEADER='#9c968a';
|
|
2953
|
+
// Deterministic greedy word wrap. Author newlines are honoured and never
|
|
2954
|
+
// merged; a run longer than the wrap width is broken only between words, so a
|
|
2955
|
+
// long identifier keeps its shape and simply widens the box.
|
|
2956
|
+
function noteWrap(text){
|
|
2957
|
+
const out=[];
|
|
2958
|
+
for(const para of String(text).split('\n')){
|
|
2959
|
+
const words=para.split(/ +/).filter(w=>w.length);
|
|
2960
|
+
if(!words.length){ out.push(''); continue; }
|
|
2961
|
+
let cur=words[0];
|
|
2962
|
+
for(let i=1;i<words.length;i++){
|
|
2963
|
+
if((cur+' '+words[i]).length<=NOTE_MAXCH) cur+=' '+words[i];
|
|
2964
|
+
else { out.push(cur); cur=words[i]; }
|
|
2965
|
+
}
|
|
2966
|
+
out.push(cur);
|
|
2967
|
+
}
|
|
2968
|
+
return out;
|
|
2969
|
+
}
|
|
2970
|
+
function noteBox(text){
|
|
2971
|
+
const lines=noteWrap(text);
|
|
2972
|
+
const lh=NOTE_FS*1.35;
|
|
2973
|
+
const w=Math.max(28, Math.max.apply(null,lines.map(tw))+NOTE_PAD*2+NOTE_FOLD);
|
|
2974
|
+
const h=lines.length*lh+NOTE_PAD*2-lh*0.15;
|
|
2975
|
+
return {w:Math.round(w*100)/100, h:Math.round(h*100)/100, lines, lh};
|
|
2976
|
+
}
|
|
2977
|
+
// The folded-corner outline, plus the small triangle that reads as the back of
|
|
2978
|
+
// the fold. One path each, so the shape is one primitive and the output is
|
|
2979
|
+
// byte-stable.
|
|
2980
|
+
function noteSvg(x,y,box,carrier){
|
|
2981
|
+
const F=NOTE_FOLD, w=box.w, h=box.h;
|
|
2982
|
+
const d='M'+x+' '+y+' H'+(x+w-F)+' L'+(x+w)+' '+(y+F)+' V'+(y+h)+' H'+x+' Z';
|
|
2983
|
+
const fold='M'+(x+w-F)+' '+y+' V'+(y+F)+' H'+(x+w)+' Z';
|
|
2984
|
+
const out=['<g class="fd-note"'+(carrier&&carrier.kind?' data-note-on="'+carrier.kind+'"':'')+'>',
|
|
2985
|
+
'<path d="'+d+'" fill="'+NOTE_FILL+'" stroke="'+NOTE_STROKE+'" stroke-width="1"/>',
|
|
2986
|
+
'<path d="'+fold+'" fill="'+NOTE_STROKE+'" fill-opacity="0.35" stroke="'+NOTE_STROKE+'" stroke-width="1"/>'];
|
|
2987
|
+
const first=y+NOTE_PAD+NOTE_FS*0.85;
|
|
2988
|
+
box.lines.forEach((ln,i)=>{
|
|
2989
|
+
out.push('<text x="'+(x+NOTE_PAD)+'" y="'+Math.round((first+i*box.lh)*100)/100+
|
|
2990
|
+
'" font-size="'+NOTE_FS+'" text-anchor="start" fill="'+NOTE_INK+'">'+esc(ln)+'</text>');
|
|
2991
|
+
});
|
|
2992
|
+
out.push('</g>');
|
|
2993
|
+
return out.join('');
|
|
2994
|
+
}
|
|
2995
|
+
// The leader is drawn ONLY when adjacency failed, and it is drawn AFTER the
|
|
2996
|
+
// box is placed, so it is correct by construction: it runs from the box edge
|
|
2997
|
+
// facing the carrier to the carrier's nearest point, and cannot be stale.
|
|
2998
|
+
function noteLeader(best,box,rect){
|
|
2999
|
+
const bx=best.x, by=best.y, bw=box.w, bh=box.h;
|
|
3000
|
+
const cx=rect.x+rect.w/2, cy=rect.y+rect.h/2;
|
|
3001
|
+
let x1,y1;
|
|
3002
|
+
if(best.side==='right') { x1=bx; y1=by+bh/2; }
|
|
3003
|
+
else if(best.side==='left') { x1=bx+bw; y1=by+bh/2; }
|
|
3004
|
+
else if(best.side==='below') { x1=bx+bw/2; y1=by; }
|
|
3005
|
+
else { x1=bx+bw/2; y1=by+bh; }
|
|
3006
|
+
// land on the carrier's border, not its centre, so the line stops at the
|
|
3007
|
+
// thing it points at
|
|
3008
|
+
const x2=Math.max(rect.x, Math.min(rect.x+rect.w, x1));
|
|
3009
|
+
const y2=Math.max(rect.y, Math.min(rect.y+rect.h, y1));
|
|
3010
|
+
return '<line x1="'+x1+'" y1="'+y1+'" x2="'+(rect.w||rect.h?x2:cx)+'" y2="'+(rect.w||rect.h?y2:cy)+
|
|
3011
|
+
'" stroke="'+NOTE_LEADER+'" stroke-width="1" stroke-dasharray="4 3"/>';
|
|
3012
|
+
}
|
|
3013
|
+
// A band's optional edge stroke. `renderScene` has had this as a local since
|
|
3014
|
+
// 0.1; `MARKER-TARGET-KINDS` needs the same rule for a REGION-scope band, which is drawn
|
|
3015
|
+
// outside the scene, so the one expression moves to module scope rather than
|
|
3016
|
+
// being written twice with a chance to drift.
|
|
3017
|
+
const bandEdgeOf=f=>(f.stroke||f.style)?' stroke="'+(f.stroke||'#8a8880')+'"'+dashOf(f.style,''):'';
|
|
2438
3018
|
function dashOf(style,def){
|
|
2439
3019
|
const p = style==='dashed'?'6 4' : style==='dotted'?'2 4' : style==='solid'?'' : def;
|
|
2440
3020
|
return p?' stroke-dasharray="'+p+'"':'';
|
|
@@ -2630,21 +3210,17 @@ function render(doc,ropts){
|
|
|
2630
3210
|
if(C[id] && C[id][k]!==undefined) x[k]=C[id][k];
|
|
2631
3211
|
}
|
|
2632
3212
|
};
|
|
2633
|
-
// a class carries
|
|
2634
|
-
//
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
if(C[id] && C[id].plane!==undefined) x.plane=C[id].plane;
|
|
2639
|
-
}
|
|
2640
|
-
};
|
|
2641
|
-
const rsAll=(x)=>{ rs(x,'fill'); rs(x,'stroke'); rs(x,'style'); rsl(x); };
|
|
3213
|
+
// `PAINT-ORDER-CONSTRUCT`: a class carries the FOUR §5 attributes that survive.
|
|
3214
|
+
// The fifth was `plane`, and its cascade helper (`rsl`) is gone with the
|
|
3215
|
+
// key — a class can no longer set a paint order because the language has
|
|
3216
|
+
// no paint order to set. Everything is on the implicit `base` plane.
|
|
3217
|
+
const rsAll=(x)=>{ rs(x,'fill'); rs(x,'stroke'); rs(x,'style'); };
|
|
2642
3218
|
for(const n of doc.nodes){ rsAll(n); if(n.style===undefined) n.style='solid'; }
|
|
2643
3219
|
for(const g of doc.groups){ rsAll(g); }
|
|
2644
3220
|
// 0.1 (§8.4): an edge has no interior, so it takes every class
|
|
2645
3221
|
// channel EXCEPT `fill` — which the parser has already guaranteed is
|
|
2646
3222
|
// accompanied by a `stroke` on any class an edge joins.
|
|
2647
|
-
for(const e of doc.edges){ rs(e,'stroke'); rs(e,'style');
|
|
3223
|
+
for(const e of doc.edges){ rs(e,'stroke'); rs(e,'style'); if(e.style===undefined) e.style='solid'; }
|
|
2648
3224
|
for(const b of doc.blocks){
|
|
2649
3225
|
rsAll(b);
|
|
2650
3226
|
if(b.fields) for(const f of b.fields) rsAll(f);
|
|
@@ -2658,13 +3234,54 @@ function render(doc,ropts){
|
|
|
2658
3234
|
const s=renderScene(doc,y); parts.push(s.svg); y=s.y; maxW=Math.max(maxW,s.w);
|
|
2659
3235
|
sceneMeta=s.meta;
|
|
2660
3236
|
}
|
|
3237
|
+
// `MARKER-TARGET-KINDS`: a region-scope `threshold`/`band` is drawn HERE and not
|
|
3238
|
+
// in `renderScene`, because a region is not in the scene. Typed blocks stack
|
|
3239
|
+
// in document order OUTSIDE the scene (core §2, the `plane=` carve-out says
|
|
3240
|
+
// so in as many words), so at the moment `renderScene` emits its own
|
|
3241
|
+
// thresholds the region has no geometry yet and sits at a `y` the scene never
|
|
3242
|
+
// sees. The mark therefore travels with its target: each block reports its
|
|
3243
|
+
// box, and the marks that name it are painted over that box in the same
|
|
3244
|
+
// coordinate shape (`x0`/`x1`/`yA`/`yB`) the scene uses for a group.
|
|
3245
|
+
const regionBox={};
|
|
2661
3246
|
for(const b of doc.blocks){
|
|
2662
3247
|
let s;
|
|
2663
3248
|
if(b.type==='bitfield') s=renderBitfield(b,y);
|
|
2664
3249
|
else if(b.type==='table') s=renderTable(b,y);
|
|
2665
3250
|
else if(b.type==='chart') s=renderChart(b,y,doc);
|
|
2666
3251
|
else s=renderTiming(b,y);
|
|
2667
|
-
parts.push(s.svg);
|
|
3252
|
+
parts.push(s.svg);
|
|
3253
|
+
if(s.box) regionBox[b.id]=s.box;
|
|
3254
|
+
y=s.y+24; maxW=Math.max(maxW,s.w);
|
|
3255
|
+
}
|
|
3256
|
+
{
|
|
3257
|
+
const rsvg=[];
|
|
3258
|
+
for(const f of (doc.bands||[])){
|
|
3259
|
+
const B=regionBox[f.target]; if(!B) continue;
|
|
3260
|
+
const w=B.x1-B.x0, h=B.yB-B.yA;
|
|
3261
|
+
let bx,by,bw,bh;
|
|
3262
|
+
if(f.dir==='up') { bx=B.x0; by=B.yB-h*f.to/100; bw=w; bh=h*(f.to-f.from)/100; }
|
|
3263
|
+
else if(f.dir==='down') { bx=B.x0; by=B.yA+h*f.from/100; bw=w; bh=h*(f.to-f.from)/100; }
|
|
3264
|
+
else if(f.dir==='right'){ bx=B.x0+w*f.from/100; by=B.yA; bw=w*(f.to-f.from)/100; bh=h; }
|
|
3265
|
+
else { bx=B.x1-w*f.to/100; by=B.yA; bw=w*(f.to-f.from)/100; bh=h; }
|
|
3266
|
+
rsvg.push('<rect x="'+bx+'" y="'+by+'" width="'+bw+'" height="'+bh+'" fill="'+f.fill+'" opacity="0.35"'+bandEdgeOf(f)+'/>');
|
|
3267
|
+
rsvg.push(textEl(bx+bw/2, by+bh/2+4, 11, 'middle', labelInk(f.fill,'#334155'), f.label,
|
|
3268
|
+
' paint-order="stroke" stroke="#fff" stroke-width="3"'));
|
|
3269
|
+
}
|
|
3270
|
+
for(const gl of (doc.thresholds||[])){
|
|
3271
|
+
const B=regionBox[gl.target]; if(!B) continue;
|
|
3272
|
+
const ly=B.yB-(B.yB-B.yA)*gl.pct/100;
|
|
3273
|
+
const col=gl.stroke||'#ef4444';
|
|
3274
|
+
rsvg.push('<line x1="'+B.x0+'" y1="'+ly+'" x2="'+B.x1+'" y2="'+ly+'" stroke="'+col+
|
|
3275
|
+
'" stroke-width="'+(gl.pct>=100?4:2)+'"'+dashOf(gl.style,'7 4')+'/>');
|
|
3276
|
+
rsvg.push(textEl(B.x1+8, ly+4, 11, 'start', col, gl.label,' paint-order="stroke" stroke="#fff" stroke-width="3"'));
|
|
3277
|
+
maxW=Math.max(maxW, B.x1+8+tw(gl.label));
|
|
3278
|
+
}
|
|
3279
|
+
// A band is a translucent wash UNDER the grid ink; a threshold is a mark
|
|
3280
|
+
// OVER it. The region has already been pushed, so both go after it and the
|
|
3281
|
+
// band leans on opacity rather than paint order for the "under" reading —
|
|
3282
|
+
// the same compromise `renderChart` makes, and the reason the opacity here
|
|
3283
|
+
// is lower than the scene's 0.9.
|
|
3284
|
+
if(rsvg.length) parts.push(rsvg.join(''));
|
|
2668
3285
|
}
|
|
2669
3286
|
// 0.1 (`CLASS-EMPTY-MEANING`): a class whose meaning is the EMPTY string claims no
|
|
2670
3287
|
// meaning, so it has nothing to explain and draws NO legend entry — it is
|
|
@@ -2702,6 +3319,30 @@ function render(doc,ropts){
|
|
|
2702
3319
|
parts.push(es.join(''));
|
|
2703
3320
|
y=ly+rowH;
|
|
2704
3321
|
}
|
|
3322
|
+
// `DRAWN-ANNOTATION-FORM`: the FIGURE-level note — `title "…" note="…"`. It carries
|
|
3323
|
+
// the 14% of measured annotations that name no single element ("Total: 8k
|
|
3324
|
+
// tunnel indexes", a four-signal legend, a TODO about the whole figure), and
|
|
3325
|
+
// it is the acceptor that removes the last argument for a standalone `note`
|
|
3326
|
+
// keyword: the figure HAS a declaration line, so attachment-by-position
|
|
3327
|
+
// reaches it too.
|
|
3328
|
+
//
|
|
3329
|
+
// It has no geometry to sit beside, so it takes no candidates and NEVER takes
|
|
3330
|
+
// a leader — a leader must point at something, and "the figure" is not a
|
|
3331
|
+
// thing on the canvas. It is placed with the caption, at the bottom, after
|
|
3332
|
+
// the scene, the regions and the derived legend. That is a placement rule and
|
|
3333
|
+
// not an author's choice (`DOMAIN-CONVENTION-DIRECTIVES`), and it is deterministic by construction:
|
|
3334
|
+
// there is exactly one figure-level note and exactly one place for it.
|
|
3335
|
+
//
|
|
3336
|
+
// The title itself is NOT drawn by default (`DEFAULT-VALUE-SELECTION` — an embedded figure sits
|
|
3337
|
+
// under a host caption), and the note does not follow it: the note is the
|
|
3338
|
+
// thing that draws. An author who wants the sentence in the picture writes
|
|
3339
|
+
// it here whether or not the renderer is showing the title.
|
|
3340
|
+
if(doc.note!==null&&doc.note!==undefined){
|
|
3341
|
+
const nb=noteBox(doc.note);
|
|
3342
|
+
const ny=y+8;
|
|
3343
|
+
parts.push(noteSvg(0,ny,nb,{kind:'title'}));
|
|
3344
|
+
y=ny+nb.h+2; maxW=Math.max(maxW,nb.w);
|
|
3345
|
+
}
|
|
2705
3346
|
const PADL=18, PADT=6;
|
|
2706
3347
|
const W=Math.ceil(maxW)+PADL+8, H=Math.ceil(y)+PADT+4;
|
|
2707
3348
|
return {svg:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 '+W+' '+H+'" width="'+W+'" height="'+H+'" font-family="system-ui,sans-serif">'
|
|
@@ -3186,7 +3827,7 @@ function renderScene(doc,y0){
|
|
|
3186
3827
|
const halo=' paint-order="stroke" stroke="#fff" stroke-width="3"';
|
|
3187
3828
|
const seg=(p,q,t,lbl,fs)=>reqLabel({p,q,t0:t,text:lbl,fs,col:ecol,halo,e,A,B,kind:'end'});
|
|
3188
3829
|
if(isBack.has(e)&&!pinned(e.a)&&!pinned(e.b)){
|
|
3189
|
-
// ── ROUTING-CHANGE ARCHITECTURE NOTE ──────────
|
|
3830
|
+
// ── ROUTING-CHANGE ARCHITECTURE NOTE (`SELF-EDGE-DRAWING`/`EDGE-BEND-RETENTION`) ──────────
|
|
3190
3831
|
// Edge labels are DEFERRED: every label is registered against its
|
|
3191
3832
|
// FINAL segment geometry (reqLabel/lblReq above) and placed by ONE
|
|
3192
3833
|
// greedy pass after all edges are drawn; arrowheads are computed from
|
|
@@ -3198,7 +3839,7 @@ function renderScene(doc,y0){
|
|
|
3198
3839
|
// arrowheads, orphaned labels): that was external splicing, not an
|
|
3199
3840
|
// engine gap. Patch routing here; do not "fix" the label machinery.
|
|
3200
3841
|
if(A===B){
|
|
3201
|
-
// Self-transition: a small side loop on the node, the
|
|
3842
|
+
// Self-transition (`SELF-EDGE-DRAWING`): a small side loop on the node, the
|
|
3202
3843
|
// convention of every drawing tool — never a lap of the figure
|
|
3203
3844
|
// through the back-edge channel. Side order r,l,b,t; first side
|
|
3204
3845
|
// whose loop box overlaps no other node wins (deterministic).
|
|
@@ -3351,7 +3992,7 @@ function renderScene(doc,y0){
|
|
|
3351
3992
|
// their neighbours turns a 40-point staircase into the 2–4 bends a
|
|
3352
3993
|
// dummy-vertex chain should have, without moving the drawn line.
|
|
3353
3994
|
simplifyPts(pts);
|
|
3354
|
-
// Waypoint prune: after collinear simplification a
|
|
3995
|
+
// Waypoint prune (`EDGE-BEND-RETENTION`): after collinear simplification a
|
|
3355
3996
|
// chain can still carry a staircase of near-collinear jogs — the drift
|
|
3356
3997
|
// clamp allows only a few px of sideways movement per rank, so a run
|
|
3357
3998
|
// that wants to move 35px sideways alternates short diagonals and
|
|
@@ -3463,7 +4104,13 @@ function renderScene(doc,y0){
|
|
|
3463
4104
|
// the arrowheads, and the other edges — plus a pull back toward the
|
|
3464
4105
|
// preferred point on the segment. The lowest score wins. No randomness,
|
|
3465
4106
|
// no iteration to a fixed point: one deterministic pass.
|
|
3466
|
-
if(lblReq.length)
|
|
4107
|
+
// `DRAWN-ANNOTATION-FORM`: this block used to be guarded by `if(lblReq.length)`,
|
|
4108
|
+
// with `obst`, `ovl`, `segHit` and `placed` local to it. The note pass below
|
|
4109
|
+
// is a SECOND claimant on exactly that machinery and must see exactly the
|
|
4110
|
+
// same `placed` list — a note that did not know where the edge labels went
|
|
4111
|
+
// could not yield to them, which is the first of the four placement rules.
|
|
4112
|
+
// So the scaffolding is hoisted and only the LOOP keeps the guard.
|
|
4113
|
+
{
|
|
3467
4114
|
const obst=nodes.filter(n=>!n.boundary).map(n=>({x:n.x,y:n.y,w:n.w,h:n.h,n}));
|
|
3468
4115
|
const ovl=(a,b)=>{
|
|
3469
4116
|
const ix=Math.min(a.x+a.w,b.x+b.w)-Math.max(a.x,b.x);
|
|
@@ -3501,7 +4148,7 @@ function renderScene(doc,y0){
|
|
|
3501
4148
|
return {x,y:by+up,anchor,t,side,box:{x:bx,y:by,w,h}};
|
|
3502
4149
|
};
|
|
3503
4150
|
const placed=[];
|
|
3504
|
-
for(const r of lblReq){
|
|
4151
|
+
if(lblReq.length) for(const r of lblReq){
|
|
3505
4152
|
const dx=r.q[0]-r.p[0], dy=r.q[1]-r.p[1];
|
|
3506
4153
|
const across=Math.abs(dx)>=Math.abs(dy);
|
|
3507
4154
|
let sides, ts, tPref;
|
|
@@ -3541,6 +4188,146 @@ function renderScene(doc,y0){
|
|
|
3541
4188
|
W=Math.max(W, best.box.x+best.box.w+4);
|
|
3542
4189
|
Hh=Math.max(Hh, best.box.y+best.box.h+4-y0-20);
|
|
3543
4190
|
}
|
|
4191
|
+
// ── note placement (`DRAWN-ANNOTATION-FORM`) — the SAME pass, entered LAST ────────────────
|
|
4192
|
+
// Four rules, and they are in the spec rather than only here because two
|
|
4193
|
+
// engines have to agree on them:
|
|
4194
|
+
//
|
|
4195
|
+
// 1. Notes register LAST, after every edge label and arrowhead. The
|
|
4196
|
+
// reason is semantic, not convenient: a label is ON the thing it names
|
|
4197
|
+
// and an arrowhead IS part of the connector, whereas a note is BESIDE
|
|
4198
|
+
// what it is about. A NOTE YIELDS; NOTHING YIELDS TO A NOTE. That is
|
|
4199
|
+
// why this loop runs after the one above, reads the same `placed`, and
|
|
4200
|
+
// is read by nothing after it.
|
|
4201
|
+
// 2. Candidates are generated around the CARRIER's final geometry — a
|
|
4202
|
+
// node box, a group rect, or the edge's segment list, all of which are
|
|
4203
|
+
// rects or segments by the time this pass runs — and scored by the same
|
|
4204
|
+
// overlap function against `placed`, `obst`, `arrowBox` and `edgeSegs`.
|
|
4205
|
+
// 3. A LEADER LINE is drawn ONLY when the box could not be placed adjacent
|
|
4206
|
+
// to its carrier. This is where attachment-by-syntax pays off twice:
|
|
4207
|
+
// the carrier is known from the line, so the PREFERRED position is
|
|
4208
|
+
// always adjacency and the leader is a fallback the engine reaches for
|
|
4209
|
+
// rather than a permanent part of the construct. It is drawn AFTER
|
|
4210
|
+
// placement, so it is correct by construction — the property `SELF-EDGE-DRAWING`/`EDGE-BEND-RETENTION`
|
|
4211
|
+
// already records for arrowheads.
|
|
4212
|
+
// 4. Determinism is not optional. `RENDERING-DETERMINISM` promises byte-reproducible output,
|
|
4213
|
+
// and one greedy pass in registration order with no iteration is what
|
|
4214
|
+
// delivers it. Registration order here is DOCUMENT order — carriers are
|
|
4215
|
+
// sorted by source line, across kinds — so moving a `group` line above
|
|
4216
|
+
// a `node` line moves the notes with it and nothing else changes.
|
|
4217
|
+
//
|
|
4218
|
+
// `DOMAIN-CONVENTION-DIRECTIVES` binds throughout: `note=` accepts no `at=`, no `side=`, no
|
|
4219
|
+
// `left of`/`right of`. The author names the meaning; the engine owns the
|
|
4220
|
+
// drawing convention. The convention is the UML note symbol — a rectangle
|
|
4221
|
+
// with a folded top-right corner — which is what makes a note readable AS
|
|
4222
|
+
// a note without a legend entry, and is the notation belonging to the
|
|
4223
|
+
// metaclass the spelling is borrowed from (RULE 4.1).
|
|
4224
|
+
const noteCarriers=[];
|
|
4225
|
+
for(const n of nodes) if(n.note!==undefined&&n.note!==null&&!n.boundary)
|
|
4226
|
+
noteCarriers.push({line:n.line,text:n.note,kind:'node',rect:{x:n.x,y:n.y,w:n.w,h:n.h},n});
|
|
4227
|
+
for(const g of doc.groups) if(g.note!==undefined&&g.note!==null){
|
|
4228
|
+
const B=gBox[g.id]; if(!B) continue;
|
|
4229
|
+
noteCarriers.push({line:g.line,text:g.note,kind:'group',
|
|
4230
|
+
rect:{x:B.x0,y:B.yA,w:B.x1-B.x0,h:B.yB-B.yA}});
|
|
4231
|
+
}
|
|
4232
|
+
for(const e of doc.edges) if(e.note!==undefined&&e.note!==null){
|
|
4233
|
+
// An edge is a polyline, not a rect. Its carrier POINT is the midpoint of
|
|
4234
|
+
// the middle registered segment — deterministic, and it is the same
|
|
4235
|
+
// "middle of the run" an author means when they annotate a wire. A note
|
|
4236
|
+
// whose edge never made it to the canvas (an endpoint that did not
|
|
4237
|
+
// resolve) simply has no carrier and is not drawn; the missing endpoint
|
|
4238
|
+
// is already its own line error.
|
|
4239
|
+
const segs=edgeSegs.filter(g=>g.e===e); if(!segs.length) continue;
|
|
4240
|
+
const m=segs[Math.floor((segs.length-1)/2)];
|
|
4241
|
+
const cx=(m.p[0]+m.q[0])/2, cy=(m.p[1]+m.q[1])/2;
|
|
4242
|
+
noteCarriers.push({line:e.line,text:e.note,kind:'edge',rect:{x:cx,y:cy,w:0,h:0},e});
|
|
4243
|
+
}
|
|
4244
|
+
noteCarriers.sort((a,b)=>a.line-b.line);
|
|
4245
|
+
for(const c of noteCarriers){
|
|
4246
|
+
const box=noteBox(c.text);
|
|
4247
|
+
// Adjacency first, then the same four sides pushed out far enough that a
|
|
4248
|
+
// leader is legible. `si` orders the sides; `far` is what decides the
|
|
4249
|
+
// leader, and it costs enough that adjacency wins every time adjacency is
|
|
4250
|
+
// merely imperfect rather than blocked.
|
|
4251
|
+
// Sides in preference order, and for the two that straddle the carrier
|
|
4252
|
+
// an ALIGNMENT as well. Centring a wide note over a narrow carrier at the
|
|
4253
|
+
// left edge of the canvas puts the box off it; aligning the box's left
|
|
4254
|
+
// edge with the carrier's is the same "beside this thing" reading and
|
|
4255
|
+
// stays on the page. Found by eye — see the off-canvas note below.
|
|
4256
|
+
const SIDES=[['right','c'],['left','c'],
|
|
4257
|
+
['below','c'],['below','l'],['below','r'],
|
|
4258
|
+
['above','c'],['above','l'],['above','r']];
|
|
4259
|
+
// Three distance tiers, not two. `near` is adjacency and takes no leader;
|
|
4260
|
+
// the two `far` tiers do. The third exists because a crowded figure can
|
|
4261
|
+
// have NO free space within one leader length of the carrier — a note
|
|
4262
|
+
// then had to sit on top of an edge label, which is the exact inversion
|
|
4263
|
+
// of rule 1 (a note yields; nothing yields to a note). Given somewhere
|
|
4264
|
+
// further to go, it goes there and the canvas grows to fit.
|
|
4265
|
+
let best=null,bestS=Infinity, fallback=null,fallbackS=Infinity;
|
|
4266
|
+
for(const tier of [0,1,2]){
|
|
4267
|
+
const far=tier>0, gap=[10,46,96][tier];
|
|
4268
|
+
for(let si=0;si<SIDES.length;si++){
|
|
4269
|
+
const side=SIDES[si][0], al=SIDES[si][1];
|
|
4270
|
+
let bx,by;
|
|
4271
|
+
if(side==='right'){ bx=c.rect.x+c.rect.w+gap; by=c.rect.y+c.rect.h/2-box.h/2; }
|
|
4272
|
+
else if(side==='left'){ bx=c.rect.x-gap-box.w; by=c.rect.y+c.rect.h/2-box.h/2; }
|
|
4273
|
+
else {
|
|
4274
|
+
bx=al==='l'?c.rect.x
|
|
4275
|
+
:al==='r'?c.rect.x+c.rect.w-box.w
|
|
4276
|
+
:c.rect.x+c.rect.w/2-box.w/2;
|
|
4277
|
+
by=side==='below'?c.rect.y+c.rect.h+gap:c.rect.y-gap-box.h;
|
|
4278
|
+
}
|
|
4279
|
+
const cb={x:bx,y:by,w:box.w,h:box.h};
|
|
4280
|
+
let s=0;
|
|
4281
|
+
// A NOTE YIELDS; NOTHING YIELDS TO A NOTE. `placed` holds the edge
|
|
4282
|
+
// labels and the notes already sited, and its weight is the HIGHEST
|
|
4283
|
+
// of the three — higher than a node's — because a label is a small
|
|
4284
|
+
// box and an AREA-weighted penalty would otherwise let a note sit on
|
|
4285
|
+
// one for less than it costs to clip a node's corner. Found by eye: a
|
|
4286
|
+
// transition note landed across two edge labels while a node overlap
|
|
4287
|
+
// three times the area scored higher.
|
|
4288
|
+
for(const b of placed) s+=10*ovl(cb,b);
|
|
4289
|
+
for(const o of obst) s+=6*ovl(cb,o);
|
|
4290
|
+
for(const a of arrowBox) s+=8*ovl(cb,a);
|
|
4291
|
+
for(const g of edgeSegs) if(segHit(g.p,g.q,cb)) s+=26;
|
|
4292
|
+
for(const g of doc.groups){ const B=gBox[g.id];
|
|
4293
|
+
if(B) s+=1.2*ovl(cb,{x:B.x0,y:B.yA,w:B.x1-B.x0,h:B.yB-B.yA}); }
|
|
4294
|
+
s+=si*12;
|
|
4295
|
+
s+=tier*900; // the leader is a LAST resort
|
|
4296
|
+
// Falling off the top or the left margin is not a BAD placement, it
|
|
4297
|
+
// is NO placement. The canvas grows right and down but has no
|
|
4298
|
+
// mechanism here to grow up or left, so such a box is clipped away
|
|
4299
|
+
// and the annotation VANISHES — the author wrote a sentence and the
|
|
4300
|
+
// reader never sees it, which is the worst outcome available. So it
|
|
4301
|
+
// is a HARD FILTER and not a score term: every other penalty is an
|
|
4302
|
+
// AREA and grows without bound, so no constant can outrank one
|
|
4303
|
+
// reliably. Found by eye on a statechart, where the note on the
|
|
4304
|
+
// leftmost state was emitted at x=-267.8 and simply did not appear —
|
|
4305
|
+
// and then found AGAIN when a large constant was tried first and the
|
|
4306
|
+
// box landed on top of two states instead.
|
|
4307
|
+
// `<0`, not `<2`. The edge-label pass keeps a 2px margin because a
|
|
4308
|
+
// label is loose text; a note is a BOX whose left edge at x=0 IS the
|
|
4309
|
+
// canvas origin and is perfectly placed. With the label pass's
|
|
4310
|
+
// threshold copied over, the one candidate that rescues a wide note
|
|
4311
|
+
// on a leftmost element — below, left-aligned, at exactly x=0 — was
|
|
4312
|
+
// filtered as off-canvas, and the note went to the only survivor: on
|
|
4313
|
+
// top of the next two states.
|
|
4314
|
+
if(cb.x<0||cb.y<y0){
|
|
4315
|
+
if(s<fallbackS-1e-9){ fallbackS=s; fallback={x:bx,y:by,far,side}; }
|
|
4316
|
+
continue;
|
|
4317
|
+
}
|
|
4318
|
+
if(s<bestS-1e-9){ bestS=s; best={x:bx,y:by,far,side}; }
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
// Only if EVERY candidate was off-canvas: take the least-bad one and
|
|
4322
|
+
// clamp it on. It may overlap something; visible and overlapping is
|
|
4323
|
+
// recoverable by an author, invisible is not.
|
|
4324
|
+
if(!best){ best=fallback; best.x=Math.max(0,best.x); best.y=Math.max(y0,best.y); }
|
|
4325
|
+
lblsvg.push(noteSvg(best.x,best.y,box,c));
|
|
4326
|
+
if(best.far) lblsvg.push(noteLeader(best,box,c.rect));
|
|
4327
|
+
placed.push({x:best.x,y:best.y,w:box.w,h:box.h});
|
|
4328
|
+
W=Math.max(W, best.x+box.w+4);
|
|
4329
|
+
Hh=Math.max(Hh, best.y+box.h+4-y0-20);
|
|
4330
|
+
}
|
|
3544
4331
|
}
|
|
3545
4332
|
// nodes on top (each wrapped in a draggable, identifiable group)
|
|
3546
4333
|
const nsvg=[];
|
|
@@ -3853,7 +4640,7 @@ function renderBitfield(b,y0){
|
|
|
3853
4640
|
//
|
|
3854
4641
|
// and 0.1 already ruled that a spanning field follows the RFC's
|
|
3855
4642
|
// drawing rather than a FigDown one; this is the same ruling applied to the
|
|
3856
|
-
// other construct in the same figure. Until
|
|
4643
|
+
// other construct in the same figure. Until 0.1 the engine drew ONE
|
|
3857
4644
|
// occurrence and hung `[first] … [last]` on the strip — a FigDown invention
|
|
3858
4645
|
// where a convention already existed.
|
|
3859
4646
|
//
|
|
@@ -3900,7 +4687,7 @@ function renderBitfield(b,y0){
|
|
|
3900
4687
|
const shiftFor=(row)=>elisBands(row)*EL_H;
|
|
3901
4688
|
// `FIELD-WIDER-THAN-WORD`: ONE FIELD IS ONE BOX.
|
|
3902
4689
|
//
|
|
3903
|
-
// A field wider than `word=` occupies several rows. Until
|
|
4690
|
+
// A field wider than `word=` occupies several rows. Until 0.1 each
|
|
3904
4691
|
// row was a separate fully-bordered <rect> carrying the full label, so a
|
|
3905
4692
|
// 128-bit address at word=32 drew as FOUR captioned boxes and a reader saw
|
|
3906
4693
|
// four fields where the model has one. In examples/srh.fd it was worse: the
|
|
@@ -4096,7 +4883,7 @@ function renderBitfield(b,y0){
|
|
|
4096
4883
|
}
|
|
4097
4884
|
boxes.forEach(function(bx,bi){
|
|
4098
4885
|
// `DESCRIPTION-KEY-SPELLING`: the `<title>` is a CHILD of the shape it names, not
|
|
4099
|
-
// a sibling. Until
|
|
4886
|
+
// a sibling. Until 0.1 it was pushed into the block's stream
|
|
4100
4887
|
// after the rect and the label, so it landed under the figure's single
|
|
4101
4888
|
// <g> — and SVG says a <title> names its PARENT, so every description in
|
|
4102
4889
|
// a figure named the same <g> and a conforming UA showed one arbitrary
|
|
@@ -4255,7 +5042,8 @@ function renderBitfield(b,y0){
|
|
|
4255
5042
|
}
|
|
4256
5043
|
yb+=2;
|
|
4257
5044
|
}
|
|
4258
|
-
return {svg:svg.join(''), y:yb, w:wb
|
|
5045
|
+
return {svg:svg.join(''), y:yb, w:wb,
|
|
5046
|
+
box:{x0:0, x1:wb, yA:y0+18, yB:yb}};
|
|
4259
5047
|
}
|
|
4260
5048
|
|
|
4261
5049
|
// ---- table (with ^ rowspan / < colspan merging and per-cell marks) ----
|
|
@@ -4351,7 +5139,20 @@ function renderTable(t,y0){
|
|
|
4351
5139
|
i=>[xAt[i], xAt[i+1]], i=>[cellAt(r-1,i), cellAt(r,i)]));
|
|
4352
5140
|
svg.push(edgeSvg(EDG, DEF));
|
|
4353
5141
|
const yEnd=yTop+yAt[grid.length];
|
|
4354
|
-
|
|
5142
|
+
// `MARKER-TARGET-KINDS`: the GRID's box, so a region-scope `threshold`/`band`
|
|
5143
|
+
// can be drawn across it. It is the grid and not the returned slot: the slot
|
|
5144
|
+
// includes the caption row and the trailing gap, and `offset=50%` on a table
|
|
5145
|
+
// must mean half way down the ROWS, not half way down the whitespace.
|
|
5146
|
+
// `MARKER-TARGET-KINDS`: the box a region-scope `threshold`/`band` is measured
|
|
5147
|
+
// against spans the DATA ROWS, not the whole grid. Measured over the grid,
|
|
5148
|
+
// `offset=85%` on a three-row table lands on the COLUMN HEADINGS and strikes
|
|
5149
|
+
// through them — found by eye on the WRED figure this widening exists for.
|
|
5150
|
+
// The header tiers are chrome: they name the columns, they are not values,
|
|
5151
|
+
// and a threshold is a statement about values. `h1..hN` and `1..` are already
|
|
5152
|
+
// separate address spaces in this genre (genres/table.md), so the split is
|
|
5153
|
+
// the genre's own and not invented here.
|
|
5154
|
+
return {svg:svg.join(''), y:yEnd+6, w:totalW+2,
|
|
5155
|
+
box:{x0:0, x1:totalW, yA:yTop+yAt[H], yB:yEnd}};
|
|
4355
5156
|
}
|
|
4356
5157
|
|
|
4357
5158
|
// ---- chart bar3d: deterministic isometric projection of a table ----
|
|
@@ -4455,7 +5256,8 @@ function renderTiming(w,y0){
|
|
|
4455
5256
|
svg.push('<path d="M'+x+','+(y+4)+' q4,'+(hTotal/4)+' 0,'+(hTotal/2)+' q-4,'+(hTotal/4)+' 0,'+(hTotal/2)+'" fill="none" stroke="#999" stroke-width="2"/>');
|
|
4456
5257
|
}
|
|
4457
5258
|
const H=y+8+w.signals.length*(laneH+laneGap);
|
|
4458
|
-
return {svg:svg.join(''), y:H, w:nameW+cycles*cycleW+2
|
|
5259
|
+
return {svg:svg.join(''), y:H, w:nameW+cycles*cycleW+2,
|
|
5260
|
+
box:{x0:nameW, x1:nameW+cycles*cycleW, yA:y0+18, yB:H}};
|
|
4459
5261
|
}
|
|
4460
5262
|
|
|
4461
5263
|
// ============================================================
|