# Processing Markdown

version 6.0.0-beta-1

Groovy has an optional `groovy-markdown` module which provides support for parsing CommonMark Markdown. The classes are found in the `groovy.markdown` package.

## 1. MarkdownSlurper

`MarkdownSlurper` parses Markdown text into a `MarkdownDocument` backed by nested lists and maps. Each node is a `Map` with a `type` key plus type-specific fields:

    def doc = new MarkdownSlurper().parseText('# Hello World')
    def h = doc.headings[0]
    assert h.level == 1
    assert h.text == 'Hello World'

The raw structure supports all standard Groovy list/map operations. The document also exposes convenience properties — `headings`, `codeBlocks`, `links`, `tables` — that recursively walk the tree.

### 1.1. Extracting code blocks

A common pattern when consuming LLM output is to pull fenced code blocks by language:

    def md = '''
        Some intro text.

        ```groovy
        println 'hi'
        ```

        ```sql
        SELECT 1
        ```
        '''.stripIndent()
    def doc = new MarkdownSlurper().parseText(md)
    def groovySnippets = doc.codeBlocks.findAll { it.lang == 'groovy' }*.text
    assert groovySnippets == ["println 'hi'\n"]

`codeBlocks` walks the entire tree, so blocks nested inside block quotes or list items are included.

### 1.2. Links

    def doc = new MarkdownSlurper().parseText('See [docs](https://example.com/docs).')
    def link = doc.links[0]
    assert link.href == 'https://example.com/docs'
    assert link.text == 'docs'

### 1.3. Sections

`section(headingText)` returns the nodes between the given heading and the next heading of equal or higher level — useful for parsing structured agent replies:

    def md = '''
        # Title

        ## Summary

        The work is on track.

        ## Next Steps

        - Item one
        - Item two
        '''.stripIndent()
    def doc = new MarkdownSlurper().parseText(md)
    def summary = doc.section('Summary')
    assert summary[0].type == 'paragraph'
    assert summary[0].children[0].value == 'The work is on track.'

    def next = doc.section('Next Steps')
    assert next[0].type == 'list'
    assert next[0].items*.text == ['Item one', 'Item two']

### 1.4. Tables (optional)

GFM-style tables are supported when the `org.commonmark:commonmark-ext-gfm-tables` jar is on the classpath. Call `enableTables(true)` on the slurper:

    def md = '''
        | name  | age |
        |-------|-----|
        | Alice | 30  |
        | Bob   | 25  |
        '''.stripIndent()
    def doc = new MarkdownSlurper().enableTables(true).parseText(md)
    def rows = doc.tables[0].rows
    assert rows[0].name == 'Alice'
    assert rows[0].age == '30'
    assert rows[1].name == 'Bob'

Each row is returned as a `Map` keyed by header.

### 1.5. Node types

type | fields
--- | ---
`heading` | `level`, `text`, `children`
`paragraph` | `children`
`code_block` | `lang`, `text`
`list` | `ordered`, `items` (`start` for ordered lists)
`list_item` | `text`, `children`
`block_quote` | `children`
`link` | `href`, `title`, `text`, `children`
`image` | `src`, `title`, `alt`
`text` | `value`
`inline_code` | `text`
`emphasis`, `strong` | `children`
`html_block`, `html_inline` | `text`
`thematic_break` | (no fields)
`hard_line_break`, `soft_line_break` | (no fields)
`table` | `headers`, `alignments`, `rows`

### 1.6. Untrusted input

Prefer parsing Markdown from trusted sources. Like the sibling `JsonSlurper` and `XmlSlurper`, `MarkdownSlurper` is a convenience parser, not a security boundary, so the safest choice is not to hand it attacker-controlled input. If you must, bound the input by size yourself before parsing and treat the parsed result defensively.

As a backstop, `MarkdownSlurper` caps how deeply a document may nest, so that a small but pathologically nested input — thousands of nested block quotes, or a long run of emphasis markers — is reported as a `MarkdownRuntimeException` rather than being allowed to exhaust the stack with a `StackOverflowError`. The limit defaults to `1000` and can be set per instance or globally via the `groovy.markdown.maxNestingDepth` system property; a value of `0` or less disables the check.

    def slurper = new MarkdownSlurper()
    slurper.maxNestingDepth = 100   // cap block nesting (default 1000; 0 disables the check)

    // a hostile, deeply nested document is rejected, not allowed to crash the parser
    def rejected = false
    try {
        slurper.parseText(('>' * 5000) + ' text')
    } catch (MarkdownRuntimeException ignored) {
        rejected = true
    }
    assert rejected

This cap is a robustness backstop, not a licence to treat the parser as hardened.