Skip to content

Commit 6fb25d3

Browse files
committed
fix: a bare admonition style over a paragraph is the label it names
`[NOTE]` over prose and `NOTE: ` in front of it are the same admonition to Asciidoctor (ADMONITION_STYLES, parser.rb:730), so which one the author typed is a spelling. The reader now records ONE admonition node for either, the style line becoming that node's opening bytes rather than a sibling attribute list, and the printer writes the label form for both. WHERE the style line stands decides whether it may be respelled at all. A bracket line opens a block from any position and a label line does not, so the fold is refused in item-TEXT position and under a description item that spent no description of its own: there a label is more of the item's text and the admonition would be destroyed. Held to Ruby's five styles exactly and case-sensitively otherwise, and refused for an interior carrying more than the style, a second attribute line, a held node between the style and its block, and any block under it that is not a paragraph. The three paragraph shapes become one union the held run answers with, so a paragraph cannot be both an admonition and a styled block, and the reader names no builder of its own. Seven corpus documents stop diverging from Asciidoctor's own block structure; the reading net grows the matching licence, no wider than the routing; the gate gains an axis that reaches item-text position, where it also asserts what the confluence property cannot - that formatting a document there does not change what it renders. The confluence gate loses the blockFormSpelling/admonition-form row and the blockFormReplay mechanism with it.
1 parent 2d86096 commit 6fb25d3

27 files changed

Lines changed: 843 additions & 140 deletions

