wikioxide 0.1.0

MediaWiki wikitext parser producing typed domain models (templates, links, categories, warnings) with template expansion — no HTML, no network
Documentation
//! Flat→source offset mapping (parse-only).
//!
//! `flatten` renders the preprocessor DOM to flattened text in which comments vanish and
//! templates/arguments/extension tags collapse to fixed-width strip markers, so flat
//! offsets no longer equal original-source offsets. [`ProvMap`] records, for every
//! verbatim-text run, its absolute flat and source starts; because a run advances one byte
//! at a time in both, any offset landing inside text maps back exactly. Marker and comment
//! bytes are intentionally not recorded — section boundaries (heading lines) always fall in
//! verbatim text, and skipping the gaps makes a section's source span contiguous: it
//! re-absorbs the vanished templates and comments that sit between two headings.

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

#[derive(Debug, Clone, Copy)]
struct ProvSeg {
    flat_start: u32,
    src_start: u32,
}

/// Maps offsets in the flattened text back to byte offsets in the original page source.
/// Built by `flatten`; meaningful only for the parse-only pipeline (expanded text has no
/// original-source correspondence).
#[derive(Debug, Default)]
pub(crate) struct ProvMap {
    segs: Vec<ProvSeg>,
    flat_len: u32,
    src_len: u32,
}

impl ProvMap {
    pub(crate) fn new(src_len: u32) -> Self {
        Self {
            segs: Vec::new(),
            flat_len: 0,
            src_len,
        }
    }

    pub(crate) fn push_text(&mut self, flat_start: u32, span: Span) {
        self.segs.push(ProvSeg {
            flat_start,
            src_start: span.start,
        });
    }

    /// Records the final flat length: a right edge at end-of-text maps to end-of-source, so
    /// trailing vanished content (a closing comment) stays inside the last section's body.
    pub(crate) fn seal(&mut self, flat_len: u32) {
        self.flat_len = flat_len;
    }

    pub(crate) fn src_len(&self) -> u32 {
        self.src_len
    }

    /// Maps one flat offset to its original-source byte offset. An offset at or past the end
    /// maps to the end of source; an offset before the first recorded run maps to 0.
    pub(crate) fn source_offset(&self, flat_off: u32) -> u32 {
        if flat_off >= self.flat_len {
            return self.src_len;
        }
        match self.segs.partition_point(|s| s.flat_start <= flat_off) {
            0 => 0,
            n => {
                let seg = self.segs[n - 1];
                seg.src_start + (flat_off - seg.flat_start)
            }
        }
    }
}

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

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

    #[test]
    fn empty_map_maps_everything_to_end() {
        let map = ProvMap::new(0);
        assert_eq!(map.source_offset(0), 0);
        assert_eq!(map.source_offset(5), 0);
    }

    #[test]
    fn a_single_verbatim_run_maps_one_to_one() {
        // Source "abcde" flattened verbatim at flat 0.
        let mut map = ProvMap::new(5);
        assert_eq!(map.src_len(), 5);
        map.push_text(0, span(0, 5));
        map.seal(5);
        assert_eq!(map.source_offset(0), 0);
        assert_eq!(map.source_offset(3), 3);
        // End-of-flat maps to end-of-source.
        assert_eq!(map.source_offset(5), 5);
        assert_eq!(map.source_offset(9), 5);
    }

    #[test]
    fn runs_after_a_marker_keep_absolute_source_offsets() {
        // Source: "ab{{x}}cd" (marker at src 2..7). Flat: "ab<M>cd" with a 10-byte marker.
        // Text run 1: flat 0..2 = src 0..2. Text run 2: flat 12.. = src 7...
        let mut map = ProvMap::new(9);
        map.push_text(0, span(0, 2));
        map.push_text(12, span(7, 9));
        map.seal(14);
        assert_eq!(map.source_offset(1), 1);
        // A flat offset inside the second run maps past the marker in source.
        assert_eq!(map.source_offset(13), 8);
    }

    #[test]
    fn an_offset_before_the_first_run_maps_to_zero() {
        // A leading marker: first text run does not start at flat 0.
        let mut map = ProvMap::new(3);
        map.push_text(10, span(7, 10));
        map.seal(13);
        assert_eq!(map.source_offset(2), 0);
    }
}