diff --git a/Cargo.lock b/Cargo.lock index 9547e3ac..b0371e78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,7 +444,7 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hayagriva" version = "0.1.1" -source = "git+https://github.com/typst/hayagriva#992389b23f9765198ee8d3f1818d6dbdc8f46b60" +source = "git+https://github.com/typst/hayagriva#754efb7e1034bcd4d4f1366e432197edbbfb9ed5" dependencies = [ "biblatex", "chrono", diff --git a/library/src/layout/hide.rs b/library/src/layout/hide.rs index e939e6c3..1d87d3e8 100644 --- a/library/src/layout/hide.rs +++ b/library/src/layout/hide.rs @@ -24,6 +24,6 @@ pub struct HideNode { impl Show for HideNode { fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult { - Ok(self.body().styled(MetaNode::set_data(vec![Meta::Hidden]))) + Ok(self.body().styled(MetaNode::set_data(vec![Meta::Hide]))) } } diff --git a/library/src/meta/bibliography.rs b/library/src/meta/bibliography.rs index 65bdafb6..8549624a 100644 --- a/library/src/meta/bibliography.rs +++ b/library/src/meta/bibliography.rs @@ -5,14 +5,14 @@ use std::sync::Arc; use ecow::EcoVec; use hayagriva::io::{BibLaTeXError, YamlBibliographyError}; -use hayagriva::style::{self, Citation, Database, DisplayString, Formatting}; -use typst::font::{FontStyle, FontWeight}; +use hayagriva::style::{self, Brackets, Citation, Database, DisplayString, Formatting}; +use hayagriva::Entry; use super::LocalName; -use crate::layout::{GridNode, ParNode, Sizing, TrackSizings, VNode}; +use crate::layout::{BlockNode, GridNode, ParNode, Sizing, TrackSizings, VNode}; use crate::meta::HeadingNode; use crate::prelude::*; -use crate::text::{Hyphenate, TextNode}; +use crate::text::TextNode; /// A bibliography / reference listing. /// @@ -48,7 +48,7 @@ pub struct BibliographyNode { impl BibliographyNode { /// Find the document's bibliography. pub fn find(introspector: Tracked) -> StrResult { - let mut iter = introspector.locate(Selector::node::()).into_iter(); + let mut iter = introspector.query(Selector::node::()).into_iter(); let Some(node) = iter.next() else { return Err("the document does not contain a bibliography".into()); }; @@ -63,7 +63,7 @@ impl BibliographyNode { /// Whether the bibliography contains the given key. pub fn has(vt: &Vt, key: &str) -> bool { vt.introspector - .locate(Selector::node::()) + .query(Selector::node::()) .into_iter() .flat_map(|node| load(vt.world(), &node.to::().unwrap().path())) .flatten() @@ -98,24 +98,19 @@ impl Synthesize for BibliographyNode { impl Show for BibliographyNode { fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult { const COLUMN_GUTTER: Em = Em::new(0.65); - const ROW_GUTTER: Em = Em::new(1.0); const INDENT: Em = Em::new(1.5); let works = match Works::new(vt) { Ok(works) => works, - Err(error) => { - if vt.locatable() { - bail!(self.span(), error) - } else { - return Ok(TextNode::packed("bibliography")); - } - } + Err(error) if vt.locatable() => bail!(self.span(), error), + Err(_) => Arc::new(Works::default()), }; let mut seq = vec![]; if let Some(title) = self.title(styles) { let title = title.clone().unwrap_or_else(|| { TextNode::packed(self.local_name(TextNode::lang_in(styles))) + .spanned(self.span()) }); seq.push( @@ -126,6 +121,7 @@ impl Show for BibliographyNode { ); } + let row_gutter = BlockNode::below_in(styles).amount(); if works.references.iter().any(|(prefix, _)| prefix.is_some()) { let mut cells = vec![]; for (prefix, reference) in &works.references { @@ -133,19 +129,18 @@ impl Show for BibliographyNode { cells.push(reference.clone()); } + seq.push(VNode::new(row_gutter).with_weakness(3).pack()); seq.push( GridNode::new(cells) .with_columns(TrackSizings(vec![Sizing::Auto; 2])) .with_column_gutter(TrackSizings(vec![COLUMN_GUTTER.into()])) - .with_row_gutter(TrackSizings(vec![ROW_GUTTER.into()])) + .with_row_gutter(TrackSizings(vec![row_gutter.into()])) .pack(), ); } else { let mut entries = vec![]; - for (i, (_, reference)) in works.references.iter().enumerate() { - if i > 0 { - entries.push(VNode::new(ROW_GUTTER.into()).with_weakness(1).pack()); - } + for (_, reference) in &works.references { + entries.push(VNode::new(row_gutter).with_weakness(3).pack()); entries.push(reference.clone()); } @@ -204,13 +199,17 @@ impl BibliographyStyle { #[node(Locatable, Synthesize, Show)] pub struct CiteNode { /// The citation key. - #[required] - pub key: EcoString, + #[variadic] + pub keys: Vec, /// A supplement for the citation such as page or chapter number. #[positional] pub supplement: Option, + /// Whether the citation should include brackets. + #[default(true)] + pub brackets: bool, + /// The citation style. /// /// When set to `{auto}`, automatically picks the preferred citation style @@ -221,6 +220,7 @@ pub struct CiteNode { impl Synthesize for CiteNode { fn synthesize(&mut self, _: &Vt, styles: StyleChain) { self.push_supplement(self.supplement(styles)); + self.push_brackets(self.brackets(styles)); self.push_style(self.style(styles)); } } @@ -230,17 +230,12 @@ impl Show for CiteNode { let id = self.0.stable_id().unwrap(); let works = match Works::new(vt) { Ok(works) => works, - Err(error) => { - if vt.locatable() { - bail!(self.span(), error) - } else { - return Ok(TextNode::packed("citation")); - } - } + Err(error) if vt.locatable() => bail!(self.span(), error), + Err(_) => Arc::new(Works::default()), }; let Some(citation) = works.citations.get(&id).cloned() else { - return Ok(TextNode::packed("citation")); + return Ok(TextNode::packed("[1]")); }; citation @@ -268,6 +263,7 @@ pub enum CitationStyle { } /// Fully formatted citations and references. +#[derive(Default)] pub struct Works { citations: HashMap>, references: Vec<(Option, Content)>, @@ -277,20 +273,8 @@ impl Works { /// Prepare all things need to cite a work or format a bibliography. pub fn new(vt: &Vt) -> StrResult> { let bibliography = BibliographyNode::find(vt.introspector)?; - let style = bibliography.style(StyleChain::default()); - let citations = vt - .locate_node::() - .map(|node| { - ( - node.0.stable_id().unwrap(), - node.key(), - node.supplement(StyleChain::default()), - node.style(StyleChain::default()) - .unwrap_or(style.default_citation_style()), - ) - }) - .collect(); - Ok(create(vt.world(), &bibliography.path(), style, citations)) + let citations = vt.query_node::().collect(); + Ok(create(vt.world(), &bibliography, citations)) } } @@ -298,21 +282,37 @@ impl Works { #[comemo::memoize] fn create( world: Tracked, - path: &str, - style: BibliographyStyle, - citations: Vec<(StableId, EcoString, Option, CitationStyle)>, + bibliography: &BibliographyNode, + citations: Vec<&CiteNode>, ) -> Arc { - let entries = load(world, path).unwrap(); + let entries = load(world, &bibliography.path()).unwrap(); + let style = bibliography.style(StyleChain::default()); + let bib_id = bibliography.0.stable_id().unwrap(); + let ref_id = |target: &Entry| { + let i = entries + .iter() + .position(|entry| entry.key() == target.key()) + .unwrap_or_default(); + bib_id.variant(i as u64) + }; let mut db = Database::new(); + let mut ids = HashMap::new(); let mut preliminary = vec![]; - for (id, key, supplement, style) in citations { - let entry = entries.iter().find(|entry| entry.key() == key); - if let Some(entry) = &entry { - db.push(entry); - } - preliminary.push((id, entry, supplement, style)); + for citation in citations { + let cite_id = citation.0.stable_id().unwrap(); + let entries = citation + .keys() + .into_iter() + .map(|key| { + let entry = entries.iter().find(|entry| entry.key() == key)?; + ids.entry(entry.key()).or_insert(cite_id); + db.push(entry); + Some(entry) + }) + .collect::>>(); + preliminary.push((citation, entries)); } let mut current = CitationStyle::Numerical; @@ -321,34 +321,71 @@ fn create( let citations = preliminary .into_iter() - .map(|(id, result, supplement, style)| { - let formatted = result.map(|entry| { - if style != current { - current = style; - citation_style = match style { - CitationStyle::Numerical => Box::new(style::Numerical::new()), - CitationStyle::Alphanumerical => { - Box::new(style::Alphanumerical::new()) - } - CitationStyle::AuthorDate => { - Box::new(style::ChicagoAuthorDate::new()) - } - CitationStyle::AuthorTitle => Box::new(style::AuthorTitle::new()), - CitationStyle::Keys => Box::new(style::Keys::new()), - }; + .map(|(citation, cited)| { + let id = citation.0.stable_id().unwrap(); + let Some(cited) = cited else { return (id, None) }; + + let mut supplement = citation.supplement(StyleChain::default()); + let brackets = citation.brackets(StyleChain::default()); + let style = citation + .style(StyleChain::default()) + .unwrap_or(style.default_citation_style()); + + if style != current { + current = style; + citation_style = match style { + CitationStyle::Numerical => Box::new(style::Numerical::new()), + CitationStyle::Alphanumerical => { + Box::new(style::Alphanumerical::new()) + } + CitationStyle::AuthorDate => { + Box::new(style::ChicagoAuthorDate::new()) + } + CitationStyle::AuthorTitle => Box::new(style::AuthorTitle::new()), + CitationStyle::Keys => Box::new(style::Keys::new()), + }; + } + + let len = cited.len(); + let mut content = Content::empty(); + for (i, entry) in cited.into_iter().enumerate() { + let supplement = if i + 1 == len { supplement.take() } else { None }; + let mut display = db + .citation( + &mut *citation_style, + &[Citation { + entry, + supplement: supplement.is_some().then(|| SUPPLEMENT), + }], + ) + .display; + + if brackets && len == 1 { + display = display.with_default_brackets(&*citation_style); } - let citation = db.citation( - &mut *citation_style, - &[Citation { - entry, - supplement: supplement.is_some().then(|| SUPPLEMENT), - }], - ); - let bracketed = citation.display.with_default_brackets(&*citation_style); - format_display_string(&bracketed, supplement) - }); - (id, formatted) + if i > 0 { + content += TextNode::packed(",\u{a0}"); + } + + // Format and link to the reference entry. + content += format_display_string(&display, supplement) + .linked(Link::Node(ref_id(entry))); + } + + if brackets && len > 1 { + content = match citation_style.brackets() { + Brackets::None => content, + Brackets::Round => { + TextNode::packed('(') + content + TextNode::packed(')') + } + Brackets::Square => { + TextNode::packed('[') + content + TextNode::packed(']') + } + }; + } + + (id, Some(content)) }) .collect(); @@ -363,11 +400,26 @@ fn create( .bibliography(&*bibliography_style, None) .into_iter() .map(|reference| { + // Make link from citation to here work. + let backlink = { + let mut content = Content::empty(); + content.set_stable_id(ref_id(&reference.entry)); + MetaNode::set_data(vec![Meta::Node(content)]) + }; + let prefix = reference.prefix.map(|prefix| { + // Format and link to first citation. let bracketed = prefix.with_default_brackets(&*citation_style); format_display_string(&bracketed, None) + .linked(Link::Node(ids[reference.entry.key()])) + .styled(backlink.clone()) }); - let reference = format_display_string(&reference.display, None); + + let mut reference = format_display_string(&reference.display, None); + if prefix.is_none() { + reference = reference.styled(backlink); + } + (prefix, reference) }) .collect(); @@ -443,28 +495,27 @@ fn format_display_string( continue; } - let mut styles = StyleMap::new(); - for (range, fmt) in &string.formatting { - if !range.contains(&start) { - continue; - } - - styles.set(match fmt { - Formatting::Bold => TextNode::set_weight(FontWeight::BOLD), - Formatting::Italic => TextNode::set_style(FontStyle::Italic), - Formatting::NoHyphenation => { - TextNode::set_hyphenate(Hyphenate(Smart::Custom(false))) - } - }); - } - - let content = if segment == SUPPLEMENT && supplement.is_some() { + let mut content = if segment == SUPPLEMENT && supplement.is_some() { supplement.take().unwrap_or_default() } else { TextNode::packed(segment) }; - seq.push(content.styled_with_map(styles)); + for (range, fmt) in &string.formatting { + if !range.contains(&start) { + continue; + } + + content = match fmt { + Formatting::Bold => content.strong(), + Formatting::Italic => content.emph(), + Formatting::Link(link) => { + content.linked(Link::Dest(Destination::Url(link.as_str().into()))) + } + }; + } + + seq.push(content); start = stop; } diff --git a/library/src/meta/figure.rs b/library/src/meta/figure.rs index 562e43bb..a7668ffb 100644 --- a/library/src/meta/figure.rs +++ b/library/src/meta/figure.rs @@ -66,7 +66,7 @@ impl Synthesize for FigureNode { if numbering.is_some() { number = NonZeroUsize::new( 1 + vt - .locate_node::() + .query_node::() .take_while(|figure| figure.0.stable_id() != my_id) .filter(|figure| figure.element() == element) .count(), diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs index 44a940f8..527a93a3 100644 --- a/library/src/meta/heading.rs +++ b/library/src/meta/heading.rs @@ -91,7 +91,7 @@ impl Synthesize for HeadingNode { if numbering.is_some() { // Advance past existing headings. for heading in vt - .locate_node::() + .query_node::() .take_while(|figure| figure.0.stable_id() != my_id) { if heading.numbering(StyleChain::default()).is_some() { diff --git a/library/src/meta/link.rs b/library/src/meta/link.rs index bca1945a..e9b8bcc6 100644 --- a/library/src/meta/link.rs +++ b/library/src/meta/link.rs @@ -86,7 +86,7 @@ impl Show for LinkNode { impl Finalize for LinkNode { fn finalize(&self, realized: Content, _: StyleChain) -> Content { realized - .linked(self.dest()) + .linked(Link::Dest(self.dest())) .styled(TextNode::set_hyphenate(Hyphenate(Smart::Custom(false)))) } } diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs index 216f7a90..1a3e8606 100644 --- a/library/src/meta/outline.rs +++ b/library/src/meta/outline.rs @@ -76,9 +76,13 @@ pub struct OutlineNode { impl Synthesize for OutlineNode { fn synthesize(&mut self, vt: &Vt, _: StyleChain) { let headings = vt - .locate_node::() - .filter(|node| node.outlined(StyleChain::default())) - .cloned() + .introspector + .query(Selector::Node( + NodeId::of::(), + Some(dict! { "outlined" => true }), + )) + .into_iter() + .map(|node| node.to::().unwrap().clone()) .collect(); self.push_headings(headings); @@ -91,6 +95,7 @@ impl Show for OutlineNode { if let Some(title) = self.title(styles) { let title = title.clone().unwrap_or_else(|| { TextNode::packed(self.local_name(TextNode::lang_in(styles))) + .spanned(self.span()) }); seq.push( @@ -107,6 +112,7 @@ impl Show for OutlineNode { let mut ancestors: Vec<&HeadingNode> = vec![]; for heading in self.headings().iter() { + let stable_id = heading.0.stable_id().unwrap(); if !heading.outlined(StyleChain::default()) { continue; } @@ -123,11 +129,6 @@ impl Show for OutlineNode { ancestors.pop(); } - // Adjust the link destination a bit to the topleft so that the - // heading is fully visible. - let mut loc = heading.0.expect_field::("location"); - loc.pos -= Point::splat(Abs::pt(10.0)); - // Add hidden ancestors numberings to realize the indent. if indent { let mut hidden = Content::empty(); @@ -155,7 +156,7 @@ impl Show for OutlineNode { }; // Add the numbering and section name. - seq.push(start.linked(Destination::Internal(loc))); + seq.push(start.linked(Link::Node(stable_id))); // Add filler symbols between the section name and page number. if let Some(filler) = self.fill(styles) { @@ -172,8 +173,9 @@ impl Show for OutlineNode { } // Add the page number and linebreak. - let end = TextNode::packed(eco_format!("{}", loc.page)); - seq.push(end.linked(Destination::Internal(loc))); + let page = vt.introspector.page(stable_id).unwrap(); + let end = TextNode::packed(eco_format!("{}", page)); + seq.push(end.linked(Link::Node(stable_id))); seq.push(LinebreakNode::new().pack()); ancestors.push(heading); } diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index e84da56b..1616adb3 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -68,14 +68,14 @@ impl Show for RefNode { let target = self.target(); let supplement = self.supplement(styles); - let matches: Vec<_> = vt.locate(Selector::Label(self.target())).collect(); + let matches = vt.introspector.query(Selector::Label(self.target())); if !vt.locatable() || BibliographyNode::has(vt, &target.0) { if !matches.is_empty() { bail!(self.span(), "label occurs in the document and its bibliography"); } - return Ok(CiteNode::new(target.0) + return Ok(CiteNode::new(vec![target.0]) .with_supplement(match supplement { Smart::Custom(Some(Supplement::Content(content))) => Some(content), _ => None, @@ -133,8 +133,7 @@ impl Show for RefNode { bail!(self.span(), "cannot reference {}", target.id().name); }; - let loc = target.expect_field::("location"); - Ok(formatted.linked(Destination::Internal(loc))) + Ok(formatted.linked(Link::Node(target.stable_id().unwrap()))) } } diff --git a/library/src/shared/ext.rs b/library/src/shared/ext.rs index e335b4c8..14674c9d 100644 --- a/library/src/shared/ext.rs +++ b/library/src/shared/ext.rs @@ -15,8 +15,8 @@ pub trait ContentExt { /// Underline this content. fn underlined(self) -> Self; - /// Link the content to a destination. - fn linked(self, dest: Destination) -> Self; + /// Link the content somewhere. + fn linked(self, link: Link) -> Self; /// Set alignments for this content. fn aligned(self, aligns: Axes>) -> Self; @@ -41,8 +41,8 @@ impl ContentExt for Content { UnderlineNode::new(self).pack() } - fn linked(self, dest: Destination) -> Self { - self.styled(MetaNode::set_data(vec![Meta::Link(dest)])) + fn linked(self, link: Link) -> Self { + self.styled(MetaNode::set_data(vec![Meta::Link(link)])) } fn aligned(self, aligns: Axes>) -> Self { diff --git a/src/doc.rs b/src/doc.rs index ffa056cd..6add64fc 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -14,7 +14,7 @@ use crate::geom::{ Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform, }; use crate::image::Image; -use crate::model::{node, Content, Fold, StyleChain}; +use crate::model::{node, Content, Fold, Introspector, StableId, StyleChain}; use crate::syntax::Span; /// A finished document with metadata and page frames. @@ -276,7 +276,7 @@ impl Frame { return; } for meta in MetaNode::data_in(styles) { - if matches!(meta, Meta::Hidden) { + if matches!(meta, Meta::Hide) { self.clear(); break; } @@ -593,13 +593,37 @@ cast_to_value! { /// Meta information that isn't visible or renderable. #[derive(Debug, Clone, Hash)] pub enum Meta { + /// Indicates that the content should be hidden. + Hide, /// An internal or external link. - Link(Destination), + Link(Link), /// An identifiable piece of content that produces something within the /// area this metadata is attached to. Node(Content), - /// Indicates that the content is hidden. - Hidden, +} + +/// A possibly unresolved link. +#[derive(Debug, Clone, Hash)] +pub enum Link { + /// A fully resolved. + Dest(Destination), + /// An unresolved link to a node. + Node(StableId), +} + +impl Link { + /// Resolve a destination. + /// + /// Needs to lazily provide an introspector. + pub fn resolve<'a>( + &self, + introspector: impl FnOnce() -> &'a Introspector, + ) -> Option { + match self { + Self::Dest(dest) => Some(dest.clone()), + Self::Node(id) => introspector().location(*id).map(Destination::Internal), + } + } } /// Host for metadata. diff --git a/src/export/pdf/mod.rs b/src/export/pdf/mod.rs index 3813bad5..bdbb2bb7 100644 --- a/src/export/pdf/mod.rs +++ b/src/export/pdf/mod.rs @@ -19,6 +19,7 @@ use crate::doc::{Document, Lang}; use crate::font::Font; use crate::geom::{Abs, Dir, Em}; use crate::image::Image; +use crate::model::Introspector; /// Export a document into a PDF file. /// @@ -40,6 +41,7 @@ const D65_GRAY: Name<'static> = Name(b"d65gray"); /// Context for exporting a whole PDF document. pub struct PdfContext<'a> { document: &'a Document, + introspector: Introspector, writer: PdfWriter, pages: Vec, page_heights: Vec, @@ -61,6 +63,7 @@ impl<'a> PdfContext<'a> { let page_tree_ref = alloc.bump(); Self { document, + introspector: Introspector::new(&document.pages), writer: PdfWriter::new(), pages: vec![], page_heights: vec![], diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs index 94af6c70..7f8c20ef 100644 --- a/src/export/pdf/page.rs +++ b/src/export/pdf/page.rs @@ -4,7 +4,7 @@ use pdf_writer::writers::ColorSpace; use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str}; use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB}; -use crate::doc::{Destination, Element, Frame, Group, Meta, Text}; +use crate::doc::{Destination, Element, Frame, Group, Link, Meta, Text}; use crate::font::Font; use crate::geom::{ self, Abs, Color, Em, Geometry, Numeric, Paint, Point, Ratio, Shape, Size, Stroke, @@ -110,22 +110,31 @@ fn write_page(ctx: &mut PdfContext, page: Page) { page_writer.contents(content_id); let mut annotations = page_writer.annotations(); - for (dest, rect) in page.links { - let mut link = annotations.push(); - link.subtype(AnnotationType::Link).rect(rect); - link.border(0.0, 0.0, 0.0, None); + for (link, rect) in page.links { + let mut annotation = annotations.push(); + annotation.subtype(AnnotationType::Link).rect(rect); + annotation.border(0.0, 0.0, 0.0, None); + + let dest = link.resolve(|| &ctx.introspector); + let Some(dest) = dest else { continue }; + match dest { Destination::Url(uri) => { - link.action().action_type(ActionType::Uri).uri(Str(uri.as_bytes())); + annotation + .action() + .action_type(ActionType::Uri) + .uri(Str(uri.as_bytes())); } Destination::Internal(loc) => { let index = loc.page.get() - 1; + let y = (loc.pos.y - Abs::pt(10.0)).max(Abs::zero()); if let Some(&height) = ctx.page_heights.get(index) { - link.action() + annotation + .action() .action_type(ActionType::GoTo) .destination_direct() .page(ctx.page_refs[index]) - .xyz(loc.pos.x.to_f32(), height - loc.pos.y.to_f32(), None); + .xyz(loc.pos.x.to_f32(), height - y.to_f32(), None); } } } @@ -148,7 +157,7 @@ pub struct Page { /// The page's content stream. pub content: Content, /// Links in the PDF coordinate system. - pub links: Vec<(Destination, Rect)>, + pub links: Vec<(Link, Rect)>, } /// An exporter for the contents of a single PDF page. @@ -159,7 +168,7 @@ struct PageContext<'a, 'b> { state: State, saves: Vec, bottom: f32, - links: Vec<(Destination, Rect)>, + links: Vec<(Link, Rect)>, } /// A simulated graphics state used to deduplicate graphics state changes and @@ -287,9 +296,9 @@ fn write_frame(ctx: &mut PageContext, frame: &Frame) { Element::Shape(shape) => write_shape(ctx, x, y, shape), Element::Image(image, size) => write_image(ctx, x, y, image, *size), Element::Meta(meta, size) => match meta { - Meta::Link(dest) => write_link(ctx, pos, dest, *size), + Meta::Link(link) => write_link(ctx, pos, link, *size), Meta::Node(_) => {} - Meta::Hidden => {} + Meta::Hide => {} }, } } @@ -449,7 +458,7 @@ fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size) } /// Save a link for later writing in the annotations dictionary. -fn write_link(ctx: &mut PageContext, pos: Point, dest: &Destination, size: Size) { +fn write_link(ctx: &mut PageContext, pos: Point, link: &Link, size: Size) { let mut min_x = Abs::inf(); let mut min_y = Abs::inf(); let mut max_x = -Abs::inf(); @@ -475,5 +484,5 @@ fn write_link(ctx: &mut PageContext, pos: Point, dest: &Destination, size: Size) let y2 = min_y.to_f32(); let rect = Rect::new(x1, y1, x2, y2); - ctx.links.push((dest.clone(), rect)); + ctx.links.push((link.clone(), rect)); } diff --git a/src/export/render.rs b/src/export/render.rs index 2fca8827..bf183ebe 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -61,7 +61,7 @@ fn render_frame( Element::Meta(meta, _) => match meta { Meta::Link(_) => {} Meta::Node(_) => {} - Meta::Hidden => {} + Meta::Hide => {} }, } } diff --git a/src/ide/analyze.rs b/src/ide/analyze.rs index 7338ba57..ed868e53 100644 --- a/src/ide/analyze.rs +++ b/src/ide/analyze.rs @@ -74,12 +74,11 @@ pub fn analyze_labels( frames: &[Frame], ) -> (Vec<(Label, Option)>, usize) { let mut output = vec![]; - let mut introspector = Introspector::new(); + let introspector = Introspector::new(frames); let items = &world.library().items; - introspector.update(frames); // Labels in the document. - for node in introspector.iter() { + for node in introspector.nodes() { let Some(label) = node.label() else { continue }; let details = node .field("caption") diff --git a/src/ide/jump.rs b/src/ide/jump.rs index 033d0f7f..95f2fa02 100644 --- a/src/ide/jump.rs +++ b/src/ide/jump.rs @@ -2,6 +2,7 @@ use std::num::NonZeroUsize; use crate::doc::{Destination, Element, Frame, Location, Meta}; use crate::geom::{Point, Size}; +use crate::model::Introspector; use crate::syntax::{LinkedNode, Source, SourceId, Span, SyntaxKind}; use crate::World; @@ -15,11 +16,19 @@ pub enum Jump { } /// Determine where to jump to based on a click in a frame. -pub fn jump_from_click(world: &dyn World, frame: &Frame, click: Point) -> Option { +pub fn jump_from_click( + world: &dyn World, + frames: &[Frame], + frame: &Frame, + click: Point, +) -> Option { + let mut introspector = None; + for (mut pos, element) in frame.elements() { if let Element::Group(group) = element { // TODO: Handle transformation. - if let Some(span) = jump_from_click(world, &group.frame, click - pos) { + if let Some(span) = jump_from_click(world, frames, &group.frame, click - pos) + { return Some(span); } } @@ -55,9 +64,14 @@ pub fn jump_from_click(world: &dyn World, frame: &Frame, click: Point) -> Option } } - if let Element::Meta(Meta::Link(dest), size) = element { + if let Element::Meta(Meta::Link(link), size) = element { if is_in_rect(pos, *size, click) { - return Some(Jump::Dest(dest.clone())); + let dest = link.resolve(|| { + introspector.get_or_insert_with(|| Introspector::new(frames)) + }); + + let Some(dest) = dest else { continue }; + return Some(Jump::Dest(dest)); } } } diff --git a/src/model/typeset.rs b/src/model/typeset.rs index 8120f58f..f68d337d 100644 --- a/src/model/typeset.rs +++ b/src/model/typeset.rs @@ -20,7 +20,7 @@ pub fn typeset(world: Tracked, content: &Content) -> SourceResult, content: &Content) -> SourceResult Vt<'a> { self.introspector.init() } - /// Locate all metadata matches for the given selector. - pub fn locate(&self, selector: Selector) -> impl Iterator { - self.introspector.locate(selector).into_iter() - } - /// Locate all metadata matches for the given node. - pub fn locate_node(&self) -> impl Iterator { - self.locate(Selector::node::()) + pub fn query_node(&self) -> impl Iterator { + self.introspector + .query(Selector::node::()) + .into_iter() .map(|content| content.to::().unwrap()) } } @@ -97,7 +92,14 @@ impl<'a> Vt<'a> { /// /// This struct is created by [`Vt::identify`]. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct StableId(u128, u64); +pub struct StableId(u128, u64, u64); + +impl StableId { + /// Produce a variant of this id. + pub fn variant(self, n: u64) -> Self { + Self(self.0, self.1, n) + } +} /// Provides stable identities to nodes. #[derive(Clone)] @@ -115,7 +117,7 @@ impl StabilityProvider { /// Produce a stable identifier for this call site. fn identify(&mut self, hash: u128) -> StableId { let slot = self.0.entry(hash).or_default(); - let id = StableId(hash, *slot); + let id = StableId(hash, *slot, 0); *slot += 1; id } @@ -124,35 +126,33 @@ impl StabilityProvider { /// Provides access to information about the document. pub struct Introspector { init: bool, - nodes: Vec, + nodes: Vec<(Content, Location)>, queries: RefCell>, } impl Introspector { /// Create a new introspector. - pub fn new() -> Self { - Self { + pub fn new(frames: &[Frame]) -> Self { + let mut introspector = Self { init: false, nodes: vec![], queries: RefCell::new(vec![]), - } + }; + introspector.extract_from_frames(frames); + introspector } /// Update the information given new frames and return whether we can stop /// layouting. pub fn update(&mut self, frames: &[Frame]) -> bool { self.nodes.clear(); - - for (i, frame) in frames.iter().enumerate() { - let page = NonZeroUsize::new(1 + i).unwrap(); - self.extract(frame, page, Transform::identity()); - } + self.extract_from_frames(frames); let was_init = std::mem::replace(&mut self.init, true); let queries = std::mem::take(&mut self.queries).into_inner(); for (selector, hash) in &queries { - let nodes = self.locate_impl(selector); + let nodes = self.query_impl(selector); if hash128(&nodes) != *hash { return false; } @@ -166,32 +166,36 @@ impl Introspector { } /// Iterate over all nodes. - pub fn iter(&self) -> impl Iterator { - self.nodes.iter() + pub fn nodes(&self) -> impl Iterator { + self.nodes.iter().map(|(node, _)| node) + } + + /// Extract metadata from frames. + fn extract_from_frames(&mut self, frames: &[Frame]) { + for (i, frame) in frames.iter().enumerate() { + let page = NonZeroUsize::new(1 + i).unwrap(); + self.extract_from_frame(frame, page, Transform::identity()); + } } /// Extract metadata from a frame. - fn extract(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) { + fn extract_from_frame(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) { for (pos, element) in frame.elements() { match element { Element::Group(group) => { let ts = ts .pre_concat(Transform::translate(pos.x, pos.y)) .pre_concat(group.transform); - self.extract(&group.frame, page, ts); + self.extract_from_frame(&group.frame, page, ts); } - Element::Meta(Meta::Node(content), _) => { + Element::Meta(Meta::Node(content), _) if !self .nodes .iter() - .any(|prev| prev.stable_id() == content.stable_id()) - { - let pos = pos.transform(ts); - let mut node = content.clone(); - let loc = Location { page, pos }; - node.push_field("location", loc); - self.nodes.push(node); - } + .any(|(prev, _)| prev.stable_id() == content.stable_id()) => + { + let pos = pos.transform(ts); + self.nodes.push((content.clone(), Location { page, pos })); } _ => {} } @@ -206,19 +210,29 @@ impl Introspector { self.init } - /// Locate all metadata matches for the given selector. - pub fn locate(&self, selector: Selector) -> Vec<&Content> { - let nodes = self.locate_impl(&selector); + /// Query for all metadata matches for the given selector. + pub fn query(&self, selector: Selector) -> Vec<&Content> { + let nodes = self.query_impl(&selector); let mut queries = self.queries.borrow_mut(); if !queries.iter().any(|(prev, _)| prev == &selector) { queries.push((selector, hash128(&nodes))); } nodes } + + /// Find the page number for the given stable id. + pub fn page(&self, id: StableId) -> Option { + Some(self.location(id)?.page) + } + + /// Find the location for the given stable id. + pub fn location(&self, id: StableId) -> Option { + Some(self.nodes.iter().find(|(node, _)| node.stable_id() == Some(id))?.1) + } } impl Introspector { - fn locate_impl(&self, selector: &Selector) -> Vec<&Content> { - self.nodes.iter().filter(|target| selector.matches(target)).collect() + fn query_impl(&self, selector: &Selector) -> Vec<&Content> { + self.nodes().filter(|node| selector.matches(node)).collect() } } diff --git a/tests/ref/meta/bibliography.png b/tests/ref/meta/bibliography.png index 3ff542d1..15b99ec0 100644 Binary files a/tests/ref/meta/bibliography.png and b/tests/ref/meta/bibliography.png differ diff --git a/tests/typ/meta/bibliography.typ b/tests/typ/meta/bibliography.typ index 2e2ddd35..ce4b8f31 100644 --- a/tests/typ/meta/bibliography.typ +++ b/tests/typ/meta/bibliography.typ @@ -14,7 +14,7 @@ --- #set page(width: 200pt) = Details -See also #cite("arrgh", [p. 22]), @arrgh[p. 4], and @cannonfodder[p. 5]. +See also #cite("arrgh", "cannonfodder", [p. 22]), @arrgh[p. 4], and @cannonfodder[p. 5]. #bibliography("/works.bib") --- @@ -22,6 +22,7 @@ See also #cite("arrgh", [p. 22]), @arrgh[p. 4], and @cannonfodder[p. 5]. #set page(width: 200pt) #bibliography("/works.bib", title: [Works to be cited], style: "author-date") #line(length: 100%) -The net-work is a creature of its own. @stupid +As described by #cite("stupid", brackets: false), +the net-work is a creature of its own. This is close to piratery! @arrgh And quark! @quark