# santoku-markdown > Markdown to HTML rendering: a C binding to Sundown exposing a single to_html function This file is one tab of the santoku reference, at https://santoku.dev/santoku-markdown/llms.txt. https://santoku.dev/santoku-markdown is the same tab in a browser, https://santoku.dev/llms.txt indexes every tab, and https://santoku.dev/llms-small.txt summarizes them all on one page. santoku-markdown is the framework's Markdown renderer: a C binding to Sundown that exposes a single function, to_html, taking a Markdown string and returning the rendered HTML string. The binding constructs Sundown's standard HTML renderer with every render flag set to zero and a block nesting depth of 16. On top of base Markdown (ATX and setext headings, paragraphs, lists, blockquotes, code, emphasis, links, images, backslash escapes and raw HTML passthrough) it enables the Sundown extensions tables, fenced_code, autolink, strikethrough, no_intra_emphasis and space_headers. superscript and lax_spacing stay off. Output is plain HTML: hr and br render without the self-closing slash. It compiles both natively and to WebAssembly, so the same call renders on the server and in the browser; this docs site renders its own prose through it at build time. ### to_html: render a document The whole API is one function: Markdown string in, HTML string out. Block elements are separated by blank lines in the output and each ends with a newline. ```lua local md = require("santoku.markdown") local html = md.to_html([=[ # Title Some text ## Subtitle 1. Hello 2. World ]=]) print(html) return #html ``` ### Headings, three ways ATX hashes give h1 through h6. The space_headers extension is on, so a heading needs a space after its hashes, and #tag stays paragraph text. Trailing hashes and spaces are stripped. Setext underlines with equals or dashes give h1 and h2. ```lua local md = require("santoku.markdown") print(md.to_html("# one\n## two\n### three\n###### six")) print(md.to_html("#tag stays text")) print(md.to_html("## closed style ##")) return md.to_html("Top\n===\n\nSection\n-------") ``` ### Inline markup Emphasis, strong, code spans, and links inside a paragraph. ```lua local md = require("santoku.markdown") print(md.to_html("*emphasis*, **strong**, and `code spans`")) print(md.to_html("See [santoku](https://santoku.dev/#santoku).")) return md.to_html("plain text") ``` ### Emphasis in detail Asterisks and underscores are interchangeable, and tripling nests strong and em. The no_intra_emphasis extension is on, so underscores inside a word never emphasize and snake_case identifiers survive as plain text. ```lua local md = require("santoku.markdown") print(md.to_html("*star* _underscore_ **bold** __also bold__")) print(md.to_html("***both at once***")) print(md.to_html("a snake_case_name stays intact")) return md.to_html("`snake_case_name` survives in a code span") ``` ### Code spans Single backticks open a span, double backticks let a literal backtick appear inside, and surrounding spaces next to the delimiters are trimmed. ```lua local md = require("santoku.markdown") print(md.to_html("call `md.to_html(s)` to render")) print(md.to_html("use `` ` `` for a literal backtick")) return md.to_html("`a < b` escapes inside spans too") ``` ### Block elements Blockquotes, four-space indented code blocks, and horizontal rules. The rule renders as a bare hr tag because the XHTML flag is off. ```lua local md = require("santoku.markdown") print(md.to_html("> Markdown in, HTML out.")) print(md.to_html(" local x = 1\n print(x)")) return md.to_html("---") ``` ### Lists and nesting Dashes, pluses, and asterisks all open unordered lists, four extra spaces of indent nest a sublist, and ordered list source numbers are discarded: the renderer emits a plain ol and the browser numbers it. ```lua local md = require("santoku.markdown") print(md.to_html("- dash\n+ plus\n* star")) print(md.to_html("- fruits\n - apple\n - pear\n- roots\n - beet")) return md.to_html("3. renders first\n7. renders second\n1. renders third") ``` ### Tight versus loose lists Items packed together render bare text inside each li; blank lines between items switch the list to block mode and wrap every item's text in paragraph tags. ```lua local md = require("santoku.markdown") print(md.to_html("- alpha\n- beta")) return md.to_html("- alpha\n\n- beta") ``` ### Blank lines matter Without the lax_spacing extension, a list glued to a paragraph stays paragraph text: the blank line is what opens the list. ```lua local md = require("santoku.markdown") local glued = md.to_html("Shopping:\n- milk\n- eggs") print(glued) return md.to_html("Shopping:\n\n- milk\n- eggs") ``` ### Hard line breaks A single newline inside a paragraph is a soft wrap and renders as whitespace; two trailing spaces before the newline force a br tag (no self-closing slash, XHTML is off). ```lua local md = require("santoku.markdown") print(md.to_html("soft wrap\nsame paragraph")) return md.to_html("hard break \nnew line, same paragraph") ``` ### Links: inline, reference, implicit Inline links take an optional quoted title. Reference definitions live on their own line anywhere in the document, and an empty second bracket reuses the link text as the reference name. ```lua local md = require("santoku.markdown") print(md.to_html('[rock](https://santoku.dev/#santoku-markdown "the rock")')) print(md.to_html("read [the docs][site] first\n\n[site]: https://example.com")) return md.to_html("[example][]\n\n[example]: https://example.com") ``` ### Images Image syntax is link syntax with a leading bang: alt text, href, optional title. The src and title are attribute-escaped and the tag closes without a slash. ```lua local md = require("santoku.markdown") print(md.to_html('![logo](logo.png "Santoku")')) return md.to_html("![plain alt](icons/knife.svg)") ``` ### Autolinks: angle brackets and bare URLs Wrapping a URL or email in angle brackets links it, and emails gain a mailto: prefix. The autolink extension is on, so a bare URL links too. ```lua local md = require("santoku.markdown") print(md.to_html("go to now")) print(md.to_html("mail about it")) return md.to_html("bare https://example.com links too") ``` ### Backslash escapes A backslash before any of \ ` * _ { } [ ] ( ) # + - . ! : | & < > ^ ~ renders the character literally; before anything else the backslash itself stays in the output. ```lua local md = require("santoku.markdown") print(md.to_html("\\*not emphasized\\* but *emphasized*")) print(md.to_html("\\# not a heading")) return md.to_html("\\q is not in the escape set") ``` ### Entities and ampersands Anything matching the entity syntax (ampersand, optional hash, alphanumerics, semicolon) passes through untouched; a lone ampersand is escaped to amp. ```lua local md = require("santoku.markdown") print(md.to_html("& and © pass through")) print(md.to_html("AT&T gets its ampersand escaped")) return md.to_html("¬anentity because no semicolon") ``` ### HTML escaping Special characters in plain text and code spans are entity-escaped: angle brackets, ampersands, double quotes, and single quotes. ```lua local md = require("santoku.markdown") print(md.to_html("a < b & c")) print(md.to_html("`5 > 4` is true")) print(md.to_html("it's got 'singles'")) return md.to_html('say "hello"') ``` ### Raw HTML passes through With all render flags at zero, inline tags and block-level HTML are emitted verbatim, not escaped. Useful for trusted prose, and a reason to sanitize or pre-escape untrusted input before rendering. ```lua local md = require("santoku.markdown") print(md.to_html("inline spans survive")) return md.to_html("
\nA whole block, kept as is.\n
") ``` ### Fenced code, pipe tables and strikethrough The binding enables the fenced_code, tables and strikethrough extensions: triple backticks open a code block, pipe rows with a dashed separator render a table, and double tildes render del. Indented code blocks still work. ```lua local md = require("santoku.markdown") print(md.to_html("```\na code block\n```")) print(md.to_html("| a | b |\n|---|---|\n| 1 | 2 |")) return md.to_html("~~struck~~") ``` ### Argument handling and edge cases The binding takes exactly one argument through luaL_checklstring: extra arguments are dropped, numbers coerce through Lua's standard string conversion, non-strings like nil raise, and empty input renders to the empty string. ```lua local md = require("santoku.markdown") print(md.to_html("first", "ignored", "also ignored")) print(md.to_html(42)) print("empty in, empty out:", "[" .. md.to_html("") .. "]") local ok, err = pcall(md.to_html, nil) print(ok, err) return ok ``` ### Composing with santoku.string Interpolate a Markdown template with the base library, then render it. ```lua local md = require("santoku.markdown") local str = require("santoku.string") local doc = str.interp("# %name\n\nThe %name rock wraps %impl and returns HTML.", { name = "santoku-markdown", impl = "Sundown", }) print(doc) return md.to_html(doc) ``` ### Data to page with santoku.mustache The two renderers chain: mustache turns a table into Markdown source, to_html turns that into markup. A changelog or notes pipeline works this way. ```lua local md = require("santoku.markdown") local mch = require("santoku.mustache") local tpl = mch("# {{title}}\n\n{{#items}}- {{.}}\n{{/items}}") local doc = tpl({ title = "Release notes", items = { "faster parse", "smaller wasm", "same one-function API" }, }) print(doc) return md.to_html(doc) ```