use std::io::{self, Write};
use std::path::PathBuf;
use anyhow::Context;
use clap::{Parser, ValueEnum};
use rayon::prelude::*;
use wikrs::{dump, extract, output, parser, render};
const BATCH_PAGES: usize = 4096;
const BATCH_BYTES: usize = 32 << 20;
#[derive(Debug, Clone, Copy, PartialEq, ValueEnum)]
enum Format {
Text,
Jsonl,
Sections,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum Engine {
Strip,
Ast,
}
#[derive(Debug, Parser)]
#[command(name = "wikrs", version, about)]
struct Cli {
#[arg(long)]
input: PathBuf,
#[arg(long)]
index: Option<PathBuf>,
#[arg(long, value_enum, default_value_t = Format::Text)]
format: Format,
#[arg(long, value_enum, default_value_t = Engine::Ast)]
engine: Engine,
#[arg(long)]
stats: bool,
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
if cli.format == Format::Sections && matches!(cli.engine, Engine::Strip) {
anyhow::bail!("--format sections needs the AST; use --engine ast (the default)");
}
if cli.format == Format::Sections && cli.stats {
anyhow::bail!("--stats measures plain-text conversion; use --format text or jsonl");
}
let mut pages = match &cli.index {
Some(index) => dump::open_multistream(&cli.input, index)?,
None => dump::open(&cli.input)?,
};
let stdout = io::stdout();
let mut w = io::BufWriter::new(stdout.lock());
let (mut total, mut clean, mut read) = (0usize, 0usize, 0usize);
loop {
let mut batch: Vec<dump::Page> = Vec::with_capacity(BATCH_PAGES);
let mut bytes = 0usize;
for res in pages.by_ref() {
let page = res.with_context(|| {
format!(
"reading dump {} (after {read} page(s))",
cli.input.display()
)
})?;
read += 1;
if !page.is_article() {
continue;
}
bytes += page.text.len();
batch.push(page);
if batch.len() >= BATCH_PAGES || bytes >= BATCH_BYTES {
break;
}
}
if batch.is_empty() {
break;
}
let rendered: Vec<(String, String)> = batch
.into_par_iter()
.map(|p| {
let text = match (cli.format, cli.engine) {
(Format::Sections, _) => {
output::to_sections_jsonl(&p.title, &parser::parse(&p.text).nodes)
}
(_, Engine::Strip) => extract::strip(&p.text),
(_, Engine::Ast) => render::plain(&parser::parse(&p.text).nodes),
};
(p.title, text)
})
.collect();
for (title, text) in &rendered {
total += 1;
if cli.stats {
if extract::looks_clean(text) {
clean += 1;
}
} else {
match cli.format {
Format::Text | Format::Sections => writeln!(w, "{text}")?,
Format::Jsonl => writeln!(w, "{}", output::to_jsonl(title, text))?,
}
}
}
}
if cli.stats {
let pct = if total == 0 {
0.0
} else {
100.0 * clean as f64 / total as f64
};
eprintln!("pages={total} clean={clean} ({pct:.1}% clean conversion)");
return Ok(());
}
w.flush()?;
Ok(())
}