Documentation
¶
Overview ¶
Package html converts Markdown to the HTML a built page's body carries.
It is the converter half of selfdoc's page rendering: block tokens in, body HTML out, plus the pieces that operate on that HTML afterwards -- heading anchors, the glossary's definition sites and cross-page term links, internal link rewriting, the syntax-highlight stylesheet, and the JavaScript minifier. The page chrome that wraps a body -- navigation, table of contents, breadcrumbs, SEO tags, pickers -- is built on top of this package rather than inside it.
Heading anchors are decided once ¶
AssignHeadingAnchors is the one place a heading's element id is decided. The renderer emits those ids and the search index links to them, so a repeated heading gets "setup", "setup-1", "setup-2" in both. Because the input is the block token list, a "#"-prefixed line inside a fenced code block is code and never becomes an anchor.
Highlighting ¶
Code blocks are highlighted at build time by chroma, and the stylesheet that paints them is generated by GeneratePygmentsCSS as one set of custom properties defined three times over -- the default scheme, an explicitly chosen dark one, and the system fallback for a reader who has recorded no preference -- referenced by one scheme-agnostic set of rules. A token's colour is therefore a value in the token layer, and the two schemes cannot drift apart rule by rule.
Chroma replaces the Pygments this package's Python predecessor used, so the emitted token class names are chroma's. Everything else about the stylesheet's shape is unchanged.
Index ¶
- Constants
- func AdmonitionTypes() []string
- func ApplyCrossPageTerms(bodyHTML string, siteTerms *SiteTerms, currentPage, prefix, sitePrefix string) string
- func EmittedRef(mdPath, ref string, legacyHTMLLinks bool) string
- func EscapeHTML(text string) string
- func GeneratePygmentsCSS(lightStyle, darkStyle string) (string, error)
- func GetCSS(themeName string) (string, error)
- func HTMLPathToURL(htmlPath string) string
- func HTMLToMdPath(htmlPath string) string
- func LinkDefinitionSites(bodyHTML string, siteTerms *SiteTerms, currentPage, glossaryURL string) string
- func MdToHTML(text string, metadata, cfg map[string]any) string
- func MdToHTMLPath(mdPath string) string
- func MinifyJS(jsText string) string
- func PageTitleAnchor(title string) string
- func ParseTable(tableLines []string) string
- func PathHop(p, prefix, sitePrefix string) string
- func RewriteInternalLinks(bodyHTML, mdPath string, legacyHTMLLinks bool) string
- func Slugify(text string) string
- func SourceRefs(source, mdPath string) map[string]bool
- func TermAnchor(term string) string
- func ThemeCSSRel(themeMeta *themes.Metadata) string
- type CalloutKind
- type DeclaredTerm
- type HeadingAnchor
- type SiteTerm
- type SiteTerms
Constants ¶
const ( // ChevronIcon is the framework's own chevron. ChevronIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" ` + `stroke-width="2.5"><path d="M6 9l6 6 6-6"/></svg>` // CloseIcon dismisses a dialog or a notice. CloseIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" ` + `stroke-width="2"><path d="M5 5l14 14M19 5L5 19"/></svg>` // MenuIcon opens the mobile sidebar. MenuIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" ` + `stroke-width="2"><path d="M3 6h18M3 12h18M3 18h18"/></svg>` // InfoIcon marks the "info" callout kind. InfoIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" ` + `stroke-width="2"><rect x="3" y="3" width="18" height="18"/>` + `<path d="M12 10v6M12 7v.5"/></svg>` // WarnIcon marks the "warn" and "danger" callout kinds. WarnIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" ` + `stroke-width="2"><path d="M12 3L2 21h20z"/>` + `<path d="M12 10v5M12 18v.5"/></svg>` // NoteIcon marks the "note" callout kind. NoteIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" ` + `stroke-width="2"><path d="M17 3l4 4L8 20H4v-4z"/></svg>` // CheckIcon marks the "tip" callout kind. CheckIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" ` + `stroke-width="2.5"><path d="M4 12.5l5 5L20 6"/></svg>` // NoticeIcon is the superseded-version banner's glyph. The banner is // the "warn" kind, so it carries the warn glyph. NoticeIcon = WarnIcon )
The framework's icon set this build emits, copied verbatim from its own sources so a server-emitted component and a factory-built one carry the same glyph.
const PygmentsScope = ".tm-code code"
PygmentsScope is the selector every highlight rule is written under. It matches the markup a rendered code block carries, and nothing outside a code block.
const PygmentsVarPrefix = "--sd-hl-"
PygmentsVarPrefix is the prefix every generated highlight custom property carries.
Variables ¶
This section is empty.
Functions ¶
func AdmonitionTypes ¶
func AdmonitionTypes() []string
AdmonitionTypes returns, sorted, every admonition name a GitHub-flavored blockquote marker may name -- the "TYPE" in a leading "> [!TYPE]" line.
A blockquote whose marker names anything else is a plain blockquote.
func ApplyCrossPageTerms ¶
func ApplyCrossPageTerms(bodyHTML string, siteTerms *SiteTerms, currentPage, prefix, sitePrefix string) string
ApplyCrossPageTerms links the first occurrence of each cross-page term in bodyHTML.
For every term defined on a DIFFERENT page, the first occurrence in bodyHTML that is not inside an <a>, <code>, <pre>, <dfn>, <dt> or heading element is wrapped in an <a class="term-link"> pointing at the definition page. Only the first match per term is linked, to avoid link spam.
A term can be defined on either side of the mount boundary, so the hop is chosen per target: prefix reaches the project's own pages and sitePrefix the site level, and under a mount those are two different roots. A caller with only one root passes it as both.
func EmittedRef ¶
EmittedRef returns the href that ref, written on the page whose Markdown source is mdPath, is emitted as.
It is the one rule RewriteInternalLinks applies, per reference: a reference naming a page of this docs tree comes back as the address the build gives that page, and every other reference -- another origin, a fragment, a path outside the tree, anything that is not a ".md" -- comes back as written, which is how the renderer emits it.
legacyHTMLLinks is what RewriteInternalLinks documents: it additionally treats a relative "*.html" reference as naming the same page's Markdown source, and is set only for archive builds.
func EscapeHTML ¶
EscapeHTML escapes the HTML special characters "&", "<", ">" and the double quote, and deliberately not the apostrophe.
It is util.EscapeHTML, re-exported because every emitter in this package and in the page chrome above it escapes through one name.
func GeneratePygmentsCSS ¶
GeneratePygmentsCSS generates the syntax-highlight CSS, tokenized across the light/dark split.
Two highlight styles are resolved -- one per colour scheme -- and neither of them reaches a rule as a literal. Every declaration either style makes becomes a custom property defined three times over (the default scheme, an explicitly chosen dark one, and the system fallback for a reader with no choice recorded) and referenced once by a single set of rules. A token's colour is therefore a value in the token layer and the rules that paint it are scheme-agnostic, which is the shape the framework's conformance checker requires and, independent of that, the only spelling where the two schemes cannot drift apart rule by rule.
The three token blocks are spelled ":root", `html[data-theme="dark"]` and "html:not([data-theme])" inside a prefers-color-scheme query -- the same CSS-only three-state resolution the tinymoon theme uses, so a reader who has expressed no preference and runs no JavaScript still gets the dark palette.
lightStyle and darkStyle are Pygments style names, as a theme's companion JSON declares them; see [chromaStyleNames] for the one name whose chroma spelling differs. An unknown name is an error.
A variable name is derived from a selector, so two selectors that slug the same would share one value and paint one of the two tokens wrong. Nothing in chroma's class vocabulary collides today; a style that introduced one would be a silent miscolouring, so it is an error.
func GetCSS ¶
GetCSS returns the composed stylesheet for the named theme.
It is themes.CSS, re-exported so a page renderer reads its stylesheet and its highlight sheet from one package.
func HTMLPathToURL ¶
HTMLPathToURL converts an HTML file path to its clean URL form.
"guide/index.html" becomes "guide/", and "index.html" stays "index.html" (the root page). Used for link hrefs, canonical URLs and sitemap entries.
func HTMLToMdPath ¶
HTMLToMdPath is the reverse of MdToHTMLPath.
"guide/index.html" becomes "guide.md", "index.html" becomes "index.md", and "api/endpoints/index.html" becomes "api/endpoints.md".
func LinkDefinitionSites ¶
func LinkDefinitionSites(bodyHTML string, siteTerms *SiteTerms, currentPage, glossaryURL string) string
LinkDefinitionSites turns each definition site on currentPage into a glossary link.
The <dfn> an author wrote keeps its id and gains a tooltip with the definition's first sentence; its text becomes a link to the term's glossary entry. Other occurrences of the term on the same page are left alone -- ApplyCrossPageTerms deliberately links only terms defined elsewhere, and a page does not need a forest of links to a term it defines itself.
func MdToHTML ¶
MdToHTML converts Markdown text to the HTML a page's body carries.
It handles headings, code blocks (with tabs and annotations), inline code, paragraphs, unordered and ordered lists, links, bold, italic and tables.
metadata is the page's frontmatter. Its "auto_steps" and "auto_api" keys override the corresponding global settings from cfg.
cfg is the project config. Its "auto_detect" key -- an object with optional bool keys "steps" and "api_entries" -- controls whether the heuristics run globally; per-page metadata takes precedence. "run_button", "line_numbers" and "code_icons" configure code blocks. Either map may be nil.
func MdToHTMLPath ¶
MdToHTMLPath converts a ".md" path to a directory-index HTML path.
"guide.md" becomes "guide/index.html" (served as "/guide/"). "index.md" stays "index.html" (the root page, not "index/index.html"). Subdirectory pages follow the same rule: "api/endpoints.md" becomes "api/endpoints/index.html".
func MinifyJS ¶
MinifyJS removes comments from JavaScript and collapses its whitespace.
The approach is conservative: it does not break a URL containing "//" and it keeps a single space between identifiers so two of them cannot merge into one.
A line whose first non-whitespace characters are "//" is a comment, full stop -- no script this package ships carries a multi-line string literal, so there is nothing else it could be. It is stripped unconditionally. The quote check applies only to a "//" that follows code on the same line, where it really might be inside a string.
That distinction is not a nicety. The check used to apply to line-initial comments too, so a comment containing an apostrophe -- "the framework's combobox shape" -- was left in place, and the whitespace collapse below then pulled the FOLLOWING statement up onto the comment's line and commented it out, along with every block after it in the same assembled script. The symptom was a page whose scripts simply did not run, with no error anywhere.
func PageTitleAnchor ¶
PageTitleAnchor returns the element id the page-title H1 carries.
Part of the anchor authority: the page title is rendered as an H1 by the page chrome rather than by the body renderer, so both sides ask this function instead of slugifying the title themselves. Like every other heading, the title is slugified from its RENDERED inline form, so "# The `build` command" anchors the same way whether the words reach the page through frontmatter or through markdown.
func ParseTable ¶
ParseTable parses markdown table lines into an HTML <table>.
It expects lines like:
| Header1 | Header2 | | ------- | ------- | | Cell1 | Cell2 |
The separator line -- the one whose cells hold only "|", "-", ":" and spaces -- separates the header from the body rows, and its alignment markers produce text-align styles on the cells below. An escaped pipe in a cell is a literal pipe character.
func PathHop ¶
PathHop returns the hop that reaches path from the page rendering the reference.
A term can be defined on either side of the mount boundary, so the hop is chosen per target: prefix reaches the project's own pages and sitePrefix the site level, and under a mount those are two different roots. It is the answer for every reference that carries a bare target path and no unversioned marker: cross-page term links, breadcrumb ancestors, the glossary's source links.
func RewriteInternalLinks ¶
RewriteInternalLinks rewrites the page references bodyHTML wrote to their emitted addresses.
An author links to checks.md -- a path relative to the source file's own directory in docs/. Under directory addressing the page writing that link is emitted at "<page>/index.html", one level deeper than its source, so the sibling is reached at "../checks/". Every reference is therefore resolved back to a source path, mapped through MdToHTMLPath, and re-expressed relative to the emitted page's directory: siblings, subdirectory pages, parent pages and the root index all come out right from the one rule.
Fragments are kept through the rewrite ("checks.md#detail" becomes "../checks/#detail"); a bare "#anchor" addresses the page itself and is left as written.
legacyHTMLLinks additionally treats a relative "*.html" reference as naming the same page's Markdown source. It is set only for archive builds, whose content comes from an immutable git tag and can predate this addressing -- links there cannot be fixed at source. A build of the working tree never gets that tolerance: source under edit must name pages the way the build emits them, and a stale ".html" link there is a defect broken-emitted-reference reports.
func Slugify ¶
Slugify converts heading text to a URL-friendly slug for deep linking.
HTML tags are stripped first, then the text is NFKD-normalized so an accented character decomposes, its combining marks are dropped, the result is lower-cased, spaces become hyphens, everything that is neither a letter, a digit, an underscore nor a hyphen is removed, runs of hyphens collapse to one, and the edges are trimmed of hyphens.
Dropping only the combining marks is what preserves CJK and Cyrillic: those characters are letters, so they stay, while "Déploiement" and "Deploiement" slug the same way.
func SourceRefs ¶
SourceRefs returns every href a page built now from source, whose Markdown path is mdPath, would emit.
A reference is anything the source addresses: a Markdown link or image destination, a reference definition's target, or a raw href/src attribute written into the Markdown. Each is mapped through EmittedRef, the same rule the renderer applies to the converted body, so the answer is in the emitted spelling ("../guide/") rather than the authored one ("guide.md").
It exists so a pass over a BUILT tree can tell a reference the current source still writes from one only an older rendering wrote. Comparing an emitted href against the Markdown text directly cannot do that: the two are never spelled the same.
func TermAnchor ¶
TermAnchor returns the id a definition site carries for term.
Terms live in their own "term-" namespace so a term can never take an id a heading already owns -- heading ids come from AssignHeadingAnchors and are bare slugs.
func ThemeCSSRel ¶
ThemeCSSRel returns where this page's stylesheet sits, relative to the output root.
A framework theme's sheet is written in "css/" with "fonts/" beside it, because the framework addresses its faces at "../fonts/". Every other theme keeps "style.css" at the root. Pages that render before a theme is known -- and the tests that build one by hand -- pass nil and get the plain answer.
Types ¶
type CalloutKind ¶
type CalloutKind struct {
// Kind is the framework callout kind, as in "tm-callout-note".
Kind string
// Icon is the inline SVG the callout's title carries.
Icon string
// Role is the ARIA role the callout element takes.
Role string
}
CalloutKind is how one admonition type is painted: the framework kind it maps to, the glyph it carries, and the ARIA role it takes.
func CalloutKindFor ¶
func CalloutKindFor(admonitionType string) (CalloutKind, bool)
CalloutKindFor returns how the named admonition type is painted, and whether it is one this build recognizes.
type DeclaredTerm ¶
type DeclaredTerm struct {
// Term is the term as the author wrote it, with any markup stripped.
Term string
// Anchor is the id the definition site already carries.
Anchor string
// Definition is the definition's HTML: the <dd> of a glossary entry,
// or the text of the paragraph a standalone <dfn> stands in.
Definition string
}
DeclaredTerm is one author-declared term: the term as written, the id its definition site carries, and the definition's HTML.
func CollectDeclaredTerms ¶
func CollectDeclaredTerms(bodyHTML string) []DeclaredTerm
CollectDeclaredTerms returns every author-declared term in bodyHTML, in document order.
There is one result per definition site: a <dt><dfn> inside a glossary block, whose definition is its <dd>, or a standalone <dfn> in a paragraph, whose definition is the paragraph. The anchor is the id the definition site already carries, so a caller linking to it has a real target.
Nothing here is inferred -- a term appears only where an author wrote a <dfn>, a definition list, or the glossary directive.
type HeadingAnchor ¶
type HeadingAnchor struct {
// Index is the heading's position in the token list it came from.
Index int
// Level is the heading level, 1 through 6.
Level int
// Text is the heading's Markdown text, as written.
Text string
// Anchor is the element id the heading carries on the built page.
Anchor string
// IsPageTitle marks the first H1, which the page chrome emits as the
// page title heading instead of the body renderer emitting it.
IsPageTitle bool
}
HeadingAnchor is one heading and the element id it will carry on the built page.
A renderer walking the same tokens the anchors were assigned from looks each one up by its token index.
func AssignHeadingAnchors ¶
func AssignHeadingAnchors(tokens []tokenizer.Token, pageTitle *string) []HeadingAnchor
AssignHeadingAnchors assigns the final element id to every heading in tokens.
This is the one place heading anchors are decided. The HTML renderer emits these ids and the search index links to them, so the two cannot drift: a repeated heading gets "setup", "setup-1", "setup-2" in both. Because the input is the block token list, a "#"-prefixed line inside a fenced code block is code and never becomes an anchor.
The first H1 is not rendered in the body -- the page chrome emits it as the page title heading, whose id comes from the page title. Pass pageTitle (the frontmatter title, else the H1 text) to get that id right; it is reported with IsPageTitle true. A nil pageTitle means the H1's own text is the title.
The result is in document order.
type SiteTerm ¶
type SiteTerm struct {
// Term is the term as the author wrote it.
Term string
// Page is the html_path of the page that defines it.
Page string
// Anchor is the id of the definition site on that page.
Anchor string
// Definition is the definition's HTML.
Definition string
// GlossaryAnchor is the id of this term's entry on the synthesized
// glossary page, empty until that page has been built.
GlossaryAnchor string
}
SiteTerm is one term in the site-wide term table: where it is defined, under what id, with what definition, and -- once a glossary page has been synthesized -- the id of its entry on that page.
type SiteTerms ¶
type SiteTerms struct {
// contains filtered or unexported fields
}
SiteTerms is the site-wide term table: every term any page declared, keyed by the lower-cased term and kept in the order the terms were first seen.
The order is part of the contract, not an implementation detail: the passes that consume the table walk it in order, and a Go map's iteration order would make a built site differ between two runs over the same sources. It stands in for the insertion-ordered dict the Python surface passed around.
func (*SiteTerms) Add ¶
Add records term as defined on page, and returns the table's entry for it.
The first page to declare a term owns it: a later declaration of the same term, in any casing, is ignored and the existing entry returned.