Build a single offline page
Bundle your shim, Inkkit and its stylesheet into one HTML file with no external requests. With Vite, thevite-plugin-singlefile plugin inlines every script and stylesheet into index.html:
@aicayzer/inkkit, even if no diagram ever renders. Import from @aicayzer/inkkit/core throughout the page unless you need diagram previews. Compatibility has the page sizes for each entry.
If the page sets a Content Security Policy, it must allow the inline script and style that a single-file build produces, and blob: images. Inkkit decodes images and diagrams through blob: URLs for clipboard and print output. Without img-src blob:, printableSnapshot() rejects for documents with images or diagrams. Also allow any custom scheme your adapters use for display URLs.
Follow the bridge rules
These rules apply to every call that crosses between native code and the page:- Pass the generation your native model holds as the expected generation on every call that takes one. A call meant for a document that has since been replaced then fails with
stale-documentinstead of changing the new one. See Documents and saving. - Send JSON
nullfor a missing value. Inkkit treatsnullas omitted for optional parameters and option fields. - Await promises in the shim.
mount,printableSnapshot,clipboardSnapshot,pasteanddestroyreturn promises. Every other member returns its result at once. - Send
codeandmessageto native code, not the error object. JSON serialization drops an error’smessage, and native code branches oncode. See Errors. - Encode bytes. A
Uint8Arrayhas no useful JSON form. Encode image bytes, for example as base64, on both sides. This applies toCapturedImage,PortableImage,ClipboardInput.images,ClipboardOutputimages andPrintableDocument.assets. - Convert coordinates. Points and rectangles, such as
ClientPointandTextRect, are CSS pixels in the web view’s client area, not device pixels. Convert them in native code. - Wait for the page.
mountis asynchronous. Have the shim post a ready message aftermountresolves, and send no calls before it arrives.
Write the shim and the native side
The shim mounts the editor, exposes a few functions onwindow, and posts events through window.webkit.messageHandlers. Each function returns a reply object, so a failure reaches native code as a code and message instead of a rejected promise. The native side calls the functions with callAsyncJavaScript and receives the messages in a WKScriptMessageHandler.
changed message carries only the document version. Take the text with snapshot at the save boundary. If snapshotText() throws, don’t save and keep the document open: a failed snapshot is not an unchanged document. Documents and saving has the full save loop, and reloadDocument for files that change on disk.
Call native code from adapters
An adapter is a set of JavaScript functions, so it lives in the shim. To keep images or files in native storage, have each asynchronous adapter function call native code and return its answer as a promise. Register the handler withaddScriptMessageHandler(_:contentWorld:name:) and a WKScriptMessageHandlerWithReply, so that postMessage returns a promise that native code resolves or rejects.
ImageAdapter.presentation and WikiLinkAdapter.resolve are synchronous. Answer them in the shim without waiting for native code. For images, return a display URL that the shim can build from the reference, such as one in a custom scheme that native code serves through a WKURLSchemeHandler:
DocumentContext with each native request, and check it in native code before you act on the answer. Images, files and wiki links covers what each adapter call must do.
Pass native text input
Most hosts let the web view receive keys and text as usual. When your native layer receives typed text or a key before the web view does, pass it on:insertText(text, generation)inserts exactly that text, as typing would. It returnsfalse, and inserts nothing, during composition.keyDown(input, generation)runs a key press as if it reached the editor. It returnstruewhen Inkkit handled the key; then don’t also handle it natively.
Manage focus and disposal
Loading a document doesn’t focus the editor. Callfocus(generation) when editing should start at once, and blur(generation) to take focus off the editor’s text surfaces. Don’t find Inkkit’s elements to blur them.
Before you close the document or tear down the web view, take a fresh snapshot and save it. Then await destroy(), and only after it resolves remove the root or release the web view. The content controller keeps a strong reference to its message handlers, so remove them with removeScriptMessageHandler(forName:) when you are done, as tearDown() does above.
