From d94acd615e5bde7f6d131be351e145477e515721 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 30 Sep 2024 14:45:44 +0200 Subject: [PATCH] Add internal URL type (#5074) --- crates/typst-ide/src/jump.rs | 5 +- crates/typst/src/eval/markup.rs | 7 ++- crates/typst/src/model/bibliography.rs | 78 +++++++++++++++----------- crates/typst/src/model/link.rs | 50 +++++++++++++++-- 4 files changed, 94 insertions(+), 46 deletions(-) diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs index d7f2e1d2b..b798defab 100644 --- a/crates/typst-ide/src/jump.rs +++ b/crates/typst-ide/src/jump.rs @@ -1,8 +1,7 @@ use std::num::NonZeroUsize; -use ecow::EcoString; use typst::layout::{Frame, FrameItem, Point, Position, Size}; -use typst::model::{Destination, Document}; +use typst::model::{Destination, Document, Url}; use typst::syntax::{FileId, LinkedNode, Side, Source, Span, SyntaxKind}; use typst::visualize::Geometry; use typst::World; @@ -13,7 +12,7 @@ pub enum Jump { /// Jump to a position in a source file. Source(FileId, usize), /// Jump to an external URL. - Url(EcoString), + Url(Url), /// Jump to a point on a page. Position(Position), } diff --git a/crates/typst/src/eval/markup.rs b/crates/typst/src/eval/markup.rs index a735bb8e2..42fede1c0 100644 --- a/crates/typst/src/eval/markup.rs +++ b/crates/typst/src/eval/markup.rs @@ -1,4 +1,4 @@ -use crate::diag::{warning, SourceResult}; +use crate::diag::{warning, At, SourceResult}; use crate::eval::{Eval, Vm}; use crate::foundations::{ Content, Label, NativeElement, Repr, Smart, Unlabellable, Value, @@ -6,7 +6,7 @@ use crate::foundations::{ use crate::math::EquationElem; use crate::model::{ EmphElem, EnumItem, HeadingElem, LinkElem, ListItem, ParbreakElem, RefElem, - StrongElem, Supplement, TermItem, + StrongElem, Supplement, TermItem, Url, }; use crate::symbols::Symbol; use crate::syntax::ast::{self, AstNode}; @@ -195,7 +195,8 @@ impl Eval for ast::Link<'_> { type Output = Content; fn eval(self, _: &mut Vm) -> SourceResult { - Ok(LinkElem::from_url(self.get().clone()).pack()) + let url = Url::new(self.get().clone()).at(self.span())?; + Ok(LinkElem::from_url(url).pack()) } } diff --git a/crates/typst/src/model/bibliography.rs b/crates/typst/src/model/bibliography.rs index 11454804d..d6b81a20a 100644 --- a/crates/typst/src/model/bibliography.rs +++ b/crates/typst/src/model/bibliography.rs @@ -34,6 +34,7 @@ use crate::layout::{ }; use crate::model::{ CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, + Url, }; use crate::syntax::{Span, Spanned}; @@ -741,8 +742,8 @@ impl<'a> Generator<'a> { /// Displays hayagriva's output as content for the citations and references. fn display(&mut self, rendered: &hayagriva::Rendered) -> StrResult { - let citations = self.display_citations(rendered); - let references = self.display_references(rendered); + let citations = self.display_citations(rendered)?; + let references = self.display_references(rendered)?; let hanging_indent = rendered.bibliography.as_ref().is_some_and(|b| b.hanging_indent); Ok(Works { citations, references, hanging_indent }) @@ -752,7 +753,7 @@ impl<'a> Generator<'a> { fn display_citations( &mut self, rendered: &hayagriva::Rendered, - ) -> HashMap> { + ) -> StrResult>> { // Determine for each citation key where in the bibliography it is, // so that we can link there. let mut links = HashMap::new(); @@ -779,7 +780,7 @@ impl<'a> Generator<'a> { Content::empty() } else { let mut content = - renderer.display_elem_children(&citation.citation, &mut None); + renderer.display_elem_children(&citation.citation, &mut None)?; if info.footnote { content = FootnoteElem::with_content(content).pack(); @@ -791,15 +792,16 @@ impl<'a> Generator<'a> { output.insert(info.location, Ok(content)); } - output + Ok(output) } /// Display the bibliography references. + #[allow(clippy::type_complexity)] fn display_references( &self, rendered: &hayagriva::Rendered, - ) -> Option, Content)>> { - let rendered = rendered.bibliography.as_ref()?; + ) -> StrResult, Content)>>> { + let Some(rendered) = &rendered.bibliography else { return Ok(None) }; // Determine for each citation key where it first occurred, so that we // can link there. @@ -829,18 +831,22 @@ impl<'a> Generator<'a> { let backlink = location.variant(k + 1); // Render the first field. - let mut prefix = item.first_field.as_ref().map(|elem| { - let mut content = renderer.display_elem_child(elem, &mut None); - if let Some(location) = first_occurrences.get(item.key.as_str()) { - let dest = Destination::Location(*location); - content = content.linked(dest); - } - content - }); + let mut prefix = item + .first_field + .as_ref() + .map(|elem| { + let mut content = renderer.display_elem_child(elem, &mut None)?; + if let Some(location) = first_occurrences.get(item.key.as_str()) { + let dest = Destination::Location(*location); + content = content.linked(dest); + } + StrResult::Ok(content) + }) + .transpose()?; // Render the main reference content. let mut reference = - renderer.display_elem_children(&item.content, &mut prefix); + renderer.display_elem_children(&item.content, &mut prefix)?; // Attach a backlink to either the prefix or the reference so that // we can link to the bibliography entry. @@ -849,7 +855,7 @@ impl<'a> Generator<'a> { output.push((prefix, reference)); } - Some(output) + Ok(Some(output)) } } @@ -874,10 +880,14 @@ impl ElemRenderer<'_> { &self, elems: &hayagriva::ElemChildren, prefix: &mut Option, - ) -> Content { - Content::sequence( - elems.0.iter().map(|elem| self.display_elem_child(elem, prefix)), - ) + ) -> StrResult { + Ok(Content::sequence( + elems + .0 + .iter() + .map(|elem| self.display_elem_child(elem, prefix)) + .collect::>>()?, + )) } /// Display a rendered hayagriva element. @@ -885,16 +895,16 @@ impl ElemRenderer<'_> { &self, elem: &hayagriva::ElemChild, prefix: &mut Option, - ) -> Content { - match elem { + ) -> StrResult { + Ok(match elem { hayagriva::ElemChild::Text(formatted) => self.display_formatted(formatted), - hayagriva::ElemChild::Elem(elem) => self.display_elem(elem, prefix), + hayagriva::ElemChild::Elem(elem) => self.display_elem(elem, prefix)?, hayagriva::ElemChild::Markup(markup) => self.display_math(markup), - hayagriva::ElemChild::Link { text, url } => self.display_link(text, url), + hayagriva::ElemChild::Link { text, url } => self.display_link(text, url)?, hayagriva::ElemChild::Transparent { cite_idx, format } => { self.display_transparent(*cite_idx, format) } - } + }) } /// Display a block-level element. @@ -902,7 +912,7 @@ impl ElemRenderer<'_> { &self, elem: &hayagriva::Elem, prefix: &mut Option, - ) -> Content { + ) -> StrResult { use citationberg::Display; let block_level = matches!(elem.display, Some(Display::Block | Display::Indent)); @@ -911,7 +921,7 @@ impl ElemRenderer<'_> { let mut content = self.display_elem_children( &elem.children, if block_level { &mut suf_prefix } else { prefix }, - ); + )?; if let Some(prefix) = suf_prefix { const COLUMN_GUTTER: Em = Em::new(0.65); @@ -941,7 +951,7 @@ impl ElemRenderer<'_> { } Some(Display::LeftMargin) => { *prefix.get_or_insert_with(Default::default) += content; - return Content::empty(); + return Ok(Content::empty()); } _ => {} } @@ -953,7 +963,7 @@ impl ElemRenderer<'_> { } } - content + Ok(content) } /// Display math. @@ -964,11 +974,11 @@ impl ElemRenderer<'_> { } /// Display a link. - fn display_link(&self, text: &hayagriva::Formatted, url: &str) -> Content { - let dest = Destination::Url(url.into()); - LinkElem::new(dest.into(), self.display_formatted(text)) + fn display_link(&self, text: &hayagriva::Formatted, url: &str) -> StrResult { + let dest = Destination::Url(Url::new(url)?); + Ok(LinkElem::new(dest.into(), self.display_formatted(text)) .pack() - .spanned(self.span) + .spanned(self.span)) } /// Display transparent pass-through content. diff --git a/crates/typst/src/model/link.rs b/crates/typst/src/model/link.rs index 107b0d9ae..b583a6fd0 100644 --- a/crates/typst/src/model/link.rs +++ b/crates/typst/src/model/link.rs @@ -1,7 +1,9 @@ +use std::ops::Deref; + use ecow::{eco_format, EcoString}; use smallvec::SmallVec; -use crate::diag::{At, SourceResult}; +use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, Content, Label, Packed, Repr, Show, Smart, StyleChain, @@ -89,7 +91,7 @@ pub struct LinkElem { impl LinkElem { /// Create a link element from a URL with its bare text. - pub fn from_url(url: EcoString) -> Self { + pub fn from_url(url: Url) -> Self { let body = body_from_url(&url); Self::new(LinkTarget::Dest(Destination::Url(url)), body) } @@ -112,13 +114,13 @@ impl Show for Packed { } } -fn body_from_url(url: &EcoString) -> Content { +fn body_from_url(url: &Url) -> Content { let mut text = url.as_str(); for prefix in ["mailto:", "tel:"] { text = text.trim_start_matches(prefix); } let shorter = text.len() < url.len(); - TextElem::packed(if shorter { text.into() } else { url.clone() }) + TextElem::packed(if shorter { text.into() } else { (**url).clone() }) } /// A target where a link can go. @@ -148,13 +150,15 @@ impl From for LinkTarget { #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Destination { /// A link to a URL. - Url(EcoString), + Url(Url), /// A link to a point on a page. Position(Position), /// An unresolved link to a location in the document. Location(Location), } +impl Destination {} + impl Repr for Destination { fn repr(&self) -> EcoString { eco_format!("{self:?}") @@ -168,7 +172,41 @@ cast! { Self::Position(v) => v.into_value(), Self::Location(v) => v.into_value(), }, - v: EcoString => Self::Url(v), + v: Url => Self::Url(v), v: Position => Self::Position(v), v: Location => Self::Location(v), } + +/// A uniform resource locator with a maximum length. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Url(EcoString); + +impl Url { + /// Create an URL from a string, checking the maximum length. + pub fn new(url: impl Into) -> StrResult { + let url = url.into(); + if url.len() > 8000 { + bail!("URL is too long") + } + Ok(Self(url)) + } + + /// Extract the underlying [`EcoString`]. + pub fn into_inner(self) -> EcoString { + self.0 + } +} + +impl Deref for Url { + type Target = EcoString; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +cast! { + Url, + self => self.0.into_value(), + v: EcoString => Self::new(v)?, +}