use ecow::EcoString;
use typst_syntax::VirtualPath;
use crate::diag::{HintedStrResult, bail, error};
use crate::foundations::{
Array, BundlePath, Cast, Content, Datetime, OneOrMultiple, Packed, ShowFn, ShowSet,
Smart, StyleChain, Styles, Target, Value, cast, elem,
};
use crate::introspection::Locatable;
use crate::text::{Locale, TextElem};
#[elem(Locatable, ShowSet)]
pub struct DocumentElem {
#[required]
pub path: BundlePath,
pub format: Smart<DocumentFormat>,
pub title: Option<Content>,
pub author: OneOrMultiple<EcoString>,
pub description: Option<Content>,
pub keywords: OneOrMultiple<EcoString>,
pub date: Smart<Option<Datetime>>,
#[required]
pub body: Content,
}
impl Packed<DocumentElem> {
pub fn determine_format(
&self,
styles: StyleChain,
) -> HintedStrResult<DocumentFormat> {
self.format
.get(styles)
.custom()
.or_else(|| determine_format_from_path(self.path.as_ref()))
.ok_or_else(|| {
error!(
"unknown document format";
hint: "try specifying the `format` explicitly";
)
})
}
}
fn determine_format_from_path(path: &VirtualPath) -> Option<DocumentFormat> {
match path.extension()? {
"pdf" => Some(PagedFormat::Pdf.into()),
"svg" => Some(PagedFormat::Svg.into()),
"png" => Some(PagedFormat::Png.into()),
"html" => Some(DocumentFormat::Html),
_ => None,
}
}
pub const DOCUMENT_UNSUPPORTED_RULE: ShowFn<DocumentElem> = |elem, _, _| {
bail!(
elem.span(),
"constructing a document is only supported in the bundle target";
hint: "try enabling the bundle target";
hint: "or use a `set document(..)` rule to configure metadata";
)
};
impl ShowSet for Packed<DocumentElem> {
fn show_set(&self, _: StyleChain) -> Styles {
let mut styles = Styles::new();
self.format.copy_into(&mut styles);
self.title.copy_into(&mut styles);
self.author.copy_into(&mut styles);
self.description.copy_into(&mut styles);
self.keywords.copy_into(&mut styles);
self.date.copy_into(&mut styles);
styles
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum DocumentFormat {
Paged(PagedFormat),
Html,
}
impl DocumentFormat {
pub fn target(self) -> Target {
match self {
Self::Paged(_) => Target::Paged,
Self::Html => Target::Html,
}
}
}
impl From<PagedFormat> for DocumentFormat {
fn from(format: PagedFormat) -> Self {
Self::Paged(format)
}
}
cast! {
DocumentFormat,
self => match self {
Self::Paged(v) => v.into_value(),
Self::Html => "html".into_value(),
},
v: PagedFormat => Self::Paged(v),
"html" => Self::Html,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum PagedFormat {
Pdf,
Png,
Svg,
}
#[derive(Debug, Default, Clone, PartialEq, Hash)]
pub struct Author(Vec<EcoString>);
cast! {
Author,
self => self.0.into_value(),
v: EcoString => Self(vec![v]),
v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?),
}
#[derive(Debug, Default, Clone, PartialEq, Hash)]
pub struct Keywords(Vec<EcoString>);
cast! {
Keywords,
self => self.0.into_value(),
v: EcoString => Self(vec![v]),
v: Array => Self(v.into_iter().map(Value::cast).collect::<HintedStrResult<_>>()?),
}
pub trait Document {
fn info(&self) -> &DocumentInfo;
}
#[derive(Debug, Default, Clone, PartialEq, Hash)]
pub struct DocumentInfo {
pub title: Option<EcoString>,
pub author: Vec<EcoString>,
pub description: Option<EcoString>,
pub keywords: Vec<EcoString>,
pub date: Smart<Option<Datetime>>,
pub locale: Smart<Locale>,
}
impl DocumentInfo {
pub fn populate(&mut self, styles: StyleChain) {
if styles.has(DocumentElem::title) {
self.title = styles
.get_ref(DocumentElem::title)
.as_ref()
.map(|content| content.plain_text());
}
if styles.has(DocumentElem::author) {
self.author = styles.get_cloned(DocumentElem::author).0;
}
if styles.has(DocumentElem::description) {
self.description = styles
.get_ref(DocumentElem::description)
.as_ref()
.map(|content| content.plain_text());
}
if styles.has(DocumentElem::keywords) {
self.keywords = styles.get_cloned(DocumentElem::keywords).0;
}
if styles.has(DocumentElem::date) {
self.date = styles.get(DocumentElem::date);
}
}
pub fn populate_locale(&mut self, styles: StyleChain) {
if self.locale.is_custom() {
return;
}
let mut locale: Option<Locale> = None;
if styles.has(TextElem::lang) {
locale.get_or_insert_default().lang = styles.get(TextElem::lang);
}
if styles.has(TextElem::region) {
locale.get_or_insert_default().region = styles.get(TextElem::region);
}
self.locale = Smart::from(locale);
}
}