aontu 0.60.0 → 0.61.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/src/cli.ts CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  renderProfile,
28
28
  } from './aontu'
29
29
  import type { RenderCoverage, RenderReport } from './render'
30
- import { desugarTemplate, resugarTemplate, markerFor } from './template'
30
+ import { desugarTemplate, resugarTemplate, templateOutputs, markerFor } from './template'
31
31
  import { outsideRoot } from './mcp'
32
32
  import { sarifReport } from './report-sarif'
33
33
  import { main as lspMain } from './lsp-server'
@@ -83,7 +83,7 @@ const HELP = `Usage: aontu [options] [file]
83
83
  aontu why <path> [options] <file>
84
84
  aontu set <path>=<value>... --entry <file> --overlay <file>
85
85
  aontu agentsmd [--write <AGENTS.md>] <file>
86
- aontu fmt [-w|-l|--check|-d|--lint] <file>...
86
+ aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] <file>...
87
87
  aontu lsp
88
88
  aontu mcp [--root <dir>]
89
89
 
@@ -351,10 +351,18 @@ Fmt options:
351
351
  --lint Report the style findings, key case and repeated
352
352
  shapes, on standard error, and print nothing else
353
353
  --strict With --lint, and exit 1 when there is a finding
354
+ --marker <t> The file is a generator, and this is its marker
355
+ (default //-, and #- --- /*- by extension)
354
356
 
355
357
  The fmt verb prints one document in the agreed form; with no file it
356
358
  reads standard input. Several files need one of the options above.
357
359
 
360
+ A file whose extension is not .aon is a GENERATOR, as it is for render:
361
+ the aontu its marker lines carry is formatted, the marker stands at the
362
+ left margin with the aontu indented after it, and every line of output
363
+ is held on a line of its own. A file with no marker line in it is
364
+ another language's, and is refused.
365
+
358
366
  Fmt exit codes: 0 formatted or clean, 1 a --check file would change or
359
367
  a --strict finding, 2 usage, 4 a document does not parse.
360
368
 
@@ -3945,7 +3953,8 @@ function runAgentsMd(argv: string[]): number {
3945
3953
  // the form itself is the library's (ts/src/format.ts), and the two
3946
3954
  // ports agree on it row by row in test/spec/fmt.tsv.
3947
3955
 
3948
- const FMT_HELP = 'aontu fmt [-w|-l|--check|-d|--lint] <file>... (try --help)'
3956
+ const FMT_HELP =
3957
+ 'aontu fmt [-w|-l|--check|-d|--lint] [--marker <token>] <file>... (try --help)'
3949
3958
 
3950
3959
  type FmtFlags = {
3951
3960
  write: boolean, list: boolean, check: boolean, diff: boolean, lint: boolean, strict: boolean,
@@ -3953,11 +3962,13 @@ type FmtFlags = {
3953
3962
 
3954
3963
  function runFmt(argv: string[]): number | Promise<number> {
3955
3964
  const files: string[] = []
3965
+ let marker: string | undefined = undefined
3956
3966
  const flags: FmtFlags = {
3957
3967
  write: false, list: false, check: false, diff: false, lint: false, strict: false,
3958
3968
  }
3959
3969
 
3960
- for (const arg of argv) {
3970
+ for (let i = 0; i < argv.length; i++) {
3971
+ const arg = argv[i]
3961
3972
  if ('-h' === arg || '--help' === arg) {
3962
3973
  process.stdout.write(HELP)
3963
3974
  return 0
@@ -3981,6 +3992,16 @@ function runFmt(argv: string[]): number | Promise<number> {
3981
3992
  flags.lint = true
3982
3993
  flags.strict = true
3983
3994
  }
3995
+ else if ('--marker' === arg) {
3996
+ // THE MARKER SAYS THE FILE IS A GENERATOR, whatever its
3997
+ // extension: `render` and `template` take the same option for
3998
+ // the same reason, a language the table has never seen.
3999
+ marker = argv[++i]
4000
+ if (null == marker) {
4001
+ process.stderr.write('aontu: --marker needs a token\n')
4002
+ return 2
4003
+ }
4004
+ }
3984
4005
  else if (arg.startsWith('-')) {
3985
4006
  process.stderr.write(`aontu: unknown fmt option ${arg} (try --help)\n`)
3986
4007
  return 2
@@ -4002,7 +4023,7 @@ function runFmt(argv: string[]): number | Promise<number> {
4002
4023
  let src = ''
4003
4024
  process.stdin.setEncoding('utf8')
4004
4025
  process.stdin.on('data', (d) => (src += d))
4005
- process.stdin.on('end', () => resolve(fmtOne('<stdin>', src, flags)))
4026
+ process.stdin.on('end', () => resolve(fmtOne('<stdin>', src, flags, marker)))
4006
4027
  })
4007
4028
  }
4008
4029
 
@@ -4018,23 +4039,6 @@ function runFmt(argv: string[]): number | Promise<number> {
4018
4039
 
4019
4040
  let worst = 0
4020
4041
  for (const file of files) {
4021
- // FMT FORMATS AONTU SOURCE, AND THE EXTENSION SAYS WHAT A FILE IS
4022
- // (ADR-012's rule, and the one `render` reads a template by).
4023
- // A TEMPLATE FILE IS NOT AONTU (docs/design/TEMPLATE.0.md; P8):
4024
- // its marker lines are fragments of a document and its other lines
4025
- // are the target's, so there is nothing here to format that would
4026
- // not also rewrite the output. Refused rather than attempted, and
4027
- // refused BY NAME rather than by a parse failure, because a `#-`
4028
- // template parses: `#` opens a comment, so every marker line
4029
- // vanishes and what is left is read as a document that was never
4030
- // written. The verb answered `0` over one, having understood none
4031
- // of it.
4032
- if (!/[.](aon|aontu)$/.test(file)) {
4033
- process.stderr.write(
4034
- `aontu: ${file} is not aontu source (.aon, .aontu); a generator ` +
4035
- 'written in the target\'s own syntax is aontu template\'s\n')
4036
- return 2
4037
- }
4038
4042
  let src: string
