use std::io::{BufRead, Write};
use std::sync::LazyLock;
use regex::Regex;
static TAG: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?s)(.*?)<(/?\w+)[^>]*>(?:([^<]*)(<.*?>)?)?").unwrap());
pub fn extract_page(
mut input: impl BufRead,
out: &mut impl Write,
id: &str,
templates: bool,
) -> std::io::Result<()> {
let mut page: Vec<String> = Vec::new();
let mut in_article = false;
let mut line = String::new();
loop {
line.clear();
if input.read_line(&mut line)? == 0 {
return Ok(());
}
if !line.contains('<') {
if !page.is_empty() {
page.push(line.clone());
}
continue;
}
let Some(caps) = TAG.captures(&line) else {
continue;
};
let tag = caps.get(2).expect("tag group").as_str();
let content = caps.get(3).map(|m| m.as_str()).unwrap_or("");
match tag {
"page" => {
page.clear();
page.push(line.clone());
in_article = false;
}
"id" => {
if id == content {
page.push(line.clone());
in_article = true;
} else if !in_article && !templates {
page.clear();
}
}
"title" => {
if templates {
if content.starts_with("Template:") {
page.push(line.clone());
} else {
page.clear();
}
} else {
page.push(line.clone());
}
}
"/page" => {
if !page.is_empty() {
page.push(line.clone());
for page_line in &page {
out.write_all(page_line.as_bytes())?;
}
out.write_all(b"\n")?; if !templates {
return Ok(());
}
}
page.clear();
}
_ => {
if !page.is_empty() {
page.push(line.clone());
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const DUMP: &str = "<mediawiki>\n <siteinfo>\n <base>http://x/wiki/Main</base>\n </siteinfo>\n\
\x20 <page>\n <title>First</title>\n <ns>0</ns>\n <id>1</id>\n <revision>\n \
<id>100</id>\n <text>first text</text>\n </revision>\n </page>\n\
\x20 <page>\n <title>Second</title>\n <ns>0</ns>\n <id>2</id>\n <revision>\n \
<id>200</id>\n <text>second text</text>\n </revision>\n </page>\n\
\x20 <page>\n <title>Template:Box</title>\n <ns>10</ns>\n <id>3</id>\n <revision>\n \
<id>300</id>\n <text>box body</text>\n </revision>\n </page>\n</mediawiki>\n";
fn run(id: &str, templates: bool) -> String {
let mut out = Vec::new();
extract_page(DUMP.as_bytes(), &mut out, id, templates).unwrap();
String::from_utf8(out).unwrap()
}
#[test]
fn extracts_page_by_id_and_stops() {
let out = run("2", false);
assert!(out.contains("<title>Second</title>"));
assert!(out.contains("second text"));
assert!(!out.contains("First"));
assert!(!out.contains("Template:Box"));
assert!(out.ends_with("</page>\n\n"));
}
#[test]
fn matches_revision_ids_too() {
let out = run("100", false);
assert!(out.contains("<id>100</id>"));
assert!(out.contains("first text"));
assert!(out.trim_end().ends_with("</page>"));
assert!(!out.contains("<title>First</title>"));
}
#[test]
fn unknown_id_prints_nothing() {
assert_eq!(run("999", false), "");
}
#[test]
fn template_mode_prints_all_template_pages() {
let out = run("1", true);
assert!(out.contains("<title>Template:Box</title>"));
assert!(out.contains("box body"));
assert!(!out.contains("<title>Second</title>"));
}
}