moss_core/lib.rs
1//! **moss-core is the pure-Rust content engine behind [moss](https://mosspub.com),**
2//! a desktop publishing app. It owns every transformation that turns a folder of
3//! markdown into a website: parsing, wikilink resolution, HTML rendering,
4//! frontmatter typing, and schema validation.
5//!
6//! Everything is **data in, data out** — strings and structs in; parsed ASTs,
7//! diagnostics, and rendered HTML out. **Zero I/O, zero async, no global state.**
8//! The filesystem, the network, and the async runtime all live one layer up, in
9//! moss's host; this crate never touches them. That makes it deterministic,
10//! trivially unit-testable, and embeddable in any Rust program — not just moss.
11//!
12//! # How it's laid out
13//!
14//! The modules cluster into four areas, plus the contract surface:
15//!
16//! - **Parse & render** — [`ast`] turns markdown into a typed tree (over
17//! `pulldown-cmark`) and renders it back to HTML through interceptable hooks;
18//! [`render`] emits media HTML (image/video/audio/iframe/pdf) for embeds.
19//! - **Frontmatter & schema** — [`frontmatter`] parses YAML while preserving the
20//! body byte-for-byte; [`frontmatter_typed`] is the canonical `FrontMatter`
21//! struct; [`schema_fields`] is the single source of truth for built-in fields;
22//! [`validation`] produces LSP-style diagnostics against a schema.
23//! - **Links & content model** — [`resolve`] is the one place wikilinks and
24//! embeds (`[[...]]`) become ordinary markdown links; [`content_graph`] does the
25//! Obsidian-style fuzzy path matching underneath.
26//! - **Utilities** — small stateless helpers the editor and build share:
27//! [`slug`], [`date`], [`sort`], [`home`], [`page_kind`], [`heading`], and
28//! [`inert_regions`] (the one answer to "which byte ranges of this markdown
29//! are code or comment, and therefore not live syntax?", shared by every
30//! pre-parse scanner in moss).
31//!
32//! Plus [`contract`]: the design surface (W3C design tokens + the `moss-*` HTML
33//! class table) that theme authors and codegen depend on.
34//!
35//! # Getting started
36//!
37//! Every entry point is a free function — pick the module and call it:
38//!
39//! ```
40//! use moss_core::frontmatter;
41//!
42//! let raw = "---\ntitle: Hello\n---\n\nBody text";
43//! let doc = frontmatter::parse(raw);
44//! assert_eq!(doc.frontmatter.get("title").and_then(|v| v.as_str()), Some("Hello"));
45//! assert_eq!(doc.body.trim(), "Body text"); // body preserved verbatim
46//! ```
47//!
48//! From there: [`ast`] for the body tree, [`resolve`] to flatten wikilinks,
49//! [`validation`] to lint frontmatter, and [`heading`] for anchors.
50//!
51//! # Guarantees
52//!
53//! Total functions: bad input degrades to a best-effort value, never an `Err` or
54//! a panic. No `unsafe` (`#![forbid(unsafe_code)]`). Schema problems are reported
55//! out-of-band as [`validation`] diagnostics, not return values.
56//!
57//! moss ships this crate in a host built with `panic = "abort"` (release
58//! profile), so a panic on user input crashes the whole desktop app (see the
59//! `date.rs` fix for the
60//! editor-mount panic on Chinese filenames). The lint attributes below enforce
61//! the panic-free contract — `string_slice` plus `unwrap_used`/`expect_used`,
62//! all denied outside tests — each with a per-site escape-hatch rule.
63
64#![forbid(unsafe_code)]
65// `clippy::string_slice` flags `&s[..n]` byte-indexed slicing on `&str`. That
66// pattern crashed the editor on `纽约诸法门.md` — `len() < 10` is bytes, not
67// chars, so the guard let the slice cut inside `法`. Safe call sites must
68// carry a per-site `#[allow(clippy::string_slice)]` with a one-line rationale
69// (e.g. "char-aligned: pos came from `find('/')`"). Audited at PR time, not
70// "we hope no one writes the bug shape again."
71//
72// Exempted in tests via `cfg_attr(not(test), ...)`, for the same reason as the
73// `unwrap_used`/`expect_used` pair below: the contract protects PRODUCTION —
74// a panic there crashes the desktop app on user input. A test that slices past
75// a char boundary just fails, which is the outcome a test wants. Prod code is
76// at zero without a single `#[allow]`; keeping the deny unconditional would
77// only tax assertion messages like `&html[..html.len().min(300)]`.
78#![cfg_attr(not(test), deny(clippy::string_slice))]
79// `clippy::unwrap_used` / `clippy::expect_used` enforce the second half of the
80// panic-free contract: production code must never `.unwrap()` / `.expect()`
81// a value that could be `None`/`Err` at runtime. Test code (`#[cfg(test)]
82// mod tests`) is exempted via `cfg_attr(not(test), ...)` because tests
83// legitimately want to fail fast on assertion violations. Safe call sites
84// must annotate with `#[allow(clippy::unwrap_used)]` + per-site rationale,
85// same pattern as `clippy::string_slice`.
86#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
87
88pub mod ast;
89pub mod asset_paths;
90pub(crate) mod path_ext;
91pub mod asset_snapshot;
92pub mod content_graph;
93pub mod contract;
94pub mod csv_table;
95pub mod date;
96pub mod dep_graph;
97pub mod home;
98pub mod frontmatter;
99pub mod frontmatter_union;
100pub mod frontmatter_typed;
101pub mod heading;
102pub use heading::{extract_headings, HeadingInfo};
103pub mod inert_regions;
104pub mod link_candidates;
105pub mod link_completions;
106pub mod media;
107pub mod page_kind;
108pub use page_kind::PageKind;
109pub mod render;
110pub mod resolve;
111pub mod resolved;
112pub use resolved::{Resolved, ResolvedOrigin};
113pub mod schema;
114pub mod schema_fields;
115pub mod slug;
116pub mod sort;
117pub mod shortcode_tokens;
118pub mod validation;