feat: gate a run of blocks to chosen output formats
This commit is contained in:
parent
bb7a5111a1
commit
603f29b0f0
9 changed files with 437 additions and 22 deletions
|
|
@ -7,6 +7,8 @@
|
|||
//! | --- | --- |
|
||||
//! | `<!-- card Title -->` | a card divider; invisible to any other renderer |
|
||||
//! | `<!-- center -->` | alignment for the block that follows |
|
||||
//! | `<!-- only gemtext -->` … `<!-- end -->` | a run of blocks only these output formats get |
|
||||
//! | `<!-- except wml -->` … `<!-- end -->` | a run of blocks every other format gets |
|
||||
//! | `` | an image whose target is text, inlined verbatim |
|
||||
//!
|
||||
//! An HTML comment that is not a recognised directive stays a comment. Comments
|
||||
|
|
@ -37,19 +39,32 @@ pub fn apply(doc: &mut Doc, base: &Path, root: &Path) -> Result<(), Error> {
|
|||
fn rewrite(blocks: Vec<Block>, base: &Path, root: &Path) -> Result<Vec<Block>, Error> {
|
||||
let mut out = Vec::with_capacity(blocks.len());
|
||||
let mut pending: Option<Directive> = None;
|
||||
// Gates in the order they were opened. Each recursion gets its own, so a
|
||||
// gate opened inside a quote or a list item has to close inside it too.
|
||||
let mut gates: Vec<Gate> = Vec::new();
|
||||
|
||||
for block in blocks {
|
||||
if let Block::Html(html) = &block {
|
||||
match directive(html) {
|
||||
Some(Directive::Card { title }) => {
|
||||
out.push(Block::CardBreak { title });
|
||||
out.extend(gated(&gates, vec![Block::CardBreak { title }]));
|
||||
continue;
|
||||
}
|
||||
Some(align @ Directive::Align { .. }) => {
|
||||
pending = Some(align);
|
||||
continue;
|
||||
}
|
||||
None => {}
|
||||
Some(Directive::Only(gate)) => {
|
||||
gates.push(gate);
|
||||
continue;
|
||||
}
|
||||
Some(Directive::End) if !gates.is_empty() => {
|
||||
gates.pop();
|
||||
continue;
|
||||
}
|
||||
// A close with no gate open is just a comment, as is anything
|
||||
// else unrecognised.
|
||||
Some(Directive::End) | None => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,24 +85,68 @@ fn rewrite(blocks: Vec<Block>, base: &Path, root: &Path) -> Result<Vec<Block>, E
|
|||
other => vec![other],
|
||||
};
|
||||
|
||||
match pending.take() {
|
||||
Some(Directive::Align { align, margin }) => out.extend(produced.into_iter().map(|b| {
|
||||
// A block that already carries its own alignment keeps it: the
|
||||
// more specific marker wins over the one that precedes it.
|
||||
match b {
|
||||
aligned @ Block::Aligned { .. } => aligned,
|
||||
other => Block::Aligned { align, margin, block: Box::new(other) },
|
||||
}
|
||||
})),
|
||||
_ => out.extend(produced),
|
||||
}
|
||||
let aligned = match pending.take() {
|
||||
Some(Directive::Align { align, margin }) => produced
|
||||
.into_iter()
|
||||
.map(|b| {
|
||||
// A block that already carries its own alignment keeps it:
|
||||
// the more specific marker wins over the one before it.
|
||||
match b {
|
||||
aligned @ Block::Aligned { .. } => aligned,
|
||||
other => Block::Aligned { align, margin, block: Box::new(other) },
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
_ => produced,
|
||||
};
|
||||
out.extend(gated(&gates, aligned));
|
||||
}
|
||||
|
||||
if !gates.is_empty() {
|
||||
return Err(Error::directive(
|
||||
"an <!-- only --> or <!-- except --> gate is never closed; add <!-- end -->",
|
||||
));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Wrap each block in every open gate, the first opened outermost, so nested
|
||||
/// gates compose: a block has to satisfy all of them to survive filtering.
|
||||
fn gated(gates: &[Gate], blocks: Vec<Block>) -> Vec<Block> {
|
||||
if gates.is_empty() {
|
||||
return blocks;
|
||||
}
|
||||
blocks
|
||||
.into_iter()
|
||||
.map(|block| {
|
||||
gates.iter().rev().fold(block, |inner, gate| Block::Gated {
|
||||
formats: gate.formats.clone(),
|
||||
negated: gate.negated,
|
||||
block: Box::new(inner),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
enum Directive {
|
||||
Card { title: Option<String> },
|
||||
Align { align: Align, margin: Option<u16> },
|
||||
Card {
|
||||
title: Option<String>,
|
||||
},
|
||||
Align {
|
||||
align: Align,
|
||||
margin: Option<u16>,
|
||||
},
|
||||
/// Opens a run of blocks restricted to some output formats.
|
||||
Only(Gate),
|
||||
/// Closes the innermost open gate.
|
||||
End,
|
||||
}
|
||||
|
||||
/// An open gate's condition: format ids, and whether they are the formats to
|
||||
/// keep (`only`) or the ones to leave out (`except`).
|
||||
struct Gate {
|
||||
formats: Vec<String>,
|
||||
negated: bool,
|
||||
}
|
||||
|
||||
/// Recognise a directive in the text of an HTML block, or `None` for an ordinary
|
||||
|
|
@ -100,11 +159,31 @@ fn directive(html: &str) -> Option<Directive> {
|
|||
let title = words.collect::<Vec<_>>().join(" ");
|
||||
return Some(Directive::Card { title: (!title.is_empty()).then_some(title) });
|
||||
}
|
||||
if let Some(negated) = gate_named(name) {
|
||||
// A gate naming no format would hide or reveal everything by accident,
|
||||
// so an argument-less one stays a comment.
|
||||
let formats = words.map(str::to_string).collect::<Vec<_>>();
|
||||
return (!formats.is_empty()).then_some(Directive::Only(Gate { formats, negated }));
|
||||
}
|
||||
if name == "end" {
|
||||
// `<!-- end of the list -->` is prose, not a close.
|
||||
return words.next().is_none().then_some(Directive::End);
|
||||
}
|
||||
let align = align_named(name)?;
|
||||
let margin = words.find_map(|word| word.strip_prefix("margin=")?.parse().ok());
|
||||
Some(Directive::Align { align, margin })
|
||||
}
|
||||
|
||||
/// Whether a name opens a gate, and if so whether it names the formats to leave
|
||||
/// out rather than the ones to keep.
|
||||
fn gate_named(name: &str) -> Option<bool> {
|
||||
match name {
|
||||
"only" => Some(false),
|
||||
"except" => Some(true),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn align_named(name: &str) -> Option<Align> {
|
||||
match name {
|
||||
"left" => Some(Align::Left),
|
||||
|
|
@ -307,6 +386,128 @@ mod tests {
|
|||
assert_eq!(doc.blocks.len(), 1);
|
||||
}
|
||||
|
||||
// -- Format gates ------------------------------------------------------
|
||||
|
||||
fn gate(formats: &[&str], negated: bool, block: Block) -> Block {
|
||||
Block::Gated {
|
||||
formats: formats.iter().map(|id| id.to_string()).collect(),
|
||||
negated,
|
||||
block: Box::new(block),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gate_wraps_every_block_of_its_run() {
|
||||
let tree = Tree::new();
|
||||
let doc = tree.doc("<!-- only html -->\n\nA.\n\nB.\n\n<!-- end -->\n\nC.\n").unwrap();
|
||||
assert_eq!(
|
||||
doc.blocks,
|
||||
vec![
|
||||
gate(&["html"], false, para("A.")),
|
||||
gate(&["html"], false, para("B.")),
|
||||
// Past the close the run is over.
|
||||
para("C."),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_except_gate_names_the_formats_to_leave_out() {
|
||||
let tree = Tree::new();
|
||||
let doc = tree.doc("<!-- except wml text -->\nA.\n<!-- end -->\n").unwrap();
|
||||
assert_eq!(doc.blocks, vec![gate(&["wml", "text"], true, para("A."))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_gates_compose_with_the_first_opened_outermost() {
|
||||
let tree = Tree::new();
|
||||
let doc = tree
|
||||
.doc("<!-- only html xhtmlmp -->\n<!-- except xhtmlmp -->\nA.\n<!-- end -->\n<!-- end -->\n")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
doc.blocks,
|
||||
vec![gate(&["html", "xhtmlmp"], false, gate(&["xhtmlmp"], true, para("A.")))]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_card_divider_inside_a_gate_is_gated_too() {
|
||||
// Otherwise WML would paginate at a divider meant for another format.
|
||||
let tree = Tree::new();
|
||||
let doc = tree.doc("<!-- only wml -->\n<!-- card Next -->\n<!-- end -->\n").unwrap();
|
||||
assert_eq!(
|
||||
doc.blocks,
|
||||
vec![gate(&["wml"], false, Block::CardBreak { title: Some("Next".into()) })]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_alignment_before_a_gate_ends_up_inside_it() {
|
||||
let tree = Tree::new();
|
||||
let doc = tree.doc("<!-- center -->\n<!-- only text -->\nA.\n<!-- end -->\n").unwrap();
|
||||
assert_eq!(
|
||||
doc.blocks,
|
||||
vec![gate(
|
||||
&["text"],
|
||||
false,
|
||||
Block::Aligned { align: Align::Center, margin: None, block: Box::new(para("A.")) },
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gate_may_be_opened_inside_a_quote_or_a_list_item() {
|
||||
let tree = Tree::new();
|
||||
let doc = tree
|
||||
.doc("> <!-- only text -->\n> A.\n> <!-- end -->\n\n- <!-- only text -->\n B.\n <!-- end -->\n")
|
||||
.unwrap();
|
||||
let Block::BlockQuote(inner) = &doc.blocks[0] else { panic!("expected a quote") };
|
||||
assert_eq!(inner, &vec![gate(&["text"], false, para("A."))]);
|
||||
let Block::List { items, .. } = &doc.blocks[1] else { panic!("expected a list") };
|
||||
assert_eq!(items[0], vec![gate(&["text"], false, para("B."))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gate_left_open_is_refused() {
|
||||
// Running it silently to the end of the document would hide the rest of
|
||||
// the page from some formats with nothing to notice it by.
|
||||
let tree = Tree::new();
|
||||
let err = tree.doc("<!-- only html -->\nA.\n").unwrap_err();
|
||||
assert!(err.to_string().contains("never closed"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gate_must_close_inside_the_quote_it_was_opened_in() {
|
||||
let tree = Tree::new();
|
||||
assert!(tree.doc("> <!-- only html -->\n> A.\n\n<!-- end -->\n").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_close_with_nothing_open_stays_a_comment() {
|
||||
let tree = Tree::new();
|
||||
let doc = tree.doc("<!-- end -->\nA.\n").unwrap();
|
||||
assert!(matches!(&doc.blocks[0], Block::Html(html) if html.contains("end")));
|
||||
assert_eq!(doc.blocks[1], para("A."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gate_naming_no_format_stays_a_comment() {
|
||||
// It would otherwise hide or reveal everything by accident.
|
||||
let tree = Tree::new();
|
||||
let doc = tree.doc("<!-- only -->\nA.\n").unwrap();
|
||||
assert!(matches!(&doc.blocks[0], Block::Html(_)));
|
||||
assert_eq!(doc.blocks[1], para("A."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_close_carrying_prose_stays_a_comment() {
|
||||
let tree = Tree::new();
|
||||
let doc =
|
||||
tree.doc("<!-- only html -->\nA.\n<!-- end of the gate -->\n<!-- end -->\n").unwrap();
|
||||
let Block::Gated { block, .. } = &doc.blocks[1] else { panic!("expected a gated block") };
|
||||
assert!(matches!(block.as_ref(), Block::Html(html) if html.contains("end of the gate")));
|
||||
}
|
||||
|
||||
// -- Art ---------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ pub enum Error {
|
|||
/// stack overflow aborts the process rather than unwinding.
|
||||
TooDeep { limit: usize },
|
||||
|
||||
/// A directive the parser could read but not complete, such as an
|
||||
/// `<!-- only ... -->` gate that is never closed. Refused rather than
|
||||
/// guessed, because the guess would hide content from some formats and show
|
||||
/// it on others with nothing to notice it by.
|
||||
Directive { message: String },
|
||||
|
||||
/// An include or ASCII-art target cannot be used. `path` is where the
|
||||
/// directive actually pointed, resolved, which is the thing an author needs
|
||||
/// to see when a relative target is wrong.
|
||||
|
|
@ -76,6 +82,7 @@ impl fmt::Display for Error {
|
|||
Error::TooDeep { limit } => {
|
||||
write!(f, "document nests more than {limit} levels deep")
|
||||
}
|
||||
Error::Directive { message } => write!(f, "{message}"),
|
||||
Error::Include { path, reason } => {
|
||||
write!(f, "include target {} {reason}", path.display())
|
||||
}
|
||||
|
|
@ -103,4 +110,8 @@ impl Error {
|
|||
pub(crate) fn config(message: impl Into<String>) -> Self {
|
||||
Error::Config { message: message.into() }
|
||||
}
|
||||
|
||||
pub(crate) fn directive(message: impl Into<String>) -> Self {
|
||||
Error::Directive { message: message.into() }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
164
core/src/ir.rs
164
core/src/ir.rs
|
|
@ -9,6 +9,8 @@
|
|||
//! the consumers want: WML packs a linear run of blocks into byte-budgeted
|
||||
//! cards, and the text renderers are a fold over blocks.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// A parsed document.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Doc {
|
||||
|
|
@ -63,6 +65,16 @@ pub enum Block {
|
|||
margin: Option<u16>,
|
||||
block: Box<Block>,
|
||||
},
|
||||
/// A block an `<!-- only ... -->` or `<!-- except ... -->` run restricted to
|
||||
/// some output formats. Resolved by [`Doc::for_format`] before rendering, so
|
||||
/// no renderer meets one; it wraps rather than being a field for the same
|
||||
/// reason `Aligned` does, and the two nest in either order.
|
||||
Gated {
|
||||
formats: Vec<String>,
|
||||
/// `true` for `except`: keep the block for every format *but* these.
|
||||
negated: bool,
|
||||
block: Box<Block>,
|
||||
},
|
||||
/// Raw block HTML. Kept rather than dropped so XHTML-MP can pass it through
|
||||
/// and the text formats can strip it, instead of the parser deciding.
|
||||
Html(String),
|
||||
|
|
@ -107,6 +119,19 @@ impl Doc {
|
|||
out
|
||||
}
|
||||
|
||||
/// This document as one output format sees it: a block gated to other
|
||||
/// formats is dropped and a gate that passes is unwrapped.
|
||||
///
|
||||
/// Borrowed unchanged when there are no gates, which is the ordinary page.
|
||||
/// `first_h1` is kept whatever happens: the title is resolved once per page,
|
||||
/// so a heading inside a gate still names the page on every format.
|
||||
pub fn for_format(&self, format: &str) -> Cow<'_, Doc> {
|
||||
if !any_gated(&self.blocks) {
|
||||
return Cow::Borrowed(self);
|
||||
}
|
||||
Cow::Owned(Doc { blocks: retain(&self.blocks, format), first_h1: self.first_h1.clone() })
|
||||
}
|
||||
|
||||
fn write_plain(inline: &[Inline], out: &mut String) {
|
||||
for item in inline {
|
||||
match item {
|
||||
|
|
@ -124,10 +149,149 @@ impl Doc {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether a gate appears anywhere in a run of blocks, including inside the
|
||||
/// wrappers a gate can be written in.
|
||||
fn any_gated(blocks: &[Block]) -> bool {
|
||||
blocks.iter().any(|block| match block {
|
||||
Block::Gated { .. } => true,
|
||||
Block::Aligned { block, .. } => any_gated(std::slice::from_ref(block)),
|
||||
Block::BlockQuote(inner) => any_gated(inner),
|
||||
Block::List { items, .. } => items.iter().any(|item| any_gated(item)),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn retain(blocks: &[Block], format: &str) -> Vec<Block> {
|
||||
blocks.iter().filter_map(|block| keep(block, format)).collect()
|
||||
}
|
||||
|
||||
/// One block as `format` sees it, or `None` if a gate excludes it.
|
||||
fn keep(block: &Block, format: &str) -> Option<Block> {
|
||||
match block {
|
||||
Block::Gated { formats, negated, block } => {
|
||||
let named = formats.iter().any(|id| id == format);
|
||||
// `only` keeps the formats it names, `except` keeps all the others.
|
||||
(named != *negated).then(|| keep(block, format)).flatten()
|
||||
}
|
||||
Block::Aligned { align, margin, block } => Some(Block::Aligned {
|
||||
align: *align,
|
||||
margin: *margin,
|
||||
block: Box::new(keep(block, format)?),
|
||||
}),
|
||||
Block::BlockQuote(inner) => Some(Block::BlockQuote(retain(inner, format))),
|
||||
Block::List { ordered, start, items } => Some(Block::List {
|
||||
ordered: *ordered,
|
||||
start: *start,
|
||||
items: items.iter().map(|item| retain(item, format)).collect(),
|
||||
}),
|
||||
other => Some(other.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn para(text: &str) -> Block {
|
||||
Block::Paragraph(vec![Inline::Text(text.into())])
|
||||
}
|
||||
|
||||
fn gate(formats: &[&str], negated: bool, block: Block) -> Block {
|
||||
Block::Gated {
|
||||
formats: formats.iter().map(|id| id.to_string()).collect(),
|
||||
negated,
|
||||
block: Box::new(block),
|
||||
}
|
||||
}
|
||||
|
||||
fn doc(blocks: Vec<Block>) -> Doc {
|
||||
Doc { blocks, first_h1: None }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_document_without_gates_is_borrowed_unchanged() {
|
||||
let source = doc(vec![para("a")]);
|
||||
assert!(matches!(source.for_format("html"), Cow::Borrowed(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_only_gate_keeps_the_formats_it_names_and_drops_the_rest() {
|
||||
let source = doc(vec![gate(&["html", "wml"], false, para("a")), para("b")]);
|
||||
assert_eq!(source.for_format("html").blocks, vec![para("a"), para("b")]);
|
||||
assert_eq!(source.for_format("gemtext").blocks, vec![para("b")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_except_gate_drops_the_formats_it_names() {
|
||||
let source = doc(vec![gate(&["wml"], true, para("a"))]);
|
||||
assert_eq!(source.for_format("wml").blocks, Vec::new());
|
||||
assert_eq!(source.for_format("html").blocks, vec![para("a")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_gates_must_both_pass() {
|
||||
let source = doc(vec![gate(&["html", "wml"], false, gate(&["wml"], true, para("a")))]);
|
||||
assert_eq!(source.for_format("html").blocks, vec![para("a")]);
|
||||
assert_eq!(source.for_format("wml").blocks, Vec::new());
|
||||
assert_eq!(source.for_format("text").blocks, Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_format_no_gate_names_is_simply_not_named() {
|
||||
// A build without the wml feature therefore hides an `only wml` run,
|
||||
// which is the answer that run asked for.
|
||||
let source = doc(vec![gate(&["wml"], false, para("a"))]);
|
||||
assert_eq!(source.for_format("gemtext").blocks, Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gates_are_resolved_inside_quotes_lists_and_alignment() {
|
||||
let aligned = Block::Aligned {
|
||||
align: Align::Center,
|
||||
margin: None,
|
||||
block: Box::new(gate(&["html"], false, para("c"))),
|
||||
};
|
||||
let source = doc(vec![
|
||||
Block::BlockQuote(vec![gate(&["html"], false, para("a")), para("b")]),
|
||||
Block::List {
|
||||
ordered: false,
|
||||
start: 1,
|
||||
items: vec![vec![gate(&["html"], false, para("x"))], vec![para("y")]],
|
||||
},
|
||||
aligned,
|
||||
]);
|
||||
|
||||
let kept = source.for_format("html");
|
||||
assert_eq!(kept.blocks[0], Block::BlockQuote(vec![para("a"), para("b")]));
|
||||
let Block::List { items, .. } = &kept.blocks[1] else { panic!("expected a list") };
|
||||
assert_eq!(items, &vec![vec![para("x")], vec![para("y")]]);
|
||||
assert_eq!(
|
||||
kept.blocks[2],
|
||||
Block::Aligned { align: Align::Center, margin: None, block: Box::new(para("c")) }
|
||||
);
|
||||
|
||||
let dropped = source.for_format("text");
|
||||
assert_eq!(dropped.blocks[0], Block::BlockQuote(vec![para("b")]));
|
||||
let Block::List { items, .. } = &dropped.blocks[1] else { panic!("expected a list") };
|
||||
assert_eq!(items, &vec![Vec::new(), vec![para("y")]]);
|
||||
// An alignment wrapping nothing that survives goes with it.
|
||||
assert_eq!(dropped.blocks.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_title_survives_a_gate_around_the_heading() {
|
||||
// One page, one title, whichever format asks for it.
|
||||
let source = Doc {
|
||||
blocks: vec![gate(
|
||||
&["html"],
|
||||
false,
|
||||
Block::Heading { level: 1, inline: vec![Inline::Text("T".into())] },
|
||||
)],
|
||||
first_h1: Some("T".into()),
|
||||
};
|
||||
assert_eq!(source.for_format("gemtext").first_h1.as_deref(), Some("T"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_text_flattens_markup_and_keeps_link_labels() {
|
||||
let inline = vec![
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
//! formats' `[n]` references must agree about link identity and order, which
|
||||
//! cannot be relied on when each format parses the source for itself.
|
||||
//!
|
||||
//! A renderer must not open files or sockets. Includes and art are already
|
||||
//! resolved by the time it runs, which is what keeps the root-containment check
|
||||
//! in one place rather than in every format.
|
||||
//! A renderer must not open files or sockets. Includes, art and format gates are
|
||||
//! already resolved by the time it runs, which is what keeps the
|
||||
//! root-containment check in one place rather than in every format.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -133,7 +133,10 @@ impl Registry {
|
|||
})?;
|
||||
let ctx =
|
||||
RenderCtx { url, title: &page.title, settings, width: renderer.default_width() };
|
||||
let rendered = renderer.render(doc, &ctx)?;
|
||||
// Format gates are resolved here, so no renderer meets one and a
|
||||
// gated run costs nothing extra in the cache: bodies are already
|
||||
// kept per format.
|
||||
let rendered = renderer.render(&doc.for_format(id), &ctx)?;
|
||||
for part in rendered.parts {
|
||||
page.parts.insert((id.clone(), part.slug), part.body);
|
||||
}
|
||||
|
|
@ -202,9 +205,12 @@ mod tests {
|
|||
self.width
|
||||
}
|
||||
|
||||
fn render(&self, _doc: &Doc, ctx: &RenderCtx<'_>) -> Result<Rendered, Error> {
|
||||
fn render(&self, doc: &Doc, ctx: &RenderCtx<'_>) -> Result<Rendered, Error> {
|
||||
Ok(Rendered::body(
|
||||
format!("{} {} {:?} {}", self.id, ctx.url, ctx.width, ctx.title).into_bytes(),
|
||||
// The blocks are printed too, so what a format was handed —
|
||||
// after gates — is visible in the body.
|
||||
format!("{} {} {:?} {} {:?}", self.id, ctx.url, ctx.width, ctx.title, doc.blocks)
|
||||
.into_bytes(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -264,6 +270,23 @@ mod tests {
|
|||
assert_eq!(from_stem.unwrap().title, "stem");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gated_block_reaches_only_the_formats_it_names() {
|
||||
let gated = Doc {
|
||||
blocks: vec![Block::Gated {
|
||||
formats: vec!["one".to_string()],
|
||||
negated: false,
|
||||
block: Box::new(Block::Paragraph(vec![Inline::Text("secret".into())])),
|
||||
}],
|
||||
first_h1: None,
|
||||
};
|
||||
let formats = vec!["one".to_string(), "two".to_string()];
|
||||
let page = registry().page(&formats, &gated, "/x", &PageSettings::default(), "x").unwrap();
|
||||
// The stub prints the blocks it was handed, so presence is visible.
|
||||
assert!(String::from_utf8_lossy(page.body("one").unwrap()).contains("secret"));
|
||||
assert!(!String::from_utf8_lossy(page.body("two").unwrap()).contains("secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_format_is_a_bug_not_bad_input() {
|
||||
let err = registry()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue