//! XHTML Mobile Profile markup, and the `text/html` body derived from it. //! //! One document serves both: a WAP 2.0 client needs well-formed XML with the //! prolog and the Mobile Profile doctype, while a browser parsing the same bytes //! as `text/html` tolerates the leading `` as a bogus comment but //! logs a warning for it. So the HTML form is the same markup with that one line //! removed. use itsybitsy_core::ir::{Block, Doc, Inline}; use crate::escape; /// The XML declaration, which only the XHTML-MP form carries. const PROLOG: &str = "\n"; const DOCTYPE: &str = "\n"; /// Build the whole document. `prolog` distinguishes the two media types. pub fn document(doc: &Doc, title: &str, prolog: bool) -> String { let mut out = String::new(); if prolog { out.push_str(PROLOG); } out.push_str(DOCTYPE); out.push_str("\n"); out.push_str(&format!("{}\n", escape::text(title))); out.push_str("\n"); blocks(&doc.blocks, &mut out); out.push_str("\n\n"); out } fn blocks(blocks: &[Block], out: &mut String) { for block in blocks { self_block(block, out); } } fn self_block(block: &Block, out: &mut String) { match block { Block::Heading { level, inline } => { let level = (*level).clamp(1, 6); out.push_str(&format!("{}\n", inlines(inline))); } Block::Paragraph(inline) => out.push_str(&format!("

{}

\n", inlines(inline))), Block::CodeBlock { lines, .. } => { out.push_str("
");
            out.push_str(&escape::text(&lines.join("\n")));
            out.push_str("
\n"); } Block::Art { alt, lines } => { // The label is the only description a client that cannot show the // art would have, so it is kept as the title attribute. out.push_str(&format!("
", escape::attr(alt)));
            out.push_str(&escape::text(&lines.join("\n")));
            out.push_str("
\n"); } Block::BlockQuote(inner) => { out.push_str("
\n"); blocks(inner, out); out.push_str("
\n"); } Block::List { ordered, start, items } => { if *ordered { // XHTML-MP has no `start` attribute, so a list beginning // elsewhere than 1 cannot be expressed; the numbers restart. out.push_str("
    \n"); } else { out.push_str("
\n" } else { "\n" }); } Block::Table { head, rows, .. } => { out.push_str("\n"); if !head.is_empty() { out.push_str(""); for cell in head { out.push_str(&format!("", inlines(cell))); } out.push_str("\n"); } for row in rows { out.push_str(""); for cell in row { out.push_str(&format!("", inlines(cell))); } out.push_str("\n"); } out.push_str("
{}
{}
\n"); } Block::Rule => out.push_str("
\n"), // Raw HTML is passed through: this is the one family of formats where it // is already in the right language. Block::Html(html) => { out.push_str(html.trim_end()); out.push('\n'); } // Alignment reaches only the fixed-width text formats. Pagination is a // WML concern; a browser scrolls one document. Block::Aligned { block, .. } => self_block(block, out), // Gates are filtered out before rendering; keeping the content is the // harmless reading if one ever arrives here. Block::Gated { block, .. } => self_block(block, out), Block::CardBreak { .. } => {} } } fn inlines(inline: &[Inline]) -> String { let mut out = String::new(); for item in inline { match item { Inline::Text(text) => out.push_str(&escape::text(text)), Inline::Code(code) => out.push_str(&format!("{}", escape::text(code))), Inline::Emph(inner) => out.push_str(&format!("{}", inlines(inner))), Inline::Strong(inner) => out.push_str(&format!("{}", inlines(inner))), // XHTML-MP 1.0 has no , and is not in the profile, so // the text survives without its markup rather than being dropped. Inline::Strike(inner) => out.push_str(&inlines(inner)), Inline::Link { href, title, label } => { let title = title .as_deref() .map(|t| format!(" title=\"{}\"", escape::attr(t))) .unwrap_or_default(); out.push_str(&format!( "{}", escape::attr(href), inlines(label) )); } Inline::Image { src, title, alt } => { let title = title .as_deref() .map(|t| format!(" title=\"{}\"", escape::attr(t))) .unwrap_or_default(); out.push_str(&format!( "\"{}\"{title}/", escape::attr(src), escape::attr(&Doc::plain_text(alt)) )); } Inline::SoftBreak => out.push('\n'), Inline::HardBreak => out.push_str("
\n"), Inline::Html(html) => out.push_str(html), } } out } #[cfg(test)] mod tests { use super::*; #[test] fn the_prolog_is_present_only_when_asked_for() { let doc = Doc { blocks: Vec::new(), first_h1: None }; assert!(document(&doc, "T", true).starts_with("