use quillmark_core::error::MAX_NESTING_DEPTH;
use quillmark_content::model::{Container, LineKind, Mark, MarkKind, Content, ISLAND_SLOT};
use std::ops::Range;
pub fn escape_markup(s: &str) -> String {
s.replace('\\', "\\\\")
.replace("//", "\\/\\/")
.replace('~', "\\~") .replace('*', "\\*")
.replace('_', "\\_")
.replace('`', "\\`")
.replace('#', "\\#")
.replace('[', "\\[")
.replace(']', "\\]")
.replace('{', "\\{")
.replace('}', "\\}")
.replace('$', "\\$")
.replace('<', "\\<")
.replace('>', "\\>")
.replace('@', "\\@")
}
pub fn escape_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c.is_control() => {
use std::fmt::Write as _;
let _ = write!(out, "\\u{{{:x}}}", c as u32);
}
c => out.push(c),
}
}
out
}
fn opens_line_anchor(s: &str) -> bool {
let b = s.as_bytes();
match b.first().copied() {
Some(b'=') => {
let n = b.iter().take_while(|&&c| c == b'=').count();
b.get(n) == Some(&b' ')
}
Some(b'-') | Some(b'+') | Some(b'/') => b.get(1) == Some(&b' '),
Some(c) if c.is_ascii_digit() => {
let n = b.iter().take_while(|&&c| c.is_ascii_digit()).count();
b.get(n) == Some(&b'.') && b.get(n + 1) == Some(&b' ')
}
_ => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EscapeCtx {
Markup,
StringLit,
}
fn gen_cluster(gen: &str, i: usize, ctx: EscapeCtx) -> (usize, usize) {
let rest = &gen[i..];
match ctx {
EscapeCtx::Markup => {
if rest.as_bytes().starts_with(br"\/\/") {
return (2, 4);
}
let mut chars = rest.chars();
let c = chars.next().expect("i is a char boundary within gen");
if c == '\\' {
(1, 1 + chars.next().map(char::len_utf8).unwrap_or(0))
} else {
(1, c.len_utf8())
}
}
EscapeCtx::StringLit => {
let mut chars = rest.chars();
let c = chars.next().expect("i is a char boundary within gen");
if c == '\\' {
match chars.next() {
Some('u') => (1, rest.find('}').map(|b| b + 1).unwrap_or(rest.len())),
Some(c2) => (1, 1 + c2.len_utf8()),
None => (1, 1),
}
} else {
(1, c.len_utf8())
}
}
}
}
pub(crate) fn invert_gen_offset(gen: &str, ctx: EscapeCtx, target: usize) -> usize {
let (mut byte, mut content) = (0usize, 0usize);
while byte < gen.len() {
let (cc, bl) = gen_cluster(gen, byte, ctx);
if target < byte + bl {
return content;
}
byte += bl;
content += cc;
}
content
}
pub(crate) fn forward_content_offset(gen: &str, ctx: EscapeCtx, content_off: usize) -> usize {
let (mut byte, mut content) = (0usize, 0usize);
while byte < gen.len() {
if content >= content_off {
return byte;
}
let (cc, bl) = gen_cluster(gen, byte, ctx);
content += cc;
byte += bl;
}
byte
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SegmentMap {
pub content: Range<usize>,
pub gen: Range<usize>,
pub runs: Vec<(Range<usize>, Range<usize>, EscapeCtx)>,
}
#[derive(Debug, Clone)]
pub struct Emission {
pub markup: String,
pub segments: Vec<SegmentMap>,
}
#[derive(Debug, thiserror::Error)]
pub enum EmitError {
#[error("Nesting too deep: {depth} levels (max: {max} levels)")]
NestingTooDeep {
depth: usize,
max: usize,
},
}
pub fn emit_content(rt: &Content) -> Result<Emission, EmitError> {
let max_depth = rt
.lines
.iter()
.map(|l| l.containers.len())
.max()
.unwrap_or(0);
if max_depth > MAX_NESTING_DEPTH {
return Err(EmitError::NestingTooDeep {
depth: max_depth,
max: MAX_NESTING_DEPTH,
});
}
let mut e = Emit::new(rt);
let n = rt.lines.len();
e.emit_block_level(0..n, 0);
Ok(Emission {
markup: e.out,
segments: e.segments,
})
}
pub(crate) fn emit_content_inline(rt: &Content) -> Result<Emission, EmitError> {
if !rt.is_inline() {
return emit_content(rt);
}
let mut e = Emit::new(rt);
e.emit_segment(0..rt.lines.len());
Ok(Emission {
markup: e.out,
segments: e.segments,
})
}
struct Emit<'a> {
rt: &'a Content,
chars: Vec<char>,
line_usv: Vec<(usize, usize)>,
slot_offsets: Vec<usize>,
out: String,
segments: Vec<SegmentMap>,
end_newline: bool,
}
impl<'a> Emit<'a> {
fn new(rt: &'a Content) -> Self {
let chars: Vec<char> = rt.text.chars().collect();
let line_usv: Vec<(usize, usize)> = quillmark_content::export::line_segments(rt)
.iter()
.map(|s| (s.start, s.end))
.collect();
let slot_offsets: Vec<usize> = chars
.iter()
.enumerate()
.filter_map(|(i, &c)| (c == ISLAND_SLOT).then_some(i))
.collect();
Emit {
rt,
chars,
line_usv,
slot_offsets,
out: String::new(),
segments: Vec::new(),
end_newline: true,
}
}
fn emit_block_level(&mut self, range: Range<usize>, depth: usize) {
let mut i = range.start;
while i < range.end {
if let Some(j) = self.try_emit_container(range.clone(), depth, i) {
i = j;
continue;
}
let j = self.segment_end(range.clone(), depth, i);
self.emit_leaf_terminated(i..j);
i = j;
}
}
fn try_emit_container(&mut self, range: Range<usize>, depth: usize, i: usize) -> Option<usize> {
match self.rt.lines[i].containers.get(depth).cloned() {
Some(Container::ListItem { ordered, start, .. }) => {
let j = self.list_run_end(range.clone(), depth, ordered, start, i);
self.emit_list(i..j, depth, ordered, start);
Some(j)
}
Some(Container::Quote) => {
let j = self.quote_run_end(range.clone(), depth, i);
self.emit_quote(i..j, depth);
Some(j)
}
_ => None,
}
}
fn segment_end(&self, range: Range<usize>, depth: usize, i: usize) -> usize {
let mut j = i + 1;
while j < range.end
&& self.rt.lines[j].containers.len() == depth
&& self.rt.lines[j].continues
{
j += 1;
}
j
}
fn list_run_end(
&self,
range: Range<usize>,
depth: usize,
ordered: bool,
start: u64,
i: usize,
) -> usize {
let mut j = i + 1;
while j < range.end {
match self.rt.lines[j].containers.get(depth) {
Some(Container::ListItem {
ordered: o,
start: s,
..
}) if *o == ordered && *s == start => j += 1,
_ => break,
}
}
j
}
fn quote_run_end(&self, range: Range<usize>, depth: usize, i: usize) -> usize {
let mut j = i + 1;
while j < range.end
&& matches!(
self.rt.lines[j].containers.get(depth),
Some(Container::Quote)
)
{
j += 1;
}
j
}
fn emit_leaf_terminated(&mut self, range: Range<usize>) {
let first = &self.rt.lines[range.start];
let (lo, _) = self.line_usv[range.start];
let (_, hi) = self.line_usv[range.end - 1];
if matches!(first.kind, LineKind::Para) && lo == hi {
return;
}
self.emit_segment(range);
self.out.push_str("\n\n");
self.end_newline = true;
}
fn emit_list(&mut self, range: Range<usize>, depth: usize, ordered: bool, start: u64) {
if !self.end_newline {
self.out.push('\n');
self.end_newline = true;
}
let outermost = !self.rt.lines[range.start].containers[..depth]
.iter()
.any(|c| matches!(c, Container::ListItem { .. }));
let mut k = range.start;
while k < range.end {
let key = self.rt.lines[k].containers[depth].clone();
let mut m = k + 1;
while m < range.end && self.rt.lines[m].containers.get(depth) == Some(&key) {
m += 1;
}
let ordinal = match &key {
Container::ListItem { ordinal, .. } => *ordinal,
_ => 0,
};
self.emit_item(k..m, depth, ordered, start, ordinal == 0);
k = m;
}
if outermost {
self.out.push('\n');
self.end_newline = true;
}
}
fn emit_item(
&mut self,
range: Range<usize>,
depth: usize,
ordered: bool,
start: u64,
first: bool,
) {
let indent = " ".repeat(depth);
self.out.push_str(&indent);
if ordered {
if first && start != 1 {
self.out.push_str(&format!("{}. ", start));
} else {
self.out.push_str("+ ");
}
} else {
self.out.push_str("- ");
}
self.end_newline = false;
self.emit_item_body(range, depth + 1);
if !self.end_newline {
self.out.push('\n');
self.end_newline = true;
}
}
fn emit_item_body(&mut self, range: Range<usize>, content_depth: usize) {
let cont_indent = " ".repeat(content_depth);
let mut first_block = true;
let mut i = range.start;
while i < range.end {
if let Some(j) = self.try_emit_container(range.clone(), content_depth, i) {
i = j;
first_block = false;
continue;
}
let j = self.segment_end(range.clone(), content_depth, i);
if !first_block {
self.out.push('\n');
self.out.push_str(&cont_indent);
self.end_newline = false;
}
self.emit_segment(i..j);
if !self.end_newline {
self.out.push('\n');
self.end_newline = true;
}
i = j;
first_block = false;
}
}
fn emit_quote(&mut self, range: Range<usize>, depth: usize) {
if !self.end_newline {
self.out.push('\n');
}
self.out.push_str("#quote(block: true)[\n");
self.end_newline = true;
self.emit_block_level(range, depth + 1);
self.out.push(']');
self.out.push_str("\n\n");
self.end_newline = true;
}
fn emit_segment(&mut self, range: Range<usize>) {
let kind = self.rt.lines[range.start].kind.clone();
let (lo, _) = self.line_usv[range.start];
let (_, hi) = self.line_usv[range.end - 1];
if let LineKind::Code { lang } = &kind {
self.emit_code(range, lang.as_deref(), lo, hi);
self.end_newline = false;
return;
}
let (g0, runs) = match kind {
LineKind::Heading { level } => {
self.out.push_str(&"=".repeat(level as usize));
self.out.push(' ');
let g0 = self.out.len();
(g0, self.emit_inline(lo, hi))
}
LineKind::Island | LineKind::Para => {
let g0 = self.out.len();
(g0, self.emit_inline(lo, hi))
}
LineKind::Rule => {
let g0 = self.out.len();
self.out.push_str("#line(length: 100%)");
(g0, Vec::new())
}
LineKind::Code { .. } => unreachable!("code handled by early return"),
};
let g1 = self.out.len();
self.segments.push(SegmentMap {
content: lo..hi,
gen: g0..g1,
runs,
});
self.end_newline = false;
}
fn emit_code(&mut self, range: Range<usize>, lang: Option<&str>, lo: usize, hi: usize) {
let g0 = self.out.len();
self.out.push_str("#raw(block: true");
if let Some(l) = lang {
self.out.push_str(", lang: \"");
self.out.push_str(&escape_string(l));
self.out.push('"');
}
self.out.push_str(", \"");
let mut runs = Vec::new();
for (idx, li) in range.clone().enumerate() {
if idx > 0 {
self.out.push_str(&escape_string("\n"));
}
let (l0, l1) = self.line_usv[li];
let content: String = self.chars[l0..l1].iter().collect();
let rg0 = self.out.len();
self.out.push_str(&escape_string(&content));
let rg1 = self.out.len();
runs.push((l0..l1, rg0..rg1, EscapeCtx::StringLit));
}
self.out.push_str("\")");
let g1 = self.out.len();
self.segments.push(SegmentMap {
content: lo..hi,
gen: g0..g1,
runs,
});
}
fn emit_inline(
&mut self,
lo: usize,
hi: usize,
) -> Vec<(Range<usize>, Range<usize>, EscapeCtx)> {
let mut runs = Vec::new();
let (mut wraps, codes) = wraps_and_codes(&self.rt.marks, lo, hi);
clip_wraps_to_codes(&mut wraps, &codes);
let base = self.out.len();
let at_col0 = self.out.trim_end_matches([' ', '\t']).ends_with('\n') || self.out.is_empty();
let mut buf = String::new();
sweep_marks(lo, hi, &wraps, &mut buf, |buf, pos| {
let c = self.chars[pos];
if c == '\n' {
buf.push_str("#linebreak()");
return pos + 1;
}
if c == ISLAND_SLOT {
let markup = self.island_markup(pos);
buf.push_str(&markup);
return pos + 1;
}
if let Some(&(cs, ce)) = codes.iter().find(|(s, _)| *s == pos) {
let content: String = self.chars[cs..ce].iter().collect();
buf.push_str("#raw(\"");
let rg0 = base + buf.len();
buf.push_str(&escape_string(&content));
let rg1 = base + buf.len();
buf.push_str("\")");
runs.push((cs..ce, rg0..rg1, EscapeCtx::StringLit));
return ce;
}
let re = next_boundary(pos, hi, &self.chars, &wraps, &codes);
let content: String = self.chars[pos..re].iter().collect();
if at_col0 && buf.is_empty() && opens_line_anchor(&content) {
buf.push('\\');
}
let rg0 = base + buf.len();
buf.push_str(&escape_markup(&content));
let rg1 = base + buf.len();
runs.push((pos..re, rg0..rg1, EscapeCtx::Markup));
re
});
self.out.push_str(&buf);
runs
}
fn island_markup(&self, pos: usize) -> String {
let idx = self.slot_offsets.partition_point(|&p| p < pos);
let Some(isl) = self.rt.islands.get(idx) else {
return String::new();
};
match isl.island_type.as_str() {
"image" => image_markup(&isl.props),
"table" => table_markup(&isl.props),
_ => String::new(),
}
}
}
struct Wrap {
start: usize,
end: usize,
ord: u8,
open: String,
}
fn wrap_open(kind: &MarkKind) -> String {
match kind {
MarkKind::Strong => "#strong[".to_string(),
MarkKind::Emph => "#emph[".to_string(),
MarkKind::Underline => "#underline[".to_string(),
MarkKind::Strike => "#strike[".to_string(),
_ => String::new(),
}
}
fn clip_wraps_to_codes(wraps: &mut Vec<Wrap>, codes: &[(usize, usize)]) {
for w in wraps.iter_mut() {
quillmark_content::export::clip_range_to_atomic(&mut w.start, &mut w.end, codes);
}
wraps.retain(|w| w.start < w.end);
}
fn sweep_marks(
lo: usize,
hi: usize,
wraps: &[Wrap],
out: &mut String,
mut emit_run: impl FnMut(&mut String, usize) -> usize,
) {
let mut stack: Vec<usize> = Vec::new();
let mut pos = lo;
while pos <= hi {
if let Some(idx) = stack.iter().position(|&wi| wraps[wi].end == pos) {
let mut reopen: Vec<usize> = Vec::new();
while stack.len() > idx {
let wi = stack.pop().unwrap();
out.push(']');
if wraps[wi].end != pos {
reopen.push(wi);
}
}
for wi in reopen.into_iter().rev() {
out.push_str(&wraps[wi].open);
stack.push(wi);
}
}
if pos == hi {
break;
}
let mut opening: Vec<usize> = (0..wraps.len()).filter(|&wi| wraps[wi].start == pos).collect();
opening.sort_by(|&a, &b| wraps[b].end.cmp(&wraps[a].end).then(wraps[a].ord.cmp(&wraps[b].ord)));
for wi in opening {
out.push_str(&wraps[wi].open);
stack.push(wi);
}
pos = emit_run(out, pos);
}
while stack.pop().is_some() {
out.push(']');
}
}
fn next_boundary(
pos: usize,
hi: usize,
chars: &[char],
wraps: &[Wrap],
codes: &[(usize, usize)],
) -> usize {
let mut p = pos + 1;
while p < hi {
let c = chars[p];
if c == '\n' || c == ISLAND_SLOT {
break;
}
if wraps.iter().any(|w| w.start == p || w.end == p) {
break;
}
if codes.iter().any(|(s, _)| *s == p) {
break;
}
p += 1;
}
p
}
fn wraps_and_codes(marks: &[Mark], lo: usize, hi: usize) -> (Vec<Wrap>, Vec<(usize, usize)>) {
let mut wraps = Vec::new();
let mut codes = Vec::new();
for m in marks {
if m.end <= lo || m.start >= hi {
continue;
}
let s = m.start.max(lo);
let e = m.end.min(hi);
if s >= e {
continue;
}
match &m.kind {
MarkKind::Code => codes.push((s, e)),
MarkKind::Strong | MarkKind::Emph | MarkKind::Underline | MarkKind::Strike => {
wraps.push(Wrap {
start: s,
end: e,
ord: m.kind.ord(),
open: wrap_open(&m.kind),
});
}
MarkKind::Link { url } => wraps.push(Wrap {
start: s,
end: e,
ord: m.kind.ord(),
open: format!("#link(\"{}\")[", escape_string(url)),
}),
MarkKind::Anchor { .. } | MarkKind::Unknown { .. } => {}
}
}
codes.sort_unstable();
(wraps, codes)
}
fn cell_markup(text: &str, marks: &[Mark]) -> String {
let chars: Vec<char> = text.chars().collect();
let (mut wraps, codes) = wraps_and_codes(marks, 0, chars.len());
clip_wraps_to_codes(&mut wraps, &codes);
let mut out = String::new();
sweep_marks(0, chars.len(), &wraps, &mut out, |out, pos| {
if let Some(&(cs, ce)) = codes.iter().find(|(s, _)| *s == pos) {
let content: String = chars[cs..ce].iter().collect();
out.push_str("#raw(\"");
out.push_str(&escape_string(&content));
out.push_str("\")");
return ce;
}
let re = next_boundary(pos, chars.len(), &chars, &wraps, &codes);
let content: String = chars[pos..re].iter().collect();
out.push_str(&escape_markup(&content));
re
});
out
}
fn image_markup(props: &serde_json::Value) -> String {
let url = props.get("url").and_then(|v| v.as_str()).unwrap_or("");
let alt = props.get("alt").and_then(|v| v.as_str()).unwrap_or("");
let mut out = format!("#image(\"{}\"", escape_string(url));
if !alt.trim().is_empty() {
out.push_str(&format!(", alt: \"{}\"", escape_string(alt.trim())));
}
out.push(')');
out
}
fn table_markup(props: &serde_json::Value) -> String {
let header = props.get("header").and_then(|v| v.as_array());
let rows = props.get("rows").and_then(|v| v.as_array());
let aligns = props.get("aligns").and_then(|v| v.as_array());
let mut cols = header.map(|h| h.len()).unwrap_or(0);
if let Some(rs) = rows {
for row in rs {
if let Some(r) = row.as_array() {
cols = cols.max(r.len());
}
}
}
if cols == 0 {
return String::new();
}
let cell = |v: &serde_json::Value| {
let (text, marks) = quillmark_content::serial::parse_cell(v);
cell_markup(&text, &marks)
};
let mut out = String::from("#table(\n");
out.push_str(&format!(" columns: {},\n", cols));
if let Some(al) = aligns {
if al
.iter()
.any(|a| a.as_str().map(|s| s != "none").unwrap_or(false))
{
out.push_str(" align: (");
for (i, a) in al.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(match a.as_str().unwrap_or("none") {
"left" => "left",
"center" => "center",
"right" => "right",
_ => "auto",
});
}
out.push_str("),\n");
}
}
out.push_str(" table.header(");
if let Some(h) = header {
for c in h {
out.push('[');
out.push_str(&cell(c));
out.push_str("], ");
}
}
out.push_str("),\n");
if let Some(rs) = rows {
for row in rs {
if let Some(r) = row.as_array() {
out.push_str(" ");
for c in r {
out.push('[');
out.push_str(&cell(c));
out.push_str("], ");
}
out.push('\n');
}
}
}
out.push(')');
out
}
#[cfg(test)]
mod tests {
use super::*;
use quillmark_content::import::from_markdown;
fn emit(md: &str) -> Emission {
let rt = from_markdown(md).expect("import");
assert_eq!(rt.validate(), Ok(()), "content invariants for {md:?}");
emit_content(&rt).expect("emit")
}
fn sample_inputs() -> Vec<&'static str> {
vec",
"Visit [our site](https://example.com) for more.",
"[Link](https://example.com/foo_bar)",
"[Link](#anchor)",
"[*bold* link](https://example.com)",
"[Click 🎉](https://example.com)",
"Check out https://example.com for more.",
"// This should not be a comment",
"Some text // with slashes in the middle",
"Typst uses * for bold and # for functions.",
"Use [brackets] and $math$ symbols.",
"Text {{ content }}",
"Hello~World",
"Use path/to/file for the file",
"Path: C:\\\\Users\\\\file",
"one\n\n---\n\ntwo",
"one\n\n***\n\ntwo",
"one\n\n___\n\ntwo",
"- Item 1\n- Item 2\n- Item 3",
"1. First\n2. Second\n3. Third",
"3. Three\n4. Four\n5. Five",
"0. Zero\n1. One",
"1. a\n2. b\n\ntext\n\n5. c\n6. d",
"- Item 1\n- Item 2\n - Nested item\n- Item 3",
"- Level 1\n - Level 2\n - Level 3",
"- Bullet item\n\n1. Ordered item\n2. Another ordered",
"- **Bold** item\n- _Italic_ item\n- `Code` item",
"- First line.\n\n Second line.",
"- Para 1.\n\n Para 2.\n\n Para 3.",
"- Item 1\n\n More text.\n\n- Item 2",
"1. First para.\n\n Second para.\n\n2. Next item.",
"1. First\n - Nested bullet\n - Another bullet\n2. Second",
"- Item text\n\n ```\n code here\n ```",
"- First para.\n\n ```\n code\n ```\n\n After code.",
"```rust\nfn main() {}\n```",
"```\nhello\n```",
"````\n```\nnested\n```\n````",
"```\n*bold* #heading $math$\n```",
" fn main() {}\n println!()",
"```rust#evil\ncode\n```",
"```c++\ncode\n```",
"",
"",
"",
"| Name | Age |\n|------|-----|\n| Alice | 30 |\n| Bob | 25 |",
"| A | B |\n|---|---|\n| 1 | 2 |",
"| L | C | R |\n|:---|:---:|---:|\n| a | b | c |",
"| L | D | R |\n|:---|---|---:|\n| a | b | c |",
"| Name | Value |\n|------|-------|\n",
"| A | B | C |\n|---|---|---|\n| | | |",
"| A | B | C |\n|---|---|---|\n| 1 | 2 |",
"| A | B |\n|---|---|\n| 1 | 2 | 3 |",
"| H |\n|:--|\n| x |",
"| L | C | R | D |\n|:---|:---:|---:|---|\n| a | b | c | d |",
"| A | B | C | D | E | F |\n|---|---|---|---|---|---|\n| 1 | 2 | 3 | 4 | 5 | 6 |",
"- item1\n- item2\n\n| A |\n|---|\n| 1 |",
"| Status |\n|--------|\n| ✅ Done |",
"Before.\n\n| A |\n|---|\n| 1 |\n\nAfter.",
"# Title\n\n| A |\n|---|\n| 1 |",
"| Name | Note |\n|------|------|\n| **bold** | _italic_ |",
"| Func | Desc |\n|------|------|\n| `foo()` | does stuff |",
"| Site |\n|------|\n| [Example](https://example.com) |",
"| A |\n|---|\n| ~~deleted~~ |",
"| A |\n|---|\n| **bold** and _italic_ |",
"| A |\n|---|\n| <u>underlined</u> |",
"| A |\n|---|\n| use #tag |",
"| A |\n|---|\n| a\\|b |",
"| A |\n|---|\n| $100 @ref ~space {brace} |",
"| A |\n|---|\n| a // comment |",
"| A |\n|---|\n| line1<br>line2 |",
"| Bold | Link |\n|------|------|\n| **b** | [x](https://e.com) |",
"| Mixed |\n|-------|\n| a **b** `c` [d](https://e.com) ~~e~~ |",
"A paragraph with **bold** and a [link](https://example.com).\n\nAnother paragraph with `inline code`.\n\n- A list item\n- Another item",
"# Title\n\nThis is a paragraph with **bold** and *italic* text.\n\n## Section\n\n- List item 1\n- List item 2 with [link](https://example.com)\n\nMore text with `inline code`.",
"你好世界",
"**你好** _世界_",
"Hello 🎉 World 🚀",
"مرحبا بالعالم",
]
}
#[test]
fn empty_and_blank_emit_nothing() {
for md in ["", " ", "\n\n\n", " \n \n "] {
assert_eq!(emit(md).markup, "", "for {md:?}");
}
}
#[test]
fn inline_emits_no_block_terminator() {
let rt = from_markdown("A **bold** subject").expect("import");
assert!(rt.is_inline());
let inline = emit_content_inline(&rt).expect("emit").markup;
assert_eq!(inline, "A #strong[bold] subject");
assert!(!inline.contains("\n\n"), "no parbreak in {inline:?}");
let block = emit_content(&rt).expect("emit").markup;
assert!(block.ends_with("\n\n"), "block path keeps its terminator");
}
#[test]
fn inline_falls_back_to_block_for_non_inline_content() {
for md in ["# Heading", "one\n\ntwo", "- item"] {
let rt = from_markdown(md).expect("import");
assert!(!rt.is_inline(), "{md:?} is not inline");
assert_eq!(
emit_content_inline(&rt).expect("emit").markup,
emit_content(&rt).expect("emit").markup,
"non-inline {md:?} must fall back to block lowering"
);
}
}
#[test]
fn block_quote_renders_not_flattened() {
let out = emit("> quoted text").markup;
assert!(
out.contains("#quote(block: true)["),
"quote must render, got {out:?}"
);
assert!(out.contains("quoted text"), "quote body present: {out:?}");
}
#[test]
fn thematic_break_renders_line() {
for src in ["---", "***", "___"] {
let out = emit(&format!("one\n\n{src}\n\ntwo")).markup;
assert_eq!(
out, "one\n\n#line(length: 100%)\n\ntwo\n\n",
"source: {src}, got {out:?}"
);
}
}
#[test]
fn formatted_table_cell_renders_marks() {
let md = "| Name | Note |\n|------|------|\n| **bold** | _italic_ |";
let got = emit(md).markup;
assert!(got.contains("#strong[bold]"), "got {got:?}");
assert!(got.contains("#emph[italic]"), "got {got:?}");
}
#[test]
fn escaped_pipe_table_cell_renders_literally() {
let md = "| A |\n|---|\n| a\\|b |";
let got = emit(md).markup;
assert!(got.contains("[a|b]"), "got {got:?}");
}
#[test]
fn coincident_strong_emph_nests_canonically() {
for m in ["**_x_**", "***x***", "_**x**_"] {
assert_eq!(emit(m).markup, "#strong[#emph[x]]\n\n");
}
}
#[test]
fn empty_text_link_dropped_at_import() {
assert_eq!(emit("[](https://example.com)").markup, "");
}
fn scan_reconstruct(run: &str, ctx: EscapeCtx) -> String {
let chars: Vec<char> = run.chars().collect();
let mut out = String::new();
let mut i = 0;
while i < chars.len() {
if ctx == EscapeCtx::Markup && chars[i] == '/' && chars.get(i + 1) == Some(&'/') {
out.push_str("\\/\\/");
i += 2;
continue;
}
let s: String = chars[i].to_string();
out.push_str(&match ctx {
EscapeCtx::Markup => escape_markup(&s),
EscapeCtx::StringLit => escape_string(&s),
});
i += 1;
}
out
}
#[test]
fn escape_tripwire_markup() {
let samples = [
"plain text",
"https://example.com",
"a // b // c",
"///",
"Use * for _bold_ and # for @refs",
"{braces} [brackets] $math$ <tags>",
"C:\\Users\\file",
"tilde~and`backtick`",
"你好 🎉 mixed",
"file://path//to//x",
];
for s in samples {
assert_eq!(
scan_reconstruct(s, EscapeCtx::Markup),
escape_markup(s),
"markup scan != escape_markup for {s:?}"
);
}
}
#[test]
fn escape_tripwire_string_lit() {
let samples = [
"plain text",
"https://example.com",
"a // b",
"say \"hi\"\nnow\ttab",
"C:\\Users\\file",
"control\x07bell",
"你好 🎉 mixed",
"trailing//",
];
for s in samples {
assert_eq!(
scan_reconstruct(s, EscapeCtx::StringLit),
escape_string(s),
"string scan != escape_string for {s:?}"
);
}
}
fn cluster_starts(run: &str, ctx: EscapeCtx) -> (String, Vec<(usize, usize)>) {
let chars: Vec<char> = run.chars().collect();
let mut gen = String::new();
let mut starts = Vec::new();
let mut i = 0;
while i < chars.len() {
let byte_start = gen.len();
if ctx == EscapeCtx::Markup && chars[i] == '/' && chars.get(i + 1) == Some(&'/') {
starts.push((i, byte_start)); gen.push_str("\\/\\/");
i += 2;
continue;
}
starts.push((i, byte_start));
let s: String = chars[i].to_string();
gen.push_str(&match ctx {
EscapeCtx::Markup => escape_markup(&s),
EscapeCtx::StringLit => escape_string(&s),
});
i += 1;
}
(gen, starts)
}
#[test]
fn run_offset_inversion_is_cluster_exact() {
let cases = [
(EscapeCtx::Markup, "plain"),
(EscapeCtx::Markup, "a*b_c#d"),
(EscapeCtx::Markup, "http://a//b"),
(EscapeCtx::Markup, "///x"),
(EscapeCtx::Markup, "你好*x*🎉"),
(EscapeCtx::Markup, "C:\\x"),
(EscapeCtx::StringLit, "fn add(a, b)"),
(EscapeCtx::StringLit, "a\"b\\c"),
(EscapeCtx::StringLit, "tab\there"),
(EscapeCtx::StringLit, "bell\x07end"),
(EscapeCtx::StringLit, "a//b"),
];
for (ctx, run) in cases {
let (gen, starts) = cluster_starts(run, ctx);
for w in 0..starts.len() {
let (content_i, byte_start) = starts[w];
let byte_end = starts.get(w + 1).map(|s| s.1).unwrap_or(gen.len());
for b in byte_start..byte_end {
assert_eq!(
invert_gen_offset(&gen, ctx, b),
content_i,
"byte {b} of {run:?} ({ctx:?}) floors to cluster {content_i}"
);
}
assert_eq!(
forward_content_offset(&gen, ctx, content_i),
byte_start,
"content {content_i} of {run:?} ({ctx:?}) → byte {byte_start}"
);
}
let content_len: usize = run.chars().count();
assert_eq!(invert_gen_offset(&gen, ctx, gen.len()), content_len);
assert_eq!(forward_content_offset(&gen, ctx, content_len), gen.len());
}
}
#[test]
fn runs_map_content_to_generated_bytes() {
for md in sample_inputs() {
let rt = from_markdown(md).unwrap();
let ec = emit_content(&rt).unwrap();
let chars: Vec<char> = rt.text.chars().collect();
for seg in &ec.segments {
assert!(seg.gen.start <= seg.gen.end && seg.gen.end <= ec.markup.len());
for (content, gen, ctx) in &seg.runs {
let src: String = chars[content.clone()].iter().collect();
let expect = match ctx {
EscapeCtx::Markup => escape_markup(&src),
EscapeCtx::StringLit => escape_string(&src),
};
assert_eq!(
&ec.markup[gen.clone()],
expect,
"run bytes mismatch in {md:?}"
);
assert!(gen.start >= seg.gen.start && gen.end <= seg.gen.end);
}
}
}
}
#[test]
fn overlapping_marks_close_and_reopen() {
use quillmark_content::model::{Line, Mark};
let mut rt = Content {
text: "abcdef".to_string(),
lines: vec![Line {
kind: LineKind::Para,
containers: vec![],
continues: false,
}],
marks: vec![
Mark {
start: 0,
end: 4,
kind: MarkKind::Strong,
},
Mark {
start: 2,
end: 6,
kind: MarkKind::Emph,
},
],
islands: vec![],
};
rt.normalize();
assert_eq!(rt.validate(), Ok(()));
let out = emit_content(&rt).unwrap().markup;
assert_eq!(out, "#strong[ab#emph[cd]]#emph[ef]\n\n");
assert_eq!(out.matches('[').count(), out.matches(']').count());
}
fn emit_marked(text: &str, marks: Vec<Mark>) -> String {
use quillmark_content::model::Line;
let mut rt = Content {
text: text.to_string(),
lines: vec![Line {
kind: LineKind::Para,
containers: vec![],
continues: false,
}],
marks,
islands: vec![],
};
rt.normalize();
assert_eq!(rt.validate(), Ok(()), "content invariants");
emit_content(&rt).unwrap().markup
}
fn balanced(out: &str) -> bool {
out.matches('[').count() == out.matches(']').count()
}
use quillmark_content::model::Mark;
#[test]
fn wrap_over_atomic_code_stays_balanced() {
let out = emit_marked(
"abcdef",
vec![
Mark {
start: 0,
end: 4,
kind: MarkKind::Strong,
},
Mark {
start: 2,
end: 6,
kind: MarkKind::Code,
},
],
);
assert_eq!(out, "#strong[ab]#raw(\"cdef\")\n\n");
assert!(balanced(&out));
let out = emit_marked(
"abcdef",
vec![
Mark {
start: 0,
end: 4,
kind: MarkKind::Code,
},
Mark {
start: 2,
end: 6,
kind: MarkKind::Strong,
},
],
);
assert_eq!(out, "#raw(\"abcd\")#strong[ef]\n\n");
assert!(balanced(&out));
let out = emit_marked(
"abcdef",
vec![
Mark {
start: 0,
end: 6,
kind: MarkKind::Strong,
},
Mark {
start: 2,
end: 4,
kind: MarkKind::Code,
},
],
);
assert_eq!(out, "#strong[ab#raw(\"cd\")ef]\n\n");
assert!(balanced(&out));
}
#[test]
fn wrap_trailing_to_end_closes() {
let out = emit_marked(
"abcdef",
vec![Mark {
start: 0,
end: 6,
kind: MarkKind::Strong,
}],
);
assert_eq!(out, "#strong[abcdef]\n\n");
assert!(balanced(&out));
}
#[test]
fn overlapping_links_reopen_with_correct_url() {
let out = emit_marked(
"abcdef",
vec![
Mark {
start: 0,
end: 6,
kind: MarkKind::Link {
url: "wrong".into(),
},
},
Mark {
start: 0,
end: 4,
kind: MarkKind::Strong,
},
Mark {
start: 2,
end: 6,
kind: MarkKind::Link {
url: "right".into(),
},
},
],
);
assert!(
out.contains("#link(\"right\")[ef]"),
"reopened link keeps its own URL, got {out:?}"
);
assert!(
!out.contains("#link(\"wrong\")[ef]"),
"reopen must not borrow the outer link's URL, got {out:?}"
);
assert!(balanced(&out));
}
#[test]
fn table_columns_span_widest_row() {
let cell = |t: &str| serde_json::json!({ "text": t, "marks": [] });
let props = serde_json::json!({
"header": [ cell("H") ],
"rows": [ [ cell("a"), cell("b"), cell("c") ] ],
});
let out = table_markup(&props);
assert!(out.contains("columns: 3,"), "got {out:?}");
let props = serde_json::json!({
"header": [],
"rows": [ [ cell("a"), cell("b") ] ],
});
let out = table_markup(&props);
assert!(out.contains("columns: 2,"), "got {out:?}");
}
#[test]
fn empty_table_emits_nothing() {
let props = serde_json::json!({ "header": [], "aligns": [], "rows": [] });
assert_eq!(table_markup(&props), "");
}
#[test]
fn segment_shape() {
assert_eq!(
emit("a \nb \nc").segments.len(),
1,
"hard-break paragraph"
);
assert_eq!(emit("```\nl1\nl2\nl3\n```").segments.len(), 1, "code fence");
assert_eq!(emit("- a\n- b\n- c").segments.len(), 3);
}
#[test]
fn line_anchor_predicate() {
for yes in ["= h", "== h", "- b", "+ n", "/ term: d", "1. i", "42. i"] {
assert!(opens_line_anchor(yes), "{yes:?} should trigger");
}
for no in [
"=foo", "==foo", "-5 degrees", "and/or", "1.item", "1) i", "hi", "", " = x",
] {
assert!(!opens_line_anchor(no), "{no:?} should not trigger");
}
}
#[test]
fn line_anchor_paragraph_prefixes_backslash() {
assert_eq!(emit_marked("= x", vec![]), "\\= x\n\n");
assert_eq!(emit_marked("- x", vec![]), "\\- x\n\n");
assert_eq!(emit_marked("+ x", vec![]), "\\+ x\n\n");
assert_eq!(emit_marked("/ t: d", vec![]), "\\/ t: d\n\n");
assert_eq!(emit_marked("1. x", vec![]), "\\1. x\n\n");
assert_eq!(emit_marked("hello = x", vec![]), "hello = x\n\n");
assert_eq!(emit_marked("=x no space", vec![]), "=x no space\n\n");
assert_eq!(
emit_marked(
"= x",
vec![Mark { start: 0, end: 3, kind: MarkKind::Strong }],
),
"#strong[= x]\n\n",
);
use quillmark_content::model::Line;
let mut rt = Content {
text: "= x".to_string(),
lines: vec![Line { kind: LineKind::Para, containers: vec![], continues: false }],
marks: vec![],
islands: vec![],
};
rt.normalize();
let ec = emit_content(&rt).unwrap();
let chars: Vec<char> = rt.text.chars().collect();
for seg in &ec.segments {
for (content, gen, ctx) in &seg.runs {
let src: String = chars[content.clone()].iter().collect();
let expect = match ctx {
EscapeCtx::Markup => escape_markup(&src),
EscapeCtx::StringLit => escape_string(&src),
};
assert_eq!(&ec.markup[gen.clone()], expect, "run slices to its escape");
}
}
}
}