#![cfg_attr(
feature = "filter_doc",
doc = "Documentation for `scrapelect`'s built-in filters.
Conventions used:
- Signature of a filter: `value: T | name(arg_1: U_1, ...): V` means that `name` is a filter
that takes a *pipeline value* of type `T`, has arguments `arg_i` of type `U_i`, and returns
a value of type `V`
- Specifying an arg type with a question mark (e.g., `value: Value | dbg(msg: String?): Value`)
means that that argument (e.g., `msg`) is *optional* and can be omitted.
- List shorthand: a `List` is represented as `[a_0, a_1, a_2, ..., a_n]` to mean that
its elements are `a_0, ..., a_n` in that order. Indexing starts at 0. This syntax is currently
not valid `scrapelect`, but it is useful to express in documentation.
- Structure shorthand: similarly, a structure is represented as { key_1: value_1, key_2: value_2, ... } to
indicate that it has keys that correspond to the given values. This is not valid `scrapelect`,
but it is useful in documentation.
- Element shorthand: an inline HTML element (e.g., `<a href=\"github.com/suaviloquence/scrapelect\">Link text</a>`)
is also not valid `scrapelect`, but is useful to demonstrate how `Element`s are used
in filters.
"
)]
use std::{
collections::BTreeMap,
sync::{Arc, LazyLock},
};
use anyhow::Context as _;
use crate::interpreter::{
filter::{filter_fn, FilterDyn},
value::{EValue, ListIter, PValue, Pipeline, Value},
ElementContext,
};
type Structure<'doc> = BTreeMap<Arc<str>, PValue<'doc>>;
#[filter_fn]
pub fn id<'doc>(value: PValue<'doc>) -> anyhow::Result<PValue<'doc>> {
Ok(value)
}
#[filter_fn]
pub fn dbg<'doc>(value: PValue<'doc>, msg: Option<Arc<str>>) -> anyhow::Result<PValue<'doc>> {
let value: EValue = value.into();
eprintln!("{}: {}", value, msg.as_deref().unwrap_or("dbg message"));
Ok(value.into())
}
#[filter_fn]
pub fn tee<'doc>(
value: PValue<'doc>,
into: Arc<str>,
ctx: &mut ElementContext<'_, 'doc>,
) -> anyhow::Result<PValue<'doc>> {
let value: EValue = value.into();
ctx.set_var(into.to_string().into(), value.clone())?;
Ok(value.into())
}
#[filter_fn]
pub fn strip<'doc>(value: Arc<str>) -> anyhow::Result<PValue<'doc>> {
Ok(Value::String(value.trim().into()))
}
#[filter_fn]
pub fn attrs<'doc>(value: scraper::ElementRef<'doc>) -> anyhow::Result<PValue<'doc>> {
Ok(Value::Structure(
value
.value()
.attrs()
.map(|(k, v)| (Arc::from(k), Value::String(Arc::from(v))))
.collect(),
))
}
#[filter_fn]
pub fn take<'doc>(mut value: Structure<'doc>, key: Arc<str>) -> anyhow::Result<PValue<'doc>> {
Ok(value.remove(&key).unwrap_or(Value::Null))
}
#[filter_fn]
pub fn int<'doc>(value: PValue<'doc>) -> anyhow::Result<PValue<'doc>> {
let n = match value {
Value::Int(n) => n,
Value::Float(x) => x as i64,
Value::String(s) => s
.parse()
.with_context(|| format!("`{s}` is not an integer."))?,
_ => anyhow::bail!("expected an int, float, or string"),
};
Ok(Value::Int(n))
}
#[filter_fn]
pub fn float<'doc>(value: PValue<'doc>) -> anyhow::Result<PValue<'doc>> {
let x = match value {
Value::Int(n) => n as f64,
Value::Float(x) => x,
Value::String(s) => s
.parse()
.with_context(|| format!("`{s}` is not a float."))?,
_ => anyhow::bail!("expected an int, float, or string"),
};
Ok(Value::Float(x))
}
#[filter_fn]
pub fn nth<'doc>(mut value: ListIter<'doc>, i: i64) -> anyhow::Result<PValue<'doc>> {
match value.nth(i.try_into().context("negative indices are not supported")?) {
Some(x) => Ok(x),
None => anyhow::bail!("No element at index {i}"),
}
}
#[filter_fn]
pub fn keys<'doc>(value: Structure<'doc>) -> anyhow::Result<PValue<'doc>> {
Ok(Value::Extra(Pipeline::ListIter(Box::new(
value.into_keys().map(Value::String),
))))
}
#[filter_fn]
pub fn values<'doc>(value: Structure<'doc>) -> anyhow::Result<PValue<'doc>> {
Ok(Value::Extra(Pipeline::ListIter(Box::new(
value.into_values(),
))))
}
#[filter_fn]
pub fn and<'doc>(value: bool, with: bool) -> anyhow::Result<PValue<'doc>> {
Ok(Value::Bool(value && with))
}
#[filter_fn]
pub fn or<'doc>(value: bool, with: bool) -> anyhow::Result<PValue<'doc>> {
Ok(Value::Bool(value || with))
}
#[filter_fn]
pub fn not<'doc>(value: bool) -> anyhow::Result<PValue<'doc>> {
Ok(Value::Bool(!value))
}
#[filter_fn]
pub fn split<'doc>(value: Arc<str>, on: Option<Arc<str>>) -> anyhow::Result<PValue<'doc>> {
if let Some(delim) = on {
Ok(Value::List(
value
.split(&*delim)
.map(|x| Value::String(Arc::from(x)))
.collect(),
))
} else {
Ok(Value::List(
value
.split_whitespace()
.map(|x| Value::String(Arc::from(x)))
.collect(),
))
}
}
#[filter_fn]
pub fn eq<'doc>(value: PValue<'doc>, to: EValue<'doc>) -> anyhow::Result<PValue<'doc>> {
Ok(Value::Bool(EValue::from(value) == to))
}
#[filter_fn]
pub fn is_in<'doc>(value: PValue<'doc>, list: Vec<EValue<'doc>>) -> anyhow::Result<PValue<'doc>> {
Ok(Value::Bool(list.contains(&value.into())))
}
#[filter_fn]
pub fn truthy<'doc>(value: PValue<'doc>) -> anyhow::Result<PValue<'doc>> {
let truthy = match value {
Value::Null => false,
Value::Float(f) => f != 0.,
Value::Int(i) => i != 0,
Value::Bool(b) => b,
Value::String(s) => !s.is_empty(),
Value::List(l) => !l.is_empty(),
Value::Structure(s) => !s.is_empty(),
Value::Extra(Pipeline::Element(_)) => true,
Value::Extra(Pipeline::ListIter(mut i)) => i.next().is_some(),
Value::Extra(Pipeline::StructIter(mut i)) => i.next().is_some(),
};
Ok(Value::Bool(truthy))
}
#[filter_fn]
pub fn text<'doc>(value: scraper::ElementRef<'doc>) -> anyhow::Result<PValue<'doc>> {
Ok(Value::String(
value
.children()
.filter_map(|x| x.value().as_text().map(|text| &*text.text))
.collect::<String>()
.into(),
))
}
macro_rules! build_map {
($(
$id: ident,
)*) => {
[$(
(stringify!($id), Box::new($id()) as Box<dyn FilterDyn + Send + Sync>),
)*]
};
}
#[cfg(not(feature = "filter_doc"))]
pub static FILTERS: LazyLock<BTreeMap<&'static str, Box<dyn FilterDyn + Send + Sync>>> =
LazyLock::new(|| {
build_map! {
dbg,
tee,
strip,
take,
attrs,
int,
float,
nth,
keys,
values,
and,
or,
not,
split,
eq,
is_in,
text,
}
.into_iter()
.collect()
});