kex docs Tey

Tey.Docgen.Md

Markdown → typed blocks → HTML.

Docgen already had a Markdown *emitter* (Tey.Docgen.Markdown, model → md for LLMs) and an RDoc parser for doc comments; neither reads a Markdown document. This does, so hand-written prose can go through the same chrome as the generated reference.

The subset is what the kex.run guide pages actually use: YAML-ish frontmatter, ATX headings, paragraphs, fenced code (```kex runs through the highlighter), blockquotes, ordered and unordered lists with nesting, GFM tables, thematic breaks, and inline code / bold / italics / links. It is deliberately not CommonMark — there is no HTML passthrough, no reference links, no setext headings — and anything it does not recognise stays a paragraph rather than becoming an error.

Nothing here knows about guides, books, or the kexhq site.

module Tey.Docgen.Md

parseDocument

parseDocument(text: String) -> Document

The whole job: text in, document out.

toHtml

toHtml(text: String) -> String

Text in, HTML out — parse then render, which is all a caller wanting a page body needs.

splitFrontmatter

splitFrontmatter(lines: [String]) -> (Frontmatter, [String])

--- frontmatter -------------------------------------------------------

A leading --- fence delimits frontmatter. Without one the document is all body, and every field takes its default.

closingFence

closingFence(lines: [String], index: Integer) -> Integer?

frontmatterOf

frontmatterOf(lines: [String], accum: Frontmatter) -> Frontmatter

assign

assign(front: Frontmatter, key: String, value: String) -> Frontmatter

unquote

unquote(value: String) -> String

trimEnds

trimEnds(value: String) -> String

Drops the first and last character — the quotes quoted? just matched.

quoted?

quoted?(value: String, mark: String) -> Bool

parseBlocks

parseBlocks(lines: [String]) -> [MdBlock]

--- block structure ---------------------------------------------------

blockLoop

blockLoop(lines: [String], accum: [MdBlock]) -> [MdBlock]

One pass over the lines, each branch consuming the whole construct it recognises and returning the lines it did not take.

fence?

fence?(line: String) -> Bool

rule?

rule?(line: String) -> Bool

---, *** or ___, three or more, nothing else on the line. Checked after frontmatter is gone, so a stray --- here really is a break.

ruleOf?

ruleOf?(text: String, mark: String) -> Bool

headingLevel

headingLevel(line: String) -> Integer

## → 2. Zero when the line is not a heading; #hashtag is not one.

countHashes

countHashes(text: String, seen: Integer) -> Integer

takeCode

takeCode(lines: [String]) -> (MdBlock, [String])

--- fenced code -------------------------------------------------------

The info string is the language. An unterminated fence runs to the end of the document rather than failing — a truncated page still renders.

closingCodeFence

closingCodeFence(lines: [String], index: Integer) -> Integer

takeQuote

takeQuote(lines: [String]) -> (MdBlock, [String])

--- blockquotes -------------------------------------------------------

> is stripped from each line and what remains is parsed as blocks, so a quote can hold anything a document can, nesting included.

quoteLine?

quoteLine?(line: String) -> Bool

stripQuote

stripQuote(line: String) -> String

takeList

takeList(lines: [String]) -> (MdBlock, [String])

--- lists -------------------------------------------------------------

Nesting is by indentation: a line indented past the first item's marker belongs to that item and is parsed as blocks in its own right, so a list can hold paragraphs, code and deeper lists.

listLine?

listLine?(line: String) -> Bool

A list continues through its own items, indented continuation lines, and blank lines — but a blank line followed by unindented text ends it, which is why the run is cut at the first line that is neither.

listItems

listItems(lines: [String], indent: Integer, current: [String], accum: [ListItem]) -> [ListItem]

flushItem

flushItem(current: [String], accum: [ListItem]) -> [ListItem]

The first line is the item's own text; anything below it is nested content, dedented so it parses as a document of its own.

dropTrailingBlanks

dropTrailingBlanks(lines: [String]) -> [String]

dedent

dedent(lines: [String]) -> [String]

Removes the common indent, so nested content is parsed at column zero.

indentOf

indentOf(line: String) -> Integer

itemMarker

itemMarker(line: String) -> String

bulletMarker

bulletMarker(line: String) -> String

numberMarker

numberMarker(line: String) -> String

"12. " → "12." ; anything else → "".

tableStart?

tableStart?(lines: [String]) -> Bool

--- tables ------------------------------------------------------------

A GFM table is a header row and a delimiter row of dashes; without the delimiter the lines are ordinary paragraphs.

delimiterRow?

delimiterRow?(line: String) -> Bool

delimiterChar?

delimiterChar?(c: String) -> Bool

takeTable

takeTable(lines: [String]) -> (MdBlock, [String])

cellsOf

cellsOf(line: String) -> [String]

Leading and trailing pipes are optional, so the split is trimmed of the empty cells they produce.

takeParagraph

takeParagraph(lines: [String]) -> (MdBlock, [String])

--- paragraphs --------------------------------------------------------

Runs to a blank line or to the start of any other construct, so a list or fence directly under prose is not swallowed into it.

paragraphLine?

paragraphLine?(line: String) -> Bool

render

render(items: [MdBlock]) -> String

--- rendering ---------------------------------------------------------

renderBlock

renderBlock(block: MdBlock) -> String

renderCode

renderCode(language: String, body: String) -> String

Kex code goes through the same highlighter the reference pages use, so a snippet in the guide looks like a signature in the reference. Everything else is escaped and left plain.

renderItems

renderItems(items: [ListItem]) -> String

renderItem

renderItem(item: ListItem) -> String

renderTable

renderTable(header: [String], rows: [[String]]) -> String

renderRow

renderRow(row: [String]) -> String

anchor

anchor(text: String) -> String

A heading's id: lowercase, non-alphanumerics folded to single dashes.

trimDashes

trimDashes(text: String) -> String

String.trim takes no argument, and the folding above can leave a dash at either end (## \try\ → -try-).

anchorChar

anchorChar(c: Char) -> String

squeezeDashes

squeezeDashes(text: String, accum: String) -> String

stripInline

stripInline(text: String) -> String

Inline markers removed, for anchors and any other plain-text use.

inline

inline(text: String) -> String

--- inline ------------------------------------------------------------

Code spans first and separately: their content is escaped but never scanned for emphasis, so a*b*c inside backticks stays literal.

inlineLoop

inlineLoop(text: String, accum: String) -> String

emphasis

emphasis(text: String) -> String

Links before emphasis, so a * inside a URL is not read as a marker.

linkParts

linkParts(rest: String) -> (String, String, String)?

label](href) → (label, href, what follows). None when the shape is not a link after all, in which case the [ is literal text.

marks

marks(text: String) -> String

**bold** before *italic*, or the opening ** would parse as an empty emphasis. Escaping happens here, on the text that is left.

wrap

wrap(text: String, strongMark: String, strongTag: String, emMark: String, emTag: String) -> String

pairs

pairs(text: String, mark: String, tag: String, accum: String) -> String

Replaces each surviving pair of markers with a tag. An unpaired marker is left as written rather than swallowing the rest of the line.

indexOfText

indexOfText(haystack: String, needle: String, at: Integer) -> Integer?

Where needle first occurs in haystack, or None. String.indexOf takes a Char, and the markers here are two characters wide ("**"), so the search is spelled out.

esc

esc(s: String) -> String

record Frontmatter

A document's frontmatter: the keys the prose pipeline reads. Unknown keys are parsed and ignored rather than rejected, so a page can carry metadata a later renderer will use.

Fields

title
String optional
description
String optional
order
Integer optional

record ListItem

One item of a list: its own inline text, plus any blocks nested under it.

Fields

text
String
children
[MdBlock]

type MdBlock

Variants

  • Heading(Integer, String)
  • Paragraph(String)
  • Code(String, String)
  • Quote([MdBlock])
  • Bullets([ListItem])
  • Numbers([ListItem])
  • Table([String], [[String]])
  • Rule

record Document

Fields

frontmatter
Frontmatter
blocks
[MdBlock]