use alloc::collections::BTreeMap;
use alloc::format;
use alloc::rc::Rc;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use crate::ed_core::TextPos;
use crate::i18n::tr;
use crate::md_dom::{Dom, MdNode, NIL};
use crate::rich_doc::*;
use crate::rich_html::{attrs, dom_para, dom_read, entity, html_read, HExt, HRead, Sheet};
pub const MB_PARA: u8 = 0;
pub const MB_HEADING: u8 = 1;
pub const MB_CODE: u8 = 2;
pub const MB_RULE: u8 = 3;
pub const MB_ROW: u8 = 4;
pub const MS_BOLD: u16 = 1;
pub const MS_ITALIC: u16 = 2;
pub const MS_STRIKE: u16 = 4;
pub const MS_CODE: u16 = 8;
pub const MS_UNDER: u16 = 16;
pub const MK_NONE: u8 = 0;
pub const MK_BULLET: u8 = 1;
pub const MK_NUMBER: u8 = 2;
pub const MA_NONE: u8 = 0;
pub const MA_LEFT: u8 = 1;
pub const MA_CENTER: u8 = 2;
pub const MA_RIGHT: u8 = 3;
pub const MF_TEXT: u8 = 0;
pub const MF_PASSWORD: u8 = 1;
pub const MF_CHECKBOX: u8 = 2;
pub const MF_RADIO: u8 = 3;
pub const MF_HIDDEN: u8 = 4;
pub const MF_TEXTAREA: u8 = 5;
pub const MF_SELECT: u8 = 6;
pub const MF_SUBMIT: u8 = 7;
pub const MF_RESET: u8 = 8;
pub const MF_BUTTON: u8 = 9;
const INPUT_MAX: usize = 16 << 20; const NEST_MAX: usize = 32; const BLOCKS_MAX: usize = 1 << 20;
const LABEL_MAX: usize = 999;
const PARENS_MAX: usize = 32;
const HTML_STACK: usize = 64;
const COLS_MAX: usize = 128;
const CELLS_MAX: usize = 1 << 21;
const FIELDS_MAX: usize = 0xFFFE;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MdSpan {
pub a: u32,
pub b: u32,
pub style: u16,
pub link: u32,
pub image: u32,
pub color: u32,
pub bg: u32,
pub field: u32,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct MdBlock {
pub kind: u8,
pub level: u8,
pub text: String,
pub spans: Vec<MdSpan>,
pub quote: u8,
pub list: u8,
pub path: u32,
pub marker: u8,
pub number: u32,
pub delim: u8,
pub task: u8,
pub space: u8,
pub lang: String,
pub cells: Vec<(u32, u32)>,
pub table: u32,
pub head: bool,
pub align: u8,
pub node: MdNode,
pub boxes: Vec<u32>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MdBox {
pub margin: [i16; 4],
pub padding: [i16; 4],
pub border: [u8; 4],
pub border_color: [u32; 4],
pub bg: u32,
pub mset: u8,
pub depth: u8,
pub node: MdNode,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MdImage {
pub src: String,
pub alt: String,
pub title: String,
pub w: u32,
pub h: u32,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MdTable {
pub aligns: Vec<u8>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MdLinkSpot {
pub block: usize,
pub a: u32,
pub b: u32,
pub link: u32,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MdForm {
pub action: String,
pub method: String,
pub id: String,
pub name: String,
pub node: MdNode,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MdOption {
pub value: String,
pub label: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MdField {
pub kind: u8,
pub form: u32,
pub name: String,
pub id: String,
pub value: String,
pub label: String,
pub placeholder: String,
pub checked: bool,
pub options: Vec<MdOption>,
pub selected: i32,
pub size: u32,
pub rows: u32,
pub maxlength: u32,
pub disabled: bool,
pub readonly: bool,
pub node: MdNode,
}
impl MdField {
pub fn caption(&self) -> &str {
match self.kind {
MF_SUBMIT | MF_RESET | MF_BUTTON if !self.label.is_empty() => &self.label,
MF_SUBMIT => tr("Senden"),
MF_RESET => tr("Zurücksetzen"),
_ => "",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MdFieldSpot {
pub block: usize,
pub a: u32,
pub field: u32,
}
#[derive(Clone, Debug, Default)]
pub struct MdDoc {
blocks: Vec<MdBlock>,
links: Vec<String>,
images: Vec<MdImage>,
tables: Vec<MdTable>,
spots: Vec<MdLinkSpot>,
forms: Vec<MdForm>,
fields: Vec<MdField>,
fspots: Vec<MdFieldSpot>,
truncated: bool,
boxes: Vec<MdBox>,
box_ends: Vec<(u32, u32)>,
pub(crate) dom: Dom,
sheet: Rc<Sheet>,
linked: BTreeMap<String, String>,
cut: bool,
change: Option<DocChange>,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct DocChange {
pub range: Option<(usize, usize, usize)>,
pub redo: Vec<usize>,
pub full: bool,
pub images: Vec<MdImage>,
pub spots: Vec<MdLinkSpot>,
}
impl DocChange {
fn then(self, n: DocChange) -> DocChange {
let range = match (self.range, n.range) {
(Some((a1, o1, n1)), Some((a2, o2, n2))) => {
let lo = a1.min(a2);
let hi = (a1 + n1).max(a2 + o2);
Some((lo, hi - n1 + o1 - lo, hi - o2 + n2 - lo))
}
(r, None) | (None, r) => r,
};
let mut redo: Vec<usize> = Vec::new();
for &x in &self.redo {
match n.range {
Some((a, o, nn)) if x >= a + o => redo.push(x - o + nn),
Some((a, _, _)) if x >= a => {}
_ => redo.push(x),
}
}
redo.extend(n.redo.iter().copied());
if let Some((a, _, nn)) = range {
redo.retain(|&x| x < a || x >= a + nn);
}
redo.sort_unstable();
redo.dedup();
let full = self.full || n.full;
let (images, spots) = if self.full { (self.images, self.spots) } else { (n.images, n.spots) };
DocChange { range, redo, full, images, spots }
}
}
fn is_punct(c: char) -> bool {
c.is_ascii_punctuation() || (!c.is_alphanumeric() && !c.is_whitespace() && !c.is_control() && c as u32 > 127)
}
fn blank(s: &str) -> bool {
s.bytes().all(|b| b == b' ' || b == b'\t')
}
fn indent_of(s: &str) -> usize {
s.bytes().take_while(|&b| b == b' ').count()
}
fn expand_tabs(s: &str) -> String {
if !s.contains('\t') {
return String::from(s);
}
let mut out = String::with_capacity(s.len() + 8);
let mut col = 0;
for c in s.chars() {
if c == '\t' {
let n = 4 - col % 4;
for _ in 0..n {
out.push(' ');
}
col += n;
} else {
out.push(c);
col += 1;
}
}
out
}
fn norm_label(s: &str) -> String {
let mut out = String::new();
let mut sp = false;
for c in s.trim().chars() {
if c.is_whitespace() {
sp = true;
continue;
}
if sp && !out.is_empty() {
out.push(' ');
}
sp = false;
for l in c.to_lowercase() {
out.push(l);
}
}
out
}
fn unescape(s: &str) -> String {
let b = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < b.len() {
if b[i] == b'\\' && i + 1 < b.len() && b[i + 1].is_ascii_punctuation() {
out.push(b[i + 1] as char);
i += 2;
continue;
}
if b[i] == b'&' {
if let Some((c, n)) = entity_at(&s[i..]) {
out.push(c);
i += n;
continue;
}
}
let c = s[i..].chars().next().unwrap_or(' ');
out.push(c);
i += c.len_utf8();
}
out
}
fn entity_at(s: &str) -> Option<(char, usize)> {
let b = s.as_bytes();
let end = b.iter().take(34).position(|&c| c == b';')?;
if end < 2 {
return None;
}
let name = &s[1..end];
if !name.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'#') {
return None;
}
entity(name).map(|c| (c, end + 1))
}
#[derive(Clone)]
struct Cont {
quote: bool,
indent: usize,
ordered: bool,
number: u32,
delim: u8,
task: u8,
filed: bool,
empty: bool,
}
enum Leaf {
None,
Para(Vec<String>),
Fence { ch: u8, len: usize, indent: usize, info: String, lines: Vec<String> },
Code(Vec<String>),
Table { aligns: Vec<u8>, rows: Vec<(Vec<String>, bool)> },
Html(Vec<String>),
}
#[derive(Default)]
struct Raw {
kind: u8,
level: u8,
src: String,
cells: Vec<String>,
html: bool,
quote: u8,
list: u8,
path: u32,
marker: u8,
number: u32,
delim: u8,
task: u8,
space: u8,
lang: String,
table: u32,
head: bool,
}
struct BlockParser {
conts: Vec<Cont>,
leaf: Leaf,
raws: Vec<Raw>,
defs: BTreeMap<String, (String, String)>,
tables: Vec<MdTable>,
blank: bool,
truncated: bool,
table_cells: usize,
}
fn list_marker(s: &str) -> Option<(bool, u8, u32, usize)> {
let b = s.as_bytes();
let sp = indent_of(s);
if sp > 3 || sp >= b.len() {
return None;
}
let c = b[sp];
let after = |k: usize| k >= b.len() || b[k] == b' ';
if matches!(c, b'-' | b'+' | b'*') {
return after(sp + 1).then_some((false, c, 0, sp + 1));
}
let digits = b[sp..].iter().take_while(|c| c.is_ascii_digit()).count();
if (1..=9).contains(&digits) {
let k = sp + digits;
if k < b.len() && (b[k] == b'.' || b[k] == b')') && after(k + 1) {
let n = s[sp..k].parse::<u32>().unwrap_or(0);
return Some((true, b[k], n, k + 1));
}
}
None
}
fn is_rule(s: &str) -> bool {
if indent_of(s) > 3 {
return false;
}
let t = s.trim();
let Some(c) = t.bytes().next() else { return false };
if !matches!(c, b'*' | b'-' | b'_') {
return false;
}
let mut n = 0;
for b in t.bytes() {
if b == c {
n += 1;
} else if b != b' ' {
return false;
}
}
n >= 3
}
fn atx(s: &str) -> Option<(u8, &str)> {
if indent_of(s) > 3 {
return None;
}
let t = s.trim_start();
let n = t.bytes().take_while(|&b| b == b'#').count();
if !(1..=6).contains(&n) {
return None;
}
let rest = &t[n..];
if !rest.is_empty() && !rest.starts_with(' ') {
return None;
}
let mut c = rest.trim();
let k = c.trim_end_matches('#');
if k.is_empty() || k.ends_with(' ') {
c = k.trim_end();
}
Some((n as u8, c))
}
fn fence_open(s: &str) -> Option<(u8, usize, String)> {
let sp = indent_of(s);
if sp > 3 {
return None;
}
let t = &s[sp..];
let c = *t.as_bytes().first()?;
if c != b'`' && c != b'~' {
return None;
}
let n = t.bytes().take_while(|&b| b == c).count();
if n < 3 {
return None;
}
let info = t[n..].trim();
if c == b'`' && info.contains('`') {
return None;
}
Some((c, n, unescape(info)))
}
fn split_cells(s: &str) -> Vec<String> {
let mut t = s.trim();
if let Some(r) = t.strip_prefix('|') {
t = r;
}
if t.ends_with('|') && !t.ends_with("\\|") {
t = &t[..t.len() - 1];
}
let b = t.as_bytes();
let mut out = Vec::new();
let (mut start, mut i) = (0, 0);
while i < b.len() {
if b[i] == b'\\' && i + 1 < b.len() {
i += 2;
continue;
}
if b[i] == b'|' {
out.push(t[start..i].trim().replace("\\|", "|"));
start = i + 1;
if out.len() >= COLS_MAX {
return out;
}
}
i += 1;
}
out.push(t[start..].trim().replace("\\|", "|"));
out
}
fn delim_row(s: &str) -> Option<Vec<u8>> {
if indent_of(s) > 3 || !s.contains('-') {
return None;
}
let t = s.trim();
if !t.contains('|') && !t.starts_with(':') && !t.starts_with('-') {
return None;
}
let cells = split_cells(s);
let mut v = Vec::new();
for c in cells {
let l = c.starts_with(':');
let r = c.ends_with(':');
let core = c.trim_matches(':');
if core.is_empty() || !core.bytes().all(|b| b == b'-') {
return None;
}
v.push(match (l, r) {
(true, true) => MA_CENTER,
(true, false) => MA_LEFT,
(false, true) => MA_RIGHT,
_ => MA_NONE,
});
}
if v.len() == 1 && !t.contains('|') {
return None;
}
Some(v)
}
fn html_start(s: &str) -> bool {
if indent_of(s) > 3 {
return false;
}
let t = s.trim_start();
if t.starts_with("<!--") {
return true;
}
let Some(r) = t.strip_prefix('<') else { return false };
let r = r.strip_prefix('/').unwrap_or(r);
let n = r.bytes().take_while(|b| b.is_ascii_alphanumeric()).count();
if n == 0 {
return false;
}
let name = r[..n].to_ascii_lowercase();
let end = r.as_bytes().get(n).copied();
if !matches!(end, None | Some(b' ') | Some(b'>') | Some(b'/')) {
return false;
}
matches!(
name.as_str(),
"address" | "article" | "aside" | "blockquote" | "center" | "dd" | "details" | "dialog" | "div" | "dl" | "dt" | "fieldset"
| "figcaption" | "figure" | "footer" | "form" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "header" | "hr" | "legend" | "li"
| "main" | "nav" | "ol" | "p" | "pre" | "section" | "summary" | "table" | "tbody" | "td" | "tfoot" | "th" | "thead" | "tr"
| "ul" | "img" | "picture"
)
}
fn take_defs(src: &str, defs: &mut BTreeMap<String, (String, String)>) -> String {
let mut rest = src;
while rest.starts_with('[') {
let Some((label, dest, title, after)) = parse_def(rest) else { break };
let key = norm_label(&label);
if !key.is_empty() {
defs.entry(key).or_insert((dest, title));
}
rest = after;
}
String::from(rest)
}
fn parse_def(s: &str) -> Option<(String, String, String, &str)> {
let b = s.as_bytes();
let mut i = 1;
while i < b.len() && b[i] != b']' {
if b[i] == b'[' || i > LABEL_MAX {
return None;
}
if b[i] == b'\\' {
i += 1;
}
i += 1;
}
if i >= b.len() || b.get(i + 1) != Some(&b':') {
return None;
}
let label = &s[1..i];
if label.trim().is_empty() {
return None;
}
let mut k = i + 2;
let ws = |k: &mut usize, nl: bool| {
let mut seen = false;
while *k < b.len() && (b[*k] == b' ' || (nl && !seen && b[*k] == b'\n')) {
if b[*k] == b'\n' {
seen = true;
}
*k += 1;
}
};
ws(&mut k, true);
let (dest, mut k) = parse_dest(s, k)?;
let line_end = |k: usize| -> Option<usize> {
let mut j = k;
while j < b.len() && b[j] == b' ' {
j += 1;
}
(j >= b.len() || b[j] == b'\n').then_some((j + 1).min(b.len()))
};
let before = k;
let mut t = k;
ws(&mut t, true);
if t > before && t < b.len() && matches!(b[t], b'"' | b'\'' | b'(') {
if let Some((title, e)) = parse_title(s, t) {
if let Some(end) = line_end(e) {
return Some((String::from(label), dest, title, &s[end..]));
}
}
}
k = line_end(k)?;
Some((String::from(label), dest, String::new(), &s[k..]))
}
fn parse_dest(s: &str, k: usize) -> Option<(String, usize)> {
let b = s.as_bytes();
if k < b.len() && b[k] == b'<' {
let mut j = k + 1;
while j < b.len() && b[j] != b'>' {
if b[j] == b'\n' || b[j] == b'<' {
return None;
}
if b[j] == b'\\' {
j += 1;
}
j += 1;
}
if j >= b.len() {
return None;
}
return Some((unescape(&s[k + 1..j]), j + 1));
}
let mut j = k;
let mut depth = 0usize;
while j < b.len() {
let c = b[j];
if c == b'\\' && j + 1 < b.len() && b[j + 1].is_ascii_punctuation() {
j += 2;
continue;
}
if c <= b' ' {
break;
}
if c == b'(' {
depth += 1;
if depth > PARENS_MAX {
return None;
}
} else if c == b')' {
if depth == 0 {
break;
}
depth -= 1;
}
j += 1;
}
if depth != 0 || j == k {
return None;
}
Some((unescape(&s[k..j]), j))
}
fn parse_title(s: &str, k: usize) -> Option<(String, usize)> {
let b = s.as_bytes();
let close = match b.get(k)? {
b'"' => b'"',
b'\'' => b'\'',
b'(' => b')',
_ => return None,
};
let mut j = k + 1;
while j < b.len() && b[j] != close {
if b[j] == b'\\' {
j += 1;
}
j += 1;
}
if j >= b.len() {
return None;
}
Some((unescape(&s[k + 1..j]), j + 1))
}
impl BlockParser {
fn new() -> BlockParser {
BlockParser { conts: Vec::new(), leaf: Leaf::None, raws: Vec::new(), defs: BTreeMap::new(), tables: Vec::new(), blank: false, truncated: false, table_cells: 0 }
}
fn file(&mut self, mut r: Raw) {
if self.raws.len() >= BLOCKS_MAX {
self.truncated = true;
return;
}
r.quote = self.conts.iter().filter(|c| c.quote).count() as u8;
r.list = self.conts.iter().filter(|c| !c.quote).count() as u8;
r.path = self.conts.iter().enumerate().filter(|(_, c)| c.quote).fold(0, |p, (i, _)| p | 1u32 << (i & 31));
let tight = r.list > 0 && self.raws.last().is_some_and(|p| p.list > 0) && !self.blank;
r.space = if self.raws.is_empty() { 0 } else if tight { 1 } else { 2 };
if let Some(c) = self.conts.iter_mut().rev().find(|c| !c.quote) {
if !c.filed {
c.filed = true;
r.marker = if c.ordered { MK_NUMBER } else { MK_BULLET };
r.number = c.number;
r.delim = c.delim;
r.task = c.task;
}
}
self.blank = false;
self.raws.push(r);
}
fn close_leaf(&mut self) {
match core::mem::replace(&mut self.leaf, Leaf::None) {
Leaf::None => {}
Leaf::Para(lines) => {
let mut src = lines.join("\n");
let t = src.trim_end().len();
src.truncate(t);
let rest = take_defs(&src, &mut self.defs);
if !rest.trim().is_empty() {
self.file(Raw { kind: MB_PARA, src: rest, ..Raw::default() });
}
}
Leaf::Fence { info, lines, .. } => {
let lang = String::from(info.split_whitespace().next().unwrap_or(""));
self.file(Raw { kind: MB_CODE, src: lines.join("\n"), lang, ..Raw::default() });
}
Leaf::Code(mut lines) => {
while lines.last().is_some_and(|l| blank(l)) {
lines.pop();
}
if !lines.is_empty() {
self.file(Raw { kind: MB_CODE, src: lines.join("\n"), ..Raw::default() });
}
}
Leaf::Table { aligns, rows } => {
let t = self.tables.len() as u32;
let n = aligns.len();
self.tables.push(MdTable { aligns });
for (ri, (mut cells, head)) in rows.into_iter().enumerate() {
cells.resize(n, String::new());
self.file(Raw { kind: MB_ROW, cells, table: t, head, ..Raw::default() });
if ri > 0 {
if let Some(x) = self.raws.last_mut() {
x.space = 0;
}
}
}
}
Leaf::Html(lines) => {
self.file(Raw { kind: MB_PARA, src: lines.join("\n"), html: true, ..Raw::default() });
}
}
}
fn close_conts(&mut self, n: usize) {
if self.conts.len() <= n {
return;
}
self.close_leaf();
while self.conts.len() > n {
let last = self.conts.len() - 1;
if !self.conts[last].quote && !self.conts[last].filed {
self.file(Raw { kind: MB_PARA, ..Raw::default() });
}
self.conts.pop();
}
}
fn line(&mut self, raw: &str) {
let ln = expand_tabs(raw);
let mut p = 0usize;
let mut matched = 0;
for k in 0..self.conts.len() {
let rest = &ln[p..];
let c = &self.conts[k];
if c.quote {
let sp = indent_of(rest);
if sp <= 3 && rest.as_bytes().get(sp) == Some(&b'>') {
p += sp + 1;
if ln.as_bytes().get(p) == Some(&b' ') {
p += 1;
}
matched += 1;
continue;
}
break;
}
if blank(rest) {
if c.empty && !c.filed {
break; }
matched += 1;
continue;
}
if indent_of(rest) >= c.indent {
p += c.indent;
matched += 1;
continue;
}
break;
}
let rest = String::from(&ln[p..]);
if let Leaf::Fence { ch, len, indent, lines, .. } = &mut self.leaf {
if matched == self.conts.len() {
let sp = indent_of(&rest);
let t = &rest[sp..];
let n = t.bytes().take_while(|&b| b == *ch).count();
if sp <= 3 && n >= *len && t[n..].trim().is_empty() {
self.close_leaf();
return;
}
let cut = sp.min(*indent);
lines.push(String::from(&rest[cut..]));
return;
}
}
if matched < self.conts.len() && matches!(self.leaf, Leaf::Para(_)) && !blank(&rest) && !Self::starts_block(&rest, true) {
if let Leaf::Para(lines) = &mut self.leaf {
lines.push(String::from(rest.trim_start()));
}
return;
}
self.close_conts(matched);
let mut rest = rest;
loop {
if self.conts.len() >= NEST_MAX {
break;
}
let sp = indent_of(&rest);
if sp >= 4 && !matches!(self.leaf, Leaf::Para(_)) {
break;
}
if sp <= 3 && rest.as_bytes().get(sp) == Some(&b'>') {
self.close_leaf();
let mut q = sp + 1;
if rest.as_bytes().get(q) == Some(&b' ') {
q += 1;
}
rest = String::from(&rest[q..]);
self.conts.push(Cont { quote: true, indent: 0, ordered: false, number: 0, delim: 0, task: 0, filed: true, empty: false });
continue;
}
if is_rule(&rest) {
break;
}
if let Some((ordered, ch, number, mlen)) = list_marker(&rest) {
let after = &rest[mlen..];
let empty = blank(after);
if matches!(self.leaf, Leaf::Para(_)) && (empty || (ordered && number != 1)) {
break;
}
self.close_leaf();
let pad = indent_of(after);
let (indent, skip) = if empty {
(mlen + 1, after.len())
} else if pad > 4 {
(mlen + 1, 1)
} else {
(mlen + pad, pad)
};
let mut body = String::from(&after[skip.min(after.len())..]);
let mut task = 0;
let bb = body.as_bytes();
if bb.len() >= 3 && bb[0] == b'[' && bb[2] == b']' && matches!(bb[1], b' ' | b'x' | b'X') && (bb.len() == 3 || bb[3] == b' ') {
task = if bb[1] == b' ' { 1 } else { 2 };
body = String::from(body[3.min(body.len())..].trim_start());
}
self.conts.push(Cont {
quote: false,
indent,
ordered,
number,
delim: if ordered { ch } else { 0 },
task,
filed: false,
empty,
});
rest = body;
if empty {
break;
}
continue;
}
break;
}
self.leaf_line(rest);
}
fn starts_block(s: &str, lazy: bool) -> bool {
let sp = indent_of(s);
if sp > 3 {
return false;
}
let t = &s[sp..];
t.starts_with('>')
|| is_rule(s)
|| atx(s).is_some()
|| fence_open(s).is_some()
|| list_marker(s).is_some_and(|m| lazy || (!blank(&s[m.3..]) && (!m.0 || m.2 == 1)))
|| html_start(s)
}
fn leaf_line(&mut self, rest: String) {
if blank(&rest) {
match &mut self.leaf {
Leaf::Code(lines) => lines.push(String::new()),
Leaf::None => {}
_ => self.close_leaf(),
}
self.blank = true;
return;
}
let sp = indent_of(&rest);
if sp >= 4 {
match &mut self.leaf {
Leaf::Para(lines) => {
lines.push(String::from(rest.trim_start()));
return;
}
Leaf::Code(lines) => {
lines.push(String::from(&rest[4..]));
return;
}
Leaf::Table { .. } | Leaf::Html(_) => {}
_ => {
self.close_leaf();
self.leaf = Leaf::Code(vec![String::from(&rest[4..])]);
return;
}
}
}
if matches!(self.leaf, Leaf::Code(_)) {
self.close_leaf();
}
if let Leaf::Html(lines) = &mut self.leaf {
lines.push(rest);
return;
}
if let Some((ch, len, info)) = fence_open(&rest) {
self.close_leaf();
self.leaf = Leaf::Fence { ch, len, indent: sp, info, lines: Vec::new() };
return;
}
if let Some((level, text)) = atx(&rest) {
self.close_leaf();
let src = String::from(text);
self.file(Raw { kind: MB_HEADING, level, src, ..Raw::default() });
return;
}
if sp <= 3 {
if let Leaf::Para(lines) = &self.leaf {
let t = rest.trim();
let lv = if !t.is_empty() && t.bytes().all(|b| b == b'=') {
1
} else if !t.is_empty() && t.bytes().all(|b| b == b'-') {
2
} else {
0
};
if lv > 0 {
let src = lines.join("\n");
let rest_src = take_defs(&src, &mut self.defs);
self.leaf = Leaf::None;
if rest_src.trim().is_empty() {
if lv == 2 && is_rule(&rest) {
self.file(Raw { kind: MB_RULE, ..Raw::default() });
} else {
self.leaf = Leaf::Para(vec![String::from(rest.trim())]);
}
return;
}
self.file(Raw { kind: MB_HEADING, level: lv, src: String::from(rest_src.trim_end()), ..Raw::default() });
return;
}
}
}
if is_rule(&rest) {
self.close_leaf();
self.file(Raw { kind: MB_RULE, ..Raw::default() });
return;
}
if let Leaf::Para(lines) = &mut self.leaf {
if let (Some(aligns), Some(last)) = (delim_row(&rest), lines.last()) {
let head = split_cells(last);
if head.len() == aligns.len() && last.contains('|') {
lines.pop();
if lines.is_empty() {
self.leaf = Leaf::None;
} else {
self.close_leaf();
}
self.table_cells = self.table_cells.saturating_add(aligns.len());
self.leaf = Leaf::Table { aligns, rows: vec![(head, true)] };
return;
}
}
}
if let Leaf::Table { aligns, rows } = &mut self.leaf {
if !blank(&rest) && !Self::starts_block(&rest, false) {
if rows.len() < BLOCKS_MAX && self.table_cells + aligns.len() <= CELLS_MAX {
self.table_cells += aligns.len();
rows.push((split_cells(&rest), false));
} else {
self.truncated = true;
}
return;
}
self.close_leaf();
}
if html_start(&rest) {
self.close_leaf();
self.leaf = Leaf::Html(vec![rest]);
return;
}
match &mut self.leaf {
Leaf::Para(lines) => lines.push(String::from(rest.trim_start())),
_ => {
self.close_leaf();
self.leaf = Leaf::Para(vec![String::from(rest.trim_start())]);
}
}
}
fn finish(mut self) -> (Vec<Raw>, BTreeMap<String, (String, String)>, Vec<MdTable>, bool) {
self.close_conts(0);
self.close_leaf();
(self.raws, self.defs, self.tables, self.truncated)
}
}
const N_TEXT: u8 = 0;
const N_CODE: u8 = 1;
const N_IMG: u8 = 2;
const N_SKIP: u8 = 3;
struct Node {
t: String,
k: u8,
img: u32,
}
struct Delim {
node: usize,
ch: u8,
n: usize,
orig: usize,
open: bool,
close: bool,
}
struct Bracket {
node: usize,
src: usize,
image: bool,
ivs: usize,
}
struct Iv {
a: usize,
b: usize,
style: u16,
link: u32,
color: u32,
bg: u32,
}
struct Inline<'a> {
s: &'a str,
b: &'a [u8],
defs: &'a BTreeMap<String, (String, String)>,
nodes: Vec<Node>,
pend: String,
delims: Vec<Delim>,
brackets: Vec<Bracket>,
dead: usize,
ivs: Vec<Iv>,
html: Vec<(String, usize, u16, u32)>,
ticks: BTreeMap<usize, Vec<usize>>,
budget: usize,
no_cmt: bool,
links: &'a mut Vec<String>,
images: &'a mut Vec<MdImage>,
}
fn char_before(s: &str, i: usize) -> Option<char> {
s[..i].chars().next_back()
}
fn char_at(s: &str, i: usize) -> Option<char> {
s.get(i..).and_then(|t| t.chars().next())
}
impl<'a> Inline<'a> {
fn new(s: &'a str, defs: &'a BTreeMap<String, (String, String)>, links: &'a mut Vec<String>, images: &'a mut Vec<MdImage>) -> Inline<'a> {
let b = s.as_bytes();
let mut ticks: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
let mut i = 0;
while i < b.len() {
if b[i] == b'`' {
let n = b[i..].iter().take_while(|&&c| c == b'`').count();
ticks.entry(n).or_default().push(i);
i += n;
} else {
i += 1;
}
}
Inline {
s,
b,
defs,
nodes: Vec::new(),
pend: String::new(),
delims: Vec::new(),
brackets: Vec::new(),
dead: 0,
ivs: Vec::new(),
html: Vec::new(),
ticks,
budget: s.len() * 8 + 4096,
no_cmt: false,
links,
images,
}
}
fn flush(&mut self) {
if !self.pend.is_empty() {
let t = core::mem::take(&mut self.pend);
self.nodes.push(Node { t, k: N_TEXT, img: 0 });
}
}
fn push(&mut self, t: String, k: u8) -> usize {
self.flush();
self.nodes.push(Node { t, k, img: 0 });
self.nodes.len() - 1
}
fn link_id(&mut self, url: String) -> u32 {
self.links.push(url);
self.links.len() as u32
}
fn run(&mut self) {
let (s, b) = (self.s, self.b);
let mut i = 0;
while i < b.len() {
let c = b[i];
match c {
b'\\' => {
if i + 1 < b.len() && b[i + 1].is_ascii_punctuation() {
self.pend.push(b[i + 1] as char);
i += 2;
} else if i + 1 < b.len() && b[i + 1] == b'\n' {
self.push(String::from("\n"), N_TEXT);
i += 2;
i += b[i..].iter().take_while(|&&c| c == b' ').count();
} else {
self.pend.push('\\');
i += 1;
}
}
b'`' => {
let n = b[i..].iter().take_while(|&&c| c == b'`').count();
let close = self.ticks.get(&n).and_then(|v| {
let k = v.partition_point(|&p| p <= i);
v.get(k).copied()
});
match close {
Some(j) => {
let mut t: String = s[i + n..j].chars().map(|c| if c == '\n' { ' ' } else { c }).collect();
if t.len() >= 2 && t.starts_with(' ') && t.ends_with(' ') && !t.bytes().all(|c| c == b' ') {
t = String::from(&t[1..t.len() - 1]);
}
self.push(t, N_CODE);
i = j + n;
}
None => {
for _ in 0..n {
self.pend.push('`');
}
i += n;
}
}
}
b'*' | b'_' | b'~' => {
let n = b[i..].iter().take_while(|&&x| x == c).count();
let before = char_before(s, i);
let after = char_at(s, i + n);
let ws = |c: Option<char>| c.is_none_or(|c| c.is_whitespace());
let pu = |c: Option<char>| c.is_some_and(is_punct);
let left = !ws(after) && (!pu(after) || ws(before) || pu(before));
let right = !ws(before) && (!pu(before) || ws(after) || pu(after));
let (open, close) = if c == b'_' {
(left && (!right || pu(before)), right && (!left || pu(after)))
} else {
(left, right)
};
let node = self.push(String::from(&s[i..i + n]), N_TEXT);
if (open || close) && (c != b'~' || n <= 2) {
self.delims.push(Delim { node, ch: c, n, orig: n, open, close });
}
i += n;
}
b'[' => {
let node = self.push(String::from("["), N_TEXT);
self.brackets.push(Bracket { node, src: i + 1, image: false, ivs: self.ivs.len() });
i += 1;
}
b'!' if b.get(i + 1) == Some(&b'[') => {
let node = self.push(String::from("!["), N_TEXT);
self.brackets.push(Bracket { node, src: i + 2, image: true, ivs: self.ivs.len() });
i += 2;
}
b']' => i = self.close_bracket(i),
b'<' => i = self.angle(i),
b'&' => match entity_at(&s[i..]) {
Some((ch, n)) => {
self.pend.push(ch);
i += n;
}
None => {
self.pend.push('&');
i += 1;
}
},
b'\n' => {
let hard = self.pend.ends_with(" ");
let t = self.pend.trim_end_matches(' ').len();
self.pend.truncate(t);
self.push(String::from(if hard { "\n" } else { " " }), N_TEXT);
i += 1;
i += b[i..].iter().take_while(|&&c| c == b' ').count();
}
b'h' | b'H' | b'w' | b'W' => {
if let Some(n) = self.bare_link(i) {
i += n;
} else {
self.pend.push(c as char);
i += 1;
}
}
_ => {
let j = b[i..]
.iter()
.position(|&x| matches!(x, b'\\' | b'`' | b'*' | b'_' | b'~' | b'[' | b'!' | b']' | b'<' | b'&' | b'\n' | b'h' | b'H' | b'w' | b'W'))
.map_or(b.len(), |k| i + k.max(1));
let j = (j..=b.len()).find(|&k| s.is_char_boundary(k)).unwrap_or(b.len());
self.pend.push_str(&s[i..j]);
i = j;
}
}
}
self.flush();
}
fn bare_link(&mut self, i: usize) -> Option<usize> {
let (s, b) = (self.s, self.b);
if let Some(p) = char_before(s, i) {
if !(p.is_whitespace() || matches!(p, '(' | '*' | '_' | '~' | '"' | '\'')) {
return None;
}
}
let rest = &b[i..];
let pre = |p: &[u8]| rest.len() > p.len() && rest[..p.len()].eq_ignore_ascii_case(p);
let www = pre(b"www.");
if !(www || pre(b"http://") || pre(b"https://")) {
return None;
}
let mut e = i;
while e < b.len() && !b[e].is_ascii_whitespace() && b[e] != b'<' {
e += 1;
}
while !s.is_char_boundary(e) {
e -= 1;
}
let opens = s[i..e].matches('(').count();
let mut closes = s[i..e].matches(')').count();
loop {
let t = &s[i..e];
let Some(last) = t.chars().next_back() else { break };
if matches!(last, '?' | '!' | '.' | ',' | ':' | '*' | '_' | '~' | '\'' | '"' | ';') {
e -= last.len_utf8();
continue;
}
if last == ')' && closes > opens {
closes -= 1;
e -= 1;
continue;
}
break;
}
let t = &s[i..e];
let host = if www { t.get(4..).unwrap_or("") } else { t.split("://").nth(1).unwrap_or("") };
if host.is_empty() || (www && !host.contains('.')) {
return None;
}
let url = if www { format!("http://{}", t) } else { String::from(t) };
let link = self.link_id(url);
let node = self.push(String::from(t), N_TEXT);
self.ivs.push(Iv { a: node, b: node + 1, style: 0, link, color: 0, bg: 0 });
Some(e - i)
}
fn close_bracket(&mut self, i: usize) -> usize {
let Some(br) = self.brackets.pop() else {
self.pend.push(']');
return i + 1;
};
let idx = self.brackets.len();
let dead = idx < self.dead;
self.dead = self.dead.min(idx);
if dead && !br.image {
self.pend.push(']');
return i + 1;
}
let (s, b) = (self.s, self.b);
let mut found: Option<(String, String, usize)> = None;
if b.get(i + 1) == Some(&b'(') && self.budget > 0 {
let start = i + 2;
let r = (|| {
let mut k = start;
let ws = |k: &mut usize| {
while *k < b.len() && (b[*k] == b' ' || b[*k] == b'\n') {
*k += 1;
}
};
ws(&mut k);
let (dest, mut k) = if b.get(k) == Some(&b')') { (String::new(), k) } else { parse_dest(s, k)? };
let before = k;
ws(&mut k);
let mut title = String::new();
if k > before && k < b.len() && matches!(b[k], b'"' | b'\'' | b'(') {
let (t, e) = parse_title(s, k)?;
title = t;
k = e;
ws(&mut k);
}
(b.get(k) == Some(&b')')).then_some((dest, title, k + 1))
})();
let used = r.as_ref().map_or(b.len() - start, |x| x.2 - start);
self.budget = self.budget.saturating_sub(used.min(b.len()));
found = r;
}
if found.is_none() && i - br.src <= LABEL_MAX && !self.defs.is_empty() && self.budget > 0 {
self.budget = self.budget.saturating_sub(i - br.src + 1);
let text_label = &s[br.src..i];
let mut label = String::from(text_label);
let mut end = i + 1;
if b.get(i + 1) == Some(&b'[') {
let lim = (i + 2 + LABEL_MAX + 1).min(b.len());
if let Some(k) = b[i + 2..lim].iter().position(|&c| c == b']' || c == b'[') {
let k = i + 2 + k;
if b[k] == b']' {
if k > i + 2 {
label = String::from(&s[i + 2..k]);
}
end = k + 1;
}
}
}
if let Some((d, t)) = self.defs.get(&norm_label(&label)) {
found = Some((d.clone(), t.clone(), end));
}
}
let Some((dest, title, end)) = found else {
self.pend.push(']');
return i + 1;
};
self.flush();
let a = br.node + 1;
if br.image {
let first = self.delims.partition_point(|d| d.node < a);
let runs: Vec<usize> = self.delims[first..].iter().map(|d| d.node).collect();
let mut alt = String::new();
for (k, n) in self.nodes[a..].iter().enumerate() {
if (n.k == N_TEXT || n.k == N_CODE) && runs.binary_search(&(a + k)).is_err() {
alt.push_str(&n.t);
}
}
self.nodes.truncate(a);
let tail = self.ivs.split_off(br.ivs.min(self.ivs.len()));
self.ivs.extend(tail.into_iter().filter(|iv| iv.a < a).map(|mut iv| {
iv.b = iv.b.min(a);
iv
}));
self.html.retain(|h| h.1 < a);
self.delims.truncate(first);
self.images.push(MdImage { src: dest, alt, title, w: 0, h: 0 });
let img = self.images.len() as u32;
let nd = &mut self.nodes[br.node];
nd.k = N_IMG;
nd.img = img;
nd.t = String::from("\u{FFFC}");
} else {
let link = self.link_id(dest);
self.nodes[br.node].k = N_SKIP;
let bn = self.nodes.len();
self.ivs.push(Iv { a, b: bn, style: 0, link, color: 0, bg: 0 });
self.dead = self.brackets.len();
}
end
}
fn angle(&mut self, i: usize) -> usize {
let (s, b) = (self.s, self.b);
let lim = (i + 2048).min(b.len());
let end = b[i + 1..lim].iter().position(|&c| c == b'>' || c == b'<' || c == b'\n').map(|k| i + 1 + k);
if let Some(e) = end.filter(|&e| b[e] == b'>') {
let inner = &s[i + 1..e];
let sch = inner.bytes().take_while(|c| c.is_ascii_alphanumeric() || matches!(c, b'+' | b'.' | b'-')).count();
if (2..=32).contains(&sch) && inner.as_bytes()[0].is_ascii_alphabetic() && inner.as_bytes().get(sch) == Some(&b':') && !inner.contains(' ') {
let link = self.link_id(String::from(inner));
let node = self.push(String::from(inner), N_TEXT);
self.ivs.push(Iv { a: node, b: node + 1, style: 0, link, color: 0, bg: 0 });
return e + 1;
}
if let Some((user, host)) = inner.split_once('@') {
if !user.is_empty() && host.contains('.') && !inner.contains(' ') && !host.contains('@') {
let link = self.link_id(format!("mailto:{}", inner));
let node = self.push(String::from(inner), N_TEXT);
self.ivs.push(Iv { a: node, b: node + 1, style: 0, link, color: 0, bg: 0 });
return e + 1;
}
}
}
if s[i..].starts_with("<!--") && !self.no_cmt {
match s[i + 4..].find("-->") {
Some(k) => return i + 4 + k + 3,
None => self.no_cmt = true,
}
}
if self.budget == 0 {
self.pend.push('<');
return i + 1;
}
let mut q = 0u8;
let mut gt = None;
let mut scanned = 0;
for (k, &c) in b[i + 1..lim].iter().enumerate() {
scanned = k + 1;
if q != 0 {
if c == q {
q = 0;
}
} else if c == b'"' || c == b'\'' {
q = c;
} else if c == b'>' {
gt = Some(i + 1 + k);
break;
} else if c == b'<' {
break;
}
}
self.budget = self.budget.saturating_sub(scanned);
let Some(gt) = gt else {
self.pend.push('<');
return i + 1;
};
let inner = &s[i + 1..gt];
let closing = inner.starts_with('/');
let body = inner.trim_start_matches('/');
let nl = body.bytes().take_while(|c| c.is_ascii_alphanumeric()).count();
if nl == 0 || !body.as_bytes()[0].is_ascii_alphabetic() {
self.pend.push('<');
return i + 1;
}
let name = body[..nl].to_ascii_lowercase();
let style = match name.as_str() {
"b" | "strong" => MS_BOLD,
"i" | "em" | "cite" => MS_ITALIC,
"s" | "del" | "strike" => MS_STRIKE,
"u" | "ins" => MS_UNDER,
"code" | "kbd" | "tt" | "samp" => MS_CODE,
_ => 0,
};
match name.as_str() {
"br" => {
self.push(String::from("\n"), N_TEXT);
}
"img" if !closing => {
let at = attrs(&body[nl..]);
let get = |k: &str| at.iter().find(|a| a.0 == k).map(|a| a.1.clone()).unwrap_or_default();
let num = |k: &str| get(k).trim().trim_end_matches("px").parse::<u32>().unwrap_or(0).min(100_000);
self.images.push(MdImage { src: get("src"), alt: get("alt"), title: get("title"), w: num("width"), h: num("height") });
let img = self.images.len() as u32;
let node = self.push(String::from("\u{FFFC}"), N_IMG);
self.nodes[node].img = img;
}
_ if style != 0 || name == "a" => {
self.flush();
if closing {
if let Some(k) = self.html.iter().rposition(|h| h.0 == name) {
let (_, a, st, link) = self.html.remove(k);
let bn = self.nodes.len();
if a < bn {
self.ivs.push(Iv { a, b: bn, style: st, link, color: 0, bg: 0 });
}
}
} else if self.html.len() < HTML_STACK {
let link = if name == "a" {
let at = attrs(&body[nl..]);
match at.iter().find(|a| a.0 == "href") {
Some(h) => self.link_id(h.1.clone()),
None => 0,
}
} else {
0
};
let a = self.nodes.len();
self.html.push((name, a, style, link));
}
}
_ => {}
}
gt + 1
}
fn emphasis(&mut self) {
let n = self.delims.len();
if n == 0 {
return;
}
const NONE: usize = usize::MAX;
let mut prev: Vec<usize> = (0..n).map(|i| if i == 0 { NONE } else { i - 1 }).collect();
let mut next: Vec<usize> = (0..n).map(|i| if i + 1 < n { i + 1 } else { NONE }).collect();
let mut gone = vec![false; n];
let mut bottom = [[[0usize; 2]; 3]; 3];
let ci = |c: u8| match c {
b'*' => 0,
b'_' => 1,
_ => 2,
};
let unlink = |k: usize, prev: &mut Vec<usize>, next: &mut Vec<usize>, gone: &mut Vec<bool>| {
gone[k] = true;
let (p, x) = (prev[k], next[k]);
if p != NONE {
next[p] = x;
}
if x != NONE {
prev[x] = p;
}
};
let mut c = 0;
while c != NONE {
if gone[c] || !self.delims[c].close {
c = next[c];
continue;
}
let (ch, cn, corig, copen) = {
let d = &self.delims[c];
(d.ch, d.n, d.orig, d.open)
};
let key = (ci(ch), corig % 3, copen as usize);
let lim = bottom[key.0][key.1][key.2];
let mut o = prev[c];
let mut found = NONE;
while o != NONE && o >= lim {
let d = &self.delims[o];
if !gone[o] && d.open && d.ch == ch {
let three = ch != b'~' && (d.close || copen) && (d.orig + corig) % 3 == 0 && !(d.orig % 3 == 0 && corig % 3 == 0);
let tilde = ch == b'~' && d.n != cn;
if !three && !tilde {
found = o;
break;
}
}
o = prev[o];
}
if found == NONE {
bottom[key.0][key.1][key.2] = c;
let nx = next[c];
if !copen {
unlink(c, &mut prev, &mut next, &mut gone);
}
c = nx;
continue;
}
let o = found;
let used = if ch == b'~' { cn } else if cn >= 2 && self.delims[o].n >= 2 { 2 } else { 1 };
let style = if ch == b'~' { MS_STRIKE } else if used == 2 { MS_BOLD } else { MS_ITALIC };
let (on, cnode) = (self.delims[o].node, self.delims[c].node);
self.ivs.push(Iv { a: on + 1, b: cnode, style, link: 0, color: 0, bg: 0 });
let mut k = next[o];
while k != NONE && k != c {
let nx = next[k];
unlink(k, &mut prev, &mut next, &mut gone);
k = nx;
}
self.delims[o].n -= used;
let t = &mut self.nodes[on].t;
t.truncate(t.len() - used);
self.delims[c].n -= used;
self.nodes[cnode].t.drain(..used);
if self.delims[o].n == 0 {
unlink(o, &mut prev, &mut next, &mut gone);
}
if self.delims[c].n == 0 {
let nx = next[c];
unlink(c, &mut prev, &mut next, &mut gone);
c = nx;
}
}
}
fn output(mut self) -> (String, Vec<MdSpan>) {
self.emphasis();
let n = self.nodes.len();
let mut ev: Vec<(usize, bool, usize)> = Vec::with_capacity(self.ivs.len() * 2);
for (k, iv) in self.ivs.iter().enumerate() {
if iv.a < iv.b && iv.a < n {
ev.push((iv.a, true, k));
ev.push((iv.b.min(n), false, k));
}
}
ev.sort_by_key(|e| (e.0, e.1));
let mut counts = [0u32; 16];
let mut links: Vec<usize> = Vec::new();
let mut text = String::new();
let mut spans: Vec<MdSpan> = Vec::new();
let mut ei = 0;
for (k, node) in self.nodes.iter().enumerate() {
while ei < ev.len() && ev[ei].0 == k {
let (_, start, iv) = ev[ei];
let v = &self.ivs[iv];
for (bit, c) in counts.iter_mut().enumerate() {
if v.style & (1 << bit) != 0 {
if start {
*c += 1;
} else {
*c = c.saturating_sub(1);
}
}
}
if v.link != 0 || v.color != 0 || v.bg != 0 {
if start {
links.push(iv);
} else if let Some(p) = links.iter().rposition(|&x| x == iv) {
links.remove(p);
}
}
ei += 1;
}
if node.k == N_SKIP || (node.t.is_empty() && node.k != N_IMG) {
continue;
}
let mut style = 0u16;
for (bit, &c) in counts.iter().enumerate() {
if c > 0 {
style |= 1 << bit;
}
}
if node.k == N_CODE {
style |= MS_CODE;
}
let link = links.iter().rev().map(|&i| self.ivs[i].link).find(|&l| l != 0).unwrap_or(0);
let a = text.len() as u32;
text.push_str(&node.t);
let b = text.len() as u32;
let img = if node.k == N_IMG { node.img } else { 0 };
if style == 0 && link == 0 && img == 0 {
continue;
}
match spans.last_mut() {
Some(l) if l.b == a && l.style == style && l.link == link && img == 0 && l.image == 0 => l.b = b,
_ => spans.push(MdSpan { a, b, style, link, image: img, color: 0, bg: 0, field: 0 }),
}
}
(text, spans)
}
}
fn inline(src: &str, defs: &BTreeMap<String, (String, String)>, links: &mut Vec<String>, images: &mut Vec<MdImage>) -> (String, Vec<MdSpan>) {
let mut p = Inline::new(src, defs, links, images);
p.run();
p.output()
}
fn html_blocks(h: HRead, doc: &mut MdDoc, quote: u8, list: u8, path: u32, first_space: u8) -> Vec<u32> {
let depth = (quote + list) as u32;
let path_of = |dq: u8| path | (((1u64 << dq.min(31)) - 1) << depth.min(31)) as u32;
let HRead { paras, ext, forms, fields, nodes, boxes, para_boxes, rule_boxes, .. } = h;
let mut para_blk = vec![NIL; paras.len()];
let box0 = doc.boxes.len() as u32;
doc.boxes.extend(boxes);
let chain = |v: Option<&Vec<u32>>| -> Vec<u32> { v.map_or(Vec::new(), |v| v.iter().map(|&b| b + box0).collect()) };
let mut rules = rule_boxes.iter().peekable();
let (form0, field0) = (doc.forms.len() as u32, doc.fields.len() as u32);
let room = FIELDS_MAX.saturating_sub(doc.fields.len());
doc.forms.extend(forms);
doc.fields.extend(fields.into_iter().take(room).map(|mut f| {
if f.form > 0 {
f.form += form0;
}
f
}));
let mut by: BTreeMap<usize, Vec<&HExt>> = BTreeMap::new();
for (at, e) in &ext {
by.entry(*at).or_default().push(e);
}
let mut table_map: BTreeMap<u32, u32> = BTreeMap::new();
let mut pre_open: Option<(u32, usize)> = None; let n = paras.len();
let mut first = true;
let space = |first: &mut bool, s: u8| -> u8 {
if core::mem::take(first) { first_space } else { s }
};
for k in 0..=n {
let items: Vec<&HExt> = by.get(&k).cloned().unwrap_or_default();
let mut rb = Vec::new();
while rules.peek().is_some_and(|r| r.0 <= k) {
if let Some(r) = rules.next() {
rb = chain(Some(&r.1));
}
}
if items.iter().any(|e| matches!(e, HExt::Rule)) {
let sp = space(&mut first, 2);
doc.push_block(MdBlock { kind: MB_RULE, quote, list, path, space: sp, boxes: rb, ..MdBlock::default() });
pre_open = None;
}
if k == n {
break;
}
let p = ¶s[k];
let mut b = MdBlock { quote, list, path, node: nodes.get(k).copied().unwrap_or_default(), boxes: chain(para_boxes.get(k)), ..MdBlock::default() };
let mut imgs: Vec<(usize, u32, bool)> = Vec::new();
for e in &items {
match e {
HExt::Image { byte, src, alt, w, h } => {
doc.images.push(MdImage { src: src.clone(), alt: alt.clone(), title: String::new(), w: *w, h: *h });
imgs.push((*byte, doc.images.len() as u32, false));
}
HExt::Field { byte: Some(byte), n } if *n < room => imgs.push((*byte, field0 + *n as u32 + 1, true)),
_ => {}
}
}
imgs.sort_unstable();
para_text(p, &imgs, doc, &mut b);
if p.style == PS_QUOTE {
b.quote = quote + 1;
}
b.align = match p.align {
RA_CENTER => 1,
RA_RIGHT => 2,
_ => 0,
};
if p.list != LS_NONE {
b.list = list + p.level + 1;
}
let mut sp = 2;
let mut pre = None;
for e in &items {
match e {
HExt::Heading(l) => {
b.kind = MB_HEADING;
b.level = *l;
}
HExt::Quote(d) => b.quote = quote + d,
HExt::Item(num) => {
b.marker = if *num > 0 { MK_NUMBER } else { MK_BULLET };
b.number = *num;
b.delim = b'.';
sp = if b.list > 0 && doc.blocks.last().is_some_and(|l| l.list > 0) { 1 } else { 2 };
}
HExt::Task(done) => b.task = if *done { 2 } else { 1 },
HExt::Pre { block, lang } => pre = Some((*block, lang.clone())),
HExt::Row { table, head, cells } => {
let next_t = doc.tables.len() as u32;
let t = *table_map.entry(*table).or_insert(next_t);
if t == next_t {
doc.tables.push(MdTable { aligns: Vec::new() });
}
b.kind = MB_ROW;
b.table = t;
b.head = *head;
let len = b.text.len();
for (ci, &(start, al)) in cells.iter().enumerate().take(COLS_MAX) {
let end = cells.get(ci + 1).map_or(len, |c| c.0.saturating_sub(1));
b.cells.push((start.min(len) as u32, end.max(start).min(len) as u32));
let tb = &mut doc.tables[t as usize];
if tb.aligns.len() <= ci {
tb.aligns.resize(ci + 1, MA_NONE);
}
if al != 0 && tb.aligns[ci] == MA_NONE {
tb.aligns[ci] = al;
}
}
sp = if doc.blocks.last().is_some_and(|l| l.kind == MB_ROW && l.table == t) { 0 } else { 2 };
}
_ => {}
}
}
if b.list > 0 && b.marker == MK_NONE && p.list != LS_NONE {
sp = 1;
}
b.path = path_of(b.quote.saturating_sub(quote));
if let Some((blk, lang)) = pre {
match pre_open {
Some((pb, idx)) if pb == blk && idx < doc.blocks.len() => {
let cb = &mut doc.blocks[idx];
cb.text.push('\n');
cb.text.push_str(&expand_tabs(&b.text));
}
_ => {
b.text = expand_tabs(&b.text);
b.kind = MB_CODE;
b.lang = lang;
b.spans.clear();
b.space = space(&mut first, 2);
doc.push_block(b);
pre_open = Some((blk, doc.blocks.len() - 1));
}
}
continue;
}
pre_open = None;
if b.kind == MB_ROW {
trim_cells(&mut b);
}
b.space = space(&mut first, sp);
let (kind, before) = (b.kind, doc.blocks.len());
doc.push_block(b);
if doc.blocks.len() > before && (kind == MB_PARA || kind == MB_HEADING) {
para_blk[k] = before as u32;
}
}
for b in doc.blocks.iter_mut().filter(|b| b.kind == MB_CODE) {
while b.text.ends_with('\n') {
b.text.pop();
}
}
para_blk
}
fn para_text(p: &Para, imgs: &[(usize, u32, bool)], doc: &mut MdDoc, b: &mut MdBlock) {
let mut ii = 0;
let mut last_link: Option<(usize, u32)> = None;
for (t, f) in p.runs() {
let a0 = b.text.len();
b.text.push_str(t);
let e0 = b.text.len();
let mut style = 0;
if f.flags & RF_BOLD != 0 {
style |= MS_BOLD;
}
if f.flags & RF_ITALIC != 0 {
style |= MS_ITALIC;
}
if f.flags & RF_UNDER != 0 {
style |= MS_UNDER;
}
if f.flags & RF_STRIKE != 0 {
style |= MS_STRIKE;
}
if f.font.eq_ignore_ascii_case("monospace") {
style |= MS_CODE;
}
let link = if f.link.is_empty() {
0
} else {
match last_link {
Some((end, l)) if end == a0 && doc.link(l) == f.link => l,
_ => {
doc.links.push(f.link.clone());
doc.links.len() as u32
}
}
};
last_link = (link != 0).then_some((e0, link));
let styled = style != 0 || link != 0 || f.color != 0 || f.bg != 0;
let span = |a: usize, e: usize, image: u32| MdSpan { a: a as u32, b: e as u32, style, link, image, color: f.color, bg: f.bg, field: 0 };
let mut s0 = a0;
while ii < imgs.len() && imgs[ii].0 < e0 {
let (ib, obj, field) = imgs[ii];
ii += 1;
if ib >= s0 && b.text.get(ib..ib + 3) == Some("\u{FFFC}") {
if ib > s0 && styled {
b.spans.push(span(s0, ib, 0));
}
if field {
b.spans.push(MdSpan { a: ib as u32, b: ib as u32 + 3, field: obj, ..MdSpan::default() });
} else {
b.spans.push(span(ib, ib + 3, obj));
}
s0 = ib + 3;
}
}
if s0 < e0 && styled {
b.spans.push(span(s0, e0, 0));
}
}
}
fn alike(x: &MdBlock, y: &MdBlock) -> bool {
x.kind == y.kind
&& x.level == y.level
&& x.text == y.text
&& (x.quote, x.list, x.path, x.marker, x.number, x.delim, x.task, x.space, x.head, x.align) == (y.quote, y.list, y.path, y.marker, y.number, y.delim, y.task, y.space, y.head, y.align)
&& x.lang == y.lang
&& x.cells == y.cells
&& x.node == y.node
&& x.boxes.len() == y.boxes.len()
&& x.spans.len() == y.spans.len()
&& x.spans.iter().zip(&y.spans).all(|(s, t)| {
(s.a, s.b, s.style, s.color, s.bg) == (t.a, t.b, t.style, t.color, t.bg) && (s.link != 0) == (t.link != 0) && (s.image != 0) == (t.image != 0) && (s.field != 0) == (t.field != 0)
})
}
fn trim_cells(b: &mut MdBlock) {
for c in b.cells.iter_mut() {
let t = &b.text[c.0 as usize..c.1 as usize];
let lead = t.len() - t.trim_start().len();
let trail = t.len() - t.trim_end().len();
c.0 += lead as u32;
c.1 = c.1.saturating_sub(trail as u32).max(c.0);
}
}
impl MdDoc {
pub fn markdown(s: &str) -> MdDoc {
let mut doc = MdDoc::default();
let mut s = s;
if s.len() > INPUT_MAX {
let mut k = INPUT_MAX;
while !s.is_char_boundary(k) {
k -= 1;
}
s = &s[..k];
doc.truncated = true;
}
let s = s.strip_prefix('\u{FEFF}').unwrap_or(s);
let mut bp = BlockParser::new();
let mut rest = s;
while !rest.is_empty() {
let (line, next) = match rest.find(['\n', '\r']) {
Some(i) => {
let skip = if rest[i..].starts_with("\r\n") { 2 } else { 1 };
(&rest[..i], &rest[i + skip..])
}
None => (rest, ""),
};
if line.contains('\0') {
bp.line(&line.replace('\0', "\u{FFFD}"));
} else {
bp.line(line);
}
rest = next;
}
let (raws, defs, tables, trunc) = bp.finish();
doc.truncated |= trunc;
doc.tables = tables;
for r in raws {
if r.html {
html_blocks(html_read(&r.src), &mut doc, r.quote, r.list, r.path, r.space);
continue;
}
let mut b = MdBlock {
kind: r.kind,
level: r.level,
quote: r.quote,
list: r.list,
path: r.path,
marker: r.marker,
number: r.number,
delim: r.delim,
task: r.task,
space: r.space,
lang: r.lang,
table: r.table,
head: r.head,
..MdBlock::default()
};
match r.kind {
MB_CODE => b.text = r.src,
MB_RULE => {}
MB_ROW => {
for (ci, c) in r.cells.iter().enumerate() {
if ci > 0 {
b.text.push('\t');
}
let (t, spans) = inline(c, &defs, &mut doc.links, &mut doc.images);
let off = b.text.len() as u32;
let t = t.replace('\t', " ").replace('\n', " ");
b.text.push_str(&t);
b.cells.push((off, b.text.len() as u32));
b.spans.extend(spans.into_iter().map(|mut s| {
s.a += off;
s.b += off;
s
}));
}
}
_ => {
let (t, spans) = inline(&r.src, &defs, &mut doc.links, &mut doc.images);
b.text = t;
b.spans = spans;
}
}
doc.push_block(b);
}
doc.finish();
doc
}
pub fn html(s: &str) -> MdDoc {
let mut s = s;
let mut cut = false;
if s.len() > INPUT_MAX {
let mut k = INPUT_MAX;
while !s.is_char_boundary(k) {
k -= 1;
}
s = &s[..k];
cut = true;
}
let mut doc = MdDoc { dom: Dom::parse(s), cut, ..MdDoc::default() };
doc.derive();
doc
}
fn derive(&mut self) {
let mut dom = core::mem::take(&mut self.dom);
let mut d = MdDoc::default();
let sheet = Rc::new(Sheet::of(&dom, &self.linked));
let mut h = dom_read(&dom, &sheet, true);
let ranges = core::mem::take(&mut h.ranges);
let para_blk = html_blocks(h, &mut d, 0, 0, 0, 0);
d.finish();
for n in dom.nodes.iter_mut() {
n.blk = NIL;
}
for (e, p0, p1) in ranges {
if p1 == p0 + 1 {
if let Some(&b) = para_blk.get(p0 as usize) {
dom.nodes[e as usize].blk = b;
}
}
}
dom.changed = false;
dom.touched = Some(Vec::new());
self.blocks = d.blocks;
self.links = d.links;
self.images = d.images;
self.tables = d.tables;
self.spots = d.spots;
self.forms = d.forms;
self.fields = d.fields;
self.fspots = d.fspots;
self.boxes = d.boxes;
self.box_ends = d.box_ends;
self.sheet = sheet;
self.truncated = self.cut || d.truncated || dom.truncated;
self.dom = dom;
}
pub fn update(&mut self) -> bool {
if !self.dom.changed {
return false;
}
let touched = self.dom.touched.take();
let ch = match touched.and_then(|t| self.update_fast(&t)) {
Some(redo) => DocChange { redo, ..DocChange::default() },
None => self.update_full(),
};
self.dom.changed = false;
self.dom.touched = Some(Vec::new());
self.change = Some(match self.change.take() {
Some(c) => c.then(ch),
None => ch,
});
true
}
pub(crate) fn take_change(&mut self) -> Option<DocChange> {
self.change.take()
}
fn update_fast(&mut self, touched: &[(u32, bool)]) -> Option<Vec<usize>> {
let mut leaves: Vec<(u32, usize)> = Vec::new();
for &(e, own) in touched {
let d = &self.dom;
if d.nodes.get(e as usize).is_none_or(|n| n.serial == 0) || !d.attached(e) {
continue;
}
let l = d.leaf_of(e, own)?;
if !leaves.iter().any(|x| x.0 == l) {
leaves.push((l, d.nodes[l as usize].blk as usize));
}
}
let mut new: Vec<(usize, MdBlock)> = Vec::new();
for &(l, b) in &leaves {
let old = self.blocks.get(b)?;
if !matches!(old.kind, MB_PARA | MB_HEADING) || old.spans.iter().any(|s| s.link != 0 || s.image != 0 || s.field != 0) {
return None;
}
let p = dom_para(&self.dom, &self.sheet, l)?;
let mut nb = MdBlock { text: String::new(), spans: Vec::new(), node: self.dom.handle(l), ..old.clone() };
let mut scratch = MdDoc::default();
para_text(&p, &[], &mut scratch, &mut nb);
if !scratch.links.is_empty() {
return None;
}
new.push((b, nb));
}
let mut redo = Vec::new();
for (b, nb) in new {
if self.blocks[b] != nb {
self.blocks[b] = nb;
redo.push(b);
}
}
redo.sort_unstable();
redo.dedup();
Some(redo)
}
fn update_full(&mut self) -> DocChange {
let old = core::mem::take(&mut self.blocks);
let images = core::mem::take(&mut self.images);
let spots = core::mem::take(&mut self.spots);
let old_boxes = core::mem::take(&mut self.boxes);
self.derive();
let new = &self.blocks;
let (on, nn) = (old.len(), new.len());
let same = |x: &MdBlock, y: &MdBlock| x.boxes.len() == y.boxes.len() && x.boxes.iter().zip(&y.boxes).all(|(&i, &j)| old_boxes.get(i as usize) == self.boxes.get(j as usize));
let mut a = 0;
while a < on && a < nn && old[a] == new[a] && same(&old[a], &new[a]) {
a += 1;
}
let mut s = 0;
while s < on - a && s < nn - a && alike(&old[on - 1 - s], &new[nn - 1 - s]) {
s += 1;
}
let redo: Vec<usize> = (nn - s..nn).filter(|&j| old[j - nn + on] != new[j] || !same(&old[j - nn + on], &new[j])).collect();
let range = (a, on - a - s, nn - a - s);
DocChange { range: (range.1 > 0 || range.2 > 0).then_some(range), redo, full: true, images, spots }
}
pub fn sheet_links(&self) -> &[String] {
self.sheet.links()
}
pub fn set_sheet(&mut self, href: &str, css: Option<&str>) {
let href = href.trim();
match css {
Some(c) => {
if self.linked.get(href).is_some_and(|x| x == c) {
return;
}
self.linked.insert(String::from(href), String::from(c));
}
None => {
if self.linked.remove(href).is_none() {
return;
}
}
}
if self.sheet.links().iter().any(|l| l == href) {
self.dom.changed = true;
self.dom.touched = None;
}
}
pub fn style_of(&self, n: MdNode) -> String {
match self.elem(n) {
Some(i) => self.sheet.style_of(&self.dom, i),
None => String::new(),
}
}
pub fn boxes(&self) -> &[MdBox] {
&self.boxes
}
pub(crate) fn box_ends(&self, i: u32) -> (usize, usize) {
self.box_ends.get(i as usize).map_or((0, 0), |e| (e.0 as usize, e.1 as usize))
}
pub fn field_of(&self, n: MdNode) -> Option<usize> {
if n.is_none() {
return None;
}
self.fields.iter().position(|f| f.node == n)
}
fn push_block(&mut self, b: MdBlock) {
if self.blocks.len() < BLOCKS_MAX {
self.blocks.push(b);
} else {
self.truncated = true;
}
}
fn finish(&mut self) {
if let Some(b) = self.blocks.first_mut() {
b.space = 0;
}
self.box_ends = vec![(u32::MAX, 0); self.boxes.len()];
for (k, b) in self.blocks.iter().enumerate() {
for &i in &b.boxes {
if let Some(e) = self.box_ends.get_mut(i as usize) {
e.0 = e.0.min(k as u32);
e.1 = e.1.max(k as u32);
}
}
}
self.spots.clear();
self.fspots.clear();
for (k, b) in self.blocks.iter().enumerate() {
for s in &b.spans {
if s.field != 0 && (s.field as usize) <= self.fields.len() {
self.fspots.push(MdFieldSpot { block: k, a: s.a, field: s.field });
}
if s.link == 0 {
continue;
}
match self.spots.last_mut() {
Some(l) if l.block == k && l.link == s.link && l.b == s.a => l.b = s.b,
_ => self.spots.push(MdLinkSpot { block: k, a: s.a, b: s.b, link: s.link }),
}
}
}
}
pub fn blocks(&self) -> &[MdBlock] {
&self.blocks
}
pub fn links(&self) -> &[String] {
&self.links
}
pub fn link(&self, link: u32) -> &str {
if link == 0 { "" } else { self.links.get(link as usize - 1).map_or("", |s| s.as_str()) }
}
pub fn images(&self) -> &[MdImage] {
&self.images
}
pub fn tables(&self) -> &[MdTable] {
&self.tables
}
pub fn link_spots(&self) -> &[MdLinkSpot] {
&self.spots
}
pub fn forms(&self) -> &[MdForm] {
&self.forms
}
pub fn fields(&self) -> &[MdField] {
&self.fields
}
pub fn field_spots(&self) -> &[MdFieldSpot] {
&self.fspots
}
pub fn truncated(&self) -> bool {
self.truncated
}
pub fn end(&self) -> TextPos {
let n = self.blocks.len();
if n == 0 { TextPos::default() } else { TextPos::new(n - 1, self.blocks[n - 1].text.len()) }
}
pub fn plain(&self) -> String {
self.plain_range(TextPos::default(), self.end())
}
pub fn to_html(&self) -> String {
self.html_range(TextPos::default(), self.end())
}
pub(crate) fn block_text(&self, k: usize, a: usize, b: usize) -> String {
let bl = &self.blocks[k];
let t = &bl.text;
let (a, b) = (clamp_char(t, a), clamp_char(t, b.max(a)));
let mut out = String::new();
let mut i = a;
for s in &bl.spans {
if (s.image == 0 && s.field == 0) || (s.b as usize) <= a || (s.a as usize) >= b {
continue;
}
out.push_str(&t[i..s.a as usize]);
if s.image != 0 {
out.push_str(&self.images.get(s.image as usize - 1).map_or(String::new(), |m| m.alt.clone()));
} else if let Some(f) = self.fields.get(s.field as usize - 1) {
out.push_str(f.caption());
}
i = s.b as usize;
}
out.push_str(&t[i.min(b)..b]);
out
}
pub fn plain_range(&self, a: TextPos, b: TextPos) -> String {
let (a, b) = if a <= b { (a, b) } else { (b, a) };
let mut out = String::new();
if self.blocks.is_empty() {
return out;
}
let last = b.line.min(self.blocks.len() - 1);
for k in a.line..=last {
let bl = &self.blocks[k];
let s = if k == a.line { a.byte } else { 0 };
let e = if k == b.line { b.byte } else { bl.text.len() };
if k > a.line {
out.push('\n');
if bl.space >= 2 {
out.push('\n');
}
}
if s == 0 {
for _ in 1..bl.list {
out.push_str(" ");
}
match bl.marker {
MK_BULLET => out.push_str("- "),
MK_NUMBER => out.push_str(&format!("{}{} ", bl.number, bl.delim as char)),
_ if bl.list > 0 => out.push_str(" "),
_ => {}
}
match bl.task {
1 => out.push_str("[ ] "),
2 => out.push_str("[x] "),
_ => {}
}
if bl.kind == MB_RULE && k != last {
out.push_str("---");
}
}
out.push_str(&self.block_text(k, s, e));
}
out
}
pub fn html_range(&self, a: TextPos, b: TextPos) -> String {
let (a, b) = if a <= b { (a, b) } else { (b, a) };
let mut out = String::new();
if self.blocks.is_empty() {
return out;
}
let last = b.line.min(self.blocks.len() - 1);
let mut quotes = 0u8;
let mut lists: Vec<(bool, bool)> = Vec::new();
let mut table: Option<u32> = None;
for k in a.line..=last {
let bl = &self.blocks[k];
let s = if k == a.line { a.byte } else { 0 };
let e = if k == b.line { b.byte } else { bl.text.len() };
if bl.kind != MB_ROW || table != Some(bl.table) {
if table.take().is_some() {
out.push_str("</table>");
}
}
loop {
let depth = bl.list as usize;
let pop = match lists.last() {
Some(_) if lists.len() > depth => true,
Some(l) if lists.len() == depth && bl.marker != MK_NONE => l.0 != (bl.marker == MK_NUMBER),
_ => false,
};
if !pop {
break;
}
if let Some((ord, li)) = lists.pop() {
if li {
out.push_str("</li>");
}
out.push_str(if ord { "</ol>" } else { "</ul>" });
}
}
while quotes > bl.quote {
out.push_str("</blockquote>");
quotes -= 1;
}
while quotes < bl.quote {
out.push_str("<blockquote>");
quotes += 1;
}
while lists.len() < bl.list as usize {
let ord = bl.marker == MK_NUMBER && lists.len() + 1 == bl.list as usize;
if ord && bl.number != 1 {
out.push_str(&format!("<ol start=\"{}\">", bl.number));
} else {
out.push_str(if ord { "<ol>" } else { "<ul>" });
}
lists.push((ord, false));
}
if bl.marker != MK_NONE {
if let Some(l) = lists.last_mut() {
if l.1 {
out.push_str("</li>");
}
out.push_str("<li>");
l.1 = true;
}
match bl.task {
1 => out.push_str("<input type=\"checkbox\" disabled> "),
2 => out.push_str("<input type=\"checkbox\" checked disabled> "),
_ => {}
}
}
match bl.kind {
MB_HEADING => {
let l = bl.level.clamp(1, 6);
out.push_str(&format!("<h{}>", l));
self.inline_html(k, s, e, &mut out);
out.push_str(&format!("</h{}>", l));
}
MB_CODE => {
if bl.lang.is_empty() {
out.push_str("<pre><code>");
} else {
out.push_str("<pre><code class=\"language-");
esc(&bl.lang, &mut out);
out.push_str("\">");
}
esc(&bl.text[clamp_char(&bl.text, s)..clamp_char(&bl.text, e)], &mut out);
out.push_str("</code></pre>");
}
MB_RULE => out.push_str("<hr>"),
MB_ROW => {
if table.is_none() {
out.push_str("<table>");
table = Some(bl.table);
}
out.push_str("<tr>");
let aligns = self.tables.get(bl.table as usize).map(|t| t.aligns.as_slice()).unwrap_or(&[]);
for (ci, &(ca, cb)) in bl.cells.iter().enumerate() {
let (ca, cb) = (ca as usize, cb as usize);
if cb < s || ca > e {
continue;
}
let tag = if bl.head { "th" } else { "td" };
match aligns.get(ci).copied().unwrap_or(MA_NONE) {
MA_LEFT => out.push_str(&format!("<{} align=\"left\">", tag)),
MA_CENTER => out.push_str(&format!("<{} align=\"center\">", tag)),
MA_RIGHT => out.push_str(&format!("<{} align=\"right\">", tag)),
_ => out.push_str(&format!("<{}>", tag)),
}
self.inline_html(k, ca.max(s), cb.min(e), &mut out);
out.push_str(&format!("</{}>", tag));
}
out.push_str("</tr>");
}
_ => {
match bl.align {
1 => out.push_str("<p style=\"text-align:center\">"),
2 => out.push_str("<p style=\"text-align:right\">"),
_ => out.push_str("<p>"),
}
self.inline_html(k, s, e, &mut out);
out.push_str("</p>");
}
}
}
if table.is_some() {
out.push_str("</table>");
}
while let Some((ord, li)) = lists.pop() {
if li {
out.push_str("</li>");
}
out.push_str(if ord { "</ol>" } else { "</ul>" });
}
while quotes > 0 {
out.push_str("</blockquote>");
quotes -= 1;
}
out
}
fn inline_html(&self, k: usize, a: usize, b: usize, out: &mut String) {
let bl = &self.blocks[k];
let t = &bl.text;
let (a, b) = (clamp_char(t, a), clamp_char(t, b.max(a)));
let mut i = a;
let plain = |out: &mut String, s: &str| {
for (n, part) in s.split('\n').enumerate() {
if n > 0 {
out.push_str("<br>");
}
esc(part, out);
}
};
for s in &bl.spans {
let (sa, sb) = (s.a as usize, s.b as usize);
if sb <= a || sa >= b {
continue;
}
let (sa, sb) = (clamp_char(t, sa.max(a)), clamp_char(t, sb.min(b)));
if sa > i {
plain(out, &t[i..sa]);
}
if s.image != 0 {
if let Some(m) = self.images.get(s.image as usize - 1) {
out.push_str("<img src=\"");
esc(&m.src, out);
out.push_str("\" alt=\"");
esc(&m.alt, out);
out.push_str("\">");
}
i = sb;
continue;
}
if s.field != 0 {
if let Some(f) = self.fields.get(s.field as usize - 1) {
esc(f.caption(), out);
}
i = sb;
continue;
}
let mut close: Vec<&str> = Vec::new();
if s.link != 0 {
out.push_str("<a href=\"");
esc(self.link(s.link), out);
out.push_str("\">");
close.push("</a>");
}
if s.color != 0 || s.bg != 0 {
out.push_str("<span style=\"");
if s.color != 0 {
out.push_str(&format!("color:#{:06x};", s.color & 0xFF_FFFF));
}
if s.bg != 0 {
out.push_str(&format!("background-color:#{:06x};", s.bg & 0xFF_FFFF));
}
out.push_str("\">");
close.push("</span>");
}
for (bit, o, c) in [(MS_BOLD, "<strong>", "</strong>"), (MS_ITALIC, "<em>", "</em>"), (MS_STRIKE, "<del>", "</del>"), (MS_UNDER, "<u>", "</u>"), (MS_CODE, "<code>", "</code>")] {
if s.style & bit != 0 {
out.push_str(o);
close.push(c);
}
}
plain(out, &t[sa..sb]);
for c in close.iter().rev() {
out.push_str(c);
}
i = sb;
}
if i < b {
plain(out, &t[i..b]);
}
}
}
fn clamp_char(t: &str, i: usize) -> usize {
let mut i = i.min(t.len());
while !t.is_char_boundary(i) {
i -= 1;
}
i
}
fn esc(s: &str, out: &mut String) {
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\u{FFFC}' => {}
_ => out.push(c),
}
}
}