mirror of
https://github.com/typst/typst
synced 2025-05-13 12:36:23 +08:00
Reference supplements
This commit is contained in:
parent
e50189cfa7
commit
2a86e4db0b
@ -11,7 +11,7 @@ pub mod text;
|
||||
pub mod visualize;
|
||||
|
||||
use typst::eval::{LangItems, Library, Module, Scope};
|
||||
use typst::geom::{Align, Color, Dir, GenAlign};
|
||||
use typst::geom::{Align, Color, Dir, GenAlign, Smart};
|
||||
use typst::model::{Node, NodeId, StyleMap};
|
||||
|
||||
use self::layout::LayoutRoot;
|
||||
@ -185,13 +185,21 @@ fn items() -> LangItems {
|
||||
},
|
||||
raw_languages: text::RawNode::languages,
|
||||
link: |url| meta::LinkNode::from_url(url).pack(),
|
||||
ref_: |target| meta::RefNode::new(target).pack(),
|
||||
reference: |target, supplement| {
|
||||
let mut node = meta::RefNode::new(target);
|
||||
if let Some(supplement) = supplement {
|
||||
node.push_supplement(Smart::Custom(Some(meta::Supplement::Content(
|
||||
supplement,
|
||||
))));
|
||||
}
|
||||
node.pack()
|
||||
},
|
||||
heading: |level, title| meta::HeadingNode::new(title).with_level(level).pack(),
|
||||
list_item: |body| layout::ListItem::new(body).pack(),
|
||||
enum_item: |number, body| {
|
||||
let mut node = layout::EnumItem::new(body);
|
||||
if let Some(number) = number {
|
||||
node = node.with_number(Some(number));
|
||||
node.push_number(Some(number));
|
||||
}
|
||||
node.pack()
|
||||
},
|
||||
@ -202,10 +210,10 @@ fn items() -> LangItems {
|
||||
math_attach: |base, bottom, top| {
|
||||
let mut node = math::AttachNode::new(base);
|
||||
if let Some(bottom) = bottom {
|
||||
node = node.with_bottom(Some(bottom));
|
||||
node.push_bottom(Some(bottom));
|
||||
}
|
||||
if let Some(top) = top {
|
||||
node = node.with_top(Some(top));
|
||||
node.push_top(Some(top));
|
||||
}
|
||||
node.pack()
|
||||
},
|
||||
|
@ -15,3 +15,11 @@ pub use self::link::*;
|
||||
pub use self::numbering::*;
|
||||
pub use self::outline::*;
|
||||
pub use self::reference::*;
|
||||
|
||||
use typst::doc::Lang;
|
||||
|
||||
/// The named with which an element is referenced.
|
||||
pub trait LocalName {
|
||||
/// Get the name in the given language.
|
||||
fn local_name(&self, lang: Lang) -> &'static str;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use super::{FigureNode, HeadingNode, Numbering};
|
||||
use super::{FigureNode, HeadingNode, LocalName, Numbering};
|
||||
use crate::prelude::*;
|
||||
use crate::text::TextNode;
|
||||
|
||||
@ -41,11 +41,14 @@ pub struct RefNode {
|
||||
#[required]
|
||||
pub target: Label,
|
||||
|
||||
/// The prefix before the referenced number.
|
||||
/// A supplement for the reference.
|
||||
///
|
||||
/// For references to headings or figures, this is added before the
|
||||
/// referenced number. For citations, this can be used to add a page number.
|
||||
///
|
||||
/// ```example
|
||||
/// #set heading(numbering: "1.")
|
||||
/// #set ref(prefix: it => {
|
||||
/// #set ref(supplement: it => {
|
||||
/// if it.func() == heading {
|
||||
/// "Chapter"
|
||||
/// } else {
|
||||
@ -57,11 +60,11 @@ pub struct RefNode {
|
||||
/// 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>,
|
||||
pub supplement: Smart<Option<Supplement>>,
|
||||
}
|
||||
|
||||
impl Synthesize for RefNode {
|
||||
@ -90,34 +93,36 @@ impl Show for RefNode {
|
||||
}
|
||||
};
|
||||
|
||||
let mut prefix = match self.prefix(styles) {
|
||||
let supplement = self.supplement(styles);
|
||||
let mut supplement = match supplement {
|
||||
Smart::Auto => target
|
||||
.with::<dyn LocalName>()
|
||||
.map(|node| node.local_name(TextNode::lang_in(styles)))
|
||||
.map(TextNode::packed)
|
||||
.unwrap_or_default(),
|
||||
Smart::Custom(None) => Content::empty(),
|
||||
Smart::Custom(Some(func)) => {
|
||||
Smart::Custom(Some(Supplement::Content(content))) => content.clone(),
|
||||
Smart::Custom(Some(Supplement::Func(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}');
|
||||
if !supplement.is_empty() {
|
||||
supplement += 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)?
|
||||
numbered(vt, supplement, &numbering, &numbers)?
|
||||
} else {
|
||||
bail!(self.span(), "cannot reference unnumbered heading");
|
||||
}
|
||||
} else if let Some(figure) = target.to::<FigureNode>() {
|
||||
if let Some(numbering) = figure.numbering(StyleChain::default()) {
|
||||
let number = figure.number().unwrap();
|
||||
numbered(vt, prefix, &numbering, &[number])?
|
||||
numbered(vt, supplement, &numbering, &[number])?
|
||||
} else {
|
||||
bail!(self.span(), "cannot reference unnumbered figure");
|
||||
}
|
||||
@ -146,8 +151,21 @@ fn numbered(
|
||||
})
|
||||
}
|
||||
|
||||
/// The named with which an element is referenced.
|
||||
pub trait LocalName {
|
||||
/// Get the name in the given language.
|
||||
fn local_name(&self, lang: Lang) -> &'static str;
|
||||
/// Additional content for a reference.
|
||||
pub enum Supplement {
|
||||
Content(Content),
|
||||
Func(Func),
|
||||
}
|
||||
|
||||
cast_from_value! {
|
||||
Supplement,
|
||||
v: Content => Self::Content(v),
|
||||
v: Func => Self::Func(v),
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: Supplement => match v {
|
||||
Supplement::Content(v) => v.into(),
|
||||
Supplement::Func(v) => v.into(),
|
||||
}
|
||||
}
|
||||
|
@ -433,19 +433,19 @@ fn create_construct_impl(node: &Node) -> TokenStream {
|
||||
&& (!field.internal || field.parse.is_some())
|
||||
})
|
||||
.map(|field| {
|
||||
let with_ident = &field.with_ident;
|
||||
let push_ident = &field.push_ident;
|
||||
let (prefix, value) = create_field_parser(field);
|
||||
if field.settable() {
|
||||
quote! {
|
||||
#prefix
|
||||
if let Some(value) = #value {
|
||||
node = node.#with_ident(value);
|
||||
node.#push_ident(value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#prefix
|
||||
node = node.#with_ident(#value);
|
||||
node.#push_ident(#value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -59,8 +59,8 @@ pub struct LangItems {
|
||||
pub raw_languages: fn() -> Vec<(&'static str, Vec<&'static str>)>,
|
||||
/// A hyperlink: `https://typst.org`.
|
||||
pub link: fn(url: EcoString) -> Content,
|
||||
/// A reference: `@target`.
|
||||
pub ref_: fn(target: Label) -> Content,
|
||||
/// A reference: `@target`, `@target[..]`.
|
||||
pub reference: fn(target: Label, supplement: Option<Content>) -> Content,
|
||||
/// A section heading: `= Introduction`.
|
||||
pub heading: fn(level: NonZeroUsize, body: Content) -> Content,
|
||||
/// An item in a bullet list: `- ...`.
|
||||
@ -106,7 +106,7 @@ impl Hash for LangItems {
|
||||
self.emph.hash(state);
|
||||
self.raw.hash(state);
|
||||
self.link.hash(state);
|
||||
self.ref_.hash(state);
|
||||
self.reference.hash(state);
|
||||
self.heading.hash(state);
|
||||
self.list_item.hash(state);
|
||||
self.enum_item.hash(state);
|
||||
|
@ -567,7 +567,9 @@ impl Eval for ast::Ref {
|
||||
type Output = Content;
|
||||
|
||||
fn eval(&self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
||||
Ok((vm.items.ref_)(Label(self.get().into())))
|
||||
let label = Label(self.target().into());
|
||||
let supplement = self.supplement().map(|block| block.eval(vm)).transpose()?;
|
||||
Ok((vm.items.reference)(label, supplement))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,6 +129,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Tag> {
|
||||
SyntaxKind::Link => Some(Tag::Link),
|
||||
SyntaxKind::Label => Some(Tag::Label),
|
||||
SyntaxKind::Ref => Some(Tag::Ref),
|
||||
SyntaxKind::RefMarker => None,
|
||||
SyntaxKind::Heading => Some(Tag::Heading),
|
||||
SyntaxKind::HeadingMarker => None,
|
||||
SyntaxKind::ListItem => None,
|
||||
|
@ -101,7 +101,7 @@ pub enum Expr {
|
||||
Link(Link),
|
||||
/// A label: `<intro>`.
|
||||
Label(Label),
|
||||
/// A reference: `@target`.
|
||||
/// A reference: `@target`, `@target[..]`.
|
||||
Ref(Ref),
|
||||
/// A section heading: `= Introduction`.
|
||||
Heading(Heading),
|
||||
@ -604,14 +604,23 @@ impl Label {
|
||||
}
|
||||
|
||||
node! {
|
||||
/// A reference: `@target`.
|
||||
/// A reference: `@target`, `@target[..]`.
|
||||
Ref
|
||||
}
|
||||
|
||||
impl Ref {
|
||||
/// Get the target.
|
||||
pub fn get(&self) -> &str {
|
||||
self.0.text().trim_start_matches('@')
|
||||
pub fn target(&self) -> &str {
|
||||
self.0
|
||||
.children()
|
||||
.find(|node| node.kind() == SyntaxKind::RefMarker)
|
||||
.map(|node| node.text().trim_start_matches('@'))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the supplement.
|
||||
pub fn supplement(&self) -> Option<ContentBlock> {
|
||||
self.0.cast_last_match()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,8 +36,10 @@ pub enum SyntaxKind {
|
||||
Link,
|
||||
/// A label: `<intro>`.
|
||||
Label,
|
||||
/// A reference: `@target`.
|
||||
/// A reference: `@target`, `@target[..]`.
|
||||
Ref,
|
||||
/// Introduces a reference: `@target`.
|
||||
RefMarker,
|
||||
/// A section heading: `= Introduction`.
|
||||
Heading,
|
||||
/// Introduces a section heading: `=`, `==`, ...
|
||||
@ -324,12 +326,14 @@ impl SyntaxKind {
|
||||
Self::Parbreak => "paragraph break",
|
||||
Self::Escape => "escape sequence",
|
||||
Self::Shorthand => "shorthand",
|
||||
Self::SmartQuote => "smart quote",
|
||||
Self::Strong => "strong content",
|
||||
Self::Emph => "emphasized content",
|
||||
Self::Raw => "raw block",
|
||||
Self::Link => "link",
|
||||
Self::Label => "label",
|
||||
Self::Ref => "reference",
|
||||
Self::RefMarker => "reference marker",
|
||||
Self::Heading => "heading",
|
||||
Self::HeadingMarker => "heading marker",
|
||||
Self::ListItem => "list item",
|
||||
@ -358,7 +362,6 @@ impl SyntaxKind {
|
||||
Self::Star => "star",
|
||||
Self::Underscore => "underscore",
|
||||
Self::Dollar => "dollar sign",
|
||||
Self::SmartQuote => "smart quote",
|
||||
Self::Plus => "plus",
|
||||
Self::Minus => "minus",
|
||||
Self::Slash => "slash",
|
||||
|
@ -172,7 +172,7 @@ impl Lexer<'_> {
|
||||
'h' if self.s.eat_if("ttps://") => self.link(),
|
||||
'0'..='9' => self.numbering(start),
|
||||
'<' if self.s.at(is_id_continue) => self.label(),
|
||||
'@' if self.s.at(is_id_continue) => self.reference(),
|
||||
'@' => self.ref_marker(),
|
||||
|
||||
'.' if self.s.eat_if("..") => SyntaxKind::Shorthand,
|
||||
'-' if self.s.eat_if("--") => SyntaxKind::Shorthand,
|
||||
@ -297,6 +297,11 @@ impl Lexer<'_> {
|
||||
self.text()
|
||||
}
|
||||
|
||||
fn ref_marker(&mut self) -> SyntaxKind {
|
||||
self.s.eat_while(is_id_continue);
|
||||
SyntaxKind::RefMarker
|
||||
}
|
||||
|
||||
fn label(&mut self) -> SyntaxKind {
|
||||
let label = self.s.eat_while(is_id_continue);
|
||||
if label.is_empty() {
|
||||
@ -310,11 +315,6 @@ impl Lexer<'_> {
|
||||
SyntaxKind::Label
|
||||
}
|
||||
|
||||
fn reference(&mut self) -> SyntaxKind {
|
||||
self.s.eat_while(is_id_continue);
|
||||
SyntaxKind::Ref
|
||||
}
|
||||
|
||||
fn text(&mut self) -> SyntaxKind {
|
||||
macro_rules! table {
|
||||
($(|$c:literal)*) => {
|
||||
|
@ -104,8 +104,7 @@ fn markup_expr(p: &mut Parser, at_start: &mut bool) {
|
||||
| SyntaxKind::SmartQuote
|
||||
| SyntaxKind::Raw
|
||||
| SyntaxKind::Link
|
||||
| SyntaxKind::Label
|
||||
| SyntaxKind::Ref => p.eat(),
|
||||
| SyntaxKind::Label => p.eat(),
|
||||
|
||||
SyntaxKind::Hashtag => embedded_code_expr(p),
|
||||
SyntaxKind::Star => strong(p),
|
||||
@ -114,6 +113,7 @@ fn markup_expr(p: &mut Parser, at_start: &mut bool) {
|
||||
SyntaxKind::ListMarker if *at_start => list_item(p),
|
||||
SyntaxKind::EnumMarker if *at_start => enum_item(p),
|
||||
SyntaxKind::TermMarker if *at_start => term_item(p),
|
||||
SyntaxKind::RefMarker => reference(p),
|
||||
SyntaxKind::Dollar => formula(p),
|
||||
|
||||
SyntaxKind::LeftBracket
|
||||
@ -198,6 +198,15 @@ fn term_item(p: &mut Parser) {
|
||||
p.wrap(m, SyntaxKind::TermItem);
|
||||
}
|
||||
|
||||
fn reference(p: &mut Parser) {
|
||||
let m = p.marker();
|
||||
p.assert(SyntaxKind::RefMarker);
|
||||
if p.directly_at(SyntaxKind::LeftBracket) {
|
||||
content_block(p);
|
||||
}
|
||||
p.wrap(m, SyntaxKind::Ref);
|
||||
}
|
||||
|
||||
fn whitespace_line(p: &mut Parser) {
|
||||
while !p.newline() && p.current().is_trivia() {
|
||||
p.eat();
|
||||
|
Loading…
x
Reference in New Issue
Block a user