OverviewFilesCommitsIssuesDocsReleasesPeers

favela-markdown commits / 89eff9d

spec+lib: the dialect is closed, so syntax outside it is text — and one option was widening it
Writes hyper-social #43's last unwritten amendment as a normative rule in 2.1, with three vectors (39 total). Writing the vector caught a real defect: micromark's gfmStrikethrough defaults singleTilde:true, consuming ~x~ although 2.1 pins strikethrough as ~~x~~. So the rule constrains parser OPTIONS, not just which extensions are enabled - every option that affects accepted syntax is part of the dialect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
89eff9d3b4437e09fb18b230e01e654784742fc6
by plankatron <fleeky@gmail.com> · 2026-09-05T23:14:16.000Z

Changed files (4) +47 -5

lib/strip.js +5 -2
……@@ -1,7 +1,7 @@
11// strip(source) -> { text, facets } — spec/markdown-strip.md
22//
33// The strip's output is what bsky sees and what a CID commits to, so every rule here cites the
4// spec section it implements and nothing is left to "whatever the parser does" (§1). The 36
4// spec section it implements and nothing is left to "whatever the parser does" (§1). The conformance
55// vectors are the oracle; this file exists to reproduce them byte-for-byte.
66//
77// Structure, and why:
……@@ -32,7 +32,10 @@ import { directive } from 'micromark-extension-directive'
3232import { directiveFromMarkdown } from 'mdast-util-directive'
3333
3434const MICROMARK = {
35 extensions: [gfmStrikethrough(), gfmTable(), gfmFootnote(), directive()],
35 // ⛔ singleTilde:false — micromark defaults it TRUE, which would consume `~x~` as strikethrough.
36 // §2.1 pins the extension as `~~x~~`, and the dialect is CLOSED: syntax outside it is text, not
37 // markup. Caught by the `single-tilde-is-literal` vector, which exists because of that rule.
38 extensions: [gfmStrikethrough({ singleTilde: false }), gfmTable(), gfmFootnote(), directive()],
3639 mdastExtensions: [
3740 gfmStrikethroughFromMarkdown(),
3841 gfmTableFromMarkdown(),
spec/markdown-strip.md +18 -0
……@@ -47,6 +47,24 @@ strip(source: string) -> { text: string, facets: Facet[] }
4747An implementation MUST NOT enable further extensions. A new one is a spec change, because it
4848changes the bytes of every record that uses it.
4949
50⛔ **The dialect is CLOSED, so syntax outside it is TEXT, not markup.** Decided 2026-08-11
51(hyper-social #43) as *"unknown syntax → literal text"*, written 2026-09-05. `==highlight==`,
52`$$x$$`, `{: .cls}`, a task-list checkbox and a definition list are all **literal text**, because
53they are not in the table above. This is not leniency — it is what "the dialect is exactly these six"
54means, and it is the property that lets a future extension be added without reinterpreting a single
55existing record.
56
57⚠ **It also constrains how the parser is CONFIGURED, not just which extensions are enabled.** A
58parser option that widens a pinned syntax silently breaks the rule: micromark's `gfmStrikethrough`
59defaults `singleTilde: true`, which consumes `~x~` even though §2.1 pins strikethrough as `~~x~~`.
60That defect shipped in `lib/strip.js` and was caught only by writing the vector for this rule
61(`single-tilde-is-literal`). **Every parser option that affects accepted syntax is part of the
62dialect and must be pinned explicitly.**
63
64**One exception, and only one:** an unknown *directive name* still yields its contents (§4.8), because
65`:::name` itself IS in the dialect — only the name is unrecognised. `unknown-directive-keeps-contents`
66pins it.
67
5068---
5169
5270## 3. Inline rules
spec/vectors/markdown-strip.json +22 -1
……@@ -329,6 +329,27 @@
329329 "value": "https://two.example"
330330 }
331331 ]
332 },
333 {
334 "name": "unknown-syntax-is-literal",
335 "why": "§2.1 the dialect is closed, so syntax outside it is TEXT, never markup (hyper-social #43)",
336 "source": "==highlight== and $$x$$ and {: .cls}",
337 "text": "==highlight== and $$x$$ and {: .cls}",
338 "facets": []
339 },
340 {
341 "name": "single-tilde-is-literal",
342 "why": "§2.1 strikethrough is pinned as ~~x~~; a SINGLE tilde is outside the dialect and stays text",
343 "source": "~sub~ not strikethrough",
344 "text": "~sub~ not strikethrough",
345 "facets": []
346 },
347 {
348 "name": "unknown-directive-keeps-contents",
349 "why": "§4.8 the ONE exception — an unknown directive NAME still yields its contents, not literal text",
350 "source": ":::unknownName\nbody\n:::",
351 "text": "body",
352 "facets": []
332353 }
333354 ]
334}
355}
test/strip.test.js +2 -2
……@@ -32,8 +32,8 @@ function expandFacet (f) {
3232
3333const utf8 = (s) => Buffer.from(s, 'utf8')
3434
35test('strip: all 36 vectors reproduce byte-for-byte', (t) => {
36 t.is(VECTORS.length, 36, 'the vector set is the one the spec pins')
35test('strip: every vector reproduces byte-for-byte', (t) => {
36 t.ok(VECTORS.length >= 36, 'the pinned vector set may grow, never shrink')
3737
3838 for (const v of VECTORS) {
3939 const got = strip(v.source)