car-inference 0.61.2

Local model inference for CAR — Candle backend with Qwen3 models
Documentation
//! What a page's markup hides, and which addresses are web addresses, for
//! the search-result readers that hand page content to a model: Parslee
//! Core's `web_search` (`car-server-core`'s assistant net tools) and the
//! daemon's keyless DuckDuckGo search ([`crate::search`]). One copy, so a
//! way of hiding one reader catches is never a way past the other.

/// Whether an element's attributes hide it: `hidden`, `aria-hidden="true"`,
/// or an inline style that [`style_hides`]. `attrs` are its attributes as
/// (lowercase name, value) pairs.
pub fn hides_by_attributes<'a>(attrs: impl IntoIterator<Item = (&'a str, &'a str)>) -> bool {
    attrs.into_iter().any(|(name, value)| match name {
        "hidden" => true,
        "aria-hidden" => value
            .trim_matches(|c: char| c.is_ascii_whitespace())
            .eq_ignore_ascii_case("true"),
        "style" => style_hides(value),
        _ => false,
    })
}

/// `address` as a web address: parsed as a URL whose scheme is `http` or
/// `https`, and serialized, or `None` for any other scheme (`javascript:`,
/// `data:`, `mailto:`, `file:`) and for an address that is not an absolute
/// URL, such as a relative path.
pub fn web_address(address: &str) -> Option<String> {
    reqwest::Url::parse(address)
        .ok()
        .filter(|url| matches!(url.scheme(), "http" | "https"))
        .map(String::from)
}

/// The values of `display` other than `none` that every major browser
/// accepts, as single keywords.
#[rustfmt::skip]
const SHOWN_DISPLAYS: &[&str] = &[
    "block", "inline", "inline-block", "flow-root", "flex", "inline-flex", "grid",
    "inline-grid", "table", "inline-table", "table-row-group", "table-header-group",
    "table-footer-group", "table-row", "table-cell", "table-column-group", "table-column",
    "table-caption", "list-item", "contents",
];

/// Whether an inline `style` hides its element: the `display` it sets is
/// `none`, or the `visibility` it sets is `hidden` or `collapse`. As in a
/// browser, the last declaration of each counts and an `!important` one
/// beats any that is not; comments, escapes and case are read as CSS reads
/// them ([`css_declarations`]). Only a value every major browser accepts
/// counts: `none` or one of [`SHOWN_DISPLAYS`], and `visible`, `hidden` or
/// `collapse`. Any other (a display of two keywords, a keyword such as
/// `inherit`, `var()`) is skipped, which leaves an earlier `none`, `hidden`
/// or `collapse` in force. Other ways of hiding (opacity, size, position)
/// are not recognized.
pub fn style_hides(style: &str) -> bool {
    // For `display` and for `visibility`: whether the declaration that
    // counts hides, and whether it is `!important`.
    let (mut display, mut visibility) = (None, None);
    for declaration in css_declarations(style) {
        let Some((name, value)) = declaration.split_once(':') else {
            continue;
        };
        let (value, important) = match value.rsplit_once('!') {
            Some((value, flag)) if css_trim(flag) == "important" => (value, true),
            _ => (value, false),
        };
        let (slot, hides) = match (css_trim(name), css_trim(value)) {
            ("display", "none") => (&mut display, true),
            ("display", value) if SHOWN_DISPLAYS.contains(&value) => (&mut display, false),
            ("visibility", value @ ("visible" | "hidden" | "collapse")) => {
                (&mut visibility, value != "visible")
            }
            _ => continue,
        };
        if important || !matches!(slot, Some((_, true))) {
            *slot = Some((hides, important));
        }
    }
    [display, visibility]
        .iter()
        .any(|slot| matches!(slot, Some((true, _))))
}

/// `s` without the whitespace CSS skips around a name or value.
fn css_trim(s: &str) -> &str {
    s.trim_matches(|c: char| c.is_ascii_whitespace())
}

