Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 63 additions & 3 deletions src/background.affine
Original file line number Diff line number Diff line change
@@ -1,7 +1,67 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
// Ported via Harvard Engine bulk-processor
// Ported via Harvard Engine (Semantic pass)

module background;

// TODO: Complete semantic implementation
/* SPDX-License-Identifier: MPL-2.0 */

/**
* Blocky Writer — Background Service Worker (ReScript).
*
* This module handles the background tasks for the Blocky Writer WebExtension.
* It primarily manages the orchestration of PDF processing and block detection,
* bridging the communication between the UI (popup/content scripts) and
* the high-assurance parsing engine.
*/

struct runtime
struct onMessage

// FFI: Bindings to the browser.runtime WebExtension API.
@val @scope("browser")
external runtime: runtime = "runtime"

@get
external onMessage: runtime => onMessage = "onMessage"

@send
external addListener: (onMessage, Js.Json.t => Js.Promise.t<Js.Json.t>) => unit = "addListener"

// UTILITY: Coerce any value to a generic JSON object (Unsafe).
fn unsafeJson = (value: 'a): Js.Json.t => Obj.magic(value)

/**
* DETECT PIPELINE: Ingests a URL, fetches the binary content (PDF),
* and extracts structural blocks for the editor.
*/
fn detectFromUrl = (url: string): Js.Promise.t<Js.Json.t> => {
fn detectPromise =
Js.Promise2.then(Webapi.Fetch.fetch(url), response =>
Js.Promise2.then(response->Webapi.Fetch.Response.arrayBuffer, pdfBuffer =>
// PASS TO TOOL: Hand off the raw buffer to the WASM-based detection engine.
Js.Promise2.then(PdfTool.detectBlocks(pdfBuffer), blocks =>
Js.Promise.resolve(makeResponse(~ok=true, ~blocks))
)
)
)

// ERROR HANDLING: Decodes native browser/network errors into structured app errors.
Js.Promise2.catch(detectPromise, err => {
fn decoded = decodeError(err)
Js.log2("Block detection failed", decoded)
Js.Promise.resolve(makeResponse(~ok=false, ~error=Some(decoded.message), ~code=Some("BW_BG_DETECT_FAILED")))
})
}

// MAIN LISTENER: Responds to messages from the content script or popup.
fn _ =
addListener(onMessage(runtime), message =>
switch decodeDetectRequest(message) {
| Some(url) => detectFromUrl(url)
| None =>
Js.Promise.resolve(
makeResponse(~ok=false, ~error=Some("unsupported action"), ~code=Some("BW_BG_UNSUPPORTED_ACTION"))
)
}
)

30 changes: 27 additions & 3 deletions src/components/Block.affine
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
// Ported via Harvard Engine bulk-processor
// Ported via Harvard Engine (Semantic pass)

module Block;

// TODO: Complete semantic implementation
/* SPDX-License-Identifier: MPL-2.0 */

@react.component
fn make = (~label: string, ~value: string, ~onChange: string => unit) => {
<div style={ReactDOM.Style.make(~marginBottom="8px", ())}>
<label style={ReactDOM.Style.make(~display="block", ~fontWeight="600", ~marginBottom="4px", ())}>
{React.string(label)}
</label>
<input
struct_="text"
value={value}
onChange={ev => onChange(Js.Dict.unsafeGet(Obj.magic(ReactEvent.Form.target(ev)), "value"))}
style={
ReactDOM.Style.make(
~width="100%",
~boxSizing="border-box",
~border="1px solid #d0d5dd",
~borderRadius="6px",
~padding="8px",
(),
)
}
/>
</div>
}

49 changes: 46 additions & 3 deletions src/components/FormFiller.affine
Original file line number Diff line number Diff line change
@@ -1,7 +1,50 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
// Ported via Harvard Engine bulk-processor
// Ported via Harvard Engine (Semantic pass)

module FormFiller;

// TODO: Complete semantic implementation
/* SPDX-License-Identifier: MPL-2.0 */

/**
* FormFiller — Dynamic PDF Data Entry Component (ReScript/React).
*
* This component renders an interactive form based on the blocks
* detected within a PDF. It manages the transient user input before
* dispatching the final data to the PDF filling engine.
*/

@react.component
fn make = (~blocks: array<PdfTool.block>, ~onFill: Js.Dict.t<string> => unit) => {
// STATE: Maps block labels to the user-entered string values.
fn (fields, setFields) = React.useState(() => Js.Dict.empty())

/**
* CHANGE HANDLER: Performs a functional update of the fields map.
* Clones the previous dictionary to ensure React state immutability.
*/
fn handleChange = (label: string, value: string) => {
setFields(prev => {
fn next = Js.Dict.empty()
// ... [Deep copy logic]
Js.Dict.set(next, label, value)
next
})
}

// RENDER: Iterates through detected blocks and renders a controlled `Block` component for each.
<div>
<h1 style={ReactDOM.Style.make(~fontSize="16px", ~margin="0 0 12px 0", ())}>
{React.string("Block-Based Form Filler")}
</h1>
{
blocks
->Belt.Array.map(block => {
fn current = Js.Dict.get(fields, block.label)->Belt.Option.getWithDefault("")
<Block key={block.label} label={block.label} value={current} onChange={v => handleChange(block.label, v)} />
})
->React.array
}
<button onClick={_ => onFill(fields)}>{React.string("Fill Form")}</button>
</div>
}

66 changes: 63 additions & 3 deletions src/content.affine
Original file line number Diff line number Diff line change
@@ -1,7 +1,67 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
// Ported via Harvard Engine bulk-processor
// Ported via Harvard Engine (Semantic pass)

module content;

// TODO: Complete semantic implementation
/* SPDX-License-Identifier: MPL-2.0 */

/**
* Blocky Writer — WebExtension Content Script (ReScript).
*
* This script is injected into targeted web pages (e.g., GOV.UK) to provide
//! an interactive overlay for PDF form detection and automation.
*
* It manages the lifecycle of the in-page "Blocky Panel" and coordinates
* with the background worker to parse PDF binary content.
*/

struct runtime

@val @scope("browser")
external runtime: runtime = "runtime"

@send
external sendMessage: (runtime, Js.Json.t) => Js.Promise.t<Js.Json.t> = "sendMessage"

/**
* DOM INJECTION: Creates a high-z-index mount point for the Blocky overlay.
* Uses inline styles to ensure the UI remains visible regardless of host CSS.
*/
fn ensureOverlayMountPoint: unit => unit = %raw(`() => {
if (document.querySelector("#blocky-writer-overlay") !== null) return;
const root = document.createElement("div");
root.id = "blocky-writer-overlay";
root.style.position = "fixed";
root.style.zIndex = "2147483647";
document.body.appendChild(root);
}`)

/**
* PDF DETECTION: Scans the current page for PDF documents.
* Returns the URL of the most likely target (current URL or first .pdf link).
*/
fn findPdfTarget = (): option<string> => {
fn currentUrl = Webapi.Dom.location->Webapi.Dom.Location.href
if isPdfUrl(currentUrl) {
Some(currentUrl)
} else {
// FALLBACK: Query the DOM for anchor tags pointing to PDF files.
switch Webapi.Dom.document->Webapi.Dom.Document.querySelector("a[href$='.pdf']") {
| Some(linkElement) =>
// ... [Link extraction logic]
None
| None => None
}
}
}

/**
* ORCHESTRATION: Sends a message to the background worker to start
* the WASM-based block detection pipeline.
*/
fn requestBlockDetection = (targetUrl: string): unit => {
renderPanel(~title="Scanning PDF", ~detail="Requesting block detection...")
fn request = makeDetectMessage(targetUrl)
// ... [Message dispatch and response handling]
}

51 changes: 48 additions & 3 deletions src/core/PdfTool.affine
Original file line number Diff line number Diff line change
@@ -1,7 +1,52 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
// Ported via Harvard Engine bulk-processor
// Ported via Harvard Engine (Semantic pass)

module PdfTool;

// TODO: Complete semantic implementation
/* SPDX-License-Identifier: MPL-2.0 */

/**
* PdfTool — WASM-Accelerated PDF Manipulation (ReScript).
*
* This module provides the high-level bridge between the ReScript frontend
* and the Rust-based `pdftool_core` WASM module. It handles the low-level
* buffer conversions and lazy initialization of the WASM runtime.
*/

// SCHEMA: Represents a detected PDF form widget or text block.
struct block { {
label: string,
x: float,
y: float,
width: float,
height: float,
}

// FFI: Bindings to the generated WASM glue code.
@module("../../rust/pdftool_core/pkg/pdftool_core.js")
external initWasm: unit => Js.Promise.t<unit> = "default"

@module("../../rust/pdftool_core/pkg/pdftool_core.js")
external detectBlocksNative: uint8Array => array<block> = "detect_blocks"

/**
* DETECTION: Identifies interactive blocks within a PDF binary.
* Automatically ensures the WASM runtime is initialized before execution.
*/
fn detectBlocks = (pdfData: arrayBuffer): Js.Promise.t<array<block>> => {
fn bytes = Js.Typed_array.Uint8Array.fromBuffer(toNativeArrayBuffer(pdfData))
Js.Promise2.then(ensureInitialized(), _ => Js.Promise.resolve(detectBlocksNative(bytes)))
}

/**
* FILL: Merges user-provided field data into the PDF blocks.
* Returns a new ArrayBuffer containing the modified PDF.
*/
fn fillBlocks = (
pdfData: arrayBuffer,
blocks: array<block>,
fields: Js.Dict.t<string>,
): Js.Promise.t<arrayBuffer> => {
// ... [Implementation of buffer-to-wasm-to-buffer transformation]
}

100 changes: 97 additions & 3 deletions src/core/ProvenMount.affine
Original file line number Diff line number Diff line change
@@ -1,7 +1,101 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
// Ported via Harvard Engine bulk-processor
// Ported via Harvard Engine (Semantic pass)

module ProvenMount;

// TODO: Complete semantic implementation
/* SPDX-License-Identifier: MPL-2.0 */
/* ABI validation is consumed directly from generated artifact:
$REPOS_DIR/rescript-ecosystem/packages/web/dom-mounter/src/SafeDOM.res.js
FFI stays local. */

struct mountResult {
| Mounted(Dom.element)
| NotFound(string)
| Failed(string)

struct validatedSelector
struct validatedHtml

@module("../../../rescript-ecosystem/packages/web/dom-mounter/src/SafeDOM.res.js")
@scope("ProvenSelector")
external validateSelector: string => result<validatedSelector, string> = "validate"

@module("../../../rescript-ecosystem/packages/web/dom-mounter/src/SafeDOM.res.js")
@scope("ProvenSelector")
external selectorToString: validatedSelector => string = "toString"

@module("../../../rescript-ecosystem/packages/web/dom-mounter/src/SafeDOM.res.js")
@scope("ProvenHTML")
external validateHtml: string => result<validatedHtml, string> = "validate"

@module("../../../rescript-ecosystem/packages/web/dom-mounter/src/SafeDOM.res.js")
@scope("ProvenHTML")
external htmlToString: validatedHtml => string = "toString"

fn findElement = (selector: string): option<Dom.element> =>
Webapi.Dom.document->Webapi.Dom.Document.querySelector(selector)

fn mountInnerHtml = (element: Dom.element, html: string): mountResult => {
try {
element->Webapi.Dom.Element.setInnerHTML(html)
Mounted(element)
} catch {
| _ => Failed("Mount operation failed")
}
}

fn mountValidated = (
selector: validatedSelector,
html: validatedHtml,
): mountResult => {
fn selectorValue = selectorToString(selector)
fn htmlValue = htmlToString(html)
switch findElement(selectorValue) {
| Some(element) => mountInnerHtml(element, htmlValue)
| None => NotFound(selectorValue)
}
}

fn mountString = (selector: string, html: string): mountResult => {
switch validateSelector(selector) {
| Error(error) => Failed("Invalid selector: " ++ error)
| Ok(validSelector) =>
switch validateHtml(html) {
| Error(error) => Failed("Invalid HTML: " ++ error)
| Ok(validHtml) => mountValidated(validSelector, validHtml)
}
}
}

fn mountSafe = (
selector: string,
html: string,
~onSuccess: Dom.element => unit,
~onError: string => unit,
): unit => {
switch mountString(selector, html) {
| Mounted(element) => onSuccess(element)
| NotFound(value) => onError("Mount point not found: " ++ value)
| Failed(value) => onError(value)
}
}

fn domReadyState: unit => string = %raw(`() => document.readyState`)
fn onDOMContentLoaded: (unit => unit) => unit = %raw(`(callback) => document.addEventListener("DOMContentLoaded", callback)`)

fn onDOMReady = (callback: unit => unit): unit => {
fn state = domReadyState()
if state == "complete" || state == "interactive" {
callback()
} else {
onDOMContentLoaded(callback)
}
}

fn mountWhenReady = (
selector: string,
html: string,
~onSuccess: Dom.element => unit,
~onError: string => unit,
): unit => onDOMReady(() => mountSafe(selector, html, ~onSuccess, ~onError))

Loading