Compare commits

...

53 Commits
8.5.0 ... 9.0.1

Author SHA1 Message Date
6eb11d8842 Revert github action version change 2024-10-13 02:06:22 +01:00
3ad68337e7 Version bump 2024-10-13 01:58:44 +01:00
07f363fcb7 feat: Add npm publish GitHub Action workflow (#2762)
* feat: Add npm publish GitHub Action workflow

* Fix typo
2024-10-13 01:48:04 +01:00
2d2e4cdab2 fix: Missing text property for patchDocument (#2760)
* fix: Missing text property

Ignore <br /> for patched files

* Fix spelling issues
2024-10-13 01:29:32 +01:00
0cadec7f58 build(deps-dev): bump @typescript-eslint/parser from 6.17.0 to 8.8.1 (#2742)
* build(deps-dev): bump @typescript-eslint/parser from 6.17.0 to 8.8.1

Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.17.0 to 8.8.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.8.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix type definitions

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dolan Miu <dolan_miu@hotmail.com>
2024-10-11 04:47:51 +01:00
e86dbd3398 Change ImageRun keys to be based on image data content (#2681)
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-10-11 01:13:51 +01:00
021f1b0c4d Update bullet-points.md (#2684)
Excluding ; from example

Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-10-11 01:13:26 +01:00
5aa2027252 build(deps): bump send and serve-static (#2736)
Bumps [send](https://github.com/pillarjs/send) and [serve-static](https://github.com/expressjs/serve-static). These dependencies needed to be updated together.

Updates `send` from 0.17.2 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.17.2...0.19.0)

Updates `serve-static` from 1.14.2 to 1.16.2
- [Release notes](https://github.com/expressjs/serve-static/releases)
- [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md)
- [Commits](https://github.com/expressjs/serve-static/compare/v1.14.2...v1.16.2)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: serve-static
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-10-11 01:13:14 +01:00
2fc297ef3c build(deps-dev): bump braces from 3.0.2 to 3.0.3 (#2749)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-10-11 01:12:50 +01:00
3fe216846c build(deps-dev): bump dompurify from 2.3.6 to 2.5.7 (#2737)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.3.6 to 2.5.7.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.3.6...2.5.7)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-10-11 01:05:41 +01:00
649a50f7c5 build(deps-dev): bump serve-static from 1.14.2 to 1.16.2 (#2739)
Bumps [serve-static](https://github.com/expressjs/serve-static) from 1.14.2 to 1.16.2.
- [Release notes](https://github.com/expressjs/serve-static/releases)
- [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md)
- [Commits](https://github.com/expressjs/serve-static/compare/v1.14.2...v1.16.2)

---
updated-dependencies:
- dependency-name: serve-static
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-10-11 01:05:35 +01:00
dc14123a5a build(deps-dev): bump rollup from 3.29.4 to 4.24.0 (#2740)
Bumps [rollup](https://github.com/rollup/rollup) from 3.29.4 to 4.24.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v3.29.4...v4.24.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-10-11 01:05:25 +01:00
048a035a5d build(deps-dev): bump elliptic from 6.5.4 to 6.5.7 (#2738)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.4 to 6.5.7.
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.4...v6.5.7)

---
updated-dependencies:
- dependency-name: elliptic
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-10-11 00:58:47 +01:00
f212cf0251 Update styling-with-js.md (#2734)
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-10-11 00:57:11 +01:00
1f2f5988e1 build(deps-dev): bump vite from 5.0.10 to 5.4.8 (#2741)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.10 to 5.4.8.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.8/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.8/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-10-11 00:56:49 +01:00
f1359a4750 build(deps-dev): bump @types/prompt from 1.1.8 to 1.1.9 (#2746)
Bumps [@types/prompt](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/prompt) from 1.1.8 to 1.1.9.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/prompt)

---
updated-dependencies:
- dependency-name: "@types/prompt"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-10-11 00:33:21 +01:00
f2775ed13c build(deps): bump @types/node from 20.12.4 to 22.7.5 (#2747)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.12.4 to 22.7.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-10-11 00:33:12 +01:00
3ae749278e build(deps-dev): bump eslint-plugin-jsdoc from 48.0.2 to 50.3.1 (#2745)
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 48.0.2 to 50.3.1.
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v48.0.2...v50.3.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-10-11 00:30:35 +01:00
0bcc6910f4 Use codecov token (#2748)
* Use codecov token

* Using env to pass token
2024-10-11 00:22:43 +01:00
c15951550c Version bump 2024-10-10 01:30:32 +01:00
8308b6413e Add documentation 2024-10-10 00:38:25 +01:00
5b75875684 Enable rollupTypes in the vite build configuration. (#2714)
This resolves issues with in dependent TypeScipt packages that are using moduleResolution: NodeNext
Modern ES modules cannot use extensionless relative paths in imports.
2024-10-10 00:15:04 +01:00
28029f4c1c chore[tables.md]: replace semicolon to commas (#2708) 2024-10-10 00:14:09 +01:00
824d7f9893 build(deps-dev): bump vite from 5.0.10 to 5.2.8 (#2668)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.10 to 5.2.8.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.2.8/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-10 00:13:52 +01:00
b3aea4b9a0 build(deps-dev): bump @vitest/coverage-v8 from 1.1.1 to 1.4.0 (#2645)
Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 1.1.1 to 1.4.0.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.4.0/packages/coverage-v8)

---
updated-dependencies:
- dependency-name: "@vitest/coverage-v8"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-10 00:10:13 +01:00
32377d187d build(deps-dev): bump @vitest/ui from 1.1.1 to 1.4.0 (#2644)
Bumps [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui) from 1.1.1 to 1.4.0.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.4.0/packages/ui)

---
updated-dependencies:
- dependency-name: "@vitest/ui"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 21:26:07 +01:00
a80815822d build(deps-dev): bump vitest from 1.1.1 to 1.4.0 (#2642)
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 1.1.1 to 1.4.0.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.4.0/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 21:26:01 +01:00
c198154fdc build(deps-dev): bump typedoc from 0.25.6 to 0.25.12 (#2634)
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.25.6 to 0.25.12.
- [Release notes](https://github.com/TypeStrong/TypeDoc/releases)
- [Changelog](https://github.com/TypeStrong/typedoc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.25.6...v0.25.12)

---
updated-dependencies:
- dependency-name: typedoc
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 21:25:52 +01:00
618c7a8578 build(deps-dev): bump @typescript-eslint/eslint-plugin (#2595)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.17.0 to 7.0.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.0.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 21:25:44 +01:00
ef7b930d4d build(deps-dev): bump prettier from 3.1.1 to 3.2.5 (#2579)
Bumps [prettier](https://github.com/prettier/prettier) from 3.1.1 to 3.2.5.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.1.1...3.2.5)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 21:25:29 +01:00
8296895cc6 build(deps-dev): bump jsdom from 23.0.1 to 24.0.0 (#2557)
Bumps [jsdom](https://github.com/jsdom/jsdom) from 23.0.1 to 24.0.0.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Changelog](https://github.com/jsdom/jsdom/blob/main/Changelog.md)
- [Commits](https://github.com/jsdom/jsdom/compare/23.0.1...24.0.0)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 21:25:12 +01:00
1dce6fee15 build(deps): bump @types/node from 20.10.6 to 20.12.4 (#2669)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.10.6 to 20.12.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 21:25:03 +01:00
444e7771b4 Version bump 2024-10-09 21:21:08 +01:00
962795743c feat: Add ability to detect patches which are present in a file (#2633)
* feat: Add ability to detect patches which are present in a file

* chore: export patchDetector function

* fix: Make sure we don't attempt to call toJson on binary content

---------

Co-authored-by: Christopher Fox <cfox@homebound.com>
2024-05-22 03:05:48 +01:00
f98f852a55 Allow disabling numbering inherited from a paragraph style (#2531) 2024-05-20 03:15:11 +01:00
e379a7fe04 Document the 'keepOriginalStyles' options (#2549)
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-05-20 03:13:05 +01:00
022b25cfcd Export FileChild class (#2523) 2024-01-25 01:53:26 +00:00
e20bd66663 Limit the list of supported highlight colors (#2522)
* Limit the list of supported highlight colors

* Fix tests: use fixed highlight color values

---------

Co-authored-by: ilyasogonov <sogonov.ilya@customscard.ru>
2024-01-12 00:16:15 +00:00
6b8e22368b build(deps-dev): bump eslint-plugin-jsdoc from 47.0.2 to 48.0.2 (#2515)
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 47.0.2 to 48.0.2.
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v47.0.2...v48.0.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-04 02:42:26 +00:00
4304e82751 build(deps-dev): bump cspell from 8.3.1 to 8.3.2 (#2514)
Bumps [cspell](https://github.com/streetsidesoftware/cspell) from 8.3.1 to 8.3.2.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell/compare/v8.3.1...v8.3.2)

---
updated-dependencies:
- dependency-name: cspell
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-04 02:42:19 +00:00
75715fde37 Fix the incorrect limit location value of MathIntergral #2512 (#2513) 2024-01-04 02:42:05 +00:00
e779f6ea62 build(deps-dev): bump cspell from 8.2.4 to 8.3.1 (#2509)
Bumps [cspell](https://github.com/streetsidesoftware/cspell) from 8.2.4 to 8.3.1.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell/compare/v8.2.4...v8.3.1)

---
updated-dependencies:
- dependency-name: cspell
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-03 00:15:33 +00:00
9280cdba50 build(deps-dev): bump @typescript-eslint/eslint-plugin (#2508)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.16.0 to 6.17.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.17.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-03 00:15:27 +00:00
8410d0c06d build(deps-dev): bump @typescript-eslint/parser from 6.16.0 to 6.17.0 (#2507)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.16.0 to 6.17.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.17.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-03 00:15:22 +00:00
d47d85bdcf build(deps-dev): bump typedoc from 0.25.4 to 0.25.6 (#2506)
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.25.4 to 0.25.6.
- [Release notes](https://github.com/TypeStrong/TypeDoc/releases)
- [Changelog](https://github.com/TypeStrong/typedoc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/TypeDoc/compare/v0.25.4...v0.25.6)

---
updated-dependencies:
- dependency-name: typedoc
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-03 00:15:16 +00:00
6ae45327fe build(deps-dev): bump @vitest/coverage-v8 from 1.1.0 to 1.1.1 (#2503)
Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.1.1/packages/coverage-v8)

---
updated-dependencies:
- dependency-name: "@vitest/coverage-v8"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-03 00:15:10 +00:00
464cd946dc build(deps-dev): bump @vitest/ui from 1.1.0 to 1.1.1 (#2500)
Bumps [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.1.1/packages/ui)

---
updated-dependencies:
- dependency-name: "@vitest/ui"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-03 00:15:03 +00:00
cbff540b6e build(deps-dev): bump eslint-plugin-jsdoc from 46.9.1 to 47.0.2 (#2504)
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 46.9.1 to 47.0.2.
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v46.9.1...v47.0.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-03 00:14:53 +00:00
a8e6ba4c49 build(deps): bump @types/node from 20.10.5 to 20.10.6 (#2502)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.10.5 to 20.10.6.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-03 00:14:45 +00:00
efc1ceaf1a build(deps-dev): bump vitest from 1.1.0 to 1.1.1 (#2498)
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.1.1/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-03 00:14:38 +00:00
c4ed19e589 Add math limit component (#2510) 2024-01-03 00:13:13 +00:00
13cf3eee5a Feature/multiple patch document exports (#2497)
* Turn patch document into options object

Add outputType to options

* Set keep styles to true by default

* Simplify method

* Rename variable

* #2267 Multiple patches of same key

* Remove path which won't be visited
2023-12-31 23:16:48 +00:00
24c159de37 Add SVG image suport (#2487)
* Add SVG blip extentions

* SVG Image feature now works

* Add and simplify tests

* Fix falsey issue with file

Write tests
100% Coverage
2023-12-31 18:54:35 +00:00
82 changed files with 4976 additions and 1425 deletions

View File

@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@master
uses: actions/checkout@v4
- uses: "./.github/actions/install-and-build"
- name: Archive Production Artifact
uses: actions/upload-artifact@master
uses: actions/upload-artifact@v4
with:
name: build
path: build
@ -25,22 +25,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@master
uses: actions/checkout@v4
- name: Install Dependencies
run: npm ci --force
- name: Test
run: npm run test:ci
- name: Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: true
verbose: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@master
uses: actions/checkout@v4
- name: Install Dependencies
run: npm ci --force
- name: Lint
@ -50,7 +52,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@master
uses: actions/checkout@v4
- name: Install Dependencies
run: npm ci --force
- name: Prettier
@ -60,7 +62,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@master
uses: actions/checkout@v4
- name: Install Dependencies
run: npm ci --force
- name: Prettier

View File

@ -12,7 +12,7 @@ jobs:
name: Demos
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v4
- uses: "./.github/actions/install-and-build"
- name: Run Demos
run: npm run run-ts -- ./demo/1-basic.ts

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@master
uses: actions/checkout@v4
- name: Install Dependencies
run: npm ci --force
- name: Build 🔧
@ -19,7 +19,7 @@ jobs:
echo "docx.js.org" > docs/.nojekyll
echo "docx.js.org" > docs/CNAME
- name: Archive Production Artifact
uses: actions/upload-artifact@master
uses: actions/upload-artifact@v4
with:
name: docs
path: docs
@ -28,11 +28,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo 🛎️
uses: actions/checkout@master
uses: actions/checkout@v4
- name: Install Dependencies
run: npm ci --force
- name: Download Artifact
uses: actions/download-artifact@master
uses: actions/download-artifact@v4
with:
name: docs
path: docs

46
.github/workflows/npm-publish.yml vendored Normal file
View File

@ -0,0 +1,46 @@
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
name: Node.js Package
on:
release:
types: [created]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20.x"
- run: npm ci
- run: npm run cspell
- run: npm run prettier
- run: npm run lint
- run: npm run test:ci
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20.x"
- run: npm ci
- run: npm run build
publish-npm:
needs: [test, build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20.x"
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}

View File

@ -21,6 +21,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
type: "jpg",
data: fs.readFileSync("./demo/images/image1.jpeg"),
transformation: {
width: 100,
@ -37,6 +38,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
type: "png",
data: fs.readFileSync("./demo/images/dog.png").toString("base64"),
transformation: {
width: 100,
@ -53,6 +55,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
type: "jpg",
data: fs.readFileSync("./demo/images/cat.jpg"),
transformation: {
width: 100,
@ -73,6 +76,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
type: "bmp",
data: fs.readFileSync("./demo/images/parrots.bmp"),
transformation: {
width: 150,
@ -88,6 +92,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
type: "gif",
data: fs.readFileSync("./demo/images/pizza.gif"),
transformation: {
width: 200,
@ -103,6 +108,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
type: "gif",
data: fs.readFileSync("./demo/images/pizza.gif"),
transformation: {
width: 200,
@ -124,6 +130,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
type: "jpg",
data: fs.readFileSync("./demo/images/cat.jpg"),
transformation: {
width: 200,
@ -143,6 +150,22 @@ const doc = new Document({
}),
],
}),
new Paragraph({
children: [
new ImageRun({
type: "svg",
data: fs.readFileSync("./demo/images/linux-svg.svg"),
transformation: {
width: 200,
height: 200,
},
fallback: {
type: "png",
data: fs.readFileSync("./demo/images/linux-png.png"),
},
}),
],
}),
],
},
],

View File

@ -21,6 +21,8 @@ import {
Packer,
Paragraph,
TextRun,
MathLimitLower,
MathLimitUpper,
} from "docx";
const doc = new Document({
@ -316,6 +318,23 @@ const doc = new Document({
}),
],
}),
new Paragraph({
children: [
new Math({
children: [
new MathLimitUpper({
children: [new MathRun("x")],
limit: [new MathRun("-")],
}),
new MathRun("="),
new MathLimitLower({
children: [new MathRun("lim")],
limit: [new MathRun("x→0")],
}),
],
}),
],
}),
],
},
],

View File

@ -16,7 +16,9 @@ import {
VerticalAlign,
} from "docx";
patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
patchDocument({
outputType: "nodebuffer",
data: fs.readFileSync("demo/assets/simple-template.docx"),
patches: {
name: {
type: PatchType.PARAGRAPH,
@ -56,7 +58,11 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
],
link: "https://www.google.co.uk",
}),
new ImageRun({ data: fs.readFileSync("./demo/images/dog.png"), transformation: { width: 100, height: 100 } }),
new ImageRun({
type: "png",
data: fs.readFileSync("./demo/images/dog.png"),
transformation: { width: 100, height: 100 },
}),
],
}),
],
@ -82,7 +88,13 @@ patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
},
image_test: {
type: PatchType.PARAGRAPH,
children: [new ImageRun({ data: fs.readFileSync("./demo/images/image1.jpeg"), transformation: { width: 100, height: 100 } })],
children: [
new ImageRun({
type: "jpg",
data: fs.readFileSync("./demo/images/image1.jpeg"),
transformation: { width: 100, height: 100 },
}),
],
},
table: {
type: PatchType.DOCUMENT,

View File

@ -3,7 +3,9 @@
import * as fs from "fs";
import { patchDocument, PatchType, TextRun } from "docx";
patchDocument(fs.readFileSync("demo/assets/simple-template-2.docx"), {
patchDocument({
outputType: "nodebuffer",
data: fs.readFileSync("demo/assets/simple-template-2.docx"),
patches: {
name: {
type: PatchType.PARAGRAPH,

View File

@ -24,7 +24,9 @@ const patches = getPatches({
paragraph_replace: "Lorem ipsum paragraph",
});
patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
patchDocument({
outputType: "nodebuffer",
data: fs.readFileSync("demo/assets/simple-template.docx"),
patches,
}).then((doc) => {
fs.writeFileSync("My Document.docx", doc);

View File

@ -22,8 +22,11 @@ const patches = getPatches({
"first-name": "John",
});
patchDocument(fs.readFileSync("demo/assets/simple-template-3.docx"), {
patchDocument({
outputType: "nodebuffer",
data: fs.readFileSync("demo/assets/simple-template-3.docx"),
patches,
keepOriginalStyles: true,
}).then((doc) => {
fs.writeFileSync("My Document.docx", doc);
});

View File

@ -0,0 +1,72 @@
// Patch a document with patches
import * as fs from "fs";
import { patchDocument, PatchType, TextRun } from "docx";
patchDocument({
outputType: "nodebuffer",
data: fs.readFileSync("demo/assets/field-trip.docx"),
patches: {
todays_date: {
type: PatchType.PARAGRAPH,
children: [new TextRun({ text: new Date().toLocaleDateString() })],
},
school_name: {
type: PatchType.PARAGRAPH,
children: [new TextRun({ text: "test" })],
},
address: {
type: PatchType.PARAGRAPH,
children: [new TextRun({ text: "blah blah" })],
},
city: {
type: PatchType.PARAGRAPH,
children: [new TextRun({ text: "test" })],
},
state: {
type: PatchType.PARAGRAPH,
children: [new TextRun({ text: "test" })],
},
zip: {
type: PatchType.PARAGRAPH,
children: [new TextRun({ text: "test" })],
},
phone: {
type: PatchType.PARAGRAPH,
children: [new TextRun({ text: "test" })],
},
first_name: {
type: PatchType.PARAGRAPH,
children: [new TextRun({ text: "test" })],
},
last_name: {
type: PatchType.PARAGRAPH,
children: [new TextRun({ text: "test" })],
},
email_address: {
type: PatchType.PARAGRAPH,
children: [new TextRun({ text: "test" })],
},
ft_dates: {
type: PatchType.PARAGRAPH,
children: [new TextRun({ text: "test" })],
},
grade: {
type: PatchType.PARAGRAPH,
children: [new TextRun({ text: "test" })],
},
},
}).then((doc) => {
fs.writeFileSync("My Document.docx", doc);
});

BIN
demo/assets/field-trip.docx Normal file

Binary file not shown.

Binary file not shown.

BIN
demo/images/linux-png.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

183
demo/images/linux-svg.svg Normal file
View File

@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="500pt" height="600pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://web.resource.org/cc/">
<defs>
<linearGradient id="linearGradient172">
<stop style="stop-color:#3f2600;stop-opacity:0.6;" offset="0" id="stop173" />
<stop style="stop-color:#3f2600;stop-opacity:0;" offset="1" id="stop174" />
</linearGradient>
<linearGradient id="linearGradient167">
<stop style="stop-color:#ffffff;stop-opacity:0.65;" offset="0" id="stop168" />
<stop style="stop-color:#ffffff;stop-opacity:0;" offset="1" id="stop169" />
</linearGradient>
<linearGradient id="linearGradient162">
<stop style="stop-color:#ffa63f;stop-opacity:1;" offset="0" id="stop163" />
<stop style="stop-color:#ffff00;stop-opacity:1;" offset="1" id="stop164" />
</linearGradient>
<linearGradient id="linearGradient153">
<stop style="stop-color:#ffeed7;stop-opacity:1;" offset="0" id="stop154" />
<stop style="stop-color:#bdbfc2;stop-opacity:1;" offset="1" id="stop155" /></linearGradient>
<linearGradient id="linearGradient138">
<stop style="stop-color:#ffffff;stop-opacity:0.8;" offset="0" id="stop139" />
<stop style="stop-color:#ffffff;stop-opacity:0;" offset="1" id="stop140" />
</linearGradient>
<linearGradient xlink:href="#linearGradient138" id="linearGradient141" x1="0.47424799" y1="0.020191999" x2="0.417539" y2="0.90125799" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient142" x1="0.55880702" y1="0.031192999" x2="0.553922" y2="0.94531101" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient143" x1="0.46557701" y1="0.028819799" x2="0.41365999" y2="0.93366498" gradientUnits="objectBoundingBox"/>
<linearGradient xlink:href="#linearGradient167" id="linearGradient144" x1="0.70346397" y1="0.059404202" x2="0.64553201" y2="0.94063401" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient145" x1="0.46741399" y1="-0.036155298" x2="0.86741799" y2="0.75857902" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient146" x1="0.57152498" y1="0.023441499" x2="0.57143003" y2="0.71875" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient147" x1="0.5" y1="0.0234362" x2="0.5" y2="0.8125" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient148" x1="0.50799799" y1="0.37435901" x2="0.51599997" y2="0.92820501" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient138" id="linearGradient149" x1="0.5" y1="0.131707" x2="0.50400001" y2="0.94634098" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient150" x1="-0.30509499" y1="0.099496603" x2="0.156323" y2="0.94191301" gradientUnits="objectBoundingBox" gradientTransform="matrix(-0.928523,0.283938,0.435332,0.943857,-1.91327e-7,5.49908e-8)" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient151" x1="0.433979" y1="0.022184599" x2="0.487055" y2="1.02569" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient153" id="linearGradient152" x1="0.5" y1="0.89842999" x2="0.5" y2="0.40625" gradientUnits="objectBoundingBox" spreadMethod="reflect" />
<linearGradient xlink:href="#linearGradient153" id="linearGradient156" x1="0.43568701" y1="0.98882002" x2="0.453989" y2="0.23093501" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient153" id="linearGradient157" x1="0.49180499" y1="1.15284" x2="0.49482101" y2="0.41252401" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient153" id="linearGradient158" x1="0.51730198" y1="0.85418499" x2="0.49843901" y2="0.136172" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient153" id="linearGradient159" x1="0.46201" y1="0.87917101" x2="0.49215299" y2="0.096282303" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient162" id="linearGradient161" x1="0.50086302" y1="0.34872901" x2="0.41209599" y2="0.98558098" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient162" id="linearGradient165" x1="0.60399801" y1="0.51020199" x2="0.46399999" y2="0.98367399" gradientUnits="objectBoundingBox" />
<linearGradient xlink:href="#linearGradient162" id="linearGradient166" x1="0.50000501" y1="0.191616" x2="0.50800002" y2="0.97005898" gradientUnits="objectBoundingBox" />
<radialGradient xlink:href="#linearGradient172" id="radialGradient171" cx="0.5" cy="0.5" fx="0.5" fy="0.5" r="0.5" gradientUnits="objectBoundingBox" />
<radialGradient xlink:href="#linearGradient172" id="radialGradient176" />
<linearGradient xlink:href="#linearGradient153" id="linearGradient178" x1="0.94027299" y1="1.2934099" x2="0.19452" y2="-0.675295" gradientUnits="objectBoundingBox" />
<radialGradient xlink:href="#linearGradient172" id="radialGradient1399" gradientTransform="scale(1.045233,0.956725)" cx="446.77762" cy="1219.4125" fx="446.77762" fy="1219.4125" r="195.07191" gradientUnits="userSpaceOnUse" />
<linearGradient xlink:href="#linearGradient153" id="linearGradient1401" gradientUnits="userSpaceOnUse" x1="400.57785" y1="369.53015" x2="400.84448" y2="304.07886" gradientTransform="scale(0.575262,1.738339)" />
<linearGradient xlink:href="#linearGradient138" id="linearGradient1403" gradientUnits="userSpaceOnUse" x1="303.01761" y1="237.93179" x2="297.0856" y2="330.09561" gradientTransform="scale(1.116071,0.896001)" />
<linearGradient xlink:href="#linearGradient153" id="linearGradient1405" gradientUnits="userSpaceOnUse" gradientTransform="scale(0.816497,1.224744)" x1="378.93771" y1="278.60202" x2="380.27319" y2="243.91606" />
<linearGradient xlink:href="#linearGradient153" id="linearGradient1407" gradientUnits="userSpaceOnUse" x1="381.38742" y1="277.495" x2="380.5517" y2="245.68338" gradientTransform="scale(0.816497,1.224744)" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient1409" gradientUnits="userSpaceOnUse" gradientTransform="scale(0.816497,1.224744)" x1="379.09573" y1="240.92712" x2="376.79556" y2="281.01636" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient1411" gradientUnits="userSpaceOnUse" x1="389.63535" y1="242.28218" x2="387.06866" y2="281.32513" gradientTransform="scale(0.816497,1.224744)" />
<linearGradient xlink:href="#linearGradient153" id="linearGradient1413" gradientUnits="userSpaceOnUse" spreadMethod="reflect" x1="437.57941" y1="528.87177" x2="437.57941" y2="394.10361" gradientTransform="scale(0.812855,1.230232)" />
<linearGradient xlink:href="#linearGradient153" id="linearGradient1415" gradientUnits="userSpaceOnUse" x1="375.17325" y1="419.78485" x2="377.48541" y2="324.03815" gradientTransform="scale(0.649784,1.538974)" />
<linearGradient xlink:href="#linearGradient138" id="linearGradient1417" gradientUnits="userSpaceOnUse" x1="320.75104" y1="498.17776" x2="321.32224" y2="614.50439" gradientTransform="scale(1.074798,0.930408)" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient1419" gradientUnits="userSpaceOnUse" x1="322.48257" y1="435.26761" x2="323.2514" y2="488.48251" gradientTransform="scale(1.077001,0.928504)" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient1421" gradientUnits="userSpaceOnUse" x1="411.2215" y1="242.94365" x2="411.2215" y2="331.44858" gradientTransform="scale(0.571707,1.749147)" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient1423" gradientUnits="userSpaceOnUse" x1="867.34546" y1="234.73897" x2="867.33453" y2="314.83911" gradientTransform="scale(0.572667,1.746214)" />
<linearGradient xlink:href="#linearGradient162" id="linearGradient1425" gradientUnits="userSpaceOnUse" x1="236.25362" y1="657.11133" x2="212.5099" y2="737.41229" gradientTransform="scale(1.011514,0.988617)" />
<linearGradient xlink:href="#linearGradient153" id="linearGradient1427" gradientUnits="userSpaceOnUse" x1="381.56607" y1="655.73102" x2="279.64313" y2="386.66583" gradientTransform="scale(1.065499,0.938527)" />
<linearGradient xlink:href="#linearGradient162" id="linearGradient1429" gradientUnits="userSpaceOnUse" x1="218.11714" y1="630.30475" x2="203.12654" y2="737.8537" gradientTransform="scale(1.009851,0.990245)" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient1431" gradientUnits="userSpaceOnUse" gradientTransform="scale(1.007724,0.992335)" x1="117.88966" y1="587.23602" x2="182.24524" y2="704.73077" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient1433" gradientUnits="userSpaceOnUse" x1="223.10072" y1="570.41809" x2="230.53499" y2="710.97723" gradientTransform="scale(0.999504,1.000496)" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient1435" gradientUnits="userSpaceOnUse" x1="316.93988" y1="474.01779" x2="371.60889" y2="582.63507" gradientTransform="scale(1.065499,0.938527)" />
<linearGradient xlink:href="#linearGradient162" id="linearGradient1437" gradientUnits="userSpaceOnUse" x1="284.68652" y1="410.46326" x2="285.45923" y2="485.69934" gradientTransform="scale(1.218684,0.820557)" />
<linearGradient xlink:href="#linearGradient167" id="linearGradient1439" gradientUnits="userSpaceOnUse" x1="288.82358" y1="398.85422" x2="288.37628" y2="482.55939" gradientTransform="scale(1.221941,0.81837)" />
</defs>
<g id="g1369" transform="translate(-310.7524,-64.25268)">
<path transform="matrix(1.4177,0,0,0.414745,-38.7944,222.194)" d="M 670.88202 1166.6423 A 203.89551 186.63016 0 1 1 263.091,1166.6423 A 203.89551 186.63016 0 1 1 670.88202 1166.6423 z" id="path175" style="fill:url(#radialGradient1399);stroke:none;stroke-width:1pt;stroke-linecap:butt;stroke-linejoin:miter" />
<path transform="matrix(1.25,0,0,1.25,185.454,-167.505)" id="path106" d="M 223.627,632.24 C 201.239,600.017 196.873,495.256 249.114,430.81 C 275,399.892 281.604,378.345 283.645,349.417 C 285.034,316.438 260.32,217.975 353.528,210.473 C 447.934,202.941 442.864,296.133 442.321,345.448 C 441.87,387.088 472.895,410.689 494.117,443.143 C 533.396,502.773 530.074,605.443 486.718,661.015 C 431.801,730.583 384.765,700.413 353.528,702.945 C 295.035,706.147 293.101,737.336 223.627,632.24 z " style="fill:#000000;stroke:none;stroke-width:1.25;" />
<path transform="matrix(-1.67739,-2.24516e-2,-2.11236e-2,1.4709,1173.58,-293.017)" id="path113" d="M 246.571,470.864 C 234.332,483.36 202.175,539.956 251.44,576.224 C 268.809,588.857 235.063,635.719 219.435,612.532 C 191.865,570.914 210.604,505.591 227.75,482.344 C 239.402,465.857 256.98,459.668 246.571,470.864 z " style="fill:url(#linearGradient1401);stroke:none;stroke-width:0.99464899;" />
<path transform="matrix(-1.67755,0,0,1.52374,1174.62,-318.082)" id="path111" d="M 256.513,459.837 C 236.598,477.554 200.337,539.928 253.225,580.443 C 270.595,593.075 237.832,632.906 219.435,612.532 C 155.472,541.712 221.104,460.278 243.697,432.282 C 263.889,407.935 281.775,438.034 256.513,459.837 z " style="fill:#000000;stroke:#000000;stroke-width:0.97729802;" />
<path transform="matrix(1.26626,-7.13667e-2,-4.59795e-2,1.19574,202.143,-125.761)" d="M 399.56879 258.15753 A 58.37323 46.863022 0 1 1 282.82233,258.15753 A 58.37323 46.863022 0 1 1 399.56879 258.15753 z" id="path114" style="fill:url(#linearGradient1403);stroke:none;stroke-width:1.26498997;" />
<path transform="matrix(1.30445,-7.55326e-2,7.71251e-2,1.34257,144.757,-177.617)" d="M 328.86324 320.64151 A 18.087479 27.131195 0 1 1 292.68828,320.64151 A 18.087479 27.131195 0 1 1 328.86324 320.64151 z" id="path115" style="fill:url(#linearGradient1405);stroke:none;stroke-width:1.17873001;" />
<path transform="matrix(-1.81082,4.95107e-2,3.17324e-2,1.55333,1207.46,-284.777)" d="M 328.86324 320.64151 A 18.087479 27.131195 0 1 1 292.68828,320.64151 A 18.087479 27.131195 0 1 1 328.86324 320.64151 z" id="path116" style="fill:url(#linearGradient1407);stroke:none;stroke-width:0.93138498;" />
<path transform="matrix(-0.823196,-1.76123e-3,-1.82321e-2,0.852662,913.674,-37.9902)" d="M 328.86324 320.64151 A 18.087479 27.131195 0 1 1 292.68828,320.64151 A 18.087479 27.131195 0 1 1 328.86324 320.64151 z" id="path117" style="fill:#000000;stroke:none;stroke-width:1.86495996;" />
<path transform="matrix(0.59438,-7.22959e-2,6.88176e-2,0.705838,367.448,32.4186)" d="M 328.86324 320.64151 A 18.087479 27.131195 0 1 1 292.68828,320.64151 A 18.087479 27.131195 0 1 1 328.86324 320.64151 z" id="path118" style="fill:#000000;stroke:none;stroke-width:2.39814997;" />
<path transform="matrix(-0.480323,-3.6454e-2,-4.67935e-2,0.475606,813.496,87.0124)" d="M 328.86324 320.64151 A 18.087479 27.131195 0 1 1 292.68828,320.64151 A 18.087479 27.131195 0 1 1 328.86324 320.64151 z" id="path121" style="fill:url(#linearGradient1409);stroke:none;stroke-width:3.1916101;" />
<path transform="matrix(0.35691,-4.08211e-2,4.13232e-2,0.398544,449.334,114.991)" d="M 328.86324 320.64151 A 18.087479 27.131195 0 1 1 292.68828,320.64151 A 18.087479 27.131195 0 1 1 328.86324 320.64151 z" id="path122" style="fill:url(#linearGradient1411);stroke:none;stroke-width:4.12025976;" />
<path transform="matrix(1.25,0,0,1.25,185.454,-168.23)" id="path128" d="M 258.702,495.425 C 271.538,466.322 298.816,415.199 299.397,375.667 C 299.397,344.225 393.576,336.716 401.134,368.109 C 408.692,399.502 427.875,446.592 440.084,469.265 C 452.292,491.937 487.893,563.96 449.968,626.811 C 415.811,682.455 312.243,726.477 256.958,619.254 C 238.355,582.047 241.673,535.939 258.702,495.425 z " style="fill:url(#linearGradient1413);stroke:none;stroke-width:1.25;" />
<path transform="matrix(1.38936,-0.111074,0.102211,1.30214,108.413,-165.938)" id="path112" d="M 242.905,473.815 C 231.642,492.782 207.405,543.124 255.042,575.862 C 306.353,610.682 301.515,672.924 239.435,637.817 C 182.658,606.028 216.59,500.039 234.925,475.551 C 247.032,458.337 264.822,437.52 242.905,473.815 z " style="fill:url(#linearGradient1415);stroke:none;stroke-width:1.15804005;" />
<path transform="matrix(1.25,0,0,1.25,185.454,-167.505)" id="path109" d="M 256.513,449.72 C 239.048,478.228 197.136,545.533 253.225,580.443 C 328.794,626.798 307.398,673.154 238.426,631.417 C 141.317,573.153 226.601,455.801 265.557,411.079 C 310.001,360.879 274.111,420.166 256.513,449.72 z " style="fill:#000000;stroke:#000000;stroke-width:1.25;" />
<path id="path125" d="M 421.481,504.727 C 421.481,537.139 392.209,579.243 341.953,578.865 C 290.125,579.32 268.004,537.139 268.004,504.727 C 268.004,472.315 302.383,446.01 344.743,446.01 C 387.102,446.01 421.481,472.315 421.481,504.727 z " style="font-size:12px;fill:url(#linearGradient1417);stroke:none;stroke-width:1.23705006;stroke-dasharray:none" transform="matrix(1.30209,0,0,1.22525,170.042,-153.557)" />
<path id="path127" d="M 398.227,412.292 C 397.615,450.864 375.047,459.963 346.487,459.963 C 317.926,459.963 297.195,454.269 294.746,412.292 C 294.746,385.978 317.926,370.75 346.487,370.75 C 375.047,370.75 398.227,385.978 398.227,412.292 z " style="font-size:12px;fill:url(#linearGradient1419);stroke:none;stroke-width:1.38846004;stroke-dasharray:none" transform="matrix(1.1868,0,0,1.06708,210.623,-100.078)" />
<path transform="matrix(1.25,0,0,1.25,185.454,-167.505)" id="path129" d="M 234.285,456.475 C 252.001,429.479 289.3,388.111 241.262,462.288 C 202.311,523.331 226.859,562.561 239.518,573.327 C 276.045,605.889 274.484,627.676 245.913,610.533 C 184.288,573.907 197.078,512.285 234.285,456.475 z " style="fill:url(#linearGradient1421);stroke:none;stroke-width:1.25;" />
<path transform="matrix(1.25,0,0,1.25,185.454,-167.505)" id="path131" d="M 490.662,467.52 C 475.343,435.819 426.528,355.618 492.988,448.917 C 553.449,533.214 511.01,591.93 503.452,597.744 C 495.895,603.557 470.315,615.184 477.873,594.837 C 485.43,574.49 523.107,535.864 490.662,467.52 z " style="fill:url(#linearGradient1423);stroke:none;stroke-width:1.25;" />
<path transform="matrix(1.25,0,0,1.25,185.454,-167.505)" id="path132" d="M 220.915,716.921 C 180.473,695.505 121.663,721.045 143.013,662.855 C 147.289,649.617 136.638,629.847 143.594,616.929 C 151.733,601.231 169.174,604.72 179.639,594.255 C 189.957,583.364 196.498,564.606 215.683,567.513 C 234.867,570.42 247.628,593.974 261.027,622.742 C 270.91,643.38 305.968,672.406 303.677,695.5 C 300.981,731 260.65,737.69 220.915,716.921 z " style="fill:url(#linearGradient1425);stroke:#e68c3f;stroke-width:6.25;" />
<path id="path177" d="M 415.072,495.764 C 412.065,520.67 379.259,572.391 345.554,577.298 C 311.294,582.634 279.122,543.238 271.407,506.184 C 261.518,464.978 293.994,448.584 343.345,449.557 C 396.646,451.211 417.466,463.448 415.072,495.764 z " style="font-size:12px;fill:url(#linearGradient1427);stroke:none;stroke-width:2.85509992;stroke-dasharray:none" transform="matrix(0.598206,0.268584,-0.239623,0.617213,700.568,140.464)" />
<path transform="matrix(-1.1685,0.423145,0.475283,1.16478,728.343,-213.821)" id="path133" d="M 220.274,718.402 C 178.947,694.812 120.38,724.007 143.013,662.855 C 147.749,649.787 136.417,629.303 143.373,616.385 C 151.512,600.687 169.174,604.72 179.639,594.255 C 189.957,583.364 198.466,566.387 217.651,569.294 C 236.835,572.201 247.628,593.974 261.027,622.742 C 270.91,643.38 304.442,671.713 302.151,694.807 C 299.455,730.307 259.427,740.278 220.274,718.402 z " style="fill:url(#linearGradient1429);stroke:#e68c3f;stroke-width:6.25067997;" />
<path transform="matrix(-0.945096,0.343745,0.424076,0.956058,714.328,-64.342)" id="path134" d="M 216.482,675.68 C 129.951,618.177 169.174,604.72 179.639,594.255 C 189.957,583.364 198.466,566.387 217.651,569.294 C 236.835,572.201 247.628,593.974 261.027,622.742 C 270.91,643.38 304.087,671.66 302.151,694.807 C 299.535,721.917 253.961,700.294 216.482,675.68 z " style="fill:url(#linearGradient1431);stroke:none;stroke-width:1.52532005;" />
<path transform="matrix(1.00431,-5.2286e-2,-1.74e-2,1.04575,244.191,-28.4653)" id="path135" d="M 216.506,677.071 C 129.975,619.568 169.709,603.501 182.56,595.791 C 197.959,585.849 197.718,564.96 216.903,567.867 C 236.087,570.774 247.628,593.974 261.027,622.742 C 270.91,643.38 304.087,671.66 302.151,694.807 C 299.535,721.917 253.985,701.685 216.506,677.071 z " style="fill:url(#linearGradient1433);stroke:none;stroke-width:1.52532005;" />
<path id="path136" d="M 415.072,495.764 C 412.065,520.67 379.259,572.391 345.554,577.298 C 311.294,582.634 279.122,543.238 271.407,506.184 C 261.518,464.978 293.994,448.584 343.345,449.557 C 396.646,451.211 417.466,463.448 415.072,495.764 z " style="font-size:12px;fill:#000000;stroke:none;stroke-width:2.85509992;" transform="matrix(0.515584,0.215259,-0.206526,0.49467,713.3,222.559)" />
<path id="path137" d="M 415.072,495.764 C 412.065,520.67 379.259,572.391 345.554,577.298 C 311.294,582.634 279.122,543.238 271.407,506.184 C 261.518,464.978 293.994,448.584 343.345,449.557 C 396.646,451.211 417.466,463.448 415.072,495.764 z " style="font-size:12px;fill:url(#linearGradient1435);stroke:none;stroke-width:2.85509992;" transform="matrix(0.351231,0.149463,-0.128856,0.343469,724.522,318.291)" />
<path transform="matrix(1.25,0,0,1.25,185.454,-167.505)" id="path119" d="M 309.954,338.729 C 317.101,331.959 334.765,311.663 367.915,332.974 C 374.077,336.984 379.077,337.351 390.936,342.429 C 414.662,352.178 403.318,375.688 378.192,383.537 C 367.434,387.026 357.656,400.093 338.063,398.976 C 321.329,397.999 316.944,387.102 306.665,381.07 C 288.396,370.759 285.7,356.816 295.565,349.417 C 305.431,342.018 309.29,339.358 309.954,338.729 z " style="fill:url(#linearGradient1437);stroke:#e68c3f;stroke-width:3.75;" />
<path transform="matrix(1.25,0,0,1.25,185.454,-167.505)" id="path120" d="M 391.251,357.645 C 381.368,358.226 359.858,379.736 337.185,379.736 C 314.512,379.736 301.141,358.807 297.653,358.807" style="fill:none;stroke:#e68c3f;stroke-width:2.5;" />
<path transform="matrix(0.627885,0,0,0.595666,392.366,51.8173)" id="path123" d="M 309.954,338.729 C 317.101,331.959 339.645,313.381 369.542,332.401 C 375.841,336.167 382.346,340.266 392.02,345.865 C 411.182,357.613 401.691,374.543 378.734,385.255 C 368.316,389.75 351.141,399.67 338.063,398.976 C 323.53,397.568 314.128,387.577 304.496,381.07 C 286.826,368.767 287.899,358.833 296.107,350.562 C 302.312,344.883 309.29,339.358 309.954,338.729 z " style="fill:url(#linearGradient1439);stroke:none;" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -4,7 +4,7 @@
- Simple, declarative API
- 80+ usage examples
- Battle tested, mature, 99.9%+ coverage
- Battle tested, mature, 100% coverage (yes, every line is tested)
[GitHub](https://github.com/dolanmiu/docx)
[Get Started](#Welcome)

View File

@ -22,7 +22,7 @@ const doc = new Document({
}
})
],
}];
}]
});
```

View File

@ -6,6 +6,7 @@ To create a `floating` image on top of text:
```ts
const image = new ImageRun({
type: 'gif',
data: fs.readFileSync("./demo/images/pizza.gif"),
transformation: {
width: 200,
@ -26,6 +27,7 @@ By default with no arguments, its an `inline` image:
```ts
const image = new ImageRun({
type: 'gif',
data: fs.readFileSync("./demo/images/pizza.gif"),
transformation: {
width: 100,
@ -59,6 +61,7 @@ const doc = new Document({
new Paragraph({
children: [
new ImageRun({
type: [IMAGE_TYPE],
data: [IMAGE_BUFFER],
transformation: {
width: [IMAGE_SIZE],
@ -97,6 +100,7 @@ To change the position the image to be on top of the text, simply add the `float
```ts
const image = new ImageRun({
type: 'png',
data: buffer,
transformation: {
width: 903,
@ -115,6 +119,7 @@ const image = new ImageRun({
```ts
const image = new ImageRun({
type: 'png',
data: buffer,
transformation: {
width: 903,
@ -180,6 +185,7 @@ For example:
```ts
const image = new ImageRun({
type: 'gif',
data: fs.readFileSync("./demo/images/pizza.gif"),
transformation: {
width: 200,
@ -228,6 +234,7 @@ For example:
```ts
const image = new ImageRun({
type: 'gif',
data: fs.readFileSync("./demo/images/pizza.gif"),
transformation: {
width: 200,
@ -258,6 +265,7 @@ Specifies common non-visual DrawingML properties. A name, title and description
```ts
const image = new ImageRun({
type: 'gif',
data: fs.readFileSync("./demo/images/pizza.gif"),
altText: {
title: "This is an ultimate title",

View File

@ -263,3 +263,23 @@ new MathAngledBrackets({
],
}),
```
### Limit
#### Limit Upper
```ts
new MathLimitUpper({
children: [new MathRun("x")],
limit: [new MathRun("-")],
}),
```
#### Limit Lower
```ts
new MathLimitLower({
children: [new MathRun("lim")],
limit: [new MathRun("x→0")],
}),
```

View File

@ -130,6 +130,62 @@ new Paragraph({
}),
```
## Disabling numbering inherited from paragraph style
If the numbering is set on a paragraph style, you may wish to disable it for a specific paragraph:
```ts
const doc = new Document({
...
numbering: {
config: [
{
reference: "my-bullet-points",
levels: [
{
level: 0,
format: LevelFormat.BULLET,
text: "\u1F60",
alignment: AlignmentType.LEFT,
style: {
paragraph: {
indent: { left: convertInchesToTwip(0.5), hanging: convertInchesToTwip(0.25) },
},
},
},
],
},
],
},
styles: {
paragraphStyles: [
{
id: 'bullet',
name: 'Bullet',
basedOn: 'Normal',
next: 'Normal',
run: {},
paragraph: {
numbering: {
reference: 'my-bullet-points',
level: 0,
},
},
},
],
},
...
});
```
```ts
new Paragraph({
text: "No bullet points!",
style: "Bullet",
numbering: false,
}),
```
## Full Example
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/3-numbering-and-bullet-points.ts ":include")

View File

@ -35,6 +35,9 @@ interface Patch {
| type | `PatchType` | Required | `DOCUMENT`, `PARAGRAPH` |
| children | `FileChild[] or ParagraphChild[]` | Required | The contents to replace with. A `FileChild` is a `Paragraph` or `Table`, whereas a `ParagraphChild` is typical `Paragraph` children. |
The patcher also takes in a `keepOriginalStyles` boolean, which will preserve the styles of the patched text when set to true.
### How to patch existing document
1. Open your existing word document in your favorite Word Processor
@ -76,7 +79,7 @@ patchDocument(fs.readFileSync("My Document.docx"), {
],
link: "https://www.google.co.uk",
}),
new ImageRun({ data: fs.readFileSync("./demo/images/dog.png"), transformation: { width: 100, height: 100 } }),
new ImageRun({ type: 'png', data: fs.readFileSync("./demo/images/dog.png"), transformation: { width: 100, height: 100 } }),
],
}),
],

View File

@ -126,10 +126,10 @@ const doc = new Document({
next: "Normal",
quickFormat: true,
run: {
size: 26
size: 26,
bold: true,
color: "999999",
{
underline: {
type: UnderlineType.DOUBLE,
color: "FF0000",
},

View File

@ -22,7 +22,7 @@ Then add the table in the `section`
const doc = new Document({
sections: [{
children: [table],
}];
}],
});
```

3481
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "docx",
"version": "8.5.0",
"version": "9.0.1",
"description": "Easily generate .docx files with JS/TS with a nice declarative API. Works for Node and on the Browser.",
"type": "module",
"main": "build/index.umd.js",
@ -54,7 +54,8 @@
"clippy"
],
"dependencies": {
"@types/node": "^20.3.1",
"@types/node": "^22.7.5",
"hash.js": "^1.1.7",
"jszip": "^3.10.1",
"nanoid": "^5.0.4",
"xml": "^1.0.1",
@ -71,8 +72,8 @@
"@types/prompt": "^1.1.1",
"@types/unzipper": "^0.10.4",
"@types/xml": "^1.0.8",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^8.8.1",
"@vitest/coverage-v8": "^1.1.0",
"@vitest/ui": "^1.1.0",
"cspell": "^8.2.3",
@ -80,14 +81,14 @@
"eslint": "^8.23.0",
"eslint-plugin-functional": "^6.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^46.2.6",
"eslint-plugin-jsdoc": "^50.3.1",
"eslint-plugin-no-null": "^1.0.2",
"eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-unicorn": "^50.0.1",
"execa": "^8.0.1",
"glob": "^10.2.7",
"inquirer": "^9.2.7",
"jsdom": "^23.0.1",
"jsdom": "^24.0.0",
"pre-commit": "^1.2.2",
"prettier": "^3.1.1",
"tsconfig-paths": "^4.0.0",

View File

@ -12,7 +12,8 @@ describe("ImageReplacer", () => {
"test {test-image.png} test",
[
{
stream: Buffer.from(""),
type: "png",
data: Buffer.from(""),
fileName: "test-image.png",
transformation: {
pixels: {

View File

@ -150,12 +150,25 @@ describe("Compiler", () => {
new Paragraph({
children: [
new ImageRun({
type: "png",
data: Buffer.from("", "base64"),
transformation: {
width: 100,
height: 100,
},
}),
new ImageRun({
type: "svg",
data: Buffer.from("", "base64"),
transformation: {
width: 100,
height: 100,
},
fallback: {
type: "png",
data: Buffer.from("", "base64"),
},
}),
],
}),
],
@ -165,7 +178,8 @@ describe("Compiler", () => {
vi.spyOn(compiler["imageReplacer"], "getMediaData").mockReturnValue([
{
stream: Buffer.from(""),
type: "png",
data: Buffer.from(""),
fileName: "test",
transformation: {
pixels: {
@ -178,6 +192,36 @@ describe("Compiler", () => {
},
},
},
{
type: "svg",
data: Buffer.from(""),
fileName: "test",
transformation: {
pixels: {
x: 100,
y: 100,
},
emus: {
x: 100,
y: 100,
},
},
fallback: {
type: "png",
data: Buffer.from(""),
fileName: "test",
transformation: {
pixels: {
x: 100,
y: 100,
},
emus: {
x: 100,
y: 100,
},
},
},
},
]);
compiler.compile(file);

View File

@ -62,8 +62,13 @@ export class Compiler {
}
}
for (const { stream, fileName } of file.Media.Array) {
zip.file(`word/media/${fileName}`, stream);
for (const data of file.Media.Array) {
if (data.type !== "svg") {
zip.file(`word/media/${data.fileName}`, data.data);
} else {
zip.file(`word/media/${data.fileName}`, data.data);
zip.file(`word/media/${data.fallback.fileName}`, data.fallback.data);
}
}
for (const { data: buffer, name, fontKey } of file.FontTable.fontOptionsWithKey) {

View File

@ -25,12 +25,13 @@ describe("ContentTypes", () => {
expect(tree["Types"][3]).to.deep.equal({ Default: { _attr: { ContentType: "image/jpeg", Extension: "jpg" } } });
expect(tree["Types"][4]).to.deep.equal({ Default: { _attr: { ContentType: "image/bmp", Extension: "bmp" } } });
expect(tree["Types"][5]).to.deep.equal({ Default: { _attr: { ContentType: "image/gif", Extension: "gif" } } });
expect(tree["Types"][6]).to.deep.equal({
expect(tree["Types"][6]).to.deep.equal({ Default: { _attr: { ContentType: "image/svg+xml", Extension: "svg" } } });
expect(tree["Types"][7]).to.deep.equal({
Default: { _attr: { ContentType: "application/vnd.openxmlformats-package.relationships+xml", Extension: "rels" } },
});
expect(tree["Types"][7]).to.deep.equal({ Default: { _attr: { ContentType: "application/xml", Extension: "xml" } } });
expect(tree["Types"][8]).to.deep.equal({ Default: { _attr: { ContentType: "application/xml", Extension: "xml" } } });
expect(tree["Types"][8]).to.deep.equal({
expect(tree["Types"][9]).to.deep.equal({
Default: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.obfuscatedFont",
@ -38,7 +39,7 @@ describe("ContentTypes", () => {
},
},
});
expect(tree["Types"][9]).to.deep.equal({
expect(tree["Types"][10]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
@ -46,7 +47,7 @@ describe("ContentTypes", () => {
},
},
});
expect(tree["Types"][10]).to.deep.equal({
expect(tree["Types"][11]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
@ -54,7 +55,7 @@ describe("ContentTypes", () => {
},
},
});
expect(tree["Types"][11]).to.deep.equal({
expect(tree["Types"][12]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-package.core-properties+xml",
@ -62,7 +63,7 @@ describe("ContentTypes", () => {
},
},
});
expect(tree["Types"][12]).to.deep.equal({
expect(tree["Types"][13]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.custom-properties+xml",
@ -70,7 +71,7 @@ describe("ContentTypes", () => {
},
},
});
expect(tree["Types"][13]).to.deep.equal({
expect(tree["Types"][14]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.extended-properties+xml",
@ -78,7 +79,7 @@ describe("ContentTypes", () => {
},
},
});
expect(tree["Types"][14]).to.deep.equal({
expect(tree["Types"][15]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
@ -86,7 +87,7 @@ describe("ContentTypes", () => {
},
},
});
expect(tree["Types"][15]).to.deep.equal({
expect(tree["Types"][16]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
@ -94,7 +95,7 @@ describe("ContentTypes", () => {
},
},
});
expect(tree["Types"][16]).to.deep.equal({
expect(tree["Types"][17]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
@ -111,7 +112,7 @@ describe("ContentTypes", () => {
contentTypes.addFooter(102);
const tree = new Formatter().format(contentTypes);
expect(tree["Types"][19]).to.deep.equal({
expect(tree["Types"][20]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
@ -120,7 +121,7 @@ describe("ContentTypes", () => {
},
});
expect(tree["Types"][20]).to.deep.equal({
expect(tree["Types"][21]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml",
@ -137,7 +138,7 @@ describe("ContentTypes", () => {
contentTypes.addHeader(202);
const tree = new Formatter().format(contentTypes);
expect(tree["Types"][19]).to.deep.equal({
expect(tree["Types"][20]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
@ -146,7 +147,7 @@ describe("ContentTypes", () => {
},
});
expect(tree["Types"][20]).to.deep.equal({
expect(tree["Types"][21]).to.deep.equal({
Override: {
_attr: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",

View File

@ -18,6 +18,7 @@ export class ContentTypes extends XmlComponent {
this.root.push(new Default("image/jpeg", "jpg"));
this.root.push(new Default("image/bmp", "bmp"));
this.root.push(new Default("image/gif", "gif"));
this.root.push(new Default("image/svg+xml", "svg"));
this.root.push(new Default("application/vnd.openxmlformats-package.relationships+xml", "rels"));
this.root.push(new Default("application/xml", "xml"));
this.root.push(new Default("application/vnd.openxmlformats-officedocument.obfuscatedFont", "odttf"));

View File

@ -11,8 +11,9 @@ import { Anchor } from "./anchor";
const createAnchor = (drawingOptions: IDrawingOptions): Anchor =>
new Anchor({
mediaData: {
type: "png",
fileName: "test.png",
stream: Buffer.from(""),
data: Buffer.from(""),
transformation: {
pixels: {
x: 0,

View File

@ -11,8 +11,9 @@ const imageBase64Data = `iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAACzVBMVEU
const createDrawing = (drawingOptions?: IDrawingOptions): Drawing =>
new Drawing(
{
type: "jpg",
fileName: "test.jpg",
stream: Buffer.from(imageBase64Data, "base64"),
data: Buffer.from(imageBase64Data, "base64"),
transformation: {
pixels: {
x: 100,

View File

@ -7,6 +7,12 @@ export type EffectExtentAttributes = {
readonly left: number;
};
// <xsd:complexType name="CT_EffectExtent">
// <xsd:attribute name="l" type="a:ST_Coordinate" use="required"/>
// <xsd:attribute name="t" type="a:ST_Coordinate" use="required"/>
// <xsd:attribute name="r" type="a:ST_Coordinate" use="required"/>
// <xsd:attribute name="b" type="a:ST_Coordinate" use="required"/>
// </xsd:complexType>
export const createEffectExtent = ({ top, right, bottom, left }: EffectExtentAttributes): XmlComponent =>
new BuilderElement<EffectExtentAttributes>({
name: "wp:effectExtent",

View File

@ -0,0 +1,35 @@
import { BuilderElement, XmlComponent } from "@file/xml-components";
import { IMediaData } from "@file/media";
const createSvgBlip = (mediaData: IMediaData): XmlComponent =>
new BuilderElement({
name: "asvg:svgBlip",
attributes: {
asvg: {
key: "xmlns:asvg",
value: "http://schemas.microsoft.com/office/drawing/2016/SVG/main",
},
embed: {
key: "r:embed",
value: `rId{${mediaData.fileName}}`,
},
},
});
const createExtention = (mediaData: IMediaData): XmlComponent =>
new BuilderElement({
name: "a:ext",
attributes: {
uri: {
key: "uri",
value: "{96DAC541-7B7A-43D3-8B79-37D633B846F1}",
},
},
children: [createSvgBlip(mediaData)],
});
export const createExtentionList = (mediaData: IMediaData): XmlComponent =>
new BuilderElement({
name: "a:extLst",
children: [createExtention(mediaData)],
});

View File

@ -1,7 +1,7 @@
import { IMediaData } from "@file/media";
import { XmlComponent } from "@file/xml-components";
import { Blip } from "./blip";
import { createBlip } from "./blip";
import { SourceRectangle } from "./source-rectangle";
import { Stretch } from "./stretch";
@ -9,7 +9,7 @@ export class BlipFill extends XmlComponent {
public constructor(mediaData: IMediaData) {
super("pic:blipFill");
this.root.push(new Blip(mediaData));
this.root.push(createBlip(mediaData));
this.root.push(new SourceRectangle());
this.root.push(new Stretch());
}

View File

@ -1,24 +1,24 @@
import { IMediaData } from "@file/media";
import { XmlAttributeComponent, XmlComponent } from "@file/xml-components";
import { BuilderElement, XmlComponent } from "@file/xml-components";
import { createExtentionList } from "./blip-extentions";
class BlipAttributes extends XmlAttributeComponent<{
type BlipAttributes = {
readonly embed: string;
readonly cstate: string;
}> {
protected readonly xmlKeys = {
embed: "r:embed",
cstate: "cstate",
};
}
export class Blip extends XmlComponent {
public constructor(mediaData: IMediaData) {
super("a:blip");
this.root.push(
new BlipAttributes({
embed: `rId{${mediaData.fileName}}`,
cstate: "none",
}),
);
}
}
export const createBlip = (mediaData: IMediaData): XmlComponent =>
new BuilderElement<BlipAttributes>({
name: "a:blip",
attributes: {
embed: {
key: "r:embed",
value: `rId{${mediaData.type === "svg" ? mediaData.fallback.fileName : mediaData.fileName}}`,
},
cstate: {
key: "cstate",
value: "none",
},
},
children: mediaData.type === "svg" ? [createExtentionList(mediaData)] : [],
});

View File

@ -8,8 +8,9 @@ describe("Inline", () => {
const tree = new Formatter().format(
createInline({
mediaData: {
type: "png",
fileName: "test.png",
stream: Buffer.from(""),
data: Buffer.from(""),
transformation: {
pixels: {
x: 0,

View File

@ -436,7 +436,44 @@ describe("File", () => {
it("should work with external styles", () => {
const doc = new File({
sections: [],
externalStyles: "",
externalStyles: `
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:mc="first" xmlns:r="second">
<w:docDefaults>
<w:rPrDefault>
<w:rPr>
<w:rFonts w:ascii="Arial" w:eastAsiaTheme="minorHAnsi" w:hAnsi="Arial" w:cstheme="minorHAnsi"/>
<w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA"/>
</w:rPr>
</w:rPrDefault>
<w:pPrDefault>
<w:pPr>
<w:spacing w:after="160" w:line="259" w:lineRule="auto"/>
</w:pPr>
</w:pPrDefault>
</w:docDefaults>
<w:latentStyles w:defLockedState="1" w:defUIPriority="99">
</w:latentStyles>
<w:style w:type="paragraph" w:default="1" w:styleId="Normal">
<w:name w:val="Normal"/>
<w:qFormat/>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="heading 1"/>
<w:basedOn w:val="Normal"/>
<w:pPr>
<w:keepNext/>
<w:keepLines/>
<w:pBdr>
<w:bottom w:val="single" w:sz="4" w:space="1" w:color="auto"/>
</w:pBdr>
</w:pPr>
</w:style>
</w:styles>`,
});
expect(doc.Styles).to.not.be.undefined;

View File

@ -84,7 +84,7 @@ export class File {
this.media = new Media();
if (options.externalStyles) {
if (options.externalStyles !== undefined) {
const stylesFactory = new ExternalStylesFactory();
this.styles = stylesFactory.newInstance(options.externalStyles);
} else if (options.styles) {

View File

@ -1,6 +1,7 @@
export * from "./paragraph";
export * from "./table";
export * from "./file";
export * from "./file-child";
export * from "./numbering";
export * from "./media";
export * from "./drawing";

View File

@ -14,11 +14,25 @@ export interface IMediaDataTransformation {
readonly rotation?: number;
}
export interface IMediaData {
readonly stream: Buffer | Uint8Array | ArrayBuffer;
type CoreMediaData = {
readonly fileName: string;
readonly transformation: IMediaDataTransformation;
}
readonly data: Buffer | Uint8Array | ArrayBuffer;
};
type RegularMediaData = {
readonly type: "jpg" | "png" | "gif" | "bmp";
};
type SvgMediaData = {
readonly type: "svg";
/**
* Required in case the Word processor does not support SVG.
*/
readonly fallback: RegularMediaData & CoreMediaData;
};
export type IMediaData = (RegularMediaData | SvgMediaData) & CoreMediaData;
// Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432
/**

View File

@ -19,7 +19,8 @@ describe("Media", () => {
const media = new Media();
media.addImage("test2.png", {
stream: Buffer.from(""),
type: "png",
data: Buffer.from(""),
fileName: "test.png",
transformation: {
pixels: {

View File

@ -4,6 +4,7 @@ import { Formatter } from "@export/formatter";
import { AlignmentType, EmphasisMarkType, TabStopPosition } from "../paragraph";
import { UnderlineType } from "../paragraph/run/underline";
import { HighlightColor } from "../paragraph/run/properties";
import { ShadingType } from "../shading";
import { AbstractNumbering } from "./abstract-numbering";
import { LevelFormat, LevelSuffix } from "./level";
@ -2048,23 +2049,23 @@ describe("AbstractNumbering", () => {
const highlightTests = [
{
highlight: "005599",
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }, { "w:highlightCs": { _attr: { "w:val": "005599" } } }],
highlight: HighlightColor.YELLOW,
expected: [{ "w:highlight": { _attr: { "w:val": "yellow" } } }, { "w:highlightCs": { _attr: { "w:val": "yellow" } } }],
},
{
highlight: "005599",
highlight: HighlightColor.YELLOW,
highlightComplexScript: true,
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }, { "w:highlightCs": { _attr: { "w:val": "005599" } } }],
expected: [{ "w:highlight": { _attr: { "w:val": "yellow" } } }, { "w:highlightCs": { _attr: { "w:val": "yellow" } } }],
},
{
highlight: "005599",
highlight: HighlightColor.YELLOW,
highlightComplexScript: false,
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }],
expected: [{ "w:highlight": { _attr: { "w:val": "yellow" } } }],
},
{
highlight: "005599",
highlight: HighlightColor.YELLOW,
highlightComplexScript: "550099",
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }, { "w:highlightCs": { _attr: { "w:val": "550099" } } }],
expected: [{ "w:highlight": { _attr: { "w:val": "yellow" } } }, { "w:highlightCs": { _attr: { "w:val": "550099" } } }],
},
];
highlightTests.forEach(({ highlight, highlightComplexScript, expected }) => {

View File

@ -24,6 +24,8 @@ class SpacingAttributes extends XmlAttributeComponent<ISpacingProperties> {
before: "w:before",
line: "w:line",
lineRule: "w:lineRule",
beforeAutoSpacing: "w:beforeAutospacing",
afterAutoSpacing: "w:afterAutoSpacing",
};
}

View File

@ -6,3 +6,6 @@ export * from "./math-sub-script";
export * from "./math-sum";
export * from "./math-integral";
export * from "./math-super-script";
export * from "./math-limit";
export * from "./math-limit-upper";
export * from "./math-limit-lower";

View File

@ -22,7 +22,7 @@ describe("MathIntegral", () => {
{
"m:limLoc": {
_attr: {
"m:val": "undOvr",
"m:val": "subSup",
},
},
},
@ -78,7 +78,7 @@ describe("MathIntegral", () => {
{
"m:limLoc": {
_attr: {
"m:val": "undOvr",
"m:val": "subSup",
},
},
},

View File

@ -16,7 +16,7 @@ export class MathIntegral extends XmlComponent {
public constructor(options: IMathIntegralOptions) {
super("m:nary");
this.root.push(new MathNAryProperties("", !!options.superScript, !!options.subScript));
this.root.push(new MathNAryProperties("", !!options.superScript, !!options.subScript, "subSup"));
if (!!options.subScript) {
this.root.push(new MathSubScriptElement(options.subScript));

View File

@ -6,9 +6,9 @@ class MathLimitLocationAttributes extends XmlAttributeComponent<{ readonly value
}
export class MathLimitLocation extends XmlComponent {
public constructor() {
public constructor(value?: string) {
super("m:limLoc");
this.root.push(new MathLimitLocationAttributes({ value: "undOvr" }));
this.root.push(new MathLimitLocationAttributes({ value: value || "undOvr" }));
}
}

View File

@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { Formatter } from "@export/formatter";
import { MathRun } from "../math-run";
import { MathLimitLower } from "./math-limit-lower";
describe("MathLimitLower", () => {
describe("#constructor()", () => {
it("should create a MathLimitLower with correct root key", () => {
const mathLimitLower = new MathLimitLower({
children: [new MathRun("lim")],
limit: [new MathRun("x→0")],
});
const tree = new Formatter().format(mathLimitLower);
expect(tree).to.deep.equal({
"m:limLow": [
{
"m:e": [
{
"m:r": [
{
"m:t": ["lim"],
},
],
},
],
},
{
"m:lim": [
{
"m:r": [
{
"m:t": ["x→0"],
},
],
},
],
},
],
});
});
});
});

View File

@ -0,0 +1,19 @@
// http://www.datypic.com/sc/ooxml/e-m_limLow-1.html
import { XmlComponent } from "@file/xml-components";
import { MathComponent } from "../math-component";
import { MathBase } from "./math-base";
import { MathLimit } from "./math-limit";
export interface IMathLimitLowerOptions {
readonly children: readonly MathComponent[];
readonly limit: readonly MathComponent[];
}
export class MathLimitLower extends XmlComponent {
public constructor(options: IMathLimitLowerOptions) {
super("m:limLow");
this.root.push(new MathBase(options.children));
this.root.push(new MathLimit(options.limit));
}
}

View File

@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { Formatter } from "@export/formatter";
import { MathRun } from "../math-run";
import { MathLimitUpper } from "./math-limit-upper";
describe("MathLimitUpper", () => {
describe("#constructor()", () => {
it("should create a MathLimitUpper with correct root key", () => {
const mathLimitUpper = new MathLimitUpper({
children: [new MathRun("x")],
limit: [new MathRun("-")],
});
const tree = new Formatter().format(mathLimitUpper);
expect(tree).to.deep.equal({
"m:limUpp": [
{
"m:e": [
{
"m:r": [
{
"m:t": ["x"],
},
],
},
],
},
{
"m:lim": [
{
"m:r": [
{
"m:t": ["-"],
},
],
},
],
},
],
});
});
});
});

View File

@ -0,0 +1,19 @@
// http://www.datypic.com/sc/ooxml/e-m_limUpp-1.html
import { XmlComponent } from "@file/xml-components";
import { MathComponent } from "../math-component";
import { MathBase } from "./math-base";
import { MathLimit } from "./math-limit";
export interface IMathLimitUpperOptions {
readonly children: readonly MathComponent[];
readonly limit: readonly MathComponent[];
}
export class MathLimitUpper extends XmlComponent {
public constructor(options: IMathLimitUpperOptions) {
super("m:limUpp");
this.root.push(new MathBase(options.children));
this.root.push(new MathLimit(options.limit));
}
}

View File

@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { Formatter } from "@export/formatter";
import { MathRun } from "../math-run";
import { MathLimit } from "./math-limit";
describe("MathLimit", () => {
describe("#constructor()", () => {
it("should create a MathLimit with correct root key", () => {
const mathLimit = new MathLimit([new MathRun("x→0")]);
const tree = new Formatter().format(mathLimit);
expect(tree).to.deep.equal({
"m:lim": [
{
"m:r": [
{
"m:t": ["x→0"],
},
],
},
],
});
});
});
});

View File

@ -0,0 +1,13 @@
// http://www.datypic.com/sc/ooxml/e-m_lim-1.html
import { XmlComponent } from "@file/xml-components";
import { MathComponent } from "../math-component";
export class MathLimit extends XmlComponent {
public constructor(children: readonly MathComponent[]) {
super("m:lim");
for (const child of children) {
this.root.push(child);
}
}
}

View File

@ -7,13 +7,13 @@ import { MathSubScriptHide } from "./math-sub-script-hide";
import { MathSuperScriptHide } from "./math-super-script-hide";
export class MathNAryProperties extends XmlComponent {
public constructor(accent: string, hasSuperScript: boolean, hasSubScript: boolean) {
public constructor(accent: string, hasSuperScript: boolean, hasSubScript: boolean, limitLocationVal?: string) {
super("m:naryPr");
if (!!accent) {
this.root.push(new MathAccentCharacter(accent));
}
this.root.push(new MathLimitLocation());
this.root.push(new MathLimitLocation(limitLocationVal));
if (!hasSuperScript) {
this.root.push(new MathSuperScriptHide());

View File

@ -65,6 +65,36 @@ describe("ParagraphProperties", () => {
});
});
it("should create with numbering disabled", () => {
const properties = new ParagraphProperties({
numbering: false,
});
const tree = new Formatter().format(properties);
expect(tree).to.deep.equal({
"w:pPr": [
{
"w:numPr": [
{
"w:ilvl": {
_attr: {
"w:val": 0,
},
},
},
{
"w:numId": {
_attr: {
"w:val": 0,
},
},
},
],
},
],
});
});
it("should create with widowControl", () => {
const properties = new ParagraphProperties({
widowControl: true,

View File

@ -37,12 +37,14 @@ export interface ILevelParagraphStylePropertiesOptions {
}
export interface IParagraphStylePropertiesOptions extends ILevelParagraphStylePropertiesOptions {
readonly numbering?: {
readonly numbering?:
| {
readonly reference: string;
readonly level: number;
readonly instance?: number;
readonly custom?: boolean;
};
}
| false;
}
export interface IParagraphPropertiesOptions extends IParagraphStylePropertiesOptions {
@ -135,6 +137,8 @@ export class ParagraphProperties extends IgnoreIfEmptyXmlComponent {
});
this.push(new NumberProperties(`${options.numbering.reference}-${options.numbering.instance ?? 0}`, options.numbering.level));
} else if (options.numbering === false) {
this.push(new NumberProperties(0, 0));
}
if (options.border) {

View File

@ -1,24 +1,16 @@
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { Formatter } from "@export/formatter";
import { IViewWrapper } from "@file/document-wrapper";
import { File } from "@file/file";
import * as convenienceFunctions from "@util/convenience-functions";
import { ImageRun } from "./image-run";
describe("ImageRun", () => {
beforeAll(() => {
vi.spyOn(convenienceFunctions, "uniqueId").mockReturnValue("test-unique-id");
});
afterAll(() => {
vi.resetAllMocks();
});
describe("#constructor()", () => {
it("should create with Buffer", () => {
const currentImageRun = new ImageRun({
type: "png",
data: Buffer.from(""),
transformation: {
width: 200,
@ -39,8 +31,7 @@ describe("ImageRun", () => {
const tree = new Formatter().format(currentImageRun, {
file: {
Media: {
// eslint-disable-next-line @typescript-eslint/no-empty-function
addImage: () => {},
addImage: vi.fn(),
},
} as unknown as File,
viewWrapper: {} as unknown as IViewWrapper,
@ -193,7 +184,8 @@ describe("ImageRun", () => {
"a:blip": {
_attr: {
cstate: "none",
"r:embed": "rId{test-unique-id.png}",
"r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
},
},
},
@ -271,6 +263,7 @@ describe("ImageRun", () => {
it("should create with string", () => {
const currentImageRun = new ImageRun({
type: "png",
data: "",
transformation: {
width: 200,
@ -291,8 +284,7 @@ describe("ImageRun", () => {
const tree = new Formatter().format(currentImageRun, {
file: {
Media: {
// eslint-disable-next-line @typescript-eslint/no-empty-function
addImage: () => {},
addImage: vi.fn(),
},
} as unknown as File,
viewWrapper: {} as unknown as IViewWrapper,
@ -445,7 +437,8 @@ describe("ImageRun", () => {
"a:blip": {
_attr: {
cstate: "none",
"r:embed": "rId{test-unique-id.png}",
"r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
},
},
},
@ -522,10 +515,10 @@ describe("ImageRun", () => {
});
it("should return UInt8Array if atob is present", () => {
// eslint-disable-next-line functional/immutable-data
global.atob = () => "atob result";
vi.spyOn(global, "atob").mockReturnValue("atob result");
const currentImageRun = new ImageRun({
type: "png",
data: "",
transformation: {
width: 200,
@ -546,8 +539,7 @@ describe("ImageRun", () => {
const tree = new Formatter().format(currentImageRun, {
file: {
Media: {
// eslint-disable-next-line @typescript-eslint/no-empty-function
addImage: () => {},
addImage: vi.fn(),
},
} as unknown as File,
viewWrapper: {} as unknown as IViewWrapper,
@ -701,7 +693,8 @@ describe("ImageRun", () => {
"a:blip": {
_attr: {
cstate: "none",
"r:embed": "rId{test-unique-id.png}",
"r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
},
},
},
@ -775,16 +768,13 @@ describe("ImageRun", () => {
},
],
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any, functional/immutable-data
(global as any).atob = undefined;
});
it("should use data as is if its not a string", () => {
// eslint-disable-next-line functional/immutable-data
global.atob = () => "atob result";
vi.spyOn(global, "atob").mockReturnValue("atob result");
const currentImageRun = new ImageRun({
type: "png",
data: "",
transformation: {
width: 200,
@ -805,8 +795,7 @@ describe("ImageRun", () => {
const tree = new Formatter().format(currentImageRun, {
file: {
Media: {
// eslint-disable-next-line @typescript-eslint/no-empty-function
addImage: () => {},
addImage: vi.fn(),
},
} as unknown as File,
viewWrapper: {} as unknown as IViewWrapper,
@ -960,7 +949,8 @@ describe("ImageRun", () => {
"a:blip": {
_attr: {
cstate: "none",
"r:embed": "rId{test-unique-id.png}",
"r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
},
},
},
@ -1034,9 +1024,158 @@ describe("ImageRun", () => {
},
],
});
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any, functional/immutable-data
(global as any).atob = undefined;
it("should strip base64 marker", () => {
const spy = vi.spyOn(global, "atob").mockReturnValue("atob result");
new ImageRun({
type: "png",
data: ";base64,",
transformation: {
width: 200,
height: 200,
rotation: 45,
},
});
expect(spy).toBeCalledWith("");
});
it("should work with svgs", () => {
const currentImageRun = new ImageRun({
type: "svg",
data: Buffer.from(""),
transformation: {
width: 200,
height: 200,
},
fallback: {
type: "png",
data: Buffer.from(""),
},
});
const tree = new Formatter().format(currentImageRun, {
file: {
Media: {
addImage: vi.fn(),
},
} as unknown as File,
viewWrapper: {} as unknown as IViewWrapper,
stack: [],
});
expect(tree).toStrictEqual({
"w:r": [
{
"w:drawing": [
{
"wp:inline": expect.arrayContaining([
{
"a:graphic": expect.arrayContaining([
{
"a:graphicData": expect.arrayContaining([
{
"pic:pic": expect.arrayContaining([
{
"pic:blipFill": expect.arrayContaining([
{
"a:blip": [
{
_attr: {
cstate: "none",
"r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.png}",
},
},
{
"a:extLst": [
{
"a:ext": [
{
_attr: {
uri: "{96DAC541-7B7A-43D3-8B79-37D633B846F1}",
},
},
{
"asvg:svgBlip": {
_attr: expect.objectContaining({
"r:embed":
"rId{da39a3ee5e6b4b0d3255bfef95601890afd80709.svg}",
}),
},
},
],
},
],
},
],
},
]),
},
]),
},
]),
},
]),
},
]),
},
],
},
],
});
});
it("using same data twice should use same media key", () => {
const imageRunStringData = new ImageRun({
type: "png",
data: "DATA",
transformation: {
width: 100,
height: 100,
rotation: 42,
},
});
const imageRunBufferData = new ImageRun({
type: "png",
data: Buffer.from("DATA"),
transformation: {
width: 200,
height: 200,
rotation: 45,
},
});
const addImageSpy = vi.fn();
const context = {
file: {
Media: {
addImage: addImageSpy,
},
} as unknown as File,
viewWrapper: {} as unknown as IViewWrapper,
stack: [],
};
new Formatter().format(imageRunStringData, context);
new Formatter().format(imageRunBufferData, context);
const expectedHash = "580393f5a94fb469585f5dd2a6859a4aab899f37";
expect(addImageSpy).toHaveBeenCalledTimes(2);
expect(addImageSpy).toHaveBeenNthCalledWith(
1,
`${expectedHash}.png`,
expect.objectContaining({ fileName: `${expectedHash}.png` }),
);
expect(addImageSpy).toHaveBeenNthCalledWith(
2,
`${expectedHash}.png`,
expect.objectContaining({ fileName: `${expectedHash}.png` }),
);
});
});
});

View File

@ -1,4 +1,4 @@
import { uniqueId } from "@util/convenience-functions";
import { hashedId } from "@util/convenience-functions";
import { IContext, IXmlableObject } from "@file/xml-components";
import { DocPropertiesOptions } from "@file/drawing/doc-properties/doc-properties";
@ -9,54 +9,30 @@ import { IMediaTransformation } from "../../media";
import { IMediaData } from "../../media/data";
import { Run } from "../run";
export interface IImageOptions {
readonly data: Buffer | string | Uint8Array | ArrayBuffer;
type CoreImageOptions = {
readonly transformation: IMediaTransformation;
readonly floating?: IFloating;
readonly altText?: DocPropertiesOptions;
readonly outline?: OutlineOptions;
}
export class ImageRun extends Run {
private readonly key = `${uniqueId()}.png`;
private readonly imageData: IMediaData;
public constructor(options: IImageOptions) {
super({});
const newData = typeof options.data === "string" ? this.convertDataURIToBinary(options.data) : options.data;
this.imageData = {
stream: newData,
fileName: this.key,
transformation: {
pixels: {
x: Math.round(options.transformation.width),
y: Math.round(options.transformation.height),
},
emus: {
x: Math.round(options.transformation.width * 9525),
y: Math.round(options.transformation.height * 9525),
},
flip: options.transformation.flip,
rotation: options.transformation.rotation ? options.transformation.rotation * 60000 : undefined,
},
};
const drawing = new Drawing(this.imageData, {
floating: options.floating,
docProperties: options.altText,
outline: options.outline,
});
this.root.push(drawing);
}
type RegularImageOptions = {
readonly type: "jpg" | "png" | "gif" | "bmp";
readonly data: Buffer | string | Uint8Array | ArrayBuffer;
};
public prepForXml(context: IContext): IXmlableObject | undefined {
context.file.Media.addImage(this.key, this.imageData);
type SvgMediaOptions = {
readonly type: "svg";
readonly data: Buffer | string | Uint8Array | ArrayBuffer;
/**
* Required in case the Word processor does not support SVG.
*/
readonly fallback: RegularImageOptions;
};
return super.prepForXml(context);
}
export type IImageOptions = (RegularImageOptions | SvgMediaOptions) & CoreImageOptions;
private convertDataURIToBinary(dataURI: string): Uint8Array {
const convertDataURIToBinary = (dataURI: string): Uint8Array => {
if (typeof atob === "function") {
// https://gist.github.com/borismus/1032746
// https://github.com/mafintosh/base64-to-uint8array
@ -77,5 +53,73 @@ export class ImageRun extends Run {
const b = require("buf" + "fer");
return new b.Buffer(dataURI, "base64");
}
};
const standardizeData = (data: string | Buffer | Uint8Array | ArrayBuffer): Buffer | Uint8Array | ArrayBuffer =>
typeof data === "string" ? convertDataURIToBinary(data) : data;
const createImageData = (options: IImageOptions, key: string): Pick<IMediaData, "data" | "fileName" | "transformation"> => ({
data: standardizeData(options.data),
fileName: key,
transformation: {
pixels: {
x: Math.round(options.transformation.width),
y: Math.round(options.transformation.height),
},
emus: {
x: Math.round(options.transformation.width * 9525),
y: Math.round(options.transformation.height * 9525),
},
flip: options.transformation.flip,
rotation: options.transformation.rotation ? options.transformation.rotation * 60000 : undefined,
},
});
export class ImageRun extends Run {
private readonly imageData: IMediaData;
public constructor(options: IImageOptions) {
super({});
const hash = hashedId(options.data);
const key = `${hash}.${options.type}`;
this.imageData =
options.type === "svg"
? {
type: options.type,
...createImageData(options, key),
fallback: {
type: options.fallback.type,
...createImageData(
{
...options.fallback,
transformation: options.transformation,
},
`${hashedId(options.fallback.data)}.${options.fallback.type}`,
),
},
}
: {
type: options.type,
...createImageData(options, key),
};
const drawing = new Drawing(this.imageData, {
floating: options.floating,
docProperties: options.altText,
outline: options.outline,
});
this.root.push(drawing);
}
public prepForXml(context: IContext): IXmlableObject | undefined {
context.file.Media.addImage(this.imageData.fileName, this.imageData);
if (this.imageData.type === "svg") {
context.file.Media.addImage(this.imageData.fallback.fileName, this.imageData.fallback);
}
return super.prepForXml(context);
}
}

View File

@ -36,6 +36,33 @@ export const TextEffect = {
NONE: "none",
} as const;
/*
* http://officeopenxml.com/WPtextShading.php
*
* Limit the list of supported highlight colors
*
* */
export const HighlightColor = {
BLACK: "black",
BLUE: "blue",
CYAN: "cyan",
DARK_BLUE: "darkBlue",
DARK_CYAN: "darkCyan",
DARK_GRAY: "darkGray",
DARK_GREEN: "darkGreen",
DARK_MAGENTA: "darkMagenta",
DARK_RED: "darkRed",
DARK_YELLOW: "darkYellow",
GREEN: "green",
LIGHT_GRAY: "lightGray",
MAGENTA: "magenta",
NONE: "none",
RED: "red",
WHITE: "white",
YELLOW: "yellow",
} as const;
/* eslint-enable */
export interface IRunStylePropertiesOptions {
@ -65,7 +92,7 @@ export interface IRunStylePropertiesOptions {
readonly subScript?: boolean;
readonly superScript?: boolean;
readonly font?: string | IFontOptions | IFontAttributesProperties;
readonly highlight?: string;
readonly highlight?: (typeof HighlightColor)[keyof typeof HighlightColor];
readonly highlightComplexScript?: boolean | string;
readonly characterSpacing?: number;
readonly shading?: IShadingAttributesProperties;

View File

@ -23,11 +23,9 @@ export class Text extends XmlComponent {
if (typeof options === "string") {
this.root.push(new TextAttributes({ space: SpaceType.PRESERVE }));
this.root.push(options);
return this;
} else {
this.root.push(new TextAttributes({ space: options.space ?? SpaceType.DEFAULT }));
this.root.push(options.text);
return this;
}
}
}

View File

@ -7,7 +7,7 @@ import { ShadingType } from "@file/shading";
import { EmphasisMarkType } from "./emphasis-mark";
import { PageNumber, Run } from "./run";
import { UnderlineType } from "./underline";
import { TextEffect } from "./properties";
import { HighlightColor, TextEffect } from "./properties";
describe("Run", () => {
describe("#noProof()", () => {
it("turns off spelling and grammar checkers for a run", () => {
@ -215,18 +215,18 @@ describe("Run", () => {
describe("#highlight()", () => {
it("it should add highlight to the properties", () => {
const run = new Run({
highlight: "005599",
highlight: HighlightColor.YELLOW,
});
const tree = new Formatter().format(run);
expect(tree).to.deep.equal({
"w:r": [
{
"w:rPr": [
{ "w:highlight": { _attr: { "w:val": "005599" } } },
{ "w:highlight": { _attr: { "w:val": "yellow" } } },
{
"w:highlightCs": {
_attr: {
"w:val": "005599",
"w:val": "yellow",
},
},
},

View File

@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import { Formatter } from "@export/formatter";
import { EmphasisMarkType } from "@file/paragraph/run/emphasis-mark";
import { UnderlineType } from "@file/paragraph/run/underline";
import { HighlightColor } from "@file/paragraph/run/properties";
import { ShadingType } from "@file/shading";
import { EMPTY_OBJECT } from "@file/xml-components";
@ -728,23 +729,23 @@ describe("CharacterStyle", () => {
const highlightTests = [
{
highlight: "005599",
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }, { "w:highlightCs": { _attr: { "w:val": "005599" } } }],
highlight: HighlightColor.YELLOW,
expected: [{ "w:highlight": { _attr: { "w:val": "yellow" } } }, { "w:highlightCs": { _attr: { "w:val": "yellow" } } }],
},
{
highlight: "005599",
highlight: HighlightColor.YELLOW,
highlightComplexScript: true,
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }, { "w:highlightCs": { _attr: { "w:val": "005599" } } }],
expected: [{ "w:highlight": { _attr: { "w:val": "yellow" } } }, { "w:highlightCs": { _attr: { "w:val": "yellow" } } }],
},
{
highlight: "005599",
highlight: HighlightColor.YELLOW,
highlightComplexScript: false,
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }],
expected: [{ "w:highlight": { _attr: { "w:val": "yellow" } } }],
},
{
highlight: "005599",
highlight: HighlightColor.YELLOW,
highlightComplexScript: "550099",
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }, { "w:highlightCs": { _attr: { "w:val": "550099" } } }],
expected: [{ "w:highlight": { _attr: { "w:val": "yellow" } } }, { "w:highlightCs": { _attr: { "w:val": "550099" } } }],
},
];
highlightTests.forEach(({ highlight, highlightComplexScript, expected }) => {

View File

@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import { Formatter } from "@export/formatter";
import { AlignmentType, EmphasisMarkType, TabStopPosition } from "@file/paragraph";
import { UnderlineType } from "@file/paragraph/run/underline";
import { HighlightColor } from "@file/paragraph/run";
import { ShadingType } from "@file/shading";
import { EMPTY_OBJECT } from "@file/xml-components";
@ -615,23 +616,23 @@ describe("ParagraphStyle", () => {
const highlightTests = [
{
highlight: "005599",
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }, { "w:highlightCs": { _attr: { "w:val": "005599" } } }],
highlight: HighlightColor.YELLOW,
expected: [{ "w:highlight": { _attr: { "w:val": "yellow" } } }, { "w:highlightCs": { _attr: { "w:val": "yellow" } } }],
},
{
highlight: "005599",
highlight: HighlightColor.YELLOW,
highlightComplexScript: true,
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }, { "w:highlightCs": { _attr: { "w:val": "005599" } } }],
expected: [{ "w:highlight": { _attr: { "w:val": "yellow" } } }, { "w:highlightCs": { _attr: { "w:val": "yellow" } } }],
},
{
highlight: "005599",
highlight: HighlightColor.YELLOW,
highlightComplexScript: false,
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }],
expected: [{ "w:highlight": { _attr: { "w:val": "yellow" } } }],
},
{
highlight: "005599",
highlight: HighlightColor.YELLOW,
highlightComplexScript: "550099",
expected: [{ "w:highlight": { _attr: { "w:val": "005599" } } }, { "w:highlightCs": { _attr: { "w:val": "550099" } } }],
expected: [{ "w:highlight": { _attr: { "w:val": "yellow" } } }, { "w:highlightCs": { _attr: { "w:val": "550099" } } }],
},
];
highlightTests.forEach(({ highlight, highlightComplexScript, expected }) => {

View File

@ -1,12 +1,13 @@
import { BaseXmlComponent, IContext } from "./base";
import { IXmlableObject, IXmlAttribute } from "./xmlable-object";
export type AttributeMap<T> = { readonly [P in keyof T]: string };
type AttributeMap<T> = Record<keyof T, string>;
export type AttributeData = { readonly [key: string]: boolean | number | string };
export type AttributePayload<T> = { readonly [P in keyof T]: { readonly key: string; readonly value: T[P] } };
export abstract class XmlAttributeComponent<T extends object> extends BaseXmlComponent {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export abstract class XmlAttributeComponent<T extends { readonly [key: string]: any }> extends BaseXmlComponent {
protected readonly xmlKeys?: AttributeMap<T>;
public constructor(private readonly root: T) {
@ -16,12 +17,11 @@ export abstract class XmlAttributeComponent<T extends object> extends BaseXmlCom
public prepForXml(_: IContext): IXmlableObject {
// eslint-disable-next-line functional/prefer-readonly-type
const attrs: { [key: string]: string } = {};
Object.keys(this.root).forEach((key) => {
Object.entries(this.root).forEach(([key, value]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const value = (this.root as any)[key];
if (value !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newKey = (this.xmlKeys && (this.xmlKeys as any)[key]) || key;
const newKey = (this.xmlKeys && this.xmlKeys[key]) || key;
// eslint-disable-next-line functional/immutable-data
attrs[newKey] = value;
}

View File

@ -218,7 +218,9 @@ describe("from-docx", () => {
});
it("should patch the document", async () => {
const output = await patchDocument(Buffer.from(""), {
const output = await patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
patches: {
name: {
type: PatchType.PARAGRAPH,
@ -254,6 +256,7 @@ describe("from-docx", () => {
link: "https://www.google.co.uk",
}),
new ImageRun({
type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
@ -266,6 +269,7 @@ describe("from-docx", () => {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
@ -277,7 +281,9 @@ describe("from-docx", () => {
});
it("should patch the document", async () => {
const output = await patchDocument(Buffer.from(""), {
const output = await patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
patches: {},
});
expect(output).to.not.be.undefined;
@ -303,13 +309,16 @@ describe("from-docx", () => {
});
it("should use the relationships file rather than create one", async () => {
const output = await patchDocument(Buffer.from(""), {
const output = await patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
patches: {
// eslint-disable-next-line @typescript-eslint/naming-convention
image_test: {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
@ -347,13 +356,16 @@ describe("from-docx", () => {
it("should throw an error if the content types is not found", () =>
expect(
patchDocument(Buffer.from(""), {
patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
patches: {
// eslint-disable-next-line @typescript-eslint/naming-convention
image_test: {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
@ -384,13 +396,16 @@ describe("from-docx", () => {
it("should throw an error if the content types is not found", () =>
expect(
patchDocument(Buffer.from(""), {
patchDocument({
outputType: "uint8array",
data: Buffer.from(""),
patches: {
// eslint-disable-next-line @typescript-eslint/naming-convention
image_test: {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
type: "png",
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),

View File

@ -12,13 +12,12 @@ import { TargetModeType } from "@file/relationships/relationship/relationship";
import { uniqueId } from "@util/convenience-functions";
import { replacer } from "./replacer";
import { findLocationOfText } from "./traverser";
import { toJson } from "./util";
import { appendRelationship, getNextRelationshipIndex } from "./relationship-manager";
import { appendContentType } from "./content-types-manager";
// eslint-disable-next-line functional/prefer-readonly-type
type InputDataType = Buffer | string | number[] | Uint8Array | ArrayBuffer | Blob | NodeJS.ReadableStream;
export type InputDataType = Buffer | string | number[] | Uint8Array | ArrayBuffer | Blob | NodeJS.ReadableStream;
export const PatchType = {
DOCUMENT: "file",
@ -47,14 +46,37 @@ interface IHyperlinkRelationshipAddition {
export type IPatch = ParagraphPatch | FilePatch;
export interface PatchDocumentOptions {
// From JSZip
type OutputByType = {
readonly base64: string;
// eslint-disable-next-line id-denylist
readonly string: string;
readonly text: string;
readonly binarystring: string;
readonly array: readonly number[];
readonly uint8array: Uint8Array;
readonly arraybuffer: ArrayBuffer;
readonly blob: Blob;
readonly nodebuffer: Buffer;
};
export type PatchDocumentOutputType = keyof OutputByType;
export type PatchDocumentOptions<T extends PatchDocumentOutputType = PatchDocumentOutputType> = {
readonly outputType: T;
readonly data: InputDataType;
readonly patches: { readonly [key: string]: IPatch };
readonly keepOriginalStyles?: boolean;
}
};
const imageReplacer = new ImageReplacer();
export const patchDocument = async (data: InputDataType, options: PatchDocumentOptions): Promise<Uint8Array> => {
export const patchDocument = async <T extends PatchDocumentOutputType = PatchDocumentOutputType>({
outputType,
data,
patches,
keepOriginalStyles,
}: PatchDocumentOptions<T>): Promise<OutputByType[T]> => {
const zipContent = await JSZip.loadAsync(data);
const contexts = new Map<string, IContext>();
const file = {
@ -104,13 +126,20 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
};
contexts.set(key, context);
for (const [patchKey, patchValue] of Object.entries(options.patches)) {
for (const [patchKey, patchValue] of Object.entries(patches)) {
const patchText = `{{${patchKey}}}`;
const renderedParagraphs = findLocationOfText(json, patchText);
// TODO: mutates json. Make it immutable
replacer(
// We need to loop through to catch every occurrence of the patch text
// It is possible that the patch text is in the same run
// This algorithm is limited to one patch per text run
// Once it cannot find any more occurrences, it will throw an error, and then we break out of the loop
// https://github.com/dolanmiu/docx/issues/2267
// eslint-disable-next-line no-constant-condition
while (true) {
try {
replacer({
json,
{
patch: {
...patchValue,
children: patchValue.children.map((element) => {
// We need to replace external hyperlinks with concrete hyperlinks
@ -132,10 +161,13 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
patchText,
renderedParagraphs,
context,
options.keepOriginalStyles,
);
keepOriginalStyles,
});
} catch {
break;
}
}
}
const mediaDatas = imageReplacer.getMediaData(JSON.stringify(json), context.file.Media);
@ -201,6 +233,7 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
appendContentType(contentTypesJson, "image/jpeg", "jpg");
appendContentType(contentTypesJson, "image/bmp", "bmp");
appendContentType(contentTypesJson, "image/gif", "gif");
appendContentType(contentTypesJson, "image/svg+xml", "svg");
}
const zip = new JSZip();
@ -215,12 +248,12 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
zip.file(key, value);
}
for (const { stream, fileName } of file.Media.Array) {
for (const { data: stream, fileName } of file.Media.Array) {
zip.file(`word/media/${fileName}`, stream);
}
return zip.generateAsync({
type: "uint8array",
type: outputType,
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
compression: "DEFLATE",
});

View File

@ -1 +1,2 @@
export * from "./from-docx";
export * from "./patch-detector";

View File

@ -273,5 +273,65 @@ describe("paragraph-split-inject", () => {
},
});
});
it("should create an empty end element if it is at the end", () => {
const output = splitRunElement(
{
type: "element",
name: "w:r",
elements: [
{
type: "element",
name: "w:rPr",
elements: [
{ type: "element", name: "w:rFonts", attributes: { "w:eastAsia": "Times New Roman" } },
{ type: "element", name: "w:kern", attributes: { "w:val": "0" } },
{ type: "element", name: "w:sz", attributes: { "w:val": "20" } },
{
type: "element",
name: "w:lang",
attributes: { "w:val": "en-US", "w:eastAsia": "en-US", "w:bidi": "ar-SA" },
},
],
},
{ type: "element", name: "w:t", elements: [], attributes: { "xml:space": "preserve" } },
{ type: "element", name: "w:br" },
{ type: "element", name: "w:t", elements: [{ type: "text", text: "ɵ" }] },
],
},
"ɵ",
);
expect(output).to.deep.equal({
left: {
type: "element",
name: "w:r",
elements: [
{
type: "element",
name: "w:rPr",
elements: [
{ type: "element", name: "w:rFonts", attributes: { "w:eastAsia": "Times New Roman" } },
{ type: "element", name: "w:kern", attributes: { "w:val": "0" } },
{ type: "element", name: "w:sz", attributes: { "w:val": "20" } },
{
type: "element",
name: "w:lang",
attributes: { "w:val": "en-US", "w:eastAsia": "en-US", "w:bidi": "ar-SA" },
},
],
},
{ type: "element", name: "w:t", elements: [], attributes: { "xml:space": "preserve" } },
{ type: "element", name: "w:br" },
{ type: "element", name: "w:t", elements: [], attributes: { "xml:space": "preserve" } },
],
},
right: {
type: "element",
name: "w:r",
elements: [{ type: "element", name: "w:t", elements: [], attributes: { "xml:space": "preserve" } }],
},
});
});
});
});

View File

@ -29,7 +29,7 @@ export const splitRunElement = (runElement: Element, token: string): { readonly
runElement.elements
?.map((e, i) => {
if (e.type === "element" && e.name === "w:t") {
const text = (e.elements?.[0].text as string) ?? "";
const text = (e.elements?.[0]?.text as string) ?? "";
const splitText = text.split(token);
const newElements = splitText.map((t) => ({
...e,

View File

@ -27,7 +27,7 @@ describe("paragraph-token-replacer", () => {
},
renderedParagraph: {
index: 0,
path: [0],
pathToParagraph: [0],
runs: [
{
end: 4,
@ -128,7 +128,7 @@ describe("paragraph-token-replacer", () => {
{ text: "World", parts: [{ text: "World", index: 0, start: 15, end: 19 }], index: 3, start: 15, end: 19 },
],
index: 0,
path: [0, 1, 0, 0],
pathToParagraph: [0, 1, 0, 0],
},
originalText: "{{name}}",
replacementText: "John",

View File

@ -0,0 +1,393 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import JSZip from "jszip";
import { patchDetector } from "./patch-detector";
const MOCK_XML = `
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex"
xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex"
xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex"
xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex"
xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex"
xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex"
xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex"
xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex"
xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink"
xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:oel="http://schemas.microsoft.com/office/2019/extlst"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
xmlns:w10="urn:schemas-microsoft-com:office:word"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex"
xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid"
xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml"
xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash"
xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex"
xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
<w:body>
<w:p w14:paraId="2499FE9F" w14:textId="0A3D130F" w:rsidR="00B51233"
w:rsidRDefault="007B52ED" w:rsidP="007B52ED">
<w:pPr>
<w:pStyle w:val="Title" />
</w:pPr>
<w:r>
<w:t>Hello World</w:t>
</w:r>
</w:p>
<w:p w14:paraId="6410D9A0" w14:textId="7579AB49" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED" />
<w:p w14:paraId="57ACF964" w14:textId="315D7A05" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED">
<w:r>
<w:t>Hello {{name}},</w:t>
</w:r>
<w:r w:rsidR="008126CB">
<w:t xml:space="preserve"> how are you?</w:t>
</w:r>
</w:p>
<w:p w14:paraId="38C7DF4A" w14:textId="66CDEC9A" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED" />
<w:p w14:paraId="04FABE2B" w14:textId="3DACA001" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED">
<w:r>
<w:t>{{paragraph_replace}}</w:t>
</w:r>
</w:p>
<w:p w14:paraId="7AD7975D" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
<w:p w14:paraId="3BD6D75A" w14:textId="19AE3121" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F">
<w:r>
<w:t>{{table}}</w:t>
</w:r>
</w:p>
<w:p w14:paraId="76023962" w14:textId="4E606AB9" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED" />
<w:tbl>
<w:tblPr>
<w:tblStyle w:val="TableGrid" />
<w:tblW w:w="0" w:type="auto" />
<w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1"
w:lastColumn="0" w:noHBand="0" w:noVBand="1" />
</w:tblPr>
<w:tblGrid>
<w:gridCol w:w="3003" />
<w:gridCol w:w="3003" />
<w:gridCol w:w="3004" />
</w:tblGrid>
<w:tr w:rsidR="00EF161F" w14:paraId="1DEC5955" w14:textId="77777777" w:rsidTr="00EF161F">
<w:tc>
<w:tcPr>
<w:tcW w:w="3003" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="54DA5587" w14:textId="625BAC60" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F">
<w:r>
<w:t>{{table_heading_1}}</w:t>
</w:r>
</w:p>
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3003" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="57100910" w14:textId="71FD5616" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3004" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="1D388FAB" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
</w:tr>
<w:tr w:rsidR="00EF161F" w14:paraId="0F53D2DC" w14:textId="77777777" w:rsidTr="00EF161F">
<w:tc>
<w:tcPr>
<w:tcW w:w="3003" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="0F2BCCED" w14:textId="3C3B6706" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F">
<w:r>
<w:t>Item: {{item_1}}</w:t>
</w:r>
</w:p>
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3003" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="1E6158AC" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3004" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="17937748" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
</w:tr>
<w:tr w:rsidR="00EF161F" w14:paraId="781DAC1A" w14:textId="77777777" w:rsidTr="00EF161F">
<w:tc>
<w:tcPr>
<w:tcW w:w="3003" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="1DCD0343" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3003" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="5D02E3CD" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3004" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="52EA0DBB" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
</w:tr>
</w:tbl>
<w:p w14:paraId="47CD1FBC" w14:textId="23474CBC" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED" />
<w:p w14:paraId="0ACCEE90" w14:textId="67907499" w:rsidR="00EF161F"
w:rsidRDefault="0077578F">
<w:r>
<w:t>{{image_test}}</w:t>
</w:r>
</w:p>
<w:p w14:paraId="23FA9862" w14:textId="77777777" w:rsidR="0077578F"
w:rsidRDefault="0077578F" />
<w:p w14:paraId="01578F2F" w14:textId="3BDC6C85" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED">
<w:r>
<w:t>Thank you</w:t>
</w:r>
</w:p>
<w:sectPr w:rsidR="007B52ED" w:rsidSect="0072043F">
<w:headerReference w:type="default" r:id="rId6" />
<w:footerReference w:type="default" r:id="rId7" />
<w:pgSz w:w="11900" w:h="16840" />
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="708"
w:footer="708" w:gutter="0" />
<w:cols w:space="708" />
<w:docGrid w:linePitch="360" />
</w:sectPr>
</w:body>
</w:document>
`;
// cspell:disable
const MOCK_XML_2 = `
<w:body>
<w:tbl>
<w:tblPr>
<w:tblStyle w:val="TableGrid" />
<w:tblW w:w="9350" w:type="dxa" />
<w:jc w:val="left" />
<w:tblInd w:w="0" w:type="dxa" />
<w:tblLayout w:type="fixed" />
<w:tblCellMar>
<w:top w:w="0" w:type="dxa" />
<w:left w:w="108" w:type="dxa" />
<w:bottom w:w="0" w:type="dxa" />
<w:right w:w="108" w:type="dxa" />
</w:tblCellMar>
<w:tblLook w:firstRow="1" w:noVBand="1" w:lastRow="0" w:firstColumn="1"
w:lastColumn="0" w:noHBand="0" w:val="04a0" />
</w:tblPr>
<w:tblGrid>
<w:gridCol w:w="3119" />
<w:gridCol w:w="3141" />
<w:gridCol w:w="3090" />
</w:tblGrid>
<w:tr>
<w:trPr></w:trPr>
<w:tc>
<w:tcPr>
<w:tcW w:w="3119" w:type="dxa" />
<w:tcBorders>
<w:right w:val="nil" />
</w:tcBorders>
<w:shd w:color="auto" w:fill="D9D9D9" w:themeFill="background1"
w:themeFillShade="d9" w:val="clear" />
</w:tcPr>
<w:p>
<w:pPr>
<w:pStyle w:val="NormalSpaceAboveandBelow" />
<w:widowControl />
<w:spacing w:before="120" w:after="120" />
<w:jc w:val="left" />
<w:rPr>
<w:rFonts w:eastAsia="Times New Roman" />
<w:kern w:val="0" />
<w:sz w:val="20" />
<w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA" />
</w:rPr>
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:eastAsia="Times New Roman" />
<w:kern w:val="0" />
<w:sz w:val="20" />
<w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA" />
</w:rPr>
<w:t>{{</w:t>
</w:r>
<w:r>
<w:rPr>
<w:rFonts w:eastAsia="Times New Roman" />
<w:kern w:val="0" />
<w:sz w:val="20" />
<w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA" />
</w:rPr>
<w:t>s</w:t>
</w:r>
<w:r>
<w:rPr>
<w:rFonts w:eastAsia="Times New Roman" />
<w:kern w:val="0" />
<w:sz w:val="20" />
<w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA" />
</w:rPr>
<w:t>chool_</w:t>
</w:r>
<w:r>
<w:rPr>
<w:rFonts w:eastAsia="Times New Roman" />
<w:kern w:val="0" />
<w:sz w:val="20" />
<w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA" />
</w:rPr>
<w:t>n</w:t>
</w:r>
<w:r>
<w:rPr>
<w:rFonts w:eastAsia="Times New Roman" />
<w:kern w:val="0" />
<w:sz w:val="20" />
<w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA" />
</w:rPr>
<w:t>ame}}</w:t>
<w:br />
<w:t>{{</w:t>
</w:r>
<w:r>
<w:rPr>
<w:rFonts w:eastAsia="Times New Roman" />
<w:kern w:val="0" />
<w:sz w:val="20" />
<w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA" />
</w:rPr>
<w:t>a</w:t>
</w:r>
<w:r>
<w:rPr>
<w:rFonts w:eastAsia="Times New Roman" />
<w:kern w:val="0" />
<w:sz w:val="20" />
<w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA" />
</w:rPr>
<w:t>ddr</w:t>
</w:r>
<w:r>
<w:rPr>
<w:rFonts w:eastAsia="Times New Roman" />
<w:kern w:val="0" />
<w:sz w:val="20" />
<w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA" />
</w:rPr>
<w:t>ess</w:t>
</w:r>
<w:r>
<w:rPr>
<w:rFonts w:eastAsia="Times New Roman" />
<w:kern w:val="0" />
<w:sz w:val="20" />
<w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA" />
</w:rPr>
<w:t>}}</w:t>
<w:br />
<w:t>{{</w:t>
</w:r>
</w:p>
</w:tc>
</w:tr>
</w:tbl>
</w:body>
`;
// cspell:enable
describe("patch-detector", () => {
describe("patchDetector", () => {
describe("document.xml and [Content_Types].xml", () => {
beforeEach(() => {
vi.spyOn(JSZip, "loadAsync").mockReturnValue(
new Promise<JSZip>((resolve) => {
const zip = new JSZip();
zip.file("word/document.xml", MOCK_XML);
zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`);
resolve(zip);
}),
);
});
afterEach(() => {
vi.restoreAllMocks();
});
it("should patch the document", async () => {
const output = await patchDetector({
data: Buffer.from(""),
});
expect(output).toMatchObject(["name", "paragraph_replace", "table", "image_test", "table_heading_1", "item_1"]);
});
});
});
describe("patchDetector", () => {
describe("document.xml and [Content_Types].xml", () => {
beforeEach(() => {
vi.spyOn(JSZip, "loadAsync").mockReturnValue(
new Promise<JSZip>((resolve) => {
const zip = new JSZip();
zip.file("word/document.xml", MOCK_XML_2);
zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`);
resolve(zip);
}),
);
});
afterEach(() => {
vi.restoreAllMocks();
});
it("should patch the document", async () => {
const output = await patchDetector({
data: Buffer.from(""),
});
expect(output).toMatchObject(["school_name", "address"]);
});
});
});
});

View File

@ -0,0 +1,30 @@
import JSZip from "jszip";
import { toJson } from "./util";
import { traverse } from "./traverser";
import { InputDataType } from "./from-docx";
type PatchDetectorOptions = {
readonly data: InputDataType;
};
/** Detects which patches are needed/present in a template */
export const patchDetector = async ({ data }: PatchDetectorOptions): Promise<readonly string[]> => {
const zipContent = await JSZip.loadAsync(data);
const patches = new Set<string>();
for (const [key, value] of Object.entries(zipContent.files)) {
if (!key.endsWith(".xml") && !key.endsWith(".rels")) {
continue;
}
if (key.startsWith("word/") && !key.endsWith(".xml.rels")) {
const json = toJson(await value.async("text"));
traverse(json).forEach((p) => findPatchKeys(p.text).forEach((patch) => patches.add(patch)));
}
}
return Array.from(patches);
};
const findPatchKeys = (text: string): readonly string[] => {
const pattern = /(?<=\{\{).+?(?=\}\})/gs;
return text.match(pattern) ?? [];
};

View File

@ -8,7 +8,7 @@ import { PatchType } from "./from-docx";
import { replacer } from "./replacer";
const MOCK_JSON = {
export const MOCK_JSON = {
elements: [
{
type: "element",
@ -73,103 +73,60 @@ const MOCK_JSON = {
describe("replacer", () => {
describe("replacer", () => {
it("should return the same object if nothing is added", () => {
const output = replacer(
{
it("should throw an error if nothing is added", () => {
expect(() =>
replacer({
json: {
elements: [],
},
{
patch: {
type: PatchType.PARAGRAPH,
children: [],
},
"hello",
[],
patchText: "hello",
// eslint-disable-next-line functional/prefer-readonly-type
vi.fn<[], IContext>()(),
);
expect(output).to.deep.equal({
elements: [],
});
context: vi.fn<[], IContext>()(),
}),
).toThrow();
});
it("should replace paragraph type", () => {
const output = replacer(
MOCK_JSON,
{
const output = replacer({
json: JSON.parse(JSON.stringify(MOCK_JSON)),
patch: {
type: PatchType.PARAGRAPH,
children: [new TextRun("Delightful Header")],
},
"{{header_adjective}}",
[
{
text: "This is a {{header_adjective}} dont you think?",
runs: [
{
text: "This is a {{head",
parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }],
index: 1,
start: 0,
end: 15,
},
{ text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 },
{
text: "_adjective}} dont you think?",
parts: [{ text: "_adjective}} dont you think?", index: 0, start: 18, end: 46 }],
index: 3,
start: 18,
end: 46,
},
],
index: 0,
path: [0, 0, 0],
},
],
{
patchText: "{{header_adjective}}",
context: {
file: {} as unknown as File,
viewWrapper: {
Relationships: {},
} as unknown as IViewWrapper,
stack: [],
},
);
});
expect(JSON.stringify(output)).to.contain("Delightful Header");
});
it("should replace paragraph type keeping original styling if keepOriginalStyles is true", () => {
const output = replacer(
MOCK_JSON,
{
const output = replacer({
json: JSON.parse(JSON.stringify(MOCK_JSON)),
patch: {
type: PatchType.PARAGRAPH,
children: [new TextRun("sweet")],
},
"{{bold}}",
[
{
text: "What a {{bold}} text!",
runs: [
{
text: "What a {{bold}} text!",
parts: [{ text: "What a {{bold}} text!", index: 1, start: 0, end: 21 }],
index: 0,
start: 0,
end: 21,
},
],
index: 0,
path: [0, 0, 1],
},
],
{
patchText: "{{bold}}",
context: {
file: {} as unknown as File,
viewWrapper: {
Relationships: {},
} as unknown as IViewWrapper,
stack: [],
},
true,
);
keepOriginalStyles: true,
});
expect(JSON.stringify(output)).to.contain("sweet");
expect(output.elements![0].elements![1].elements).toMatchObject([
@ -225,91 +182,472 @@ describe("replacer", () => {
});
it("should replace document type", () => {
const output = replacer(
MOCK_JSON,
{
const output = replacer({
json: JSON.parse(JSON.stringify(MOCK_JSON)),
patch: {
type: PatchType.DOCUMENT,
children: [new Paragraph("Lorem ipsum paragraph")],
},
"{{header_adjective}}",
[
{
text: "This is a {{header_adjective}} dont you think?",
runs: [
{
text: "This is a {{head",
parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }],
index: 1,
start: 0,
end: 15,
},
{ text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 },
{
text: "_adjective}} dont you think?",
parts: [{ text: "_adjective}} dont you think?", index: 0, start: 18, end: 46 }],
index: 3,
start: 18,
end: 46,
},
],
index: 0,
path: [0, 0, 0],
},
],
{
patchText: "{{header_adjective}}",
context: {
file: {} as unknown as File,
viewWrapper: {
Relationships: {},
} as unknown as IViewWrapper,
stack: [],
},
);
});
expect(JSON.stringify(output)).to.contain("Lorem ipsum paragraph");
});
it("should throw an error if the type is not supported", () => {
expect(() =>
replacer(
{},
it("should replace", () => {
// cspell:disable
const output = replacer({
json: {
elements: [
{
type: PatchType.DOCUMENT,
type: "element",
name: "w:hdr",
elements: [
{
type: "element",
name: "w:p",
elements: [
{
type: "element",
name: "w:r",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rPr",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rFonts",
attributes: { "w:eastAsia": "Times New Roman" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:kern",
attributes: { "w:val": "0" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:sz",
attributes: { "w:val": "20" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:lang",
attributes: {
"w:val": "en-US",
"w:eastAsia": "en-US",
"w:bidi": "ar-SA",
},
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "{{" }],
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:r",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rPr",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rFonts",
attributes: { "w:eastAsia": "Times New Roman" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:kern",
attributes: { "w:val": "0" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:sz",
attributes: { "w:val": "20" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:lang",
attributes: {
"w:val": "en-US",
"w:eastAsia": "en-US",
"w:bidi": "ar-SA",
},
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "s" }],
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:r",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rPr",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rFonts",
attributes: { "w:eastAsia": "Times New Roman" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:kern",
attributes: { "w:val": "0" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:sz",
attributes: { "w:val": "20" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:lang",
attributes: {
"w:val": "en-US",
"w:eastAsia": "en-US",
"w:bidi": "ar-SA",
},
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "chool_" }],
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:r",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rPr",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rFonts",
attributes: { "w:eastAsia": "Times New Roman" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:kern",
attributes: { "w:val": "0" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:sz",
attributes: { "w:val": "20" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:lang",
attributes: {
"w:val": "en-US",
"w:eastAsia": "en-US",
"w:bidi": "ar-SA",
},
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "n" }],
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:r",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "{{" }],
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:r",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rPr",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rFonts",
attributes: { "w:eastAsia": "Times New Roman" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:kern",
attributes: { "w:val": "0" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:sz",
attributes: { "w:val": "20" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:lang",
attributes: {
"w:val": "en-US",
"w:eastAsia": "en-US",
"w:bidi": "ar-SA",
},
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "a" }],
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:r",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rPr",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rFonts",
attributes: { "w:eastAsia": "Times New Roman" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:kern",
attributes: { "w:val": "0" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:sz",
attributes: { "w:val": "20" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:lang",
attributes: {
"w:val": "en-US",
"w:eastAsia": "en-US",
"w:bidi": "ar-SA",
},
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "ddr" }],
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:r",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rPr",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rFonts",
attributes: { "w:eastAsia": "Times New Roman" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:kern",
attributes: { "w:val": "0" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:sz",
attributes: { "w:val": "20" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:lang",
attributes: {
"w:val": "en-US",
"w:eastAsia": "en-US",
"w:bidi": "ar-SA",
},
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "ess" }],
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:r",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rPr",
elements: [
{ type: "text", text: "\n " },
{
type: "element",
name: "w:rFonts",
attributes: { "w:eastAsia": "Times New Roman" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:kern",
attributes: { "w:val": "0" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:sz",
attributes: { "w:val": "20" },
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:lang",
attributes: {
"w:val": "en-US",
"w:eastAsia": "en-US",
"w:bidi": "ar-SA",
},
},
{ type: "text", text: "\n " },
],
},
{ type: "text", text: "\n " },
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "}}" }],
},
],
},
],
},
],
},
],
},
// cspell:enable
patch: {
type: PatchType.PARAGRAPH,
children: [new Paragraph("Lorem ipsum paragraph")],
},
"{{header_adjective}}",
[
{
text: "This is a {{header_adjective}} dont you think?",
runs: [
{
text: "This is a {{head",
parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }],
index: 1,
start: 0,
end: 15,
},
{ text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 },
{
text: "_adjective}} dont you think?",
parts: [{ text: "_adjective}} dont you think?", index: 0, start: 18, end: 46 }],
index: 3,
start: 18,
end: 46,
},
],
index: 0,
path: [0, 0, 0],
},
],
{
patchText: "{{address}}",
context: {
file: {} as unknown as File,
viewWrapper: {
Relationships: {},
} as unknown as IViewWrapper,
stack: [],
},
),
).to.throw();
});
expect(JSON.stringify(output)).to.contain("Lorem ipsum paragraph");
});
});
});

View File

@ -6,22 +6,33 @@ import { IContext, XmlComponent } from "@file/xml-components";
import { IPatch, PatchType } from "./from-docx";
import { toJson } from "./util";
import { IRenderedParagraphNode } from "./run-renderer";
import { replaceTokenInParagraphElement } from "./paragraph-token-replacer";
import { findRunElementIndexWithToken, splitRunElement } from "./paragraph-split-inject";
import { findLocationOfText } from "./traverser";
const formatter = new Formatter();
const SPLIT_TOKEN = "ɵ";
export const replacer = (
json: Element,
patch: IPatch,
patchText: string,
renderedParagraphs: readonly IRenderedParagraphNode[],
context: IContext,
keepOriginalStyles: boolean = false,
): Element => {
export const replacer = ({
json,
patch,
patchText,
context,
keepOriginalStyles = true,
}: {
readonly json: Element;
readonly patch: IPatch;
readonly patchText: string;
readonly context: IContext;
readonly keepOriginalStyles?: boolean;
}): Element => {
const renderedParagraphs = findLocationOfText(json, patchText);
if (renderedParagraphs.length === 0) {
throw new Error(`Could not find text ${patchText}`);
}
for (const renderedParagraph of renderedParagraphs) {
const textJson = patch.children
// eslint-disable-next-line no-loop-func
@ -30,15 +41,15 @@ export const replacer = (
switch (patch.type) {
case PatchType.DOCUMENT: {
const parentElement = goToParentElementFromPath(json, renderedParagraph.path);
const elementIndex = getLastElementIndexFromPath(renderedParagraph.path);
const parentElement = goToParentElementFromPath(json, renderedParagraph.pathToParagraph);
const elementIndex = getLastElementIndexFromPath(renderedParagraph.pathToParagraph);
// eslint-disable-next-line functional/immutable-data, prefer-destructuring
parentElement.elements!.splice(elementIndex, 1, ...textJson);
break;
}
case PatchType.PARAGRAPH:
default: {
const paragraphElement = goToElementFromPath(json, renderedParagraph.path);
const paragraphElement = goToElementFromPath(json, renderedParagraph.pathToParagraph);
replaceTokenInParagraphElement({
paragraphElement,
renderedParagraph,
@ -56,7 +67,7 @@ export const replacer = (
if (keepOriginalStyles) {
const runElementNonTextualElements = runElementToBeReplaced.elements!.filter(
(e) => e.type === "element" && e.name !== "w:t",
(e) => e.type === "element" && e.name !== "w:t" && e.name !== "w:br",
);
newRunElements = textJson.map((e) => ({
@ -87,11 +98,7 @@ const goToElementFromPath = (json: Element, path: readonly number[]): Element =>
// Which we do not want to double count
for (let i = 1; i < path.length; i++) {
const index = path[i];
const nextElements = element.elements;
if (!nextElements) {
throw new Error("Could not find element");
}
const nextElements = element.elements!;
element = nextElements[index];
}

View File

@ -7,7 +7,7 @@ describe("run-renderer", () => {
const output = renderParagraphNode({ element: { name: "w:p" }, index: 0, parent: undefined });
expect(output).to.deep.equal({
index: -1,
path: [],
pathToParagraph: [],
runs: [],
text: "",
});
@ -39,7 +39,7 @@ describe("run-renderer", () => {
});
expect(output).to.deep.equal({
index: 0,
path: [0],
pathToParagraph: [0],
runs: [
{
end: 4,
@ -79,7 +79,7 @@ describe("run-renderer", () => {
});
expect(output).to.deep.equal({
index: 0,
path: [0],
pathToParagraph: [0],
runs: [
{
end: 0,

View File

@ -6,7 +6,7 @@ export interface IRenderedParagraphNode {
readonly text: string;
readonly runs: readonly IRenderedRunNode[];
readonly index: number;
readonly path: readonly number[];
readonly pathToParagraph: readonly number[];
}
interface StartAndEnd {
@ -35,7 +35,7 @@ export const renderParagraphNode = (node: ElementWrapper): IRenderedParagraphNod
text: "",
runs: [],
index: -1,
path: [],
pathToParagraph: [],
};
}
@ -50,8 +50,7 @@ export const renderParagraphNode = (node: ElementWrapper): IRenderedParagraphNod
return renderedRunNode;
})
.filter((e) => !!e)
.map((e) => e as IRenderedRunNode);
.filter((e) => !!e);
const text = runs.reduce((acc, curr) => acc + curr.text, "");
@ -59,7 +58,7 @@ export const renderParagraphNode = (node: ElementWrapper): IRenderedParagraphNod
text,
runs,
index: node.index,
path: buildNodePath(node),
pathToParagraph: buildNodePath(node),
};
};

View File

@ -139,6 +139,28 @@ const MOCK_JSON = {
},
],
},
{
type: "element",
name: "w:p",
elements: [
{
type: "element",
name: "w:r",
elements: [
{
type: "element",
name: "w:rPr",
elements: [{ type: "element", name: "w:b", attributes: { "w:val": "1" } }],
},
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "What a {{bold}} text!" }],
},
],
},
],
},
{
type: "element",
name: "w:p",
@ -535,6 +557,45 @@ const MOCK_JSON = {
},
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "3BE1A671",
"w14:textId": "74E856C4",
"w:rsidR": "000D38A7",
"w:rsidRDefault": "000D38A7",
},
elements: [
{
type: "element",
name: "w:pPr",
elements: [{ type: "element", name: "w:pStyle", attributes: { "w:val": "Header" } }],
},
{
type: "element",
name: "w:r",
elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "This is a {{head" }] }],
},
{
type: "element",
name: "w:r",
attributes: { "w:rsidR": "004A3A99" },
elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "er" }] }],
},
{
type: "element",
name: "w:r",
elements: [
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "_adjective}} dont you think?" }],
},
],
},
],
},
{
type: "element",
name: "w:sectPr",
@ -574,7 +635,7 @@ describe("traverser", () => {
expect(output).to.deep.equal([
{
index: 1,
path: [0, 0, 0, 8, 2, 0, 1],
pathToParagraph: [0, 0, 0, 9, 2, 0, 1],
runs: [
{
end: 18,
@ -595,5 +656,76 @@ describe("traverser", () => {
},
]);
});
it("should find the location of text", () => {
const output = findLocationOfText(MOCK_JSON, "{{bold}}");
expect(output).to.deep.equal([
{
text: "What a {{bold}} text!",
runs: [
{
text: "What a {{bold}} text!",
parts: [{ text: "What a {{bold}} text!", index: 1, start: 0, end: 20 }],
index: 0,
start: 0,
end: 20,
},
],
index: 5,
pathToParagraph: [0, 0, 0, 5],
},
]);
});
it("should find the location of text", () => {
const output = findLocationOfText(MOCK_JSON, "{{bold}}");
expect(output).to.deep.equal([
{
text: "What a {{bold}} text!",
runs: [
{
text: "What a {{bold}} text!",
parts: [{ text: "What a {{bold}} text!", index: 1, start: 0, end: 20 }],
index: 0,
start: 0,
end: 20,
},
],
index: 5,
pathToParagraph: [0, 0, 0, 5],
},
]);
});
it("should find the location of text", () => {
const output = findLocationOfText(MOCK_JSON, "{{header_adjective}}");
expect(output).to.deep.equal([
{
text: "This is a {{header_adjective}} dont you think?",
runs: [
{
text: "This is a {{head",
parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }],
index: 1,
start: 0,
end: 15,
},
{ text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 },
{
text: "_adjective}} dont you think?",
parts: [{ text: "_adjective}} dont you think?", index: 0, start: 18, end: 46 }],
index: 3,
start: 18,
end: 46,
},
],
index: 14,
pathToParagraph: [0, 0, 0, 14],
},
]);
});
});
});

View File

@ -15,7 +15,7 @@ const elementsToWrapper = (wrapper: ElementWrapper): readonly ElementWrapper[] =
parent: wrapper,
})) ?? [];
export const findLocationOfText = (node: Element, text: string): readonly IRenderedParagraphNode[] => {
export const traverse = (node: Element): readonly IRenderedParagraphNode[] => {
let renderedParagraphs: readonly IRenderedParagraphNode[] = [];
// eslint-disable-next-line functional/prefer-readonly-type
@ -41,5 +41,8 @@ export const findLocationOfText = (node: Element, text: string): readonly IRende
}
}
return renderedParagraphs.filter((p) => p.text.includes(text));
return renderedParagraphs;
};
export const findLocationOfText = (node: Element, text: string): readonly IRenderedParagraphNode[] =>
traverse(node).filter((p) => p.text.includes(text));

View File

@ -8,6 +8,7 @@ import {
convertMillimetersToTwip,
docPropertiesUniqueNumericIdGen,
uniqueId,
hashedId,
uniqueNumericIdCreator,
uniqueUuid,
} from "./convenience-functions";
@ -72,6 +73,26 @@ describe("Utility", () => {
});
});
describe("#hashedId", () => {
it("should generate a hex string", () => {
expect(hashedId("")).to.equal("da39a3ee5e6b4b0d3255bfef95601890afd80709");
});
it("should work with string, Uint8Array, Buffer and ArrayBuffer", () => {
const stringInput = "DATA";
const uint8ArrayInput = new Uint8Array(new TextEncoder().encode(stringInput));
const bufferInput = Buffer.from(uint8ArrayInput);
const arrayBufferInput = uint8ArrayInput.buffer;
const expectedHash = "580393f5a94fb469585f5dd2a6859a4aab899f37";
expect(hashedId(stringInput)).to.equal(expectedHash);
expect(hashedId(uint8ArrayInput)).to.equal(expectedHash);
expect(hashedId(bufferInput)).to.equal(expectedHash);
expect(hashedId(arrayBufferInput)).to.equal(expectedHash);
});
});
describe("#uniqueUuid", () => {
it("should generate a unique pseudorandom ID", () => {
expect(uniqueUuid()).to.not.be.empty;

View File

@ -1,4 +1,5 @@
import { nanoid, customAlphabet } from "nanoid/non-secure";
import hash from "hash.js";
// Twip - twentieths of a point
export const convertMillimetersToTwip = (millimeters: number): number => Math.floor((millimeters / 25.4) * 72 * 20);
@ -24,6 +25,12 @@ export const bookmarkUniqueNumericIdGen = (): UniqueNumericIdCreator => uniqueNu
export const uniqueId = (): string => nanoid().toLowerCase();
export const hashedId = (data: Buffer | string | Uint8Array | ArrayBuffer): string =>
hash
.sha1()
.update(data instanceof ArrayBuffer ? new Uint8Array(data) : data)
.digest("hex");
const generateUuidPart = (count: number): string => customAlphabet("1234567890abcdef", count)();
export const uniqueUuid = (): string =>
`${generateUuidPart(8)}-${generateUuidPart(4)}-${generateUuidPart(4)}-${generateUuidPart(4)}-${generateUuidPart(12)}`;

View File

@ -7,7 +7,9 @@ import { nodePolyfills } from "vite-plugin-node-polyfills";
export default defineConfig({
plugins: [
tsconfigPaths(),
dts(),
dts({
rollupTypes: true
}),
nodePolyfills({
exclude: [],
globals: {
@ -62,10 +64,10 @@ export default defineConfig({
provider: "v8",
reporter: ["text", "json", "html"],
thresholds: {
statements: 99.98,
branches: 99.15,
statements: 100,
branches: 99.35,
functions: 100,
lines: 99.98,
lines: 100,
},
exclude: [
...configDefaults.exclude,