//! 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");
}
let _ = start;
for item in items {
out.push_str("- ");
// A single paragraph needs no block wrapper inside the item.
match item.as_slice() {
[Block::Paragraph(inline)] => out.push_str(&inlines(inline)),
blocks_in_item => {
out.push('\n');
blocks(blocks_in_item, out);
}
}
out.push_str("
\n");
}
out.push_str(if *ordered { "
\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),
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!(
"
",
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("