Cutdown conformance corpus
This directory contains the golden test fixtures for the Cutdown language spec. A compliant parser must pass all of them.
Structure
Fixtures are organized by spec section. Each subdirectory maps to one section:
tests/
003-document-model/
004-block-segments/
005-inline-segments/
006-universal-attributes/
008-escaping/
010-block-structure/
012-whitespace-rules/
diagnostics/ ← one fixture per CDN-xxxx diagnostic code
A test runner must walk all *.yaml files recursively — loading only the top level will silently pass zero tests.
Fixture format
Each .yaml file is a single fixture:
id: emphasis-basic
section: "5.2"
description: "__ delimiters produce Emphasis; single _ is literal text"
input: "__bold__"
ast:
- type: Paragraph
children:
- type: Emphasis
children:
- type: Text
value: bold
Fields
| Field | Required | Description |
|---|---|---|
id |
yes | Unique fixture ID (kebab-case, matches filename without .yaml) |
section |
yes | Spec section number as a string (e.g. "9.4") |
description |
yes | One-line human-readable description |
input |
yes | Raw Cutdown source text |
ast |
— | Expected children of the first page (see Assertions) |
pages |
— | Expected full Document.children array; use when the fixture spans multiple pages. Overrides ast when present |
diagnostics |
— | Expected diagnostics (see Assertions); if omitted, the parser must emit zero diagnostics |
Exactly ONE OF ast or pages MUST be present.
Assertions
AST matching uses a deep subset match:
- All keys present in the expected object MUST exist in the actual output with identical values.
- Extra keys in the actual output MAY exist and are ignored (implementations may add position info, etc.).
- Arrays MUST be the same length; each element is subset-matched recursively.
- Primitives MUST be compared by value (
===).
This means a minimal fixture only needs to assert the fields it cares about — it will not break if the parser adds new optional fields later.
Diagnostics matching is order-independent:
- Every
{ code, level }entry listed underdiagnosticsMUST appear in the actual output. - No diagnostic MAY appear in the actual output that is not listed in the fixture.
- If
diagnosticsis omitted entirely, the parser MUST produce zero diagnostics.
ast vs pages
ast matches the children array of the first page — the common case for single-page documents:
ast:
- type: Paragraph
children:
- type: Text
value: Hello
pages matches the full Document.children array and is used when the fixture tests page-splitting (PageBreak, Meta block pagination). A Ghost Page is written {}:
pages:
- children:
- type: Paragraph
children:
- type: Text
value: First
- {}
Attribute fixtures
Attributes are asserted as an ordered array of { key, value } objects. The class shorthand (.class) produces value: string[] (array of strings); all other values are plain strings:
ast:
- type: List
attributes:
- key: class
value: [list-class]
children:
- type: ListItem
attributes:
- key: class
value: [item-class]
Diagnostic fixtures
Fixtures in diagnostics/ test warning behaviour. The fixture asserts both the AST produced (recovery output) and the diagnostic code emitted:
id: cdn-0001-unclosed-codeblock
section: "4.4"
description: "Unclosed ``` fence; content runs to end of document; CDN-0001 warning emitted"
input: "```python\nx = 1\n"
ast:
- type: CodeBlock
language: python
content: x = 1
diagnostics:
- code: CDN-0001
level: warning
ID and filename conventions
- ID is kebab-case:
code-block-basic,cdn-0001-unclosed-codeblock - Filename matches ID exactly:
code-block-basic.yaml - Diagnostic fixtures are prefixed with the code:
cdn-0001-*.yaml - Variant fixtures suffix the base name:
emphasis-basic,emphasis-triple,emphasis-unclosed
Contributing a test
- Identify the spec section the fixture covers.
- Create
tests/<section-dir>/<id>.yaml. Use an existing fixture in that directory as a template. - Fill all required fields. Keep
inputminimal — test one behaviour per fixture. - Verify the
idis unique across the entire corpus (no two fixtures share anid). - Open a PR. See
CONTRIBUTION.mdfor process.
All fixtures
002-comments
- comment-block-basic
- comment-block-in-list-item
- comment-block-opaque-content
- comment-block-pass-through-page-assembly
- comment-inline-after-attrs-transparent
- comment-inline-basic
- comment-inline-between-paragraph-lines
- comment-inline-degrades-emphasis
- comment-inline-escape
- comment-inline-heading-attrs-transparent
- comment-inline-in-codeinline-literal
- comment-inline-in-link-text-degrades
- comment-inline-in-meta-literal
- comment-inline-midline
- comment-inline-namedblock-opener
- comment-inline-triple-hash-inline
- comment-table-delim-row-trailing
- comment-table-mid-cell-degrades-to-paragraph
- comment-table-row-trailing
- single-hash-literal
003-document-model
- consecutive-breaks-ghost-page
- document-empty
- meta-block-opens-new-page
- pagebreak-leading-ghost-page
- pagebreak-only-document
- pagebreak-page-split
- pagination-meta-after-break-ghost
- pagination-meta-first-fills-initial-page
- section-bounded-by-container
- section-closed-by-pagebreak
- section-nesting
- section-skipped-levels
004-block-segments
- code-block-attrs
- code-block-basic
- code-block-inside-listitem
- code-block-inside-namedblock
- code-block-inside-quoteblock
- code-block-literal
- fileref-basic
- fileref-fragment
- fileref-groups-dont-merge
- fileref-query-and-fragment
- fileref-query-string
- heading-attrs
- heading-first-line-of-container
- heading-inline-formatting
- heading-inline-variable
- heading-inside-listitem
- heading-inside-quote-inside-list
- heading-inside-quoteblock
- heading-last-attr-rule
- heading-level1
- heading-level2
- imageblock-basic
- imageblock-inline-alt
- list-blank-col0-resets
- list-blank-indented-absorbed
- list-inline-hyphen-continuation
- list-inline-hyphen
- list-intraitem-blank
- list-item-inside-quote-inside-list
- list-loose
- list-mixed-numeric-task
- list-mixed-task-numeric
- list-multiline-item
- list-nested
- list-ordered-inline-dot
- list-ordered-nonone-start
- list-ordered
- list-stack-arbitrary-indent
- list-unordered
- math-block-attrs
- math-block-basic
- math-block-inside-listitem
- meta-block-inside-container
- meta-block-toml
- meta-block-yaml
- named-block-attrs
- named-block-basic
- named-block-indent-collapse
- named-block-nested
- pagebreak-basic
- pagebreak-four-hyphens
- pagebreak-glued-to-paragraph
- pagebreak-inside-listitem
- pagebreak-inside-namedblock
- pagebreak-inside-quoteblock
- paragraph-basic
- paragraph-continuation-leading-spaces
- paragraph-multiline-trailing-space
- paragraph-multiline
- paragraph-textbreak
- quote-block-attrs-own-line
- quote-block-attrs-trailing
- quote-block-basic
- quote-block-consecutive
- quote-block-nested
- quote-block-no-lazy-continuation
- quote-block-with-ordered-list
- quote-block-with-tasklist
- ref-definition-basic
- spoiler-block-attrs
- spoiler-block-basic
- spoiler-block-indent-collapse
- spoiler-block-inside-listitem
- spoiler-block-inside-quoteblock
- spoiler-block-nested-in-named
- spoiler-block-unclosed
- table-attrs-last-row-noop
- table-attrs-last-row-single
- table-attrs-last-row
- table-empty
- table-header-separator-attrs
- table-multiline-attrs
- table-multiline-basic
- table-multiline-header
- table-multiline-multirow-cell
- table-multiline-plus-colons-inert
- table-pipe-dash-placeholder-not-separator
- table-pipe-plus-row-ignored
- table-pipe
- table-scope-chain-attrs
- table-simple
- task-item-checked
- task-item-mixed-list
- task-item-unchecked
005-inline-segments
- attr-brace-literal-span-idiom
- code-inline-basic
- code-inline-escape-backslash
- code-inline-escape-backtick
- code-inline-escape-unknown
- code-inline-escape-with-run-of-three
- code-inline-softbreak
- emphasis-basic
- emphasis-cross-nesting
- emphasis-empty
- emphasis-single-asterisk
- emphasis-spanning-lines
- emphasis-triple
- emphasis-unclosed
- highlight-basic
- highlight-with-attrs
- image-inline
- link-both-brackets-empty
- link-cite-shorthand-no-text
- link-cite
- link-closed-twin-inner-survives
- link-comment-cut-slice
- link-empty-href
- link-external
- link-nested-dead-openers
- link-page
- link-ref
- link-tag
- link-unclosed-verbatim-slice
- math-inline-basic
- math-inline-single-dollar
- math-inline-spaced
- math-inline-text-after
- math-inline-unclosed
- quote-inline-cross-nesting
- quote-inline-double-spaced
- quote-inline-double
- quote-inline-single-spaced
- quote-inline-single
- quote-inline-unclosed
- span-attrs
- span-basic
- span-no-name
- spoiler-attrs
- spoiler-basic
- spoiler-boundary-cross
- spoiler-escape
- spoiler-nested-emphasis
- spoiler-run-of-three
- spoiler-same-type-nesting
- spoiler-single-caret-literal
- spoiler-unclosed
- spoiler-with-link
- strong-basic
- strong-unclosed-inner-survives
- text-break
- variable-basic
- variable-empty-key
- variable-id-literal
- variable-unclosed
006-universal-attributes
- attrs-bare-key
- attrs-class-merge
- attrs-escape-brace
- attrs-heading-last-attr-empty
- attrs-heading-last-attr-inline
- attrs-heading-last-attr-simple
- attrs-link-text-orphan
- attrs-link-text
- attrs-orphan-quoteblock
- attrs-scope-chain-fileref-1
- attrs-scope-chain-fileref-empty
- attrs-scope-chain-fileref-noop
- attrs-scope-chain-fileref
- attrs-scope-chain-list
- attrs-scope-chain-span-1
- attrs-scope-chain-span-2
- attrs-scope-chain-span-empty
- attrs-scope-chain-span-noop
- attrs-scope-chain-span
- attrs-scope-chain-text-1
- attrs-scope-chain-text-3
- attrs-scope-chain-text
- caption-attrs-own-line
- caption-blank-line-breaks-binding
- caption-codeblock-basic
- caption-comment-own-line-transparent
- caption-fileref-basic
- caption-filerefgroup-basic
- caption-filerefgroup-mid-run-close
- caption-imageblock-basic
- caption-inline-emphasis
- caption-inline-variable
- caption-mathblock-basic
- caption-namedblock-basic
- caption-quoteblock-attribution
- caption-scope-local-inside-namedblock
- caption-second-line-is-paragraph
- caption-spoilerblock-basic
- caption-table-basic
007-input-interpretation
008-escaping
- escape-backslash
- escape-codeblock-closer
- escape-codeblock-opener
- escape-commentblock-closer
- escape-commentblock-opener
- escape-fileref-opener
- escape-heading-opener
- escape-list-opener
- escape-mathblock-no-closer-escape
- escape-mathblock-opener
- escape-meta-closer
- escape-meta-opener
- escape-namedblock-opener
- escape-non-special
- escape-pagebreak-opener
- escape-quote-opener
- escape-special-char
- escape-spoilerblock-opener
010-block-structure
- block-cannot-interrupt-paragraph
- multiple-blank-lines
- paragraph-only-comment-not-elided
- trim-codeblock-edge-blanks-preserved
- trim-namedblock-edge-blanks
- trim-namedblock-empty-body
- trim-quoteblock-edge-blanks
- trim-spoilerblock-edge-blanks
012-whitespace-rules
- attr-orphan-no-strip
- attr-strip-heading
- attr-strip-namedblock-opener
- attr-strip-paragraph-trailing
- whitespace-adjacent-inlines
- whitespace-boundary-strip
- whitespace-empty-delimiter
- whitespace-inline-adjacent
- whitespace-interior-collapse
- whitespace-nested-boundary
diagnostics
- cdn-0001-unclosed-codeblock
- cdn-0002-unclosed-metablock
- cdn-0003-unclosed-mathblock
- cdn-0004-unclosed-namedblock
- cdn-0005-unclosed-spoilerblock
- cdn-0006-unclosed-commentblock
- cdn-0008-orphaned-caption-blank-line
- cdn-0008-orphaned-caption-no-predecessor
- cdn-0008-orphaned-caption-non-captionable
- cdn-0008-orphaned-caption-slot-filled
- cdn-0009-attrs-on-caption
- cdn-0011-orphaned-scope-chain
- cdn-0012-heading-level-out-of-range
- cdn-0013-nameless-namedblock-opener
- cdn-0014-emphasis-strong-cross
- cdn-0014-no-fire-valid-nesting
- cdn-0014-quote-link-cross
- cdn-0015-invalid-variable-key
- cdn-0016-no-fire-bare-pagebreak
- cdn-0016-pagebreak-tail-dropped
- cdn-0017-no-fire-glued-hyphens
- cdn-0017-pagebreak-inside-container
- cdn-0020-duplicate-id
- cdn-0021-class-syntax-conflict
- cdn-0022-duplicate-custom-key
- cdn-0030-meta-block-inside-container