Documents are worked on by section. Automation pulls the "Usage" section out of a README, replaces the body of a generated section while leaving hand-written ones untouched, lifts a section and everything under it into another document, or checks that every required section is present. In each of these jobs the unit of work is a heading together with everything that belongs to it.
Request
Current experience
The object hierarchy specified in #8 models a document the way CommonMark defines it: a heading is a leaf block sitting as a sibling next to paragraphs, lists, and code blocks in one flat Children collection. Nothing in the object graph says that a heading owns the content beneath it. Acting on a section means finding the heading, scanning forward for the next heading of the same or a lower level, and slicing the collection — the outline rules of markdown, reimplemented at every call site, and easy to get wrong at the edges.
The DSL has the opposite shape. Heading 1 'Title' { ... } already treats a section as a container with its content inside it, and that is the model the module presents to its users today. A parsed document that comes back flat does not read like the document the same user just wrote.
Desired experience
A section is a first-class object. A document holds a list of sections; a section holds its heading, its own content, and the sections nested inside it — recursively, to any depth. A section with no subsections is the same type with an empty collection, not a different type.
Document
├── FrontMatter the metadata part
├── (blocks) content before the first heading
└── Section a heading and everything under it
├── Heading
├── (blocks) content before the first subheading
└── Section recursive, empty for a leaf section
The shape a caller works with:
$doc = Get-Content -Raw 'README.md' | ConvertFrom-Markdown
# A section is addressable by its heading, not by index arithmetic over a flat list.
$usage = $doc.GetSection('Usage')
# It carries its subsections with it, so moving or copying it is one assignment.
$other.Children.Add($usage)
# Replacing a generated section leaves every hand-written section alone.
$doc.GetSection('Usage/Parameters').Children = $generated.Children
# The outline falls out of the model rather than being computed from heading levels.
$doc.Descendants('Section') | ForEach-Object { $_.Heading.GetText() }
$doc | ConvertTo-Markdown | Set-Content 'README.md'
Acceptance criteria
- A document parses into a tree of sections, where a section owns its heading, its own blocks, and its nested sections.
- A section with no nested sections is the same type as one that has them, holding an empty collection.
- The document is structurally the same container as a section, minus a heading and plus frontmatter, so the same code walks both.
- Content that appears before the first heading belongs to the document; content before the first subheading belongs to the section it is under.
- A skipped heading level — an
h1 followed by an h3 — nests without inventing a section that is not in the document, and re-renders at its original level.
- A document that starts at a level other than
h1, or that raises the level again later, parses without error and re-renders unchanged.
- Headings inside a block quote or a list item section that container's own content, not the document.
- Markdown rendered from the section tree is byte-identical to markdown rendered from the flat hierarchy for the same document, so CommonMark conformance is unaffected.
- A section is addressable by its heading text, including a path through nested headings.
- The existing
Set-Markdown* DSL keeps working unchanged.
Out of scope
- Heading anchors and slugs. Slug generation is platform-specific rather than a CommonMark construct, and belongs with the dialect work in #30.
- Every construct below the section level. The block and inline nodes stay exactly as specified in #8.
- Section-aware editing cmdlets. This issue delivers the model; any
Get-MarkdownSection style surface is decided separately.
Technical decisions
Sections replace the flat heading model, and this is not a breaking change: #8 is milestone 1.3 and has not shipped — the latest release is v1.2.5. The section tree therefore lands as part of 1.3 rather than as a change to it. #8 is restructured around this model and stays the epic; this issue owns the section layer.
MarkdownSection is a block node: It derives from MarkdownBlock like every other container, so $_ -is [MarkdownBlock] keeps working and the section tree is not a parallel structure bolted onto the side of the hierarchy.
The heading is a property, not a child: MarkdownSection exposes [MarkdownHeading] $Heading alongside Children. A section has exactly one heading, and a property makes that structural rather than a convention about Children[0]. $section.Heading.Level++ is also the obvious way to write the most common mutation. Descendants() yields a section's Heading before its Children, which is one documented traversal rule rather than type-specific branching in caller code.
Children stays the single storage and the single serialization surface: A section's blocks and its nested sections live in one Children collection in document order — content first, subsections after, which is the only order markdown can produce. Sections() and Blocks() are filter methods over Children, following the existing Descendants() idiom. They are methods rather than properties on purpose: a property returning a filtered view would put the same node under two names, and ConvertTo-Json, ConvertTo-Yaml, and Export-Clixml would emit it twice — the duplicate-reference problem #8 already rules out.
Nesting depth is not the heading level: MarkdownHeading.Level remains the source of truth for rendering, and nesting depth is never used to derive it. A document that goes h1 then h3 nests the h3 section directly under the h1 section and re-renders it as an h3. No synthetic section is inserted for the missing level, because a section that is not in the document is not in the tree.
Sectioning is a rule about block sequences, not about the document: Any block container — the document, a section, a block quote, a list item — groups its own child blocks into sections. A heading opens a section at its level, closes every open section at that level or deeper, and attaches to the nearest open section of a lower level, or to the container. One algorithm, applied everywhere blocks appear.
Rendering flattens: A section emits its heading followed by its children in order. The output for a given document is identical to what the flat hierarchy would emit, so the conformance suite in #8 measures the same thing and the round-trip contract is unchanged.
File placement: MarkdownSection joins the other block classes in src/classes/public/Blocks/, per the layout decided in #8. The section-grouping pass is a parser internal in src/functions/private/.
Specification: The normative model — requirements, acceptance criteria, and the sectioning algorithm — lives in docs/markdown-object-model/spec.md in this repository, following Spec-Driven Development. This issue describes the change; the spec describes the intended state.
Implementation plan
Specification
Model
Parser and renderer
Tests
Documentation
Documents are worked on by section. Automation pulls the "Usage" section out of a README, replaces the body of a generated section while leaving hand-written ones untouched, lifts a section and everything under it into another document, or checks that every required section is present. In each of these jobs the unit of work is a heading together with everything that belongs to it.
Request
Current experience
The object hierarchy specified in #8 models a document the way CommonMark defines it: a heading is a leaf block sitting as a sibling next to paragraphs, lists, and code blocks in one flat
Childrencollection. Nothing in the object graph says that a heading owns the content beneath it. Acting on a section means finding the heading, scanning forward for the next heading of the same or a lower level, and slicing the collection — the outline rules of markdown, reimplemented at every call site, and easy to get wrong at the edges.The DSL has the opposite shape.
Heading 1 'Title' { ... }already treats a section as a container with its content inside it, and that is the model the module presents to its users today. A parsed document that comes back flat does not read like the document the same user just wrote.Desired experience
A section is a first-class object. A document holds a list of sections; a section holds its heading, its own content, and the sections nested inside it — recursively, to any depth. A section with no subsections is the same type with an empty collection, not a different type.
The shape a caller works with:
Acceptance criteria
h1followed by anh3— nests without inventing a section that is not in the document, and re-renders at its original level.h1, or that raises the level again later, parses without error and re-renders unchanged.Set-Markdown*DSL keeps working unchanged.Out of scope
Get-MarkdownSectionstyle surface is decided separately.Technical decisions
Sections replace the flat heading model, and this is not a breaking change: #8 is milestone 1.3 and has not shipped — the latest release is v1.2.5. The section tree therefore lands as part of 1.3 rather than as a change to it. #8 is restructured around this model and stays the epic; this issue owns the section layer.
MarkdownSectionis a block node: It derives fromMarkdownBlocklike every other container, so$_ -is [MarkdownBlock]keeps working and the section tree is not a parallel structure bolted onto the side of the hierarchy.The heading is a property, not a child:
MarkdownSectionexposes[MarkdownHeading] $HeadingalongsideChildren. A section has exactly one heading, and a property makes that structural rather than a convention aboutChildren[0].$section.Heading.Level++is also the obvious way to write the most common mutation.Descendants()yields a section'sHeadingbefore itsChildren, which is one documented traversal rule rather than type-specific branching in caller code.Childrenstays the single storage and the single serialization surface: A section's blocks and its nested sections live in oneChildrencollection in document order — content first, subsections after, which is the only order markdown can produce.Sections()andBlocks()are filter methods overChildren, following the existingDescendants()idiom. They are methods rather than properties on purpose: a property returning a filtered view would put the same node under two names, andConvertTo-Json,ConvertTo-Yaml, andExport-Clixmlwould emit it twice — the duplicate-reference problem #8 already rules out.Nesting depth is not the heading level:
MarkdownHeading.Levelremains the source of truth for rendering, and nesting depth is never used to derive it. A document that goesh1thenh3nests theh3section directly under theh1section and re-renders it as anh3. No synthetic section is inserted for the missing level, because a section that is not in the document is not in the tree.Sectioning is a rule about block sequences, not about the document: Any block container — the document, a section, a block quote, a list item — groups its own child blocks into sections. A heading opens a section at its level, closes every open section at that level or deeper, and attaches to the nearest open section of a lower level, or to the container. One algorithm, applied everywhere blocks appear.
Rendering flattens: A section emits its heading followed by its children in order. The output for a given document is identical to what the flat hierarchy would emit, so the conformance suite in #8 measures the same thing and the round-trip contract is unchanged.
File placement:
MarkdownSectionjoins the other block classes insrc/classes/public/Blocks/, per the layout decided in #8. The section-grouping pass is a parser internal insrc/functions/private/.Specification: The normative model — requirements, acceptance criteria, and the sectioning algorithm — lives in
docs/markdown-object-model/spec.mdin this repository, following Spec-Driven Development. This issue describes the change; the spec describes the intended state.Implementation plan
Specification
docs/markdown-object-model/spec.mdwith the section model, its requirements, and the sectioning algorithmdocs/markdown-object-model/index.mddescribing the capabilityModel
MarkdownSectiontosrc/classes/public/Blocks/with aHeadingproperty and aChildrencollectionSections()andBlocks()filter methods toMarkdownNodeDescendants()to yield a section'sHeadingbefore itsChildrenParser and renderer
Tests
h1, and a level that rises againDocumentation