Skip to main content

max / docengine

git clone https://makenot.work/git/max/docengine.git git clone git@ssh.makenot.work:max/docengine.git
Name Size
/ .cargo/
/ benches/
/ docs/
/ fuzz/
/ scripts/
/ src/
/ tests/
· .gitignore 439 B
· Cargo.toml 2.6 KB
· LICENSE 1.0 KB
· README.md 8.1 KB
· rust-toolchain.toml 523 B

README

DocEngine

Configurable markdown-to-HTML rendering library with sanitization presets. Built on pulldown-cmark (GFM) and ammonia.

Used by MNW (site docs, blog posts, user-generated content), Multithreaded (forum posts), and the desktop apps (descriptions, notes).

Internal: not published to crates.io. Consumed by path in-tree, and by git dependency from anything built in a container, where a cross-repo path dependency is not reachable.

Presets

Five rendering presets, each with different security/feature tradeoffs:

PresetUse caseTablesImagesRaw HTMLDangerous scheme filterSanitization
PermissiveDocs, blog posts (trusted)YYYNDefault ammonia
StandardApp text fields (descriptions)YNYNDefault ammonia
StrictUser-generated content (forums)NNNYnofollow on links
ChatChat messagesNNNYnofollow on links, inline tags only
Sanitize-onlyExternal HTML (RSS feeds)Default ammonia, no markdown parsing

Plain text

The presets all answer “how do I show this markdown”. render_plain answers “what does this markdown say”, for the places that cannot take markup at all: a list row’s preview line, a notification body, a search snippet, an alt attribute. Block structure survives as newlines; emphasis, code spans and link URLs do not. It produces text, not safe HTML, so whatever puts that text in a document still escapes it.

use docengine::{render_chat, render_permissive, render_plain, render_standard, render_strict, sanitize_html};

// Convenience functions
let html = render_permissive("# Hello\n\n**Bold** text");
let html = render_standard("A description with [link](https://example.com)");
let html = render_strict("User post with @mentions and `code`");
let html = render_chat("A chat message with a [link](https://example.com)");
let html = sanitize_html("<p>Pre-rendered</p><script>stripped</script>");

// Markdown to text, for a preview line rather than a document
let text = render_plain("See **the [docs](https://example.com)**."); // "See the docs." 

// Builder pattern for custom configurations
use docengine::{Renderer, SanitizePreset};

let html = Renderer::permissive()
    .with_strip_images(true)       // override: strip images even in permissive
    .with_footnotes(false)
    .render("# Custom config");

// Render with metadata (word count, reading time)
let result = Renderer::standard().render_with_meta("Some article text...");
println!("{} words, ~{} min read", result.word_count, result.reading_time_minutes);

Feature Flags

All optional features are off by default. Enable what you need:

FlagDependenciesProvides
doc-loaderregexDocLoader – load a directory of .md files into an in-memory page store
directivesregex-litepost_process_directives[!NOTE]/[!TIP]/[!TABS] blockquote alerts and code tabs
frontmattertomlparse_frontmatter – extract TOML frontmatter delimited by +++
mentionsregex-liteextract_mentions, resolve_mentions@username parsing and linking
quotesregex-lite, uuidpost_process_quotes – replace [quote:POST_ID:HASH] markers with author attribution
media-urlsregex-literewrite_media_paths, img_to_video – CDN path rewriting and video tag conversion
fullall of the aboveEnable everything
# In Cargo.toml
docengine = { path = "../../Libraries/docengine" }                 # From MNW/server/ or Apps/
docengine = { git = "https://makenot.work/git/max/docengine" }     # From a container build

The path form needs the ~/Code layout on disk, which a clone does not reproduce. Anything building in a container takes the git form.

Core API

Types

  • Renderer – configurable markdown renderer with builder pattern
  • RenderResult – rendered HTML plus word_count and reading_time_minutes
  • SanitizePresetPermissive, Standard, Strict, Minimal, Chat
  • TocEntry – heading level, text, and anchor for table of contents

Functions

FunctionDescription
render_permissive(md)Render with full GFM features
render_standard(md)Render without images
render_strict(md)Render with all restrictions (UGC-safe)
render_chat(md)Render a chat message: strict, and inline only
sanitize_html(html)Clean pre-rendered HTML without markdown parsing
word_count(text)Count words in raw text
reading_time_minutes(wc)Estimate reading time (200 wpm)
extract_title(md)Pull the first # Heading from markdown
strip_first_heading(md)Remove the first # Heading (for template-rendered titles)
extract_toc(md)Build a Vec<TocEntry> from all headings
render_toc_html(entries)Render TOC entries as a <nav class="toc"> HTML list

Feature-gated

Function / TypeFeatureDescription
DocLoader::load(path, config)doc-loaderLoad .md files from disk, render to HTML, build searchable index
DocPage, DocIndexEntrydoc-loaderPage and index entry types
post_process_directives(html)directivesConvert [!NOTE]/[!TIP]/etc. blockquotes to alert divs, [!TABS] to tabbed code blocks
parse_frontmatter(input)frontmatterParse +++-delimited TOML frontmatter
FrontmatterfrontmatterStruct with title, date, tags, section, draft, extra
extract_mentions(md)mentionsFind unique @username mentions (skips code blocks)
resolve_mentions(md, valid, template)mentionsReplace @user with [@user](/path/to/user) for known usernames
post_process_quotes(html, authors)quotesReplace [quote:UUID:HASH] with clickable attribution
rewrite_media_paths(md, base, user)media-urlsRewrite relative image paths to absolute CDN URLs
img_to_video(html)media-urlsConvert <img> tags pointing to video files into <video> elements

Value substitution

{{ dotted.path | filter(args) }} substitution is not a docengine feature. It lives in subst (the generic engine) and mnw-assumptions (the MNW business-model layer on top), both in the MNW tree. The seam is DocLoaderConfig::pre_process: MNW’s server builds an Assumptions at boot and hands over its substitute. Any other pre-render text transform plugs into the same hook.

Consumers

ProjectFeatures usedPreset
MNWdoc-loader, directives, frontmatter, media-urlsPermissive (docs/blog), Standard (descriptions)
Multithreadedmentions, quotesStrict (forum posts)
GoingsOncore onlyStandard (notes, descriptions)
Balanced Breakfastcore onlySanitize-only (RSS feed content)
livechatcore onlyChat (messages). Not mentions: highlighting is client-side, so @name stays plain text

Security

All presets sanitize output through ammonia. The strict preset additionally:

  • Strips all raw HTML and images at the parser level (before ammonia)
  • Replaces javascript:, data:, vbscript: URLs with #
  • Adds rel="noopener noreferrer nofollow" to all links

The chat preset is strict plus an inline-only tag allowlist, and it carries one guarantee the others do not state: rendering a message makes no outbound request. A link becomes an anchor and stays one. There is no preview fetch, no URL resolution, no metadata lookup, and no HTTP client anywhere in this crate’s dependency tree – chat_render_path_cannot_fetch asserts the last of those against the lockfile, so a fetcher cannot be added without a test failing first.

Zero unsafe code.

License

MIT. See LICENSE.

Permissive on purpose. The products carry a different license: MNW and Multithreaded are PolyForm Noncommercial, while the libraries meant for reuse are MIT. That split is what lets a GPL consumer link this crate at all, since PolyForm’s noncommercial term is an added restriction GPL section 7 forbids.