Skip to content

Commit e5b1050

Browse files
author
Jonathan D.A. Jewell
committed
Migrate to rescript.json and wire background/content PDF detection
1 parent 013c806 commit e5b1050

4 files changed

Lines changed: 198 additions & 16 deletions

File tree

bsconfig.json renamed to rescript.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
}
88
],
99
"package-specs": {
10-
"module": "es6",
10+
"module": "esmodule",
1111
"in-source": true
1212
},
1313
"suffix": ".bs.js",

src/background.res

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,72 @@
11
/* SPDX-License-Identifier: AGPL-3.0-or-later WITH Palimpsest */
22

3+
type runtime
4+
type onMessage
5+
6+
@val @scope("browser")
7+
external runtime: runtime = "runtime"
8+
9+
@get
10+
external onMessage: runtime => onMessage = "onMessage"
11+
12+
@send
13+
external addListener: (onMessage, Js.Json.t => Js.Promise.t<Js.Json.t>) => unit = "addListener"
14+
15+
let unsafeJson = (value: 'a): Js.Json.t => Obj.magic(value)
16+
17+
let makeResponse = (
18+
~ok: bool,
19+
~blocks: array<PdfTool.block>=[],
20+
~error: option<string>=None,
21+
): Js.Json.t => {
22+
let response = Js.Dict.empty()
23+
Js.Dict.set(response, "ok", Js.Json.boolean(ok))
24+
Js.Dict.set(response, "blocks", unsafeJson(blocks))
25+
switch error {
26+
| Some(message) => Js.Dict.set(response, "error", Js.Json.string(message))
27+
| None => ()
28+
}
29+
unsafeJson(response)
30+
}
31+
32+
let decodeDetectRequest = (message: Js.Json.t): option<string> => {
33+
switch Js.Json.decodeObject(message) {
34+
| Some(payload) =>
35+
let action =
36+
payload->Js.Dict.get("action")->Belt.Option.flatMap(Js.Json.decodeString)->Belt.Option.getWithDefault("")
37+
let url =
38+
payload->Js.Dict.get("url")->Belt.Option.flatMap(Js.Json.decodeString)->Belt.Option.getWithDefault("")
39+
if action == "detectBlocks" && url != "" {
40+
Some(url)
41+
} else {
42+
None
43+
}
44+
| None => None
45+
}
46+
}
47+
48+
let detectFromUrl = (url: string): Js.Promise.t<Js.Json.t> => {
49+
let detectPromise =
50+
Js.Promise2.then(Webapi.Fetch.fetch(url), response =>
51+
Js.Promise2.then(response->Webapi.Fetch.Response.arrayBuffer, pdfBuffer =>
52+
Js.Promise2.then(PdfTool.detectBlocks(pdfBuffer), blocks =>
53+
Js.Promise.resolve(makeResponse(~ok=true, ~blocks))
54+
)
55+
)
56+
)
57+
58+
Js.Promise2.catch(detectPromise, err => {
59+
Js.log2("detect blocks failed", err)
60+
Js.Promise.resolve(makeResponse(~ok=false, ~error=Some("detectBlocks failed")))
61+
})
62+
}
63+
64+
let _ =
65+
addListener(onMessage(runtime), message =>
66+
switch decodeDetectRequest(message) {
67+
| Some(url) => detectFromUrl(url)
68+
| None => Js.Promise.resolve(makeResponse(~ok=false, ~error=Some("unsupported action")))
69+
}
70+
)
71+
372
Js.log("Blocky Writer background service worker loaded")

src/content.res

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,91 @@
11
/* SPDX-License-Identifier: AGPL-3.0-or-later WITH Palimpsest */
22

3-
@val
4-
external href: string = "window.location.href"
3+
type runtime
54

6-
if href->Js.String2.includes("gov.uk") {
7-
Js.log2("Blocky Writer active on", href)
5+
@val @scope("browser")
6+
external runtime: runtime = "runtime"
7+
8+
@send
9+
external sendMessage: (runtime, Js.Json.t) => Js.Promise.t<Js.Json.t> = "sendMessage"
10+
11+
let unsafeJson = (value: 'a): Js.Json.t => Obj.magic(value)
12+
13+
let makeDetectMessage = (url: string): Js.Json.t => {
14+
let payload = Js.Dict.empty()
15+
Js.Dict.set(payload, "action", Js.Json.string("detectBlocks"))
16+
Js.Dict.set(payload, "url", Js.Json.string(url))
17+
unsafeJson(payload)
18+
}
19+
20+
let isPdfUrl = (url: string): bool => url->Js.String2.toLowerCase->Js.String2.endsWith(".pdf")
21+
22+
let findPdfTarget = (): option<string> => {
23+
let currentUrl = Webapi.Dom.location->Webapi.Dom.Location.href
24+
if isPdfUrl(currentUrl) {
25+
Some(currentUrl)
26+
} else {
27+
switch Webapi.Dom.document->Webapi.Dom.Document.querySelector("a[href$='.pdf']") {
28+
| Some(linkElement) =>
29+
switch Webapi.Dom.Element.asHtmlElement(linkElement) {
30+
| Some(link) =>
31+
let linkHref = link->Webapi.Dom.HtmlElement.href
32+
if isPdfUrl(linkHref) {
33+
Some(linkHref)
34+
} else {
35+
None
36+
}
37+
| None => None
38+
}
39+
| None => None
40+
}
41+
}
42+
}
43+
44+
let logDetectionResponse = (response: Js.Json.t): unit => {
45+
switch Js.Json.decodeObject(response) {
46+
| Some(payload) =>
47+
let ok =
48+
payload->Js.Dict.get("ok")->Belt.Option.flatMap(Js.Json.decodeBoolean)->Belt.Option.getWithDefault(false)
49+
if ok {
50+
let count =
51+
payload
52+
->Js.Dict.get("blocks")
53+
->Belt.Option.flatMap(Js.Json.decodeArray)
54+
->Belt.Option.map(Belt.Array.length)
55+
->Belt.Option.getWithDefault(0)
56+
Js.log2("Blocky Writer detected blocks", count)
57+
} else {
58+
let message =
59+
payload
60+
->Js.Dict.get("error")
61+
->Belt.Option.flatMap(Js.Json.decodeString)
62+
->Belt.Option.getWithDefault("unknown error")
63+
Js.log2("Blocky Writer detection failed", message)
64+
}
65+
| None => Js.log("Blocky Writer received non-object response")
66+
}
67+
}
68+
69+
let requestBlockDetection = (targetUrl: string): unit => {
70+
Js.log2("Blocky Writer detectBlocks target", targetUrl)
71+
let request = makeDetectMessage(targetUrl)
72+
let sendPromise = sendMessage(runtime, request)
73+
let loggedPromise = Js.Promise2.then(sendPromise, response => {
74+
logDetectionResponse(response)
75+
Js.Promise.resolve(response)
76+
})
77+
let _ = Js.Promise2.catch(loggedPromise, err => {
78+
Js.log2("Blocky Writer message failed", err)
79+
Js.Promise.resolve(Js.Json.null)
80+
})
81+
()
82+
}
83+
84+
let currentUrl = Webapi.Dom.location->Webapi.Dom.Location.href
85+
86+
if currentUrl->Js.String2.includes("gov.uk") {
87+
switch findPdfTarget() {
88+
| Some(targetUrl) => requestBlockDetection(targetUrl)
89+
| None => Js.log("Blocky Writer found no PDF target on page")
90+
}
891
}

src/core/PdfTool.res

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,55 @@ type block = {
88
height: float,
99
}
1010

11-
type arrayBuffer = Js.Typed_array.ArrayBuffer.t
11+
type arrayBuffer = Webapi.Fetch.arrayBuffer
12+
type uint8Array = Js.Typed_array.Uint8Array.t
1213

13-
type wasmModule
14+
@module("../../rust/pdftool_core/pkg/pdftool_core.js")
15+
external initWasm: unit => Js.Promise.t<unit> = "default"
16+
17+
@module("../../rust/pdftool_core/pkg/pdftool_core.js")
18+
external detectBlocksNative: uint8Array => array<block> = "detect_blocks"
1419

1520
@module("../../rust/pdftool_core/pkg/pdftool_core.js")
16-
external wasm: wasmModule = "default"
21+
external fillBlocksNative: (uint8Array, array<block>, Js.Dict.t<string>) => uint8Array = "fill_blocks"
22+
23+
let initState: ref<option<Js.Promise.t<unit>>> = ref(None)
1724

18-
@send
19-
external detectBlocksRaw: (wasmModule, arrayBuffer) => Js.Promise.t<array<block>> = "detect_blocks"
25+
let toNativeArrayBuffer = (buffer: arrayBuffer): Js.Typed_array.ArrayBuffer.t => Obj.magic(buffer)
2026

21-
@send
22-
external fillBlocksRaw: (wasmModule, arrayBuffer, array<block>, Js.Dict.t<string>) => Js.Promise.t<arrayBuffer> = "fill_blocks"
27+
let fromNativeArrayBuffer = (buffer: Js.Typed_array.ArrayBuffer.t): arrayBuffer => Obj.magic(buffer)
2328

24-
let detectBlocks = (pdfData: arrayBuffer): Js.Promise.t<array<block>> =>
25-
detectBlocksRaw(wasm, pdfData)
29+
let ensureInitialized = (): Js.Promise.t<unit> => {
30+
switch initState.contents {
31+
| Some(promise) => promise
32+
| None =>
33+
let promise = initWasm()
34+
initState.contents = Some(promise)
35+
promise
36+
}
37+
}
38+
39+
let detectBlocks = (pdfData: arrayBuffer): Js.Promise.t<array<block>> => {
40+
let bytes = Js.Typed_array.Uint8Array.fromBuffer(toNativeArrayBuffer(pdfData))
41+
let detection = Js.Promise2.then(ensureInitialized(), _ => Js.Promise.resolve(detectBlocksNative(bytes)))
42+
Js.Promise2.catch(detection, err => {
43+
Js.log2("detectBlocks error", err)
44+
Js.Promise.resolve([])
45+
})
46+
}
2647

2748
let fillBlocks = (
2849
pdfData: arrayBuffer,
2950
blocks: array<block>,
3051
fields: Js.Dict.t<string>,
31-
): Js.Promise.t<arrayBuffer> =>
32-
fillBlocksRaw(wasm, pdfData, blocks, fields)
52+
): Js.Promise.t<arrayBuffer> => {
53+
let bytes = Js.Typed_array.Uint8Array.fromBuffer(toNativeArrayBuffer(pdfData))
54+
let fill = Js.Promise2.then(ensureInitialized(), _ => {
55+
let filled = fillBlocksNative(bytes, blocks, fields)
56+
Js.Promise.resolve(fromNativeArrayBuffer(Js.Typed_array.Uint8Array.buffer(filled)))
57+
})
58+
Js.Promise2.catch(fill, err => {
59+
Js.log2("fillBlocks error", err)
60+
Js.Promise.resolve(fromNativeArrayBuffer(Js.Typed_array.ArrayBuffer.make(0)))
61+
})
62+
}

0 commit comments

Comments
 (0)