Files
pbakaus_impeccable/crates/foundation/src/fonts.rs
T
Paul BakausandClaude Fable 5.1 0547ed6a63 reorg C: the open Rust runtime joins this repo as one Cargo workspace
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
2026-09-01 15:31:26 -07:00

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("&amp;", "&");
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&amp;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}");
}
}