lagrange-library 0.2.3

A WASI-rendered Markdown static site facility — multilingual out of the box
// Lagrange inline markdown grammar.
//
// A pragmatic GFM-ish inline grammar parsed with pest. Block structure is
// detected line-by-line in `parser.rs` (mirroring the ratatui-markdown scheme);
// each block's text run is fed through this inline grammar, which yields a flat
// (but nestable for emphasis) sequence of spans.
//
// Emphasis/strong use *atomic* inner atoms (see `strong_atom` / `em_atom`) that
// do not recurse into arbitrary `span`s. This keeps the grammar linear — a line
// full of `**bold**` cells (common in GFM tables) cannot trigger exponential
// backtracking. The only nesting allowed is italic-around-bold
// (`*… **b** …*`), which is bounded to two levels.

inline_seq = { SOI ~ span* ~ EOI }

span = {
    badge_link
  | image
  | link
  | code_span
  | strong
  | emphasis
  | inline_html
  | escape
  | entity
  | raw
}

// A link wrapping an image — `[![alt](img-url)](link-url)`. Standard shield.io
// badge syntax. Matched before `link` so the nested `[`/`]` don't confuse the
// generic label rule (which stops at the first `]`).
badge_link = {
    "[!" ~ "[" ~ (!"]" ~ ANY)* ~ "]" ~ "(" ~ url ~ ")" ~ "]" ~ "(" ~ url ~ ")"
}

image     = { "!" ~ label ~ "(" ~ url ~ ")" }
link      = { label ~ "(" ~ url ~ ")" }
label     = { "[" ~ (!"]" ~ ANY)* ~ "]" }
url       = { (!")" ~ ANY)+ }

code_span = { "`" ~ (!"`" ~ ANY)+ ~ "`" }

// `strong`/`emphasis` close on their own delimiter; their inner atoms never
// recurse into arbitrary spans (only into non-ambiguous atoms +, for emphasis,
// one level of `strong`). Only `*`/`**` are treated as emphasis delimiters —
// `_` is left literal so identifiers like `snake_case` / `my_var` in technical
// prose are not mangled (CommonMark underscore flanking is deliberately not
// modelled).
strong      = { "**" ~ strong_atom* ~ "**" }
emphasis    = { "*" ~ em_atom* ~ "*" }
strong_atom = { code_span | image | link | badge_link | inline_html | escape | entity | raw_double_atom }
em_atom     = { code_span | image | link | badge_link | strong | inline_html | escape | entity | raw_single_atom }

// HTML character reference: ` `, ` `, ` `. Named references
// start with a letter (per HTML5); decoding happens in `build_inline`.
entity = @{ "&" ~ ("#" ~ ("x" | "X") ~ ASCII_HEX_DIGIT+ | "#" ~ ASCII_DIGIT+ | ASCII_ALPHA ~ ASCII_ALPHANUMERIC*) ~ ";" }

// Atomic runs of non-special chars (relative to the delimiter set) so the
// surrounding repetition is non-backtracking and produces few nodes. `&` is
// excluded so entity references get their own span; a bare `&` that does not
// form an entity still matches through `raw`'s single-char fallback.
raw_double_atom = @{ ( !( "**" | "`" | "[" | "\\" | "!" | "<" | "&" ) ~ ANY )+ }
raw_single_atom = @{ ( !( "**" | "*" | "`" | "[" | "\\" | "!" | "<" | "&" ) ~ ANY )+ }

escape    = { "\\" ~ ANY }

// Greedy run of non-special chars; the trailing `ANY` guarantees forward
// progress even on an unmatched delimiter (e.g. a stray `*`). Atomic so it does
// not contribute to backtracking at the span level.
raw       = @{ (!special_start ~ ANY)+ | ANY }

special_start = { "**" | "*" | "`" | "[" | "\\" | "!" | "<" | "&" }

// Inline HTML: <tag attrs>body</tag> or <self-closing/>
inline_html = { "<" ~ tag_name ~ attrs* ~ ( "/>" | ">" ~ inline_seq ~ "</" ~ tag_name ~ ">" ) }
tag_name    = @{ ASCII_ALPHA+ }
attrs       = { " " ~ (!( ">" | "/>" ) ~ ANY)+ }