SPB Git

spb/zyquo-atlas Public License

The AI-native macOS web browser — every surface, intelligent.

Swift 75.2% JavaScript 22% Shell 2% Makefile 0.9%
3.6 KB · 98 lines swift
Raw Blame History
1//2//  ContentExtractor.swift3//  Zyquo Atlas4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Injects the extraction scripts (Mozilla Readability + AtlasExtractor driver)9//  into an isolated WKContentWorld and pulls a normalized PageContext out of a10//  live tab via callAsyncJavaScript. Isolation keeps the extractor from11//  colliding with or being observed by page JS (docs/AI-BROWSER-RESEARCH.md §2).12//  All page content is treated as untrusted downstream (Phase 3.C prompt13//  hygiene).14//1516import Foundation17import WebKit1819enum ContentExtractionError: LocalizedError {20    case scriptsUnavailable21    case scriptFailure(String)22    case decodeFailure(String)2324    var errorDescription: String? {25        switch self {26        case .scriptsUnavailable:27            return "Content extraction scripts are missing from the app bundle."28        case .scriptFailure(let detail):29            return "Couldn't read this page's content (\(detail))."30        case .decodeFailure(let detail):31            return "Couldn't parse the extracted page content (\(detail))."32        }33    }34}3536@MainActor37final class ContentExtractor {38    /// Dedicated world so extraction never touches the page's JS globals.39    static let world = WKContentWorld.world(name: "ZyquoAtlasContent")4041    /// Installs the extraction user scripts into a configuration. Call once per42    /// WKWebViewConfiguration (ProfileStore) before any tab loads.43    static func install(into config: WKWebViewConfiguration) {44        let ucc = config.userContentController45        for name in ["Readability", "Readability-readerable", "AtlasExtractor"] {46            guard let source = script(name) else { continue }47            ucc.addUserScript(WKUserScript(48                source: source,49                injectionTime: .atDocumentEnd,50                forMainFrameOnly: true,51                in: world52            ))53        }54    }5556    /// Extracts a PageContext from a tab's web view. `mode` "auto" tries57    /// Readability first; "rawText" forces the visible-text fallback.58    static func extract(from webView: WKWebView, mode: String = "auto") async throws -> PageContext {59        let result: Any?60        do {61            result = try await webView.callAsyncJavaScript(62                "return window.__zyquoAtlas ? window.__zyquoAtlas.extract(mode, maxBytes) : { ok:false, error:'driver-missing' };",63                arguments: ["mode": mode, "maxBytes": 600_000],64                in: nil,65                contentWorld: world66            )67        } catch {68            throw ContentExtractionError.scriptFailure(error.localizedDescription)69        }7071        guard let dict = result as? [String: Any] else {72            throw ContentExtractionError.scriptFailure("no result")73        }74        if dict["ok"] as? Bool != true {75            throw ContentExtractionError.scriptFailure((dict["error"] as? String) ?? "unknown")76        }77        guard let context = dict["context"] else {78            throw ContentExtractionError.scriptFailure("empty context")79        }80        do {81            let data = try JSONSerialization.data(withJSONObject: context)82            return try JSONDecoder().decode(PageContext.self, from: data)83        } catch {84            throw ContentExtractionError.decodeFailure(error.localizedDescription)85        }86    }8788    // MARK: - Script loading8990    private static func script(_ name: String) -> String? {91        guard let url = Bundle.module.url(forResource: name, withExtension: "js"),92              let source = try? String(contentsOf: url, encoding: .utf8) else {93            return nil94        }95        return source96    }97}98