use std::fmt::{self, Debug, Formatter};
use std::num::NonZeroUsize;
use comemo::Tracked;
use ecow::{EcoString, eco_format};
use typst_syntax::{Span, VirtualPath};
use typst_utils::NonZeroExt;
use crate::diag::{SourceDiagnostic, warning};
use crate::engine::Engine;
use crate::foundations::{Content, IntoValue, Repr, Selector, func, repr, scope, ty};
use crate::introspection::{
DocumentPosition, History, Introspect, Introspector, PagedPosition,
};
use crate::layout::Abs;
use crate::model::Numbering;
pub trait Locatable {}
pub trait Unqueriable: Locatable {}
pub trait Tagged {}
#[ty(scope)]
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct Location(u128);
impl Location {
pub fn new(hash: u128) -> Self {
Self(hash)
}
pub fn hash(self) -> u128 {
self.0
}
pub fn variant(self, n: usize) -> Self {
Self(typst_utils::hash128(&(self.0, n)))
}
}
#[scope]
impl Location {
#[func]
pub fn page(self, engine: &mut Engine, span: Span) -> NonZeroUsize {
engine.introspect(PageIntrospection(self, span))
}
#[func]
pub fn position(self, engine: &mut Engine, span: Span) -> PagedPosition {
engine.introspect(PositionIntrospection(self, span))
}
#[func]
pub fn page_numbering(self, engine: &mut Engine, span: Span) -> Option<Numbering> {
engine.introspect(PageNumberingIntrospection(self, span))
}
}
impl Debug for Location {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if f.alternate() {
write!(f, "Location({})", self.0)
} else {
let truncated = self.0 as u16;
write!(f, "Location({truncated})")
}
}
}
impl Repr for Location {
fn repr(&self) -> EcoString {
"location(..)".into()
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct LocationKey(u128);
impl LocationKey {
pub fn new(location: Location) -> Self {
Self(location.0)
}
}
impl From<Location> for LocationKey {
fn from(location: Location) -> Self {
Self::new(location)
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct PositionIntrospection(pub Location, pub Span);
impl Introspect for PositionIntrospection {
type Output = PagedPosition;
fn introspect(
&self,
_: &mut Engine,
introspector: Tracked<dyn Introspector + '_>,
) -> Self::Output {
match introspector.position(self.0) {
Some(DocumentPosition::Paged(pos)) => pos,
Some(DocumentPosition::Html(_)) | None => PagedPosition::ORIGIN,
}
}
fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
format_convergence_warning(
self.0,
self.1,
history,
"positions",
|element| eco_format!("{element} position"),
|pos| {
let coord = |v: Abs| repr::format_float(v.to_pt(), Some(0), false, "pt");
eco_format!(
"page {} at ({}, {})",
pos.page,
coord(pos.point.x),
coord(pos.point.y)
)
},
)
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct PageIntrospection(pub Location, pub Span);
impl Introspect for PageIntrospection {
type Output = NonZeroUsize;
fn introspect(
&self,
_: &mut Engine,
introspector: Tracked<dyn Introspector + '_>,
) -> Self::Output {
introspector.page(self.0).unwrap_or(NonZeroUsize::ONE)
}
fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
format_convergence_warning(
self.0,
self.1,
history,
"page numbers",
|element| eco_format!("page number of the {element}"),
|n| eco_format!("page {n}"),
)
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct PageNumberingIntrospection(pub Location, pub Span);
impl Introspect for PageNumberingIntrospection {
type Output = Option<Numbering>;
fn introspect(
&self,
_: &mut Engine,
introspector: Tracked<dyn Introspector + '_>,
) -> Self::Output {
introspector.page_numbering(self.0).cloned()
}
fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
format_convergence_warning(
self.0,
self.1,
history,
"numberings",
|element| {
eco_format!("numbering of the page on which the {element} is located")
},
|numbering| eco_format!("`{}`", numbering.clone().into_value().repr()),
)
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct PageSupplementIntrospection(pub Location, pub Span);
impl Introspect for PageSupplementIntrospection {
type Output = Content;
fn introspect(
&self,
_: &mut Engine,
introspector: Tracked<dyn Introspector + '_>,
) -> Self::Output {
introspector.page_supplement(self.0).cloned().unwrap_or_default()
}
fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
format_convergence_warning(
self.0,
self.1,
history,
"supplements",
|element| {
eco_format!("supplement of the page on which the {element} is located")
},
|supplement| eco_format!("`{}`", supplement.repr()),
)
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct PathIntrospection(pub Location, pub Span);
impl Introspect for PathIntrospection {
type Output = Option<VirtualPath>;
fn introspect(
&self,
_: &mut Engine,
introspector: Tracked<dyn Introspector + '_>,
) -> Self::Output {
introspector.path(self.0).cloned()
}
fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
format_convergence_warning(
self.0,
self.1,
history,
"path",
|element| {
eco_format!("path of the document in which the {element} is located")
},
|path| {
eco_format!(
"`{}`",
path.as_ref().map(|p| p.get_with_slash()).into_value().repr()
)
},
)
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct DocumentIntrospection(pub Location, pub Span);
impl Introspect for DocumentIntrospection {
type Output = Option<Location>;
fn introspect(
&self,
_: &mut Engine,
introspector: Tracked<dyn Introspector + '_>,
) -> Self::Output {
introspector.document(self.0)
}
fn diagnose(&self, history: &History<Self::Output>) -> SourceDiagnostic {
format_convergence_warning(
self.0,
self.1,
history,
"path",
|element| eco_format!("document in which the {element} is located"),
|_loc| eco_format!("TODO"),
)
}
}
fn format_convergence_warning<T>(
loc: Location,
span: Span,
history: &History<T>,
output_kind_plural: &str,
format_output_kind: impl FnOnce(&str) -> EcoString,
format_output: impl FnMut(&T) -> EcoString,
) -> SourceDiagnostic {
let elem = history.final_introspector().query_first(&Selector::Location(loc));
let kind = match &elem {
Some(content) => content.elem().name(),
None => "element",
};
let what = format_output_kind(kind);
let mut diag = warning!(span, "{what} did not stabilize");
if let Some(elem) = elem
&& !elem.span().is_detached()
{
diag.spanned_hint(eco_format!("{kind} was created here"), elem.span());
}
diag.with_hint(history.hint(output_kind_plural, format_output))
}