wikioxide 0.1.0

MediaWiki wikitext parser producing typed domain models (templates, links, categories, warnings) with template expansion — no HTML, no network
Documentation
//! Preprocessor DOM data structures — the frozen contract between the scanner/builder,
//! the flatten/expansion stages and the as-written extractor.

use crate::engine::source::Span;

/// Index of a node within a [`PpDom`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct PpNodeId(pub(crate) u32);

/// One preprocessor node.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PpNode {
    pub(crate) kind: PpKind,
    /// The node's full extent in the original source (`{{` through `}}` for templates).
    pub(crate) span: Span,
}

/// Node kinds the preprocessor produces.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PpKind {
    /// A run of plain text (may contain markup for later stages — links, quotes, tables).
    Text,
    /// An HTML comment `<!-- … -->`; vanishes from flattened/expanded output.
    Comment,
    /// Content dropped in the current inclusion mode (`<noinclude>` body when
    /// transcluding, `<includeonly>` body when viewing directly, etc.).
    Ignored,
    /// A template call `{{title|part|…}}`. Part 0 is the title.
    Template {
        /// Title part followed by the argument parts, in source order.
        parts: Vec<Part>,
    },
    /// A template argument `{{{name|default}}}`. Part 0 is the name; an optional
    /// second part is the default.
    TplArg {
        /// Name part, then at most one default part (further `|` go into the default).
        parts: Vec<Part>,
    },
    /// An extension tag capture (`<nowiki>…</nowiki>`, `<ref/>`, …). The body is raw —
    /// never scanned for wikitext at this stage.
    ExtTag {
        /// The tag name as written (slice it from the source; compare lower-cased).
        name: Span,
        /// Raw attribute text between the name and `>`, if non-empty.
        attributes: Option<Span>,
        /// Raw body between `>` and `</name>`; `None` for self-closing tags and for
        /// tags left unclosed at end of input (their body runs to EOF — see `closed`).
        body: Option<Span>,
        /// False when the closing tag was missing and the capture ran to EOF.
        closed: bool,
    },
}

/// One `|`-separated part of a template or template-argument node.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Part {
    /// The part's content, in order. Empty for an empty part (`{{t|}}`).
    pub(crate) children: Vec<PpNodeId>,
    /// Position within `children` of the split at the first top-level `=`:
    /// `children[..eq]` is the name, `children[eq]` is the `=` text node itself,
    /// `children[eq + 1..]` is the value. `None` for positional parts.
    pub(crate) eq: Option<usize>,
    /// The part's extent in the source (excluding the `|`).
    pub(crate) span: Span,
}

/// A parsed preprocessor document: a flat arena plus the root sequence.
#[derive(Debug, Default)]
pub(crate) struct PpDom {
    nodes: Vec<PpNode>,
    root: Vec<PpNodeId>,
}

impl PpDom {
    pub(crate) fn new() -> Self {
        Self::default()
    }

    /// Adds a node to the arena and returns its id.
    pub(crate) fn push(&mut self, node: PpNode) -> PpNodeId {
        let id = PpNodeId(u32::try_from(self.nodes.len()).unwrap_or(u32::MAX));
        self.nodes.push(node);
        id
    }

    /// Appends a node id to the document's top-level sequence.
    pub(crate) fn push_root(&mut self, id: PpNodeId) {
        self.root.push(id);
    }

    pub(crate) fn node(&self, id: PpNodeId) -> &PpNode {
        &self.nodes[id.0 as usize]
    }

    /// The document's top-level node sequence.
    pub(crate) fn root(&self) -> &[PpNodeId] {
        &self.root
    }

    /// Number of nodes in the arena.
    #[allow(
        dead_code,
        reason = "consumed by the expansion-stage limits accounting"
    )]
    pub(crate) fn len(&self) -> usize {
        self.nodes.len()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::engine::source::SourceId;

    fn span(start: u32, end: u32) -> Span {
        Span::new(SourceId(0), start, end)
    }

    #[test]
    fn arena_stores_and_retrieves_nodes() {
        let mut dom = PpDom::new();
        let text = dom.push(PpNode {
            kind: PpKind::Text,
            span: span(0, 5),
        });
        let comment = dom.push(PpNode {
            kind: PpKind::Comment,
            span: span(5, 14),
        });
        dom.push_root(text);
        dom.push_root(comment);

        assert_eq!(dom.len(), 2);
        assert_eq!(dom.root(), [text, comment]);
        assert_eq!(dom.node(text).kind, PpKind::Text);
        assert_eq!(dom.node(comment).span.len(), 9);
    }

    #[test]
    fn template_parts_carry_children_and_eq_split() {
        let mut dom = PpDom::new();
        let name = dom.push(PpNode {
            kind: PpKind::Text,
            span: span(2, 6),
        });
        let arg_name = dom.push(PpNode {
            kind: PpKind::Text,
            span: span(7, 8),
        });
        let eq_text = dom.push(PpNode {
            kind: PpKind::Text,
            span: span(8, 9),
        });
        let arg_value = dom.push(PpNode {
            kind: PpKind::Text,
            span: span(9, 10),
        });
        let parts = vec![
            Part {
                children: vec![name],
                eq: None,
                span: span(2, 6),
            },
            Part {
                children: vec![arg_name, eq_text, arg_value],
                eq: Some(1),
                span: span(7, 10),
            },
        ];
        let template = dom.push(PpNode {
            kind: PpKind::Template {
                parts: parts.clone(),
            },
            span: span(0, 12),
        });
        dom.push_root(template);

        assert_eq!(dom.node(template).kind, PpKind::Template { parts });
        assert_eq!(dom.node(template).span.len(), 12);
    }

    #[test]
    fn ext_tag_kind_models_open_and_selfclosing_forms() {
        let closed = PpKind::ExtTag {
            name: span(1, 7),
            attributes: Some(span(8, 20)),
            body: Some(span(21, 30)),
            closed: true,
        };
        let self_closing = PpKind::ExtTag {
            name: span(1, 4),
            attributes: None,
            body: None,
            closed: true,
        };
        assert_ne!(closed, self_closing);
        assert_ne!(PpKind::Ignored, PpKind::Comment);
        let debug = format!("{closed:?} {:?}", PpNodeId(3));
        assert!(debug.contains("ExtTag"));
    }
}