1use std::fmt::{self, Debug, Display, Formatter};
2
3use ecow::{EcoString, EcoVec};
4use typst_library::diag::{HintedStrResult, StrResult, bail};
5use typst_library::foundations::{Dict, Repr, Str, StyleChain, cast};
6use typst_library::introspection::{Introspector, Location, Tag};
7use typst_library::layout::{Abs, Frame, Point};
8use typst_library::model::DocumentInfo;
9use typst_library::text::TextElem;
10use typst_syntax::Span;
11use typst_utils::{PicoStr, ResolvedPicoStr};
12
13use crate::{attr, charsets, css};
14
15#[derive(Debug, Clone)]
17pub struct HtmlDocument {
18    pub root: HtmlElement,
20    pub info: DocumentInfo,
22    pub introspector: Introspector,
24}
25
26#[derive(Debug, Clone, Hash)]
28pub enum HtmlNode {
29    Tag(Tag),
31    Text(EcoString, Span),
33    Element(HtmlElement),
35    Frame(HtmlFrame),
37}
38
39impl HtmlNode {
40    pub fn text(text: impl Into<EcoString>, span: Span) -> Self {
42        Self::Text(text.into(), span)
43    }
44
45    pub fn span(&self) -> Span {
47        match self {
48            Self::Tag(_) => Span::detached(),
49            Self::Text(_, span) => *span,
50            Self::Element(element) => element.span,
51            Self::Frame(frame) => frame.span,
52        }
53    }
54}
55
56impl From<Tag> for HtmlNode {
57    fn from(tag: Tag) -> Self {
58        Self::Tag(tag)
59    }
60}
61
62impl From<HtmlElement> for HtmlNode {
63    fn from(element: HtmlElement) -> Self {
64        Self::Element(element)
65    }
66}
67
68impl From<HtmlFrame> for HtmlNode {
69    fn from(frame: HtmlFrame) -> Self {
70        Self::Frame(frame)
71    }
72}
73
74#[derive(Debug, Clone, Hash)]
76pub struct HtmlElement {
77    pub tag: HtmlTag,
79    pub attrs: HtmlAttrs,
81    pub children: EcoVec<HtmlNode>,
83    pub parent: Option<Location>,
86    pub span: Span,
88    pub pre_span: bool,
96}
97
98impl HtmlElement {
99    pub fn new(tag: HtmlTag) -> Self {
101        Self {
102            tag,
103            attrs: HtmlAttrs::default(),
104            children: EcoVec::new(),
105            parent: None,
106            span: Span::detached(),
107            pre_span: false,
108        }
109    }
110
111    pub fn with_children(mut self, children: EcoVec<HtmlNode>) -> Self {
115        self.children = children;
116        self
117    }
118
119    pub fn with_attr(mut self, key: HtmlAttr, value: impl Into<EcoString>) -> Self {
121        self.attrs.push(key, value);
122        self
123    }
124
125    pub(crate) fn with_styles(self, properties: css::Properties) -> Self {
127        if let Some(value) = properties.into_inline_styles() {
128            self.with_attr(attr::style, value)
129        } else {
130            self
131        }
132    }
133
134    pub fn spanned(mut self, span: Span) -> Self {
136        self.span = span;
137        self
138    }
139}
140
141#[derive(Copy, Clone, Eq, PartialEq, Hash)]
143pub struct HtmlTag(PicoStr);
144
145impl HtmlTag {
146    pub fn intern(string: &str) -> StrResult<Self> {
148        if string.is_empty() {
149            bail!("tag name must not be empty");
150        }
151
152        let mut has_hyphen = false;
153        let mut has_uppercase = false;
154
155        for c in string.chars() {
156            if c == '-' {
157                has_hyphen = true;
158            } else if !charsets::is_valid_in_tag_name(c) {
159                bail!("the character {} is not valid in a tag name", c.repr());
160            } else {
161                has_uppercase |= c.is_ascii_uppercase();
162            }
163        }
164
165        if has_hyphen {
177            if !string.starts_with(|c: char| c.is_ascii_lowercase()) {
178                bail!("custom element name must start with a lowercase letter");
179            }
180            if has_uppercase {
181                bail!("custom element name must not contain uppercase letters");
182            }
183
184            if matches!(
187                string,
188                "annotation-xml"
189                    | "color-profile"
190                    | "font-face"
191                    | "font-face-src"
192                    | "font-face-uri"
193                    | "font-face-format"
194                    | "font-face-name"
195                    | "missing-glyph"
196            ) {
197                bail!("name is reserved and not valid for a custom element");
198            }
199        }
200
201        Ok(Self(PicoStr::intern(string)))
202    }
203
204    #[track_caller]
208    pub const fn constant(string: &'static str) -> Self {
209        if string.is_empty() {
210            panic!("tag name must not be empty");
211        }
212
213        let bytes = string.as_bytes();
214        let mut i = 0;
215        while i < bytes.len() {
216            if !bytes[i].is_ascii() || !charsets::is_valid_in_tag_name(bytes[i] as char) {
217                panic!("not all characters are valid in a tag name");
218            }
219            i += 1;
220        }
221
222        Self(PicoStr::constant(string))
223    }
224
225    pub fn resolve(self) -> ResolvedPicoStr {
227        self.0.resolve()
228    }
229
230    pub const fn into_inner(self) -> PicoStr {
232        self.0
233    }
234}
235
236impl Debug for HtmlTag {
237    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
238        Display::fmt(self, f)
239    }
240}
241
242impl Display for HtmlTag {
243    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
244        write!(f, "<{}>", self.resolve())
245    }
246}
247
248cast! {
249    HtmlTag,
250    self => self.0.resolve().as_str().into_value(),
251    v: Str => Self::intern(&v)?,
252}
253
254#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
256pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>);
257
258impl HtmlAttrs {
259    pub fn new() -> Self {
261        Self::default()
262    }
263
264    pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
266        self.0.push((attr, value.into()));
267    }
268
269    pub fn push_front(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
271        self.0.insert(0, (attr, value.into()));
272    }
273
274    pub fn get(&self, attr: HtmlAttr) -> Option<&EcoString> {
276        self.0.iter().find(|&&(k, _)| k == attr).map(|(_, v)| v)
277    }
278}
279
280cast! {
281    HtmlAttrs,
282    self => self.0
283        .into_iter()
284        .map(|(key, value)| (key.resolve().as_str().into(), value.into_value()))
285        .collect::<Dict>()
286        .into_value(),
287    values: Dict => Self(values
288        .into_iter()
289        .map(|(k, v)| {
290            let attr = HtmlAttr::intern(&k)?;
291            let value = v.cast::<EcoString>()?;
292            Ok((attr, value))
293        })
294        .collect::<HintedStrResult<_>>()?),
295}
296
297#[derive(Copy, Clone, Eq, PartialEq, Hash)]
299pub struct HtmlAttr(PicoStr);
300
301impl HtmlAttr {
302    pub fn intern(string: &str) -> StrResult<Self> {
304        if string.is_empty() {
305            bail!("attribute name must not be empty");
306        }
307
308        if let Some(c) =
309            string.chars().find(|&c| !charsets::is_valid_in_attribute_name(c))
310        {
311            bail!("the character {} is not valid in an attribute name", c.repr());
312        }
313
314        Ok(Self(PicoStr::intern(string)))
315    }
316
317    #[track_caller]
324    pub const fn constant(string: &'static str) -> Self {
325        if string.is_empty() {
326            panic!("attribute name must not be empty");
327        }
328
329        let bytes = string.as_bytes();
330        let mut i = 0;
331        while i < bytes.len() {
332            if !bytes[i].is_ascii()
333                || !charsets::is_valid_in_attribute_name(bytes[i] as char)
334            {
335                panic!("not all characters are valid in an attribute name");
336            }
337            i += 1;
338        }
339
340        Self(PicoStr::constant(string))
341    }
342
343    pub fn resolve(self) -> ResolvedPicoStr {
345        self.0.resolve()
346    }
347
348    pub const fn into_inner(self) -> PicoStr {
350        self.0
351    }
352}
353
354impl Debug for HtmlAttr {
355    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
356        Display::fmt(self, f)
357    }
358}
359
360impl Display for HtmlAttr {
361    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
362        write!(f, "{}", self.resolve())
363    }
364}
365
366cast! {
367    HtmlAttr,
368    self => self.0.resolve().as_str().into_value(),
369    v: Str => Self::intern(&v)?,
370}
371
372#[derive(Debug, Clone, Hash)]
374pub struct HtmlFrame {
375    pub inner: Frame,
377    pub text_size: Abs,
381    pub id: Option<EcoString>,
383    pub link_points: EcoVec<(Point, EcoString)>,
385    pub span: Span,
387}
388
389impl HtmlFrame {
390    pub fn new(inner: Frame, styles: StyleChain, span: Span) -> Self {
392        Self {
393            inner,
394            text_size: styles.resolve(TextElem::size),
395            id: None,
396            link_points: EcoVec::new(),
397            span,
398        }
399    }
400}