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
// LinkRel is the rel attribute every link to an http or https address
// carries: space-separated link types out of external, nofollow, noopener,
// noreferrer, sponsored and ugc, such as "noopener nofollow ugc" for a body
// visitors wrote. Each is written once, in the order given. Empty writes no
// rel at all, which is the output a configuration without it always had.
//
// A same-site path, an in-page anchor and a mailto: address never carry
// it: each leads to a page the site serves itself or to no page at all, so
// there is no other site for the relation to describe, and a nofollow on
// the site's own pages would tell a crawler to skip them.
LinkRel string
// Images is what an image in a body becomes. Empty means
// ImagesAllowedOrigins.
Images ImageMode
}
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 {
// 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
// contains filtered or unexported fields
}
Document is a rendered body.
func (Document) HTML ¶
HTML is the body as markup. Every element and attribute in it is one the allowlist wrote, and every character of text in it was escaped, so a view writes it unescaped: {!! .Body.HTML() !!} in Kyse.
It is a method and not a field because that is the shape a view's raw output is entitled to. `aru doctor` accepts {!! !!} around a call -- markup something produced by escaping -- and warns on a value, which is what a field reads as; the renderer is the thing that escaped this one.
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 ImageMode ¶ added in v0.3.0
type ImageMode string
ImageMode is what an image in a body becomes.
const ( // ImagesAllowedOrigins draws an image whose address is a same-site path or // on an origin Config.ImageOrigins names, and writes the description of // any other as text. It is what the empty ImageMode means. ImagesAllowedOrigins ImageMode = "allowed-origins" // ImagesNone never draws an image: every one is written as its // description, as text, wherever its address points. It is for a body // nobody vetted, where a picture is a request the reader's browser makes // to an address the author chose, same-site paths included. ImagesNone ImageMode = "none" )
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, and str.Markdown is linear in the length of the source on hostile input too: on a run of openers that never close, and on quotes and lists nested to the hundred levels it allows, whose lines it holds once rather than copying them at every level. Linear is not cheap on every shape -- a line under a hundred nested lists is read again at each of them -- so a field that takes a body from people the application does not trust still 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.