/// The declarations of an inline style: its text split at each `;` that is
/// outside strings and brackets, with ASCII letters lowercased, a comment
/// read as a space and escapes decoded. A whole string, and an escaped
/// character that is not a letter, digit, `-`, `_` or outside ASCII, read
/// as U+FFFD, which matches no name or keyword and splits nothing.
fn css_declarations(style: &str) -> Vec<String> {
    let mut declarations = vec![String::new()];
    let mut closers = Vec::new();
    let mut chars = style.chars().peekable();
    while let Some(c) = chars.next() {
        let c = match c {
            '/' if chars.peek() == Some(&'*') => {
                chars.next();
                let mut star = false;
                for c in chars.by_ref() {
                    if star && c == '/' {
                        break;
                    }
                    star = c == '*';
                }
                ' '
            }
            '\\' => css_escape(&mut chars)
                .filter(|&c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_') || !c.is_ascii())
                .unwrap_or('\u{FFFD}'),
            '"' | '\'' => {
                // A string ends at its closing quote or, unclosed, at a
                // newline.
                while let Some(inner) = chars.next() {
                    match inner {
                        '\\' => {
                            chars.next();
                        }
                        '\n' | '\r' | '\x0C' => break,
                        _ if inner == c => break,
                        _ => {}
                    }
                }
                '\u{FFFD}'
            }
            '(' | '[' | '{' => {
                closers.push(match c {
                    '(' => ')',
                    '[' => ']',
                    _ => '}',
                });
                c
            }
            ')' | ']' | '}' if closers.last() == Some(&c) => {
                closers.pop();
                c
            }
            ';' if closers.is_empty() => {
                declarations.push(String::new());
                continue;
            }
            c => c,
        };
        if let Some(declaration) = declarations.last_mut() {
            declaration.push(c.to_ascii_lowercase());
        }
    }
    declarations
}

/// The character a CSS escape stands for, read after its `\`: up to six hex
/// digits (and one whitespace character after them), or else the next
/// character. `None` when the `\` escapes nothing: before a newline or at
/// the end.
fn css_escape(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<char> {
    let mut code = 0u32;
    let mut digits = 0;
    while digits < 6 {
        match chars.peek().and_then(|d| d.to_digit(16)) {
            Some(digit) => {
                code = code * 16 + digit;
                digits += 1;
                chars.next();
            }
            None => break,
        }
    }
    if digits == 0 {
        return match chars.peek() {
            Some('\n' | '\r' | '\x0C') | None => None,
            Some(_) => chars.next(),
        };
    }
    if chars.peek().is_some_and(char::is_ascii_whitespace) {
        chars.next();
    }
    Some(
        char::from_u32(code)
            .filter(|&c| c != '\0')
            .unwrap_or('\u{FFFD}'),
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn only_http_and_https_urls_are_web_addresses() {
        for (address, web) in [
            (
                "https://one.example/a?b=c",
                Some("https://one.example/a?b=c"),
            ),
            ("HTTP://One.Example", Some("http://one.example/")),
            (" https://one.example/ ", Some("https://one.example/")),
            ("javascript:alert(1)", None),
            ("java\nscript:alert(1)", None),
            ("data:text/html,hi", None),
            ("mailto:a@example.com", None),
            ("file:///etc/passwd", None),
            ("/relative", None),
            ("//one.example/", None),
            ("", None),
        ] {
            assert_eq!(web_address(address).as_deref(), web, "{address:?}");
        }
    }

    #[test]
    fn hidden_attributes_are_read_from_pairs() {
        assert!(hides_by_attributes([("class", "x"), ("hidden", "")]));
        assert!(hides_by_attributes([("aria-hidden", " TRUE ")]));
        assert!(!hides_by_attributes([("aria-hidden", "false")]));
        assert!(hides_by_attributes([("style", "display/**/:none")]));
        assert!(!hides_by_attributes([(
            "style",
            "display:none;display:block"
        )]));
        assert!(!hides_by_attributes([("type", "hidden")]));
    }
}