scripts/block-structure-corpus.json

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22
"oracle": "@asciidoctor/core 4.0.11",
33
"regenerate": "Measured, not authored: regenerate with `bun run block-structure --write` whenever the oracle pin or the parser moves, then read the diff - it is the artifact that says what a change did to our conformance.",
44
"cases": {
5-
"api_test.rb#should return false when sections? is called on anything that is not a section#0": {
6-
"family": "gap:styled-block-remodel",
7-
"signature": "paragraph=>admonition"
8-
},
95
"attributes_test.rb#id, role and options attributes can be specified on block style using shorthand syntax#0": {
106
"family": "gap:styled-block-remodel",
117
"signature": "paragraph=>literal"
@@ -14,18 +10,6 @@
1410
"family": "gap:styled-block-remodel",
1511
"signature": "paragraph=>literal"
1612
},
17-
"blocks_test.rb#can resolve icon relative to custom iconsdir#0": {
18-
"family": "gap:styled-block-remodel",
19-
"signature": "paragraph=>admonition"
20-
},
21-
"blocks_test.rb#can resolve icon relative to default iconsdir#0": {
22-
"family": "gap:styled-block-remodel",
23-
"signature": "paragraph=>admonition"
24-
},
25-
"blocks_test.rb#cleans reference to ancestor directories before reading icon if safe mode level is at least SAFE#0": {
26-
"family": "gap:styled-block-remodel",
27-
"signature": "paragraph=>admonition"
28-
},
2913
"blocks_test.rb#comment style on open block should only skip block#0": {
3014
"family": "oracle:comment-style",
3115
"signature": "-pass"
@@ -38,10 +22,6 @@
3822
"family": "oracle:comment-style",
3923
"signature": "-paragraph"
4024
},
41-
"blocks_test.rb#does not embed base64-encoded data uri of icon when safe mode level is SECURE or greater#0": {
42-
"family": "gap:styled-block-remodel",
43-
"signature": "paragraph=>admonition"
44-
},
4525
"blocks_test.rb#dropped image does not break processing of following section and attribute-missing is drop-line#0": {
4626
"family": "oracle:attribute-missing",
4727
"signature": "-image"
@@ -50,10 +30,6 @@
5030
"family": "oracle:attribute-missing",
5131
"signature": "-image"
5232
},
53-
"blocks_test.rb#embeds base64-encoded data uri of icon when data-uri attribute is set and safe mode level is less than SECURE#0": {
54-
"family": "gap:styled-block-remodel",
55-
"signature": "paragraph=>admonition"
56-
},
5733
"blocks_test.rb#font-based icon should not override icon specified on admonition#0": {
5834
"family": "gap:styled-block-remodel",
5935
"signature": "paragraph=>admonition"
@@ -126,10 +102,6 @@
126102
"family": "oracle:book-partintro",
127103
"signature": "paragraph=>open"
128104
},
129-
"blocks_test.rb#should import Font Awesome and use font-based icons when value of icons attribute is font#0": {
130-
"family": "gap:styled-block-remodel",
131-
"signature": "paragraph=>admonition"
132-
},
133105
"blocks_test.rb#should not allow abstract as direct child of document if doctype is book converted to DocBook#0": {
134106
"family": "oracle:book-partintro",
135107
"signature": "paragraph=>open"

scripts/metrics/crossings-registry.json

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@
118118
"kind": "vocabulary",
119119
"reason": "the one read of a bracket line's named values; a table's open asks it rather than splitting the interior a second way"
120120
},
121+
{
122+
"file": "src/parse/attrlist.ts",
123+
"symbol": "canonicalAttrlist",
124+
"importer": "src/parse/lines/open-style.ts",
125+
"kind": "vocabulary",
126+
"reason": "the one answer to where one attribute inside a bracket line ends, asked here so the style an open acts on is the style the printer would write back"
127+
},
121128
{
122129
"file": "src/parse/attrlist.ts",
123130
"symbol": "canonicalAttrlist",
@@ -349,6 +356,13 @@
349356
"kind": "vocabulary",
350357
"reason": "pure (line, index) -> Node constructor, in the leaf builder table"
351358
},
359+
{
360+
"file": "src/parse/build/paragraph.ts",
361+
"symbol": "ParagraphOpening",
362+
"importer": "src/parse/lines/held-metadata.ts",
363+
"kind": "vocabulary",
364+
"reason": "the three shapes a held run can make of the paragraph about to open, decided once by the run and consumed by the builder that reads it"
365+
},
352366
{
353367
"file": "src/parse/build/paragraph.ts",
354368
"symbol": "buildAdmonitionParagraph",
@@ -372,10 +386,10 @@
372386
},
373387
{
374388
"file": "src/parse/build/paragraph.ts",
375-
"symbol": "buildParagraphFormBlock",
389+
"symbol": "buildParagraphNode",
376390
"importer": "src/parse/lines/reader.ts",
377391
"kind": "vocabulary",
378-
"reason": "pure (variant, tokens, source, index) -> Node constructor for a paragraph a style re-modelled"
392+
"reason": "pure (opening, tokens, source, index) -> Node constructor for every paragraph shape, so the reader names no builder of its own"
379393
},
380394
{
381395
"file": "src/parse/build/paragraph.ts",
@@ -937,6 +951,13 @@
937951
"kind": "vocabulary",
938952
"reason": "registry reading: which interrupting set an item's own text is read under, named where the two list reads choose between them"
939953
},
954+
{
955+
"file": "src/parse/line-shapes.ts",
956+
"symbol": "ParagraphContext",
957+
"importer": "src/parse/lines/open-style.ts",
958+
"kind": "vocabulary",
959+
"reason": "the registry's own name for which paragraph is open; the style rules read it rather than restating the set of positions"
960+
},
940961
{
941962
"file": "src/parse/line-shapes.ts",
942963
"symbol": "ParagraphContext",

scripts/metrics/score-minimums.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,13 +395,13 @@
395395
},
396396
{
397397
"file": "src/parse/lines/reader.ts",
398-
"what": "the LEFT disjunct of the child reader's tailSafe (reader.ts:845 `tailSafe: extent.close !== undefined || tailSafeIn(this.confinement)`) - 3 mutants: -> `true`, -> `false`, and `!==` -> `===`",
398+
"what": "the LEFT disjunct of the child reader's tailSafe (reader.ts:837 `tailSafe: extent.close !== undefined || tailSafeIn(this.confinement)`) - 3 mutants: -> `true`, -> `false`, and `!==` -> `===`",
399399
"class": "never",
400400
"reason": "EQUIVALENT, measured rather than assumed: all three produce byte-identical output over 66,429 documents (a depth-5 product over `----`, `====`, `--`, `****`, `* a`, `** b`, `+`, blank and `para`) and over all 111,121 list-shape sweep documents. Whenever a block CLOSED, the reader's own `tailSafe` is already true, so the left disjunct never decides. The RIGHT disjunct is live and pinned in tests/format/trailing-continuation.test.ts (a trailing `+` at the end of an unclosed `====` or `--` block comes back), which is what took this file from 93.6 to 94.2"
401401
},
402402
{
403403
"file": "src/parse/lines/reader.ts",
404-
"what": "reader.ts:668 `line.continuationTag === \"marker\"` -> `true` in continuationLine's fold arm (#56's new code)",
404+
"what": "reader.ts:660 `line.continuationTag === \"marker\"` -> `true` in continuationLine's fold arm (#56's new code)",
405405
"class": "never",
406406
"reason": "EQUIVALENT, and worth writing down because the guard IS reached with an untagged `+`. Every `+` that enters an item buffer goes through `pushMarker` and carries the tag, with ONE exception: the `+` that `activeContent` buffers as the attached block's own first line when it stands one blank under an active continuation (`* a` / `+` / blank / `+` is the smallest such document). That untagged cell is only ever the buffer's LAST line, because any line after it takes the `+` arm that blanks it, so the fold the mutant opens there reads exactly the one raw line the leaf arm builds and stops on the same index. Measured: byte-identical output AND byte-identical ASTs over all 111,110 depth-5 sweep documents, byte-identical output over 66,429 delimiter-bearing documents, and an instrumented build reaches the untagged case MID-buffer zero times in either product"
407407
},

scripts/parity-ledger.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,44 @@ export const OPEN_BLOCK_TILDE_FAMILY = "open-block-tilde";
743743
*/
744744
const BLOCK_MACRO_NAME_FAMILY = "block-macro-name";
745745

746+
/**
747+
* `[NOTE]`/`[TIP]`/`[IMPORTANT]`/`[WARNING]`/`[CAUTION]` over a
748+
* paragraph and the bare `STYLE: ` label in front of it are the same
749+
* admonition to Asciidoctor (`ADMONITION_STYLES`, parser.rb:730), so
750+
* which one the author typed is a spelling; the fold that respells the
751+
* bracket form to the label form is `admonitionLabelOpensABlock`
752+
* (src/parse/lines/open-style.ts). Every declared case loses the
753+
* `blockAttributeList` sibling the bracket style used to open and
754+
* gains one admonition node whose opening bytes ARE the style line, so
755+
* the bracket line's own bytes move to the label spelling and every
756+
* byte after it is unchanged. NOT formatted-only: the node kind
757+
* itself changes, the same way `BLOCK_MACRO_NAME_FAMILY` above does.
758+
* Not a blanket family either: it does not name a field every node
759+
* gained, it names a whole sibling node the fold removes.
760+
*
761+
* EXPORTED: the standing grid's `under-note-attrlist` container (its
762+
* own comment, scripts/shape-registry.ts) puts this style line over
763+
* every compound delimiter kind, and
764+
* scripts/metrics/shape-census.ts's `admonition-label` GRID_EXEMPT
765+
* entry names the same cell in advance as the one place the
766+
* delimited-admonition path (`buildDelimitedAdmonition`,
767+
* src/parse/build/delimited.ts) is grid-covered - both anticipated
768+
* this family's first realized row before this fold existed to
769+
* realize it. The witness row
770+
* (`fencedCode/under-note-attrlist/minimum-delimiter-inside`) carries
771+
* the measured proof rather than an assumption: the harness's own
772+
* per-row instrumentation holds `headIdempotent`,
773+
* `headRenderEqualsInput`, and `renderNeutral` all true there.
774+
*
775+
* Matched on the OUTPUTS, not claimed by coordinate
776+
* (`admonitionLabelFoldFamily`, scripts/shape-registry-families.ts):
777+
* a coordinate names where a shape is generated FROM, not what a
778+
* diff there actually is, and a coordinate-only rule would excuse any
779+
* future diff at that address rather than only this one mechanism -
780+
* the over-broad shape is issue #202's lesson.
781+
*/
782+
export const ADMONITION_LABEL_FOLD_FAMILY = "admonition-label-fold";
783+
746784
export const LEDGER_FAMILIES: FamilySets = {
747785
families: new Set([
748786
ATTRIBUTE_CONTINUATION_FAMILY,
@@ -786,6 +824,7 @@ export const LEDGER_FAMILIES: FamilySets = {
786824
TEXTLESS_DESCRIPTION_TEXT_FAMILY,
787825
OPEN_BLOCK_TILDE_FAMILY,
788826
BLOCK_MACRO_NAME_FAMILY,
827+
ADMONITION_LABEL_FOLD_FAMILY,
789828
]),
790829
formattedOnly: new Set([
791830
AUTHOR_PLUS_FAMILY,

scripts/shape-diff.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import path from "node:path";
3838
import { formatAdoc, renderedHtml } from "../tests/helpers.js";
3939
import { CHILD_MAX_BUFFER, materialize } from "./lib/checkout.js";
4040
import { cannotRun, GATE_FAILED, printUsage, wantsHelp } from "./lib/cli.js";
41+
import { admonitionLabelFoldFamily } from "./shape-registry-families.js";
4142
import { listRunGrid } from "./shape-registry-list-run.js";
4243
import {
4344
headingAdjacencyGrid,
@@ -234,9 +235,13 @@ async function reportRow(
234235
if (byteEqual) {
235236
return row;
236237
}
237-
if (shape.family !== undefined) {
238-
row.family = shape.family;
239-
}
238+
// The registry's family is a property of the COORDINATE, fixed
239+
// before any diff exists; the admonition fold's family is a
240+
// property of what the two outputs actually are, so it is tested
241+
// here, on the bytes, rather than added to the registry as one more
242+
// coordinate claim (issue #202's lesson - see
243+
// scripts/shape-registry-families.ts).
244+
row.family = shape.family ?? admonitionLabelFoldFamily(baseOut, headOut);
240245
try {
241246
row.headIdempotent = (await formatAdoc(headOut)) === headOut;
242247
} catch (error) {

scripts/shape-registry-families.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
* file's own.
2626
*/
2727
import {
28+
ADMONITION_LABEL_FOLD_FAMILY,
2829
BLOCK_DELIMITER_LENGTH_FAMILY,
2930
DESCRIPTION_LIST_ITEM_FAMILY,
3031
MARKDOWN_THEMATIC_BREAK_FAMILY,
@@ -35,6 +36,57 @@ import {
3536
UNDERLINED_SECTION_TITLE_FAMILY,
3637
} from "./parity-ledger.js";
3738

39+
// Ruby's five admonition styles (ADMONITION_STYLES, parser.rb:730),
40+
// in the bracket spelling a style line carries before the fold.
41+
const ADMONITION_STYLES: readonly string[] = [
42+
"NOTE",
43+
"TIP",
44+
"IMPORTANT",
45+
"WARNING",
46+
"CAUTION",
47+
];
48+
49+
/**
50+
* Whether a base/head pair differs by EXACTLY one admonition style
51+
* line's fold to its label form: `[STYLE]\n` at the head of `baseOut`
52+
* respelled `STYLE: ` at the head of `headOut`, with every byte after
53+
* that line identical in both. Tested on the OUTPUTS a diff actually
54+
* produced rather than claimed by the coordinate that happened to
55+
* realize it first ({@link ADMONITION_LABEL_FOLD_FAMILY}'s own
56+
* citation names why - issue #202's lesson): {@link gridRowFamily}
57+
* answers by coordinate for the rows below it because each of THOSE
58+
* mechanisms is a property of the coordinate itself (a kind the base
59+
* registry has no dimension for, a container every row inside moves
60+
* for), but the admonition fold is a property of what the printer
61+
* wrote, realized at exactly one coordinate today and not chosen
62+
* because of it.
63+
* @param baseOut - the base revision's formatted output
64+
* @param headOut - this checkout's formatted output
65+
* @returns the family, or undefined when no admonition style line
66+
* explains the whole difference
67+
*/
68+
export function admonitionLabelFoldFamily(
69+
baseOut: string,
70+
headOut: string,
71+
): string | undefined {
72+
for (const style of ADMONITION_STYLES) {
73+
const bracketPrefix = `[${style}]\n`;
74+
const labelPrefix = `${style}: `;
75+
if (
76+
!baseOut.startsWith(bracketPrefix) ||
77+
!headOut.startsWith(labelPrefix)
78+
) {
79+
continue;
80+
}
81+
if (
82+
baseOut.slice(bracketPrefix.length) === headOut.slice(labelPrefix.length)
83+
) {
84+
return ADMONITION_LABEL_FOLD_FAMILY;
85+
}
86+
}
87+
return undefined;
88+
}
89+
3890
/** The delimiter kind base's registry has no dimension for at all. */
3991
const OPEN_BLOCK_TILDE_KIND = "openBlockTilde";
4092

src/parse/build/metadata.ts

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -97,39 +97,26 @@ function buildBlockAttributeList(
9797

9898
/**
9999
* The bytes between a block attribute line's brackets, taken off in
100-
* ONE place, so the node's value and the id question below cannot
101-
* come to disagree about where the interior starts.
100+
* ONE place, so every question asked of an interior is asked of the
101+
* same bytes the node's value carries.
102+
*
103+
* Exported for the reading net (tests/lib/reading.ts), which licenses
104+
* exactly the respellings {@link buildAttributeLine} and the reader
105+
* make of such a line and must therefore ask the interior the same
106+
* questions they do rather than cut the brackets its own way. Its src
107+
* callers are the two builders below.
102108
* @param image - a block attribute line, bracket-delimited
103109
* @returns the interior, brackets excluded
110+
* @internal
104111
*/
105-
function attributeLineInterior(image: string): string {
112+
export function attributeLineInterior(image: string): string {
106113
return image.slice(
107114
BLOCK_ATTR_LIST_PREFIX_LEN,
108115
// Negated to slice from the end: -1 drops the trailing `]`.
109116
-BLOCK_ATTR_LIST_SUFFIX_LEN,
110117
);
111118
}
112119

113-
/**
114-
* The id a block attribute LINE names when it names an id and nothing
115-
* else. THE question, asked in one place by the two consumers that
116-
* must agree on it: {@link buildAttributeLine} routes on it, and the
117-
* reading net's projection folds `[#intro]` onto the anchor token
118-
* because of it.
119-
*
120-
* Exported for that net (tests/lib/reading.ts), which must license
121-
* exactly the respelling this routing makes and no wider one; asking
122-
* the routing's own question is what keeps the licence from drifting
123-
* into a second pattern. Its src caller is `buildAttributeLine`,
124-
* below.
125-
* @param image - a block attribute line, bracket-delimited
126-
* @returns the id, or undefined when the line spells anything else
127-
* @internal
128-
*/
129-
export function anchorIdOfAttributeLine(image: string): string | undefined {
130-
return attrlistAnchorId(attributeLineInterior(image));
131-
}
132-
133120
/**
134121
* Builds the node a held `[...]` line becomes: a block anchor when the
135122
* line names an id and nothing else, an attribute list otherwise.
@@ -150,7 +137,7 @@ export function buildAttributeLine(
150137
line: Fragment,
151138
at: LocationIndex,
152139
): BlockNode {
153-
const id = anchorIdOfAttributeLine(line.image);
140+
const id = attrlistAnchorId(attributeLineInterior(line.image));
154141
return id === undefined
155142
? buildBlockAttributeList(line, at)
156143
: {

0 commit comments

Comments
 (0)