{"_id":"@arnon3339/thtex","_rev":"2-18e545658ecac41af8f20863b49356e0","name":"@arnon3339/thtex","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@arnon3339/thtex","version":"0.1.0","keywords":["latex","xelatex","xetex","webassembly","wasm","web-worker","pdf"],"author":{"name":"Arnon Songmoolnak"},"license":"MIT","_id":"@arnon3339/thtex@0.1.0","maintainers":[{"name":"arnon3339","email":"arnon3339@gmail.com"}],"homepage":"https://github.com/arnon3339/thtex#readme","bugs":{"url":"https://github.com/arnon3339/thtex/issues"},"bin":{"thtex":"scripts/copy-assets.mjs","thtex-assets":"scripts/copy-assets.mjs","thtex-manifest":"scripts/create-runtime-manifest.mjs"},"dist":{"shasum":"447081bf0e4dcde3022585fd3ab2e201a0e0b6b1","tarball":"https://registry.npmjs.org/@arnon3339/thtex/-/thtex-0.1.0.tgz","fileCount":443,"integrity":"sha512-+XX8u7S9c0rpNGQ8jMm0GcrvKjGDzGZkYCYeB4zzTkw4NXOEOh9lVxJ8yQoAJmlje/1w97e5P/twR8FcAGuxVQ==","signatures":[{"sig":"MEQCIHdPYxRilKFw5G4yNW06mUB4PvhjfUvs/oDHL7XUgA1EAiBoZq/cyyS70wno5IE4ve/fCTfJr4ym5UK195TF0sp8VA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":74675136},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"add0874498323a8d7dc6e96864947dc655b22b48","scripts":{"dev":"vite","lint":"oxlint src scripts","build":"node scripts/clean-dist.mjs && tsc -p tsconfig.lib.json && tsc -p tsconfig.worker.json","check":"pnpm run build:runtime && pnpm run lint && pnpm run test:runtime && pnpm pack --dry-run","predev":"pnpm run build && node scripts/copy-assets.mjs --to public/xelatex","prepack":"pnpm run build","preview":"vite preview","build:demo":"pnpm run build && node scripts/copy-assets.mjs --to public/xelatex && tsc -b && vite build","pack:check":"pnpm pack --dry-run","test:runtime":"node scripts/runtime-smoke-test.mjs","build:runtime":"pnpm xelatex:runtime && pnpm run build","xelatex:format":"node scripts/create-xelatex-format.mjs","xelatex:runtime":"pnpm xelatex:sync-artifacts && pnpm xelatex:sync-tex-support && pnpm xelatex:format && pnpm xelatex:manifest","xelatex:manifest":"node scripts/create-runtime-manifest.mjs","xelatex:sync-artifacts":"node scripts/sync-engine-artifacts.mjs","xelatex:sync-tex-support":"node scripts/sync-tex-support.mjs"},"_npmUser":{"name":"arnon3339","email":"arnon3339@gmail.com"},"repository":{"url":"git+https://github.com/arnon3339/thtex.git","type":"git"},"_npmVersion":"11.8.0","description":"Browser XeLaTeX compiler powered by WebAssembly and an isolated Web Worker.","directories":{},"sideEffects":false,"_nodeVersion":"25.6.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.9.0","devDependencies":{"vite":"^8.1.1","react":"^19.2.7","oxlint":"^1.71.0","react-dom":"^19.2.7","typescript":"~6.0.2","@types/node":"^24.13.2","@types/react":"^19.2.17","@types/react-dom":"^19.2.3","@vitejs/plugin-react":"^6.0.3"},"_npmOperationalInternal":{"tmp":"tmp/thtex_0.1.0_1784378554670_0.5279526158049594","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@arnon3339/thtex","version":"0.1.1","description":"Browser XeLaTeX compiler powered by WebAssembly and an isolated Web Worker.","type":"module","license":"MIT","author":{"name":"Arnon Songmoolnak"},"homepage":"https://thtex.pages.dev/","repository":{"type":"git","url":"git+https://github.com/arnon3339/thtex.git"},"bugs":{"url":"https://github.com/arnon3339/thtex/issues"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"keywords":["latex","xelatex","xetex","bibtex","bibliography","webassembly","wasm","web-worker","pdf"],"engines":{"node":">=20"},"packageManager":"pnpm@11.9.0","sideEffects":false,"main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./package.json":"./package.json"},"bin":{"thtex":"scripts/copy-assets.mjs","thtex-assets":"scripts/copy-assets.mjs","thtex-manifest":"scripts/create-runtime-manifest.mjs"},"scripts":{"predev":"pnpm run build && node scripts/copy-assets.mjs --to public/xelatex","dev":"vite","build":"node scripts/clean-dist.mjs && tsc -p tsconfig.lib.json && tsc -p tsconfig.worker.json","build:runtime":"pnpm xelatex:runtime && pnpm run build","build:demo":"pnpm run build && node scripts/copy-assets.mjs --to public/xelatex && tsc -b && vite build","check":"pnpm run build:runtime && pnpm run lint && pnpm run test:runtime && pnpm pack --dry-run","lint":"oxlint src scripts","pack:check":"pnpm pack --dry-run","prepack":"pnpm run build","preview":"vite preview","test:runtime":"node scripts/runtime-smoke-test.mjs","xelatex:format":"node scripts/create-xelatex-format.mjs","xelatex:manifest":"node scripts/create-runtime-manifest.mjs","xelatex:runtime":"pnpm xelatex:sync-artifacts && pnpm xelatex:sync-tex-support && pnpm xelatex:format && pnpm xelatex:manifest","xelatex:sync-artifacts":"node scripts/sync-engine-artifacts.mjs","xelatex:sync-tex-support":"node scripts/sync-tex-support.mjs"},"devDependencies":{"@types/node":"^24.13.2","@types/react":"^19.2.17","@types/react-dom":"^19.2.3","@vitejs/plugin-react":"^6.0.3","oxlint":"^1.71.0","react":"^19.2.7","react-dom":"^19.2.7","typescript":"~6.0.2","vite":"^8.1.1"},"gitHead":"3cdf206a22157994f4f902091e558325724ea510","_id":"@arnon3339/thtex@0.1.1","_nodeVersion":"25.6.0","_npmVersion":"11.8.0","dist":{"integrity":"sha512-jeqyGnXgc4l7w/LIJnSqc/hKflslYAluCHPg1p0WMp9S1AdAOlRVY8cAcjaNHCWi/0A7czxExd4mI4B7+OZ6bQ==","shasum":"31738b43b7dec87c1b42e8d6f259f00ade1b6b89","tarball":"https://registry.npmjs.org/@arnon3339/thtex/-/thtex-0.1.1.tgz","fileCount":453,"unpackedSize":76051484,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBqtHaUOEEvOrQ3bgUSJJmmCZWRJT7i3GXNJMAfAU51GAiEA9QPv6sGpXZ3Zbn5EMo5Mf4UdcxxzobHQbQnt/U7EfQw="}]},"_npmUser":{"name":"arnon3339","email":"arnon3339@gmail.com"},"directories":{},"maintainers":[{"name":"arnon3339","email":"arnon3339@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/thtex_0.1.1_1784386873372_0.4798556860605139"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-18T12:42:34.538Z","modified":"2026-07-18T15:01:14.031Z","0.1.0":"2026-07-18T12:42:35.151Z","0.1.1":"2026-07-18T15:01:13.872Z"},"bugs":{"url":"https://github.com/arnon3339/thtex/issues"},"author":{"name":"Arnon Songmoolnak"},"license":"MIT","homepage":"https://thtex.pages.dev/","keywords":["latex","xelatex","xetex","bibtex","bibliography","webassembly","wasm","web-worker","pdf"],"repository":{"type":"git","url":"git+https://github.com/arnon3339/thtex.git"},"description":"Browser XeLaTeX compiler powered by WebAssembly and an isolated Web Worker.","maintainers":[{"name":"arnon3339","email":"arnon3339@gmail.com"}],"readme":"# ThTeX\n\n[![Live Demo](https://img.shields.io/badge/Live_Demo-thtex.pages.dev-F38020?logo=cloudflarepages&logoColor=white)](https://thtex.pages.dev/)\n[![npm](https://img.shields.io/npm/v/@arnon3339/thtex?logo=npm)](https://www.npmjs.com/package/@arnon3339/thtex)\n\n**[Open the live React/PWA demo](https://thtex.pages.dev/)** to compile\nXeLaTeX documents directly in your browser.\n\nCompile XeLaTeX documents to PDF in the browser. XeTeX, BibTeX, and `xdvipdfmx` run in\nan isolated Web Worker, so compilation does not block the React/UI thread.\n\nThe package provides:\n\n- a typed, framework-independent browser API;\n- configurable one-to-five-pass XeTeX compilation;\n- optional classic BibTeX execution with standard `.bst` styles;\n- **additional files** — inject fonts, images, style files, and other resources\n  into the compiler's virtual filesystem at compile time;\n- automatic preservation of `.aux`, `.toc`, `.lof`, `.lot`, `.out`, and other\n  generated files between passes;\n- serialized calls within one compiler instance;\n- isolated filesystems between browser tabs and worker instances;\n- a CLI that installs the worker, WASM engines, fonts, and TeX runtime into an\n  application's public directory.\n\n## Requirements\n\n- A modern browser with WebAssembly, ES modules, and Web Workers.\n- Node.js 20 or newer for installing/building assets.\n- A web server. WASM compilation will not work through a `file://` URL.\n\n## Quick start\n\nInstall from npm:\n\n```bash\nnpm install @arnon3339/thtex\n# or: pnpm add @arnon3339/thtex\n# or: yarn add @arnon3339/thtex\n```\n\nInstall and verify the browser runtime in your public directory:\n\n```bash\nnpx @arnon3339/thtex --to public/xelatex\n```\n\nThe command works with npm, pnpm, and Yarn. The legacy command remains\navailable:\n\n```bash\npnpm exec thtex-assets --to public/xelatex\n```\n\nBoth commands copy and verify the worker, engines, ICU data, fonts, and TeX\nruntime. They overwrite package-owned files but do not delete unrelated files.\nRun the command again whenever the installed package version changes. To check\nan existing installation without changing it:\n\n```bash\nnpx @arnon3339/thtex --check --to public/xelatex\n```\n\nFor a Vite application, automate this in `package.json`:\n\n```json\n{\n  \"scripts\": {\n    \"xelatex:assets\": \"thtex --to public/xelatex\",\n    \"predev\": \"pnpm xelatex:assets\",\n    \"prebuild\": \"pnpm xelatex:assets\",\n    \"dev\": \"vite\",\n    \"build\": \"vite build\"\n  }\n}\n```\n\n## Basic usage\n\n```ts\nimport { XeLaTeXCompiler } from \"@arnon3339/thtex\";\n\nconst compiler = new XeLaTeXCompiler({\n  assetBaseUrl: \"/xelatex/\",\n  onStatus({ message }) {\n    console.log(message);\n  },\n  onLog({ stream, message }) {\n    console[stream === \"stderr\" ? \"error\" : \"log\"](message);\n  },\n});\n\nawait compiler.ready;\n\nconst source = String.raw`\\RequirePackage[OT1]{fontenc}\n\\documentclass{article}\n\\begin{document}\n\\section{WebAssembly}\nHello from XeLaTeX.\n\\end{document}`;\n\nconst result = await compiler.compile(source);\nconst pdfBlob = new Blob([result.pdf], { type: \"application/pdf\" });\nconst pdfUrl = URL.createObjectURL(pdfBlob);\n\ndocument.querySelector(\"iframe\")!.src = pdfUrl;\n\n// When the page/component is destroyed:\ncompiler.dispose();\nURL.revokeObjectURL(pdfUrl);\n```\n\nIf the application is deployed below a URL prefix, include that prefix in\n`assetBaseUrl`. In Vite this can be derived from `import.meta.env.BASE_URL`:\n\n```ts\nconst compiler = new XeLaTeXCompiler({\n  assetBaseUrl: `${import.meta.env.BASE_URL}xelatex/`,\n});\n```\n\n`compile()` resolves with:\n\n```ts\ntype XeLaTeXCompileResult = {\n  pdf: ArrayBuffer;\n  log: string;\n  passes: number;\n  bibtexRan: boolean;\n};\n```\n\n## BibTeX bibliographies\n\nClassic BibTeX is optional and disabled by default. Add the `.bib` database\nthrough `additionalFiles` and pass `bibtex: true`. The worker runs XeTeX,\nBibTeX, two follow-up XeTeX passes, and finally `xdvipdfmx`. Standard styles\nsuch as `plain`, `abbrv`, `alpha`, and `unsrt` are included.\n\n```ts\nconst bibliography = new TextEncoder().encode(String.raw`\n@book{knuth1984,\n  author = {Donald E. Knuth},\n  title = {The TeXbook},\n  year = {1984},\n  publisher = {Addison-Wesley}\n}`);\n\nconst source = String.raw`\\documentclass{article}\n\\begin{document}\nSee \\cite{knuth1984}.\n\\bibliographystyle{plain}\n\\bibliography{references}\n\\end{document}`;\n\nconst result = await compiler.compile(source, {\n  bibtex: true,\n  additionalFiles: [{ path: \"references.bib\", data: bibliography }],\n});\n\nconsole.log(result.bibtexRan); // true\n```\n\nThe available modes are:\n\n| Value | Behavior |\n| --- | --- |\n| `false` or omitted | Do not run BibTeX. This is the default. |\n| `true` | Run BibTeX and report an error if the first XeTeX pass did not create classic bibliography directives. |\n| `\"auto\"` | Inspect the first `.aux` file and run BibTeX only when bibliography data and style directives are present. |\n\nWhen BibTeX runs, the worker guarantees at least three XeTeX passes regardless\nof the requested `passes` value. The first creates BibTeX input, the second\nloads the generated bibliography and writes citation mappings, and the third\nreads those mappings so citations no longer render as `[?]`:\n\n```ts\nawait compiler.compile(source, {\n  passes: 3,\n  bibtex: true,\n  additionalFiles: [{ path: \"references.bib\", data: bibliography }],\n});\n```\n\nBibLaTeX/Biber is not supported by this runtime.\n\n## Additional files (fonts, images, and resources)\n\nUse `additionalFiles` to inject any file into the compiler's virtual\nfilesystem before compilation. The files are available to both XeTeX and\n`xdvipdfmx`, so they work for fonts, images, `.sty` files, `.bib` databases,\nand any other resource the LaTeX source references.\n\nEach entry has a `path` (relative to the working directory) and `data`\n(a `Uint8Array` of raw bytes). Paths create parent directories automatically.\n\n### Injecting custom fonts\n\n```ts\n// Load a font from your server or from a user upload\nconst fontResponse = await fetch(\"/fonts/THSarabun.ttf\");\nconst fontBytes = new Uint8Array(await fontResponse.arrayBuffer());\n\nconst source = String.raw`\\documentclass{article}\n\\usepackage{fontspec}\n\\setmainfont{THSarabun.ttf}[Path=fonts/]\n\\begin{document}\nHello in Thai Sarabun!\n\\end{document}`;\n\nconst result = await compiler.compile(source, {\n  additionalFiles: [\n    { path: \"fonts/THSarabun.ttf\", data: fontBytes },\n  ],\n});\n```\n\n### Including images\n\n```ts\nconst logoResponse = await fetch(\"/assets/logo.png\");\nconst logoBytes = new Uint8Array(await logoResponse.arrayBuffer());\n\nconst source = String.raw`\\documentclass{article}\n\\usepackage{graphicx}\n\\begin{document}\n\\includegraphics[width=3cm]{images/logo.png}\n\\end{document}`;\n\nconst result = await compiler.compile(source, {\n  additionalFiles: [\n    { path: \"images/logo.png\", data: logoBytes },\n  ],\n});\n```\n\n### Mixing fonts, images, and styles\n\n```ts\nconst result = await compiler.compile(source, {\n  passes: 2,\n  additionalFiles: [\n    { path: \"fonts/MyFont.ttf\",       data: fontBytes },\n    { path: \"fonts/MyFont-Bold.ttf\",  data: fontBoldBytes },\n    { path: \"images/header.png\",      data: headerImageBytes },\n    { path: \"custom.sty\",             data: customStyleBytes },\n  ],\n});\n```\n\n### Type reference\n\n```ts\nimport type { XeLaTeXAdditionalFile } from \"@arnon3339/thtex\";\n\nconst files: XeLaTeXAdditionalFile[] = [\n  { path: \"fonts/MyFont.ttf\", data: fontBytes },\n];\n```\n\nThe compiler preserves the `Uint8Array` buffers supplied by your application,\nso the same font or image data can be reused for multiple compilations:\n\n```ts\nawait compiler.compile(firstSource, { additionalFiles: sharedFiles });\nawait compiler.compile(secondSource, { additionalFiles: sharedFiles });\n```\n\nPaths must be relative to `/work`; absolute paths and `..` traversal are\nrejected with a clear error before work is sent to the compiler.\n\n## React example\n\nCreate one compiler for the component lifetime and dispose it during cleanup:\n\n```tsx\nimport { useEffect, useRef, useState } from \"react\";\nimport { XeLaTeXCompiler } from \"@arnon3339/thtex\";\n\nexport function PdfCompiler({ source }: { source: string }) {\n  const compilerRef = useRef<XeLaTeXCompiler | null>(null);\n  const pdfUrlRef = useRef<string | null>(null);\n  const [pdfUrl, setPdfUrl] = useState<string>();\n\n  useEffect(() => {\n    const compiler = new XeLaTeXCompiler({\n      assetBaseUrl: \"/xelatex/\",\n    });\n\n    compilerRef.current = compiler;\n    return () => {\n      compiler.dispose();\n\n      if (pdfUrlRef.current) {\n        URL.revokeObjectURL(pdfUrlRef.current);\n        pdfUrlRef.current = null;\n      }\n    };\n  }, []);\n\n  async function compile() {\n    const result = await compilerRef.current!.compile(source);\n    const nextUrl = URL.createObjectURL(\n      new Blob([result.pdf], { type: \"application/pdf\" }),\n    );\n\n    if (pdfUrlRef.current) {\n      URL.revokeObjectURL(pdfUrlRef.current);\n    }\n\n    pdfUrlRef.current = nextUrl;\n    setPdfUrl(nextUrl);\n  }\n\n  return (\n    <>\n      <button onClick={() => void compile()}>Compile PDF</button>\n      {pdfUrl && <iframe title=\"Generated PDF\" src={pdfUrl} />}\n    </>\n  );\n}\n```\n\nThe application in this repository is a complete working React example and\nalso exposes a pass-count selector.\n\n### React example with custom fonts\n\nThis pattern pre-loads font files once and injects them on each compile:\n\n```tsx\nimport { useEffect, useRef, useState } from \"react\";\nimport { XeLaTeXCompiler } from \"@arnon3339/thtex\";\nimport type { XeLaTeXAdditionalFile } from \"@arnon3339/thtex\";\n\nexport function PdfCompiler({ source }: { source: string }) {\n  const compilerRef = useRef<XeLaTeXCompiler | null>(null);\n  const pdfUrlRef = useRef<string | null>(null);\n  const fontsRef = useRef<XeLaTeXAdditionalFile[]>([]);\n  const [pdfUrl, setPdfUrl] = useState<string>();\n\n  useEffect(() => {\n    const compiler = new XeLaTeXCompiler({ assetBaseUrl: \"/xelatex/\" });\n    compilerRef.current = compiler;\n\n    // Pre-load fonts once\n    Promise.all(\n      [\"THSarabun.ttf\", \"THSarabun-Bold.ttf\"].map(async (name) => {\n        const res = await fetch(`/fonts/${name}`);\n        const buf = await res.arrayBuffer();\n        return { path: `fonts/${name}`, data: new Uint8Array(buf) };\n      }),\n    ).then((fonts) => {\n      fontsRef.current = fonts;\n    });\n\n    return () => {\n      compiler.dispose();\n      if (pdfUrlRef.current) URL.revokeObjectURL(pdfUrlRef.current);\n    };\n  }, []);\n\n  async function compile() {\n    const result = await compilerRef.current!.compile(source, {\n      additionalFiles: fontsRef.current,\n    });\n\n    const nextUrl = URL.createObjectURL(\n      new Blob([result.pdf], { type: \"application/pdf\" }),\n    );\n\n    if (pdfUrlRef.current) URL.revokeObjectURL(pdfUrlRef.current);\n    pdfUrlRef.current = nextUrl;\n    setPdfUrl(nextUrl);\n  }\n\n  return (\n    <>\n      <button onClick={() => void compile()}>Compile PDF</button>\n      {pdfUrl && <iframe title=\"Generated PDF\" src={pdfUrl} />}\n    </>\n  );\n}\n```\n\n## Lifecycle and memory management\n\nCreating a compiler starts its Web Worker and begins loading the WASM engines\nand TeX runtime. For applications where PDF compilation is optional, create it\nlazily after the user first requests compilation:\n\n```ts\nlet compiler: XeLaTeXCompiler | undefined;\n\nasync function getCompiler() {\n  compiler ??= new XeLaTeXCompiler({\n    assetBaseUrl: \"/xelatex/\",\n  });\n\n  await compiler.ready;\n  return compiler;\n}\n\nasync function compilePdf(source: string) {\n  const activeCompiler = await getCompiler();\n  return activeCompiler.compile(source);\n}\n\nfunction closeCompiler() {\n  compiler?.dispose();\n  compiler = undefined;\n}\n```\n\nReuse one compiler while its editor or feature is active. This avoids loading\nthe large runtime again for every PDF, and calls on that instance are safely\nqueued. Temporary XeTeX modules, virtual filesystems, XDV data, and pass data\nbecome eligible for browser garbage collection after each job. The worker\nkeeps the shared runtime assets in memory to make the next job faster; browser\ngarbage collection timing is not controlled by the package.\n\nThe returned PDF buffer belongs to the application. If it is converted into a\nBlob URL, revoke the previous URL when replacing the preview and revoke the\nlast URL when the component or page is destroyed:\n\n```ts\nlet pdfUrl: string | undefined;\n\nfunction showPdf(pdf: ArrayBuffer, iframe: HTMLIFrameElement) {\n  if (pdfUrl) URL.revokeObjectURL(pdfUrl);\n\n  pdfUrl = URL.createObjectURL(\n    new Blob([pdf], { type: \"application/pdf\" }),\n  );\n  iframe.src = pdfUrl;\n}\n\nfunction closePdfPreview() {\n  if (pdfUrl) URL.revokeObjectURL(pdfUrl);\n  pdfUrl = undefined;\n}\n```\n\nCall `compiler.dispose()` when the owning React component unmounts, when the\nuser closes the PDF feature, or when the application no longer expects more\ncompilations. Disposal terminates the worker and releases its WASM/runtime\nmemory. A disposed compiler cannot be reused; create a new instance if the\nfeature is opened again. Do not dispose it while a wanted compilation is\nrunning because the active job will be rejected.\n\n## Multiple XeTeX passes\n\nThe default is one pass, which is fastest for simple documents. References,\ntables of contents, `longtable`, page labels, and complex layouts often write\nmeasurements or labels during the first pass and consume them during a second\npass, so request two passes for those documents.\n\n```ts\n// Use the compiler default (one pass).\nawait compiler.compile(source);\n\n// Resolve references and layout calculations with two passes.\nawait compiler.compile(source, { passes: 2 });\n\n// Or make two passes the default for this compiler instance.\nconst multiPassCompiler = new XeLaTeXCompiler({\n  assetBaseUrl: \"/xelatex/\",\n  defaultPasses: 2,\n});\n\n// Documents whose references stabilize later can use three passes.\nawait compiler.compile(source, { passes: 3 });\n```\n\nBetween passes, the worker creates a fresh Emscripten module and copies the\ngenerated working files into it. Fresh modules are required because the\nengines are linked with `EXIT_RUNTIME=1`. Only the final pass's XDV is sent to\n`xdvipdfmx`.\n\nValid pass counts are integers from 1 through 5. Two passes are a good general\ndefault. Additional passes increase CPU time and peak memory pressure.\n\nMultiple XeTeX passes do not implicitly enable external programs. BibTeX runs\nonly when selected with the separate `bibtex` option. Biber, MakeIndex, and\nMakeGlossaries are not included and require separate WASM engines and pipeline\nstages.\n\n## Compilation queue and isolation\n\nCalls made through one `XeLaTeXCompiler` instance are queued and run in order:\n\n```ts\nconst first = compiler.compile(firstSource);\nconst second = compiler.compile(secondSource);\nconst [firstPdf, secondPdf] = await Promise.all([first, second]);\n```\n\nThey do not overwrite each other. Separate tabs create separate workers and\nseparate in-memory filesystems. To compile truly in parallel in one tab,\ncreate multiple compiler instances; be aware that every active engine has a\nlarge WebAssembly memory allocation.\n\n## API\n\n### `new XeLaTeXCompiler(options?)`\n\n| Option | Default | Description |\n| --- | --- | --- |\n| `assetBaseUrl` | `/xelatex/` | Public directory created by the asset CLI. |\n| `workerUrl` | `<assetBaseUrl>/xelatex.worker.js` | Explicit worker override. Runtime assets must remain beside it. |\n| `defaultPasses` | `1` | Default XeTeX pass count, from 1 through 5. |\n| `onStatus` | — | Receives initialization/compilation status and download progress. |\n| `onLog` | — | Receives streamed stdout/stderr messages. |\n\n### `compiler.ready`\n\nA promise resolving after the engines, runtime manifest, and build-time\ngenerated `xelatex.fmt` have loaded:\n\n```ts\nconst { runtimeFileCount, runtimeBytes } = await compiler.ready;\n```\n\nDuring the initial download, `onStatus` receives `loadedBytes`, `totalBytes`,\n`loadedFiles`, and `totalFiles`. This makes it straightforward to display a\nreal progress bar instead of an indefinite loading message.\n\n### `compiler.compile(source, options?)`\n\nQueues a compilation and returns `Promise<XeLaTeXCompileResult>`.\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `passes` | `number` | Compiler default | XeTeX pass count (1–5). |\n| `bibtex` | `boolean \\| \"auto\"` | `false` | Whether to run classic BibTeX after the first XeTeX pass. |\n| `additionalFiles` | `XeLaTeXAdditionalFile[]` | `[]` | Files to mount in the VFS before compilation — fonts, images, styles, etc. |\n| `onLog` | `(event) => void` | — | Per-job log callback. |\n| `onStatus` | `(event) => void` | — | Per-job status callback. |\n\n### `compiler.dispose()`\n\nTerminates the worker. Call it when the owning page or component is destroyed.\n\n### Errors\n\nCompilation failures reject with `XeLaTeXCompileError`, which includes the\ncomplete compiler log:\n\n```ts\nimport { XeLaTeXCompileError } from \"@arnon3339/thtex\";\n\ntry {\n  await compiler.compile(source);\n} catch (error) {\n  if (error instanceof XeLaTeXCompileError) {\n    console.error(error.message, error.log);\n  }\n}\n```\n\n## Installed browser assets\n\nThe asset CLI creates this structure:\n\n```text\npublic/xelatex/\n├── xelatex.worker.js\n├── runtime-manifest.json\n├── engine/\n│   ├── xetex.mjs\n│   ├── xetex.wasm\n│   ├── bibtex.mjs\n│   ├── bibtex.wasm\n│   ├── xdvipdfmx.mjs\n│   └── xdvipdfmx.wasm\n├── texmf/\n├── fonts/\n├── fontconfig/\n└── icu/                 # present when ICU runtime files are required\n```\n\nThe focused runtime includes configuration and dependencies for LaTeX,\n`fontspec`, `expl3`, `graphicx`, `xcolor`, `geometry`, `amsmath`, `hyperref`,\nand `longtable`. The exact files are recorded in `runtime-manifest.json`.\n\nIf you add files under `public/xelatex/texmf`, `fonts`, `fontconfig`, or `icu`,\nregenerate the application's manifest:\n\n```bash\npnpm exec thtex-manifest --root public/xelatex\n```\n\n## Building this package\n\nThe XeTeX, BibTeX, and `xdvipdfmx` browser engines are sourced from the sibling\n`xelatex-artifacts` project. A build synchronizes the six browser artifacts,\ngenerates a format with that exact XeTeX WASM binary, and creates the runtime\nmanifest before compiling the TypeScript package.\n\nThe default artifact directory is `../xelatex-artifacts`. Override it when\nneeded with `XELATEX_ARTIFACTS_DIR` or run the synchronization command with\n`--from`:\n\n```bash\npnpm xelatex:sync-artifacts --from /path/to/xelatex-artifacts\n```\n\nThe focused TeX runtime synchronizes its 11/12 pt class support from the\ninstalled TeX Live tree discovered by `kpsewhich`. Set `XELATEX_TEXMF_DIST`\nto override that source directory.\n\n```bash\npnpm install\npnpm build          # TypeScript/worker build using the bundled runtime\npnpm build:runtime  # Maintainers: resync artifacts + TeX, then build\npnpm check          # Full runtime, lint, PDF, and package validation\n```\n\n`pnpm build` generates the typed ESM library and standalone module worker in\n`dist/`. Commit `dist/` before pushing a tag or commit intended for GitHub\ninstallation.\n\nRun the repository's React demo with:\n\n```bash\npnpm dev\n```\n\nA standalone Vite + React PWA is available in\n[`examples/react`](./examples/react). It includes a source editor, PDF preview,\noffline service worker, Cloudflare Pages headers, SPA redirects, and automatic\nCloudflare-safe runtime chunking.\n\n## Runtime size and hosting\n\nXeTeX, ICU, fonts, and the focused TeX tree are substantial browser assets.\nThe initial installation is roughly 75 MiB and should be served with long-lived\ncache headers or a service worker. The largest individual asset is ICU data at\nabout 31.9 MB.\n\nFor a host with a per-file limit, ask the asset CLI to split oversized runtime\nfiles. For example, Cloudflare Pages has a 25 MiB asset limit:\n\n```bash\nthtex --to public/xelatex --max-file-size 24MiB\n```\n\nThe destination manifest records the chunks, and the worker reassembles the\noriginal runtime file in memory. Engine WASM files are not splittable, so the\nCLI reports an error if the selected limit is lower than an engine asset. The\nruntime is downloaded once per browser cache and reused by one compiler\ninstance for subsequent jobs.\n\n## License\n\nThe TypeScript/JavaScript package code is MIT licensed. Bundled XeTeX,\n`xdvipdfmx`, TeX packages, and fonts retain their respective upstream\nlicenses; see [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md).\n","readmeFilename":"README.md"}