mirror of
https://github.com/typst/typst
synced 2025-05-13 12:36:23 +08:00
Node links
This commit is contained in:
parent
8567811808
commit
ecb5543985
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -444,7 +444,7 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "hayagriva"
|
name = "hayagriva"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "git+https://github.com/typst/hayagriva#992389b23f9765198ee8d3f1818d6dbdc8f46b60"
|
source = "git+https://github.com/typst/hayagriva#754efb7e1034bcd4d4f1366e432197edbbfb9ed5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"biblatex",
|
"biblatex",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -24,6 +24,6 @@ pub struct HideNode {
|
|||||||
|
|
||||||
impl Show for HideNode {
|
impl Show for HideNode {
|
||||||
fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
|
fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
|
||||||
Ok(self.body().styled(MetaNode::set_data(vec![Meta::Hidden])))
|
Ok(self.body().styled(MetaNode::set_data(vec![Meta::Hide])))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,14 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use ecow::EcoVec;
|
use ecow::EcoVec;
|
||||||
use hayagriva::io::{BibLaTeXError, YamlBibliographyError};
|
use hayagriva::io::{BibLaTeXError, YamlBibliographyError};
|
||||||
use hayagriva::style::{self, Citation, Database, DisplayString, Formatting};
|
use hayagriva::style::{self, Brackets, Citation, Database, DisplayString, Formatting};
|
||||||
use typst::font::{FontStyle, FontWeight};
|
use hayagriva::Entry;
|
||||||
|
|
||||||
use super::LocalName;
|
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::meta::HeadingNode;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::text::{Hyphenate, TextNode};
|
use crate::text::TextNode;
|
||||||
|
|
||||||
/// A bibliography / reference listing.
|
/// A bibliography / reference listing.
|
||||||
///
|
///
|
||||||
@ -48,7 +48,7 @@ pub struct BibliographyNode {
|
|||||||
impl BibliographyNode {
|
impl BibliographyNode {
|
||||||
/// Find the document's bibliography.
|
/// Find the document's bibliography.
|
||||||
pub fn find(introspector: Tracked<Introspector>) -> StrResult<Self> {
|
pub fn find(introspector: Tracked<Introspector>) -> StrResult<Self> {
|
||||||
let mut iter = introspector.locate(Selector::node::<Self>()).into_iter();
|
let mut iter = introspector.query(Selector::node::<Self>()).into_iter();
|
||||||
let Some(node) = iter.next() else {
|
let Some(node) = iter.next() else {
|
||||||
return Err("the document does not contain a bibliography".into());
|
return Err("the document does not contain a bibliography".into());
|
||||||
};
|
};
|
||||||
@ -63,7 +63,7 @@ impl BibliographyNode {
|
|||||||
/// Whether the bibliography contains the given key.
|
/// Whether the bibliography contains the given key.
|
||||||
pub fn has(vt: &Vt, key: &str) -> bool {
|
pub fn has(vt: &Vt, key: &str) -> bool {
|
||||||
vt.introspector
|
vt.introspector
|
||||||
.locate(Selector::node::<Self>())
|
.query(Selector::node::<Self>())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|node| load(vt.world(), &node.to::<Self>().unwrap().path()))
|
.flat_map(|node| load(vt.world(), &node.to::<Self>().unwrap().path()))
|
||||||
.flatten()
|
.flatten()
|
||||||
@ -98,24 +98,19 @@ impl Synthesize for BibliographyNode {
|
|||||||
impl Show for BibliographyNode {
|
impl Show for BibliographyNode {
|
||||||
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
|
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
|
||||||
const COLUMN_GUTTER: Em = Em::new(0.65);
|
const COLUMN_GUTTER: Em = Em::new(0.65);
|
||||||
const ROW_GUTTER: Em = Em::new(1.0);
|
|
||||||
const INDENT: Em = Em::new(1.5);
|
const INDENT: Em = Em::new(1.5);
|
||||||
|
|
||||||
let works = match Works::new(vt) {
|
let works = match Works::new(vt) {
|
||||||
Ok(works) => works,
|
Ok(works) => works,
|
||||||
Err(error) => {
|
Err(error) if vt.locatable() => bail!(self.span(), error),
|
||||||
if vt.locatable() {
|
Err(_) => Arc::new(Works::default()),
|
||||||
bail!(self.span(), error)
|
|
||||||
} else {
|
|
||||||
return Ok(TextNode::packed("bibliography"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut seq = vec![];
|
let mut seq = vec![];
|
||||||
if let Some(title) = self.title(styles) {
|
if let Some(title) = self.title(styles) {
|
||||||
let title = title.clone().unwrap_or_else(|| {
|
let title = title.clone().unwrap_or_else(|| {
|
||||||
TextNode::packed(self.local_name(TextNode::lang_in(styles)))
|
TextNode::packed(self.local_name(TextNode::lang_in(styles)))
|
||||||
|
.spanned(self.span())
|
||||||
});
|
});
|
||||||
|
|
||||||
seq.push(
|
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()) {
|
if works.references.iter().any(|(prefix, _)| prefix.is_some()) {
|
||||||
let mut cells = vec![];
|
let mut cells = vec![];
|
||||||
for (prefix, reference) in &works.references {
|
for (prefix, reference) in &works.references {
|
||||||
@ -133,19 +129,18 @@ impl Show for BibliographyNode {
|
|||||||
cells.push(reference.clone());
|
cells.push(reference.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
seq.push(VNode::new(row_gutter).with_weakness(3).pack());
|
||||||
seq.push(
|
seq.push(
|
||||||
GridNode::new(cells)
|
GridNode::new(cells)
|
||||||
.with_columns(TrackSizings(vec![Sizing::Auto; 2]))
|
.with_columns(TrackSizings(vec![Sizing::Auto; 2]))
|
||||||
.with_column_gutter(TrackSizings(vec![COLUMN_GUTTER.into()]))
|
.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(),
|
.pack(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let mut entries = vec![];
|
let mut entries = vec![];
|
||||||
for (i, (_, reference)) in works.references.iter().enumerate() {
|
for (_, reference) in &works.references {
|
||||||
if i > 0 {
|
entries.push(VNode::new(row_gutter).with_weakness(3).pack());
|
||||||
entries.push(VNode::new(ROW_GUTTER.into()).with_weakness(1).pack());
|
|
||||||
}
|
|
||||||
entries.push(reference.clone());
|
entries.push(reference.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,13 +199,17 @@ impl BibliographyStyle {
|
|||||||
#[node(Locatable, Synthesize, Show)]
|
#[node(Locatable, Synthesize, Show)]
|
||||||
pub struct CiteNode {
|
pub struct CiteNode {
|
||||||
/// The citation key.
|
/// The citation key.
|
||||||
#[required]
|
#[variadic]
|
||||||
pub key: EcoString,
|
pub keys: Vec<EcoString>,
|
||||||
|
|
||||||
/// A supplement for the citation such as page or chapter number.
|
/// A supplement for the citation such as page or chapter number.
|
||||||
#[positional]
|
#[positional]
|
||||||
pub supplement: Option<Content>,
|
pub supplement: Option<Content>,
|
||||||
|
|
||||||
|
/// Whether the citation should include brackets.
|
||||||
|
#[default(true)]
|
||||||
|
pub brackets: bool,
|
||||||
|
|
||||||
/// The citation style.
|
/// The citation style.
|
||||||
///
|
///
|
||||||
/// When set to `{auto}`, automatically picks the preferred citation style
|
/// When set to `{auto}`, automatically picks the preferred citation style
|
||||||
@ -221,6 +220,7 @@ pub struct CiteNode {
|
|||||||
impl Synthesize for CiteNode {
|
impl Synthesize for CiteNode {
|
||||||
fn synthesize(&mut self, _: &Vt, styles: StyleChain) {
|
fn synthesize(&mut self, _: &Vt, styles: StyleChain) {
|
||||||
self.push_supplement(self.supplement(styles));
|
self.push_supplement(self.supplement(styles));
|
||||||
|
self.push_brackets(self.brackets(styles));
|
||||||
self.push_style(self.style(styles));
|
self.push_style(self.style(styles));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,17 +230,12 @@ impl Show for CiteNode {
|
|||||||
let id = self.0.stable_id().unwrap();
|
let id = self.0.stable_id().unwrap();
|
||||||
let works = match Works::new(vt) {
|
let works = match Works::new(vt) {
|
||||||
Ok(works) => works,
|
Ok(works) => works,
|
||||||
Err(error) => {
|
Err(error) if vt.locatable() => bail!(self.span(), error),
|
||||||
if vt.locatable() {
|
Err(_) => Arc::new(Works::default()),
|
||||||
bail!(self.span(), error)
|
|
||||||
} else {
|
|
||||||
return Ok(TextNode::packed("citation"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(citation) = works.citations.get(&id).cloned() else {
|
let Some(citation) = works.citations.get(&id).cloned() else {
|
||||||
return Ok(TextNode::packed("citation"));
|
return Ok(TextNode::packed("[1]"));
|
||||||
};
|
};
|
||||||
|
|
||||||
citation
|
citation
|
||||||
@ -268,6 +263,7 @@ pub enum CitationStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fully formatted citations and references.
|
/// Fully formatted citations and references.
|
||||||
|
#[derive(Default)]
|
||||||
pub struct Works {
|
pub struct Works {
|
||||||
citations: HashMap<StableId, Option<Content>>,
|
citations: HashMap<StableId, Option<Content>>,
|
||||||
references: Vec<(Option<Content>, Content)>,
|
references: Vec<(Option<Content>, Content)>,
|
||||||
@ -277,20 +273,8 @@ impl Works {
|
|||||||
/// Prepare all things need to cite a work or format a bibliography.
|
/// Prepare all things need to cite a work or format a bibliography.
|
||||||
pub fn new(vt: &Vt) -> StrResult<Arc<Self>> {
|
pub fn new(vt: &Vt) -> StrResult<Arc<Self>> {
|
||||||
let bibliography = BibliographyNode::find(vt.introspector)?;
|
let bibliography = BibliographyNode::find(vt.introspector)?;
|
||||||
let style = bibliography.style(StyleChain::default());
|
let citations = vt.query_node::<CiteNode>().collect();
|
||||||
let citations = vt
|
Ok(create(vt.world(), &bibliography, citations))
|
||||||
.locate_node::<CiteNode>()
|
|
||||||
.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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,21 +282,37 @@ impl Works {
|
|||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
fn create(
|
fn create(
|
||||||
world: Tracked<dyn World>,
|
world: Tracked<dyn World>,
|
||||||
path: &str,
|
bibliography: &BibliographyNode,
|
||||||
style: BibliographyStyle,
|
citations: Vec<&CiteNode>,
|
||||||
citations: Vec<(StableId, EcoString, Option<Content>, CitationStyle)>,
|
|
||||||
) -> Arc<Works> {
|
) -> Arc<Works> {
|
||||||
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 db = Database::new();
|
||||||
|
let mut ids = HashMap::new();
|
||||||
let mut preliminary = vec![];
|
let mut preliminary = vec![];
|
||||||
|
|
||||||
for (id, key, supplement, style) in citations {
|
for citation in citations {
|
||||||
let entry = entries.iter().find(|entry| entry.key() == key);
|
let cite_id = citation.0.stable_id().unwrap();
|
||||||
if let Some(entry) = &entry {
|
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);
|
db.push(entry);
|
||||||
}
|
Some(entry)
|
||||||
preliminary.push((id, entry, supplement, style));
|
})
|
||||||
|
.collect::<Option<Vec<_>>>();
|
||||||
|
preliminary.push((citation, entries));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut current = CitationStyle::Numerical;
|
let mut current = CitationStyle::Numerical;
|
||||||
@ -321,8 +321,16 @@ fn create(
|
|||||||
|
|
||||||
let citations = preliminary
|
let citations = preliminary
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(id, result, supplement, style)| {
|
.map(|(citation, cited)| {
|
||||||
let formatted = result.map(|entry| {
|
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 {
|
if style != current {
|
||||||
current = style;
|
current = style;
|
||||||
citation_style = match style {
|
citation_style = match style {
|
||||||
@ -338,17 +346,46 @@ fn create(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let citation = db.citation(
|
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,
|
&mut *citation_style,
|
||||||
&[Citation {
|
&[Citation {
|
||||||
entry,
|
entry,
|
||||||
supplement: supplement.is_some().then(|| SUPPLEMENT),
|
supplement: supplement.is_some().then(|| SUPPLEMENT),
|
||||||
}],
|
}],
|
||||||
);
|
)
|
||||||
let bracketed = citation.display.with_default_brackets(&*citation_style);
|
.display;
|
||||||
format_display_string(&bracketed, supplement)
|
|
||||||
});
|
if brackets && len == 1 {
|
||||||
(id, formatted)
|
display = display.with_default_brackets(&*citation_style);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
.collect();
|
||||||
|
|
||||||
@ -363,11 +400,26 @@ fn create(
|
|||||||
.bibliography(&*bibliography_style, None)
|
.bibliography(&*bibliography_style, None)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|reference| {
|
.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| {
|
let prefix = reference.prefix.map(|prefix| {
|
||||||
|
// Format and link to first citation.
|
||||||
let bracketed = prefix.with_default_brackets(&*citation_style);
|
let bracketed = prefix.with_default_brackets(&*citation_style);
|
||||||
format_display_string(&bracketed, None)
|
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)
|
(prefix, reference)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -443,28 +495,27 @@ fn format_display_string(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut styles = StyleMap::new();
|
let mut content = if segment == SUPPLEMENT && supplement.is_some() {
|
||||||
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() {
|
|
||||||
supplement.take().unwrap_or_default()
|
supplement.take().unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
TextNode::packed(segment)
|
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;
|
start = stop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ impl Synthesize for FigureNode {
|
|||||||
if numbering.is_some() {
|
if numbering.is_some() {
|
||||||
number = NonZeroUsize::new(
|
number = NonZeroUsize::new(
|
||||||
1 + vt
|
1 + vt
|
||||||
.locate_node::<Self>()
|
.query_node::<Self>()
|
||||||
.take_while(|figure| figure.0.stable_id() != my_id)
|
.take_while(|figure| figure.0.stable_id() != my_id)
|
||||||
.filter(|figure| figure.element() == element)
|
.filter(|figure| figure.element() == element)
|
||||||
.count(),
|
.count(),
|
||||||
|
@ -91,7 +91,7 @@ impl Synthesize for HeadingNode {
|
|||||||
if numbering.is_some() {
|
if numbering.is_some() {
|
||||||
// Advance past existing headings.
|
// Advance past existing headings.
|
||||||
for heading in vt
|
for heading in vt
|
||||||
.locate_node::<Self>()
|
.query_node::<Self>()
|
||||||
.take_while(|figure| figure.0.stable_id() != my_id)
|
.take_while(|figure| figure.0.stable_id() != my_id)
|
||||||
{
|
{
|
||||||
if heading.numbering(StyleChain::default()).is_some() {
|
if heading.numbering(StyleChain::default()).is_some() {
|
||||||
|
@ -86,7 +86,7 @@ impl Show for LinkNode {
|
|||||||
impl Finalize for LinkNode {
|
impl Finalize for LinkNode {
|
||||||
fn finalize(&self, realized: Content, _: StyleChain) -> Content {
|
fn finalize(&self, realized: Content, _: StyleChain) -> Content {
|
||||||
realized
|
realized
|
||||||
.linked(self.dest())
|
.linked(Link::Dest(self.dest()))
|
||||||
.styled(TextNode::set_hyphenate(Hyphenate(Smart::Custom(false))))
|
.styled(TextNode::set_hyphenate(Hyphenate(Smart::Custom(false))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,9 +76,13 @@ pub struct OutlineNode {
|
|||||||
impl Synthesize for OutlineNode {
|
impl Synthesize for OutlineNode {
|
||||||
fn synthesize(&mut self, vt: &Vt, _: StyleChain) {
|
fn synthesize(&mut self, vt: &Vt, _: StyleChain) {
|
||||||
let headings = vt
|
let headings = vt
|
||||||
.locate_node::<HeadingNode>()
|
.introspector
|
||||||
.filter(|node| node.outlined(StyleChain::default()))
|
.query(Selector::Node(
|
||||||
.cloned()
|
NodeId::of::<HeadingNode>(),
|
||||||
|
Some(dict! { "outlined" => true }),
|
||||||
|
))
|
||||||
|
.into_iter()
|
||||||
|
.map(|node| node.to::<HeadingNode>().unwrap().clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
self.push_headings(headings);
|
self.push_headings(headings);
|
||||||
@ -91,6 +95,7 @@ impl Show for OutlineNode {
|
|||||||
if let Some(title) = self.title(styles) {
|
if let Some(title) = self.title(styles) {
|
||||||
let title = title.clone().unwrap_or_else(|| {
|
let title = title.clone().unwrap_or_else(|| {
|
||||||
TextNode::packed(self.local_name(TextNode::lang_in(styles)))
|
TextNode::packed(self.local_name(TextNode::lang_in(styles)))
|
||||||
|
.spanned(self.span())
|
||||||
});
|
});
|
||||||
|
|
||||||
seq.push(
|
seq.push(
|
||||||
@ -107,6 +112,7 @@ impl Show for OutlineNode {
|
|||||||
|
|
||||||
let mut ancestors: Vec<&HeadingNode> = vec![];
|
let mut ancestors: Vec<&HeadingNode> = vec![];
|
||||||
for heading in self.headings().iter() {
|
for heading in self.headings().iter() {
|
||||||
|
let stable_id = heading.0.stable_id().unwrap();
|
||||||
if !heading.outlined(StyleChain::default()) {
|
if !heading.outlined(StyleChain::default()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -123,11 +129,6 @@ impl Show for OutlineNode {
|
|||||||
ancestors.pop();
|
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>("location");
|
|
||||||
loc.pos -= Point::splat(Abs::pt(10.0));
|
|
||||||
|
|
||||||
// Add hidden ancestors numberings to realize the indent.
|
// Add hidden ancestors numberings to realize the indent.
|
||||||
if indent {
|
if indent {
|
||||||
let mut hidden = Content::empty();
|
let mut hidden = Content::empty();
|
||||||
@ -155,7 +156,7 @@ impl Show for OutlineNode {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add the numbering and section name.
|
// 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.
|
// Add filler symbols between the section name and page number.
|
||||||
if let Some(filler) = self.fill(styles) {
|
if let Some(filler) = self.fill(styles) {
|
||||||
@ -172,8 +173,9 @@ impl Show for OutlineNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add the page number and linebreak.
|
// Add the page number and linebreak.
|
||||||
let end = TextNode::packed(eco_format!("{}", loc.page));
|
let page = vt.introspector.page(stable_id).unwrap();
|
||||||
seq.push(end.linked(Destination::Internal(loc)));
|
let end = TextNode::packed(eco_format!("{}", page));
|
||||||
|
seq.push(end.linked(Link::Node(stable_id)));
|
||||||
seq.push(LinebreakNode::new().pack());
|
seq.push(LinebreakNode::new().pack());
|
||||||
ancestors.push(heading);
|
ancestors.push(heading);
|
||||||
}
|
}
|
||||||
|
@ -68,14 +68,14 @@ impl Show for RefNode {
|
|||||||
let target = self.target();
|
let target = self.target();
|
||||||
let supplement = self.supplement(styles);
|
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 !vt.locatable() || BibliographyNode::has(vt, &target.0) {
|
||||||
if !matches.is_empty() {
|
if !matches.is_empty() {
|
||||||
bail!(self.span(), "label occurs in the document and its bibliography");
|
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 {
|
.with_supplement(match supplement {
|
||||||
Smart::Custom(Some(Supplement::Content(content))) => Some(content),
|
Smart::Custom(Some(Supplement::Content(content))) => Some(content),
|
||||||
_ => None,
|
_ => None,
|
||||||
@ -133,8 +133,7 @@ impl Show for RefNode {
|
|||||||
bail!(self.span(), "cannot reference {}", target.id().name);
|
bail!(self.span(), "cannot reference {}", target.id().name);
|
||||||
};
|
};
|
||||||
|
|
||||||
let loc = target.expect_field::<Location>("location");
|
Ok(formatted.linked(Link::Node(target.stable_id().unwrap())))
|
||||||
Ok(formatted.linked(Destination::Internal(loc)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,8 +15,8 @@ pub trait ContentExt {
|
|||||||
/// Underline this content.
|
/// Underline this content.
|
||||||
fn underlined(self) -> Self;
|
fn underlined(self) -> Self;
|
||||||
|
|
||||||
/// Link the content to a destination.
|
/// Link the content somewhere.
|
||||||
fn linked(self, dest: Destination) -> Self;
|
fn linked(self, link: Link) -> Self;
|
||||||
|
|
||||||
/// Set alignments for this content.
|
/// Set alignments for this content.
|
||||||
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self;
|
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self;
|
||||||
@ -41,8 +41,8 @@ impl ContentExt for Content {
|
|||||||
UnderlineNode::new(self).pack()
|
UnderlineNode::new(self).pack()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn linked(self, dest: Destination) -> Self {
|
fn linked(self, link: Link) -> Self {
|
||||||
self.styled(MetaNode::set_data(vec![Meta::Link(dest)]))
|
self.styled(MetaNode::set_data(vec![Meta::Link(link)]))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self {
|
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self {
|
||||||
|
34
src/doc.rs
34
src/doc.rs
@ -14,7 +14,7 @@ use crate::geom::{
|
|||||||
Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform,
|
Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform,
|
||||||
};
|
};
|
||||||
use crate::image::Image;
|
use crate::image::Image;
|
||||||
use crate::model::{node, Content, Fold, StyleChain};
|
use crate::model::{node, Content, Fold, Introspector, StableId, StyleChain};
|
||||||
use crate::syntax::Span;
|
use crate::syntax::Span;
|
||||||
|
|
||||||
/// A finished document with metadata and page frames.
|
/// A finished document with metadata and page frames.
|
||||||
@ -276,7 +276,7 @@ impl Frame {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for meta in MetaNode::data_in(styles) {
|
for meta in MetaNode::data_in(styles) {
|
||||||
if matches!(meta, Meta::Hidden) {
|
if matches!(meta, Meta::Hide) {
|
||||||
self.clear();
|
self.clear();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -593,13 +593,37 @@ cast_to_value! {
|
|||||||
/// Meta information that isn't visible or renderable.
|
/// Meta information that isn't visible or renderable.
|
||||||
#[derive(Debug, Clone, Hash)]
|
#[derive(Debug, Clone, Hash)]
|
||||||
pub enum Meta {
|
pub enum Meta {
|
||||||
|
/// Indicates that the content should be hidden.
|
||||||
|
Hide,
|
||||||
/// An internal or external link.
|
/// An internal or external link.
|
||||||
Link(Destination),
|
Link(Link),
|
||||||
/// An identifiable piece of content that produces something within the
|
/// An identifiable piece of content that produces something within the
|
||||||
/// area this metadata is attached to.
|
/// area this metadata is attached to.
|
||||||
Node(Content),
|
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<Destination> {
|
||||||
|
match self {
|
||||||
|
Self::Dest(dest) => Some(dest.clone()),
|
||||||
|
Self::Node(id) => introspector().location(*id).map(Destination::Internal),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Host for metadata.
|
/// Host for metadata.
|
||||||
|
@ -19,6 +19,7 @@ use crate::doc::{Document, Lang};
|
|||||||
use crate::font::Font;
|
use crate::font::Font;
|
||||||
use crate::geom::{Abs, Dir, Em};
|
use crate::geom::{Abs, Dir, Em};
|
||||||
use crate::image::Image;
|
use crate::image::Image;
|
||||||
|
use crate::model::Introspector;
|
||||||
|
|
||||||
/// Export a document into a PDF file.
|
/// 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.
|
/// Context for exporting a whole PDF document.
|
||||||
pub struct PdfContext<'a> {
|
pub struct PdfContext<'a> {
|
||||||
document: &'a Document,
|
document: &'a Document,
|
||||||
|
introspector: Introspector,
|
||||||
writer: PdfWriter,
|
writer: PdfWriter,
|
||||||
pages: Vec<Page>,
|
pages: Vec<Page>,
|
||||||
page_heights: Vec<f32>,
|
page_heights: Vec<f32>,
|
||||||
@ -61,6 +63,7 @@ impl<'a> PdfContext<'a> {
|
|||||||
let page_tree_ref = alloc.bump();
|
let page_tree_ref = alloc.bump();
|
||||||
Self {
|
Self {
|
||||||
document,
|
document,
|
||||||
|
introspector: Introspector::new(&document.pages),
|
||||||
writer: PdfWriter::new(),
|
writer: PdfWriter::new(),
|
||||||
pages: vec![],
|
pages: vec![],
|
||||||
page_heights: vec![],
|
page_heights: vec![],
|
||||||
|
@ -4,7 +4,7 @@ use pdf_writer::writers::ColorSpace;
|
|||||||
use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str};
|
use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str};
|
||||||
|
|
||||||
use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB};
|
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::font::Font;
|
||||||
use crate::geom::{
|
use crate::geom::{
|
||||||
self, Abs, Color, Em, Geometry, Numeric, Paint, Point, Ratio, Shape, Size, Stroke,
|
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);
|
page_writer.contents(content_id);
|
||||||
|
|
||||||
let mut annotations = page_writer.annotations();
|
let mut annotations = page_writer.annotations();
|
||||||
for (dest, rect) in page.links {
|
for (link, rect) in page.links {
|
||||||
let mut link = annotations.push();
|
let mut annotation = annotations.push();
|
||||||
link.subtype(AnnotationType::Link).rect(rect);
|
annotation.subtype(AnnotationType::Link).rect(rect);
|
||||||
link.border(0.0, 0.0, 0.0, None);
|
annotation.border(0.0, 0.0, 0.0, None);
|
||||||
|
|
||||||
|
let dest = link.resolve(|| &ctx.introspector);
|
||||||
|
let Some(dest) = dest else { continue };
|
||||||
|
|
||||||
match dest {
|
match dest {
|
||||||
Destination::Url(uri) => {
|
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) => {
|
Destination::Internal(loc) => {
|
||||||
let index = loc.page.get() - 1;
|
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) {
|
if let Some(&height) = ctx.page_heights.get(index) {
|
||||||
link.action()
|
annotation
|
||||||
|
.action()
|
||||||
.action_type(ActionType::GoTo)
|
.action_type(ActionType::GoTo)
|
||||||
.destination_direct()
|
.destination_direct()
|
||||||
.page(ctx.page_refs[index])
|
.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.
|
/// The page's content stream.
|
||||||
pub content: Content,
|
pub content: Content,
|
||||||
/// Links in the PDF coordinate system.
|
/// 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.
|
/// An exporter for the contents of a single PDF page.
|
||||||
@ -159,7 +168,7 @@ struct PageContext<'a, 'b> {
|
|||||||
state: State,
|
state: State,
|
||||||
saves: Vec<State>,
|
saves: Vec<State>,
|
||||||
bottom: f32,
|
bottom: f32,
|
||||||
links: Vec<(Destination, Rect)>,
|
links: Vec<(Link, Rect)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A simulated graphics state used to deduplicate graphics state changes and
|
/// 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::Shape(shape) => write_shape(ctx, x, y, shape),
|
||||||
Element::Image(image, size) => write_image(ctx, x, y, image, *size),
|
Element::Image(image, size) => write_image(ctx, x, y, image, *size),
|
||||||
Element::Meta(meta, size) => match meta {
|
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::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.
|
/// 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_x = Abs::inf();
|
||||||
let mut min_y = Abs::inf();
|
let mut min_y = Abs::inf();
|
||||||
let mut max_x = -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 y2 = min_y.to_f32();
|
||||||
let rect = Rect::new(x1, y1, x2, y2);
|
let rect = Rect::new(x1, y1, x2, y2);
|
||||||
|
|
||||||
ctx.links.push((dest.clone(), rect));
|
ctx.links.push((link.clone(), rect));
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ fn render_frame(
|
|||||||
Element::Meta(meta, _) => match meta {
|
Element::Meta(meta, _) => match meta {
|
||||||
Meta::Link(_) => {}
|
Meta::Link(_) => {}
|
||||||
Meta::Node(_) => {}
|
Meta::Node(_) => {}
|
||||||
Meta::Hidden => {}
|
Meta::Hide => {}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,12 +74,11 @@ pub fn analyze_labels(
|
|||||||
frames: &[Frame],
|
frames: &[Frame],
|
||||||
) -> (Vec<(Label, Option<EcoString>)>, usize) {
|
) -> (Vec<(Label, Option<EcoString>)>, usize) {
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
let mut introspector = Introspector::new();
|
let introspector = Introspector::new(frames);
|
||||||
let items = &world.library().items;
|
let items = &world.library().items;
|
||||||
introspector.update(frames);
|
|
||||||
|
|
||||||
// Labels in the document.
|
// Labels in the document.
|
||||||
for node in introspector.iter() {
|
for node in introspector.nodes() {
|
||||||
let Some(label) = node.label() else { continue };
|
let Some(label) = node.label() else { continue };
|
||||||
let details = node
|
let details = node
|
||||||
.field("caption")
|
.field("caption")
|
||||||
|
@ -2,6 +2,7 @@ use std::num::NonZeroUsize;
|
|||||||
|
|
||||||
use crate::doc::{Destination, Element, Frame, Location, Meta};
|
use crate::doc::{Destination, Element, Frame, Location, Meta};
|
||||||
use crate::geom::{Point, Size};
|
use crate::geom::{Point, Size};
|
||||||
|
use crate::model::Introspector;
|
||||||
use crate::syntax::{LinkedNode, Source, SourceId, Span, SyntaxKind};
|
use crate::syntax::{LinkedNode, Source, SourceId, Span, SyntaxKind};
|
||||||
use crate::World;
|
use crate::World;
|
||||||
|
|
||||||
@ -15,11 +16,19 @@ pub enum Jump {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Determine where to jump to based on a click in a frame.
|
/// 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<Jump> {
|
pub fn jump_from_click(
|
||||||
|
world: &dyn World,
|
||||||
|
frames: &[Frame],
|
||||||
|
frame: &Frame,
|
||||||
|
click: Point,
|
||||||
|
) -> Option<Jump> {
|
||||||
|
let mut introspector = None;
|
||||||
|
|
||||||
for (mut pos, element) in frame.elements() {
|
for (mut pos, element) in frame.elements() {
|
||||||
if let Element::Group(group) = element {
|
if let Element::Group(group) = element {
|
||||||
// TODO: Handle transformation.
|
// 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);
|
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) {
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ pub fn typeset(world: Tracked<dyn World>, content: &Content) -> SourceResult<Doc
|
|||||||
|
|
||||||
let mut document;
|
let mut document;
|
||||||
let mut iter = 0;
|
let mut iter = 0;
|
||||||
let mut introspector = Introspector::new();
|
let mut introspector = Introspector::new(&[]);
|
||||||
|
|
||||||
// Relayout until all introspections stabilize.
|
// Relayout until all introspections stabilize.
|
||||||
// If that doesn't happen within five attempts, we give up.
|
// If that doesn't happen within five attempts, we give up.
|
||||||
@ -40,8 +40,6 @@ pub fn typeset(world: Tracked<dyn World>, content: &Content) -> SourceResult<Doc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Took {iter} iterations");
|
|
||||||
|
|
||||||
Ok(document)
|
Ok(document)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,14 +79,11 @@ impl<'a> Vt<'a> {
|
|||||||
self.introspector.init()
|
self.introspector.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Locate all metadata matches for the given selector.
|
|
||||||
pub fn locate(&self, selector: Selector) -> impl Iterator<Item = &Content> {
|
|
||||||
self.introspector.locate(selector).into_iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Locate all metadata matches for the given node.
|
/// Locate all metadata matches for the given node.
|
||||||
pub fn locate_node<T: Node>(&self) -> impl Iterator<Item = &T> {
|
pub fn query_node<T: Node>(&self) -> impl Iterator<Item = &T> {
|
||||||
self.locate(Selector::node::<T>())
|
self.introspector
|
||||||
|
.query(Selector::node::<T>())
|
||||||
|
.into_iter()
|
||||||
.map(|content| content.to::<T>().unwrap())
|
.map(|content| content.to::<T>().unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,7 +92,14 @@ impl<'a> Vt<'a> {
|
|||||||
///
|
///
|
||||||
/// This struct is created by [`Vt::identify`].
|
/// This struct is created by [`Vt::identify`].
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
#[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.
|
/// Provides stable identities to nodes.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -115,7 +117,7 @@ impl StabilityProvider {
|
|||||||
/// Produce a stable identifier for this call site.
|
/// Produce a stable identifier for this call site.
|
||||||
fn identify(&mut self, hash: u128) -> StableId {
|
fn identify(&mut self, hash: u128) -> StableId {
|
||||||
let slot = self.0.entry(hash).or_default();
|
let slot = self.0.entry(hash).or_default();
|
||||||
let id = StableId(hash, *slot);
|
let id = StableId(hash, *slot, 0);
|
||||||
*slot += 1;
|
*slot += 1;
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
@ -124,35 +126,33 @@ impl StabilityProvider {
|
|||||||
/// Provides access to information about the document.
|
/// Provides access to information about the document.
|
||||||
pub struct Introspector {
|
pub struct Introspector {
|
||||||
init: bool,
|
init: bool,
|
||||||
nodes: Vec<Content>,
|
nodes: Vec<(Content, Location)>,
|
||||||
queries: RefCell<Vec<(Selector, u128)>>,
|
queries: RefCell<Vec<(Selector, u128)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Introspector {
|
impl Introspector {
|
||||||
/// Create a new introspector.
|
/// Create a new introspector.
|
||||||
pub fn new() -> Self {
|
pub fn new(frames: &[Frame]) -> Self {
|
||||||
Self {
|
let mut introspector = Self {
|
||||||
init: false,
|
init: false,
|
||||||
nodes: vec![],
|
nodes: vec![],
|
||||||
queries: RefCell::new(vec![]),
|
queries: RefCell::new(vec![]),
|
||||||
}
|
};
|
||||||
|
introspector.extract_from_frames(frames);
|
||||||
|
introspector
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the information given new frames and return whether we can stop
|
/// Update the information given new frames and return whether we can stop
|
||||||
/// layouting.
|
/// layouting.
|
||||||
pub fn update(&mut self, frames: &[Frame]) -> bool {
|
pub fn update(&mut self, frames: &[Frame]) -> bool {
|
||||||
self.nodes.clear();
|
self.nodes.clear();
|
||||||
|
self.extract_from_frames(frames);
|
||||||
for (i, frame) in frames.iter().enumerate() {
|
|
||||||
let page = NonZeroUsize::new(1 + i).unwrap();
|
|
||||||
self.extract(frame, page, Transform::identity());
|
|
||||||
}
|
|
||||||
|
|
||||||
let was_init = std::mem::replace(&mut self.init, true);
|
let was_init = std::mem::replace(&mut self.init, true);
|
||||||
let queries = std::mem::take(&mut self.queries).into_inner();
|
let queries = std::mem::take(&mut self.queries).into_inner();
|
||||||
|
|
||||||
for (selector, hash) in &queries {
|
for (selector, hash) in &queries {
|
||||||
let nodes = self.locate_impl(selector);
|
let nodes = self.query_impl(selector);
|
||||||
if hash128(&nodes) != *hash {
|
if hash128(&nodes) != *hash {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -166,32 +166,36 @@ impl Introspector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over all nodes.
|
/// Iterate over all nodes.
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &Content> {
|
pub fn nodes(&self) -> impl Iterator<Item = &Content> {
|
||||||
self.nodes.iter()
|
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.
|
/// 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() {
|
for (pos, element) in frame.elements() {
|
||||||
match element {
|
match element {
|
||||||
Element::Group(group) => {
|
Element::Group(group) => {
|
||||||
let ts = ts
|
let ts = ts
|
||||||
.pre_concat(Transform::translate(pos.x, pos.y))
|
.pre_concat(Transform::translate(pos.x, pos.y))
|
||||||
.pre_concat(group.transform);
|
.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
|
if !self
|
||||||
.nodes
|
.nodes
|
||||||
.iter()
|
.iter()
|
||||||
.any(|prev| prev.stable_id() == content.stable_id())
|
.any(|(prev, _)| prev.stable_id() == content.stable_id()) =>
|
||||||
{
|
{
|
||||||
let pos = pos.transform(ts);
|
let pos = pos.transform(ts);
|
||||||
let mut node = content.clone();
|
self.nodes.push((content.clone(), Location { page, pos }));
|
||||||
let loc = Location { page, pos };
|
|
||||||
node.push_field("location", loc);
|
|
||||||
self.nodes.push(node);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -206,19 +210,29 @@ impl Introspector {
|
|||||||
self.init
|
self.init
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Locate all metadata matches for the given selector.
|
/// Query for all metadata matches for the given selector.
|
||||||
pub fn locate(&self, selector: Selector) -> Vec<&Content> {
|
pub fn query(&self, selector: Selector) -> Vec<&Content> {
|
||||||
let nodes = self.locate_impl(&selector);
|
let nodes = self.query_impl(&selector);
|
||||||
let mut queries = self.queries.borrow_mut();
|
let mut queries = self.queries.borrow_mut();
|
||||||
if !queries.iter().any(|(prev, _)| prev == &selector) {
|
if !queries.iter().any(|(prev, _)| prev == &selector) {
|
||||||
queries.push((selector, hash128(&nodes)));
|
queries.push((selector, hash128(&nodes)));
|
||||||
}
|
}
|
||||||
nodes
|
nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the page number for the given stable id.
|
||||||
|
pub fn page(&self, id: StableId) -> Option<NonZeroUsize> {
|
||||||
|
Some(self.location(id)?.page)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the location for the given stable id.
|
||||||
|
pub fn location(&self, id: StableId) -> Option<Location> {
|
||||||
|
Some(self.nodes.iter().find(|(node, _)| node.stable_id() == Some(id))?.1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Introspector {
|
impl Introspector {
|
||||||
fn locate_impl(&self, selector: &Selector) -> Vec<&Content> {
|
fn query_impl(&self, selector: &Selector) -> Vec<&Content> {
|
||||||
self.nodes.iter().filter(|target| selector.matches(target)).collect()
|
self.nodes().filter(|node| selector.matches(node)).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 84 KiB |
@ -14,7 +14,7 @@
|
|||||||
---
|
---
|
||||||
#set page(width: 200pt)
|
#set page(width: 200pt)
|
||||||
= Details
|
= 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")
|
#bibliography("/works.bib")
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -22,6 +22,7 @@ See also #cite("arrgh", [p. 22]), @arrgh[p. 4], and @cannonfodder[p. 5].
|
|||||||
#set page(width: 200pt)
|
#set page(width: 200pt)
|
||||||
#bibliography("/works.bib", title: [Works to be cited], style: "author-date")
|
#bibliography("/works.bib", title: [Works to be cited], style: "author-date")
|
||||||
#line(length: 100%)
|
#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
|
This is close to piratery! @arrgh
|
||||||
And quark! @quark
|
And quark! @quark
|
||||||
|
Loading…
x
Reference in New Issue
Block a user