use std::ops::Deref;
use std::str::FromStr;
use comemo::Tracked;
use ecow::{EcoString, eco_format};
use crate::diag::{SourceResult, StrResult, bail};
use crate::engine::Engine;
use crate::foundations::{
Args, Construct, Content, Label, Packed, Repr, Selector, ShowSet, Smart, StyleChain,
Styles, cast, elem,
};
use crate::introspection::{
Counter, CounterKey, Introspector, Locatable, Location, Tagged,
};
use crate::layout::{PageElem, Position};
use crate::model::{NumberingPattern, Refable};
use crate::text::{LocalName, TextElem};
#[elem(Locatable)]
pub struct LinkElem {
#[required]
#[parse(
let dest = args.expect::<LinkTarget>("destination")?;
dest.clone()
)]
pub dest: LinkTarget,
#[required]
#[parse(match &dest {
LinkTarget::Dest(Destination::Url(url)) => match args.eat()? {
Some(body) => body,
None => body_from_url(url),
},
_ => args.expect("body")?,
})]
pub body: Content,
#[internal]
#[ghost]
pub current: Option<Destination>,
}
impl LinkElem {
pub fn from_url(url: Url) -> Self {
let body = body_from_url(&url);
Self::new(LinkTarget::Dest(Destination::Url(url)), body)
}
}
impl ShowSet for Packed<LinkElem> {
fn show_set(&self, _: StyleChain) -> Styles {
let mut out = Styles::new();
out.set(TextElem::hyphenate, Smart::Custom(false));
out
}
}
pub(crate) fn body_from_url(url: &Url) -> Content {
let stripped = url.strip_contact_scheme().map(|(_, s)| s.into());
TextElem::packed(stripped.unwrap_or_else(|| url.clone().into_inner()))
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum LinkTarget {
Dest(Destination),
Label(Label),
}
impl LinkTarget {
pub fn resolve(&self, introspector: Tracked<Introspector>) -> StrResult<Destination> {
Ok(match self {
LinkTarget::Dest(dest) => dest.clone(),
LinkTarget::Label(label) => {
let elem = introspector.query_label(*label)?;
Destination::Location(elem.location().unwrap())
}
})
}
}
cast! {
LinkTarget,
self => match self {
Self::Dest(v) => v.into_value(),
Self::Label(v) => v.into_value(),
},
v: Destination => Self::Dest(v),
v: Label => Self::Label(v),
}
impl From<Destination> for LinkTarget {
fn from(dest: Destination) -> Self {
Self::Dest(dest)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Destination {
Url(Url),
Position(Position),
Location(Location),
}
impl Destination {
pub fn alt_text(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<EcoString> {
match self {
Destination::Url(url) => {
let contact = url.strip_contact_scheme().map(|(scheme, stripped)| {
eco_format!("{} {stripped}", scheme.local_name_in(styles))
});
Ok(contact.unwrap_or_else(|| url.clone().into_inner()))
}
Destination::Position(pos) => {
let page_nr = eco_format!("{}", pos.page.get());
let page_str = PageElem::local_name_in(styles);
Ok(eco_format!("{page_str} {page_nr}"))
}
&Destination::Location(loc) => {
let fallback = |engine: &mut Engine| {
let numbering = loc.page_numbering(engine).unwrap_or_else(|| {
NumberingPattern::from_str("1").unwrap().into()
});
let page_nr = Counter::new(CounterKey::Page)
.display_at_loc(engine, loc, styles, &numbering)?
.plain_text();
let page_str = PageElem::local_name_in(styles);
Ok(eco_format!("{page_str} {page_nr}"))
};
let loc_selector = Selector::Location(loc);
if let Some(elem) = engine.introspector.query_first(&loc_selector)
&& let Some(refable) = elem.with::<dyn Refable>()
{
let counter = refable.counter();
let supplement = refable.supplement().plain_text();
if let Some(numbering) = refable.numbering() {
let numbers = counter.display_at_loc(
engine,
loc,
styles,
&numbering.clone().trimmed(),
)?;
return Ok(eco_format!("{supplement} {}", numbers.plain_text()));
} else {
let page_ref = fallback(engine)?;
return Ok(eco_format!("{supplement}, {page_ref}"));
}
}
fallback(engine)
}
}
}
}
impl Repr for Destination {
fn repr(&self) -> EcoString {
eco_format!("{self:?}")
}
}
cast! {
Destination,
self => match self {
Self::Url(v) => v.into_value(),
Self::Position(v) => v.into_value(),
Self::Location(v) => v.into_value(),
},
v: Url => Self::Url(v),
v: Position => Self::Position(v),
v: Location => Self::Location(v),
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Url(EcoString);
impl Url {
pub fn new(url: impl Into<EcoString>) -> StrResult<Self> {
let url = url.into();
if url.len() > 8000 {
bail!("URL is too long")
} else if url.is_empty() {
bail!("URL must not be empty")
}
Ok(Self(url))
}
pub fn into_inner(self) -> EcoString {
self.0
}
pub fn strip_contact_scheme(&self) -> Option<(UrlContactScheme, &str)> {
[UrlContactScheme::Mailto, UrlContactScheme::Tel]
.into_iter()
.find_map(|scheme| {
let stripped = self.strip_prefix(scheme.as_str())?;
Some((scheme, stripped))
})
}
}
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)?,
}
#[elem(Construct)]
pub struct DirectLinkElem {
#[required]
#[internal]
pub loc: Location,
#[required]
#[internal]
pub body: Content,
#[required]
#[internal]
pub alt: Option<EcoString>,
}
impl Construct for DirectLinkElem {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}
#[elem(Tagged, Construct)]
pub struct LinkMarker {
#[internal]
#[required]
pub body: Content,
#[internal]
#[required]
pub alt: Option<EcoString>,
}
impl Construct for LinkMarker {
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
bail!(args.span, "cannot be constructed manually");
}
}
#[derive(Copy, Clone)]
pub enum UrlContactScheme {
Mailto,
Tel,
}
impl UrlContactScheme {
pub fn as_str(self) -> &'static str {
match self {
Self::Mailto => "mailto:",
Self::Tel => "tel:",
}
}
pub fn local_name_in(self, styles: StyleChain) -> &'static str {
match self {
UrlContactScheme::Mailto => Email::local_name_in(styles),
UrlContactScheme::Tel => Telephone::local_name_in(styles),
}
}
}
#[derive(Copy, Clone)]
pub struct Email;
impl LocalName for Email {
const KEY: &'static str = "email";
}
#[derive(Copy, Clone)]
pub struct Telephone;
impl LocalName for Telephone {
const KEY: &'static str = "telephone";
}