mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 00:56:30 +03:00
The engine no longer lives in a separate repo. `crates/` is a snapshot of the
open crates (foundation, core, common, context, live, hook, skills, comp,
comp-verbs, html, browser, detect, cli) plus `Cargo.lock`, taken as a git
archive of the engine repo at the commit that finished the boundary split.
None of that repo's history comes with it, and none of it should: the closed
half stays private.
The closed half is the rule engine. It ships as a prebuilt native archive per
target, `libimpeccable_detector.a`, published as a `detector-v<X>` GitHub
Release on this repo. `crates/core/build.rs` resolves and links it three ways:
`IMPECCABLE_DETECTOR_LIB=<dir>` for a local detector build, else the
`~/.impeccable/detector/<version>/<target>/` cache, else a download verified
against its `.sha256` sidecar. `crates/core` is a thin shim over a three-symbol
C ABI; nothing above it knows the boundary exists.
What changed versus the engine repo copy:
- Every crate manifest moves from `license-file.workspace` to
`license.workspace` (this workspace declares Apache-2.0), and the workspace
gains the `postcard` dependency the boundary encoding needs.
- The launcher contract test reads `skill/scripts/impeccable{,.cmd}` instead of
a sibling `launcher/` dir, and `engine_binary` downloads from
`github.com/pbakaus/impeccable/releases/download/engine-v<version>/` instead
of the retired dist repo. No oracle golden carried the old URL, so no
re-recording was owed.
- The tests that hunted for a public repo through `IMPECCABLE_PUBLIC_REPO`,
`../impeccable-second` or a hardcoded home directory now resolve the root as
`CARGO_MANIFEST_DIR/../..`, because they are in it. The env var stays as an
override for an out-of-tree checkout.
- The in-page bundle (`detect-antipatterns-browser.js`, 2 MB of generated wasm
glue) is no longer tracked. `crates/core/build.rs` resolves it beside the
archive, hands the path to `impeccable_core::browser::IN_PAGE_BUNDLE_JS`, and
live mode serves that. `scripts/check-detector-release.mjs` now requires it
and its `.sha256` in a detector release.
- The live crate embeds `skill/scripts/live-browser*.js` and
`modern-screenshot.umd.js` directly rather than through vendored copies, so
the binary and the installed skill cannot drift.
- `crates/browser/assets/` (an unused second copy of the bundle) is gone.
- `tests/lib/engine-bin.mjs` also accepts `target/release/impeccable`, so a
plain `cargo build --release -p impeccable` is enough to run `bun run test`.
Verified with the archive from a local detector build: `cargo test --workspace`
267 pass, oracle 795 pass / 0 fail / 0 missing, `bun run build` clean, the
default suite green, and the launcher's `engine-probe` handshake answering
through `skill/scripts/impeccable`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
121 lines
3.6 KiB
Rust
121 lines
3.6 KiB
Rust
//! Port of `cli/engine/shared/fonts.mjs`.
|
|
|
|
use crate::js::{self, ci, WS_CHARS};
|
|
use once_cell::sync::Lazy;
|
|
use regex::Regex;
|
|
|
|
/// JS `GOOGLE_FONTS_URL_RE` = `/fonts\.googleapis\.com\/css2?\?[^"'\s)<>]*/gi`.
|
|
static GOOGLE_FONTS_URL_RE: Lazy<Regex> = Lazy::new(|| {
|
|
Regex::new(&format!(
|
|
r#"{fonts}\.{googleapis}\.{com}/{css}2?\?[^"'{WSC})<>]*"#,
|
|
fonts = ci("fonts"),
|
|
googleapis = ci("googleapis"),
|
|
com = ci("com"),
|
|
css = ci("css"),
|
|
WSC = WS_CHARS
|
|
))
|
|
.unwrap()
|
|
});
|
|
|
|
/// JS `normalizeGoogleFontFamilyParam(value)`.
|
|
pub fn normalize_google_font_family_param(value: &str) -> Vec<String> {
|
|
value
|
|
.split('|')
|
|
.map(|part| js::to_lower_case(js::trim(part.split(':').next().unwrap_or(""))))
|
|
.filter(|s| !s.is_empty())
|
|
.collect()
|
|
}
|
|
|
|
/// Percent-decode one application/x-www-form-urlencoded value the way
|
|
/// `URLSearchParams` does: `+` is a space, `%XX` is a byte, invalid UTF-8
|
|
/// becomes U+FFFD.
|
|
fn form_urldecode(s: &str) -> String {
|
|
let bytes = s.as_bytes();
|
|
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
|
|
let mut i = 0;
|
|
while i < bytes.len() {
|
|
let b = bytes[i];
|
|
if b == b'+' {
|
|
out.push(b' ');
|
|
i += 1;
|
|
} else if b == b'%' && i + 2 < bytes.len() {
|
|
let h = &bytes[i + 1..i + 3];
|
|
match std::str::from_utf8(h)
|
|
.ok()
|
|
.and_then(|hs| u8::from_str_radix(hs, 16).ok())
|
|
{
|
|
Some(v) if h.iter().all(|c| c.is_ascii_hexdigit()) => {
|
|
out.push(v);
|
|
i += 3;
|
|
}
|
|
_ => {
|
|
out.push(b'%');
|
|
i += 1;
|
|
}
|
|
}
|
|
} else {
|
|
out.push(b);
|
|
i += 1;
|
|
}
|
|
}
|
|
String::from_utf8_lossy(&out).into_owned()
|
|
}
|
|
|
|
/// `new URLSearchParams(query).getAll('family')`.
|
|
fn get_all_family(query: &str) -> Vec<String> {
|
|
let mut out = Vec::new();
|
|
for pair in query.split('&') {
|
|
if pair.is_empty() {
|
|
continue;
|
|
}
|
|
let (name, value) = match pair.find('=') {
|
|
Some(i) => (&pair[..i], &pair[i + 1..]),
|
|
None => (pair, ""),
|
|
};
|
|
if form_urldecode(name) == "family" {
|
|
out.push(form_urldecode(value));
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// JS `extractGoogleFontFamilies(text)`.
|
|
pub fn extract_google_font_families(text: &str) -> Vec<String> {
|
|
let mut families = Vec::new();
|
|
if text.is_empty() {
|
|
return families;
|
|
}
|
|
for m in GOOGLE_FONTS_URL_RE.find_iter(text) {
|
|
let url = m.as_str();
|
|
let Some(query_start) = url.find('?') else {
|
|
continue;
|
|
};
|
|
let query = url[query_start + 1..].replace("&", "&");
|
|
for value in get_all_family(&query) {
|
|
families.extend(normalize_google_font_family_param(&value));
|
|
}
|
|
}
|
|
families
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn extracts_families() {
|
|
let html = r#"<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Playfair+Display&display=swap" rel="stylesheet">"#;
|
|
assert_eq!(
|
|
extract_google_font_families(html),
|
|
vec!["inter", "playfair display"]
|
|
);
|
|
let css = "@import url(https://fonts.googleapis.com/css?family=Roboto|Open+Sans:400,700);";
|
|
assert_eq!(
|
|
extract_google_font_families(css),
|
|
vec!["roboto", "open sans"]
|
|
);
|
|
assert!(extract_google_font_families("").is_empty());
|
|
assert_eq!(form_urldecode("a%20b%zz+c%E2%9C%93"), "a b%zz c\u{2713}");
|
|
}
|
|
}
|