ftml/tree/element/
object.rs

1/*
2 * tree/element/object.rs
3 *
4 * ftml - Library to parse Wikidot text
5 * Copyright (C) 2019-2025 Wikijump Team
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21use crate::data::PageRef;
22use crate::tree::clone::*;
23use crate::tree::{
24    Alignment, AnchorTarget, AttributeMap, ClearFloat, CodeBlock, Container, DateItem,
25    DefinitionListItem, Embed, FloatAlignment, ImageSource, LinkLabel, LinkLocation,
26    LinkType, ListItem, ListType, Module, PartialElement, Tab, Table, VariableMap,
27};
28use ref_map::*;
29use std::borrow::Cow;
30use std::num::NonZeroU32;
31
32/// Represents an element to be rendered.
33#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
34#[serde(rename_all = "kebab-case", tag = "element", content = "data")]
35pub enum Element<'t> {
36    /// Generic element that contains other elements within it.
37    ///
38    /// Examples would include divs, italics, paragraphs, etc.
39    Container(Container<'t>),
40
41    /// A Wikidot module being invoked, along with its arguments.
42    ///
43    /// These modules require some kind of processing by backend software,
44    /// so are represented in module forum rather than as elements to be
45    /// directly rendered.
46    Module(Module<'t>),
47
48    /// An element only containing text.
49    ///
50    /// Should be formatted like typical body text.
51    Text(Cow<'t, str>),
52
53    /// Raw text.
54    ///
55    /// This should be formatted exactly as listed.
56    /// For instance, spaces being rendered to HTML should
57    /// produce a `&nbsp;`.
58    Raw(Cow<'t, str>),
59
60    /// A wikitext variable.
61    ///
62    /// During rendering, this will be replaced with its actual value,
63    /// as appropriate to the context.
64    Variable(Cow<'t, str>),
65
66    /// An element indicating an email.
67    ///
68    /// Whether this should become a clickable href link or just text
69    /// is up to the render implementation.
70    Email(Cow<'t, str>),
71
72    /// An element representing an HTML table.
73    Table(Table<'t>),
74
75    /// An element representing a tabview.
76    TabView(Vec<Tab<'t>>),
77
78    /// An element representing an arbitrary anchor.
79    ///
80    /// This is distinct from link in that it maps to HTML `<a>`,
81    /// and does not necessarily mean a link to some other URL.
82    Anchor {
83        target: Option<AnchorTarget>,
84        attributes: AttributeMap<'t>,
85        elements: Vec<Element<'t>>,
86    },
87
88    /// An element representing a named anchor.
89    ///
90    /// This is an area of the page that can be jumped to by name.
91    /// Associated syntax is `[[# name-of-anchor]]`.
92    AnchorName(Cow<'t, str>),
93
94    /// An element linking to a different page.
95    ///
96    /// The "label" field is an optional field denoting what the link should
97    /// display.
98    ///
99    /// The "link" field is either a page reference (relative URL) or full URL.
100    ///
101    /// The "ltype" field tells what kind of link produced this element.
102    Link {
103        #[serde(rename = "type")]
104        ltype: LinkType,
105        link: LinkLocation<'t>,
106        extra: Option<Cow<'t, str>>,
107        label: LinkLabel<'t>,
108        target: Option<AnchorTarget>,
109    },
110
111    /// An element representing an image and its associated metadata.
112    ///
113    /// The "source" field is the link to the image itself.
114    ///
115    /// The "link" field is what the `<a>` points to, when the user clicks on the image.
116    Image {
117        source: ImageSource<'t>,
118        link: Option<LinkLocation<'t>>,
119        alignment: Option<FloatAlignment>,
120        attributes: AttributeMap<'t>,
121    },
122
123    /// An ordered or unordered list.
124    List {
125        #[serde(rename = "type")]
126        ltype: ListType,
127        attributes: AttributeMap<'t>,
128        items: Vec<ListItem<'t>>,
129    },
130
131    /// A definition list.
132    DefinitionList(Vec<DefinitionListItem<'t>>),
133
134    /// A radio button.
135    ///
136    /// The "name" field translates to HTML, but is standard for grouping them.
137    /// The "checked" field determines if the radio button starts checked or not.
138    RadioButton {
139        name: Cow<'t, str>,
140        checked: bool,
141        attributes: AttributeMap<'t>,
142    },
143
144    /// A checkbox.
145    ///
146    /// The "checked" field determines if the radio button starts checked or not.
147    CheckBox {
148        checked: bool,
149        attributes: AttributeMap<'t>,
150    },
151
152    /// A collapsible, containing content hidden to be opened on click.
153    ///
154    /// This is an interactable element provided by Wikidot which allows hiding
155    /// all of the internal elements until it is opened by clicking, which can
156    /// then be re-hidden by clicking again.
157    #[serde(rename_all = "kebab-case")]
158    Collapsible {
159        elements: Vec<Element<'t>>,
160        attributes: AttributeMap<'t>,
161        start_open: bool,
162        show_text: Option<Cow<'t, str>>,
163        hide_text: Option<Cow<'t, str>>,
164        show_top: bool,
165        show_bottom: bool,
166    },
167
168    /// A table of contents block.
169    ///
170    /// This contains links to sub-headings on the page.
171    TableOfContents {
172        attributes: AttributeMap<'t>,
173        align: Option<Alignment>,
174    },
175
176    /// A footnote reference.
177    ///
178    /// This specifies that a `[[footnote]]` was here, and that a clickable
179    /// link to the footnote block should be added.
180    ///
181    /// The index is not saved because it is part of the rendering context.
182    /// It is indirectly preserved as the index of the `footnotes` list in the syntax tree.
183    Footnote,
184
185    /// A footnote block, containing all the footnotes from throughout the page.
186    ///
187    /// If a `[[footnoteblock]]` is not added somewhere in the content of the page,
188    /// then it is automatically appended to the end of the syntax tree.
189    FootnoteBlock {
190        title: Option<Cow<'t, str>>,
191        hide: bool,
192    },
193
194    /// A citation of a bibliography element, invoked via `((bibcite ...))`.
195    ///
196    /// The `brackets` field tells whether the resultant HTML should be surrounded
197    /// in `[..]`, which is not very easily possible when using `[[bibcite ...]]`.
198    BibliographyCite { label: Cow<'t, str>, brackets: bool },
199
200    /// A bibliography block, containing all the cited items from throughout the page.
201    ///
202    /// The `index` field is the zero-indexed value of which bibliography block this is.
203    BibliographyBlock {
204        index: usize,
205        title: Option<Cow<'t, str>>,
206        hide: bool,
207    },
208
209    /// A user block, linking to their information and possibly showing their avatar.
210    #[serde(rename_all = "kebab-case")]
211    User {
212        name: Cow<'t, str>,
213        show_avatar: bool,
214    },
215
216    /// A date display, showcasing a particular moment in time.
217    Date {
218        value: DateItem,
219        format: Option<Cow<'t, str>>,
220        hover: bool,
221    },
222
223    /// Element containing colored text.
224    ///
225    /// The CSS designation of the color is specified, followed by the elements contained within.
226    Color {
227        color: Cow<'t, str>,
228        elements: Vec<Element<'t>>,
229    },
230
231    /// Element containing a code block.
232    Code(CodeBlock<'t>),
233
234    /// Element containing a named math equation.
235    #[serde(rename_all = "kebab-case")]
236    Math {
237        name: Option<Cow<'t, str>>,
238        latex_source: Cow<'t, str>,
239    },
240
241    /// Element containing inline math.
242    #[serde(rename_all = "kebab-case")]
243    MathInline { latex_source: Cow<'t, str> },
244
245    /// Element referring to an equation elsewhere in the page.
246    EquationReference(Cow<'t, str>),
247
248    /// An embedded piece of media or content from elsewhere.
249    Embed(Embed<'t>),
250
251    /// Element containing a sandboxed HTML block.
252    Html {
253        contents: Cow<'t, str>,
254        attributes: AttributeMap<'t>,
255    },
256
257    /// Element containing an iframe component.
258    Iframe {
259        url: Cow<'t, str>,
260        attributes: AttributeMap<'t>,
261    },
262
263    /// Element containing the contents of a page included elsewhere.
264    ///
265    /// From `[[include-elements]]`.
266    #[serde(rename_all = "kebab-case")]
267    Include {
268        paragraph_safe: bool,
269        variables: VariableMap<'t>,
270        location: PageRef,
271        elements: Vec<Element<'t>>,
272    },
273
274    /// A CSS stylesheet.
275    ///
276    /// Corresponds with a `<style>` entity in the body of the HTML.
277    Style(Cow<'t, str>),
278
279    /// A newline or line break.
280    ///
281    /// This calls for a newline in the final output, such as `<br>` in HTML.
282    LineBreak,
283
284    /// A collection of line breaks adjacent to each other.
285    LineBreaks(NonZeroU32),
286
287    /// A "clear float" div.
288    ClearFloat(ClearFloat),
289
290    /// A horizontal rule.
291    HorizontalRule,
292
293    /// A partial element.
294    ///
295    /// This will not appear in final syntax trees, but exists to
296    /// facilitate parsing of complicated structures.
297    ///
298    /// See [`WJ-816`](https://scuttle.atlassian.net/browse/WJ-816).
299    Partial(PartialElement<'t>),
300}
301
302impl Element<'_> {
303    /// Determines if the element is "unintentional whitespace".
304    ///
305    /// Specifically, it returns true if the element is:
306    /// * `Element::LineBreak`
307    /// * `Element::Text` where the contents all have the Unicode property `White_Space`.
308    ///
309    /// This does not count `Element::LineBreaks` because it is produced intentionally
310    /// via `[[lines]]` rather than extra whitespace in between syntactical elements.
311    pub fn is_whitespace(&self) -> bool {
312        match self {
313            Element::LineBreak => true,
314            Element::Text(string) if string.chars().all(|c| c.is_whitespace()) => true,
315            _ => false,
316        }
317    }
318
319    /// Returns the Rust name of this `Element` variant.
320    pub fn name(&self) -> &'static str {
321        match self {
322            Element::Container(container) => container.ctype().name(),
323            Element::Module(module) => module.name(),
324            Element::Text(_) => "Text",
325            Element::Raw(_) => "Raw",
326            Element::Variable(_) => "Variable",
327            Element::Email(_) => "Email",
328            Element::Table(_) => "Table",
329            Element::TabView(_) => "TabView",
330            Element::Anchor { .. } => "Anchor",
331            Element::AnchorName(_) => "AnchorName",
332            Element::Link { .. } => "Link",
333            Element::Image { .. } => "Image",
334            Element::List { .. } => "List",
335            Element::DefinitionList(_) => "DefinitionList",
336            Element::RadioButton { .. } => "RadioButton",
337            Element::CheckBox { .. } => "CheckBox",
338            Element::Collapsible { .. } => "Collapsible",
339            Element::TableOfContents { .. } => "TableOfContents",
340            Element::Footnote => "Footnote",
341            Element::FootnoteBlock { .. } => "FootnoteBlock",
342            Element::BibliographyCite { .. } => "BibliographyCite",
343            Element::BibliographyBlock { .. } => "BibliographyBlock",
344            Element::User { .. } => "User",
345            Element::Date { .. } => "Date",
346            Element::Color { .. } => "Color",
347            Element::Code { .. } => "Code",
348            Element::Math { .. } => "Math",
349            Element::MathInline { .. } => "MathInline",
350            Element::EquationReference(_) => "EquationReference",
351            Element::Embed(_) => "Embed",
352            Element::Html { .. } => "HTML",
353            Element::Iframe { .. } => "Iframe",
354            Element::Include { .. } => "Include",
355            Element::Style(_) => "Style",
356            Element::LineBreak => "LineBreak",
357            Element::LineBreaks { .. } => "LineBreaks",
358            Element::ClearFloat(_) => "ClearFloat",
359            Element::HorizontalRule => "HorizontalRule",
360            Element::Partial(partial) => partial.name(),
361        }
362    }
363
364    /// Determines if this element type is able to be embedded in a paragraph.
365    ///
366    /// It does *not* look into the interiors of the element, it only does a
367    /// surface-level check.
368    ///
369    /// This is to avoid making the call very expensive, but for a complete
370    /// understanding of the paragraph requirements, see the `Elements` return.
371    ///
372    /// See <https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#phrasing_content>
373    pub fn paragraph_safe(&self) -> bool {
374        match self {
375            Element::Container(container) => container.ctype().paragraph_safe(),
376            Element::Module(_) => false,
377            Element::Text(_)
378            | Element::Raw(_)
379            | Element::Variable(_)
380            | Element::Email(_) => true,
381            Element::Table(_) => false,
382            Element::TabView(_) => false,
383            Element::Anchor { .. } | Element::AnchorName(_) | Element::Link { .. } => {
384                true
385            }
386            Element::Image { .. } => true,
387            Element::List { .. } => false,
388            Element::DefinitionList(_) => false,
389            Element::RadioButton { .. } | Element::CheckBox { .. } => true,
390            Element::Collapsible { .. } => false,
391            Element::TableOfContents { .. } => false,
392            Element::Footnote => true,
393            Element::FootnoteBlock { .. } => false,
394            Element::BibliographyCite { .. } => true,
395            Element::BibliographyBlock { .. } => false,
396            Element::User { .. } => true,
397            Element::Date { .. } => true,
398            Element::Color { .. } => true,
399            Element::Code { .. } => false,
400            Element::Math { .. } => false,
401            Element::MathInline { .. } => true,
402            Element::EquationReference(_) => true,
403            Element::Embed(_) => false,
404            Element::Html { .. } | Element::Iframe { .. } => false,
405            Element::Include { paragraph_safe, .. } => *paragraph_safe,
406            Element::Style(_) => false,
407            Element::LineBreak | Element::LineBreaks { .. } => true,
408            Element::ClearFloat(_) => false,
409            Element::HorizontalRule => false,
410            Element::Partial(_) => {
411                panic!("Should not check for paragraph safety of partials")
412            }
413        }
414    }
415
416    /// Deep-clones the object, making it an owned version.
417    ///
418    /// Note that `.to_owned()` on `Cow` just copies the pointer,
419    /// it doesn't make an `Cow::Owned(_)` version like its name
420    /// suggests.
421    pub fn to_owned(&self) -> Element<'static> {
422        match self {
423            Element::Container(container) => Element::Container(container.to_owned()),
424            Element::Module(module) => Element::Module(module.to_owned()),
425            Element::Text(text) => Element::Text(string_to_owned(text)),
426            Element::Raw(text) => Element::Raw(string_to_owned(text)),
427            Element::Variable(name) => Element::Variable(string_to_owned(name)),
428            Element::Email(email) => Element::Email(string_to_owned(email)),
429            Element::Table(table) => Element::Table(table.to_owned()),
430            Element::TabView(tabs) => {
431                Element::TabView(tabs.iter().map(|tab| tab.to_owned()).collect())
432            }
433            Element::Anchor {
434                target,
435                attributes,
436                elements,
437            } => Element::Anchor {
438                target: *target,
439                attributes: attributes.to_owned(),
440                elements: elements_to_owned(elements),
441            },
442            Element::AnchorName(name) => Element::AnchorName(string_to_owned(name)),
443            Element::Link {
444                ltype,
445                link,
446                extra,
447                label,
448                target,
449            } => Element::Link {
450                ltype: *ltype,
451                link: link.to_owned(),
452                extra: option_string_to_owned(extra),
453                label: label.to_owned(),
454                target: *target,
455            },
456            Element::List {
457                ltype,
458                attributes,
459                items,
460            } => Element::List {
461                ltype: *ltype,
462                attributes: attributes.to_owned(),
463                items: list_items_to_owned(items),
464            },
465            Element::Image {
466                source,
467                link,
468                alignment,
469                attributes,
470            } => Element::Image {
471                source: source.to_owned(),
472                link: link.ref_map(|link| link.to_owned()),
473                alignment: *alignment,
474                attributes: attributes.to_owned(),
475            },
476            Element::DefinitionList(items) => Element::DefinitionList(
477                items.iter().map(|item| item.to_owned()).collect(),
478            ),
479            Element::RadioButton {
480                name,
481                checked,
482                attributes,
483            } => Element::RadioButton {
484                name: string_to_owned(name),
485                checked: *checked,
486                attributes: attributes.to_owned(),
487            },
488            Element::CheckBox {
489                checked,
490                attributes,
491            } => Element::CheckBox {
492                checked: *checked,
493                attributes: attributes.to_owned(),
494            },
495            Element::Collapsible {
496                elements,
497                attributes,
498                start_open,
499                show_text,
500                hide_text,
501                show_top,
502                show_bottom,
503            } => Element::Collapsible {
504                elements: elements_to_owned(elements),
505                attributes: attributes.to_owned(),
506                start_open: *start_open,
507                show_text: option_string_to_owned(show_text),
508                hide_text: option_string_to_owned(hide_text),
509                show_top: *show_top,
510                show_bottom: *show_bottom,
511            },
512            Element::TableOfContents { align, attributes } => Element::TableOfContents {
513                align: *align,
514                attributes: attributes.to_owned(),
515            },
516            Element::Footnote => Element::Footnote,
517            Element::FootnoteBlock { title, hide } => Element::FootnoteBlock {
518                title: option_string_to_owned(title),
519                hide: *hide,
520            },
521            Element::BibliographyCite { label, brackets } => Element::BibliographyCite {
522                label: string_to_owned(label),
523                brackets: *brackets,
524            },
525            Element::BibliographyBlock { index, title, hide } => {
526                Element::BibliographyBlock {
527                    index: *index,
528                    title: option_string_to_owned(title),
529                    hide: *hide,
530                }
531            }
532            Element::User { name, show_avatar } => Element::User {
533                name: string_to_owned(name),
534                show_avatar: *show_avatar,
535            },
536            Element::Date {
537                value,
538                format,
539                hover,
540            } => Element::Date {
541                value: *value,
542                format: option_string_to_owned(format),
543                hover: *hover,
544            },
545            Element::Color { color, elements } => Element::Color {
546                color: string_to_owned(color),
547                elements: elements_to_owned(elements),
548            },
549            Element::Code(code_block) => Element::Code(code_block.to_owned()),
550            Element::Math { name, latex_source } => Element::Math {
551                name: option_string_to_owned(name),
552                latex_source: string_to_owned(latex_source),
553            },
554            Element::MathInline { latex_source } => Element::MathInline {
555                latex_source: string_to_owned(latex_source),
556            },
557            Element::EquationReference(name) => {
558                Element::EquationReference(string_to_owned(name))
559            }
560            Element::Embed(embed) => Element::Embed(embed.to_owned()),
561            Element::Html {
562                contents,
563                attributes,
564            } => Element::Html {
565                contents: string_to_owned(contents),
566                attributes: attributes.to_owned(),
567            },
568            Element::Iframe { url, attributes } => Element::Iframe {
569                url: string_to_owned(url),
570                attributes: attributes.to_owned(),
571            },
572            Element::Include {
573                paragraph_safe,
574                variables,
575                location,
576                elements,
577            } => Element::Include {
578                paragraph_safe: *paragraph_safe,
579                variables: string_map_to_owned(variables),
580                location: location.to_owned(),
581                elements: elements_to_owned(elements),
582            },
583            Element::Style(css) => Element::Style(string_to_owned(css)),
584            Element::LineBreak => Element::LineBreak,
585            Element::LineBreaks(amount) => Element::LineBreaks(*amount),
586            Element::ClearFloat(clear_float) => Element::ClearFloat(*clear_float),
587            Element::HorizontalRule => Element::HorizontalRule,
588            Element::Partial(partial) => Element::Partial(partial.to_owned()),
589        }
590    }
591}