Documentation
¶
Overview ¶
Package markdown is an Arandu module that turns a Markdown body into what a page draws from it: HTML that is safe to write unescaped, the headings a table of contents lists, the plain text search and generative engines read, and the reading time.
It does not parse Markdown. hesape/str.Markdown is the collection's one renderer -- CommonMark with the GitHub tables, task lists, strikethrough and autolinks -- and a second parser here would be a second way to spell the same document. What str.Markdown leaves to its caller is the reason this package exists: raw HTML passes through it untouched, so rendering a body somebody typed renders whatever HTML they typed.
This package reads that output back as HTML and writes it again through a closed list of elements and attributes. Everything on the list is what the renderer itself emits; everything else is shown as the text it was, so an author who pasted an iframe sees the tag on the page instead of wondering where it went. Links keep http(s), mailto, same-site paths and in-page anchors. Images keep same-site paths and the origins the application names, which is what its content security policy lets a page load.
The files are laid out by role:
module.go -> registration and the one entry point, Render config.go -> what the application passes in render.go -> the allowlist and the rewrite
An application builds it once in bootstrap/app.go and registers it there. There is no service provider, no container and no discovery.
Index ¶
Constants ¶
const ( // DefaultTopHeading is the level the highest heading of a body is drawn at // when Config leaves TopHeading at zero. A page has one h1, its own title, // so a body starts one level below it. DefaultTopHeading = 2 // DefaultWordsPerMinute is the reading speed Document.Minutes is measured // at when Config leaves WordsPerMinute at zero. DefaultWordsPerMinute = 200 // MaxWordsPerMinute is the ceiling WordsPerMinute is refused above. A // speed nobody reads at is a reading time that says every body takes a // minute. MaxWordsPerMinute = 1000 )
The defaults for the optional settings. They are constants rather than literals inside Config.withDefaults, so the value a reader finds here is the value the package uses.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// TopHeading is the level the highest heading of a body is drawn at, from
// 1 to 6. Zero means DefaultTopHeading.
//
// A heading above it is lowered to it rather than shifted: a body written
// with # and ## for its sections comes out with both at this level, and
// the ### under them one below. Shifting every level by one would turn the
// ## an author meant as a section into a subsection whenever somebody else
// in the same collection used #.
TopHeading int
// Reserved are the element ids the page around the body already uses --
// the skip link's target, the comment form, the share bar. A heading never
// takes one, so an anchor the layout owns keeps pointing where it did.
Reserved []string
// ImageOrigins are the origins an image in a body may load from besides the
// page's own: bare https origins, such as the CDN a media library serves
// from. Empty means same-site paths only.
//
// It is the list the application's content security policy names in
// img-src, and it is asked for here rather than read from there because a
// renderer that emitted an image the policy refuses would draw a broken
// picture instead of its description.
ImageOrigins []string
// WordsPerMinute is the reading speed Document.Minutes is measured at.
// Zero means DefaultWordsPerMinute.
WordsPerMinute int
}
Config is what the application passes when it builds the renderer.
A typed struct rather than a map: a misspelled key in a map is a setting that silently keeps its default, and the failure shows up as a page nobody asked for rather than as an error. Here a field that does not exist does not compile.
type Document ¶
type Document struct {
// HTML is the body as markup. Every element and attribute in it is one the
// allowlist wrote, so a view writes it unescaped.
HTML template.HTML
// Headings are the top two levels of the body's own headings, in order,
// for a table of contents. A heading inside a quote or a list is part of
// that block, not a section of the page, and is not listed.
Headings []Heading
// Text is what the body says, without markup: one line per block, for a
// search index, a description, llms-full.txt and the word count. It is
// never HTML, and a view that prints it escapes it like any other string.
Text string
// Words is the number of words in Text.
Words int
// Minutes is the reading time at Config.WordsPerMinute, rounded up: at
// least one for a body with any words, and zero for one without.
Minutes int
}
Document is a rendered body.
type Heading ¶
type Heading struct {
// Level is the level the heading was drawn at, after Config.TopHeading.
Level int
// ID is the element id the heading carries, unique in the body and never
// one of Config.Reserved, so "#" + ID is its address.
ID string
// Text is the heading without markup.
Text string
}
Heading is one entry of a table of contents.
type Module ¶
type Module struct {
// contains filtered or unexported fields
}
Module is what the application registers, and what renders a body.
It holds nothing that changes after New, so one value is safe to share between every request that renders.
func New ¶
New returns the module, or the reason it cannot be built.
It returns an error rather than panicking or carrying on, because everything it refuses is a wiring mistake, and a wiring mistake found at boot costs one restart. The same mistake found later is a page drawn with a broken picture.
func (*Module) Render ¶
Render renders a Markdown body.
It never fails: a body is text somebody wrote, and every part of it that is not on the allowlist is still text. What comes back is the same for the same source and configuration, so a caller may cache it by the source.
The cost is str.Markdown's plus one linear pass. str.Markdown is linear on ordinary prose and quadratic on a run of link openers that never close, so a field that takes a body from people the application does not trust caps its length, as a form field does anyway.
func (*Module) Routes ¶
Routes registers nothing.
A body reaches a page through the application's own controller, which already holds the Grant that decided whether the reader may see the entry it belongs to. A route here would render text for anybody who sent some, with no entry and no Grant behind it.