sfora-cli 0.9.0 → 0.11.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 +147 -6
- package/dist/SforaFs.js +278 -10
- package/dist/api-client.d.ts +290 -5
- package/dist/api-client.js +307 -22
- package/dist/block-commands.d.ts +84 -0
- package/dist/block-commands.js +155 -0
- package/dist/cli.js +323 -29
- package/dist/format/__tests__/byteStable.d.ts +5 -0
- package/dist/format/__tests__/byteStable.js +64 -0
- package/dist/format/blockSplice.d.ts +135 -0
- package/dist/format/blockSplice.js +330 -0
- package/dist/format/blocks/dropClosure.d.ts +81 -0
- package/dist/format/blocks/dropClosure.js +196 -0
- package/dist/format/blocks/markdown-block-catalog.d.ts +18 -0
- package/dist/format/blocks/markdown-block-catalog.js +162 -0
- package/dist/format/blocks/markdown-block-ids.d.mts +1 -0
- package/dist/format/blocks/markdown-block-ids.mjs +25 -0
- package/dist/format/blocks/parsers.d.ts +105 -0
- package/dist/format/blocks/parsers.js +442 -0
- package/dist/format/blocks/structured-block-schema.d.ts +8 -0
- package/dist/format/blocks/structured-block-schema.js +30 -0
- package/dist/format/callout.d.ts +128 -0
- package/dist/format/callout.js +227 -0
- package/dist/format/cardMarkdown.d.ts +2 -0
- package/dist/format/cardMarkdown.js +10 -0
- package/dist/format/checklist.d.ts +34 -0
- package/dist/format/checklist.js +158 -0
- package/dist/format/formatAxes.d.ts +228 -0
- package/dist/format/formatAxes.js +454 -0
- package/dist/format/index.d.ts +19 -4
- package/dist/format/index.js +28 -4
- package/dist/format/lineGeometry.d.ts +100 -0
- package/dist/format/lineGeometry.js +424 -0
- package/dist/format/lint/appliesTo.d.ts +92 -0
- package/dist/format/lint/appliesTo.js +369 -0
- package/dist/format/lint/config.d.ts +106 -0
- package/dist/format/lint/config.js +205 -0
- package/dist/format/lint/fixAll.d.ts +62 -0
- package/dist/format/lint/fixAll.js +107 -0
- package/dist/format/lint/frontmatterSchema.d.ts +181 -0
- package/dist/format/lint/frontmatterSchema.js +660 -0
- package/dist/format/lint/index.d.ts +49 -0
- package/dist/format/lint/index.js +51 -0
- package/dist/format/lint/lintSource.d.ts +56 -0
- package/dist/format/lint/lintSource.js +188 -0
- package/dist/format/lint/rules/broken-wiki-link.d.ts +2 -0
- package/dist/format/lint/rules/broken-wiki-link.js +45 -0
- package/dist/format/lint/rules/frontmatter-schema.d.ts +2 -0
- package/dist/format/lint/rules/frontmatter-schema.js +92 -0
- package/dist/format/lint/rules/index.d.ts +11 -0
- package/dist/format/lint/rules/index.js +32 -0
- package/dist/format/lint/rules/malformed-callout.d.ts +2 -0
- package/dist/format/lint/rules/malformed-callout.js +88 -0
- package/dist/format/lint/rules/malformed-checklist.d.ts +2 -0
- package/dist/format/lint/rules/malformed-checklist.js +65 -0
- package/dist/format/lint/rules/malformed-frontmatter.d.ts +2 -0
- package/dist/format/lint/rules/malformed-frontmatter.js +98 -0
- package/dist/format/lint/rules/malformed-structured-block.d.ts +2 -0
- package/dist/format/lint/rules/malformed-structured-block.js +134 -0
- package/dist/format/lint/rules/malformed-wiki-link.d.ts +2 -0
- package/dist/format/lint/rules/malformed-wiki-link.js +43 -0
- package/dist/format/lint/rules/orphan-reference.d.ts +2 -0
- package/dist/format/lint/rules/orphan-reference.js +87 -0
- package/dist/format/lint/severity.d.ts +15 -0
- package/dist/format/lint/severity.js +50 -0
- package/dist/format/lint/textEdits.d.ts +86 -0
- package/dist/format/lint/textEdits.js +162 -0
- package/dist/format/lint/types.d.ts +116 -0
- package/dist/format/lint/types.js +16 -0
- package/dist/format/markdown/dates.js +2 -0
- package/dist/format/markdown/document.js +2 -0
- package/dist/format/markdown/index.js +2 -0
- package/dist/format/markdown/mentions.js +2 -0
- package/dist/format/markdown/slug.d.ts +28 -0
- package/dist/format/markdown/slug.js +65 -0
- package/dist/format/markdown/yaml.js +2 -0
- package/dist/format/noteMarkdown.js +2 -0
- package/dist/format/parseWithFallback.d.ts +13 -0
- package/dist/format/parseWithFallback.js +98 -0
- package/dist/format/plaintext.d.ts +5 -0
- package/dist/format/plaintext.js +51 -0
- package/dist/format/postMarkdown.js +3 -1
- package/dist/format/sheetCellSpans.d.ts +95 -0
- package/dist/format/sheetCellSpans.js +223 -0
- package/dist/format/sheetSelection.d.ts +136 -0
- package/dist/format/sheetSelection.js +282 -0
- package/dist/format/taskUploadFilename.d.ts +6 -0
- package/dist/format/taskUploadFilename.js +13 -0
- package/dist/format/textStats.d.ts +23 -0
- package/dist/format/textStats.js +80 -0
- package/dist/format/wayfinder.d.ts +50 -0
- package/dist/format/wayfinder.js +203 -0
- package/dist/format/wikiLinks.d.ts +78 -0
- package/dist/format/wikiLinks.js +266 -0
- package/dist/index.d.ts +26 -1
- package/dist/index.js +20 -3
- package/dist/mcp-server.js +5 -2
- package/dist/opener.d.ts +23 -0
- package/dist/opener.js +26 -0
- package/dist/render.d.ts +132 -0
- package/dist/render.js +208 -0
- package/dist/shell-commands.d.ts +34 -0
- package/dist/shell-commands.js +108 -0
- package/dist/watch.d.ts +79 -0
- package/dist/watch.js +113 -0
- package/dist/web-url.d.ts +39 -0
- package/dist/web-url.js +63 -0
- package/package.json +7 -6
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The format degrees of freedom, as a finite list somebody can read in one
|
|
3
|
+
* sitting.
|
|
4
|
+
*
|
|
5
|
+
* ---------------------------------------------------------------------------
|
|
6
|
+
* WHAT AN AXIS IS
|
|
7
|
+
* ---------------------------------------------------------------------------
|
|
8
|
+
*
|
|
9
|
+
* A format axis is a dimension along which the BYTES of a document may vary
|
|
10
|
+
* while its MEANING does not. `*em*` and `_em_` are two points on one axis.
|
|
11
|
+
* So are ` ``` ` and `~~~`, `# T` and `# T #`, a table's `-` and its `---`.
|
|
12
|
+
* Nothing on this list changes what a reader sees; everything on it changes
|
|
13
|
+
* what the file says on disk.
|
|
14
|
+
*
|
|
15
|
+
* The list exists because "lossy" and "byte-identical" are both useless as
|
|
16
|
+
* verdicts on their own. A serializer that reflows a document is not corrupting
|
|
17
|
+
* it, and a round trip that changes six bytes has not necessarily lost
|
|
18
|
+
* anything — but until the six bytes have a NAME, nobody reviewing the diff can
|
|
19
|
+
* tell which of those two sentences is true. Card #288: byte fidelity as a
|
|
20
|
+
* finite reviewable vocabulary, each entry with a witness that runs.
|
|
21
|
+
*
|
|
22
|
+
* The names follow open-knowledge's, whose `parser-drop-closure.ts:22-289`
|
|
23
|
+
* already spends axis ids the same way — `code-fence:char`, `link:title-quote`,
|
|
24
|
+
* `list:item-marker-spacing`, `setext:underline`. Where a name diverges it is
|
|
25
|
+
* because sfora's capture layer draws the line somewhere else, and the axis
|
|
26
|
+
* says so.
|
|
27
|
+
*
|
|
28
|
+
* ---------------------------------------------------------------------------
|
|
29
|
+
* WHERE THE LIST COMES FROM, AND WHY IT CANNOT BE INVENTED
|
|
30
|
+
* ---------------------------------------------------------------------------
|
|
31
|
+
*
|
|
32
|
+
* The axes are not a taxonomy somebody sat down and wrote. They are DERIVED
|
|
33
|
+
* from what the capture layer already records: `GEOMETRY_KEYS` in
|
|
34
|
+
* `editorWalker/sourceGeometry.ts` is the ground truth, and every
|
|
35
|
+
* `<nodeType>.<dataKey>` in it is one axis's `captures` entry. That is the
|
|
36
|
+
* whole discipline — an axis with no capture is a word, and a capture with no
|
|
37
|
+
* axis is a field nobody can name in a review. `__tests__/formatAxes.test.ts`
|
|
38
|
+
* closes the two against each other in both directions, so neither can drift.
|
|
39
|
+
*
|
|
40
|
+
* This file deliberately does NOT import `GEOMETRY_KEYS`. The capture layer
|
|
41
|
+
* lives under `editorWalker/`, which pulls unified and remark-stringify in and
|
|
42
|
+
* is excluded from the CLI sync (`packages/sfora/scripts/sync-format.mjs`);
|
|
43
|
+
* the drop ledger that spends these ids is NOT excluded and must keep
|
|
44
|
+
* compiling in the tarball. So the coupling is a string in a test rather than
|
|
45
|
+
* an import, and the test is what makes the string true.
|
|
46
|
+
*
|
|
47
|
+
* ---------------------------------------------------------------------------
|
|
48
|
+
* THREE CAPTURE LAYERS, ONE VOCABULARY
|
|
49
|
+
* ---------------------------------------------------------------------------
|
|
50
|
+
*
|
|
51
|
+
* Sfora has three places where an author's spelling is read and set aside, and
|
|
52
|
+
* they are not the same machinery:
|
|
53
|
+
*
|
|
54
|
+
* - the mdast geometry capture, which reads spellings off a POSITIONED parse
|
|
55
|
+
* tree and stores them on `node.data` (`layer: "mdast-geometry"`);
|
|
56
|
+
* - the structured-block parsers, which read a fence body line by line and
|
|
57
|
+
* report what they did not keep (`layer: "structured-block"`);
|
|
58
|
+
* - the blank-run plan, which reads the gaps BETWEEN blocks and the runs at
|
|
59
|
+
* the document's own two ends (`layer: "blank-runs"`).
|
|
60
|
+
*
|
|
61
|
+
* The second is why this file is not simply a rename of `GEOMETRY_KEYS`. The
|
|
62
|
+
* wave-1 drop ledger already spends the id `structured-block:entry-bullet` on
|
|
63
|
+
* a chat entry authored `* @mara: …` rather than `- @mara: …`, and that
|
|
64
|
+
* spelling has no `node.data` anywhere — the bullet is dropped, not captured.
|
|
65
|
+
* An axis vocabulary that could not name it would leave the one verdict it was
|
|
66
|
+
* built for pointing at a string nobody defined.
|
|
67
|
+
*
|
|
68
|
+
* The third is here because the list has to cover what the walker REPLAYS, not
|
|
69
|
+
* what one module happens to declare. `blankRuns.ts` writes
|
|
70
|
+
* `sourceBlankLinesBefore` and `sourceEdgeBlankLines`, `emit.ts`'s join reads
|
|
71
|
+
* both back, and an author's double gap between two sections survives a save
|
|
72
|
+
* because of it — a degree of freedom as real as a fence character, and one the
|
|
73
|
+
* first version of this list was silently missing because those two keys are
|
|
74
|
+
* written by the WALKER onto the tree it builds rather than by
|
|
75
|
+
* `captureSourceGeometry` onto a parse, and so are structurally outside
|
|
76
|
+
* `GEOMETRY_KEYS`. Deriving the list from one module's export rather than from
|
|
77
|
+
* the behaviour is exactly how a vocabulary ends up with a hole in it.
|
|
78
|
+
*
|
|
79
|
+
* So the layer is a discriminant and the closure test grades each layer against
|
|
80
|
+
* its own ground truth: the geometry axes against `GEOMETRY_KEYS`, the block
|
|
81
|
+
* axes against the `format-dof-axis` entries of `BLOCK_DROP_ADJUDICATIONS`, and
|
|
82
|
+
* the blank-run axes against the keys a live `captureRootGaps` /
|
|
83
|
+
* `applyRootGaps` pair actually writes over a corpus. That third ground truth
|
|
84
|
+
* is BEHAVIOURAL rather than declarative, and the suite says so: the two keys
|
|
85
|
+
* are declared in a `declare module "mdast"` block, which erases at runtime, so
|
|
86
|
+
* there is no list to import. A key the corpus never provokes would be invisible
|
|
87
|
+
* to it, which is a real limit and not a hidden one.
|
|
88
|
+
*
|
|
89
|
+
* ---------------------------------------------------------------------------
|
|
90
|
+
* WHAT A WITNESS PROVES, AND WHAT IT DOES NOT
|
|
91
|
+
* ---------------------------------------------------------------------------
|
|
92
|
+
*
|
|
93
|
+
* Every axis carries at least one DISCRIMINATION WITNESS: two spellings, `a`
|
|
94
|
+
* and `b`. The name matters, because these are not round-trip witnesses and
|
|
95
|
+
* calling them that would claim the one thing they do not show. The suite
|
|
96
|
+
* asserts three things about each pair, and the third is what stops the list
|
|
97
|
+
* becoming decoration.
|
|
98
|
+
*
|
|
99
|
+
* 1. the bytes differ — otherwise there is no degree of freedom here;
|
|
100
|
+
* 2. an INDEPENDENT reparse reads both to the same tree, positions and
|
|
101
|
+
* `data` stripped (`geometryReparsesTheSame`) — otherwise this is a
|
|
102
|
+
* content difference wearing a formatting name;
|
|
103
|
+
* 3. the capture layer TELLS THEM APART — the set of captured keys that move
|
|
104
|
+
* between `a` and `b` is non-empty, and across an axis's witnesses it
|
|
105
|
+
* covers every key the axis claims.
|
|
106
|
+
*
|
|
107
|
+
* Three is the anti-vacuity clause. An axis whose witnesses no longer move its
|
|
108
|
+
* own capture key is an axis whose capture stopped happening, and the suite
|
|
109
|
+
* fails on the axis rather than on some distant serializer test six months
|
|
110
|
+
* later.
|
|
111
|
+
*
|
|
112
|
+
* What a discrimination witness does NOT prove is that the spelling survives a
|
|
113
|
+
* save. Most of these axes are captured and not yet replayed —
|
|
114
|
+
* `CAPTURED_NOT_REPLAYED` in `sourceGeometry.ts` says which, and names the card
|
|
115
|
+
* that owes each one. The suite pins the replayed SET rather than a total,
|
|
116
|
+
* because a total can hold still while the membership churns — and, for one
|
|
117
|
+
* axis in each replay FAMILY, it serializes an editor document through the real
|
|
118
|
+
* walker and asserts the author's bytes come back. A scoreboard read off a
|
|
119
|
+
* hand-maintained array is a scoreboard that can be wrong in the direction
|
|
120
|
+
* nobody checks.
|
|
121
|
+
*/
|
|
122
|
+
import type { BlockDropReason } from "./blocks/dropClosure.js";
|
|
123
|
+
import type { StructuredBlockLanguage } from "./blocks/structured-block-schema.js";
|
|
124
|
+
/**
|
|
125
|
+
* Every axis id, as a union so a ledger entry citing one that does not exist
|
|
126
|
+
* fails to compile rather than fails to mean anything.
|
|
127
|
+
*
|
|
128
|
+
* `namespace:name`, lower case, and the namespace is the CONSTRUCT rather than
|
|
129
|
+
* the mdast node type where the two disagree — `task-list:checkbox-char` sits
|
|
130
|
+
* on `listItem.sourceCheckboxChar`, and an author who has never heard of mdast
|
|
131
|
+
* still knows what a task list is.
|
|
132
|
+
*/
|
|
133
|
+
export type FormatAxisId = "blank-lines:doc-edge" | "blank-lines:interior" | "block-gap:after-heading" | "blockquote:marker-spacing" | "code-block:style" | "code-fence:char" | "code-fence:info-padding" | "code-fence:length" | "definition:layout" | "definition:title-quote" | "emphasis:delimiter" | "hard-break:style" | "heading:style" | "heading:trailing-hashes" | "inline-code:fence-length" | "inline-code:padding" | "lexical:backslash-escape" | "link:angle-url" | "link:autolink-form" | "link:title-quote" | "list:bullet-marker" | "list:item-marker-spacing" | "list:item-ordinal" | "list:ordered-delimiter" | "setext:underline-length" | "strike:delimiter" | "strong:delimiter" | "structured-block:entry-bullet" | "table:cell-padding" | "table:delimiter-dashes" | "table:delimiter-padding" | "table:outer-pipes" | "task-list:checkbox-char" | "thematic-break:marker";
|
|
134
|
+
/**
|
|
135
|
+
* Two spellings of one meaning, for an axis the mdast geometry layer captures.
|
|
136
|
+
*
|
|
137
|
+
* Both are whole markdown documents rather than fragments, because the capture
|
|
138
|
+
* layer reads source OFFSETS and a fragment has none. They are kept as short as
|
|
139
|
+
* the axis allows so a reader can see the difference without diffing.
|
|
140
|
+
*/
|
|
141
|
+
export interface GeometryAxisWitness {
|
|
142
|
+
a: string;
|
|
143
|
+
b: string;
|
|
144
|
+
/** What moved, in the words a reviewer would use. */
|
|
145
|
+
note: string;
|
|
146
|
+
}
|
|
147
|
+
/** Two spellings of one meaning, for an axis a structured-block parser drops. */
|
|
148
|
+
export interface BlockAxisWitness {
|
|
149
|
+
lang: StructuredBlockLanguage;
|
|
150
|
+
/** The spelling whose bytes survive. */
|
|
151
|
+
a: string;
|
|
152
|
+
/** The spelling that provokes the drop. */
|
|
153
|
+
b: string;
|
|
154
|
+
note: string;
|
|
155
|
+
}
|
|
156
|
+
export type FormatAxis = {
|
|
157
|
+
id: FormatAxisId;
|
|
158
|
+
layer: "mdast-geometry";
|
|
159
|
+
/**
|
|
160
|
+
* `<nodeType>.<dataKey>`, exactly as `GEOMETRY_KEYS` spells it. Every
|
|
161
|
+
* entry must be declared there and every entry there must appear on
|
|
162
|
+
* exactly one axis.
|
|
163
|
+
*/
|
|
164
|
+
captures: readonly string[];
|
|
165
|
+
rationale: string;
|
|
166
|
+
witnesses: readonly GeometryAxisWitness[];
|
|
167
|
+
} | {
|
|
168
|
+
id: FormatAxisId;
|
|
169
|
+
layer: "blank-runs";
|
|
170
|
+
/**
|
|
171
|
+
* The `data` keys `blankRuns.ts` writes, by NAME rather than as
|
|
172
|
+
* `<nodeType>.<dataKey>`.
|
|
173
|
+
*
|
|
174
|
+
* The node type is not part of the identity here and pretending it were
|
|
175
|
+
* would be a lie in the vocabulary: an interior gap belongs to whatever
|
|
176
|
+
* block happens to follow it, so `sourceBlankLinesBefore` lands on a
|
|
177
|
+
* paragraph, a code block or a table depending on the document. (The edge
|
|
178
|
+
* run is always on the root, and is spelled the same way for symmetry.)
|
|
179
|
+
*/
|
|
180
|
+
dataKeys: readonly string[];
|
|
181
|
+
rationale: string;
|
|
182
|
+
witnesses: readonly GeometryAxisWitness[];
|
|
183
|
+
} | {
|
|
184
|
+
id: FormatAxisId;
|
|
185
|
+
layer: "structured-block";
|
|
186
|
+
/**
|
|
187
|
+
* The drop reasons in `BLOCK_DROP_ADJUDICATIONS` whose `format-dof-axis`
|
|
188
|
+
* verdict spends this id. Closed both directions by the suite.
|
|
189
|
+
*/
|
|
190
|
+
ledgerReasons: readonly BlockDropReason[];
|
|
191
|
+
rationale: string;
|
|
192
|
+
witnesses: readonly BlockAxisWitness[];
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* The list. Sorted by id, because a list nobody can scan is not reviewable and
|
|
196
|
+
* reviewable is the entire point.
|
|
197
|
+
*/
|
|
198
|
+
export declare const FORMAT_AXES: readonly FormatAxis[];
|
|
199
|
+
/** Every id on the list, for a cheap membership test. */
|
|
200
|
+
export declare const FORMAT_AXIS_IDS: ReadonlySet<FormatAxisId>;
|
|
201
|
+
/**
|
|
202
|
+
* The axis with this id.
|
|
203
|
+
*
|
|
204
|
+
* Throws rather than returning undefined: every call site here is spending an
|
|
205
|
+
* id that the type system already proved exists, so a miss means the list and
|
|
206
|
+
* the union disagree, which is a bug in this file and not a case a caller
|
|
207
|
+
* should be writing a branch for.
|
|
208
|
+
*/
|
|
209
|
+
export declare function formatAxis(id: FormatAxisId): FormatAxis;
|
|
210
|
+
/**
|
|
211
|
+
* The axis that owns a captured geometry key, spelled `<nodeType>.<dataKey>`.
|
|
212
|
+
*
|
|
213
|
+
* Undefined here is the interesting answer, not an error: it means the capture
|
|
214
|
+
* layer records a spelling the vocabulary cannot name, which is exactly what
|
|
215
|
+
* the closure test is looking for.
|
|
216
|
+
*/
|
|
217
|
+
export declare function axisForCapture(key: string): FormatAxis | undefined;
|
|
218
|
+
/** Every `<nodeType>.<dataKey>` the vocabulary claims, deduplicated. */
|
|
219
|
+
export declare function claimedCaptureKeys(): string[];
|
|
220
|
+
/**
|
|
221
|
+
* Every blank-run `data` key the vocabulary claims, deduplicated.
|
|
222
|
+
*
|
|
223
|
+
* Kept apart from {@link claimedCaptureKeys} rather than merged into it because
|
|
224
|
+
* the two are spelled differently on purpose — one carries a node type and the
|
|
225
|
+
* other cannot — and a single list would have to pick one spelling and be wrong
|
|
226
|
+
* about half its entries.
|
|
227
|
+
*/
|
|
228
|
+
export declare function claimedBlankRunKeys(): string[];
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The list. Sorted by id, because a list nobody can scan is not reviewable and
|
|
3
|
+
* reviewable is the entire point.
|
|
4
|
+
*/
|
|
5
|
+
export const FORMAT_AXES = [
|
|
6
|
+
{
|
|
7
|
+
id: "blank-lines:doc-edge",
|
|
8
|
+
layer: "blank-runs",
|
|
9
|
+
dataKeys: ["sourceEdgeBlankLines"],
|
|
10
|
+
rationale: "blank runs at the document's own two ends; `emitMarkdown` strips the terminator and writes no leading run, so without the plan an author's deliberate breathing room at the top or bottom of a body is gone on the first save",
|
|
11
|
+
witnesses: [
|
|
12
|
+
{
|
|
13
|
+
a: "A",
|
|
14
|
+
b: "A\n\n\n",
|
|
15
|
+
note: "a bare body against one with a trailing blank run",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "blank-lines:interior",
|
|
21
|
+
layer: "blank-runs",
|
|
22
|
+
dataKeys: ["sourceBlankLinesBefore"],
|
|
23
|
+
rationale: "how many blank lines the author left between two root blocks; mdast-util-to-markdown writes exactly one and mdast has no field for the rest, so a double gap used as a section break closes on every save",
|
|
24
|
+
witnesses: [
|
|
25
|
+
{
|
|
26
|
+
a: "A\n\nB",
|
|
27
|
+
b: "A\n\n\nB",
|
|
28
|
+
note: "one blank line between two paragraphs against two",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "block-gap:after-heading",
|
|
34
|
+
layer: "mdast-geometry",
|
|
35
|
+
captures: ["heading.sourceContiguousNext"],
|
|
36
|
+
rationale: "whether a heading is followed by a blank line before its first block; remark writes one unconditionally, so without this axis every contiguous heading in a document reflows on the first save",
|
|
37
|
+
witnesses: [
|
|
38
|
+
{
|
|
39
|
+
a: "# T\nbody",
|
|
40
|
+
b: "# T\n\nbody",
|
|
41
|
+
note: "the gap between the heading and the paragraph under it",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "blockquote:marker-spacing",
|
|
47
|
+
layer: "mdast-geometry",
|
|
48
|
+
captures: ["blockquote.sourceMarkerSpacings"],
|
|
49
|
+
rationale: "how many spaces followed each `>`; CommonMark strips one and the rest are the author's, per line",
|
|
50
|
+
witnesses: [
|
|
51
|
+
{
|
|
52
|
+
a: "> q",
|
|
53
|
+
b: ">q",
|
|
54
|
+
note: "one space after the marker against none",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "code-block:style",
|
|
60
|
+
layer: "mdast-geometry",
|
|
61
|
+
captures: ["code.sourceStyle"],
|
|
62
|
+
rationale: "fenced against indented; the same four lines of code, spelled two ways that CommonMark reads identically",
|
|
63
|
+
witnesses: [
|
|
64
|
+
{
|
|
65
|
+
a: "```\nx\n```",
|
|
66
|
+
b: " x",
|
|
67
|
+
note: "a fence against a four-space indent — the widest spelling gap on the list",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "code-fence:char",
|
|
73
|
+
layer: "mdast-geometry",
|
|
74
|
+
captures: ["code.sourceFenceChar"],
|
|
75
|
+
rationale: "backtick against tilde; the choice matters to an author whose body contains the other one",
|
|
76
|
+
witnesses: [
|
|
77
|
+
{
|
|
78
|
+
a: "```\nx\n```",
|
|
79
|
+
b: "~~~\nx\n~~~",
|
|
80
|
+
note: "the fence character",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "code-fence:info-padding",
|
|
86
|
+
layer: "mdast-geometry",
|
|
87
|
+
captures: ["code.sourceInfoPadding"],
|
|
88
|
+
rationale: "spaces between the fence and its info string; CommonMark discards them and some authors line their fences up with them",
|
|
89
|
+
witnesses: [
|
|
90
|
+
{
|
|
91
|
+
a: "```ts\nx\n```",
|
|
92
|
+
b: "``` ts\nx\n```",
|
|
93
|
+
note: "the gap before the language",
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "code-fence:length",
|
|
99
|
+
layer: "mdast-geometry",
|
|
100
|
+
captures: ["code.sourceFenceLength"],
|
|
101
|
+
rationale: "how many fence characters; three is the minimum and more is a deliberate widening around a body that contains a closer-shaped line",
|
|
102
|
+
witnesses: [
|
|
103
|
+
{
|
|
104
|
+
a: "```\nx\n```",
|
|
105
|
+
b: "````\nx\n````",
|
|
106
|
+
note: "three characters against four",
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "definition:layout",
|
|
112
|
+
layer: "mdast-geometry",
|
|
113
|
+
captures: ["definition.sourceLayout"],
|
|
114
|
+
rationale: "whether a link reference definition sits on one line or wraps after the colon",
|
|
115
|
+
witnesses: [
|
|
116
|
+
{
|
|
117
|
+
a: "[a]: u",
|
|
118
|
+
b: "[a]:\n u",
|
|
119
|
+
note: "the destination on the label's line against the line below it",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "definition:title-quote",
|
|
125
|
+
layer: "mdast-geometry",
|
|
126
|
+
captures: ["definition.sourceTitleMarker"],
|
|
127
|
+
rationale: "which of the three title containers a definition used — `\"`, `'` or `()`",
|
|
128
|
+
witnesses: [
|
|
129
|
+
{
|
|
130
|
+
a: '[a]: u "t"',
|
|
131
|
+
b: "[a]: u 't'",
|
|
132
|
+
note: "double quotes against single",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "emphasis:delimiter",
|
|
138
|
+
layer: "mdast-geometry",
|
|
139
|
+
captures: ["emphasis.sourceDelimiter"],
|
|
140
|
+
rationale: "`*` against `_` for a single-delimiter emphasis run",
|
|
141
|
+
witnesses: [
|
|
142
|
+
{ a: "*em*", b: "_em_", note: "the delimiter character" },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "hard-break:style",
|
|
147
|
+
layer: "mdast-geometry",
|
|
148
|
+
captures: ["break.sourceStyle"],
|
|
149
|
+
rationale: "a hard break spelled as a trailing backslash or as two trailing spaces; one of them is invisible in every editor ever written",
|
|
150
|
+
witnesses: [
|
|
151
|
+
{
|
|
152
|
+
a: "a\\\nb",
|
|
153
|
+
b: "a \nb",
|
|
154
|
+
note: "backslash against two spaces",
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "heading:style",
|
|
160
|
+
layer: "mdast-geometry",
|
|
161
|
+
captures: ["heading.sourceStyle"],
|
|
162
|
+
rationale: "atx (`# T`) against setext (`T` over `=`); the same heading, and the only two spellings CommonMark has for depth one and two",
|
|
163
|
+
witnesses: [
|
|
164
|
+
{
|
|
165
|
+
a: "# T",
|
|
166
|
+
b: "T\n=",
|
|
167
|
+
note: "atx against setext — moves the underline length with it, which is the next axis's business",
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "heading:trailing-hashes",
|
|
173
|
+
layer: "mdast-geometry",
|
|
174
|
+
captures: ["heading.sourceTrailingHashes"],
|
|
175
|
+
rationale: "the optional closing run on an atx heading, which CommonMark discards entirely",
|
|
176
|
+
witnesses: [
|
|
177
|
+
{ a: "# T", b: "# T #", note: "the closing hash" },
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "inline-code:fence-length",
|
|
182
|
+
layer: "mdast-geometry",
|
|
183
|
+
captures: ["inlineCode.sourceFenceLength"],
|
|
184
|
+
rationale: "how many backticks open and close a code span; more than one is how an author writes a backtick inside one",
|
|
185
|
+
witnesses: [
|
|
186
|
+
{ a: "`x`", b: "``x``", note: "one backtick against two" },
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: "inline-code:padding",
|
|
191
|
+
layer: "mdast-geometry",
|
|
192
|
+
captures: ["inlineCode.sourcePadded"],
|
|
193
|
+
rationale: "the one space either side that CommonMark strips, which is how `` ` `` itself is written inside a span",
|
|
194
|
+
witnesses: [
|
|
195
|
+
{ a: "`x`", b: "` x `", note: "the stripped padding" },
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "lexical:backslash-escape",
|
|
200
|
+
layer: "mdast-geometry",
|
|
201
|
+
captures: ["text.escapedChars"],
|
|
202
|
+
rationale: "where the author put a backslash in front of a character that did not need one; the value is the same string either way, and re-deriving the escapes from it is how a serializer starts escaping things nobody typed",
|
|
203
|
+
witnesses: [
|
|
204
|
+
{
|
|
205
|
+
a: "a\\*b",
|
|
206
|
+
b: "a*b",
|
|
207
|
+
note: "an escaped asterisk against a bare one that opens nothing",
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: "link:angle-url",
|
|
213
|
+
layer: "mdast-geometry",
|
|
214
|
+
captures: ["link.sourceUrlForm"],
|
|
215
|
+
rationale: "`[a](<u>)` against `[a](u)`; the angle form is what an author reaches for when the destination has a space in it, and keeping it is how the habit survives an edit elsewhere",
|
|
216
|
+
witnesses: [
|
|
217
|
+
{ a: "[a](u)", b: "[a](<u>)", note: "the angle brackets around the destination" },
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "link:autolink-form",
|
|
222
|
+
layer: "mdast-geometry",
|
|
223
|
+
captures: ["link.sourceStyle"],
|
|
224
|
+
rationale: "a bare GFM literal autolink against the same url in angle brackets; both are one link node with the same href",
|
|
225
|
+
witnesses: [
|
|
226
|
+
{
|
|
227
|
+
a: "https://e.com",
|
|
228
|
+
b: "<https://e.com>",
|
|
229
|
+
note: "the gfm literal form against the angle form",
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: "link:title-quote",
|
|
235
|
+
layer: "mdast-geometry",
|
|
236
|
+
captures: ["link.sourceTitleMarker"],
|
|
237
|
+
rationale: "which of the three title containers an inline link used — `\"`, `'` or `()`",
|
|
238
|
+
witnesses: [
|
|
239
|
+
{ a: '[a](u "t")', b: "[a](u 't')", note: "double quotes against single" },
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: "list:bullet-marker",
|
|
244
|
+
layer: "mdast-geometry",
|
|
245
|
+
captures: ["list.bulletMarker"],
|
|
246
|
+
rationale: "`-`, `*` or `+`; mdast-util-to-markdown picks one globally and alternates it between adjacent lists, so without the axis a document converges on whichever the serializer likes",
|
|
247
|
+
witnesses: [
|
|
248
|
+
{ a: "- a", b: "* a", note: "the bullet character" },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: "list:item-marker-spacing",
|
|
253
|
+
layer: "mdast-geometry",
|
|
254
|
+
captures: ["listItem.sourceMarkerSpacing"],
|
|
255
|
+
rationale: "spaces between a list marker and its content; one to four all mean the same item and set different continuation indents",
|
|
256
|
+
witnesses: [
|
|
257
|
+
{ a: "- a", b: "- a", note: "one space after the bullet against three" },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: "list:item-ordinal",
|
|
262
|
+
layer: "mdast-geometry",
|
|
263
|
+
captures: ["listItem.sourceOrdinal"],
|
|
264
|
+
rationale: "the number each ordered item was actually typed with; mdast keeps the list's `start` and nothing else, so `1. 1. 1.` and `1. 2. 3.` are the same tree",
|
|
265
|
+
witnesses: [
|
|
266
|
+
{
|
|
267
|
+
a: "1. a\n1. b",
|
|
268
|
+
b: "1. a\n2. b",
|
|
269
|
+
note: "all-ones against counting up",
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: "list:ordered-delimiter",
|
|
275
|
+
layer: "mdast-geometry",
|
|
276
|
+
captures: ["list.listMarkerDelimiter"],
|
|
277
|
+
rationale: "`1.` against `1)` for an ordered list",
|
|
278
|
+
witnesses: [
|
|
279
|
+
{ a: "1. a", b: "1) a", note: "the character after the ordinal" },
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
id: "setext:underline-length",
|
|
284
|
+
layer: "mdast-geometry",
|
|
285
|
+
captures: ["heading.sourceUnderlineLength"],
|
|
286
|
+
rationale: "how long the `=` or `-` run under a setext heading is; one character is enough and authors rule the whole width",
|
|
287
|
+
witnesses: [
|
|
288
|
+
{ a: "T\n=", b: "T\n===", note: "the underline run" },
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: "strike:delimiter",
|
|
293
|
+
layer: "mdast-geometry",
|
|
294
|
+
captures: ["delete.sourceDelimiter"],
|
|
295
|
+
rationale: "GFM's `~x~` against `~~x~~`; the single-tilde form is not in every reader's dialect, which is why the capture lands and the replay is a rendering decision",
|
|
296
|
+
witnesses: [
|
|
297
|
+
{ a: "~~x~~", b: "~x~", note: "two tildes against one" },
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
id: "strong:delimiter",
|
|
302
|
+
layer: "mdast-geometry",
|
|
303
|
+
captures: ["strong.sourceDelimiter"],
|
|
304
|
+
rationale: "`**` against `__` for a double-delimiter run",
|
|
305
|
+
witnesses: [
|
|
306
|
+
{ a: "**s**", b: "__s__", note: "the delimiter run" },
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: "structured-block:entry-bullet",
|
|
311
|
+
layer: "structured-block",
|
|
312
|
+
ledgerReasons: ["entry-bullet-marker"],
|
|
313
|
+
rationale: "a status or chat entry may be authored bare, with `-`, or with `*`; the block grammars strip the marker before reading the entry, so all three parse to the same timeline and which one was typed is a spelling rather than content",
|
|
314
|
+
witnesses: [
|
|
315
|
+
{
|
|
316
|
+
lang: "chat",
|
|
317
|
+
a: "- @mara: on it\n",
|
|
318
|
+
b: "* @mara: on it\n",
|
|
319
|
+
note: "the entry bullet — the id the wave-1 drop ledger already spends",
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
lang: "status",
|
|
323
|
+
a: "state: building\n- @ada: on it\n",
|
|
324
|
+
b: "state: building\n* @ada: on it\n",
|
|
325
|
+
note: "the same axis one block language over, because the ledger entry is not chat-specific",
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: "table:cell-padding",
|
|
331
|
+
layer: "mdast-geometry",
|
|
332
|
+
captures: ["tableCell.sourcePadding"],
|
|
333
|
+
rationale: "the spaces inside each cell's pipes, which is what makes a hand-aligned table a grid instead of a wall",
|
|
334
|
+
witnesses: [
|
|
335
|
+
{
|
|
336
|
+
a: "| a |\n| - |",
|
|
337
|
+
b: "|a|\n| - |",
|
|
338
|
+
note: "padded cells against tight ones",
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: "table:delimiter-dashes",
|
|
344
|
+
layer: "mdast-geometry",
|
|
345
|
+
captures: ["table.sourceDashCounts"],
|
|
346
|
+
rationale: "how many dashes each delimiter cell holds; one is legal and authors widen them to the column",
|
|
347
|
+
witnesses: [
|
|
348
|
+
{
|
|
349
|
+
a: "| a |\n| - |",
|
|
350
|
+
b: "| a |\n| --- |",
|
|
351
|
+
note: "a one-dash delimiter against a three-dash one",
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id: "table:delimiter-padding",
|
|
357
|
+
layer: "mdast-geometry",
|
|
358
|
+
captures: ["table.sourceAlignmentPadding"],
|
|
359
|
+
rationale: "the spaces around the dashes in the delimiter row, captured apart from the dash count because the two move independently",
|
|
360
|
+
witnesses: [
|
|
361
|
+
{
|
|
362
|
+
a: "| a |\n|---|",
|
|
363
|
+
b: "| a |\n| --- |",
|
|
364
|
+
note: "the same three dashes, padded and not",
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
id: "table:outer-pipes",
|
|
370
|
+
layer: "mdast-geometry",
|
|
371
|
+
captures: ["table.sourceOuterPipes"],
|
|
372
|
+
rationale: "whether the rows carry their leading and trailing pipes; GFM makes both optional and the pipe-less form is a real authoring style",
|
|
373
|
+
witnesses: [
|
|
374
|
+
{
|
|
375
|
+
a: "| a | b |\n| :-- | --: |\n| 1 | 2 |",
|
|
376
|
+
b: "a | b\n:-- | --:\n1 | 2",
|
|
377
|
+
note: "edge pipes against none — the alignment colons keep the delimiter row from reading as a setext underline",
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
id: "task-list:checkbox-char",
|
|
383
|
+
layer: "mdast-geometry",
|
|
384
|
+
captures: ["listItem.sourceCheckboxChar"],
|
|
385
|
+
rationale: "`[x]` against `[X]`; GFM checks the box either way and one of them is what the author's other tool writes",
|
|
386
|
+
witnesses: [
|
|
387
|
+
{ a: "- [x] a", b: "- [X] a", note: "the checkbox character's case" },
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
id: "thematic-break:marker",
|
|
392
|
+
layer: "mdast-geometry",
|
|
393
|
+
captures: ["thematicBreak.sourceRaw"],
|
|
394
|
+
rationale: "the whole authored run — `***`, `---`, `- - -`, `___` and every spaced variant are one thematic break",
|
|
395
|
+
witnesses: [
|
|
396
|
+
{ a: "***", b: "- - -", note: "the marker run, verbatim" },
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
];
|
|
400
|
+
/** Every id on the list, for a cheap membership test. */
|
|
401
|
+
export const FORMAT_AXIS_IDS = new Set(FORMAT_AXES.map((axis) => axis.id));
|
|
402
|
+
/**
|
|
403
|
+
* The axis with this id.
|
|
404
|
+
*
|
|
405
|
+
* Throws rather than returning undefined: every call site here is spending an
|
|
406
|
+
* id that the type system already proved exists, so a miss means the list and
|
|
407
|
+
* the union disagree, which is a bug in this file and not a case a caller
|
|
408
|
+
* should be writing a branch for.
|
|
409
|
+
*/
|
|
410
|
+
export function formatAxis(id) {
|
|
411
|
+
const found = FORMAT_AXES.find((axis) => axis.id === id);
|
|
412
|
+
if (!found)
|
|
413
|
+
throw new Error(`no format axis with id ${id}`);
|
|
414
|
+
return found;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* The axis that owns a captured geometry key, spelled `<nodeType>.<dataKey>`.
|
|
418
|
+
*
|
|
419
|
+
* Undefined here is the interesting answer, not an error: it means the capture
|
|
420
|
+
* layer records a spelling the vocabulary cannot name, which is exactly what
|
|
421
|
+
* the closure test is looking for.
|
|
422
|
+
*/
|
|
423
|
+
export function axisForCapture(key) {
|
|
424
|
+
return FORMAT_AXES.find((axis) => axis.layer === "mdast-geometry" && axis.captures.includes(key));
|
|
425
|
+
}
|
|
426
|
+
/** Every `<nodeType>.<dataKey>` the vocabulary claims, deduplicated. */
|
|
427
|
+
export function claimedCaptureKeys() {
|
|
428
|
+
const out = new Set();
|
|
429
|
+
for (const axis of FORMAT_AXES) {
|
|
430
|
+
if (axis.layer !== "mdast-geometry")
|
|
431
|
+
continue;
|
|
432
|
+
for (const key of axis.captures)
|
|
433
|
+
out.add(key);
|
|
434
|
+
}
|
|
435
|
+
return [...out].sort();
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Every blank-run `data` key the vocabulary claims, deduplicated.
|
|
439
|
+
*
|
|
440
|
+
* Kept apart from {@link claimedCaptureKeys} rather than merged into it because
|
|
441
|
+
* the two are spelled differently on purpose — one carries a node type and the
|
|
442
|
+
* other cannot — and a single list would have to pick one spelling and be wrong
|
|
443
|
+
* about half its entries.
|
|
444
|
+
*/
|
|
445
|
+
export function claimedBlankRunKeys() {
|
|
446
|
+
const out = new Set();
|
|
447
|
+
for (const axis of FORMAT_AXES) {
|
|
448
|
+
if (axis.layer !== "blank-runs")
|
|
449
|
+
continue;
|
|
450
|
+
for (const key of axis.dataKeys)
|
|
451
|
+
out.add(key);
|
|
452
|
+
}
|
|
453
|
+
return [...out].sort();
|
|
454
|
+
}
|