4039
4043
  try {
4040
4044
  src = readFileSync(file, 'utf8')
@@ -4043,11 +4047,46 @@ function runFmt(argv: string[]): number | Promise<number> {
4043
4047
  process.stderr.write(`aontu: cannot read ${err.path}: ${err.message}\n`)
4044
4048
  return 2
4045
4049
  }
4046
- worst = Math.max(worst, fmtOne(file, src, flags))
4050
+ const mark = fmtMarker(file, src, marker)
4051
+ if (false === mark) {
4052
+ process.stderr.write(
4053
+ `aontu: ${file} is not aontu source (.aon, .aontu) and carries no ` +
4054
+ `${markerFor(file)} marker line, so there is no aontu in it to ` +
4055
+ 'format; --marker names the marker for a language the table does ' +
4056
+ 'not know\n')
4057
+ return 2
4058
+ }
4059
+ worst = Math.max(worst, fmtOne(file, src, flags, mark))
4047
4060
  }
4048
4061
  return worst
4049
4062
  }
4050
4063
 
4064
+ // WHAT A FILE IS, BY ITS EXTENSION (ADR-012's rule, and the one
4065
+ // `render` reads an entry by): `.aon` and `.aontu` are aontu source,
4066
+ // and anything else is a GENERATOR written in the target's own syntax
4067
+ // (docs/design/TEMPLATE.0.md), whose marker lines carry the document
4068
+ // this formats and whose other lines are output. `undefined` is aontu,
4069
+ // a string is the generator's marker, and `false` is neither.
4070
+ //
4071
+ // A FILE WITH NO MARKER LINE IN IT IS NEITHER, and that is what keeps
4072
+ // FMT.0.md §9's boundary where it stood: a `.json`, `.yaml` or `.toml`
4073
+ // include is another language's file, and reading one as a generator
4074
+ // would answer it back unchanged having understood none of it. The
4075
+ // marker is the evidence that a file was written to carry aontu at
4076
+ // all. `--marker` says so outright, and then the file is a generator
4077
+ // whatever it is called.
4078
+ function fmtMarker(
4079
+ file: string, src: string, marker: string | undefined): string | undefined | false {
4080
+ if (undefined !== marker) {
4081
+ return marker
4082
+ }
4083
+ if (/[.](aon|aontu)$/.test(file)) {
4084
+ return undefined
4085
+ }
4086
+ const mark = markerFor(file)
4087
+ return templateOutputs(src, mark).some((out) => !out) ? mark : false
4088
+ }
4089
+
4051
4090
  // An option that says what to do with a file, in place of printing
