figdown 0.1.0-rc.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -171
- package/dist/figdown.js +2601 -580
- package/dist/figdown.mjs +2601 -580
- package/examples/evpn-fabric.svg +16 -11
- package/examples/showcase/arp-resolution.svg +18 -16
- package/examples/showcase/ethernet-frame.svg +12 -10
- package/examples/showcase/l2-forwarding-logic.svg +20 -16
- package/examples/showcase/tcp-handshake.svg +14 -13
- package/examples/showcase/tcp-header.svg +26 -19
- package/examples/showcase/tcp-state-machine.svg +57 -44
- package/guide/expressing.md +160 -0
- package/guide/layout.md +635 -0
- package/{SHOWCASE.md → guide/showcase.md} +130 -108
- package/package.json +30 -4
- package/skill/figdown/SKILL.md +173 -0
- package/skill/figdown/build-svg.js +51 -16
- package/skill/figdown/figdown.html +3005 -783
- package/skill/figdown/reference/bitfield.md +130 -0
- package/skill/figdown/reference/experimental/constructs.md +90 -0
- package/skill/figdown/reference/experimental/flowchart.md +56 -0
- package/skill/figdown/reference/experimental/timing.md +33 -0
- package/skill/figdown/reference/experimental/topology.md +38 -0
- package/skill/figdown/reference/layout.md +77 -0
- package/skill/figdown/reference/reading.md +119 -0
- package/skill/figdown/reference/scene.md +118 -0
- package/skill/figdown/reference/table.md +71 -0
- package/skill/figdown/reference/transcribe.md +46 -0
- package/EXPRESSING.md +0 -119
- package/LAYOUT-GUIDE.md +0 -367
- package/README.zh-tw.md +0 -185
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figdown
|
|
3
|
+
description: Create and maintain documentation figures as FigDown .fd text files with deterministic SVG artifacts embedded in Markdown. Use when asked to create, edit, fix, or read diagrams/figures in docs — block and architecture diagrams, topologies, flowcharts, bit-level layouts (packet headers, hardware registers), tables, timing waveforms — or when a .md contains an SVG with a "source: *.fd" footer.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# FigDown — figures as text, one source, two readers
|
|
7
|
+
|
|
8
|
+
A figure lives as a `.fd` text file (single source of truth). The SVG is a
|
|
9
|
+
deterministic build artifact for human eyes; **you read the `.fd` for meaning —
|
|
10
|
+
never OCR the SVG**. Structure (nodes, edges, fields, rows) is the content;
|
|
11
|
+
placement and colour are presentation whose only job is rendering stability.
|
|
12
|
+
|
|
13
|
+
**Choose the form that communicates best.** Three modes: (1) prose, list or
|
|
14
|
+
table alone for linear facts, values or procedures — do not draw; (2) a figure
|
|
15
|
+
alone when it is self-contained; (3) figure + prose, the common case — the
|
|
16
|
+
figure carries structure and relationships, the surrounding prose carries the
|
|
17
|
+
detail. When the answer involves a figure, author one. When it does not, do
|
|
18
|
+
not draw out of habit. The `.fd` must carry everything the rendered figure
|
|
19
|
+
conveys, and no more is owed.
|
|
20
|
+
|
|
21
|
+
**Mode 3 has an authoring consequence:** if the figure is the outline, do not
|
|
22
|
+
cram the whole story into it. Keep labels short and leave the detail to the
|
|
23
|
+
host document. Overstuffed labels are the visible symptom of an author making
|
|
24
|
+
the figure do prose's job.
|
|
25
|
+
|
|
26
|
+
## Workflow
|
|
27
|
+
|
|
28
|
+
1. Edit or create `X.fd` (never edit an `.svg`). A new file opens with the
|
|
29
|
+
spec provenance line, verbatim, then the header — see *The document
|
|
30
|
+
skeleton* below.
|
|
31
|
+
2. Build + validate: `node <this-skill-dir>/build-svg.js X.fd`
|
|
32
|
+
Errors come as `Line N: message` — fix and rerun until `OK`. Titles are not
|
|
33
|
+
drawn by default (Markdown supplies the caption); `--with-title` opts in.
|
|
34
|
+
3. Embed in Markdown — the SVG only, never `.fd` content:
|
|
35
|
+
|
|
36
|
+
```markdown
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
<sub>source: [figures/ingress.fd](figures/ingress.fd)</sub>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
4. Keep `X.fd` and `X.svg` side by side, same basename; commit both.
|
|
43
|
+
|
|
44
|
+
If a `.fd` is missing, its `.svg` carries the source verbatim, the SHA-256 of
|
|
45
|
+
that source and the engine version that rendered it, in
|
|
46
|
+
`<metadata id="figdown-source" data-sha256="…" data-engine-version="…">`.
|
|
47
|
+
Recover from there. On hash mismatch the `.fd` is truth: rebuild. If the hash
|
|
48
|
+
matches but your drawing differs, compare `data-engine-version` — that is the
|
|
49
|
+
other half of "same source → same SVG".
|
|
50
|
+
|
|
51
|
+
## Load only what you need
|
|
52
|
+
|
|
53
|
+
This file is the whole genre-independent language. **Everything else is one
|
|
54
|
+
lookup away, and the lookup is mechanical:** line 1 of a `.fd` names its genre,
|
|
55
|
+
so you know which file you need before you need it.
|
|
56
|
+
|
|
57
|
+
<!-- skill-coverage: router -->
|
|
58
|
+
|
|
59
|
+
| Genre on line 1 | Load | Add only for EXPERIMENTAL constructs |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `block` — architecture, dataflow, hierarchy | `reference/scene.md`, `reference/layout.md` | `reference/experimental/constructs.md` |
|
|
62
|
+
| `bitfield` — packet headers, register layouts | `reference/bitfield.md` | — |
|
|
63
|
+
| `table` — config, state, memory maps | `reference/table.md` | `reference/experimental/constructs.md` |
|
|
64
|
+
| `topology` | `reference/scene.md`, `reference/layout.md` | `reference/experimental/topology.md`, `reference/experimental/constructs.md` |
|
|
65
|
+
| `flowchart` | `reference/scene.md`, `reference/layout.md` | `reference/experimental/flowchart.md`, `reference/experimental/constructs.md` |
|
|
66
|
+
| `timing` | — | `reference/experimental/timing.md` |
|
|
67
|
+
|
|
68
|
+
Two more files answer a **task** rather than a genre:
|
|
69
|
+
|
|
70
|
+
- **Reading a `.fd` someone else wrote, to summarise or answer from it** →
|
|
71
|
+
`reference/reading.md`. It is the contract for what you may conclude and
|
|
72
|
+
what you must not infer, and it is all you need: a reader can skip every
|
|
73
|
+
genre file above.
|
|
74
|
+
- **Transcribing an existing figure** — a drawing, a screenshot, another
|
|
75
|
+
format → `reference/transcribe.md`.
|
|
76
|
+
|
|
77
|
+
Pick the genre by what the figure IS, not by its subject: the left-hand
|
|
78
|
+
column above says what each is for. Prefer the first three — they are the
|
|
79
|
+
portable ones.
|
|
80
|
+
|
|
81
|
+
## The document skeleton
|
|
82
|
+
|
|
83
|
+
The grammar is **CLOSED**: an unknown line is an error, never ignored. Five
|
|
84
|
+
keywords — `figdown`, `title`, `class`, `layout`, `pin` — mean the same thing
|
|
85
|
+
under every genre and no genre may redefine them. Everything else belongs to a
|
|
86
|
+
genre.
|
|
87
|
+
|
|
88
|
+
```figdown
|
|
89
|
+
# FigDown — figures as text. Spec: https://github.com/FigDown/figdown
|
|
90
|
+
# ^ FIRST LINE of every document you write.
|
|
91
|
+
# Verbatim, em dash included. It is a comment:
|
|
92
|
+
# the parser ignores it. It is there because the
|
|
93
|
+
# file travels — into a wiki, a ticket, a pasted
|
|
94
|
+
# message — and tells whoever meets it there what
|
|
95
|
+
# the format is and where it is defined.
|
|
96
|
+
figdown 0.1 block # REQUIRED first significant line; comments and
|
|
97
|
+
# blanks may precede it. The genre is REQUIRED.
|
|
98
|
+
# A later `figdown 0.1 <genre>` starts a new
|
|
99
|
+
# section with its own genre; one file still
|
|
100
|
+
# renders to one SVG.
|
|
101
|
+
title "Some Title" # optional; the quotes are REQUIRED
|
|
102
|
+
# comments start with '#'; inside quotes the only escapes are \n \" \\
|
|
103
|
+
class hot "Congested path" stroke=#dc2626 # meaning + style, declared once
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Quotes mark a STRING, and nothing else.** Every label is quoted, at every
|
|
107
|
+
position that has one — `title "TCP Header"`, `class c "meaning"`, and the
|
|
108
|
+
label of every block opener — because whitespace also separates arguments, so
|
|
109
|
+
a bare token cannot hold a phrase. Everything that is *not* a string is bare:
|
|
110
|
+
ids and references (`class=hot`), and every value drawn from a fixed list of
|
|
111
|
+
spellings (`style=dashed`, and the header's own `figdown 0.1 <genre>`).
|
|
112
|
+
Quoting one of those is a line error. Quoting a NUMBER or a point is not:
|
|
113
|
+
`width="90"` and `at="(1,2)"` are legal and mean exactly what the bare forms
|
|
114
|
+
mean.
|
|
115
|
+
|
|
116
|
+
**`class` is how meaning survives.** Colour and shape alone assert nothing a
|
|
117
|
+
reader can recover: a `class` states the meaning in text and the legend draws
|
|
118
|
+
itself from it. Anything you want a reader to *conclude* goes in a label or a
|
|
119
|
+
class meaning — never in a colour alone. A class with an empty meaning
|
|
120
|
+
(`class c ""`) deliberately claims nothing.
|
|
121
|
+
|
|
122
|
+
**Two paint channels, SVG's own**, legal on any element that has them:
|
|
123
|
+
|
|
124
|
+
| Key | What it paints |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `fill=` | the **interior** of a shape — `#rgb`, `#rrggbb`, a CSS colour name, or `transparent` |
|
|
127
|
+
| `stroke=` | the **outline** of a shape, and the **whole** of a line |
|
|
128
|
+
| `style=` | `solid` \| `dashed` \| `dotted` |
|
|
129
|
+
| `class=` | one or more declared class ids, comma-separated: `class=hot,legacy` |
|
|
130
|
+
|
|
131
|
+
A line — an edge, a marker, a ring — has no interior, so `fill=` on one is an
|
|
132
|
+
error naming `stroke=`. There is **no label-colour key**: a label's colour is
|
|
133
|
+
derived from the background it sits on, so light text lands on dark fills by
|
|
134
|
+
itself.
|
|
135
|
+
|
|
136
|
+
**`layout` and `pin`** are the layout zone. Everything from a `layout` line
|
|
137
|
+
down is geometry that exists only to stabilise the `.svg`: it carries no
|
|
138
|
+
meaning, no genre may put its own semantics inside it, and `pin` is the only
|
|
139
|
+
directive legal there. Skip the zone when reading; see `reference/layout.md`
|
|
140
|
+
when writing.
|
|
141
|
+
|
|
142
|
+
## Portability: two statuses, and the parser tells you nothing
|
|
143
|
+
|
|
144
|
+
Every keyword, option key and genre is either **NORMATIVE** (NORMATIVE — inside the
|
|
145
|
+
v0.1 conformance surface and its compatibility promise) or **EXPERIMENTAL**
|
|
146
|
+
(EXPERIMENTAL — the engine accepts it and your document keeps working, but it is
|
|
147
|
+
outside both, and may change or be withdrawn in a later `0.x`).
|
|
148
|
+
|
|
149
|
+
**The parser never warns**, so a line that parses tells you nothing about its
|
|
150
|
+
status — this is the only signal you get. Everything reachable *only* through
|
|
151
|
+
the right-hand column of the router is EXPERIMENTAL: use it when the figure needs
|
|
152
|
+
it, and say so beside the figure.
|
|
153
|
+
|
|
154
|
+
**Hybrid figures** (a scene and a table in one artifact): start a second
|
|
155
|
+
section with its own `figdown 0.1 <genre>` header rather than nesting one
|
|
156
|
+
genre's block inside another's.
|
|
157
|
+
|
|
158
|
+
## Do not invent syntax
|
|
159
|
+
|
|
160
|
+
If something seems missing, compose it from what exists or tell the user.
|
|
161
|
+
Retired spellings are hard errors whose message names the replacement — read
|
|
162
|
+
the error instead of guessing around it.
|
|
163
|
+
|
|
164
|
+
## Maintaining a document written against an older version
|
|
165
|
+
|
|
166
|
+
Line 1 pins the version, so never mix syntax generations in one file. When the
|
|
167
|
+
language moves, a **NORMATIVE** construct ships a mechanical rewrite rule and
|
|
168
|
+
you upgrade by applying the rules in order. An **EXPERIMENTAL** one carries no
|
|
169
|
+
such promise: it may be withdrawn with nothing that does what it did, in which
|
|
170
|
+
case the tooling reports and cannot rewrite, and a human decides what the
|
|
171
|
+
figure should say instead.
|
|
172
|
+
|
|
173
|
+
Full spec and docs: https://github.com/FigDown/figdown
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// FigDown
|
|
2
|
+
// FigDown sidecar generator: X.fd -> X.svg
|
|
3
3
|
// Deterministic: same source -> same SVG, bit-level. The artifact embeds
|
|
4
|
-
// its own source and a SHA-256 of it (spec
|
|
4
|
+
// its own source and a SHA-256 of it (spec core §7).
|
|
5
5
|
// The engine is never forked: parser+renderer are extracted from the editor
|
|
6
6
|
// single file at build time (the "regenerate, don't fork" rule).
|
|
7
7
|
'use strict';
|
|
@@ -24,24 +24,32 @@ function loadEngine() {
|
|
|
24
24
|
const start = h.indexOf('const SHAPES');
|
|
25
25
|
const end = h.indexOf('// 3. UI');
|
|
26
26
|
if (start < 0 || end < 0) throw new Error('cannot locate engine in ' + ENGINE);
|
|
27
|
-
const factory = new Function(h.slice(start, end) + '\nreturn {parse, render};');
|
|
27
|
+
const factory = new Function(h.slice(start, end) + '\nreturn {parse, render, stackSectionSvgs, FIGDOWN_VERSION};');
|
|
28
28
|
return factory();
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function buildOne(engine, fdPath) {
|
|
32
32
|
const src = fs.readFileSync(fdPath, 'utf8');
|
|
33
|
-
const
|
|
33
|
+
const parsed = engine.parse(src);
|
|
34
|
+
const errs = parsed.errs || parsed.errors || [];
|
|
35
|
+
const docs = parsed.docs && parsed.docs.length ? parsed.docs : (parsed.doc ? [parsed.doc] : []);
|
|
34
36
|
if (errs.length) {
|
|
35
37
|
console.error(fdPath + ':');
|
|
36
38
|
for (const e of errs) console.error(' ' + e);
|
|
37
39
|
return false;
|
|
38
40
|
}
|
|
39
|
-
const svg =
|
|
41
|
+
const svg = docs.length > 1
|
|
42
|
+
? engine.stackSectionSvgs(docs.map(d => engine.render(d, RENDER_OPTS)))
|
|
43
|
+
: engine.render(docs[0], RENDER_OPTS).svg;
|
|
40
44
|
const hash = crypto.createHash('sha256').update(src, 'utf8').digest('hex');
|
|
41
|
-
//
|
|
42
|
-
//
|
|
45
|
+
// The artifact records three things (spec core §7): the SHA-256 OF THE
|
|
46
|
+
// SOURCE, the ENGINE VERSION that rendered it, and any non-default render
|
|
47
|
+
// option — so a third-party rebuild (same source + same engine version +
|
|
48
|
+
// same recorded options) stays bit-identical, and a diff between two
|
|
49
|
+
// renderings of one source has somewhere to point.
|
|
43
50
|
const optAttr = RENDER_OPTS.title === true ? ' data-render-options="with-title"' : '';
|
|
44
|
-
const meta = '<metadata id="figdown-source" data-sha256="' + hash + '"'
|
|
51
|
+
const meta = '<metadata id="figdown-source" data-sha256="' + hash + '"'
|
|
52
|
+
+ ' data-engine-version="' + engine.FIGDOWN_VERSION + '"' + optAttr + '><![CDATA[\n'
|
|
45
53
|
+ src.replace(/]]>/g, ']]]]><![CDATA[>') + '\n]]></metadata>';
|
|
46
54
|
const artifact = svg.replace(/<\/svg>$/, meta + '</svg>');
|
|
47
55
|
const out = fdPath.replace(/\.fd$/, '') + '.svg';
|
|
@@ -54,20 +62,47 @@ const argv = process.argv.slice(2);
|
|
|
54
62
|
const RENDER_OPTS = {};
|
|
55
63
|
const args = argv.filter(a => {
|
|
56
64
|
if (a === '--with-title') { RENDER_OPTS.title = true; return false; }
|
|
57
|
-
if (a === '--no-title') { return false; } // the default
|
|
65
|
+
if (a === '--no-title') { return false; } // already the default; accepted so old command lines keep working
|
|
58
66
|
return true;
|
|
59
67
|
});
|
|
60
68
|
if (!args.length) {
|
|
61
69
|
console.error('usage: node tools/build-svg.js [--with-title] <file.fd | dir> ...');
|
|
62
70
|
process.exit(2);
|
|
63
71
|
}
|
|
72
|
+
// RECURSIVE. "A gate that does not recurse is a gate that lies" was written
|
|
73
|
+
// for the read-only CHECKS, which read only the top level of a directory.
|
|
74
|
+
// The GENERATOR had the same defect and was missed:
|
|
75
|
+
// handed `examples/ figures/` it rebuilt 28 of the 66 artifacts and reported
|
|
76
|
+
// success on all 28, so a version bump left 38 artifacts recording an engine
|
|
77
|
+
// that no longer existed. `artifact-check.js` (which does recurse) caught it,
|
|
78
|
+
// which is the division of labour working — but the generator lying is how the
|
|
79
|
+
// stale artifacts got there in the first place, twice (§3.1(b)).
|
|
80
|
+
function collect(a, acc) {
|
|
81
|
+
const st = fs.statSync(a);
|
|
82
|
+
if (!st.isDirectory()) { acc.push(a); return acc; }
|
|
83
|
+
for (const name of fs.readdirSync(a).sort()) {
|
|
84
|
+
if (name === 'node_modules' || name === '.git') continue;
|
|
85
|
+
const p = path.join(a, name);
|
|
86
|
+
if (fs.statSync(p).isDirectory()) collect(p, acc);
|
|
87
|
+
else if (name.endsWith('.fd')) acc.push(p);
|
|
88
|
+
}
|
|
89
|
+
return acc;
|
|
90
|
+
}
|
|
91
|
+
|
|
64
92
|
const engine = loadEngine();
|
|
65
|
-
let
|
|
93
|
+
let built = 0;
|
|
94
|
+
let failed = 0;
|
|
66
95
|
for (const a of args) {
|
|
67
|
-
const
|
|
68
|
-
const files = st.isDirectory()
|
|
69
|
-
? fs.readdirSync(a).filter(f => f.endsWith('.fd')).sort().map(f => path.join(a, f))
|
|
70
|
-
: [a];
|
|
71
|
-
for (const f of files) if (!buildOne(engine, f)) fail = true;
|
|
96
|
+
for (const f of collect(a, [])) { if (buildOne(engine, f)) built++; else failed++; }
|
|
72
97
|
}
|
|
73
|
-
|
|
98
|
+
// State the count. §3.1(d): coverage is verified as a number, not assumed —
|
|
99
|
+
// compare against `find <dir> -name '*.fd' | wc -l`.
|
|
100
|
+
// `built` counts artifacts WRITTEN, never files attempted: a file whose source
|
|
101
|
+
// does not parse writes nothing, and counting it as built made the summary line
|
|
102
|
+
// contradict the errors printed above it and the exit code below it — in CI
|
|
103
|
+
// logs, and in the write->validate->fix loop this message exists to serve.
|
|
104
|
+
// A failure is therefore reported as its own number, and the two together are
|
|
105
|
+
// the file count to compare against.
|
|
106
|
+
console.log('built ' + built + ' artifact(s) from ' + args.length + ' path argument(s)'
|
|
107
|
+
+ (failed ? ' — ' + failed + ' of ' + (built + failed) + ' .fd file(s) FAILED and wrote nothing' : ''));
|
|
108
|
+
process.exit(failed ? 1 : 0);
|