@tabnas/markdown 0.6.2 → 0.7.2

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/src/markdown.ts CHANGED
@@ -17,8 +17,10 @@
17
17
  // they are imported as such. Without `import type`, Node's type stripping
18
18
  // keeps them in the emitted import and demands runtime exports that do not
19
19
  // exist.
20
- import type { Tabnas, Plugin, Rule, Context } from '@tabnas/parser'
20
+ import type { Tabnas, Plugin } from '@tabnas/parser'
21
21
 
22
+ import { makeMdActions, makeMdLineMatcher } from './engine-block.ts'
23
+ import { makeInlineTn, parseInlinesEngine } from './engine-inline.ts'
22
24
  import { parse } from './commonmark.ts'
23
25
  import { toAst } from './ast.ts'
24
26
  import { renderHTML } from './html.ts'
@@ -40,49 +42,16 @@ export type MarkdownOptions = {
40
42
  }
41
43
 
42
44
  // ---------------------------------------------------------------------------
43
- // Grammar source.
45
+ // Grammar.
44
46
  //
45
- // The prose grammar is intentionally trivial: block structure is decided by
46
- // the line-oriented algorithm in `block.ts`, not by declarative alts, so the
47
- // `markdown` rule exists only to consume the token stream. The file is kept
48
- // because `ts/embed-grammar.js` embeds it verbatim into both runtimes; see
49
- // AGENTS.md. It is documentation of the entry rule, not a source of behaviour.
50
-
51
- // --- BEGIN EMBEDDED markdown-grammar.jsonic ---
52
- const grammarText = `
53
- # Markdown entry rule — CommonMark 0.31.2
54
- #
55
- # This grammar is deliberately trivial, and it is NOT where the parser
56
- # lives. Block structure is decided by the two-phase line algorithm of
57
- # spec Appendix A, implemented in block.ts / block.go, and inline
58
- # structure by the delimiter and bracket stacks in inline.ts / inline.go.
59
- # None of that is expressible as declarative alts, which is why none of
60
- # it is here.
61
- #
62
- # The markdown rule exists only to give the engine an entry point and to
63
- # consume the token stream so its trailing-content check passes. The bo
64
- # action reads the whole source via ctx.src() and hands it to the parser.
65
- # Everything the rule does is in those two lines below.
66
- #
67
- # Consequences worth knowing rather than discovering:
68
- # - The railroad diagram generated from this file is empty: a bare
69
- # track with no boxes on it. That is accurate, not a rendering fault.
70
- # - Editing this file changes documentation, not behaviour. The one
71
- # exception is the open/close alts, which really are the rule.
72
- #
73
- # The file is kept because ts/embed-grammar.js embeds it verbatim into
74
- # both runtimes as grammarText (see AGENTS.md). It may not contain
75
- # backticks: the Go copy is a raw string literal.
76
- {
77
- rule: markdown: open: [
78
- { s: '#ZZ' }
79
- ]
80
- rule: markdown: close: [
81
- { s: '#ZZ' }
82
- ]
83
- }
84
- `
85
- // --- END EMBEDDED markdown-grammar.jsonic ---
47
+ // The grammar is LIVE and lives in code: the `markdown`/`line` rules below,
48
+ // the `mdLine` matcher (engine-block.ts) and the inline instance's rule and
49
+ // matcher set (engine-inline.ts). `debug.model()` serializes it and
50
+ // `ts/tools/gen-railroad.mjs` draws ts/doc/grammar.{svg,txt} from a live
51
+ // instance, so the documentation is derived from the rules the engine
52
+ // actually parses with. The historical `markdown-grammar.jsonic` file and
53
+ // its embed step are gone: they documented an inert single-rule bypass that
54
+ // no longer exists (dx-report §47).
86
55
 
87
56
  // ---------------------------------------------------------------------------
88
57
  // Public parse API (engine-free)
@@ -114,44 +83,105 @@ export function parseTree(src: string, opts?: MarkdownOptions | ParserOptions):
114
83
  // ---------------------------------------------------------------------------
115
84
  // Plugin wiring
116
85
  //
117
- // The `markdown` rule's `bo` action reads the whole source via `ctx.src()` and
118
- // hands it to the parser; the alts then consume the token stream so the
119
- // engine's trailing-content check (lex must end at #ZZ) passes. `#AA` is the
120
- // ANY-token wildcard and `r:'markdown'` loops at the same depth, consuming one
121
- // token per iteration until only #ZZ remains.
86
+ // The engine parse IS the parse: the `mdLine` custom matcher (engine-block.ts)
87
+ // lexes one `#LB` token per physical line, `parse.prepare` seeds a fresh
88
+ // `BlockParser` on `ctx.u.md`, the `line` rule feeds each matched line to the
89
+ // shared `incorporateLine`, and the `markdown` rule's close action on `#ZZ`
90
+ // finalizes, runs the shared inline phase, and projects the public AST into
91
+ // the rule's node. No recognition or block decision happens here — the
92
+ // engine owns tokenization, dispatch and state carriage; the algorithm stays
93
+ // in the engine-free core (the anti-drift rule, dx-report §42), which is
94
+ // what keeps this path byte-identical to `parseDocument` (asserted by the
95
+ // differential gate and the engine-path conformance harness).
122
96
 