4052
4091
  // it: what to do when its form would change, or the lint.
4053
4092
  function fmtQuiet(flags: FmtFlags): boolean {
@@ -4059,8 +4098,9 @@ function fmtQuiet(flags: FmtFlags): boolean {
4059
4098
  // document that does not format, with the finding that says why. The
4060
4099
  // style findings go to standard error, one line each, in the shape
4061
4100
  // every linter prints: `file:line:col: rule: message`.
4062
- function fmtOne(name: string, src: string, flags: FmtFlags): number {
4063
- const report = format(src, { path: name, lint: flags.lint })
4101
+ function fmtOne(
4102
+ name: string, src: string, flags: FmtFlags, marker?: string): number {
4103
+ const report = format(src, { path: name, lint: flags.lint, template: marker })
4064
4104
  if ('error' === report.verdict) {
4065
4105
  process.stderr.write(`aontu: ${name} was not formatted\n` +
4066
4106
  report.errors.map(renderFinding).join('\n') + '\n')
package/src/format.ts CHANGED
@@ -26,6 +26,7 @@ import { Aontu } from './aontu'
26
26
  import { failureFinding } from './vet'
27
27
  import type { VetFinding } from './vet'
28
28
  import type { Resolver } from './type'
29
+ import { desugarTemplate, resugarTemplate, templateOutputs } from './template'
29
30
 
30
31
 
31
32
  // The packing budget (§3.1). It decides which of two legal spellings
@@ -46,6 +47,12 @@ export type FormatOptions = {
46
47
  // Report the style findings of §4 -- key case, repeated shapes --
47
48
  // beside the text. The formatter never acts on them.
48
49
  lint?: boolean
50
+ // THE SOURCE IS A GENERATOR, and this is its marker
51
+ // (docs/design/TEMPLATE.0.md; FMT.0.md §3.14). The file is desugared,
52
+ // formatted and resugared, so `text` is a template again -- the aontu
53
+ // in the agreed form, indented after the marker, and every line of
54
+ // output exactly where it was.
55
+ template?: string
49
56
  }
50
57
 
51
58
  // A style finding (§4): what the formatter points at and never
@@ -183,6 +190,10 @@ type Node = {
183
190
 
184
191
  // The source index of the node's first token: the lint's positions.
185
192
  at?: number
193
+
194
+ // The node begins a line of the target's own file (§3.14), so it has
195
+ // no one-line form and every container holding it opens.
196
+ held?: boolean
186
197
  }
187
198
 
188
199
  const BINARY: Record<string, boolean> = { '#E&': true, '#E|': true, '#E+': true }
@@ -608,7 +619,7 @@ function pairHead(node: Node, tight: boolean): string {
608
619
  // lines. `tight` is the inline form of a pair, `a:1`, used inside a
609
620
  // container; a statement's pair is `a: 1`.
610
621
  function inline(node: Node, tight: boolean): string | undefined {
611
- if (undefined !== node.trail) {
622
+ if (undefined !== node.trail || node.held) {
612
623
  return undefined
613
624
  }
614
625
  switch (node.t) {
@@ -744,6 +755,15 @@ class Writer {
744
755
  return this.lines.slice(mark).concat([this.line]).map(rtrim).join('\n') + '\n'
745
756
  }
746
757
 
758
+ // A blank line above the line at an index: the gap of §3.8, opened
759
+ // once the statement below it turns out to be a tree. Never at the
760
+ // top of the page, and never a second time.
761
+ gap(at: number): void {
762
+ if (0 < at && '' !== this.lines[at - 1]) {
763
+ this.lines.splice(at, 0, '')
764
+ }
765
+ }
766
+
747
767
  // The lines since a mark replaced by a text: the spelling before,
748
768
  // where a rewrite did not pass its check.
749
769
  replace(mark: number, text: string): void {
@@ -776,29 +796,57 @@ function rtrim(s: string): string {
776
796
  // body of a plain map that is itself the value of a statement) a pair
777
797
  // is laid out by §3.4, which may repeat its key; anywhere else -- a
778
798
  // list, an operand, an argument -- by §3.5 alone.
779
- function emitBody(w: Writer, body: Node[], indent: number, stmt: Stmt | undefined): void {
799
+ function emitBody(
800
+ w: Writer, body: Node[], indent: number, stmt: Stmt | undefined, root?: boolean
801
+ ): void {
780
802
  let pending = false
781
803
  let count = 0
804
+ // Where the run being written begins: a statement, with the comments
805
+ // standing directly above it, so that the gap below opens ABOVE the
806
+ // comments rather than between them and what they describe. A blank
807
+ // line ends a run -- comments across a gap belong to what is above.
808
+ let head = 0
809
+ let noted = false
782
810
  for (const node of body) {
783
811
  if ('blank' === node.t) {
784
812
  pending = 0 < count
785
813
  continue
786
814
  }
815
+ const gapped = pending
787
816
  w.open(indent, pending)
817
+ if (gapped || !noted) {
818
+ head = w.mark()
819
+ }
788
820
  pending = false
789
821
  count++
790
822
  if ('comment' === node.t) {
823
+ noted = true
791
824
  w.text(node.text!)
792
825
  continue
793
826
  }
827
+ noted = false
828
+ const from = w.mark()
794
829
  if (undefined !== stmt && 'pair' === node.t) {
795
830
  emitStatement(w, node, indent, stmt, '')
796
- continue
797
831
  }
798
- const e = chain(node)
799
- emitValue(w, e, indent)
800
- if (undefined !== e.trail) {
801
- w.text(' ' + e.trail)
832
+ else {
833
+ const e = chain(node)
834
+ emitValue(w, e, indent)
835
+ if (undefined !== e.trail) {
836
+ w.text(' ' + e.trail)
837
+ }
838
+ }
839
+ // A TOP-LEVEL STATEMENT WRITTEN AS A TREE STANDS APART (§3.8). A
840
+ // document states several things -- a service, then its entities,
841
+ // then its errors -- and where one of them is a tree rather than a
842
+ // line, the eye finds it by the space around it. What counts as a
843
+ // tree is measured rather than guessed: the statement took more
844
+ // than one line to write. So `a: 1` beside `b: 2` is left alone,
845
+ // and this rule cannot fire below the root, where a blank line is
846
+ // the author's (§3.8) and nothing else.
847
+ if (root && from < w.mark()) {
848
+ w.gap(head)
849
+ pending = true
802
850
  }
803
851
  }
804
852
  }
@@ -1282,7 +1330,7 @@ function emitAt(nodes: Node[], indent: number): string {
1282
1330
  function emit(root: Node[], meet: Meet | undefined): string {
1283
1331
  const w = new Writer()
1284
1332
  emitBody(w, undefined === meet ? root : mergeRuns(root), 0,
1285
- undefined === meet ? undefined : { meet, covered: false })
1333
+ undefined === meet ? undefined : { meet, covered: false }, true)
1286
1334
  return w.finish()
1287
1335
  }
1288
1336
 
@@ -1516,13 +1564,66 @@ function checkFinding(path: string | undefined, expected: string, actual: string
1516
1564
  }
1517
1565
  }
1518
1566
 
1567
+ // THE TARGET'S OWN LINES ARE HELD ON LINES OF THEIR OWN (§3.14). The
1568
+ // desugaring is line for line, so a line of output is known by the
1569
+ // offset it begins at, and the node beginning there -- the quoted
1570
+ // string the desugaring wrote -- is marked. From there on it has no
1571
+ // one-line form, so every container holding it opens, and no two lines
1572
+ // of the generated file are ever packed onto one.
1573
+ function holdOutput(nodes: Node[], at: Set<number>): void {
1574
+ for (const node of nodes) {
1575
+ if (undefined !== node.at && at.has(node.at)) {
1576
+ node.held = true
1577
+ }
1578
+ holdOutput(node.body ?? [], at)
1579
+ holdOutput(node.args ?? [], at)
1580
+ holdOutput(node.inner ?? [], at)
1581
+ holdOutput(node.items ?? [], at)
1582
+ if (undefined !== node.value) {
1583
+ holdOutput([node.value], at)
1584
+ }
1585
+ }
1586
+ }
1587
+
1588
+
1589
+ // The offsets the flagged lines of a document begin at.
1590
+ function outputAt(doc: string, flags: boolean[]): Set<number> {
1591
+ const at = new Set<number>()
1592
+ let off = 0
1593
+ const lines = doc.split('\n')
1594
+ for (let k = 0; k < lines.length; k++) {
1595
+ if (flags[k]) {
1596
+ at.add(off)
1597
+ }
1598
+ off += lines[k].length + 1
1599
+ }
1600
+ return at
1601
+ }
1602
+
1603
+
1604
+ // A finding's column in the TEMPLATE rather than in the document it
1605
+ // carries (§3.14): the marker and its one space stand before the aontu
1606
+ // on every line the resugaring writes. Every finding is on such a
1607
+ // line -- the two rules point at a key or at a container, and a line of
1608
+ // output is a bare string, which is neither.
1609
+ function shiftFindings(findings: LintFinding[], mark: string | undefined): LintFinding[] {
1610
+ return undefined === mark ? findings :
1611
+ findings.map((f) => ({ ...f, col: f.col + mark.length + 1 }))
1612
+ }
1613
+
1614
+
1519
1615
  // Format one document. The text is the agreed form of the source;
1520
1616
  // `changed` says whether it differs from what was given, which is
1521
1617
  // what `--check` and `--list` report.
1522
1618
  export function format(src: string, opts?: FormatOptions, hooks?: FormatHooks): FormatReport {
1523
1619
  const text = lf(src)
1620
+ // A GENERATOR IS FORMATTED AS THE DOCUMENT IT CARRIES (§3.14): the
1621
+ // template surface's two transforms stand either side of the
1622
+ // formatter, and between them is what happens to any other document.
1623
+ const mark = opts?.template
1624
+ const doc = undefined === mark ? text : desugarTemplate(text, mark)
1524
1625
  const toks: Tok[] = []
1525
- const parsed = parseDoc(text, opts?.path, toks)
1626
+ const parsed = parseDoc(doc, opts?.path, toks)
1526
1627
  if (undefined !== parsed.errors) {
1527
1628
  return { verdict: 'error', errors: parsed.errors }
1528
1629
  }
@@ -1531,6 +1632,9 @@ export function format(src: string, opts?: FormatOptions, hooks?: FormatHooks):
1531
1632
  if (reader.deep) {
1532
1633
  return { verdict: 'error', errors: [depthFinding()] }
1533
1634
  }
1635
+ if (undefined !== mark) {
1636
+ holdOutput(root, outputAt(doc, templateOutputs(text, mark)))
1637
+ }
1534
1638
  // The syntactic tier first, checked against the parse tree; then the
1535
1639
  // lawful tier over it, each rewrite checked by the meet.
1536
1640
  const plain = emit(root, undefined)
@@ -1542,9 +1646,10 @@ export function format(src: string, opts?: FormatOptions, hooks?: FormatHooks):
1542
1646
  }
1543
1647
  }
1544
1648
  const out = emit(root, hooks?.meet ?? sameByMeet)
1649
+ const done = undefined === mark ? out : resugarTemplate(out, mark)
1545
1650
  return {
1546
- verdict: 'formatted', text: out, changed: out !== src,
1547
- findings: opts?.lint ? lintOf(root, text) : [],
1651
+ verdict: 'formatted', text: done, changed: done !== src,
1652
+ findings: opts?.lint ? shiftFindings(lintOf(root, doc), mark) : [],
1548
1653
  }
1549
1654
  }
1550
1655
 
package/src/std.ts CHANGED
@@ -186,6 +186,7 @@ const STD_CODE = String.raw`# aontu:code --- THE OUTPUT VOCABULARY. An aontu tra
186
186
  k: "prim"
187
187
  prim: "string" | "int" | "bigint" | "float" | "decimal" | "bool" | "null" | "any"
188
188
  })
189
+
189
190
  %ref = close({ k:"ref" name:%name unit?:string })
190
191
  %leaf = %prim | %ref | %text
191
192
 
@@ -208,6 +209,7 @@ const STD_CODE = String.raw`# aontu:code --- THE OUTPUT VOCABULARY. An aontu tra
208
209
  })
209
210
 
210
211
  %member = close({ name:%name value?:string | number doc?:%doc x?:{} })
212
+
211
213
  %param = close({
212
214
  name: %name
213
215
  type: %type
@@ -221,12 +223,14 @@ const STD_CODE = String.raw`# aontu:code --- THE OUTPUT VOCABULARY. An aontu tra
221
223
  %inline = string & re("^[^\n\r]*$") | %ref
222
224
  %line = close({ k:"line" at:*0 | integer & min(0) & max(64) of:[&: %inline] })
223
225
  %blank = close({ k:"blank" n:*1 | integer & min(1) & max(16) })
226
+
224
227
  %raw = close({
225
228
  k: "raw"
226
229
  at: *0 | integer & min(0) & max(64)
227
230
  text: string
228
231
  reindent: *true | boolean
229
232
  })
233
+
230
234
  %piece = %line | %blank | %raw | string & re("^[^\n\r]*$")
231
235
  %frag = close({ k:"frag" of:[&: %piece] })
232
236
  %body = %frag | close({ k:"abstract" })
@@ -241,7 +245,9 @@ const STD_CODE = String.raw`# aontu:code --- THE OUTPUT VOCABULARY. An aontu tra
241
245
  check?: [&: %check]
242
246
  x?: {}
243
247
  })
248
+
244
249
  %enum = close({ k:"enum" name:%name doc?:%doc members:[&: %member] x?:{} })
250
+
245
251
  %alias = close({
246
252
  k: "alias"
247
253
  name: %name
@@ -250,6 +256,7 @@ const STD_CODE = String.raw`# aontu:code --- THE OUTPUT VOCABULARY. An aontu tra
250
256
  check?: [&: %check]
251
257
  x?: {}
252
258
  })
259
+
253
260
  %const = close({
254
261
  k: "const"
255
262
  name: %name
@@ -258,6 +265,7 @@ const STD_CODE = String.raw`# aontu:code --- THE OUTPUT VOCABULARY. An aontu tra
258
265
  value: string | number | boolean | null
259
266
  x?: {}
260
267
  })
268
+
261
269
  %func = close({
262
270
  k: "func"
263
271
  name: %name
package/src/template.ts CHANGED
@@ -24,12 +24,22 @@
24
24
  // comment, and derives for a language this table has never seen: the
25
25
  // caller passes the token.
26
26
  //
27
- // A MARKER IS RECOGNISED AFTER LEADING WHITESPACE AND KEEPS ITS OWN
28
- // INDENTATION (D2). That is what lets a template be written exactly
29
- // where its output appears (D7): a body line is verbatim, so a line
30
- // indented two spaces in the template is indented two spaces in the
31
- // generated file, and the marker lines around it are indented to match
32
- // the code they sit in rather than to the aontu they carry.
27
+ // A MARKER IS RECOGNISED AFTER LEADING WHITESPACE, AND THE INDENTATION
28
+ // THE RESUGARING WRITES IS THE AONTU'S (D2, amended 2026-09-07). An
29
+ // output line is verbatim -- a line indented two spaces in the template
30
+ // is indented two spaces in the generated file (D7) -- and that is the
31
+ // half a reader of the TARGET needs. The other half is the document:
32
+ // the marker lines carry a tree, and a tree nobody can see the shape of
33
+ // is the one thing this surface took away. So the marker stands at the
34
+ // left margin and the aontu is indented AFTER it, which `aontu fmt`
35
+ // (FMT.0.md M-BM-'3.14) is what writes.
36
+ //
37
+ // Reading is unchanged and stays generous: a marker after leading
38
+ // whitespace is a marker, and its own indentation still counts toward
39
+ // the aontu it carries, so a template written the other way desugars to
40
+ // the same document. What changes is the spelling the round trip
41
+ // answers, and `template --check` names the difference as the drift it
42
+ // is.
33
43
  //
34
44
  // THE SUGAR IS THE FIXPOINT OF THE TWO TRANSFORMS (D6), and that is
35
45
  // the whole of the resugaring's safety. A canonical line that looks
@@ -128,13 +138,19 @@ function indentOf(line: string): number {
128
138
  }
129
139
 
130
140
 
131
- // The line without its indentation or its trailing spaces and tabs.
132
- function trimLine(line: string): string {
141
+ // The line without its trailing spaces and tabs.
142
+ function trimEnd(line: string): string {
133
143
  let end = line.length
134
144
  while (0 < end && (' ' === line[end - 1] || '\t' === line[end - 1])) {
135
145
  end--
136
146
  }
137
- return line.slice(indentOf(line), end)
147
+ return line.slice(0, end)
148
+ }
149
+
150
+
151
+ // The line without its indentation or its trailing spaces and tabs.
152
+ function trimLine(line: string): string {
153
+ return trimEnd(line).slice(indentOf(line))
138
154
  }
139
155
 
140
156
 
@@ -159,12 +175,15 @@ function readLine(line: string, marker: string): Line {
159
175
  // The block form's closer is part of the marker, not of the aontu.
160
176
  // A block marker line that never closes is not a marker line: the
161
177
  // language's own parser would not read it as a comment either.
178
+ // Only the TRAILING space goes: what stands between the opener and
179
+ // the aontu is the aontu's indentation, exactly as it is for a line
180
+ // marker, and the one-space rule below takes the marker's own.
162
181
  if (isBlock(marker)) {
163
182
  const end = body.lastIndexOf(BLOCK_CLOSE)
164
183
  if (end < 0) {
165
184
  return { marker: false, indent: '', text: line }
166
185
  }
167
- body = trimLine(body.slice(0, end))
186
+ body = trimEnd(body.slice(0, end))
168
187
  }
169
188
  // ONE SPACE AFTER THE MARKER IS THE MARKER'S, so `//- x: 1` carries
170
189
  // `x: 1` and the resugaring writes the space back. A marker written
@@ -273,19 +292,40 @@ function resugarTemplate(src: string, marker?: string): string {
273
292
  if (undefined !== target && desugarTemplate(target, mark) === trimLine(line)) {
274
293
  return target
275
294
  }
276
- const cut = indentOf(line)
277
- const text = line.slice(cut)
278
- const open = line.slice(0, cut) + mark
295
+ // THE MARKER STANDS AT THE LEFT MARGIN and the line's indentation
296
+ // is written after it, so the aontu's own shape is on the page.
297
+ // The one space is the marker's, which the reading takes back.
298
+ const text = trimEnd(line)
279
299
  const close = isBlock(mark) ? ' ' + BLOCK_CLOSE : ''
280
- return '' === text ? open + close : open + ' ' + text + close
300
+ return '' === text ? mark + close : mark + ' ' + text + close
281
301
  })
282
302
  return out.join('\n') + (tail ? '\n' : '')
283
- } /* node:coverage ignore next 8 */
303
+ }
304
+
305
+
306
+ // WHICH LINES OF THE DESUGARED DOCUMENT ARE THE TARGET'S. The
307
+ // desugaring is line for line, so this is one flag per line of what
308
+ // `desugarTemplate` returns: true where the template's line was not a
309
+ // marker, and so where the document's line is one quoted line of the
310
+ // generated file.
311
+ //
312
+ // `aontu fmt` reads it (FMT.0.md M-BM-'3.14) to hold those lines on lines of
313
+ // their own. A body element is a string like any other, and the packing
314
+ // budget would put three of them on one line -- which is aontu where
315
+ // three lines of output were, and a generator that writes them as one.
316
+ function templateOutputs(src: string, marker: string): boolean[] {
317
+ const lines = src.split('\n')
318
+ if (1 < lines.length && '' === lines[lines.length - 1]) {
319
+ lines.pop()
320
+ }
321
+ return lines.map((line) => !readLine(line, marker).marker)
322
+ } /* node:coverage ignore next 9 */
284
323
 
285
324
 
286
325
  export {
287
326
  desugarTemplate,
288
327
  resugarTemplate,
328
+ templateOutputs,
289
329
  markerFor,
290
330
  DEFAULT_MARKER,
291
331
  }