figdown 0.5.0 → 0.5.1
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/README.md +1 -1
- package/dist/figdown.js +339 -29
- package/dist/figdown.mjs +339 -29
- package/examples/evpn-fabric.svg +1 -1
- package/examples/showcase/arp-resolution.svg +1 -1
- package/examples/showcase/ethernet-frame.svg +1 -1
- package/examples/showcase/l2-forwarding-logic.svg +1 -1
- package/examples/showcase/tcp-handshake.svg +1 -1
- package/examples/showcase/tcp-header.svg +1 -1
- package/examples/showcase/tcp-state-machine.svg +1 -1
- package/package.json +10 -1
- package/skill/figdown/build-svg.js +17 -4
- package/skill/figdown/figdown.html +480 -60
package/dist/figdown.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// figdown.mjs — FigDown embeddable library (0.5.
|
|
1
|
+
// figdown.mjs — FigDown embeddable library (0.5.1)
|
|
2
2
|
// GENERATED FILE, DO NOT EDIT. Built from editor/figdown.html.
|
|
3
3
|
// Regenerate with: node tools/make-lib.js
|
|
4
4
|
'use strict';
|
|
5
|
-
var VERSION = "0.5.
|
|
5
|
+
var VERSION = "0.5.1";
|
|
6
6
|
|
|
7
7
|
// ---- engine (extracted verbatim from editor/figdown.html) ----
|
|
8
8
|
var __engine = (function () {
|
|
@@ -16,14 +16,14 @@ const SHAPES = ['box','rounded','circle','ellipse','diamond','cylinder'];
|
|
|
16
16
|
// input to that promise, and under core §13 a 0.x renderer may differ from
|
|
17
17
|
// the next — which makes the recorded version the only thing that can
|
|
18
18
|
// explain a diff between two renderings of one source.
|
|
19
|
-
const FIGDOWN_VERSION = '0.5.
|
|
19
|
+
const FIGDOWN_VERSION = '0.5.1';
|
|
20
20
|
// `TYPED-BLOCK-TITLE-CANVAS`: a `Z`-only dev bump — RENDERER ONLY, no keyword, no
|
|
21
21
|
// option key, no model field, so `figdown 0.5` still names one language. A
|
|
22
22
|
// typed block's own title (bitfield/table/timing/chart) now joins the
|
|
23
23
|
// section's canvas computation: the returned width is the union of the data
|
|
24
24
|
// extent and the title extent (`typedBlockTitleW`, shared by all four
|
|
25
25
|
// renderers), so a title wider than the data widens the canvas instead of
|
|
26
|
-
// running off it at x=0. Fixes backlog item 66
|
|
26
|
+
// running off it at x=0. Fixes backlog item 66, raised downstream. Corpus impact
|
|
27
27
|
// surveyed first: zero shipped drawings change (every typed-block title in
|
|
28
28
|
// the corpus already fit its data-derived canvas); the fix only reaches
|
|
29
29
|
// fixtures the corpus does not yet have. See spec/migrations.md.
|
|
@@ -1290,6 +1290,35 @@ const WORD_WHY={
|
|
|
1290
1290
|
const WRONG_WORD=(surf,want,genre)=>
|
|
1291
1291
|
'"'+surf+'" is not the word genre '+genre+' uses for this — write "'+want+'": '+WORD_WHY[want]+
|
|
1292
1292
|
'. Each genre takes the term its own domain uses (block/topology `node` `edge`, flowchart `node` `flowline`, statechart `state` `transition`, sequence `lifeline` `message`) — run tools/migrate-figdown.js to rewrite it (MIGRATIONS 0.2)';
|
|
1293
|
+
// `LANGUAGE-EXTENSION-POLICY`/`RESERVED-PREFIX-ENFORCEMENT` (2026-08-23): the `x-` prefix is RESERVED against FigDown's own
|
|
1294
|
+
// vocabulary and is NOT an extension namespace. Until this entry it was the
|
|
1295
|
+
// one reservation core §10 wrote down by name and NOTHING enforced: `x-note`
|
|
1296
|
+
// earned `"x-note" is not allowed in genre block` — the same words a typo
|
|
1297
|
+
// gets — while `page`, `;` and `step` each got a diagnostic naming their
|
|
1298
|
+
// reservation. `SEMICOLON-STATUS`'s own title says a reservation that is not enforced is not
|
|
1299
|
+
// a reservation, so this is what makes `x-` one.
|
|
1300
|
+
//
|
|
1301
|
+
// The message says three things, and the THIRD is the whole value: `LANGUAGE-EXTENSION-POLICY`
|
|
1302
|
+
// RETIRED the promise of a future extension mechanism, so an author who reads
|
|
1303
|
+
// core §10's pre-0.5 wording is being told the opposite of the rule. It must
|
|
1304
|
+
// say the prefix will not open, not merely that it is taken.
|
|
1305
|
+
//
|
|
1306
|
+
// IT MUST NOT REACH IDENTIFIERS. The reservation is about the language's own
|
|
1307
|
+
// vocabulary; an id is the author's. `node x-foo "A"` and `class x-bar
|
|
1308
|
+
// "meaning"` parsed and rendered before this entry and still do, which is why
|
|
1309
|
+
// both call sites below are in KEYWORD position and OPTION-KEY position and
|
|
1310
|
+
// nowhere else.
|
|
1311
|
+
const X_RESERVED_WHY =
|
|
1312
|
+
': the "x-" prefix is reserved against FigDown\'s OWN vocabulary — no standard '+
|
|
1313
|
+
'keyword or option key may begin "x-" — and it is NOT an extension namespace. '+
|
|
1314
|
+
'The language is CLOSED, so this does not become legal by waiting (core §10, `LANGUAGE-EXTENSION-POLICY`). '+
|
|
1315
|
+
'Record the fact in a publication manifest instead, spelled x-<owner>-<key> '+
|
|
1316
|
+
'(spec/figdown-manifest.md §2.1); propose the construct through .github/CONTRIBUTING.md §2; '+
|
|
1317
|
+
'or fork with your own header token. Identifiers are untouched: node x-foo "A" and '+
|
|
1318
|
+
'class x-bar "meaning" both parse';
|
|
1319
|
+
const X_RESERVED_KW = kw => '"'+kw+'" is RESERVED, not an extension keyword'+X_RESERVED_WHY;
|
|
1320
|
+
const X_RESERVED_OPT = k => 'option "'+k+'=" is RESERVED, not an extension option key'+X_RESERVED_WHY;
|
|
1321
|
+
const isXReserved = s => typeof s==='string' && s.slice(0,2)==='x-';
|
|
1293
1322
|
// `SCENE-KEYWORD-MEMBERSHIP`: a word WITHDRAWN FROM ONE GENRE is not an unknown word,
|
|
1294
1323
|
// and `"threshold" is not allowed in genre topology` would send an author
|
|
1295
1324
|
// looking for a typo. Each cell below was legal until 0.3 and states
|
|
@@ -1929,7 +1958,12 @@ function parseOne(text){
|
|
|
1929
1958
|
// `message` is scanned by this function and never reaches that one.
|
|
1930
1959
|
if(u2.length){
|
|
1931
1960
|
const ro=(doc.genre&&REFUSED_OPT_IN[doc.genre])||null;
|
|
1932
|
-
|
|
1961
|
+
// `RESERVED-PREFIX-ENFORCEMENT` (2026-08-23): CALL SITE 2a of 2 — the reserved `x-` prefix in
|
|
1962
|
+
// OPTION-KEY position, on the connector path. `message` is scanned by
|
|
1963
|
+
// this function and never reaches `badOpts`, which is why the check has
|
|
1964
|
+
// to exist in both places (the same reason `UNDELIVERED-MESSAGE-MARKING`'s genre refusal does).
|
|
1965
|
+
if(isXReserved(u2[0])) err(n,X_RESERVED_OPT(u2[0]));
|
|
1966
|
+
else if(ro && ro[u2[0]]!==undefined) err(n,REFUSED_OPT_IN_GENRE(u2[0],doc.genre));
|
|
1933
1967
|
else err(n,'unknown option "'+u2[0]+'="');
|
|
1934
1968
|
return; }
|
|
1935
1969
|
// `SEQUENCE-GENRE-VOCABULARY`: `message` adds ONE trailing positional — the
|
|
@@ -2270,7 +2304,12 @@ function parseOne(text){
|
|
|
2270
2304
|
// ground and the replacement spelling instead of a spellcheck.
|
|
2271
2305
|
const roOpt=(doc.genre&&REFUSED_OPT_IN[doc.genre])||null;
|
|
2272
2306
|
for(const u of unk){
|
|
2273
|
-
|
|
2307
|
+
// `RESERVED-PREFIX-ENFORCEMENT` (2026-08-23): CALL SITE 2b of 2 — the reserved `x-` prefix in
|
|
2308
|
+
// OPTION-KEY position, on the ordinary directive path. Checked before
|
|
2309
|
+
// the genre refusal and before the generic message, because a key the
|
|
2310
|
+
// language will never register is a stronger fact than either.
|
|
2311
|
+
if(isXReserved(u)) err(n,X_RESERVED_OPT(u));
|
|
2312
|
+
else if(roOpt && roOpt[u]!==undefined) err(n,REFUSED_OPT_IN_GENRE(u,doc.genre));
|
|
2274
2313
|
else err(n,'unknown option "'+u+'="');
|
|
2275
2314
|
bad=true; }
|
|
2276
2315
|
// `MEMBERSHIP-KEY-ACCEPTANCE`: the PER-GENRE option-key withdrawal, checked here —
|
|
@@ -2452,6 +2491,18 @@ function parseOne(text){
|
|
|
2452
2491
|
} else { err(n,'first line must be "figdown 0.1 <genre>"'); }
|
|
2453
2492
|
} else if(kw==='figdown'){ err(n,'duplicate version header'); continue; }
|
|
2454
2493
|
|
|
2494
|
+
// `RESERVED-PREFIX-ENFORCEMENT` (2026-08-23): CALL SITE 1 of 2 — the reserved `x-` prefix in
|
|
2495
|
+
// KEYWORD position. It sits here, above every other keyword answer, for
|
|
2496
|
+
// two reasons: no registered keyword begins `x-`, so it can pre-empt
|
|
2497
|
+
// nothing legitimate; and every downstream answer this would otherwise
|
|
2498
|
+
// reach (`not allowed in genre <g>`, `not valid inside <block>`,
|
|
2499
|
+
// `unrecognized line`) is a message about a typo, which is what §8.4 of
|
|
2500
|
+
// decisions/registry.md measured and what this replaces.
|
|
2501
|
+
// It sits BELOW the first-line header check deliberately: "first line
|
|
2502
|
+
// must be figdown …" is the more important thing to tell an author, and
|
|
2503
|
+
// an `x-` first line reports both, exactly as it reported two before.
|
|
2504
|
+
if(isXReserved(kw)){ err(n,X_RESERVED_KW(kw)); continue; }
|
|
2505
|
+
|
|
2455
2506
|
// Retired spelling: `colw` → `width`. Keyword naming
|
|
2456
2507
|
// discipline — one lowercase word, borrowed standard terminology; `colw`
|
|
2457
2508
|
// was the only invented abbreviation in the registry. Reported wherever
|
|
@@ -3922,6 +3973,163 @@ function stackSectionSvgs(results){
|
|
|
3922
3973
|
+chunks.join('')+'</svg>';
|
|
3923
3974
|
}
|
|
3924
3975
|
|
|
3976
|
+
// ---- the accessibility profile's emission (spec/figdown-a11y.md) ----------
|
|
3977
|
+
// `ACCESSIBILITY-PROFILE`/`ACCESSIBLE-DESCRIPTION-SOURCES`/`ACCESSIBLE-TEXT-EMISSION` (decisions/registry.md).
|
|
3978
|
+
// The publication profile wants three things in a published artifact: a role,
|
|
3979
|
+
// a NON-VISUAL name, and a description whose review state a machine can read.
|
|
3980
|
+
// None of it is emitted by default and none of it is a language construct —
|
|
3981
|
+
// the `with-a11y` RENDER OPTION (core §7, `ACCESSIBLE-TEXT-EMISSION` route 1) is the only way in,
|
|
3982
|
+
// exactly as `with-title` (`TITLE-RENDER-DEFAULT`) is the only way to draw the title as ink.
|
|
3983
|
+
//
|
|
3984
|
+
// WHY THIS SITS OUTSIDE `render` AND NOT INSIDE IT. Two reasons, and the
|
|
3985
|
+
// second is the load-bearing one:
|
|
3986
|
+
// 1. Core §7/`RENDERING-DETERMINISM` make an artifact a pure function of (source, recorded
|
|
3987
|
+
// options) and promise byte-identical output for one engine version.
|
|
3988
|
+
// Code that never runs cannot move a byte, and `render` is left exactly
|
|
3989
|
+
// as it was so that every existing artifact and golden is untouched.
|
|
3990
|
+
// 2. `ACCESSIBILITY-PROFILE` rules ONE artifact, ONE root, ONE name — the FIRST section's
|
|
3991
|
+
// `title`. A multi-section artifact's root is built by
|
|
3992
|
+
// `stackSectionSvgs` from N per-section renders, so a `<title>` emitted
|
|
3993
|
+
// inside `render` would land inside a `<g>` (naming the group, per `DESCRIPTION-KEY-SPELLING`
|
|
3994
|
+
// and SVG 1.1 §5.4) and there would be N of them. The name belongs to
|
|
3995
|
+
// the finished root, which only the artifact layer holds.
|
|
3996
|
+
// So the artifact layer (tools/build-svg.js, dist/figdown.js's `artifact()`)
|
|
3997
|
+
// calls `a11yApply(svg, docs[0])` on the finished root and records
|
|
3998
|
+
// `with-a11y` in `data-render-options`.
|
|
3999
|
+
const A11Y_ROLE = 'graphics-document';
|
|
4000
|
+
|
|
4001
|
+
// `ACCESSIBILITY-PROFILE` item 3: the accessible name is the document's `title` string,
|
|
4002
|
+
// UNMODIFIED — not truncated, not prefixed, never invented. `title` is
|
|
4003
|
+
// optional in the grammar and stays optional (`ACCESSIBILITY-PROFILE` is explicit that the
|
|
4004
|
+
// untitled figure still parses and still renders): a document with no
|
|
4005
|
+
// `title` line gets NO root `<title>`, and whether such a figure may be
|
|
4006
|
+
// PUBLISHED is the profile's question and the verifier's, never the
|
|
4007
|
+
// engine's. The test is ABSENCE, not truthiness (`EMPTY-LABEL-STATE`): `title ""` is a
|
|
4008
|
+
// written empty name and is emitted as one.
|
|
4009
|
+
function a11yTitleOf(doc){
|
|
4010
|
+
return (doc && doc.title!==null && doc.title!==undefined) ? String(doc.title) : null;
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
// ---- the derived description (`ACCESSIBLE-DESCRIPTION-SOURCES` item 7) --------------------------------
|
|
4014
|
+
// A `derived` description is a deterministic projection of the MODEL into
|
|
4015
|
+
// prose — "exactly as good as the model, and no better" — and `ACCESSIBLE-DESCRIPTION-SOURCES`'s
|
|
4016
|
+
// operative half is the NEGATIVE rule: it MUST NOT state what the model does
|
|
4017
|
+
// not assert. No causality, no "this shows how X works", no count the model
|
|
4018
|
+
// cannot produce. Every sentence below is an inventory statement over a
|
|
4019
|
+
// collection the model actually carries.
|
|
4020
|
+
//
|
|
4021
|
+
// COVERED GENRES ARE EXACTLY THE THREE `ACCESSIBLE-DESCRIPTION-SOURCES` RULED CONTENT RULES FOR:
|
|
4022
|
+
// `topology`, `bitfield`, `sequence`. Every other genre the engine supports
|
|
4023
|
+
// (`block`, `flowchart`, `statechart`, `table`, `timing`, and the
|
|
4024
|
+
// experimental `chart` region) gets NO derived description — the state stays
|
|
4025
|
+
// `absent` — because inventing content rules for them here would be design,
|
|
4026
|
+
// not implementation. That boundary is stated, not silent: spec/figdown-a11y.md
|
|
4027
|
+
// §4.4 records which genres are covered, which are not, and why.
|
|
4028
|
+
const A11Y_DERIVED_GENRES = ['topology','bitfield','sequence'];
|
|
4029
|
+
function a11yQ(s){ return '"'+String(s)+'"'; }
|
|
4030
|
+
function a11yCount(n,word){ return n+' '+word+(n===1?'':'s'); }
|
|
4031
|
+
// id, plus the label the model carries WHEN it carries one. The renderer's
|
|
4032
|
+
// id-for-absent-label fallback (`OMITTED-LABEL-RECORDING`) is a DISPLAY rule and deliberately does
|
|
4033
|
+
// not run here: the model records absence, and a derived description that
|
|
4034
|
+
// silently promoted an id to a name would assert a name nobody wrote.
|
|
4035
|
+
function a11yNamed(x){
|
|
4036
|
+
return (x.label===null||x.label===undefined) ? String(x.id) : String(x.id)+' '+a11yQ(x.label);
|
|
4037
|
+
}
|
|
4038
|
+
function a11yDeriveTopology(doc){
|
|
4039
|
+
const s=[];
|
|
4040
|
+
s.push('Topology figure'+(a11yTitleOf(doc)!==null?' '+a11yQ(doc.title):'')+'.');
|
|
4041
|
+
s.push(a11yCount(doc.nodes.length,'node')+', '+a11yCount(doc.edges.length,'connector')
|
|
4042
|
+
+', '+a11yCount(doc.groups.length,'group')+'.');
|
|
4043
|
+
s.push('Flow direction: '+String(doc.flow)+'.');
|
|
4044
|
+
if(doc.nodes.length) s.push('Nodes: '+doc.nodes.map(a11yNamed).join(', ')+'.');
|
|
4045
|
+
for(const g of doc.groups){
|
|
4046
|
+
const mem=doc.nodes.filter(n=>n.group===g.id).map(n=>String(n.id));
|
|
4047
|
+
s.push('Group '+a11yNamed(g)+(mem.length?' contains: '+mem.join(', ')+'.':' has no declared members.'));
|
|
4048
|
+
}
|
|
4049
|
+
// A class's MEANING is `label` on the engine object (the canonical
|
|
4050
|
+
// projection renames it `meaning`). `ACCESSIBLE-DESCRIPTION-SOURCES` says "each declared class and its
|
|
4051
|
+
// stated meaning (which the legend already prints)", so the legend's own
|
|
4052
|
+
// filter is inherited rather than re-invented: `class x ""` is an attribute
|
|
4053
|
+
// grouping that states no meaning, it prints no legend entry, and it makes
|
|
4054
|
+
// no sentence here either.
|
|
4055
|
+
for(const c of (doc.classes||[])) if(c.label!=='')
|
|
4056
|
+
s.push('Class '+String(c.id)+' means '+a11yQ(c.label)+'.');
|
|
4057
|
+
return s.join(' ');
|
|
4058
|
+
}
|
|
4059
|
+
function a11yDeriveBitfield(doc){
|
|
4060
|
+
const s=[];
|
|
4061
|
+
s.push('Bitfield figure'+(a11yTitleOf(doc)!==null?' '+a11yQ(doc.title):'')+'.');
|
|
4062
|
+
const blocks=(doc.blocks||[]).filter(b=>b.type==='bitfield');
|
|
4063
|
+
if(!blocks.length) return null;
|
|
4064
|
+
for(const b of blocks){
|
|
4065
|
+
s.push('Bitfield '+a11yNamed(b)+': word '+a11yCount(b.word,'bit')
|
|
4066
|
+
+', numbering '+String(b.numbering)+'.');
|
|
4067
|
+
// `wrap` entries are ROW BREAKS (projected as `break`), a division of the
|
|
4068
|
+
// drawing and not a field; `ACCESSIBLE-DESCRIPTION-SOURCES`'s rule is "fields in order with widths".
|
|
4069
|
+
// A field's width is `w` on the engine object: a bit count, or the string
|
|
4070
|
+
// `*` for the remainder of the row.
|
|
4071
|
+
const fields=(b.fields||[]).filter(f=>!f.wrap);
|
|
4072
|
+
if(!fields.length){ s.push('No fields declared.'); continue; }
|
|
4073
|
+
s.push('Fields in order: '+fields.map(f=>
|
|
4074
|
+
a11yQ(f.name)+' '+(String(f.w)==='*'?'width *':a11yCount(f.w,'bit'))).join(', ')+'.');
|
|
4075
|
+
}
|
|
4076
|
+
return s.join(' ');
|
|
4077
|
+
}
|
|
4078
|
+
function a11yDeriveSequence(doc){
|
|
4079
|
+
const s=[];
|
|
4080
|
+
s.push('Sequence figure'+(a11yTitleOf(doc)!==null?' '+a11yQ(doc.title):'')+'.');
|
|
4081
|
+
const lls=doc.lifelines||[], msgs=doc.messages||[], frags=doc.fragments||[];
|
|
4082
|
+
s.push('Lifelines in declaration order: '+(lls.length?lls.map(a11yNamed).join(', '):'none')+'.');
|
|
4083
|
+
// The time axis is declaration order and it is TOTAL (`SEQUENCE-GENRE-VOCABULARY`, draft §31),
|
|
4084
|
+
// so "in their stated total order" is `line` ascending — the model's own
|
|
4085
|
+
// ordering key, not a re-reading of the picture.
|
|
4086
|
+
const ordered=msgs.slice().sort((p,q)=>p.line-q.line);
|
|
4087
|
+
s.push('Messages in stated order: '+(ordered.length?ordered.map((m,i)=>
|
|
4088
|
+
(i+1)+'. '+String(m.a)+' '+String(m.op)+' '+String(m.b)
|
|
4089
|
+
+(m.label===null||m.label===undefined?'':' '+a11yQ(m.label))).join('; '):'none')+'.');
|
|
4090
|
+
if(frags.length) s.push('Fragments: '+frags.map(f=>
|
|
4091
|
+
a11yNamed(f)+' (type '+String(f.type)+')').join(', ')+'.');
|
|
4092
|
+
return s.join(' ');
|
|
4093
|
+
}
|
|
4094
|
+
function a11yDerivedDesc(doc){
|
|
4095
|
+
if(!doc||A11Y_DERIVED_GENRES.indexOf(doc.genre)<0) return null;
|
|
4096
|
+
if(doc.genre==='topology') return a11yDeriveTopology(doc);
|
|
4097
|
+
if(doc.genre==='bitfield') return a11yDeriveBitfield(doc);
|
|
4098
|
+
if(doc.genre==='sequence') return a11yDeriveSequence(doc);
|
|
4099
|
+
return null; // state `absent` — an uncovered genre, spec/figdown-a11y.md §4.4
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
// Apply the profile to a FINISHED root `<svg>`: the role on the root, the
|
|
4103
|
+
// non-visual `<title>` as its FIRST child (SVG 1.1 §5.4 / `DESCRIPTION-KEY-SPELLING` — a `<title>`
|
|
4104
|
+
// names its parent and belongs first), then the `<desc>` carrying the derived
|
|
4105
|
+
// description with its state in a `data-*` attribute (`ACCESSIBLE-DESCRIPTION-SOURCES` item 6, way 1; the
|
|
4106
|
+
// manifest carries the evidence, way 3). Idempotent: a root that already
|
|
4107
|
+
// declares a role is returned unchanged.
|
|
4108
|
+
function a11yApply(svg, doc){
|
|
4109
|
+
const s=String(svg);
|
|
4110
|
+
const m=/^<svg\b[^>]*>/.exec(s);
|
|
4111
|
+
if(!m) return s;
|
|
4112
|
+
if(/\brole="/.test(m[0])) return s;
|
|
4113
|
+
const parts=[];
|
|
4114
|
+
const t=a11yTitleOf(doc);
|
|
4115
|
+
if(t!==null) parts.push('<title>'+esc(t)+'</title>');
|
|
4116
|
+
const d=a11yDerivedDesc(doc);
|
|
4117
|
+
if(d!==null&&d!=='') parts.push('<desc data-desc-state="derived">'+esc(d)+'</desc>');
|
|
4118
|
+
return m[0].replace(/>$/,' role="'+A11Y_ROLE+'">')+parts.join('')+s.slice(m[0].length);
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
// The `data-render-options` value for a render (core §7). ONE spelling, in
|
|
4122
|
+
// ONE place, so the artifact layers cannot drift: options in declaration
|
|
4123
|
+
// order, space-separated — the SVG/HTML idiom for a token list. A default
|
|
4124
|
+
// render writes no attribute at all, which is why the empty string is
|
|
4125
|
+
// returned rather than an empty attribute.
|
|
4126
|
+
function renderOptionsAttr(opts){
|
|
4127
|
+
const names=[];
|
|
4128
|
+
if(opts&&opts.title===true) names.push('with-title');
|
|
4129
|
+
if(opts&&opts.a11y===true) names.push('with-a11y');
|
|
4130
|
+
return names.length?' data-render-options="'+names.join(' ')+'"':'';
|
|
4131
|
+
}
|
|
4132
|
+
|
|
3925
4133
|
// `SEMICOLON-STATUS`: `;` is RESERVED for a future statement separator, and
|
|
3926
4134
|
// RULE 1.3 says a reserved mark MUST NOT be given any other meaning. Until
|
|
3927
4135
|
// this release it was an ordinary character: `node a ;` parsed and `;`
|
|
@@ -3998,7 +4206,7 @@ function findComment(s){
|
|
|
3998
4206
|
// ============================================================
|
|
3999
4207
|
const FONT=13, CH=7.2, PADX=14, NH=36, GAPX=56, GAPY=44;
|
|
4000
4208
|
|
|
4001
|
-
// ---- cw(): script-aware advance width --------------------------------
|
|
4209
|
+
// ---- cw(): script-aware advance width (`TEXT-ADVANCE-MEASUREMENT`) --------------------------------
|
|
4002
4210
|
// Every px-per-character constant in this file — CH here, and its siblings at
|
|
4003
4211
|
// the other font sizes (8.6 title, 6.6 legend, 6.5 edge label, 6.3/6.2 bitfield
|
|
4004
4212
|
// caption) — is calibrated on LATIN, where one character advances ~0.554 em.
|
|
@@ -4569,15 +4777,80 @@ function render(doc,ropts){
|
|
|
4569
4777
|
// Retiring `color=` removes the first case; omitting the swatch removes
|
|
4570
4778
|
// the second. Every channel a class can still declare (`fill`, `stroke`,
|
|
4571
4779
|
// `style`) is drawn, so "declared but not shown" is now unreachable.
|
|
4780
|
+
// 0.5 (`LEGEND-SWATCH-SHAPE`): the swatch depicts THE CHANNEL THE CLASS ACTUALLY
|
|
4781
|
+
// PAINTS. `CLASS-PAINT-REQUIREMENT` ruled WHETHER a swatch is drawn and never WHAT SHAPE it
|
|
4782
|
+
// takes, so every painting class got a `<rect>` — and a class that
|
|
4783
|
+
// declares only `stroke=` and is carried only by CONNECTORS then appeared
|
|
4784
|
+
// as a white box with a coloured outline while the drawing showed a
|
|
4785
|
+
// coloured LINE. The legend's visual vocabulary did not match the
|
|
4786
|
+
// figure's, and with `style=dashed` the dash landed on a rectangle's
|
|
4787
|
+
// perimeter instead of along a run. Measured on 2026-08-26: 36 of this
|
|
4788
|
+
// repository's 76 legend-bearing class-uses are connector-only (33 of them
|
|
4789
|
+
// stroke-without-fill) and 10 figures are entirely connector-only; in the
|
|
4790
|
+
// production corpus behind the field reports, 160 of 1350 legend-bearing
|
|
4791
|
+
// class-uses are connector-only and 159 of those are stroke-without-fill.
|
|
4792
|
+
//
|
|
4793
|
+
// WHICH KINDS ARE CONNECTORS is read off the model, not guessed and not
|
|
4794
|
+
// stored: the four connector spellings (`edge` `flowline` `transition`
|
|
4795
|
+
// `message`, core §10) are exactly `doc.edges` and `doc.messages` in the
|
|
4796
|
+
// model — every other collection that carries `class=` is a box, a frame
|
|
4797
|
+
// or a cell, i.e. something with an area. This adds no model field, no
|
|
4798
|
+
// option key and no spelling; it is a byte-moving RENDER change.
|
|
4799
|
+
const clsUse={};
|
|
4800
|
+
{ const use=(x,conn)=>{
|
|
4801
|
+
const ids=(x.cls===undefined||x.cls===null)?[]:(Array.isArray(x.cls)?x.cls:[x.cls]);
|
|
4802
|
+
for(const id of ids){ const u=clsUse[id]||(clsUse[id]={conn:false,box:false});
|
|
4803
|
+
if(conn) u.conn=true; else u.box=true; } };
|
|
4804
|
+
for(const x of doc.nodes) use(x,false);
|
|
4805
|
+
for(const x of doc.groups) use(x,false);
|
|
4806
|
+
for(const x of doc.edges) use(x,true);
|
|
4807
|
+
for(const x of (doc.messages||[])) use(x,true);
|
|
4808
|
+
for(const x of (doc.lifelines||[])) use(x,false);
|
|
4809
|
+
for(const x of (doc.states||[])) use(x,false);
|
|
4810
|
+
for(const x of (doc.fragments||[])) use(x,false);
|
|
4811
|
+
for(const x of (doc.operands||[])) use(x,false);
|
|
4812
|
+
for(const b of doc.blocks){ use(b,false);
|
|
4813
|
+
if(b.fields) for(const f of b.fields) use(f,false);
|
|
4814
|
+
if(b.marks) for(const mk of b.marks) use(mk,false); } }
|
|
4572
4815
|
for(const c of legendCls){
|
|
4573
4816
|
const paints=c.fill!==undefined||c.stroke!==undefined||c.style!==undefined;
|
|
4574
|
-
|
|
4575
|
-
|
|
4817
|
+
// The samples this class earns, in the ruled order: LINE then BOX.
|
|
4818
|
+
// · carried only by connectors → a line sample (`LEGEND-SWATCH-SHAPE` clause 1)
|
|
4819
|
+
// · carried only by boxes → the `<rect>` `CLASS-PAINT-REQUIREMENT` already drew (2)
|
|
4820
|
+
// · carried by both → both samples, side by side (3): the
|
|
4821
|
+
// legend exists to explain, and the extra width is cheaper than
|
|
4822
|
+
// making a reader guess which half of the figure an entry is about.
|
|
4823
|
+
// · declares `fill=` → a box sample WHATEVER it is carried
|
|
4824
|
+
// by (4), because a fill needs an area to be visible. A box on a
|
|
4825
|
+
// connector-only class is then the class declaring a channel its
|
|
4826
|
+
// elements cannot show — `CLASS-CHANNEL-COLLISION`'s territory, reported there, not
|
|
4827
|
+
// re-ruled here (and `gate:legend`'s unreachable-channel check
|
|
4828
|
+
// already refuses that shape in this repository's corpus).
|
|
4829
|
+
// · declares no paint → no sample at all (5, `CLASS-PAINT-REQUIREMENT` unchanged).
|
|
4830
|
+
// · declared but never carried → the shape comes from the DECLARED
|
|
4831
|
+
// channels alone: `fill` → box, stroke-only → line. Nothing else is
|
|
4832
|
+
// known about it, and its own paint is the only evidence there is.
|
|
4833
|
+
const u=clsUse[c.id];
|
|
4834
|
+
const samples=[];
|
|
4576
4835
|
if(paints){
|
|
4577
|
-
|
|
4578
|
-
|
|
4836
|
+
if(u ? u.conn : c.fill===undefined) samples.push('line');
|
|
4837
|
+
if(u ? (u.box||c.fill!==undefined) : c.fill!==undefined) samples.push('box');
|
|
4579
4838
|
}
|
|
4580
|
-
|
|
4839
|
+
const tw=cw(c.label)*6.6+9+21*samples.length;
|
|
4840
|
+
if(lx>0 && lx+tw>wrapW){ lx=0; ly+=rowH; }
|
|
4841
|
+
const dash=c.style==='dashed'?' stroke-dasharray="6 4"':(c.style==='dotted'?' stroke-dasharray="2 4"':'');
|
|
4842
|
+
let sx=lx;
|
|
4843
|
+
for(const s of samples){
|
|
4844
|
+
// Both samples take the SAME 16 px advance, so a mixed entry is two
|
|
4845
|
+
// swatches and not a wider one. The line is drawn at the rect's own
|
|
4846
|
+
// vertical centre (3 + 11/2) and at 1.6 — the stroke-width every
|
|
4847
|
+
// router in this engine gives a drawn connector — so the sample is
|
|
4848
|
+
// the same ink the figure lays down, at the same weight.
|
|
4849
|
+
if(s==='line') es.push('<line x1="'+sx+'" y1="'+(ly+8.5)+'" x2="'+(sx+16)+'" y2="'+(ly+8.5)+'" stroke="'+(c.stroke||'#555')+'" stroke-width="1.6"'+dash+'/>');
|
|
4850
|
+
else es.push('<rect x="'+sx+'" y="'+(ly+3)+'" width="16" height="11" fill="'+(c.fill||'#fff')+'" stroke="'+(c.stroke||'#555')+'"'+dash+'/>');
|
|
4851
|
+
sx+=21;
|
|
4852
|
+
}
|
|
4853
|
+
es.push('<text x="'+(lx+21*samples.length)+'" y="'+(ly+12.5)+'" font-size="11" fill="#1d1d1b">'+esc(c.label)+'</text>');
|
|
4581
4854
|
lx+=tw+14; maxW=Math.max(maxW,lx);
|
|
4582
4855
|
}
|
|
4583
4856
|
parts.push(es.join(''));
|
|
@@ -6506,7 +6779,18 @@ function renderScene(doc,y0){
|
|
|
6506
6779
|
// node box, which is already an obstacle.
|
|
6507
6780
|
//
|
|
6508
6781
|
// An external's label is NOT an obstacle to its OWN edge — that edge must
|
|
6509
|
-
// reach the anchor the label names.
|
|
6782
|
+
// reach the anchor the label names. A GROUP's name is the same rule one
|
|
6783
|
+
// construct over, and 0.4 shipped only half of it (`GROUP-BOUNDARY-OBSTACLE`): a group's
|
|
6784
|
+
// name strip runs the full width of the band's TOP, so it lies across every
|
|
6785
|
+
// approach an outside node has to a member inside. Made an obstacle to all
|
|
6786
|
+
// comers, it turned every boundary-crossing edge into a detour that left the
|
|
6787
|
+
// corridor, ran down the band's outer edge and entered the member from the
|
|
6788
|
+
// side — the drawing then said the line arrives at the CONTAINER when the
|
|
6789
|
+
// source says it arrives at the MEMBER. `EDGE-BEND-RETENTION` had already ruled the group
|
|
6790
|
+
// BOX is an obstacle only to an edge that "neither starts nor ends inside";
|
|
6791
|
+
// the name is part of the same band and takes the same exemption. A foreign
|
|
6792
|
+
// shaft — neither endpoint in the group — is still detoured, which is the
|
|
6793
|
+
// whole of what 0.4 was right about.
|
|
6510
6794
|
const extLbl=n=>{
|
|
6511
6795
|
const cx=n.x+n.w/2, cy=n.y+n.h/2, [bdx,bdy]=bDir(n);
|
|
6512
6796
|
const bw=lblPx(n.label), bl=String(n.label).split('\n').length, bh=13*bl;
|
|
@@ -6521,18 +6805,24 @@ function renderScene(doc,y0){
|
|
|
6521
6805
|
for(const k in gBox){ const g=doc.groups.find(z=>z.id===k);
|
|
6522
6806
|
if(!g||!g.label) continue;
|
|
6523
6807
|
const B=gBox[k];
|
|
6524
|
-
nameObs.push({x:B.x0+10, y:B.yA+16-11.5*0.85, w:cwMax(g.label)*6.5*11.5/11, h:11.5*1.1});
|
|
6808
|
+
nameObs.push({x:B.x0+10, y:B.yA+16-11.5*0.85, w:cwMax(g.label)*6.5*11.5/11, h:11.5*1.1, id:k});
|
|
6525
6809
|
}
|
|
6526
|
-
//
|
|
6527
|
-
|
|
6810
|
+
// Which group an endpoint id belongs to, for the name-strip exemption below.
|
|
6811
|
+
const grpOfId=new Map();
|
|
6812
|
+
for(const n of nodes) if(n.group) grpOfId.set(n.id,n.group);
|
|
6813
|
+
// The obstacle list a given edge must respect: every band name except the
|
|
6814
|
+
// ones this edge terminates AT or INSIDE, plus every external label except
|
|
6815
|
+
// the ones this edge itself terminates at.
|
|
6528
6816
|
// A label's ink is its glyphs plus the clearance that keeps a line from
|
|
6529
6817
|
// READING as struck through it. 4 px on every side — the same number the
|
|
6530
6818
|
// legibility floor uses for a label's association margin — so a shaft that
|
|
6531
6819
|
// grazes a name at 3 px is detoured rather than tolerated.
|
|
6532
6820
|
const LBL_PAD=4;
|
|
6533
6821
|
const padded=o=>({x:o.x-LBL_PAD,y:o.y-LBL_PAD,w:o.w+2*LBL_PAD,h:o.h+2*LBL_PAD});
|
|
6822
|
+
const ownsName=(o,e)=>o.id===e.a||o.id===e.b||
|
|
6823
|
+
o.id===grpOfId.get(e.a)||o.id===grpOfId.get(e.b);
|
|
6534
6824
|
const lblObs=e=>extObs.filter(o=>o.id!==e.a&&o.id!==e.b).map(padded)
|
|
6535
|
-
.concat(nameObs.map(padded));
|
|
6825
|
+
.concat(nameObs.filter(o=>!ownsName(o,e)).map(padded));
|
|
6536
6826
|
const rings=new Map();
|
|
6537
6827
|
for(const t of doc.trunks||[]){ const R=ringOf(t); if(R) rings.set(t,R); }
|
|
6538
6828
|
// ── A LASSO THAT ENCLOSES A NON-MEMBER IS A FALSE DRAWING (item 69) ──────
|
|
@@ -9651,8 +9941,8 @@ function r2(v){ return Math.round(v*100)/100; }
|
|
|
9651
9941
|
// canvas grows right and down only, so text at a negative coordinate is
|
|
9652
9942
|
// CLIPPED, never merely misplaced (`LABEL-PLACEMENT-METRIC`, which fixed exactly this
|
|
9653
9943
|
// for a `table` caption and a `chart` row/column label gutter but not for a
|
|
9654
|
-
// bitfield/timing title or a chart's own top caption —
|
|
9655
|
-
//
|
|
9944
|
+
// bitfield/timing title or a chart's own top caption — reported from
|
|
9945
|
+
// downstream production authoring).
|
|
9656
9946
|
//
|
|
9657
9947
|
// ONE MEASUREMENT, used by all four renderers below, so the calibration lives
|
|
9658
9948
|
// in one place rather than four. Bold text at this size is measured ~8%
|
|
@@ -10159,9 +10449,14 @@ function renderTable(t,y0){
|
|
|
10159
10449
|
const mk=markOf(r,c);
|
|
10160
10450
|
// block fill= is the default DATA-cell fill (header tint is structural)
|
|
10161
10451
|
const fill=(mk&&mk.fill)||(cell.hdr?'#eeede6':(hlRow(r)?'#fef3c7':(t.fill||'#fff')));
|
|
10162
|
-
// addressable cells carry table-id:row:col
|
|
10163
|
-
|
|
10164
|
-
|
|
10452
|
+
// addressable cells carry table-id:row:col for EVERY tier (backlog 71):
|
|
10453
|
+
// data rows are 1..n, the bottom header tier keeps its historical `0`
|
|
10454
|
+
// spelling so existing consumers of that channel see no change, and
|
|
10455
|
+
// every tier above it is `hN` (1-indexed from the top) — the same
|
|
10456
|
+
// spelling the editor's own internal row-token model already used, so
|
|
10457
|
+
// the editor can read this channel with no translation table.
|
|
10458
|
+
const addrR = r>=H ? String(r-H+1) : (r===H-1 ? '0' : 'h'+(r+1));
|
|
10459
|
+
const addr = ' data-cell="'+t.id+':'+addrR+':'+(c+1)+'" style="cursor:pointer"';
|
|
10165
10460
|
// A merged cell owns every grid square it spans, so its internal
|
|
10166
10461
|
// boundaries have the same owner on both sides and are never drawn.
|
|
10167
10462
|
const rec={id:'c'+r+'_'+c, c:(mk&&mk.stroke)||null, d:false};
|
|
@@ -10449,7 +10744,8 @@ function renderTiming(w,y0){
|
|
|
10449
10744
|
}
|
|
10450
10745
|
|
|
10451
10746
|
// ============================================================
|
|
10452
|
-
return { parse: parse, render: render, stackSectionSvgs: stackSectionSvgs
|
|
10747
|
+
return { parse: parse, render: render, stackSectionSvgs: stackSectionSvgs,
|
|
10748
|
+
a11yApply: a11yApply, renderOptionsAttr: renderOptionsAttr };
|
|
10453
10749
|
})();
|
|
10454
10750
|
|
|
10455
10751
|
// ---- minimal synchronous SHA-256 (FIPS 180-4), hex output ----
|
|
@@ -10520,7 +10816,11 @@ function __stackSectionSvgs(results) {
|
|
|
10520
10816
|
// (determinism over convenience: no partial renders of invalid input).
|
|
10521
10817
|
// opts (presentation, renderer tier): { title: true } draws the title;
|
|
10522
10818
|
// the default does NOT (embedded figures almost always sit under the
|
|
10523
|
-
// host document's caption — the majority case).
|
|
10819
|
+
// host document's caption — the majority case). { a11y: true } adds the
|
|
10820
|
+
// accessibility profile's emission (spec/figdown-a11y.md; ACCESSIBILITY-PROFILE):
|
|
10821
|
+
// role="graphics-document" on the root, the non-visual <title> as its first
|
|
10822
|
+
// child, and a state-flagged derived <desc>. The two are ORTHOGONAL — one
|
|
10823
|
+
// decides ink, the other the accessible name — and both default to off.
|
|
10524
10824
|
// Multi-section sources are stacked vertically into a single SVG (MULTI-FIGURE-DOCUMENTS).
|
|
10525
10825
|
//
|
|
10526
10826
|
// TWO ERROR CHANNELS REACH ONE errors ARRAY. parse cannot see a
|
|
@@ -10542,17 +10842,27 @@ function render(text, opts) {
|
|
|
10542
10842
|
for (var i = 0; i < rs.length; i++) errs = errs.concat(rs[i].errs || []);
|
|
10543
10843
|
if (errs.length) return { svg: null, errors: errs };
|
|
10544
10844
|
var svg = rs.length > 1 ? __engine.stackSectionSvgs(rs) : rs[0].svg;
|
|
10545
|
-
return { svg: svg, errors: [] };
|
|
10845
|
+
return { svg: __a11y(svg, p.docs[0], opts), errors: [] };
|
|
10846
|
+
}
|
|
10847
|
+
// The accessibility profile's emission (spec/figdown-a11y.md; ACCESSIBILITY-PROFILE).
|
|
10848
|
+
// Applied to the FINISHED root, never inside a per-section render: ACCESSIBILITY-PROFILE gives
|
|
10849
|
+
// an artifact ONE root, ONE name, and the name is the FIRST section's title.
|
|
10850
|
+
// A caller that does not ask gets the byte-identical default (core §7 / RENDERING-DETERMINISM).
|
|
10851
|
+
function __a11y(svg, doc, opts) {
|
|
10852
|
+
if (!svg || !(opts && opts.a11y === true)) return svg;
|
|
10853
|
+
if (typeof __engine.a11yApply !== 'function') return svg;
|
|
10854
|
+
return __engine.a11yApply(svg, doc);
|
|
10546
10855
|
}
|
|
10547
10856
|
// renderDoc(doc, opts) -> svg string, for an already-validated doc from parse().
|
|
10548
10857
|
// For multi-section, pass parse().docs to renderDocs instead.
|
|
10549
10858
|
function renderDoc(doc, opts) {
|
|
10550
|
-
return __engine.render(doc, opts).svg;
|
|
10859
|
+
return __a11y(__engine.render(doc, opts).svg, doc, opts);
|
|
10551
10860
|
}
|
|
10552
10861
|
function renderDocs(docs, opts) {
|
|
10553
10862
|
if (!docs || !docs.length) return '';
|
|
10554
|
-
if (docs.length === 1) return __engine.render(docs[0], opts).svg;
|
|
10555
|
-
return __engine.stackSectionSvgs(docs.map(function (d) { return __engine.render(d, opts); }))
|
|
10863
|
+
if (docs.length === 1) return __a11y(__engine.render(docs[0], opts).svg, docs[0], opts);
|
|
10864
|
+
return __a11y(__engine.stackSectionSvgs(docs.map(function (d) { return __engine.render(d, opts); })),
|
|
10865
|
+
docs[0], opts);
|
|
10556
10866
|
}
|
|
10557
10867
|
// artifact(text) -> { svg, errors } svg is the full self-carrying SVG:
|
|
10558
10868
|
// the render plus a <metadata id="figdown-source"> block
|
|
@@ -10569,7 +10879,7 @@ function artifact(text, opts) {
|
|
|
10569
10879
|
// The artifact records the SHA-256 OF THE SOURCE, the ENGINE VERSION that
|
|
10570
10880
|
// rendered it, and any non-default render option (core §7) — together they
|
|
10571
10881
|
// keep third-party rebuilds bit-identical and give a diff somewhere to point
|
|
10572
|
-
var optAttr = (opts
|
|
10882
|
+
var optAttr = __engine.renderOptionsAttr(opts);
|
|
10573
10883
|
var meta = '<metadata id="figdown-source" data-sha256="' + __sha256hex(src) + '"'
|
|
10574
10884
|
+ ' data-engine-version="' + VERSION + '"' + optAttr + '><![CDATA[\n'
|
|
10575
10885
|
+ src.replace(/]]>/g, ']]]]><![CDATA[>') + '\n]]></metadata>';
|
package/examples/evpn-fabric.svg
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 527 634" width="527" height="634" font-family="system-ui,sans-serif"><g transform="translate(0,0)"><defs><marker id="s0_arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#555"/></marker><pattern id="s0_hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="6" stroke="#bbb" stroke-width="2"/></pattern></defs><g transform="translate(18,6)"><line data-edge="24" x1="157.16" y1="56" x2="96.44000000000003" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="25" x1="197.12" y1="56" x2="234.08000000000004" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="26" x1="221.2" y1="50.810457516339866" x2="371.72" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="27" x1="277.20000000000005" y1="50.810457516339866" x2="126.68000000000004" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="28" x1="301.28000000000003" y1="56" x2="264.32000000000005" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="29" x1="341.24000000000007" y1="56" x2="401.96000000000004" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="30" x1="69.98000000000002" y1="136" x2="66.02000000000001" y2="180" stroke="#555" stroke-width="1.6"/><line data-edge="31" x1="249.20000000000005" y1="136" x2="249.20000000000002" y2="180" stroke="#555" stroke-width="1.6"/><line data-edge="32" x1="428.4200000000001" y1="136" x2="432.38" y2="180" stroke="#555" stroke-width="1.6"/><line data-edge="35" x1="132.40000000000003" y1="118" x2="188.40000000000003" y2="118" stroke="#dc2626" stroke-width="1.6" stroke-dasharray="6 4"/><g data-node="sp1" data-x="142.8" data-y="20" style="cursor:move"><rect x="142.8" y="20" width="78.4" height="36" rx="14" fill="#e0e7ff" stroke="#8a8880" stroke-width="1.8"/><text x="182" y="42.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Spine-1</text></g><g data-node="sp2" data-x="277.20000000000005" data-y="20" style="cursor:move"><rect x="277.20000000000005" y="20" width="78.4" height="36" rx="14" fill="#e0e7ff" stroke="#8a8880" stroke-width="1.8"/><text x="316.40000000000003" y="42.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Spine-2</text></g><g data-node="lf1" data-x="10.800000000000011" data-y="100" style="cursor:move"><rect x="10.800000000000011" y="100" width="121.60000000000001" height="36" rx="14" fill="#fff" stroke="#8a8880" stroke-width="1.8"/><text x="71.60000000000002" y="122.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Leaf-1 (VTEP)</text></g><g data-node="lf2" data-x="188.40000000000003" data-y="100" style="cursor:move"><rect x="188.40000000000003" y="100" width="121.60000000000001" height="36" rx="14" fill="#fff" stroke="#8a8880" stroke-width="1.8"/><text x="249.20000000000005" y="122.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Leaf-2 (VTEP)</text></g><g data-node="lf3" data-x="366.00000000000006" data-y="100" style="cursor:move"><rect x="366.00000000000006" y="100" width="121.60000000000001" height="36" rx="14" fill="#fff" stroke="#8a8880" stroke-width="1.8"/><text x="426.80000000000007" y="122.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Leaf-3 (VTEP)</text></g><g data-node="h1" data-x="0" data-y="180" style="cursor:move"><rect x="0" y="180" width="128.8" height="36" fill="#fff" stroke="#8a8880"/><text x="64.4" y="202.55" font-size="13" text-anchor="middle" fill="#1d1d1b">VM-A (VLAN 10)</text></g><g data-node="h2" data-x="184.8" data-y="180" style="cursor:move"><rect x="184.8" y="180" width="128.8" height="36" fill="#fff" stroke="#8a8880"/><text x="249.20000000000002" y="202.55" font-size="13" text-anchor="middle" fill="#1d1d1b">VM-B (VLAN 10)</text></g><g data-node="h3" data-x="369.6" data-y="180" style="cursor:move"><rect x="369.6" y="180" width="128.8" height="36" fill="#fff" stroke="#8a8880"/><text x="434" y="202.55" font-size="13" text-anchor="middle" fill="#1d1d1b">VM-C (VLAN 20)</text></g><text x="126.80000000000001" y="62.82971014492753" font-size="11" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="3" stroke-linejoin="round">eBGP</text><text x="126.80000000000001" y="62.82971014492753" font-size="11" text-anchor="middle" fill="#555">eBGP</text><text x="160.40000000000003" y="112.25" font-size="11" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="3" stroke-linejoin="round">VXLAN VNI 10010</text><text x="160.40000000000003" y="112.25" font-size="11" text-anchor="middle" fill="#dc2626">VXLAN VNI 10010</text><path d="M132.40000000000003 118 L142.48000000000005 112.4 L142.48000000000005 123.6 z" fill="#dc2626" stroke="none"/><path d="M188.40000000000003 118 L178.32000000000002 123.6 L178.32000000000002 112.4 z" fill="#dc2626" stroke="none"/><rect x="0" y="237" width="16" height="11" fill="#fff" stroke="#555"/><text x="21" y="246.5" font-size="11" fill="#1d1d1b">Physical underlay link — eBGP/L3 transport between leaf and spine</text><rect x="0" y="257" width="16" height="11" fill="#fff" stroke="#dc2626" stroke-dasharray="6 4"/><text x="21" y="266.5" font-size="11" fill="#1d1d1b">Logical VXLAN overlay tunnel — rides on the physical underlay</text></g></g><g transform="translate(0,312)"><defs><marker id="s1_arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#555"/></marker><pattern id="s1_hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="6" stroke="#bbb" stroke-width="2"/></pattern></defs><g transform="translate(18,6)"><text x="0" y="14" font-size="13" font-weight="600">VNI mapping</text><rect x="0" y="22" width="71.2" height="26" fill="#eeede6" stroke="none" data-cell="vni:0:1" style="cursor:pointer"/><text x="35.6" y="39.3" font-size="12" text-anchor="middle" font-weight="600">Leaf</text><rect x="71.2" y="22" width="56.8" height="26" fill="#eeede6" stroke="none" data-cell="vni:0:2" style="cursor:pointer"/><text x="99.6" y="39.3" font-size="12" text-anchor="middle" font-weight="600">VLAN</text><rect x="128" y="22" width="64" height="26" fill="#eeede6" stroke="none" data-cell="vni:0:3" style="cursor:pointer"/><text x="160" y="39.3" font-size="12" text-anchor="middle" font-weight="600">VNI</text><rect x="192" y="22" width="56.8" height="26" fill="#eeede6" stroke="none" data-cell="vni:0:4" style="cursor:pointer"/><text x="220.4" y="39.3" font-size="12" text-anchor="middle" font-weight="600">Type</text><rect x="0" y="48" width="71.2" height="26" fill="#fff" stroke="none" data-cell="vni:1:1" style="cursor:pointer"/><text x="7" y="65.3" font-size="12" text-anchor="start">Leaf-1</text><rect x="71.2" y="48" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:1:2" style="cursor:pointer"/><text x="99.6" y="65.3" font-size="12" text-anchor="middle">10</text><rect x="128" y="48" width="64" height="26" fill="#fee2e2" stroke="none" data-cell="vni:1:3" style="cursor:pointer"/><text x="135" y="65.3" font-size="12" text-anchor="start">10010</text><rect x="192" y="48" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:1:4" style="cursor:pointer"/><text x="199" y="65.3" font-size="12" text-anchor="start">L2</text><rect x="0" y="74" width="71.2" height="26" fill="#fff" stroke="none" data-cell="vni:2:1" style="cursor:pointer"/><text x="7" y="91.3" font-size="12" text-anchor="start">Leaf-2</text><rect x="71.2" y="74" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:2:2" style="cursor:pointer"/><text x="99.6" y="91.3" font-size="12" text-anchor="middle">10</text><rect x="128" y="74" width="64" height="26" fill="#fee2e2" stroke="none" data-cell="vni:2:3" style="cursor:pointer"/><text x="135" y="91.3" font-size="12" text-anchor="start">10010</text><rect x="192" y="74" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:2:4" style="cursor:pointer"/><text x="199" y="91.3" font-size="12" text-anchor="start">L2</text><rect x="0" y="100" width="71.2" height="26" fill="#fff" stroke="none" data-cell="vni:3:1" style="cursor:pointer"/><text x="7" y="117.3" font-size="12" text-anchor="start">Leaf-3</text><rect x="71.2" y="100" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:3:2" style="cursor:pointer"/><text x="99.6" y="117.3" font-size="12" text-anchor="middle">20</text><rect x="128" y="100" width="64" height="26" fill="#fff" stroke="none" data-cell="vni:3:3" style="cursor:pointer"/><text x="135" y="117.3" font-size="12" text-anchor="start">10020</text><rect x="192" y="100" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:3:4" style="cursor:pointer"/><text x="199" y="117.3" font-size="12" text-anchor="start">L2</text><line x1="0" y1="22" x2="0" y2="126" stroke="#c9c7bf"/><line x1="71.2" y1="22" x2="71.2" y2="126" stroke="#c9c7bf"/><line x1="128" y1="22" x2="128" y2="126" stroke="#c9c7bf"/><line x1="192" y1="22" x2="192" y2="126" stroke="#c9c7bf"/><line x1="248.8" y1="22" x2="248.8" y2="126" stroke="#c9c7bf"/><line x1="0" y1="22" x2="248.8" y2="22" stroke="#c9c7bf"/><line x1="0" y1="48" x2="248.8" y2="48" stroke="#c9c7bf"/><line x1="0" y1="74" x2="248.8" y2="74" stroke="#c9c7bf"/><line x1="0" y1="100" x2="248.8" y2="100" stroke="#c9c7bf"/><line x1="0" y1="126" x2="248.8" y2="126" stroke="#c9c7bf"/><text x="0" y="170" font-size="13" font-weight="600">Fabric planes</text><rect x="0" y="178" width="136" height="26" fill="#eeede6" stroke="none" data-cell="planes:0:1" style="cursor:pointer"/><text x="68" y="195.3" font-size="12" text-anchor="middle" font-weight="600">Plane</text><rect x="136" y="178" width="143.2" height="26" fill="#eeede6" stroke="none" data-cell="planes:0:2" style="cursor:pointer"/><text x="207.6" y="195.3" font-size="12" text-anchor="middle" font-weight="600">Protocol</text><rect x="279.2" y="178" width="208" height="26" fill="#eeede6" stroke="none" data-cell="planes:0:3" style="cursor:pointer"/><text x="383.2" y="195.3" font-size="12" text-anchor="middle" font-weight="600">Role</text><rect x="0" y="204" width="136" height="26" fill="#fff" stroke="none" data-cell="planes:1:1" style="cursor:pointer"/><text x="7" y="221.3" font-size="12" text-anchor="start">Underlay</text><rect x="136" y="204" width="143.2" height="26" fill="#fff" stroke="none" data-cell="planes:1:2" style="cursor:pointer"/><text x="143" y="221.3" font-size="12" text-anchor="start">eBGP</text><rect x="279.2" y="204" width="208" height="26" fill="#fff" stroke="none" data-cell="planes:1:3" style="cursor:pointer"/><text x="286.2" y="221.3" font-size="12" text-anchor="start">loopback reachability</text><rect x="0" y="230" width="136" height="26" fill="#fff" stroke="none" data-cell="planes:2:1" style="cursor:pointer"/><text x="7" y="247.3" font-size="12" text-anchor="start">Overlay control</text><rect x="136" y="230" width="143.2" height="26" fill="#fff" stroke="none" data-cell="planes:2:2" style="cursor:pointer"/><text x="143" y="247.3" font-size="12" text-anchor="start">MP-BGP EVPN</text><rect x="279.2" y="230" width="208" height="26" fill="#fff" stroke="none" data-cell="planes:2:3" style="cursor:pointer"/><text x="286.2" y="247.3" font-size="12" text-anchor="start">MAC/IP route distribution</text><rect x="0" y="256" width="136" height="26" fill="#fff" stroke="none" data-cell="planes:3:1" style="cursor:pointer"/><text x="7" y="273.3" font-size="12" text-anchor="start">Overlay data</text><rect x="136" y="256" width="143.2" height="26" fill="#fff" stroke="none" data-cell="planes:3:2" style="cursor:pointer"/><text x="143" y="273.3" font-size="12" text-anchor="start">VXLAN (UDP 4789)</text><rect x="279.2" y="256" width="208" height="26" fill="#fff" stroke="none" data-cell="planes:3:3" style="cursor:pointer"/><text x="286.2" y="273.3" font-size="12" text-anchor="start">L2-in-L3 encapsulation</text><line x1="0" y1="178" x2="0" y2="282" stroke="#c9c7bf"/><line x1="136" y1="178" x2="136" y2="282" stroke="#c9c7bf"/><line x1="279.2" y1="178" x2="279.2" y2="282" stroke="#c9c7bf"/><line x1="487.2" y1="178" x2="487.2" y2="282" stroke="#c9c7bf"/><line x1="0" y1="178" x2="487.2" y2="178" stroke="#c9c7bf"/><line x1="0" y1="204" x2="487.2" y2="204" stroke="#c9c7bf"/><line x1="0" y1="230" x2="487.2" y2="230" stroke="#c9c7bf"/><line x1="0" y1="256" x2="487.2" y2="256" stroke="#c9c7bf"/><line x1="0" y1="282" x2="487.2" y2="282" stroke="#c9c7bf"/></g></g><metadata id="figdown-source" data-sha256="ada369d66e9180a61660c696656c5e07861d70a3ec3fd0b36a2638184a7981bc" data-engine-version="0.5.0"><![CDATA[
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 527 634" width="527" height="634" font-family="system-ui,sans-serif"><g transform="translate(0,0)"><defs><marker id="s0_arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#555"/></marker><pattern id="s0_hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="6" stroke="#bbb" stroke-width="2"/></pattern></defs><g transform="translate(18,6)"><line data-edge="24" x1="157.16" y1="56" x2="96.44000000000003" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="25" x1="197.12" y1="56" x2="234.08000000000004" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="26" x1="221.2" y1="50.810457516339866" x2="371.72" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="27" x1="277.20000000000005" y1="50.810457516339866" x2="126.68000000000004" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="28" x1="301.28000000000003" y1="56" x2="264.32000000000005" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="29" x1="341.24000000000007" y1="56" x2="401.96000000000004" y2="100" stroke="#555" stroke-width="1.6"/><line data-edge="30" x1="69.98000000000002" y1="136" x2="66.02000000000001" y2="180" stroke="#555" stroke-width="1.6"/><line data-edge="31" x1="249.20000000000005" y1="136" x2="249.20000000000002" y2="180" stroke="#555" stroke-width="1.6"/><line data-edge="32" x1="428.4200000000001" y1="136" x2="432.38" y2="180" stroke="#555" stroke-width="1.6"/><line data-edge="35" x1="132.40000000000003" y1="118" x2="188.40000000000003" y2="118" stroke="#dc2626" stroke-width="1.6" stroke-dasharray="6 4"/><g data-node="sp1" data-x="142.8" data-y="20" style="cursor:move"><rect x="142.8" y="20" width="78.4" height="36" rx="14" fill="#e0e7ff" stroke="#8a8880" stroke-width="1.8"/><text x="182" y="42.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Spine-1</text></g><g data-node="sp2" data-x="277.20000000000005" data-y="20" style="cursor:move"><rect x="277.20000000000005" y="20" width="78.4" height="36" rx="14" fill="#e0e7ff" stroke="#8a8880" stroke-width="1.8"/><text x="316.40000000000003" y="42.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Spine-2</text></g><g data-node="lf1" data-x="10.800000000000011" data-y="100" style="cursor:move"><rect x="10.800000000000011" y="100" width="121.60000000000001" height="36" rx="14" fill="#fff" stroke="#8a8880" stroke-width="1.8"/><text x="71.60000000000002" y="122.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Leaf-1 (VTEP)</text></g><g data-node="lf2" data-x="188.40000000000003" data-y="100" style="cursor:move"><rect x="188.40000000000003" y="100" width="121.60000000000001" height="36" rx="14" fill="#fff" stroke="#8a8880" stroke-width="1.8"/><text x="249.20000000000005" y="122.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Leaf-2 (VTEP)</text></g><g data-node="lf3" data-x="366.00000000000006" data-y="100" style="cursor:move"><rect x="366.00000000000006" y="100" width="121.60000000000001" height="36" rx="14" fill="#fff" stroke="#8a8880" stroke-width="1.8"/><text x="426.80000000000007" y="122.55" font-size="13" text-anchor="middle" fill="#1d1d1b">Leaf-3 (VTEP)</text></g><g data-node="h1" data-x="0" data-y="180" style="cursor:move"><rect x="0" y="180" width="128.8" height="36" fill="#fff" stroke="#8a8880"/><text x="64.4" y="202.55" font-size="13" text-anchor="middle" fill="#1d1d1b">VM-A (VLAN 10)</text></g><g data-node="h2" data-x="184.8" data-y="180" style="cursor:move"><rect x="184.8" y="180" width="128.8" height="36" fill="#fff" stroke="#8a8880"/><text x="249.20000000000002" y="202.55" font-size="13" text-anchor="middle" fill="#1d1d1b">VM-B (VLAN 10)</text></g><g data-node="h3" data-x="369.6" data-y="180" style="cursor:move"><rect x="369.6" y="180" width="128.8" height="36" fill="#fff" stroke="#8a8880"/><text x="434" y="202.55" font-size="13" text-anchor="middle" fill="#1d1d1b">VM-C (VLAN 20)</text></g><text x="126.80000000000001" y="62.82971014492753" font-size="11" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="3" stroke-linejoin="round">eBGP</text><text x="126.80000000000001" y="62.82971014492753" font-size="11" text-anchor="middle" fill="#555">eBGP</text><text x="160.40000000000003" y="112.25" font-size="11" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="3" stroke-linejoin="round">VXLAN VNI 10010</text><text x="160.40000000000003" y="112.25" font-size="11" text-anchor="middle" fill="#dc2626">VXLAN VNI 10010</text><path d="M132.40000000000003 118 L142.48000000000005 112.4 L142.48000000000005 123.6 z" fill="#dc2626" stroke="none"/><path d="M188.40000000000003 118 L178.32000000000002 123.6 L178.32000000000002 112.4 z" fill="#dc2626" stroke="none"/><line x1="0" y1="242.5" x2="16" y2="242.5" stroke="#555" stroke-width="1.6"/><text x="21" y="246.5" font-size="11" fill="#1d1d1b">Physical underlay link — eBGP/L3 transport between leaf and spine</text><line x1="0" y1="262.5" x2="16" y2="262.5" stroke="#dc2626" stroke-width="1.6" stroke-dasharray="6 4"/><text x="21" y="266.5" font-size="11" fill="#1d1d1b">Logical VXLAN overlay tunnel — rides on the physical underlay</text></g></g><g transform="translate(0,312)"><defs><marker id="s1_arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#555"/></marker><pattern id="s1_hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="6" stroke="#bbb" stroke-width="2"/></pattern></defs><g transform="translate(18,6)"><text x="0" y="14" font-size="13" font-weight="600">VNI mapping</text><rect x="0" y="22" width="71.2" height="26" fill="#eeede6" stroke="none" data-cell="vni:0:1" style="cursor:pointer"/><text x="35.6" y="39.3" font-size="12" text-anchor="middle" font-weight="600">Leaf</text><rect x="71.2" y="22" width="56.8" height="26" fill="#eeede6" stroke="none" data-cell="vni:0:2" style="cursor:pointer"/><text x="99.6" y="39.3" font-size="12" text-anchor="middle" font-weight="600">VLAN</text><rect x="128" y="22" width="64" height="26" fill="#eeede6" stroke="none" data-cell="vni:0:3" style="cursor:pointer"/><text x="160" y="39.3" font-size="12" text-anchor="middle" font-weight="600">VNI</text><rect x="192" y="22" width="56.8" height="26" fill="#eeede6" stroke="none" data-cell="vni:0:4" style="cursor:pointer"/><text x="220.4" y="39.3" font-size="12" text-anchor="middle" font-weight="600">Type</text><rect x="0" y="48" width="71.2" height="26" fill="#fff" stroke="none" data-cell="vni:1:1" style="cursor:pointer"/><text x="7" y="65.3" font-size="12" text-anchor="start">Leaf-1</text><rect x="71.2" y="48" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:1:2" style="cursor:pointer"/><text x="99.6" y="65.3" font-size="12" text-anchor="middle">10</text><rect x="128" y="48" width="64" height="26" fill="#fee2e2" stroke="none" data-cell="vni:1:3" style="cursor:pointer"/><text x="135" y="65.3" font-size="12" text-anchor="start">10010</text><rect x="192" y="48" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:1:4" style="cursor:pointer"/><text x="199" y="65.3" font-size="12" text-anchor="start">L2</text><rect x="0" y="74" width="71.2" height="26" fill="#fff" stroke="none" data-cell="vni:2:1" style="cursor:pointer"/><text x="7" y="91.3" font-size="12" text-anchor="start">Leaf-2</text><rect x="71.2" y="74" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:2:2" style="cursor:pointer"/><text x="99.6" y="91.3" font-size="12" text-anchor="middle">10</text><rect x="128" y="74" width="64" height="26" fill="#fee2e2" stroke="none" data-cell="vni:2:3" style="cursor:pointer"/><text x="135" y="91.3" font-size="12" text-anchor="start">10010</text><rect x="192" y="74" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:2:4" style="cursor:pointer"/><text x="199" y="91.3" font-size="12" text-anchor="start">L2</text><rect x="0" y="100" width="71.2" height="26" fill="#fff" stroke="none" data-cell="vni:3:1" style="cursor:pointer"/><text x="7" y="117.3" font-size="12" text-anchor="start">Leaf-3</text><rect x="71.2" y="100" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:3:2" style="cursor:pointer"/><text x="99.6" y="117.3" font-size="12" text-anchor="middle">20</text><rect x="128" y="100" width="64" height="26" fill="#fff" stroke="none" data-cell="vni:3:3" style="cursor:pointer"/><text x="135" y="117.3" font-size="12" text-anchor="start">10020</text><rect x="192" y="100" width="56.8" height="26" fill="#fff" stroke="none" data-cell="vni:3:4" style="cursor:pointer"/><text x="199" y="117.3" font-size="12" text-anchor="start">L2</text><line x1="0" y1="22" x2="0" y2="126" stroke="#c9c7bf"/><line x1="71.2" y1="22" x2="71.2" y2="126" stroke="#c9c7bf"/><line x1="128" y1="22" x2="128" y2="126" stroke="#c9c7bf"/><line x1="192" y1="22" x2="192" y2="126" stroke="#c9c7bf"/><line x1="248.8" y1="22" x2="248.8" y2="126" stroke="#c9c7bf"/><line x1="0" y1="22" x2="248.8" y2="22" stroke="#c9c7bf"/><line x1="0" y1="48" x2="248.8" y2="48" stroke="#c9c7bf"/><line x1="0" y1="74" x2="248.8" y2="74" stroke="#c9c7bf"/><line x1="0" y1="100" x2="248.8" y2="100" stroke="#c9c7bf"/><line x1="0" y1="126" x2="248.8" y2="126" stroke="#c9c7bf"/><text x="0" y="170" font-size="13" font-weight="600">Fabric planes</text><rect x="0" y="178" width="136" height="26" fill="#eeede6" stroke="none" data-cell="planes:0:1" style="cursor:pointer"/><text x="68" y="195.3" font-size="12" text-anchor="middle" font-weight="600">Plane</text><rect x="136" y="178" width="143.2" height="26" fill="#eeede6" stroke="none" data-cell="planes:0:2" style="cursor:pointer"/><text x="207.6" y="195.3" font-size="12" text-anchor="middle" font-weight="600">Protocol</text><rect x="279.2" y="178" width="208" height="26" fill="#eeede6" stroke="none" data-cell="planes:0:3" style="cursor:pointer"/><text x="383.2" y="195.3" font-size="12" text-anchor="middle" font-weight="600">Role</text><rect x="0" y="204" width="136" height="26" fill="#fff" stroke="none" data-cell="planes:1:1" style="cursor:pointer"/><text x="7" y="221.3" font-size="12" text-anchor="start">Underlay</text><rect x="136" y="204" width="143.2" height="26" fill="#fff" stroke="none" data-cell="planes:1:2" style="cursor:pointer"/><text x="143" y="221.3" font-size="12" text-anchor="start">eBGP</text><rect x="279.2" y="204" width="208" height="26" fill="#fff" stroke="none" data-cell="planes:1:3" style="cursor:pointer"/><text x="286.2" y="221.3" font-size="12" text-anchor="start">loopback reachability</text><rect x="0" y="230" width="136" height="26" fill="#fff" stroke="none" data-cell="planes:2:1" style="cursor:pointer"/><text x="7" y="247.3" font-size="12" text-anchor="start">Overlay control</text><rect x="136" y="230" width="143.2" height="26" fill="#fff" stroke="none" data-cell="planes:2:2" style="cursor:pointer"/><text x="143" y="247.3" font-size="12" text-anchor="start">MP-BGP EVPN</text><rect x="279.2" y="230" width="208" height="26" fill="#fff" stroke="none" data-cell="planes:2:3" style="cursor:pointer"/><text x="286.2" y="247.3" font-size="12" text-anchor="start">MAC/IP route distribution</text><rect x="0" y="256" width="136" height="26" fill="#fff" stroke="none" data-cell="planes:3:1" style="cursor:pointer"/><text x="7" y="273.3" font-size="12" text-anchor="start">Overlay data</text><rect x="136" y="256" width="143.2" height="26" fill="#fff" stroke="none" data-cell="planes:3:2" style="cursor:pointer"/><text x="143" y="273.3" font-size="12" text-anchor="start">VXLAN (UDP 4789)</text><rect x="279.2" y="256" width="208" height="26" fill="#fff" stroke="none" data-cell="planes:3:3" style="cursor:pointer"/><text x="286.2" y="273.3" font-size="12" text-anchor="start">L2-in-L3 encapsulation</text><line x1="0" y1="178" x2="0" y2="282" stroke="#c9c7bf"/><line x1="136" y1="178" x2="136" y2="282" stroke="#c9c7bf"/><line x1="279.2" y1="178" x2="279.2" y2="282" stroke="#c9c7bf"/><line x1="487.2" y1="178" x2="487.2" y2="282" stroke="#c9c7bf"/><line x1="0" y1="178" x2="487.2" y2="178" stroke="#c9c7bf"/><line x1="0" y1="204" x2="487.2" y2="204" stroke="#c9c7bf"/><line x1="0" y1="230" x2="487.2" y2="230" stroke="#c9c7bf"/><line x1="0" y1="256" x2="487.2" y2="256" stroke="#c9c7bf"/><line x1="0" y1="282" x2="487.2" y2="282" stroke="#c9c7bf"/></g></g><metadata id="figdown-source" data-sha256="ada369d66e9180a61660c696656c5e07861d70a3ec3fd0b36a2638184a7981bc" data-engine-version="0.5.1"><![CDATA[
|
|
2
2
|
# FigDown — figures as text. Spec: https://github.com/FigDown/figdown
|
|
3
3
|
|
|
4
4
|
figdown 0.1 topology
|