123
97
  const Markdown: Plugin = (tn: Tabnas, options?: MarkdownOptions) => {
124
98
  const opts = resolveOptions(options)
125
99
 
100
+ // Human descriptions for the block token, surfaced in railroad diagram
101
+ // legends (read off the live config by @tabnas/railroad).
102
+ tn.options({
103
+ config: {
104
+ modify: {
105
+ 'md-tokendesc': (cfg: any) => {
106
+ cfg.tokenDesc = Object.assign(cfg.tokenDesc || {}, {
107
+ '#LB': 'one physical line (text, blank and table-arming bits in use)',
108
+ })
109
+ },
110
+ },
111
+ },
112
+ })
113
+
114
+ // The inline phase's own engine instance — nested parsing, one instance
115
+ // per plugin install, reused across every parse and every leaf block (see
116
+ // engine-inline.ts).
117
+ const inlineTn = makeInlineTn(opts)
118
+ const actions = makeMdActions(opts, (doc, refmap) =>
119
+ parseInlinesEngine(inlineTn, doc, refmap, opts),
120
+ )
121
+
126
122
  tn.options({ rule: { start: 'markdown' } })
127
123
 
128
- // The parser reads `ctx.src()` directly rather than the token stream, so
129
- // disable the lexers that would corrupt Markdown syntax before it gets
130
- // there: backtick code spans lex as unterminated strings, `# heading` as a
131
- // comment, and `1. list` as a number.
124
+ // Every built-in matcher is off — deliberate configuration, not defense.
125
+ // Markdown's block alphabet is *lines*, so the one registered matcher is
126
+ // the complete lexical description of this phase: `mdLine`, at an order
127
+ // ahead of every built-in, consuming each physical line whole. (The
128
+ // built-ins would misread the syntax anyway: backtick code spans lex as
129
+ // unterminated strings, `# heading` as a comment, `1. list` as a number.)
132
130
  tn.options({
131
+ space: { lex: false },
132
+ line: { lex: false },
133
133
  string: { lex: false },
134
134
  comment: { lex: false },
135
135
  number: { lex: false },
136
136
  value: { lex: false },
137
- lex: { emptyResult: { type: 'document', children: [] } },
138
- })
139
-
140
- const refs: Record<string, any> = {
141
- '@markdown-bo': (r: Rule, ctx: Context) => {
142
- const src: string = ctx.src()
143
- r.node = parseDocument(src, opts)
137
+ text: { lex: false },
138
+ lex: {
139
+ emptyResult: { type: 'document', children: [] },
140
+ match: { mdLine: { order: 1e5, make: makeMdLineMatcher } },
144
141
  },
145
- }
142
+ parse: { prepare: { 'md-reset': actions.prepare } },
143
+ })
146
144
 
147
145
  tn.rule('markdown', (rs) => {
148
146
  return rs
149
147
  .clear()
150
- .bo(refs['@markdown-bo'] as any)
151
- .open([{ s: '#AA', r: 'markdown' }, {}])
152
- .close([{ s: '#ZZ' }, {}])
148
+ .open([{ p: 'line' }])
149
+ .close([{ s: '#ZZ', a: actions.finish }, {}])
150
+ })
151
+
152
+ // One `#LB` consumed per iteration, tail-recursing via `r:` until only
153
+ // `#ZZ` remains; the empty alts are the fall-through that hands control
154
+ // back to `markdown`'s close.
155
+ //
156
+ // The first alt is the GFM extension seam: tagged `g: 'gfm'`, gated on
157
+ // the token's `tblArm` bit, arming the shared table probe for this line.
158
+ // It is injected exactly the way a downstream dialect would extend this
159
+ // grammar — an alt ahead of the base one, subtractable by its group tag —
160
+ // and `gfm: false` removes it below via `rule.exclude`, so the base
161
+ // dialect is the grammar minus the tagged alts, visibly (`debug.model()`).
162
+ tn.rule('line', (rs) => {
163
+ return rs
164
+ .clear()
165
+ .open([
166
+ {
167
+ s: '#LB',
168
+ c: (_r: any, ctx: any) => true === ctx.t0.use.tblArm,
169
+ r: 'line',
170
+ a: actions.lineGfm,
171
+ g: 'gfm',
172
+ },
173
+ { s: '#LB', r: 'line', a: actions.line },
174
+ {},
175
+ ])
176
+ .close([{}])
153
177
  })
154
178
 
179
+ // The GFM dialect is subtracted, not branched around: without the option,
180
+ // the tagged alts above simply do not exist in this instance's grammar.
181
+ if (true !== opts.gfm) {
182
+ tn.options({ rule: { exclude: 'gfm' } })
183
+ }
184
+
155
185
  // Not part of the tabnas contract, but useful for tests, docs and callers
156
186
  // that want HTML or the native tree without a second import.
157
187
  ;(tn as any).markdown = {
@@ -164,7 +194,7 @@ const Markdown: Plugin = (tn: Tabnas, options?: MarkdownOptions) => {
164
194
 
165
195
  Markdown.defaults = { gfm: true, breaks: false } as MarkdownOptions
166
196
 
167
- export { VERSION, Markdown, grammarText }
197
+ export { VERSION, Markdown }
168
198
 
169
199
  export type {
170
200
  DocumentNode,
@@ -203,4 +233,4 @@ export { renderHTML } from './html.ts'
203
233
  // VERSION is this package's version. It MUST equal package.json "version":
204
234
  // the release orchestrator rewrites both, and the version test fails the
205
235
  // build if they drift. Mirrors `const VERSION` in go/markdown.go.
206
- const VERSION = '0.6.2'
236
+ const VERSION = '0.7.2'