177 lines
5.8 KiB
Rust
177 lines
5.8 KiB
Rust
|
|
//! The WAP family: XHTML Mobile Profile, and the `text/html` body derived from it.
|
||
|
|
//!
|
||
|
|
//! One crate rather than one per format, because the HTML form is the XHTML-MP
|
||
|
|
//! bytes with the XML declaration removed, and because WML's deck configuration
|
||
|
|
//! is shared with nothing outside this family. Splitting them would duplicate the
|
||
|
|
//! escaper and the markup builder.
|
||
|
|
|
||
|
|
mod escape;
|
||
|
|
mod xhtmlmp;
|
||
|
|
|
||
|
|
use itsybitsy_core::Error;
|
||
|
|
use itsybitsy_core::ir::Doc;
|
||
|
|
use itsybitsy_core::render::{RenderCtx, Rendered, Renderer};
|
||
|
|
|
||
|
|
/// XHTML Mobile Profile, for WAP 2.0 clients: well-formed XML with the prolog.
|
||
|
|
pub struct XhtmlMp;
|
||
|
|
|
||
|
|
impl Renderer for XhtmlMp {
|
||
|
|
fn id(&self) -> &'static str {
|
||
|
|
"xhtmlmp"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn media_type(&self) -> &'static str {
|
||
|
|
"application/vnd.wap.xhtml+xml; charset=utf-8"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn render(&self, doc: &Doc, ctx: &RenderCtx<'_>) -> Result<Rendered, Error> {
|
||
|
|
Ok(Rendered::body(xhtmlmp::document(doc, ctx.title, true).into_bytes()))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The same markup served as `text/html`, for an ordinary browser.
|
||
|
|
pub struct Html;
|
||
|
|
|
||
|
|
impl Renderer for Html {
|
||
|
|
fn id(&self) -> &'static str {
|
||
|
|
"html"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn media_type(&self) -> &'static str {
|
||
|
|
"text/html; charset=utf-8"
|
||
|
|
}
|
||
|
|
|
||
|
|
fn render(&self, doc: &Doc, ctx: &RenderCtx<'_>) -> Result<Rendered, Error> {
|
||
|
|
Ok(Rendered::body(xhtmlmp::document(doc, ctx.title, false).into_bytes()))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use itsybitsy_core::config::PageSettings;
|
||
|
|
use itsybitsy_core::parse;
|
||
|
|
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
fn render(renderer: &dyn Renderer, markdown: &str) -> String {
|
||
|
|
let doc = parse::markdown(markdown);
|
||
|
|
let settings = PageSettings::default();
|
||
|
|
let ctx = RenderCtx { url: "/x", title: "The Title", settings: &settings, width: None };
|
||
|
|
String::from_utf8(renderer.render(&doc, &ctx).unwrap().body).unwrap()
|
||
|
|
}
|
||
|
|
|
||
|
|
fn body(markdown: &str) -> String {
|
||
|
|
let out = render(&Html, markdown);
|
||
|
|
let start = out.find("<body>\n").unwrap() + "<body>\n".len();
|
||
|
|
let end = out.find("</body>").unwrap();
|
||
|
|
out[start..end].to_string()
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn the_html_form_carries_no_xml_declaration() {
|
||
|
|
// A browser parsing this as text/html tolerates the declaration as a
|
||
|
|
// bogus comment and renders the page, but logs a warning for it.
|
||
|
|
let out = render(&Html, "# a\n");
|
||
|
|
assert!(!out.starts_with("<?xml"));
|
||
|
|
assert!(out.starts_with("<!DOCTYPE html"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn the_xhtml_form_keeps_it_because_wap_clients_need_valid_xml() {
|
||
|
|
assert!(render(&XhtmlMp, "# a\n").starts_with("<?xml version=\"1.0\""));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn the_two_forms_differ_only_in_that_line() {
|
||
|
|
let xml = render(&XhtmlMp, "# a\n\ntext\n");
|
||
|
|
let html = render(&Html, "# a\n\ntext\n");
|
||
|
|
assert_eq!(&xml[xml.find("<!DOCTYPE").unwrap()..], html);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn the_resolved_title_reaches_the_head() {
|
||
|
|
assert!(render(&Html, "# ignored\n").contains("<title>The Title</title>"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn blocks_become_their_markup() {
|
||
|
|
assert_eq!(body("# a\n"), "<h1>a</h1>\n");
|
||
|
|
assert_eq!(body("###### f\n"), "<h6>f</h6>\n");
|
||
|
|
assert_eq!(body("text\n"), "<p>text</p>\n");
|
||
|
|
assert_eq!(body("---\n"), "<hr/>\n");
|
||
|
|
assert_eq!(body("> q\n"), "<blockquote>\n<p>q</p>\n</blockquote>\n");
|
||
|
|
assert_eq!(body("```\nx\n```\n"), "<pre>x</pre>\n");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn lists_keep_a_single_paragraph_item_inline() {
|
||
|
|
assert_eq!(body("- a\n- b\n"), "<ul>\n<li>a</li>\n<li>b</li>\n</ul>\n");
|
||
|
|
assert_eq!(body("1. a\n"), "<ol>\n<li>a</li>\n</ol>\n");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn inline_markup_becomes_elements() {
|
||
|
|
assert_eq!(
|
||
|
|
body("a *b* **c** `d`\n"),
|
||
|
|
"<p>a <em>b</em> <strong>c</strong> <code>d</code></p>\n"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn strikethrough_keeps_its_text_since_the_profile_has_no_element_for_it() {
|
||
|
|
assert_eq!(body("~~gone~~\n"), "<p>gone</p>\n");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn links_and_images_carry_their_attributes() {
|
||
|
|
assert_eq!(body("[label](/x \"T\")\n"), "<p><a href=\"/x\" title=\"T\">label</a></p>\n");
|
||
|
|
assert_eq!(body("\n"), "<p><img src=\"/i.png\" alt=\"alt\"/></p>\n");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn markup_characters_in_text_are_escaped() {
|
||
|
|
assert_eq!(body("a < b & c\n"), "<p>a < b & c</p>\n");
|
||
|
|
assert_eq!(body("[x](/a?b=1&c=2)\n"), "<p><a href=\"/a?b=1&c=2\">x</a></p>\n");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn tables_become_tables() {
|
||
|
|
assert_eq!(
|
||
|
|
body("| h |\n| --- |\n| c |\n"),
|
||
|
|
"<table>\n<tr><th>h</th></tr>\n<tr><td>c</td></tr>\n</table>\n"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn art_keeps_its_label_as_a_title() {
|
||
|
|
let doc = Doc {
|
||
|
|
blocks: vec![itsybitsy_core::ir::Block::Art {
|
||
|
|
alt: "Dragon".into(),
|
||
|
|
lines: vec!["/\\".into()],
|
||
|
|
}],
|
||
|
|
first_h1: None,
|
||
|
|
};
|
||
|
|
let settings = PageSettings::default();
|
||
|
|
let ctx = RenderCtx { url: "/x", title: "T", settings: &settings, width: None };
|
||
|
|
let out = String::from_utf8(Html.render(&doc, &ctx).unwrap().body).unwrap();
|
||
|
|
assert!(out.contains("<pre title=\"Dragon\">/\\</pre>"), "{out}");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn raw_html_is_passed_through_because_it_is_already_the_right_language() {
|
||
|
|
assert_eq!(body("<div>x</div>\n"), "<div>x</div>\n");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn a_card_divider_emits_nothing() {
|
||
|
|
let doc = Doc {
|
||
|
|
blocks: vec![itsybitsy_core::ir::Block::CardBreak { title: Some("W".into()) }],
|
||
|
|
first_h1: None,
|
||
|
|
};
|
||
|
|
let settings = PageSettings::default();
|
||
|
|
let ctx = RenderCtx { url: "/x", title: "T", settings: &settings, width: None };
|
||
|
|
let out = String::from_utf8(Html.render(&doc, &ctx).unwrap().body).unwrap();
|
||
|
|
assert!(out.contains("<body>\n</body>"), "{out}");
|
||
|
|
}
|
||
|
|
}
|