@twip/web 0.0.0-canary-05bfdb9 → 0.0.0-canary-1644e0a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,4 +32,41 @@ await editor.destroy();
32
32
 
33
33
  In Node, workers and tests, `HeadlessEditor.create()` gives the same API with no DOM.
34
34
 
35
+ ## Your own field types
36
+
37
+ A field whose type isn't one of Word's reads as a bookmark reference, and with no result it shows "Error! Bookmark
38
+ not defined.", as in Word. Declare the types your application fills itself after each `open`:
39
+
40
+ ```ts
41
+ await editor.commands.setApplicationFieldTypes({ types: ['CUSTOMVALUE', 'CUSTOMTEXT'] });
42
+ ```
43
+
44
+ Their fields then keep their results, show nothing when empty (their `setFieldLabels` label, else «first argument»,
45
+ in design mode) and are listed by `fieldsUpdateRequested` on `updateFields`, for `setFieldResults` to fill. Types
46
+ compare without case; each call replaces the list, and `[]` clears it. Setting it doesn't mark the document modified.
47
+
48
+ ## Serving the engine's files yourself
49
+
50
+ The engine runs from files in the package's `dist/`: `worker.js`, `twip-wasm.js`, `twip_wasm.wasm`, and the
51
+ `transport/` and `worker/` folders the worker imports. By default the SDK finds them beside its own code, through
52
+ `new URL(..., import.meta.url)`. If your bundler doesn't follow those (Angular's esbuild builder doesn't), copy
53
+ `dist/` as it is to a folder you serve, and say where, once, before the first `FontLibrary` call or editor:
54
+
55
+ ```ts
56
+ import { configure } from '@twip/web';
57
+
58
+ configure({ assets: new URL('twip/', document.baseURI) }); // or a CDN: 'https://cdn.example.com/twip/'
59
+ ```
60
+
61
+ `assets` may be on another origin. The worker then starts from a same-origin `blob:` module that imports
62
+ `worker.js`, so the other origin must:
63
+
64
+ - send `Access-Control-Allow-Origin` (your page's origin, or `*`) on every file in the folder, as module scripts and
65
+ the WebAssembly are fetched with CORS;
66
+ - serve `twip_wasm.wasm` as `application/wasm`, or the engine compiles it after a full download instead of while
67
+ streaming.
68
+
69
+ With a Content Security Policy, allow `blob:` in `worker-src`, the other origin in `script-src` and `connect-src`, and
70
+ `'wasm-unsafe-eval'` in `script-src` (which the engine needs on any origin).
71
+
35
72
  Guides: https://jaxcksn.github.io/twip/docs/guides/web. Licensed under Apache-2.0.
@@ -508,6 +508,15 @@ export interface Commands {
508
508
  setSubTemplateRules(options: {
509
509
  rules: ReferenceRule[];
510
510
  }): Promise<void>;
511
+ /**
512
+ * The application's own field types (a code's first word, compared without case), replacing those set before:
513
+ * their fields keep their results, show their labels in design mode ({@link Commands.setFieldLabels}, else
514
+ * «first argument»), are listed by `fieldsUpdateRequested` and are never read as bookmark references
515
+ * ("Error! Bookmark not defined."). `[]` clears them. Set it after each `open`, as the rules. View.
516
+ */
517
+ setApplicationFieldTypes(options: {
518
+ types: string[];
519
+ }): Promise<void>;
511
520
  /** Shows references' sub-template bodies in their place, or their fields (default: every reference). View. */
512
521
  setSubTemplateView(options: {
513
522
  ids?: ElementId[];
package/dist/commands.js CHANGED
@@ -146,6 +146,7 @@ const encodings = {
146
146
  setFieldTypes: 'spread',
147
147
  setUnmappedFields: 'spread',
148
148
  setSubTemplateRules: 'spread',
149
+ setApplicationFieldTypes: 'spread',
149
150
  setSubTemplateView: 'spread',
150
151
  editSubTemplate: 'spread',
151
152
  closeSubTemplate: 'none',
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * @packageDocumentation
6
6
  */
7
7
  export { Editor, HeadlessEditor } from './editor.js';
8
+ export { configure } from './transport/module.js';
8
9
  export type { ExportOptions, ExportResult, MountOptions, PdfOptions, SessionOptions } from './editor.js';
9
10
  export type { CommandEntry, CommandId, CommandInfo, Commands, FieldTargets, NoArgumentCommandId, Op, OpArgs, PlaceholderAction, PlaceholderLook, } from './commands.js';
10
11
  export type { AutoCorrectChange, AutoCorrectEntry, AutoCorrectOptions, CharacterFormatting, CommandState, CommandValues, EditingTarget, EditorState, ElementContext, HighlightColor, HorizontalPosition, HorizontalReference, LineSpacing, MarkupView, Mixed, ObjectHorizontalAlignment, ObjectPlacement, ObjectSelection, ObjectVerticalAlignment, ObjectWrap, PageFormatting, ParagraphFormatting, ReviewState, RowHeight, SelectionSummary, TablePosition, TableSelection, TableStyleOptions, TableWidth, TextDistance, Tri, UnderlineStyle, VerticalPosition, VerticalReference, ViewState, WrapSide, } from './state.js';
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * @packageDocumentation
6
6
  */
7
7
  export { Editor, HeadlessEditor } from './editor.js';
8
+ export { configure } from './transport/module.js';
8
9
  export { EditorError } from './errors.js';
9
10
  export { FontLibrary } from './fonts/library.js';
10
11
  export { cm, inches, mm, pt } from './types.js';
@@ -47,6 +47,22 @@ export interface TwipModule {
47
47
  deleteContext(handle: number): void;
48
48
  };
49
49
  }
50
+ /**
51
+ * Sets where the package's runtime files (dist/: worker.js, twip-wasm.js, twip_wasm.wasm, transport/ and worker/) are
52
+ * served from, for an application whose bundler doesn't follow `new URL(..., import.meta.url)`. Call it once, before
53
+ * anything starts the engine: the first {@link FontLibrary} call, {@link Editor.mount} or
54
+ * {@link HeadlessEditor.create}. The engine is shared by every session, so it's not a per-session option.
55
+ *
56
+ * `assets` is the folder, resolved against the document's base URL. It may be on another origin (a CDN), which must
57
+ * then send CORS headers for every file; see the package README.
58
+ *
59
+ * @throws {@link EditorError} `not-allowed` when the engine has already loaded from another folder.
60
+ */
61
+ export declare function configure(options: {
62
+ assets: string | URL;
63
+ }): void;
64
+ /** The configured assets folder, if any, read to start the engine: it can't change after this. */
65
+ export declare function useAssets(): URL | undefined;
50
66
  /** Instantiates the engine module, with `dispatch` registered before any engine call can call back. */
51
67
  export declare function loadModule(dispatch: NonNullable<TwipModule['twipDispatch']>): Promise<TwipModule>;
52
68
  export {};
@@ -1,15 +1,49 @@
1
1
  // Loading the engine module: the one place that knows where the WebAssembly build is and what it exports.
2
2
  // The build copies the emscripten output (twip-wasm.js and twip_wasm.wasm) next to dist/index.js.
3
+ import { EditorError } from '../errors.js';
3
4
  /**
4
- * Where the engine module and its WebAssembly are, relative to this file in dist/transport/. Both are literal
5
- * `new URL(..., import.meta.url)`s, so a bundler copies both files into the application's build.
5
+ * Where the engine module and its WebAssembly are by default: relative to this file in dist/transport/. Both are
6
+ * literal `new URL(..., import.meta.url)`s, so a bundler that follows them copies both files into the application's
7
+ * build. {@link configure}'s `assets` replaces them.
6
8
  */
7
- const moduleUrl = new URL('../twip-wasm.js', import.meta.url);
8
- const wasmUrl = new URL('../twip_wasm.wasm', import.meta.url);
9
+ const defaultModuleUrl = new URL('../twip-wasm.js', import.meta.url);
10
+ const defaultWasmUrl = new URL('../twip_wasm.wasm', import.meta.url);
11
+ /** The folder of the package's dist/ files, when the application set one. */
12
+ let assets;
13
+ /** Whether this thread has read `assets` to start the engine: it can't change after. */
14
+ let used = false;
15
+ /**
16
+ * Sets where the package's runtime files (dist/: worker.js, twip-wasm.js, twip_wasm.wasm, transport/ and worker/) are
17
+ * served from, for an application whose bundler doesn't follow `new URL(..., import.meta.url)`. Call it once, before
18
+ * anything starts the engine: the first {@link FontLibrary} call, {@link Editor.mount} or
19
+ * {@link HeadlessEditor.create}. The engine is shared by every session, so it's not a per-session option.
20
+ *
21
+ * `assets` is the folder, resolved against the document's base URL. It may be on another origin (a CDN), which must
22
+ * then send CORS headers for every file; see the package README.
23
+ *
24
+ * @throws {@link EditorError} `not-allowed` when the engine has already loaded from another folder.
25
+ */
26
+ export function configure(options) {
27
+ const url = new URL(options.assets, globalThis.document?.baseURI ?? globalThis.location?.href);
28
+ if (!url.pathname.endsWith('/'))
29
+ url.pathname += '/';
30
+ if (used && url.href !== assets?.href) {
31
+ throw new EditorError('not-allowed', 'configure() must be called before the first FontLibrary call or editor: the engine has already loaded');
32
+ }
33
+ assets = url;
34
+ }
35
+ /** The configured assets folder, if any, read to start the engine: it can't change after this. */
36
+ export function useAssets() {
37
+ used = true;
38
+ return assets;
39
+ }
9
40
  /** Instantiates the engine module, with `dispatch` registered before any engine call can call back. */
10
41
  export async function loadModule(dispatch) {
42
+ const assets = useAssets();
43
+ const moduleUrl = assets ? new URL('twip-wasm.js', assets) : defaultModuleUrl;
44
+ const wasmUrl = assets ? new URL('twip_wasm.wasm', assets) : defaultWasmUrl;
11
45
  const { default: create } = (await import(moduleUrl.href));
12
- // In Node the module finds its file itself; in a browser it's wherever the bundler put it.
46
+ // In Node the module finds its file itself; in a browser it's wherever the bundler, or `assets`, put it.
13
47
  const locateFile = moduleUrl.protocol === 'file:' ? undefined : (file) => (file.endsWith('.wasm') ? wasmUrl : new URL(file, moduleUrl)).href;
14
48
  const module = await create({ twipDispatch: dispatch, locateFile });
15
49
  module.twipDispatch = dispatch;
@@ -5,6 +5,7 @@
5
5
  // cache before any event of the same drain.
6
6
  import { EditorError, unsupported } from '../errors.js';
7
7
  import { live } from '../host/dom.js';
8
+ import { useAssets } from './module.js';
8
9
  import { Method, toError } from './protocol.js';
9
10
  let modules;
10
11
  /** Whether this page can run the engine in a worker: module workers and `transferControlToOffscreen`. */
@@ -43,6 +44,26 @@ export function chooseExecution(execution = 'auto') {
43
44
  throw unsupported("execution: 'worker' on this browser");
44
45
  return 'main';
45
46
  }
47
+ /**
48
+ * Starts the engine worker. With no `assets`, it's beside this file's directory, in dist/: written out in one
49
+ * expression, as bundlers recognise it and build the worker and its imports into the application. With `assets`, it's
50
+ * there, told the folder in its URL's query (worker.ts); on another origin, through a same-origin module that imports
51
+ * it, as a worker's script must come from the page's origin.
52
+ */
53
+ function startWorker(assets) {
54
+ if (!assets)
55
+ return new Worker(new URL('../worker.js', import.meta.url), { type: 'module', name: 'twip-engine' });
56
+ const url = new URL(`worker.js?assets=${encodeURIComponent(assets.href)}`, assets);
57
+ if (url.origin === location.origin)
58
+ return new Worker(url, { type: 'module', name: 'twip-engine' });
59
+ const bootstrap = URL.createObjectURL(new Blob([`import ${JSON.stringify(url.href)};`], { type: 'text/javascript' }));
60
+ const worker = new Worker(bootstrap, { type: 'module', name: 'twip-engine' });
61
+ // Fetched by the time the worker answers, or fails.
62
+ const revoke = () => URL.revokeObjectURL(bootstrap);
63
+ worker.addEventListener('message', revoke, { once: true });
64
+ worker.addEventListener('error', revoke, { once: true });
65
+ return worker;
66
+ }
46
67
  let shared;
47
68
  /** The page's one worker transport. */
48
69
  export function workerTransport() {
@@ -139,9 +160,7 @@ export class WorkerTransport {
139
160
  #start() {
140
161
  if (this.#worker)
141
162
  return this.#worker;
142
- // The worker is beside this file's directory, in dist/. Written out in one expression, as bundlers recognise
143
- // it: they build the worker and its imports into the application.
144
- const worker = new Worker(new URL('../worker.js', import.meta.url), { type: 'module', name: 'twip-engine' });
163
+ const worker = startWorker(useAssets());
145
164
  live.workers++;
146
165
  worker.onmessage = ({ data }) => this.#receive(data);
147
166
  worker.onerror = (e) => {
Binary file
@@ -6,7 +6,6 @@
6
6
  import { EditorError } from '../errors.js';
7
7
  import { directTransport } from '../transport/direct.js';
8
8
  import { fromError } from '../transport/protocol.js';
9
- const engine = directTransport();
10
9
  const signals = new Map();
11
10
  /** Posts to the page, moving the buffer of exported bytes instead of copying it. */
12
11
  function post(message) {
@@ -35,8 +34,16 @@ function run(t, { id, op, args }) {
35
34
  return t[op].apply(t, args);
36
35
  }
37
36
  }
38
- // Session 0 is the engine's own: font requests.
39
- engine.then((t) => t.listen(0, post), () => { });
37
+ let started;
38
+ /** The engine, loaded on the first request: by then worker.ts has applied the page's `assets`. */
39
+ function engine() {
40
+ if (!started) {
41
+ started = directTransport();
42
+ // Session 0 is the engine's own: font requests.
43
+ started.then((t) => t.listen(0, post), () => { });
44
+ }
45
+ return started;
46
+ }
40
47
  onmessage = ({ data }) => {
41
48
  if (data.op === 'abort') {
42
49
  signals.get(data.id)?.abort();
@@ -44,7 +51,7 @@ onmessage = ({ data }) => {
44
51
  }
45
52
  // Every request waits on the same promise, so they reach the engine's queue in arrival order. Id 0 is a replayed
46
53
  // library change: nothing waits for its reply.
47
- engine.then((t) => run(t, data)).then((ok) => data.id && post({ id: data.id, ok }), (e) => {
54
+ engine().then((t) => run(t, data)).then((ok) => data.id && post({ id: data.id, ok }), (e) => {
48
55
  if (!data.id)
49
56
  return console.error(e);
50
57
  const error = e instanceof EditorError ? e : new EditorError('internal', String(e), { cause: e });
package/dist/worker.js CHANGED
@@ -1,2 +1,8 @@
1
1
  // The page's engine worker entry (`@twip/web/worker.js`). The SDK starts it itself; applications never load it.
2
2
  import './worker/entry.js';
3
+ import { configure } from './transport/module.js';
4
+ // The page's `assets` (configure()), in the query it started this worker with: the engine loads from there, not from
5
+ // this file's own URL. Applied before the first request, which loads the engine (worker/entry.ts).
6
+ const assets = new URL(import.meta.url).searchParams.get('assets');
7
+ if (assets)
8
+ configure({ assets });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twip/web",
3
- "version": "0.0.0-canary-05bfdb9",
3
+ "version": "0.0.0-canary-1644e0a",
4
4
  "description": "The Twip DOCX editor for the browser and Node: a managed, framework-independent editor over the WebAssembly engine.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",