diff --git a/python/xhtmlmd/__init__.py b/python/xhtmlmd/__init__.py
index 66af028..87b8056 100644
--- a/python/xhtmlmd/__init__.py
+++ b/python/xhtmlmd/__init__.py
@@ -3,10 +3,10 @@
__all__ = ["to_xhtml", "render", "blocks", "rewrite"]
-def to_xhtml(markdown: str, *, math: str = "brackets", tagfilter: bool = False, balance: bool = False, callbacks: dict | None = None,
+def to_xhtml(markdown: str, *, math: str = "brackets", mustache: bool = True, tagfilter: bool = False, balance: bool = False, callbacks: dict | None = None,
max_inline_depth: int | None = None, max_block_depth: int | None = None, max_link_paren_depth: int | None = None) -> str:
"Render Markdown to an XHTML fragment."
- return _to_xhtml(markdown, math=math, tagfilter=tagfilter, balance=balance, callbacks=callbacks, max_inline_depth=max_inline_depth,
+ return _to_xhtml(markdown, math=math, mustache=mustache, tagfilter=tagfilter, balance=balance, callbacks=callbacks, max_inline_depth=max_inline_depth,
max_block_depth=max_block_depth, max_link_paren_depth=max_link_paren_depth)
diff --git a/src/inline.rs b/src/inline.rs
index bb3f43d..1a771c9 100644
--- a/src/inline.rs
+++ b/src/inline.rs
@@ -193,6 +193,14 @@ fn parse_inner(src: &str, ctx: &InlineContext<'_>, depth: usize) -> Vec
let mut failed = FailedScans::default();
let mut i = 0;
while i < src.len() {
+ if ctx.options.mustache && starts(src, i, "{{") {
+ if let Some((item, next)) = mustache(src, i) {
+ scanner.flush_text();
+ scanner.push_inline(item);
+ i = next;
+ continue;
+ }
+ }
if starts(src, i, "\\[")
&& matches!(ctx.options.math, MathMode::Brackets | MathMode::Dollars)
{
@@ -420,6 +428,9 @@ fn plain_text_fast_path(src: &str, ctx: &InlineContext<'_>) -> bool {
if src.contains("==") {
return false;
}
+ if ctx.options.mustache && src.contains("{{") {
+ return false;
+ }
if ctx.options.math == MathMode::Dollars && src.contains('$') {
return false;
}
@@ -437,6 +448,24 @@ fn plain_text_fast_path(src: &str, ctx: &InlineContext<'_>) -> bool {
!can_link_or_span
}
+fn mustache(src: &str, i: usize) -> Option<(Inline, usize)> {
+ let end = src[i + 2..].find("}}")? + i + 4;
+ let body = src[i + 2..end - 2].trim_start();
+ let class = match body.chars().next() {
+ Some('#' | '^' | '/') => "mustache.section",
+ Some('!') => "mustache.comment",
+ Some('>') => "mustache.partial",
+ _ => "mustache.placeholder",
+ };
+ Some((
+ Inline::Span {
+ attrs: Attr::with_class(class),
+ children: vec![Inline::Html(src[i..end].to_string())],
+ },
+ end,
+ ))
+}
+
struct InlineScanner<'a, 'b> {
src: &'a str,
ctx: &'b InlineContext<'b>,
diff --git a/src/lib.rs b/src/lib.rs
index 6bb3fb1..c8c449a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -34,6 +34,7 @@ pub enum MathMode {
#[derive(Clone, Debug)]
pub struct Options {
pub math: MathMode,
+ pub mustache: bool,
pub tagfilter: bool,
pub balance: bool,
pub max_inline_depth: usize,
@@ -45,6 +46,7 @@ impl Default for Options {
fn default() -> Self {
Self {
math: MathMode::Brackets,
+ mustache: true,
tagfilter: false,
balance: false,
max_inline_depth: 64,
diff --git a/src/python.rs b/src/python.rs
index 4d4e672..377f553 100644
--- a/src/python.rs
+++ b/src/python.rs
@@ -14,6 +14,7 @@ use crate::{MathMode, Options};
markdown,
*,
math = "brackets",
+ mustache = true,
tagfilter = false,
balance = false,
callbacks = None,
@@ -24,6 +25,7 @@ use crate::{MathMode, Options};
fn to_xhtml(
markdown: &str,
math: &str,
+ mustache: bool,
tagfilter: bool,
balance: bool,
callbacks: Option>,
@@ -33,6 +35,7 @@ fn to_xhtml(
) -> PyResult {
let mut options = Options {
math: parse_math_mode(math)?,
+ mustache,
tagfilter,
balance,
..Options::default()
diff --git a/tests/test_python.py b/tests/test_python.py
index d627ddc..f367955 100644
--- a/tests/test_python.py
+++ b/tests/test_python.py
@@ -37,6 +37,21 @@ def test_invalid_math_mode_raises():
with pytest.raises(ValueError, match="math must be"): to_xhtml("x", math="inline")
+def test_mustache_placeholders_are_opaque_and_can_be_disabled():
+ src = "{{name.name}} {{#items}} {{/items}} {{^empty}} {{/empty}} {{! comment}} {{> partial}} {{*markdown*}}"
+ html = to_xhtml(src)
+ assert html.count('class="mustache.placeholder"') == 2
+ assert html.count('class="mustache.section"') == 4
+ assert html.count('class="mustache.comment"') == 1
+ assert html.count('class="mustache.partial"') == 1
+ assert '{{name.name}}' in html
+ assert '{{#items}}' in html
+ assert '' in html
+ assert '{{> partial}}' in html
+ assert "{{*markdown*}}" in html and "markdown" not in html
+ assert_html(to_xhtml("{{*markdown*}}", mustache=False), "{{markdown}}
")
+
+
def test_node_callback_can_override_heading():
calls = []