diff --git a/crates/typst/src/model/cite.rs b/crates/typst/src/model/cite.rs index e156e0ec9..fea4fcea0 100644 --- a/crates/typst/src/model/cite.rs +++ b/crates/typst/src/model/cite.rs @@ -1,3 +1,5 @@ +use ecow::eco_format; + use crate::diag::{error, At, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ @@ -5,7 +7,7 @@ use crate::foundations::{ }; use crate::introspection::Locatable; use crate::model::bibliography::Works; -use crate::model::CslStyle; +use crate::model::{unresolved_reference, BibliographyElem, CslStyle}; use crate::text::{Lang, Region, TextElem}; /// Cite a work from the bibliography. @@ -40,7 +42,7 @@ use crate::text::{Lang, Region, TextElem}; /// This function indirectly has dedicated syntax. [References]($ref) can be /// used to cite works from the bibliography. The label then corresponds to the /// citation key. -#[elem(Synthesize)] +#[elem(Synthesize, Show)] pub struct CiteElem { /// The citation key that identifies the entry in the bibliography that /// shall be cited, as a label. @@ -117,6 +119,26 @@ impl Synthesize for Packed { } } +impl Show for Packed { + #[typst_macros::time(name = "cite", span = self.span())] + fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { + if !BibliographyElem::has(engine, self.key) { + return Ok(unresolved_reference( + engine, + eco_format!( + "key `{}` does not exist in the bibliography", + self.key.as_str() + ), + "cite", + self.key, + self.span(), + )); + } + + Ok(self.clone().pack()) + } +} + cast! { CiteElem, v: Content => v.unpack::().map_err(|_| "expected citation")?, diff --git a/crates/typst/src/model/reference.rs b/crates/typst/src/model/reference.rs index 8194eec53..33ae14806 100644 --- a/crates/typst/src/model/reference.rs +++ b/crates/typst/src/model/reference.rs @@ -1,18 +1,20 @@ use comemo::Track; -use ecow::eco_format; +use ecow::{eco_format, EcoString}; -use crate::diag::{bail, At, Hint, SourceResult}; +use crate::diag::{bail, At, Hint, SourceDiagnostic, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Content, Context, Func, IntoValue, Label, NativeElement, Packed, Show, - Smart, StyleChain, Synthesize, + cast, elem, Content, Context, Func, IntoValue, Label, NativeElement, Packed, Repr, + Show, Smart, StyleChain, Synthesize, }; -use crate::introspection::{Counter, Locatable}; +use crate::introspection::{Counter, Locatable, QueryError}; use crate::math::EquationElem; use crate::model::{ BibliographyElem, CiteElem, Destination, Figurable, FootnoteElem, Numbering, }; -use crate::text::TextElem; +use crate::syntax::{is_valid_label_literal, Span}; +use crate::text::{RawContent, RawElem, TextElem}; +use crate::visualize::Color; /// A reference to a label or bibliography. /// @@ -163,22 +165,25 @@ impl Synthesize for Packed { impl Show for Packed { #[typst_macros::time(name = "ref", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let target = *self.target(); - let elem = engine.introspector.query_label(target); let span = self.span(); + let result = engine.introspector.query_label(self.target); - if BibliographyElem::has(engine, target) { - if elem.is_ok() { + if BibliographyElem::has(engine, self.target) { + if result.is_ok() { bail!(span, "label occurs in the document and its bibliography"); } return Ok(to_citation(self, engine, styles)?.pack().spanned(span)); } - let elem = elem.at(span)?; + if let Err(error @ QueryError::MissingLabel(_)) = result { + return Ok(unresolved_reference(engine, error, "ref", self.target, span)); + } + + let elem = result.at(span)?; if let Some(footnote) = elem.to_packed::() { - return Ok(footnote.into_ref(target).pack().spanned(span)); + return Ok(footnote.into_ref(self.target).pack().spanned(span)); } let elem = elem.clone(); @@ -305,3 +310,26 @@ pub trait Refable { /// Returns the numbering of this element. fn numbering(&self) -> Option<&Numbering>; } + +/// Generates a warning for an unresolved reference and returns placeholder +/// content. +pub(crate) fn unresolved_reference( + engine: &mut Engine, + message: impl Into, + func: &str, + target: Label, + span: Span, +) -> Content { + engine.sink.warn(SourceDiagnostic::warning(span, message)); + + let text = if is_valid_label_literal(target.as_str()) { + eco_format!("@{}", target.as_str()) + } else { + eco_format!("#{func}(label({}))", target.as_str().repr()) + }; + + return RawElem::new(RawContent::Text(text)) + .pack() + .spanned(span) + .styled(TextElem::set_fill(Color::RED.into())); +} diff --git a/tests/ref/cite-missing.png b/tests/ref/cite-missing.png new file mode 100644 index 000000000..beb97f587 Binary files /dev/null and b/tests/ref/cite-missing.png differ diff --git a/tests/ref/label-multiple-ignored-warn.png b/tests/ref/label-multiple-ignored-warn.png new file mode 100644 index 000000000..f623f3a72 Binary files /dev/null and b/tests/ref/label-multiple-ignored-warn.png differ diff --git a/tests/ref/ref-label-complex-missing.png b/tests/ref/ref-label-complex-missing.png new file mode 100644 index 000000000..933721a2e Binary files /dev/null and b/tests/ref/ref-label-complex-missing.png differ diff --git a/tests/ref/ref-label-missing.png b/tests/ref/ref-label-missing.png new file mode 100644 index 000000000..22596df72 Binary files /dev/null and b/tests/ref/ref-label-missing.png differ diff --git a/tests/suite/foundations/label.typ b/tests/suite/foundations/label.typ index af6d23803..f662695a0 100644 --- a/tests/suite/foundations/label.typ +++ b/tests/suite/foundations/label.typ @@ -72,16 +72,16 @@ _Visible_ // Hint: 1-8 only the last label is used, the rest are ignored = Hello -// Warning: 12-19 content labelled multiple times -// Hint: 12-19 only the last label is used, the rest are ignored -#let f = [#block()] +// Warning: 12-26 content labelled multiple times +// Hint: 12-26 only the last label is used, the rest are ignored +#let f = [#metadata(none)] #f -// Warning: 6-13 content labelled multiple times -// Hint: 6-13 only the last label is used, the rest are ignored -#[#[#block()]] +// Warning: 6-20 content labelled multiple times +// Hint: 6-20 only the last label is used, the rest are ignored +#[#[#metadata(none)]] -// Error: 1-3 label `` does not exist in the document +// Warning: 1-3 label `` does not exist in the document @a --- label-unattached-warn --- diff --git a/tests/suite/model/cite.typ b/tests/suite/model/cite.typ index ffbd3b52f..34f91f4ca 100644 --- a/tests/suite/model/cite.typ +++ b/tests/suite/model/cite.typ @@ -49,6 +49,17 @@ A @netwok @arrgh @quark, B. #show bibliography: none #bibliography("/assets/bib/works.bib") +--- cite-missing --- +// Warning: 2-15 key `peter` does not exist in the bibliography +// Warning: 31-37 label `` does not exist in the document +#cite() @netwok @arrgh @extra + +// Warning: 2-20 key `>?&` does not exist in the bibliography +#cite(label(">?&")) + +#show bibliography: none +#bibliography("/assets/bib/works.bib") + --- issue-785-cite-locate --- // Test citation in other introspection. #set page(width: 180pt) diff --git a/tests/suite/model/ref.typ b/tests/suite/model/ref.typ index cc6d6bf11..c8ff80674 100644 --- a/tests/suite/model/ref.typ +++ b/tests/suite/model/ref.typ @@ -10,9 +10,15 @@ See @setup. As seen in @intro, we proceed. --- ref-label-missing --- -// Error: 1-5 label `` does not exist in the document +// Warning: 1-5 label `` does not exist in the document @foo +--- ref-label-complex-missing --- +#set page(width: auto) + +// Warning: 2-28 label `label("is;/"bad%//#")` does not exist in the document +#ref(label("is;\"bad%//#")) + --- ref-label-duplicate --- = First = Second