mirror of
https://github.com/typst/typst
synced 2025-05-13 12:36:23 +08:00
Section references
This commit is contained in:
parent
8e5f446544
commit
529d3e10c6
@ -232,20 +232,6 @@ impl<'a, 'v, 't> Builder<'a, 'v, 't> {
|
||||
self.scratch.content.alloc(FormulaNode::new(content.clone()).pack());
|
||||
}
|
||||
|
||||
// Prepare only if this is the first application for this node.
|
||||
if content.can::<dyn Prepare>() {
|
||||
if !content.is_prepared() {
|
||||
let prepared = content
|
||||
.clone()
|
||||
.prepared()
|
||||
.with::<dyn Prepare>()
|
||||
.unwrap()
|
||||
.prepare(self.vt, styles)?;
|
||||
let stored = self.scratch.content.alloc(prepared);
|
||||
return self.accept(stored, styles);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(styled) = content.to::<StyledNode>() {
|
||||
return self.styled(styled, styles);
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ use crate::text::{TextNode, TextSize};
|
||||
///
|
||||
/// Display: Heading
|
||||
/// Category: meta
|
||||
#[node(Prepare, Show, Finalize)]
|
||||
#[node(Synthesize, Show, Finalize)]
|
||||
pub struct HeadingNode {
|
||||
/// The logical nesting depth of the heading, starting from one.
|
||||
#[default(NonZeroUsize::new(1).unwrap())]
|
||||
@ -76,44 +76,61 @@ pub struct HeadingNode {
|
||||
/// The heading's title.
|
||||
#[required]
|
||||
pub body: Content,
|
||||
|
||||
/// The heading's numbering numbers.
|
||||
///
|
||||
/// ```example
|
||||
/// #show heading: it => it.numbers
|
||||
///
|
||||
/// = First
|
||||
/// == Second
|
||||
/// = Third
|
||||
/// ```
|
||||
#[synthesized]
|
||||
pub numbers: Option<Vec<NonZeroUsize>>,
|
||||
}
|
||||
|
||||
impl Prepare for HeadingNode {
|
||||
fn prepare(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
|
||||
impl Synthesize for HeadingNode {
|
||||
fn synthesize(&self, vt: &mut Vt, styles: StyleChain) -> Content {
|
||||
let my_id = vt.identify(self);
|
||||
let numbered = self.numbering(styles).is_some();
|
||||
|
||||
let mut counter = HeadingCounter::new();
|
||||
for (node_id, node) in vt.locate(Selector::node::<HeadingNode>()) {
|
||||
if node_id == my_id {
|
||||
break;
|
||||
if numbered {
|
||||
// Advance passed existing headings.
|
||||
for (_, node) in vt
|
||||
.locate(Selector::node::<HeadingNode>())
|
||||
.into_iter()
|
||||
.take_while(|&(id, _)| id != my_id)
|
||||
{
|
||||
let heading = node.to::<HeadingNode>().unwrap();
|
||||
if heading.numbering(StyleChain::default()).is_some() {
|
||||
counter.advance(heading);
|
||||
}
|
||||
}
|
||||
|
||||
let numbers = node.field("numbers").unwrap();
|
||||
if *numbers != Value::None {
|
||||
let heading = node.to::<Self>().unwrap();
|
||||
counter.advance(heading);
|
||||
}
|
||||
// Advance passed self.
|
||||
counter.advance(self);
|
||||
}
|
||||
|
||||
let mut numbers = Value::None;
|
||||
if let Some(numbering) = self.numbering(styles) {
|
||||
numbers = numbering.apply(vt.world(), counter.advance(self))?;
|
||||
}
|
||||
let node = self
|
||||
.clone()
|
||||
.with_outlined(self.outlined(styles))
|
||||
.with_numbering(self.numbering(styles))
|
||||
.with_numbers(numbered.then(|| counter.take()))
|
||||
.pack();
|
||||
|
||||
let mut node = self.clone().pack();
|
||||
node.push_field("outlined", Value::Bool(self.outlined(styles)));
|
||||
node.push_field("numbers", numbers);
|
||||
let meta = Meta::Node(my_id, node.clone());
|
||||
Ok(node.styled(MetaNode::set_data(vec![meta])))
|
||||
node.styled(MetaNode::set_data(vec![meta]))
|
||||
}
|
||||
}
|
||||
|
||||
impl Show for HeadingNode {
|
||||
fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
|
||||
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
|
||||
let mut realized = self.body();
|
||||
let numbers = self.0.field("numbers").unwrap();
|
||||
if *numbers != Value::None {
|
||||
realized = numbers.clone().display()
|
||||
if let Some(numbering) = self.numbering(styles) {
|
||||
let numbers = self.numbers().unwrap();
|
||||
realized = numbering.apply(vt.world(), &numbers)?.display()
|
||||
+ HNode::new(Em::new(0.3).into()).with_weak(true).pack()
|
||||
+ realized;
|
||||
}
|
||||
@ -168,4 +185,14 @@ impl HeadingCounter {
|
||||
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Take out the current counts.
|
||||
pub fn take(self) -> Vec<NonZeroUsize> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
cast_from_value! {
|
||||
HeadingNode,
|
||||
v: Content => v.to::<Self>().ok_or("expected heading")?.clone(),
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ impl Numbering {
|
||||
numbers: &[NonZeroUsize],
|
||||
) -> SourceResult<Value> {
|
||||
Ok(match self {
|
||||
Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()),
|
||||
Self::Pattern(pattern) => Value::Str(pattern.apply(numbers, false).into()),
|
||||
Self::Func(func) => {
|
||||
let args = Args::new(
|
||||
func.span(),
|
||||
@ -124,12 +124,16 @@ pub struct NumberingPattern {
|
||||
|
||||
impl NumberingPattern {
|
||||
/// Apply the pattern to the given number.
|
||||
pub fn apply(&self, numbers: &[NonZeroUsize]) -> EcoString {
|
||||
pub fn apply(&self, numbers: &[NonZeroUsize], trimmed: bool) -> EcoString {
|
||||
let mut fmt = EcoString::new();
|
||||
let mut numbers = numbers.into_iter();
|
||||
|
||||
for ((prefix, kind, case), &n) in self.pieces.iter().zip(&mut numbers) {
|
||||
fmt.push_str(prefix);
|
||||
for (i, ((prefix, kind, case), &n)) in
|
||||
self.pieces.iter().zip(&mut numbers).enumerate()
|
||||
{
|
||||
if i > 0 || !trimmed {
|
||||
fmt.push_str(prefix);
|
||||
}
|
||||
fmt.push_str(&kind.apply(n, *case));
|
||||
}
|
||||
|
||||
@ -144,7 +148,10 @@ impl NumberingPattern {
|
||||
fmt.push_str(&kind.apply(n, *case));
|
||||
}
|
||||
|
||||
fmt.push_str(&self.suffix);
|
||||
if !trimmed {
|
||||
fmt.push_str(&self.suffix);
|
||||
}
|
||||
|
||||
fmt
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ use crate::text::{LinebreakNode, SpaceNode, TextNode};
|
||||
///
|
||||
/// Display: Outline
|
||||
/// Category: meta
|
||||
#[node(Prepare, Show)]
|
||||
#[node(Synthesize, Show)]
|
||||
pub struct OutlineNode {
|
||||
/// The title of the outline.
|
||||
///
|
||||
@ -67,21 +67,22 @@ pub struct OutlineNode {
|
||||
/// ```
|
||||
#[default(Some(RepeatNode::new(TextNode::packed(".")).pack()))]
|
||||
pub fill: Option<Content>,
|
||||
|
||||
/// All outlined headings in the document.
|
||||
#[synthesized]
|
||||
pub headings: Vec<HeadingNode>,
|
||||
}
|
||||
|
||||
impl Prepare for OutlineNode {
|
||||
fn prepare(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
|
||||
impl Synthesize for OutlineNode {
|
||||
fn synthesize(&self, vt: &mut Vt, _: StyleChain) -> Content {
|
||||
let headings = vt
|
||||
.locate(Selector::node::<HeadingNode>())
|
||||
.into_iter()
|
||||
.map(|(_, node)| node)
|
||||
.filter(|node| *node.field("outlined").unwrap() == Value::Bool(true))
|
||||
.map(|node| Value::Content(node.clone()))
|
||||
.map(|(_, node)| node.to::<HeadingNode>().unwrap().clone())
|
||||
.filter(|node| node.outlined(StyleChain::default()))
|
||||
.collect();
|
||||
|
||||
let mut node = self.clone().pack();
|
||||
node.push_field("headings", Value::Array(Array::from_vec(headings)));
|
||||
Ok(node)
|
||||
self.clone().with_headings(headings).pack()
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,13 +109,12 @@ impl Show for OutlineNode {
|
||||
let indent = self.indent(styles);
|
||||
let depth = self.depth(styles);
|
||||
|
||||
let mut ancestors: Vec<&Content> = vec![];
|
||||
for (_, node) in vt.locate(Selector::node::<HeadingNode>()) {
|
||||
if *node.field("outlined").unwrap() != Value::Bool(true) {
|
||||
let mut ancestors: Vec<&HeadingNode> = vec![];
|
||||
for heading in self.headings().iter() {
|
||||
if !heading.outlined(StyleChain::default()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let heading = node.to::<HeadingNode>().unwrap();
|
||||
if let Some(depth) = depth {
|
||||
if depth < heading.level(StyleChain::default()) {
|
||||
continue;
|
||||
@ -122,37 +122,40 @@ impl Show for OutlineNode {
|
||||
}
|
||||
|
||||
while ancestors.last().map_or(false, |last| {
|
||||
last.to::<HeadingNode>().unwrap().level(StyleChain::default())
|
||||
>= heading.level(StyleChain::default())
|
||||
last.level(StyleChain::default()) >= heading.level(StyleChain::default())
|
||||
}) {
|
||||
ancestors.pop();
|
||||
}
|
||||
|
||||
// Adjust the link destination a bit to the topleft so that the
|
||||
// heading is fully visible.
|
||||
let mut loc = node.field("loc").unwrap().clone().cast::<Location>().unwrap();
|
||||
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.
|
||||
if indent {
|
||||
let hidden: Vec<_> = ancestors
|
||||
.iter()
|
||||
.map(|node| node.field("numbers").unwrap())
|
||||
.filter(|&numbers| *numbers != Value::None)
|
||||
.map(|numbers| numbers.clone().display() + SpaceNode::new().pack())
|
||||
.collect();
|
||||
let mut hidden = Content::empty();
|
||||
for ancestor in &ancestors {
|
||||
if let Some(numbering) = ancestor.numbering(StyleChain::default()) {
|
||||
let numbers = ancestor.numbers().unwrap();
|
||||
hidden += numbering.apply(vt.world(), &numbers)?.display()
|
||||
+ SpaceNode::new().pack();
|
||||
};
|
||||
}
|
||||
|
||||
if !hidden.is_empty() {
|
||||
seq.push(HideNode::new(Content::sequence(hidden)).pack());
|
||||
if !ancestors.is_empty() {
|
||||
seq.push(HideNode::new(hidden).pack());
|
||||
seq.push(SpaceNode::new().pack());
|
||||
}
|
||||
}
|
||||
|
||||
// Format the numbering.
|
||||
let mut start = heading.body();
|
||||
let numbers = node.field("numbers").unwrap();
|
||||
if *numbers != Value::None {
|
||||
start = numbers.clone().display() + SpaceNode::new().pack() + start;
|
||||
if let Some(numbering) = heading.numbering(StyleChain::default()) {
|
||||
let numbers = heading.numbers().unwrap();
|
||||
start = numbering.apply(vt.world(), &numbers)?.display()
|
||||
+ SpaceNode::new().pack()
|
||||
+ start;
|
||||
};
|
||||
|
||||
// Add the numbering and section name.
|
||||
@ -176,8 +179,7 @@ impl Show for OutlineNode {
|
||||
let end = TextNode::packed(eco_format!("{}", loc.page));
|
||||
seq.push(end.linked(Destination::Internal(loc)));
|
||||
seq.push(LinebreakNode::new().pack());
|
||||
|
||||
ancestors.push(node);
|
||||
ancestors.push(heading);
|
||||
}
|
||||
|
||||
seq.push(ParbreakNode::new().pack());
|
||||
|
@ -1,31 +1,152 @@
|
||||
use super::{HeadingNode, Numbering};
|
||||
use crate::prelude::*;
|
||||
use crate::text::TextNode;
|
||||
|
||||
/// A reference to a label.
|
||||
///
|
||||
/// *Note: This function is currently unimplemented.*
|
||||
///
|
||||
/// The reference function produces a textual reference to a label. For example,
|
||||
/// a reference to a heading will yield an appropriate string such as "Section
|
||||
/// 1" for a reference to the first heading's label. The references are also
|
||||
/// links to the respective labels.
|
||||
/// 1" for a reference to the first heading. The references are also links to
|
||||
/// the respective element.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #set heading(numbering: "1.")
|
||||
///
|
||||
/// = Introduction <intro>
|
||||
/// Recent developments in typesetting
|
||||
/// software have rekindled hope in
|
||||
/// previously frustrated researchers.
|
||||
/// As shown in @results, we ...
|
||||
///
|
||||
/// = Results <results>
|
||||
/// We evaluate our method in a
|
||||
/// series of tests. @perf discusses
|
||||
/// the performance aspects of ...
|
||||
///
|
||||
/// == Performance <perf>
|
||||
/// As described in @intro, we ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax
|
||||
/// This function also has dedicated syntax: A reference to a label can be
|
||||
/// created by typing an `@` followed by the name of the label (e.g. `[=
|
||||
/// Introduction <intro>]` can be referenced by typing `[@intro]`).
|
||||
/// created by typing an `@` followed by the name of the label (e.g.
|
||||
/// `[= Introduction <intro>]` can be referenced by typing `[@intro]`).
|
||||
///
|
||||
/// Display: Reference
|
||||
/// Category: meta
|
||||
#[node(Show)]
|
||||
#[node(Synthesize, Show)]
|
||||
pub struct RefNode {
|
||||
/// The label that should be referenced.
|
||||
#[required]
|
||||
pub target: EcoString,
|
||||
pub label: Label,
|
||||
|
||||
/// The prefix before the referenced number.
|
||||
///
|
||||
/// ```example
|
||||
/// #set heading(numbering: "1.")
|
||||
/// #set ref(prefix: it => {
|
||||
/// if it.func() == heading {
|
||||
/// "Chapter"
|
||||
/// } else {
|
||||
/// "Thing"
|
||||
/// }
|
||||
/// })
|
||||
///
|
||||
/// = Introduction <intro>
|
||||
/// In @intro, we see how to turn
|
||||
/// Sections into Chapters.
|
||||
/// ```
|
||||
pub prefix: Smart<Option<Func>>,
|
||||
|
||||
/// All elements with the `target` label in the document.
|
||||
#[synthesized]
|
||||
pub matches: Vec<Content>,
|
||||
}
|
||||
|
||||
impl Synthesize for RefNode {
|
||||
fn synthesize(&self, vt: &mut Vt, _: StyleChain) -> Content {
|
||||
let matches = vt
|
||||
.locate(Selector::Label(self.label()))
|
||||
.into_iter()
|
||||
.map(|(_, node)| node.clone())
|
||||
.collect();
|
||||
|
||||
self.clone().with_matches(matches).pack()
|
||||
}
|
||||
}
|
||||
|
||||
impl Show for RefNode {
|
||||
fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(TextNode::packed(eco_format!("@{}", self.target())))
|
||||
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
|
||||
let matches = self.matches();
|
||||
let [target] = matches.as_slice() else {
|
||||
if vt.locatable() {
|
||||
bail!(self.span(), if matches.is_empty() {
|
||||
"label does not exist in the document"
|
||||
} else {
|
||||
"label occurs multiple times in the document"
|
||||
});
|
||||
} else {
|
||||
return Ok(Content::empty());
|
||||
}
|
||||
};
|
||||
|
||||
let mut prefix = match self.prefix(styles) {
|
||||
Smart::Auto => prefix(target, TextNode::lang_in(styles))
|
||||
.map(TextNode::packed)
|
||||
.unwrap_or_default(),
|
||||
Smart::Custom(None) => Content::empty(),
|
||||
Smart::Custom(Some(func)) => {
|
||||
let args = Args::new(func.span(), [target.clone().into()]);
|
||||
func.call_detached(vt.world(), args)?.display()
|
||||
}
|
||||
};
|
||||
|
||||
if !prefix.is_empty() {
|
||||
prefix += TextNode::packed('\u{a0}');
|
||||
}
|
||||
|
||||
let formatted = if let Some(heading) = target.to::<HeadingNode>() {
|
||||
if let Some(numbering) = heading.numbering(StyleChain::default()) {
|
||||
let numbers = heading.numbers().unwrap();
|
||||
numbered(vt, prefix, &numbering, &numbers)?
|
||||
} else {
|
||||
bail!(self.span(), "cannot reference unnumbered heading");
|
||||
}
|
||||
} else {
|
||||
bail!(self.span(), "cannot reference {}", target.id().name);
|
||||
};
|
||||
|
||||
let loc = target.expect_field::<Location>("location");
|
||||
Ok(formatted.linked(Destination::Internal(loc)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a numbered reference like "Section 1.1".
|
||||
fn numbered(
|
||||
vt: &Vt,
|
||||
prefix: Content,
|
||||
numbering: &Numbering,
|
||||
numbers: &[NonZeroUsize],
|
||||
) -> SourceResult<Content> {
|
||||
Ok(prefix
|
||||
+ match numbering {
|
||||
Numbering::Pattern(pattern) => {
|
||||
TextNode::packed(pattern.apply(&numbers, true))
|
||||
}
|
||||
Numbering::Func(_) => numbering.apply(vt.world(), &numbers)?.display(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The default prefix.
|
||||
fn prefix(node: &Content, lang: Lang) -> Option<&str> {
|
||||
if node.is::<HeadingNode>() {
|
||||
match lang {
|
||||
Lang::ENGLISH => Some("Section"),
|
||||
Lang::GERMAN => Some("Abschnitt"),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -22,8 +22,8 @@ pub use typst::eval::{
|
||||
pub use typst::geom::*;
|
||||
#[doc(no_inline)]
|
||||
pub use typst::model::{
|
||||
node, Construct, Content, Finalize, Fold, Introspector, Label, Node, NodeId, Prepare,
|
||||
Resolve, Selector, Set, Show, StabilityProvider, StyleChain, StyleMap, StyleVec,
|
||||
node, Construct, Content, Finalize, Fold, Introspector, Label, Node, NodeId, Resolve,
|
||||
Selector, Set, Show, StabilityProvider, StyleChain, StyleMap, StyleVec, Synthesize,
|
||||
Unlabellable, Vt,
|
||||
};
|
||||
#[doc(no_inline)]
|
||||
|
@ -35,7 +35,7 @@ use crate::prelude::*;
|
||||
///
|
||||
/// Display: Raw Text / Code
|
||||
/// Category: text
|
||||
#[node(Prepare, Show, Finalize)]
|
||||
#[node(Synthesize, Show, Finalize)]
|
||||
pub struct RawNode {
|
||||
/// The raw text.
|
||||
///
|
||||
@ -120,11 +120,9 @@ impl RawNode {
|
||||
}
|
||||
}
|
||||
|
||||
impl Prepare for RawNode {
|
||||
fn prepare(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
|
||||
let mut node = self.clone().pack();
|
||||
node.push_field("lang", self.lang(styles).clone());
|
||||
Ok(node)
|
||||
impl Synthesize for RawNode {
|
||||
fn synthesize(&self, _: &mut Vt, styles: StyleChain) -> Content {
|
||||
self.clone().with_lang(self.lang(styles)).pack()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ struct Field {
|
||||
positional: bool,
|
||||
required: bool,
|
||||
variadic: bool,
|
||||
synthesized: bool,
|
||||
fold: bool,
|
||||
resolve: bool,
|
||||
parse: Option<FieldParser>,
|
||||
@ -88,6 +89,7 @@ fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result<Node> {
|
||||
positional,
|
||||
required,
|
||||
variadic,
|
||||
synthesized: has_attr(&mut attrs, "synthesized"),
|
||||
fold: has_attr(&mut attrs, "fold"),
|
||||
resolve: has_attr(&mut attrs, "resolve"),
|
||||
parse: parse_attr(&mut attrs, "parse")?.flatten(),
|
||||
@ -154,7 +156,7 @@ fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result<Node> {
|
||||
fn create(node: &Node) -> TokenStream {
|
||||
let Node { vis, ident, docs, .. } = node;
|
||||
let all = node.fields.iter().filter(|field| !field.external);
|
||||
let settable = all.clone().filter(|field| field.settable());
|
||||
let settable = all.clone().filter(|field| !field.synthesized && field.settable());
|
||||
|
||||
// Inherent methods and functions.
|
||||
let new = create_new_func(node);
|
||||
@ -176,7 +178,7 @@ fn create(node: &Node) -> TokenStream {
|
||||
#[doc = #docs]
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
#[repr(transparent)]
|
||||
#vis struct #ident(::typst::model::Content);
|
||||
#vis struct #ident(pub ::typst::model::Content);
|
||||
|
||||
impl #ident {
|
||||
#new
|
||||
@ -205,7 +207,10 @@ fn create(node: &Node) -> TokenStream {
|
||||
|
||||
/// Create the `new` function for the node.
|
||||
fn create_new_func(node: &Node) -> TokenStream {
|
||||
let relevant = node.fields.iter().filter(|field| !field.external && field.inherent());
|
||||
let relevant = node
|
||||
.fields
|
||||
.iter()
|
||||
.filter(|field| !field.external && !field.synthesized && field.inherent());
|
||||
let params = relevant.clone().map(|Field { ident, ty, .. }| {
|
||||
quote! { #ident: #ty }
|
||||
});
|
||||
@ -224,11 +229,11 @@ fn create_new_func(node: &Node) -> TokenStream {
|
||||
/// Create an accessor methods for a field.
|
||||
fn create_field_method(field: &Field) -> TokenStream {
|
||||
let Field { vis, docs, ident, name, output, .. } = field;
|
||||
if field.inherent() {
|
||||
if field.inherent() || field.synthesized {
|
||||
quote! {
|
||||
#[doc = #docs]
|
||||
#vis fn #ident(&self) -> #output {
|
||||
self.0.field(#name).unwrap().clone().cast().unwrap()
|
||||
self.0.expect_field(#name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -311,7 +316,7 @@ fn create_node_impl(node: &Node) -> TokenStream {
|
||||
let infos = node
|
||||
.fields
|
||||
.iter()
|
||||
.filter(|field| !field.internal)
|
||||
.filter(|field| !field.internal && !field.synthesized)
|
||||
.map(create_param_info);
|
||||
quote! {
|
||||
impl ::typst::model::Node for #ident {
|
||||
@ -395,7 +400,11 @@ fn create_construct_impl(node: &Node) -> TokenStream {
|
||||
let handlers = node
|
||||
.fields
|
||||
.iter()
|
||||
.filter(|field| !field.external && (!field.internal || field.parse.is_some()))
|
||||
.filter(|field| {
|
||||
!field.external
|
||||
&& !field.synthesized
|
||||
&& (!field.internal || field.parse.is_some())
|
||||
})
|
||||
.map(|field| {
|
||||
let with_ident = &field.with_ident;
|
||||
let (prefix, value) = create_field_parser(field);
|
||||
@ -436,6 +445,7 @@ fn create_set_impl(node: &Node) -> TokenStream {
|
||||
.iter()
|
||||
.filter(|field| {
|
||||
!field.external
|
||||
&& !field.synthesized
|
||||
&& field.settable()
|
||||
&& (!field.internal || field.parse.is_some())
|
||||
})
|
||||
|
@ -9,7 +9,7 @@ use super::Module;
|
||||
use crate::diag::SourceResult;
|
||||
use crate::doc::Document;
|
||||
use crate::geom::{Abs, Dir};
|
||||
use crate::model::{Content, NodeId, StyleChain, StyleMap, Vt};
|
||||
use crate::model::{Content, Label, NodeId, StyleChain, StyleMap, Vt};
|
||||
use crate::util::hash128;
|
||||
|
||||
/// Definition of Typst's standard library.
|
||||
@ -60,7 +60,7 @@ pub struct LangItems {
|
||||
/// A hyperlink: `https://typst.org`.
|
||||
pub link: fn(url: EcoString) -> Content,
|
||||
/// A reference: `@target`.
|
||||
pub ref_: fn(target: EcoString) -> Content,
|
||||
pub ref_: fn(target: Label) -> Content,
|
||||
/// A section heading: `= Introduction`.
|
||||
pub heading: fn(level: NonZeroUsize, body: Content) -> Content,
|
||||
/// An item in a bullet list: `- ...`.
|
||||
|
@ -561,7 +561,7 @@ impl Eval for ast::Ref {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
||||
Ok((vm.items.ref_)(self.get().into()))
|
||||
Ok((vm.items.ref_)(Label(self.get().into())))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -974,7 +974,6 @@ impl<'a> CompletionContext<'a> {
|
||||
|
||||
let detail = docs.map(Into::into).or_else(|| match value {
|
||||
Value::Symbol(_) => None,
|
||||
Value::Content(_) => None,
|
||||
Value::Func(func) => {
|
||||
func.info().map(|info| plain_docs_sentence(info.docs).into())
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use once_cell::sync::Lazy;
|
||||
|
||||
use super::{node, Guard, Recipe, Style, StyleMap};
|
||||
use crate::diag::{SourceResult, StrResult};
|
||||
use crate::eval::{cast_from_value, Args, FuncInfo, Str, Value, Vm};
|
||||
use crate::eval::{cast_from_value, Args, Cast, FuncInfo, Str, Value, Vm};
|
||||
use crate::syntax::Span;
|
||||
use crate::util::pretty_array_like;
|
||||
use crate::World;
|
||||
@ -27,7 +27,7 @@ pub struct Content {
|
||||
/// Modifiers that can be attached to content.
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
enum Modifier {
|
||||
Prepared,
|
||||
Synthesized,
|
||||
Guard(Guard),
|
||||
}
|
||||
|
||||
@ -59,6 +59,12 @@ impl Content {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Whether the content is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.to::<SequenceNode>()
|
||||
.map_or(false, |seq| seq.children().is_empty())
|
||||
}
|
||||
|
||||
/// Whether the contained node is of type `T`.
|
||||
pub fn is<T>(&self) -> bool
|
||||
where
|
||||
@ -112,6 +118,21 @@ impl Content {
|
||||
.map(|(_, value)| value)
|
||||
}
|
||||
|
||||
/// Access a field on the content as a specified type.
|
||||
#[track_caller]
|
||||
pub fn cast_field<T: Cast>(&self, name: &str) -> Option<T> {
|
||||
match self.field(name) {
|
||||
Some(value) => Some(value.clone().cast().unwrap()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Expect a field on the content to exist as a specified type.
|
||||
#[track_caller]
|
||||
pub fn expect_field<T: Cast>(&self, name: &str) -> T {
|
||||
self.cast_field(name).unwrap()
|
||||
}
|
||||
|
||||
/// List all fields on the content.
|
||||
pub fn fields(&self) -> &[(EcoString, Value)] {
|
||||
&self.fields
|
||||
@ -209,14 +230,14 @@ impl Content {
|
||||
}
|
||||
|
||||
/// Mark this content as prepared.
|
||||
pub fn prepared(mut self) -> Self {
|
||||
self.modifiers.push(Modifier::Prepared);
|
||||
pub fn synthesized(mut self) -> Self {
|
||||
self.modifiers.push(Modifier::Synthesized);
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether this node was prepared.
|
||||
pub fn is_prepared(&self) -> bool {
|
||||
self.modifiers.contains(&Modifier::Prepared)
|
||||
pub fn is_synthesized(&self) -> bool {
|
||||
self.modifiers.contains(&Modifier::Synthesized)
|
||||
}
|
||||
|
||||
/// Whether no show rule was executed for this node so far.
|
||||
|
@ -3,7 +3,7 @@ use crate::diag::SourceResult;
|
||||
|
||||
/// Whether the target is affected by show rules in the given style chain.
|
||||
pub fn applicable(target: &Content, styles: StyleChain) -> bool {
|
||||
if target.can::<dyn Prepare>() && !target.is_prepared() {
|
||||
if target.can::<dyn Synthesize>() && !target.is_synthesized() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -34,6 +34,18 @@ pub fn realize(
|
||||
// Find out how many recipes there are.
|
||||
let mut n = styles.recipes().count();
|
||||
|
||||
// Synthesize if not already happened for this node.
|
||||
if target.can::<dyn Synthesize>() && !target.is_synthesized() {
|
||||
return Ok(Some(
|
||||
target
|
||||
.clone()
|
||||
.synthesized()
|
||||
.with::<dyn Synthesize>()
|
||||
.unwrap()
|
||||
.synthesize(vt, styles),
|
||||
));
|
||||
}
|
||||
|
||||
// Find an applicable recipe.
|
||||
let mut realized = None;
|
||||
for recipe in styles.recipes() {
|
||||
@ -132,10 +144,10 @@ fn try_apply(
|
||||
}
|
||||
}
|
||||
|
||||
/// Preparations before execution of any show rule.
|
||||
pub trait Prepare {
|
||||
/// Synthesize fields on a node. This happens before execution of any show rule.
|
||||
pub trait Synthesize {
|
||||
/// Prepare the node for show rule application.
|
||||
fn prepare(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content>;
|
||||
fn synthesize(&self, vt: &mut Vt, styles: StyleChain) -> Content;
|
||||
}
|
||||
|
||||
/// The base recipe for a node.
|
||||
|
@ -77,6 +77,11 @@ impl<'a> Vt<'a> {
|
||||
self.provider.identify(hash128(key))
|
||||
}
|
||||
|
||||
/// Whether things are locatable already.
|
||||
pub fn locatable(&self) -> bool {
|
||||
self.introspector.init()
|
||||
}
|
||||
|
||||
/// Locate all metadata matches for the given selector.
|
||||
pub fn locate(&self, selector: Selector) -> Vec<(StableId, &Content)> {
|
||||
self.introspector.locate(selector)
|
||||
@ -115,6 +120,7 @@ impl StabilityProvider {
|
||||
/// Provides access to information about the document.
|
||||
#[doc(hidden)]
|
||||
pub struct Introspector {
|
||||
init: bool,
|
||||
nodes: Vec<(StableId, Content)>,
|
||||
queries: RefCell<Vec<(Selector, u128)>>,
|
||||
}
|
||||
@ -122,7 +128,11 @@ pub struct Introspector {
|
||||
impl Introspector {
|
||||
/// Create a new introspector.
|
||||
fn new() -> Self {
|
||||
Self { nodes: vec![], queries: RefCell::new(vec![]) }
|
||||
Self {
|
||||
init: false,
|
||||
nodes: vec![],
|
||||
queries: RefCell::new(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the information given new frames and return whether we can stop
|
||||
@ -135,14 +145,20 @@ impl Introspector {
|
||||
self.extract(frame, page, Transform::identity());
|
||||
}
|
||||
|
||||
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);
|
||||
if hash128(&nodes) != hash {
|
||||
|
||||
for (selector, hash) in &queries {
|
||||
let nodes = self.locate_impl(selector);
|
||||
if hash128(&nodes) != *hash {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if !was_init && !queries.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@ -161,7 +177,7 @@ impl Introspector {
|
||||
let pos = pos.transform(ts);
|
||||
let mut node = content.clone();
|
||||
let loc = Location { page, pos };
|
||||
node.push_field("loc", loc);
|
||||
node.push_field("location", loc);
|
||||
self.nodes.push((id, node));
|
||||
}
|
||||
}
|
||||
@ -173,6 +189,11 @@ impl Introspector {
|
||||
|
||||
#[comemo::track]
|
||||
impl Introspector {
|
||||
/// Whether this introspector is not yet initialized.
|
||||
fn init(&self) -> bool {
|
||||
self.init
|
||||
}
|
||||
|
||||
/// Locate all metadata matches for the given selector.
|
||||
fn locate(&self, selector: Selector) -> Vec<(StableId, &Content)> {
|
||||
let nodes = self.locate_impl(&selector);
|
||||
|
@ -26,7 +26,7 @@ fn markup(
|
||||
p: &mut Parser,
|
||||
mut at_start: bool,
|
||||
min_indent: usize,
|
||||
mut stop: impl FnMut(SyntaxKind) -> bool,
|
||||
mut stop: impl FnMut(&Parser) -> bool,
|
||||
) {
|
||||
let m = p.marker();
|
||||
let mut nesting: usize = 0;
|
||||
@ -34,7 +34,7 @@ fn markup(
|
||||
match p.current() {
|
||||
SyntaxKind::LeftBracket => nesting += 1,
|
||||
SyntaxKind::RightBracket if nesting > 0 => nesting -= 1,
|
||||
_ if stop(p.current) => break,
|
||||
_ if stop(p) => break,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@ -133,10 +133,10 @@ fn markup_expr(p: &mut Parser, at_start: &mut bool) {
|
||||
fn strong(p: &mut Parser) {
|
||||
let m = p.marker();
|
||||
p.assert(SyntaxKind::Star);
|
||||
markup(p, false, 0, |kind| {
|
||||
kind == SyntaxKind::Star
|
||||
|| kind == SyntaxKind::Parbreak
|
||||
|| kind == SyntaxKind::RightBracket
|
||||
markup(p, false, 0, |p| {
|
||||
p.at(SyntaxKind::Star)
|
||||
|| p.at(SyntaxKind::Parbreak)
|
||||
|| p.at(SyntaxKind::RightBracket)
|
||||
});
|
||||
p.expect(SyntaxKind::Star);
|
||||
p.wrap(m, SyntaxKind::Strong);
|
||||
@ -145,10 +145,10 @@ fn strong(p: &mut Parser) {
|
||||
fn emph(p: &mut Parser) {
|
||||
let m = p.marker();
|
||||
p.assert(SyntaxKind::Underscore);
|
||||
markup(p, false, 0, |kind| {
|
||||
kind == SyntaxKind::Underscore
|
||||
|| kind == SyntaxKind::Parbreak
|
||||
|| kind == SyntaxKind::RightBracket
|
||||
markup(p, false, 0, |p| {
|
||||
p.at(SyntaxKind::Underscore)
|
||||
|| p.at(SyntaxKind::Parbreak)
|
||||
|| p.at(SyntaxKind::RightBracket)
|
||||
});
|
||||
p.expect(SyntaxKind::Underscore);
|
||||
p.wrap(m, SyntaxKind::Emph);
|
||||
@ -158,8 +158,10 @@ fn heading(p: &mut Parser) {
|
||||
let m = p.marker();
|
||||
p.assert(SyntaxKind::HeadingMarker);
|
||||
whitespace_line(p);
|
||||
markup(p, false, usize::MAX, |kind| {
|
||||
kind == SyntaxKind::Label || kind == SyntaxKind::RightBracket
|
||||
markup(p, false, usize::MAX, |p| {
|
||||
p.at(SyntaxKind::Label)
|
||||
|| p.at(SyntaxKind::RightBracket)
|
||||
|| (p.at(SyntaxKind::Space) && p.lexer.clone().next() == SyntaxKind::Label)
|
||||
});
|
||||
p.wrap(m, SyntaxKind::Heading);
|
||||
}
|
||||
@ -169,7 +171,7 @@ fn list_item(p: &mut Parser) {
|
||||
p.assert(SyntaxKind::ListMarker);
|
||||
let min_indent = p.column(p.prev_end());
|
||||
whitespace_line(p);
|
||||
markup(p, false, min_indent, |kind| kind == SyntaxKind::RightBracket);
|
||||
markup(p, false, min_indent, |p| p.at(SyntaxKind::RightBracket));
|
||||
p.wrap(m, SyntaxKind::ListItem);
|
||||
}
|
||||
|
||||
@ -178,7 +180,7 @@ fn enum_item(p: &mut Parser) {
|
||||
p.assert(SyntaxKind::EnumMarker);
|
||||
let min_indent = p.column(p.prev_end());
|
||||
whitespace_line(p);
|
||||
markup(p, false, min_indent, |kind| kind == SyntaxKind::RightBracket);
|
||||
markup(p, false, min_indent, |p| p.at(SyntaxKind::RightBracket));
|
||||
p.wrap(m, SyntaxKind::EnumItem);
|
||||
}
|
||||
|
||||
@ -187,12 +189,12 @@ fn term_item(p: &mut Parser) {
|
||||
p.assert(SyntaxKind::TermMarker);
|
||||
let min_indent = p.column(p.prev_end());
|
||||
whitespace_line(p);
|
||||
markup(p, false, usize::MAX, |kind| {
|
||||
kind == SyntaxKind::Colon || kind == SyntaxKind::RightBracket
|
||||
markup(p, false, usize::MAX, |p| {
|
||||
p.at(SyntaxKind::Colon) || p.at(SyntaxKind::RightBracket)
|
||||
});
|
||||
p.expect(SyntaxKind::Colon);
|
||||
whitespace_line(p);
|
||||
markup(p, false, min_indent, |kind| kind == SyntaxKind::RightBracket);
|
||||
markup(p, false, min_indent, |p| p.at(SyntaxKind::RightBracket));
|
||||
p.wrap(m, SyntaxKind::TermItem);
|
||||
}
|
||||
|
||||
@ -679,7 +681,7 @@ fn content_block(p: &mut Parser) {
|
||||
let m = p.marker();
|
||||
p.enter(LexMode::Markup);
|
||||
p.assert(SyntaxKind::LeftBracket);
|
||||
markup(p, true, 0, |kind| kind == SyntaxKind::RightBracket);
|
||||
markup(p, true, 0, |p| p.at(SyntaxKind::RightBracket));
|
||||
p.expect(SyntaxKind::RightBracket);
|
||||
p.exit();
|
||||
p.wrap(m, SyntaxKind::ContentBlock);
|
||||
|
BIN
tests/ref/meta/ref.png
Normal file
BIN
tests/ref/meta/ref.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -51,8 +51,8 @@ Hello *#x*
|
||||
---
|
||||
// Test conditional set.
|
||||
#show ref: it => {
|
||||
set text(red) if it.target == "unknown"
|
||||
it
|
||||
set text(red) if it.label == <unknown>
|
||||
"@" + str(it.label)
|
||||
}
|
||||
|
||||
@hello from the @unknown
|
||||
|
21
tests/typ/meta/ref.typ
Normal file
21
tests/typ/meta/ref.typ
Normal file
@ -0,0 +1,21 @@
|
||||
// Test references.
|
||||
|
||||
---
|
||||
#set heading(numbering: "1.")
|
||||
|
||||
= Introduction <intro>
|
||||
See @setup.
|
||||
|
||||
== Setup <setup>
|
||||
As seen in @intro, we proceed.
|
||||
|
||||
---
|
||||
// Error: 1-5 label does not exist in the document
|
||||
@foo
|
||||
|
||||
---
|
||||
= First <foo>
|
||||
= Second <foo>
|
||||
|
||||
// Error: 1-5 label occurs multiple times in the document
|
||||
@foo
|
Loading…
x
Reference in New Issue
Block a user