Compare commits

...

55 Commits
9.0.3 ... 9.3.0

Author SHA1 Message Date
a708475539 export IPropertiesOptions for use by other libraries or code using docx library (#2979)
* export IPropertiesOptions for use by other libraries or code using docx library

* fixes #2978: export OutputTypes

* Export file in main index file

* Move type export to inside

---------

Co-authored-by: Dolan Miu <dolan_miu@hotmail.com>
2025-03-15 03:18:43 +00:00
9c60cfcbc7 build(deps): bump the npm_and_yarn group with 12 updates (#2992)
Bumps the npm_and_yarn group with 15 updates:

| Package | From | To |
| --- | --- | --- |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `2.1.8` | `2.1.9` |
| [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) | `2.1.8` | `2.1.9` |
| [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui) | `2.1.8` | `2.1.9` |
| [dompurify](https://github.com/cure53/DOMPurify) | `2.5.7` | `removed` |
| [docsify](https://github.com/docsifyjs/docsify) | `4.12.2` | `4.13.1` |
| [docsify-server-renderer](https://github.com/docsifyjs/docsify) | `4.12.2` | `4.13.1` |
| [elliptic](https://github.com/indutny/elliptic) | `6.6.0` | `6.6.1` |
| [esbuild](https://github.com/evanw/esbuild) | `0.21.5` | `0.25.1` |
| [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) | `2.1.9` | `3.0.8` |
| [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui) | `2.1.9` | `3.0.8` |
| [tsx](https://github.com/privatenumber/tsx) | `4.19.2` | `4.19.3` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `6.0.9` | `6.2.2` |
| [vite-plugin-node-polyfills](https://github.com/davidmyersdev/vite-plugin-node-polyfills) | `0.22.0` | `0.23.0` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `2.1.9` | `3.0.8` |
| [prismjs](https://github.com/PrismJS/prism) | `1.27.0` | `1.30.0` |


Updates `vitest` from 2.1.8 to 2.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v2.1.9/packages/vitest)

Updates `@vitest/coverage-v8` from 2.1.8 to 2.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v2.1.9/packages/coverage-v8)

Updates `@vitest/ui` from 2.1.8 to 2.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v2.1.9/packages/ui)

Removes `dompurify`

Updates `docsify` from 4.12.2 to 4.13.1
- [Release notes](https://github.com/docsifyjs/docsify/releases)
- [Changelog](https://github.com/docsifyjs/docsify/blob/v4.13.1/CHANGELOG.md)
- [Commits](https://github.com/docsifyjs/docsify/compare/v4.12.2...v4.13.1)

Updates `docsify-server-renderer` from 4.12.2 to 4.13.1
- [Release notes](https://github.com/docsifyjs/docsify/releases)
- [Changelog](https://github.com/docsifyjs/docsify/blob/v4.13.1/CHANGELOG.md)
- [Commits](https://github.com/docsifyjs/docsify/compare/v4.12.2...v4.13.1)

Updates `elliptic` from 6.6.0 to 6.6.1
- [Commits](https://github.com/indutny/elliptic/compare/v6.6.0...v6.6.1)

Updates `esbuild` from 0.21.5 to 0.25.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.25.1)

Updates `@vitest/coverage-v8` from 2.1.9 to 3.0.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v2.1.9/packages/coverage-v8)

Updates `@vitest/ui` from 2.1.9 to 3.0.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v2.1.9/packages/ui)

Updates `tsx` from 4.19.2 to 4.19.3
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.19.2...v4.19.3)

Updates `vite` from 6.0.9 to 6.2.2
- [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/v6.2.2/packages/vite)

Updates `vite-plugin-node-polyfills` from 0.22.0 to 0.23.0
- [Release notes](https://github.com/davidmyersdev/vite-plugin-node-polyfills/releases)
- [Commits](https://github.com/davidmyersdev/vite-plugin-node-polyfills/compare/v0.22.0...v0.23.0)

Updates `vitest` from 2.1.9 to 3.0.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v2.1.9/packages/vitest)

Updates `prismjs` from 1.27.0 to 1.30.0
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.27.0...v1.30.0)

---
updated-dependencies:
- dependency-name: vitest
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: "@vitest/coverage-v8"
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: "@vitest/ui"
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: dompurify
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: docsify
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: docsify-server-renderer
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: elliptic
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: esbuild
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: "@vitest/coverage-v8"
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: "@vitest/ui"
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: tsx
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: vite-plugin-node-polyfills
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: vitest
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: prismjs
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-15 03:05:07 +00:00
a5454edc61 Feature/math improve (#2976)
* add new feature

* add test file

* update test file

* update test file

* Fix linting

---------

Co-authored-by: Dolan Miu <dolan_miu@hotmail.com>
2025-03-15 03:04:01 +00:00
7152abfe48 build(deps-dev): bump eslint-plugin-jsdoc from 50.6.0 to 50.6.6 (#2987)
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 50.6.0 to 50.6.6.
- [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/v50.6.0...v50.6.6)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  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>
2025-03-14 23:44:41 +00:00
075eeb7e3c build(deps-dev): bump eslint from 9.16.0 to 9.22.0 (#2983)
Bumps [eslint](https://github.com/eslint/eslint) from 9.16.0 to 9.22.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.16.0...v9.22.0)

---
updated-dependencies:
- dependency-name: eslint
  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>
2025-03-14 23:44:33 +00:00
f0acb3f3fb build(deps-dev): bump inquirer from 12.3.0 to 12.4.3 (#2982)
Bumps [inquirer](https://github.com/SBoudrias/Inquirer.js) from 12.3.0 to 12.4.3.
- [Release notes](https://github.com/SBoudrias/Inquirer.js/releases)
- [Commits](https://github.com/SBoudrias/Inquirer.js/compare/inquirer@12.3.0...inquirer@12.4.3)

---
updated-dependencies:
- dependency-name: inquirer
  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>
2025-03-14 23:44:26 +00:00
ae048833a1 build(deps-dev): bump vite-plugin-dts from 4.3.0 to 4.5.3 (#2973)
Bumps [vite-plugin-dts](https://github.com/qmhc/vite-plugin-dts) from 4.3.0 to 4.5.3.
- [Release notes](https://github.com/qmhc/vite-plugin-dts/releases)
- [Changelog](https://github.com/qmhc/vite-plugin-dts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/qmhc/vite-plugin-dts/compare/v4.3.0...v4.5.3)

---
updated-dependencies:
- dependency-name: vite-plugin-dts
  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>
2025-03-14 23:44:17 +00:00
ebae72004c Fix support for after auto-spacing. (#2975) 2025-03-14 23:44:09 +00:00
13744abff5 build(deps-dev): bump cspell from 8.16.1 to 8.17.5 (#2969)
Bumps [cspell](https://github.com/streetsidesoftware/cspell/tree/HEAD/packages/cspell) from 8.16.1 to 8.17.5.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/packages/cspell/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell/commits/v8.17.5/packages/cspell)

---
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>
2025-03-14 23:43:46 +00:00
492ef29d7f build(deps-dev): bump vite from 6.0.7 to 6.0.9 in the npm_and_yarn group (#2940)
Bumps the npm_and_yarn group with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.0.7 to 6.0.9
- [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/v6.0.9/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-14 23:43:39 +00:00
a6e6463a36 build(deps-dev): bump jiti from 2.4.1 to 2.4.2 (#2932)
Bumps [jiti](https://github.com/unjs/jiti) from 2.4.1 to 2.4.2.
- [Release notes](https://github.com/unjs/jiti/releases)
- [Changelog](https://github.com/unjs/jiti/blob/main/CHANGELOG.md)
- [Commits](https://github.com/unjs/jiti/compare/v2.4.1...v2.4.2)

---
updated-dependencies:
- dependency-name: jiti
  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>
2025-03-14 23:43:32 +00:00
2ef596543b build(deps-dev): bump glob from 11.0.0 to 11.0.1 (#2930)
Bumps [glob](https://github.com/isaacs/node-glob) from 11.0.0 to 11.0.1.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v11.0.0...v11.0.1)

---
updated-dependencies:
- dependency-name: glob
  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>
2025-03-14 23:43:25 +00:00
85005c3c07 build(deps-dev): bump jsdom from 25.0.1 to 26.0.0 (#2928)
Bumps [jsdom](https://github.com/jsdom/jsdom) from 25.0.1 to 26.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/25.0.1...26.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>
2025-03-14 23:43:17 +00:00
0d2b433446 Chore: bump nanoid to fix security issue (#2991) 2025-03-14 23:43:08 +00:00
b1f67652e9 docs: add Playground (Docx.js Editor) url to README.md (#2965) 2025-02-23 20:36:33 +01:00
4e2befb7ef Update docs for new Packer methods (#2961)
To cover #2920
2025-02-17 21:22:07 +00:00
a887927968 Version bump 2025-02-16 18:35:16 +00:00
ee6db1429a Add shading and border settings to paragraph style options. (#2955)
* Add shading and border settings to paragraph style options.

* Fix prettier

---------

Co-authored-by: Dolan Miu <dolan_miu@hotmail.com>
2025-02-16 18:33:21 +00:00
170309a7ed Add Packer.pack and Packer.toArrayBuffer (#2959)
* Add Packer.pack and Packer.toArrayBuffer

To mirror patchDocument's outputType parameter.

See https://github.com/dolanmiu/docx/discussions/2920

* Ignore coverage

---------

Co-authored-by: Dolan Miu <dolan_miu@hotmail.com>
2025-02-16 18:24:15 +00:00
05fcf6edd4 Feat: subfile overrides (#2941)
* Adds overrides parameter to Packer methods and compiler

* Adds tests for overrides parameter

* Update Packer usage examples
2025-01-27 10:39:23 +00:00
eb2174e566 Version bump 2025-01-10 12:31:16 +00:00
2a56360875 build(deps-dev): bump typescript-eslint from 8.16.0 to 8.19.1 (#2921)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.16.0 to 8.19.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.19.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  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>
2025-01-10 11:22:37 +00:00
ba862766a6 build(deps-dev): bump vite from 6.0.1 to 6.0.7 (#2919)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.0.1 to 6.0.7.
- [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/v6.0.7/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  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>
2025-01-10 11:22:29 +00:00
71291dab8f build(deps-dev): bump typedoc from 0.27.3 to 0.27.6 (#2913)
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.27.3 to 0.27.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.27.3...v0.27.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>
2025-01-10 11:22:21 +00:00
974ba7b450 build(deps-dev): bump inquirer from 12.1.0 to 12.3.0 (#2908)
Bumps [inquirer](https://github.com/SBoudrias/Inquirer.js) from 12.1.0 to 12.3.0.
- [Release notes](https://github.com/SBoudrias/Inquirer.js/releases)
- [Commits](https://github.com/SBoudrias/Inquirer.js/compare/inquirer@12.1.0...inquirer@12.3.0)

---
updated-dependencies:
- dependency-name: inquirer
  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>
2025-01-10 11:22:13 +00:00
8f7df39c07 build(deps-dev): bump execa from 9.5.1 to 9.5.2 (#2892)
Bumps [execa](https://github.com/sindresorhus/execa) from 9.5.1 to 9.5.2.
- [Release notes](https://github.com/sindresorhus/execa/releases)
- [Commits](https://github.com/sindresorhus/execa/compare/v9.5.1...v9.5.2)

---
updated-dependencies:
- dependency-name: execa
  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>
2025-01-10 11:22:03 +00:00
ca3d8f0121 build(deps-dev): bump vite-tsconfig-paths from 5.1.3 to 5.1.4 (#2890)
Bumps [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) from 5.1.3 to 5.1.4.
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v5.1.3...v5.1.4)

---
updated-dependencies:
- dependency-name: vite-tsconfig-paths
  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>
2025-01-10 11:21:55 +00:00
46c517d195 build(deps-dev): bump eslint-import-resolver-typescript (#2887)
Bumps [eslint-import-resolver-typescript](https://github.com/import-js/eslint-import-resolver-typescript) from 3.6.3 to 3.7.0.
- [Release notes](https://github.com/import-js/eslint-import-resolver-typescript/releases)
- [Changelog](https://github.com/import-js/eslint-import-resolver-typescript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-import-resolver-typescript/compare/v3.6.3...v3.7.0)

---
updated-dependencies:
- dependency-name: eslint-import-resolver-typescript
  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>
2025-01-10 11:21:43 +00:00
ff37f3b460 fix: Ensure necessary namespaces are in patched doc (#2698)
* refactor: Extract timestamp properties

In preparation for reworking DocumentAttributes.

* refactor: Consolidate namespaces

* fix: Ensure necessary namespaces are in patched doc

Fixes #2697

* Fix tsc and ESLint errors

* Fix CSpell

* Add a test to fix code coverage failure
2025-01-06 22:19:00 +00:00
7b9b474484 Fix: custom fonts support on the Browser (#2898) 2024-12-19 02:08:52 +00:00
e80a50d36c #2846 Type error when importing docx in CJS file with TypeScript modu… (#2883)
* #2846 Type error when importing docx in CJS file with TypeScript moduleResolution set to node16

* Adhere to publint
2024-12-06 13:44:42 +00:00
df99f96469 Chore: Update vitest (#2882)
* Chore: Update vitest

* Fix test
2024-12-05 15:43:30 +00:00
f87ad6a43c build(deps-dev): bump typedoc from 0.26.11 to 0.27.3 (#2879)
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.26.11 to 0.27.3.
- [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.26.11...v0.27.3)

---
updated-dependencies:
- dependency-name: typedoc
  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-12-05 14:04:43 +00:00
8bf5220e31 build(deps-dev): bump prettier from 3.3.3 to 3.4.2 (#2878)
Bumps [prettier](https://github.com/prettier/prettier) from 3.3.3 to 3.4.2.
- [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.3.3...3.4.2)

---
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-12-05 14:04:24 +00:00
da8148251a build(deps-dev): bump eslint from 9.14.0 to 9.16.0 (#2874)
Bumps [eslint](https://github.com/eslint/eslint) from 9.14.0 to 9.16.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.14.0...v9.16.0)

---
updated-dependencies:
- dependency-name: eslint
  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-12-05 14:04:06 +00:00
01183ac34d build(deps-dev): bump jiti from 2.4.0 to 2.4.1 (#2871)
Bumps [jiti](https://github.com/unjs/jiti) from 2.4.0 to 2.4.1.
- [Release notes](https://github.com/unjs/jiti/releases)
- [Changelog](https://github.com/unjs/jiti/blob/main/CHANGELOG.md)
- [Commits](https://github.com/unjs/jiti/compare/v2.4.0...v2.4.1)

---
updated-dependencies:
- dependency-name: jiti
  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-12-05 14:03:46 +00:00
d553e4763a build(deps): bump @types/node from 22.9.0 to 22.10.1 (#2868)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.9.0 to 22.10.1.
- [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-12-05 14:03:36 +00:00
213c3d29f3 build(deps-dev): bump eslint-plugin-unicorn from 56.0.0 to 56.0.1 (#2867)
Bumps [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) from 56.0.0 to 56.0.1.
- [Release notes](https://github.com/sindresorhus/eslint-plugin-unicorn/releases)
- [Commits](https://github.com/sindresorhus/eslint-plugin-unicorn/compare/v56.0.0...v56.0.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-unicorn
  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-12-05 14:03:28 +00:00
697c1d91c6 build(deps): bump nanoid from 5.0.8 to 5.0.9 (#2866)
Bumps [nanoid](https://github.com/ai/nanoid) from 5.0.8 to 5.0.9.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/5.0.8...5.0.9)

---
updated-dependencies:
- dependency-name: nanoid
  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-12-05 14:03:07 +00:00
b22de4ac4f build(deps-dev): bump cspell from 8.16.0 to 8.16.1 (#2865)
Bumps [cspell](https://github.com/streetsidesoftware/cspell/tree/HEAD/packages/cspell) from 8.16.0 to 8.16.1.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/packages/cspell/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell/commits/v8.16.1/packages/cspell)

---
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-12-05 14:02:53 +00:00
0af9d27dcc Chore: Remove @eslint/compat (#2881) 2024-12-05 13:54:43 +00:00
b89b571c4e Fix replacer error on patches with empty Runs (#2875)
* Add test to demonstrate empty strings not creating runs

* Fix Run not creating Text child for empty strings

* Simplify TextRun constructor

Since `Run` already has a option for instantiating a `Text` element, we don't need to push children here.

* Add replacer test for empty runs

* Add replacer test for empty runs

* replacer: handle patches with empty runs

* Fix incorrect test name
2024-12-03 11:25:22 +00:00
64505a295f build(deps-dev): bump eslint-plugin-jsdoc from 50.4.3 to 50.6.0 (#2863)
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 50.4.3 to 50.6.0.
- [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/v50.4.3...v50.6.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  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-11-28 10:47:45 +00:00
c247bbf409 build(deps-dev): bump @vitest/ui from 2.1.4 to 2.1.6 (#2862)
Bumps [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui) from 2.1.4 to 2.1.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v2.1.6/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-11-28 10:47:22 +00:00
4c60e6a0c0 build(deps-dev): bump vite from 5.4.10 to 6.0.1 (#2861)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.10 to 6.0.1.
- [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/create-vite@6.0.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  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-11-28 10:47:13 +00:00
086e5ef184 build(deps-dev): bump typescript-eslint from 8.13.0 to 8.16.0 (#2857)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.13.0 to 8.16.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.16.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  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-11-28 10:46:36 +00:00
7e81f7b368 build(deps-dev): bump vite-tsconfig-paths from 5.1.0 to 5.1.3 (#2850)
Bumps [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) from 5.1.0 to 5.1.3.
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v5.1.0...v5.1.3)

---
updated-dependencies:
- dependency-name: vite-tsconfig-paths
  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-11-28 10:46:26 +00:00
defa431aa9 build(deps-dev): bump inquirer from 12.0.1 to 12.1.0 (#2838)
Bumps [inquirer](https://github.com/SBoudrias/Inquirer.js) from 12.0.1 to 12.1.0.
- [Release notes](https://github.com/SBoudrias/Inquirer.js/releases)
- [Commits](https://github.com/SBoudrias/Inquirer.js/compare/inquirer@12.0.1...inquirer@12.1.0)

---
updated-dependencies:
- dependency-name: inquirer
  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-11-28 10:44:33 +00:00
17eb4fe8c4 Version bump 2024-11-28 10:38:29 +00:00
3997ce538d fix(replacer): errors suppressed by catch statement (#2856) 2024-11-28 10:35:11 +00:00
3654eb0800 New Textbox Component (#2718)
* Adding textbox with no props

* Fixing namings and adding shape style

* simplify usage

* Fix linting issues

* Re-name demo

* Use new shape authoring style

* Add tests

Simplify API

* Add better types for styles

* Add more options to TextBox and add documentation

---------

Co-authored-by: zohar11 <zohar@sumit-ai.com>
Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-11-28 10:33:19 +00:00
c6bb255641 Add hyphenation support (#2678)
* Add hyphenation support

* Remove unneeded linebreaks

* Add documentation and fix eslint

* Add tests

---------

Co-authored-by: Dolan Miu <dolan_miu@hotmail.com>
2024-11-21 10:41:31 +00:00
05a3cf5b43 fix: add rel to fontTable (#2800)
* fix: add rel to fontTable

* Fix prettier

---------

Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-11-20 11:19:43 +00:00
dece0f58e1 Fix duplicated br tags (#2717)
* Adjust test to demo duplicated br tags

* Fix patchDocument duplicating br tags

* Only include w:Pr

* Fix tag condition

---------

Co-authored-by: Dolan <dolan_miu@hotmail.com>
2024-11-20 11:19:25 +00:00
7f3c5615c9 Version bump 2024-11-07 23:46:27 +00:00
63 changed files with 5153 additions and 5480 deletions

View File

@ -8,11 +8,14 @@
// words - list of words to be always considered correct // words - list of words to be always considered correct
"words": [ "words": [
"Abjad", "Abjad",
"aink",
"aiueo", "aiueo",
"ATLEAST", "ATLEAST",
"chosung", "chosung",
"clippy", "clippy",
"datas", "datas",
"dcmitype",
"dcterms",
"docsify", "docsify",
"dolan", "dolan",
"execa", "execa",
@ -32,6 +35,7 @@
"panose", "panose",
"rels", "rels",
"rsid", "rsid",
"sdtdh",
"twip", "twip",
"twips", "twips",
"Xmlable", "Xmlable",

View File

@ -1,243 +0,0 @@
extends: eslint:recommended
env:
browser: true
es6: true
node: true
parser: "@typescript-eslint/parser"
parserOptions:
project:
- tsconfig.json
sourceType: module
plugins:
- eslint-plugin-import
- eslint-plugin-no-null
- eslint-plugin-unicorn
- eslint-plugin-jsdoc
- eslint-plugin-prefer-arrow
- "@typescript-eslint"
- eslint-plugin-functional
root: true
rules:
no-undef: "off"
no-extra-boolean-cast: "off"
no-alert: error
no-self-compare: error
no-unreachable-loop: error
no-template-curly-in-string: error
no-unused-private-class-members: error
no-extend-native: error
no-floating-decimal: error
no-implied-eval: error
no-iterator: error
no-lone-blocks: error
no-loop-func: error
no-new-object: error
no-proto: error
no-useless-catch: error
one-var-declaration-per-line: error
prefer-arrow-callback: error
prefer-destructuring: error
prefer-exponentiation-operator: error
prefer-promise-reject-errors: error
prefer-regex-literals: error
prefer-spread: error
prefer-template: error
require-await: error
"@typescript-eslint/adjacent-overload-signatures": error
"@typescript-eslint/array-type":
- error
- default: array
"@typescript-eslint/no-restricted-types":
- error
- types:
Object:
message: Avoid using the `Object` type. Did you mean `object`?
fixWith: object
Function:
message: >-
Avoid using the `Function` type. Prefer a specific function type,
like `() => void`.
Boolean:
message: Avoid using the `Boolean` type. Did you mean `boolean`?
fixWith: boolean
Number:
message: Avoid using the `Number` type. Did you mean `number`?
fixWith: number
String:
message: Avoid using the `String` type. Did you mean `string`?
fixWith: string
Symbol:
message: Avoid using the `Symbol` type. Did you mean `symbol`?
fixWith: symbol
"@typescript-eslint/consistent-type-assertions": error
"@typescript-eslint/dot-notation": error
"@typescript-eslint/explicit-function-return-type":
- error
- allowExpressions: true
allowTypedFunctionExpressions: true
allowHigherOrderFunctions: false
allowDirectConstAssertionInArrowFunctions: true
allowConciseArrowFunctionExpressionsStartingWithVoid: true
"@typescript-eslint/explicit-member-accessibility":
- error
- accessibility: explicit
overrides:
accessors: explicit
"@typescript-eslint/explicit-module-boundary-types":
- error
- allowArgumentsExplicitlyTypedAsAny: true
allowDirectConstAssertionInArrowFunctions: true
allowHigherOrderFunctions: false
allowTypedFunctionExpressions: false
"@typescript-eslint/naming-convention":
- error
- selector:
- objectLiteralProperty
leadingUnderscore: allow
format:
- camelCase
- PascalCase
- UPPER_CASE # for constants
filter:
regex: (^[a-z]+:.+)|_attr|[0-9]
match: false
"@typescript-eslint/no-empty-function": error
"@typescript-eslint/no-empty-interface": error
"@typescript-eslint/no-explicit-any": error
"@typescript-eslint/no-misused-new": error
"@typescript-eslint/no-namespace": error
"@typescript-eslint/no-parameter-properties": "off"
"@typescript-eslint/no-require-imports": error
"@typescript-eslint/no-shadow":
- error
- hoist: all
"@typescript-eslint/no-this-alias": error
"@typescript-eslint/no-unused-expressions": error
"@typescript-eslint/no-use-before-define": "off"
"@typescript-eslint/no-var-requires": error
"@typescript-eslint/prefer-for-of": error
"@typescript-eslint/prefer-function-type": error
"@typescript-eslint/prefer-namespace-keyword": error
"@typescript-eslint/prefer-readonly": error
"@typescript-eslint/triple-slash-reference":
- error
- path: always
types: prefer-import
lib: always
"@typescript-eslint/typedef":
- error
- parameter: true
propertyDeclaration: true
"@typescript-eslint/unified-signatures": error
arrow-body-style: error
complexity: "off"
consistent-return: error
constructor-super: error
curly: error
dot-notation: "off"
eqeqeq:
- error
- smart
guard-for-in: error
id-denylist:
- error
- any
- Number
- number
- String
- string
- Boolean
- boolean
- Undefined
- undefined
id-match: error
import/no-default-export: error
import/no-extraneous-dependencies: "off"
import/no-internal-modules: "off"
import/order: error
indent: "off"
jsdoc/check-alignment: error
jsdoc/check-indentation: "off"
max-classes-per-file: "off"
max-len: "off"
new-parens: error
no-bitwise: error
no-caller: error
no-cond-assign: error
no-console: error
no-debugger: error
no-duplicate-case: error
no-duplicate-imports: error
no-empty: error
no-empty-function: "off"
no-eval: error
no-extra-bind: error
no-fallthrough: "off"
no-invalid-this: "off"
no-multiple-empty-lines: error
no-new-func: error
no-new-wrappers: error
no-null/no-null: error
no-param-reassign: error
no-redeclare: error
no-return-await: error
no-sequences: error
no-shadow: "off"
no-sparse-arrays: error
no-throw-literal: error
no-trailing-spaces: error
no-undef-init: error
no-underscore-dangle:
- error
- allow:
- _attr
no-unsafe-finally: error
no-unused-expressions: "off"
no-unused-labels: error
no-use-before-define: "off"
no-useless-constructor: error
no-var: error
object-shorthand: "off"
one-var:
- error
- never
prefer-arrow/prefer-arrow-functions: error
prefer-const: error
prefer-object-spread: error
radix: error
space-in-parens:
- error
- never
spaced-comment:
- error
- always
- markers:
- /
unicorn/filename-case: error
unicorn/prefer-ternary: error
use-isnan: error
valid-typeof: "off"
functional/immutable-data:
- error
- ignoreImmediateMutation: true
ignoreAccessorPattern:
- "**.root*"
- "**.numberingReferences*"
- "**.sections*"
- "**.properties*"
functional/prefer-property-signatures: error
functional/no-mixed-types: error
functional/prefer-readonly-type: error
no-unused-vars:
- error
- argsIgnorePattern: ^[_]+$
ignorePatterns:
- vite.config.ts
overrides:
- files:
- "*.spec.ts"
rules:
"@typescript-eslint/no-unused-expressions": "off"
"@typescript-eslint/dot-notation": "off"
prefer-destructuring: "off"
"@typescript-eslint/explicit-function-return-type": "off"

View File

@ -65,5 +65,5 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Dependencies - name: Install Dependencies
run: npm ci --force run: npm ci --force
- name: Prettier - name: CSpell
run: npm run cspell run: npm run cspell

3
.gitignore vendored
View File

@ -33,8 +33,7 @@ node_modules
.node_repl_history .node_repl_history
# build # build
build dist
build-tests
# Documentation # Documentation
docs/api/ docs/api/

View File

@ -14,6 +14,7 @@
[![Known Vulnerabilities][snky-image]][snky-url] [![Known Vulnerabilities][snky-image]][snky-url]
[![PRs Welcome][pr-image]][pr-url] [![PRs Welcome][pr-image]][pr-url]
[![codecov][codecov-image]][codecov-url] [![codecov][codecov-image]][codecov-url]
[![Docx.js Editor][docxjs-editor-image]][docxjs-editor-url]
<p align="center"> <p align="center">
<img src="https://i.imgur.com/QeL1HuU.png" alt="drawing"/> <img src="https://i.imgur.com/QeL1HuU.png" alt="drawing"/>
@ -64,6 +65,10 @@ More [here](https://github.com/dolanmiu/docx/tree/master/demo)
Please refer to the [documentation at https://docx.js.org/](https://docx.js.org/) for details on how to use this library, examples and much more! Please refer to the [documentation at https://docx.js.org/](https://docx.js.org/) for details on how to use this library, examples and much more!
# Playground
Experience `docx` in action through [Docx.js Editor][docxjs-editor-url], an interactive playground where you can code and preview the results in real-time.
# Examples # Examples
Check the [demo folder](https://github.com/dolanmiu/docx/tree/master/demo) for examples. Check the [demo folder](https://github.com/dolanmiu/docx/tree/master/demo) for examples.
@ -115,3 +120,5 @@ Made with 💖
[patreon-url]: https://www.patreon.com/dolanmiu [patreon-url]: https://www.patreon.com/dolanmiu
[browserstack-image]: https://user-images.githubusercontent.com/2917613/54233552-128e9d00-4505-11e9-88fb-025a4e04007c.png [browserstack-image]: https://user-images.githubusercontent.com/2917613/54233552-128e9d00-4505-11e9-88fb-025a4e04007c.png
[browserstack-url]: https://www.browserstack.com [browserstack-url]: https://www.browserstack.com
[docxjs-editor-image]: https://img.shields.io/badge/Docx.js%20Editor-2b579a.svg?style=flat&amp;logo=javascript&amp;logoColor=white
[docxjs-editor-url]: https://docxjs-editor.vercel.app/

43
demo/94-texbox.ts Normal file
View File

@ -0,0 +1,43 @@
// Simple example to add textbox to a document
import { Document, Packer, Paragraph, Textbox, TextRun } from "docx";
import * as fs from "fs";
const doc = new Document({
sections: [
{
properties: {},
children: [
new Textbox({
alignment: "center",
children: [
new Paragraph({
children: [new TextRun("Hi i'm a textbox!")],
}),
],
style: {
width: "200pt",
height: "auto",
},
}),
new Textbox({
alignment: "center",
children: [
new Paragraph({
children: [new TextRun("Hi i'm a textbox with a hidden box!")],
}),
],
style: {
width: "300pt",
height: 400,
visibility: "hidden",
zIndex: "auto",
},
}),
],
},
],
});
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});

View File

@ -0,0 +1,60 @@
import * as fs from "fs";
import { BorderStyle, Document, Packer, Paragraph, TextRun } from "docx";
const doc = new Document({
styles: {
paragraphStyles: [
{
id: "withSingleBlackBordersAndYellowShading",
name: "Paragraph Style with Black Borders and Yellow Shading",
basedOn: "Normal",
paragraph: {
shading: {
color: "#fff000",
type: "solid",
},
border: {
top: {
style: BorderStyle.SINGLE,
color: "#000000",
size: 4,
},
bottom: {
style: BorderStyle.SINGLE,
color: "#000000",
size: 4,
},
left: {
style: BorderStyle.SINGLE,
color: "#000000",
size: 4,
},
right: {
style: BorderStyle.SINGLE,
color: "#000000",
size: 4,
},
},
},
},
],
},
sections: [
{
children: [
new Paragraph({
style: "withSingleBlackBordersAndYellowShading",
children: [
new TextRun({
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
}),
],
}),
],
},
],
});
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});

View File

@ -22,19 +22,30 @@ const doc = new docx.Document({
### Full list of options: ### Full list of options:
- creator | Property | Type | Notes |
- description | -------------------------- | -------------------------------------------------------- | -------- |
- title | sections | `ISectionOptions[]` | Optional |
- subject | title | `string` | Optional |
- keywords | subject | `string` | Optional |
- lastModifiedBy | creator | `string` | Optional |
- revision | keywords | `string` | Optional |
- externalStyles | description | `string` | Optional |
- styles | lastModifiedBy | `string` | Optional |
- numbering | revision | `number` | Optional |
- footnotes | externalStyles | `string` | Optional |
- hyperlinks | styles | `IStylesOptions` | Optional |
- background | numbering | `INumberingOptions` | Optional |
| comments | `ICommentsOptions` | Optional |
| footnotes | `Record<string, { children: Paragraph[] }>` | Optional |
| background | `IDocumentBackgroundOptions` | Optional |
| features | `{ trackRevisions?: boolean; updateFields?: boolean; }` | Optional |
| compatabilityModeVersion | `number` | Optional |
| compatibility | `ICompatibilityOptions` | Optional |
| customProperties | ` ICustomPropertyOptions`[] | Optional |
| evenAndOddHeaderAndFooters | `boolean` | Optional |
| defaultTabStop | `number` | Optional |
| fonts | ` FontOptions[]` | Optional |
| hyphenation | `IHyphenationOptions` | Optional |
### Change background color of Document ### Change background color of Document

View File

@ -2,7 +2,7 @@
> Packers are the way in which `docx` turns your code into `.docx` format. It is completely decoupled from the `docx.Document`. > Packers are the way in which `docx` turns your code into `.docx` format. It is completely decoupled from the `docx.Document`.
Packers works in both a node and browser environment (Angular etc). Now, the packer returns a `Buffer`, `Blob` or `base64 string`. It is up to you to take that and persist it with node's `fs`, send it down as a downloadable file, or anything else you wish. As of `version 4+`, this library will not have options to export to PDF. Packers works in both a node and browser environment (Angular etc). Now, the packer returns a `Buffer`, `Blob`, `string`, `base64 string`, `ArrayBuffer`, or `Stream`. It is up to you to take that and persist it with node's `fs`, send it down as a downloadable file, or anything else you wish. As of `version 4+`, this library will not have options to export to PDF.
### Export as Buffer ### Export as Buffer
@ -14,6 +14,14 @@ Packer.toBuffer(doc).then((buffer) => {
}); });
``` ```
### Export as string
```ts
Packer.toString(doc).then((string) => {
console.log(string);
});
```
### Export as a `base64` string ### Export as a `base64` string
```ts ```ts
@ -32,3 +40,46 @@ Packer.toBlob(doc).then((blob) => {
saveAs(blob, "example.docx"); saveAs(blob, "example.docx");
}); });
``` ```
### Export as ArrayBuffer
This may be useful when working in a Node.js worker.
```ts
Packer.toArrayBuffer(doc).then((arrayBuffer) => {
port.postMessage(arrayBuffer, [arrayBuffer]);
});
```
### Export as a Stream
```ts
Packer.toStream(doc).then((stream) => {
// read from stream
});
```
### Export using optional arguments
The `Packer` methods support 2 optional arguments.
The first is for controlling the indentation of the xml and should be a `boolean` or `keyof typeof PrettifyType`.
The second is an array of subfile overrides (`{path: string, data: string}[]`). These overrides can be used to write additional subfiles to the result or even override default subfiles in the case that the default handling of these subfiles does not meet your needs.
```ts
const overrides = [{ path: "word/commentsExtended.xml", data: "string_data" }];
Packer.toString(doc, true, overrides).then((string) => {
console.log(string);
});
```
### Export to arbitrary formats
You can also use the lower-level `Packer.pack` method to export to any specified type.
```ts
Packer.pack(doc, 'string').then((string) => {
console.log(string);
});
```

26
docs/usage/text-box.md Normal file
View File

@ -0,0 +1,26 @@
# Text Box
Similar `Text Frames`, but the difference being that it is `VML` `Shape` based.
!> `Text Boxes` requires an understanding of [Paragraphs](usage/paragraph.md).
> `Text boxes` are paragraphs of text in a document which are positioned in a separate region or frame in the document, and can be positioned with a specific size and position relative to non-frame paragraphs in the current document.
## Intro
To make a `Text Box`, simply create a `Textbox` object inside the `Document`:
```ts
new Textbox({
alignment: "center",
children: [
new Paragraph({
children: [new TextRun("Hi i'm a textbox!")],
}),
],
style: {
width: "200pt",
height: "auto",
},
});
```

View File

@ -1,6 +1,6 @@
# Text Frames # Text Frames
Also known as `Text Boxes` > Similar to `Text Boxes`!
!> Text Frames requires an understanding of [Paragraphs](usage/paragraph.md). !> Text Frames requires an understanding of [Paragraphs](usage/paragraph.md).

View File

@ -10,7 +10,7 @@ import tsEslint from "typescript-eslint";
const config: Linter.Config<Linter.RulesRecord>[] = [ const config: Linter.Config<Linter.RulesRecord>[] = [
{ {
ignores: ["**/vite.config.ts", "**/build/**", "**/coverage/**", "**/*.js", "eslint.config.ts", "**/demo/**", "**/scripts/**"], ignores: ["**/vite.config.ts", "**/dist/**", "**/coverage/**", "**/*.js", "eslint.config.ts", "**/demo/**", "**/scripts/**"],
}, },
eslint.configs.recommended, eslint.configs.recommended,
importPlugin.flatConfigs.recommended, importPlugin.flatConfigs.recommended,

8634
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,25 @@
{ {
"name": "docx", "name": "docx",
"version": "9.0.2", "version": "9.2.0",
"description": "Easily generate .docx files with JS/TS with a nice declarative API. Works for Node and on the Browser.", "description": "Easily generate .docx files with JS/TS with a nice declarative API. Works for Node and on the Browser.",
"type": "module", "type": "module",
"main": "build/index.umd.js", "main": "dist/index.umd.cjs",
"module": "./build/index.mjs", "module": "./dist/index.mjs",
"types": "./build/index.d.ts", "types": "./dist/index.d.ts",
"exports": { "exports": {
".": { ".": {
"require": "./build/index.cjs", "import": {
"types": "./build/index.d.ts", "types": "./dist/index.d.ts",
"import": "./build/index.mjs", "default": "./dist/index.mjs"
"default": "./build/index.mjs" },
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
} }
}, },
"files": [ "files": [
"build" "dist"
], ],
"scripts": { "scripts": {
"build": "tsc && vite build", "build": "tsc && vite build",
@ -57,7 +61,7 @@
"@types/node": "^22.7.5", "@types/node": "^22.7.5",
"hash.js": "^1.1.7", "hash.js": "^1.1.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"nanoid": "^5.0.4", "nanoid": "^5.1.3",
"xml": "^1.0.1", "xml": "^1.0.1",
"xml-js": "^1.6.8" "xml-js": "^1.6.8"
}, },
@ -68,7 +72,6 @@
}, },
"homepage": "https://docx.js.org", "homepage": "https://docx.js.org",
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.2.1",
"@types/eslint__js": "^8.42.3", "@types/eslint__js": "^8.42.3",
"@types/inquirer": "^9.0.3", "@types/inquirer": "^9.0.3",
"@types/prompt": "^1.1.1", "@types/prompt": "^1.1.1",
@ -76,8 +79,8 @@
"@types/xml": "^1.0.8", "@types/xml": "^1.0.8",
"@typescript-eslint/eslint-plugin": "^8.8.1", "@typescript-eslint/eslint-plugin": "^8.8.1",
"@typescript-eslint/parser": "^8.8.1", "@typescript-eslint/parser": "^8.8.1",
"@vitest/coverage-v8": "^1.1.0", "@vitest/coverage-v8": "^3.0.8",
"@vitest/ui": "^2.1.2", "@vitest/ui": "^3.0.8",
"cspell": "^8.2.3", "cspell": "^8.2.3",
"docsify-cli": "^4.3.0", "docsify-cli": "^4.3.0",
"eslint": "^9.13.0", "eslint": "^9.13.0",
@ -92,20 +95,20 @@
"glob": "^11.0.0", "glob": "^11.0.0",
"inquirer": "^12.0.0", "inquirer": "^12.0.0",
"jiti": "^2.3.3", "jiti": "^2.3.3",
"jsdom": "^25.0.1", "jsdom": "^26.0.0",
"pre-commit": "^1.2.2", "pre-commit": "^1.2.2",
"prettier": "^3.1.1", "prettier": "^3.1.1",
"tsconfig-paths": "^4.0.0", "tsconfig-paths": "^4.0.0",
"tsx": "^4.7.0", "tsx": "^4.7.0",
"typedoc": "^0.26.9", "typedoc": "^0.27.3",
"typescript": "5.3.3", "typescript": "5.3.3",
"typescript-eslint": "^8.10.0", "typescript-eslint": "^8.10.0",
"unzipper": "^0.12.3", "unzipper": "^0.12.3",
"vite": "^5.0.10", "vite": "^6.0.1",
"vite-plugin-dts": "^4.2.4", "vite-plugin-dts": "^4.2.4",
"vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-node-polyfills": "^0.23.0",
"vite-tsconfig-paths": "^5.0.1", "vite-tsconfig-paths": "^5.0.1",
"vitest": "^1.1.0" "vitest": "^3.0.8"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=10"

View File

@ -112,6 +112,41 @@ describe("Compiler", () => {
}, },
); );
it(
"should pack subfile overrides",
async () => {
const file = new File({
sections: [],
comments: {
children: [],
},
});
const subfileData1 = "comments";
const subfileData2 = "commentsExtended";
const overrides = [
{ path: "word/comments.xml", data: subfileData1 },
{ path: "word/commentsExtended.xml", data: subfileData2 },
];
const zipFile = compiler.compile(file, "", overrides);
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
expect(fileNames).is.an.instanceof(Array);
expect(fileNames).has.length(20);
expect(fileNames).to.include("word/comments.xml");
expect(fileNames).to.include("word/commentsExtended.xml");
const commentsText = await zipFile.file("word/comments.xml")?.async("text");
const commentsExtendedText = await zipFile.file("word/commentsExtended.xml")?.async("text");
expect(commentsText).toBe(subfileData1);
expect(commentsExtendedText).toBe(subfileData2);
},
{
timeout: 99999999,
},
);
it("should call the format method X times equalling X files to be formatted", () => { it("should call the format method X times equalling X files to be formatted", () => {
// This test is required because before, there was a case where Document was formatted twice, which was inefficient // This test is required because before, there was a case where Document was formatted twice, which was inefficient
// This also caused issues such as running prepForXml multiple times as format() was ran multiple times. // This also caused issues such as running prepForXml multiple times as format() was ran multiple times.

View File

@ -9,7 +9,7 @@ import { ImageReplacer } from "./image-replacer";
import { NumberingReplacer } from "./numbering-replacer"; import { NumberingReplacer } from "./numbering-replacer";
import { PrettifyType } from "./packer"; import { PrettifyType } from "./packer";
type IXmlifyedFile = { export type IXmlifyedFile = {
readonly data: string; readonly data: string;
readonly path: string; readonly path: string;
}; };
@ -47,7 +47,11 @@ export class Compiler {
this.numberingReplacer = new NumberingReplacer(); this.numberingReplacer = new NumberingReplacer();
} }
public compile(file: File, prettifyXml?: (typeof PrettifyType)[keyof typeof PrettifyType]): JSZip { public compile(
file: File,
prettifyXml?: (typeof PrettifyType)[keyof typeof PrettifyType],
overrides: readonly IXmlifyedFile[] = [],
): JSZip {
const zip = new JSZip(); const zip = new JSZip();
const xmlifiedFileMapping = this.xmlifyFile(file, prettifyXml); const xmlifiedFileMapping = this.xmlifyFile(file, prettifyXml);
const map = new Map<string, IXmlifyedFile | readonly IXmlifyedFile[]>(Object.entries(xmlifiedFileMapping)); const map = new Map<string, IXmlifyedFile | readonly IXmlifyedFile[]>(Object.entries(xmlifiedFileMapping));
@ -62,6 +66,10 @@ export class Compiler {
} }
} }
for (const subFile of overrides) {
zip.file(subFile.path, subFile.data);
}
for (const data of file.Media.Array) { for (const data of file.Media.Array) {
if (data.type !== "svg") { if (data.type !== "svg") {
zip.file(`word/media/${data.fileName}`, data.data); zip.file(`word/media/${data.fileName}`, data.data);
@ -109,6 +117,12 @@ export class Compiler {
); );
}); });
file.Document.Relationships.createRelationship(
file.Document.Relationships.RelationshipCount + 1,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable",
"fontTable.xml",
);
return xml( return xml(
this.formatter.format(file.Document.Relationships, { this.formatter.format(file.Document.Relationships, {
viewWrapper: file.Document, viewWrapper: file.Document,

View File

@ -46,7 +46,7 @@ describe("Packer", () => {
await Packer.toString(file, true); await Packer.toString(file, true);
expect(spy).toBeCalledWith(expect.anything(), PrettifyType.WITH_2_BLANKS); expect(spy).toBeCalledWith(expect.anything(), PrettifyType.WITH_2_BLANKS, expect.anything());
}); });
it("should use a prettify value", async () => { it("should use a prettify value", async () => {
@ -55,7 +55,7 @@ describe("Packer", () => {
await Packer.toString(file, PrettifyType.WITH_4_BLANKS); await Packer.toString(file, PrettifyType.WITH_4_BLANKS);
expect(spy).toBeCalledWith(expect.anything(), PrettifyType.WITH_4_BLANKS); expect(spy).toBeCalledWith(expect.anything(), PrettifyType.WITH_4_BLANKS, expect.anything());
}); });
it("should use an undefined prettify value", async () => { it("should use an undefined prettify value", async () => {
@ -64,7 +64,32 @@ describe("Packer", () => {
await Packer.toString(file, false); await Packer.toString(file, false);
expect(spy).toBeCalledWith(expect.anything(), undefined); expect(spy).toBeCalledWith(expect.anything(), undefined, expect.anything());
});
});
describe("overrides", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("should use an overrides value", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const spy = vi.spyOn((Packer as any).compiler, "compile");
const overrides = [{ path: "word/comments.xml", data: "comments" }];
await Packer.toString(file, true, overrides);
expect(spy).toBeCalledWith(expect.anything(), expect.anything(), overrides);
});
it("should use a default overrides value", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const spy = vi.spyOn((Packer as any).compiler, "compile");
await Packer.toString(file);
expect(spy).toBeCalledWith(expect.anything(), undefined, []);
}); });
}); });
@ -162,6 +187,33 @@ describe("Packer", () => {
}); });
}); });
describe("#toArrayBuffer()", () => {
it("should create a standard docx file", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.spyOn((Packer as any).compiler, "compile").mockReturnValue({
generateAsync: () => vi.fn(),
});
const str = await Packer.toArrayBuffer(file);
assert.isDefined(str);
});
it("should handle exception if it throws any", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.spyOn((Packer as any).compiler, "compile").mockImplementation(() => {
throw new Error();
});
return Packer.toArrayBuffer(file).catch((error) => {
assert.isDefined(error);
});
});
afterEach(() => {
vi.resetAllMocks();
});
});
describe("#toStream()", () => { describe("#toStream()", () => {
it("should create a standard docx file", async () => { it("should create a standard docx file", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -1,8 +1,9 @@
import { Stream } from "stream"; import { Stream } from "stream";
import { File } from "@file/file"; import { File } from "@file/file";
import { OutputByType, OutputType } from "@util/output-type";
import { Compiler } from "./next-compiler"; import { Compiler, IXmlifyedFile } from "./next-compiler";
/** /**
* Use blanks to prettify * Use blanks to prettify
@ -21,53 +22,68 @@ const convertPrettifyType = (
prettify === true ? PrettifyType.WITH_2_BLANKS : prettify === false ? undefined : prettify; prettify === true ? PrettifyType.WITH_2_BLANKS : prettify === false ? undefined : prettify;
export class Packer { export class Packer {
public static async toString(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Promise<string> { // eslint-disable-next-line require-await
const zip = this.compiler.compile(file, convertPrettifyType(prettify)); public static async pack<T extends OutputType>(
const zipData = await zip.generateAsync({ file: File,
type: "string", type: T,
prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
overrides: readonly IXmlifyedFile[] = [],
): Promise<OutputByType[T]> {
const zip = this.compiler.compile(file, convertPrettifyType(prettify), overrides);
return zip.generateAsync({
type,
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
compression: "DEFLATE", compression: "DEFLATE",
}); });
return zipData;
} }
public static async toBuffer(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Promise<Buffer> { public static toString(
const zip = this.compiler.compile(file, convertPrettifyType(prettify)); file: File,
const zipData = await zip.generateAsync({ prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
type: "nodebuffer", overrides: readonly IXmlifyedFile[] = [],
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ): Promise<string> {
compression: "DEFLATE", return Packer.pack(file, "string", prettify, overrides);
});
return zipData;
} }
public static async toBase64String(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Promise<string> { public static toBuffer(
const zip = this.compiler.compile(file, convertPrettifyType(prettify)); file: File,
const zipData = await zip.generateAsync({ prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
type: "base64", overrides: readonly IXmlifyedFile[] = [],
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ): Promise<Buffer> {
compression: "DEFLATE", return Packer.pack(file, "nodebuffer", prettify, overrides);
});
return zipData;
} }
public static async toBlob(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Promise<Blob> { public static toBase64String(
const zip = this.compiler.compile(file, convertPrettifyType(prettify)); file: File,
const zipData = await zip.generateAsync({ prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
type: "blob", overrides: readonly IXmlifyedFile[] = [],
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ): Promise<string> {
compression: "DEFLATE", return Packer.pack(file, "base64", prettify, overrides);
});
return zipData;
} }
public static toStream(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Stream { public static toBlob(
file: File,
prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
overrides: readonly IXmlifyedFile[] = [],
): Promise<Blob> {
return Packer.pack(file, "blob", prettify, overrides);
}
public static toArrayBuffer(
file: File,
prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
overrides: readonly IXmlifyedFile[] = [],
): Promise<ArrayBuffer> {
return Packer.pack(file, "arraybuffer", prettify, overrides);
}
public static toStream(
file: File,
prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
overrides: readonly IXmlifyedFile[] = [],
): Stream {
const stream = new Stream(); const stream = new Stream();
const zip = this.compiler.compile(file, convertPrettifyType(prettify)); const zip = this.compiler.compile(file, convertPrettifyType(prettify), overrides);
zip.generateAsync({ zip.generateAsync({
type: "nodebuffer", type: "nodebuffer",

View File

@ -1,7 +1,8 @@
import { FontOptions } from "@file/fonts/font-table"; import { FontOptions } from "@file/fonts/font-table";
import { ICommentsOptions } from "@file/paragraph/run/comment-run"; import { ICommentsOptions } from "@file/paragraph/run/comment-run";
import { IHyphenationOptions } from "@file/settings";
import { ICompatibilityOptions } from "@file/settings/compatibility"; import { ICompatibilityOptions } from "@file/settings/compatibility";
import { StringContainer, XmlComponent } from "@file/xml-components"; import { StringContainer, XmlAttributeComponent, XmlComponent } from "@file/xml-components";
import { dateTimeValue } from "@util/values"; import { dateTimeValue } from "@util/values";
import { ICustomPropertyOptions } from "../custom-properties"; import { ICustomPropertyOptions } from "../custom-properties";
@ -44,6 +45,7 @@ export type IPropertiesOptions = {
readonly evenAndOddHeaderAndFooters?: boolean; readonly evenAndOddHeaderAndFooters?: boolean;
readonly defaultTabStop?: number; readonly defaultTabStop?: number;
readonly fonts?: readonly FontOptions[]; readonly fonts?: readonly FontOptions[];
readonly hyphenation?: IHyphenationOptions;
}; };
// <xs:element name="coreProperties" type="CT_CoreProperties"/> // <xs:element name="coreProperties" type="CT_CoreProperties"/>
@ -73,15 +75,7 @@ export type IPropertiesOptions = {
export class CoreProperties extends XmlComponent { export class CoreProperties extends XmlComponent {
public constructor(options: Omit<IPropertiesOptions, "sections">) { public constructor(options: Omit<IPropertiesOptions, "sections">) {
super("cp:coreProperties"); super("cp:coreProperties");
this.root.push( this.root.push(new DocumentAttributes(["cp", "dc", "dcterms", "dcmitype", "xsi"]));
new DocumentAttributes({
cp: "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
dc: "http://purl.org/dc/elements/1.1/",
dcterms: "http://purl.org/dc/terms/",
dcmitype: "http://purl.org/dc/dcmitype/",
xsi: "http://www.w3.org/2001/XMLSchema-instance",
}),
);
if (options.title) { if (options.title) {
this.root.push(new StringContainer("dc:title", options.title)); this.root.push(new StringContainer("dc:title", options.title));
} }
@ -108,11 +102,15 @@ export class CoreProperties extends XmlComponent {
} }
} }
class TimestampElementProperties extends XmlAttributeComponent<{ readonly type: string }> {
protected readonly xmlKeys = { type: "xsi:type" };
}
class TimestampElement extends XmlComponent { class TimestampElement extends XmlComponent {
public constructor(name: string) { public constructor(name: string) {
super(name); super(name);
this.root.push( this.root.push(
new DocumentAttributes({ new TimestampElementProperties({
type: "dcterms:W3CDTF", type: "dcterms:W3CDTF",
}), }),
); );

View File

@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { Formatter } from "@export/formatter";
import { createLineNumberType } from "./line-number";
describe("createLineNumberType", () => {
it("should work", () => {
const textDirection = createLineNumberType({ countBy: 0, start: 0, restart: "newPage", distance: 10 });
const tree = new Formatter().format(textDirection);
expect(tree).to.deep.equal({
"w:lnNumType": { _attr: { "w:countBy": 0, "w:start": 0, "w:restart": "newPage", "w:distance": 10 } },
});
});
it("should work with string measures for distance", () => {
const textDirection = createLineNumberType({ countBy: 0, start: 0, restart: "newPage", distance: "10mm" });
const tree = new Formatter().format(textDirection);
expect(tree).to.deep.equal({
"w:lnNumType": { _attr: { "w:countBy": 0, "w:start": 0, "w:restart": "newPage", "w:distance": "10mm" } },
});
});
it("should work with blank entries", () => {
const textDirection = createLineNumberType({});
const tree = new Formatter().format(textDirection);
expect(tree).to.deep.equal({
"w:lnNumType": { _attr: {} },
});
});
});

View File

@ -1,89 +1,60 @@
import { XmlAttributeComponent } from "@file/xml-components"; import { AttributeMap, XmlAttributeComponent } from "@file/xml-components";
/* cSpell:disable */ /* cSpell:disable */
export type IDocumentAttributesProperties = { export const DocumentAttributeNamespaces = {
readonly wpc?: string; wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas",
readonly mc?: string; mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
readonly o?: string; o: "urn:schemas-microsoft-com:office:office",
readonly r?: string; r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
readonly m?: string; m: "http://schemas.openxmlformats.org/officeDocument/2006/math",
readonly v?: string; v: "urn:schemas-microsoft-com:vml",
readonly wp14?: string; wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
readonly wp?: string; wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
readonly w10?: string; w10: "urn:schemas-microsoft-com:office:word",
readonly w?: string; w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
readonly w14?: string; w14: "http://schemas.microsoft.com/office/word/2010/wordml",
readonly w15?: string; w15: "http://schemas.microsoft.com/office/word/2012/wordml",
readonly wpg?: string; wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
readonly wpi?: string; wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
readonly wne?: string; wne: "http://schemas.microsoft.com/office/word/2006/wordml",
readonly wps?: string; wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
readonly Ignorable?: string; cp: "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
readonly cp?: string; dc: "http://purl.org/dc/elements/1.1/",
readonly dc?: string; dcterms: "http://purl.org/dc/terms/",
readonly dcterms?: string; dcmitype: "http://purl.org/dc/dcmitype/",
readonly dcmitype?: string; xsi: "http://www.w3.org/2001/XMLSchema-instance",
readonly xsi?: string; cx: "http://schemas.microsoft.com/office/drawing/2014/chartex",
readonly type?: string; cx1: "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex",
readonly cx?: string; cx2: "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex",
readonly cx1?: string; cx3: "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex",
readonly cx2?: string; cx4: "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex",
readonly cx3?: string; cx5: "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex",
readonly cx4?: string; cx6: "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex",
readonly cx5?: string; cx7: "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex",
readonly cx6?: string; cx8: "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex",
readonly cx7?: string; aink: "http://schemas.microsoft.com/office/drawing/2016/ink",
readonly cx8?: string; am3d: "http://schemas.microsoft.com/office/drawing/2017/model3d",
readonly aink?: string; w16cex: "http://schemas.microsoft.com/office/word/2018/wordml/cex",
readonly am3d?: string; w16cid: "http://schemas.microsoft.com/office/word/2016/wordml/cid",
readonly w16cex?: string; w16: "http://schemas.microsoft.com/office/word/2018/wordml",
readonly w16cid?: string; w16sdtdh: "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash",
readonly w16?: string; w16se: "http://schemas.microsoft.com/office/word/2015/wordml/symex",
readonly w16sdtdh?: string;
readonly w16se?: string;
}; };
/* cSpell:enable */ /* cSpell:enable */
export type DocumentAttributeNamespace = keyof typeof DocumentAttributeNamespaces;
export type IDocumentAttributesProperties = Partial<Record<DocumentAttributeNamespace, string>> & {
readonly Ignorable?: string;
};
export class DocumentAttributes extends XmlAttributeComponent<IDocumentAttributesProperties> { export class DocumentAttributes extends XmlAttributeComponent<IDocumentAttributesProperties> {
protected readonly xmlKeys = { protected readonly xmlKeys = {
wpc: "xmlns:wpc",
mc: "xmlns:mc",
o: "xmlns:o",
r: "xmlns:r",
m: "xmlns:m",
v: "xmlns:v",
wp14: "xmlns:wp14",
wp: "xmlns:wp",
w10: "xmlns:w10",
w: "xmlns:w",
w14: "xmlns:w14",
w15: "xmlns:w15",
wpg: "xmlns:wpg",
wpi: "xmlns:wpi",
wne: "xmlns:wne",
wps: "xmlns:wps",
Ignorable: "mc:Ignorable", Ignorable: "mc:Ignorable",
cp: "xmlns:cp", ...Object.fromEntries(Object.keys(DocumentAttributeNamespaces).map((key) => [key, `xmlns:${key}`])),
dc: "xmlns:dc", } as AttributeMap<IDocumentAttributesProperties>;
dcterms: "xmlns:dcterms",
dcmitype: "xmlns:dcmitype", public constructor(ns: readonly DocumentAttributeNamespace[], Ignorable?: string) {
xsi: "xmlns:xsi", super({ Ignorable, ...Object.fromEntries(ns.map((n) => [n, DocumentAttributeNamespaces[n]])) });
type: "xsi:type", }
cx: "xmlns:cx",
cx1: "xmlns:cx1",
cx2: "xmlns:cx2",
cx3: "xmlns:cx3",
cx4: "xmlns:cx4",
cx5: "xmlns:cx5",
cx6: "xmlns:cx6",
cx7: "xmlns:cx7",
cx8: "xmlns:cx8",
aink: "xmlns:aink",
am3d: "xmlns:am3d",
w16cex: "xmlns:w16cex",
w16cid: "xmlns:w16cid",
w16: "xmlns:w16",
w16sdtdh: "xmlns:w16sdtdh",
w16se: "xmlns:w16se",
};
} }

View File

@ -37,41 +37,43 @@ export class Document extends XmlComponent {
public constructor(options: IDocumentOptions) { public constructor(options: IDocumentOptions) {
super("w:document"); super("w:document");
this.root.push( this.root.push(
new DocumentAttributes({ new DocumentAttributes(
wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", [
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006", "wpc",
o: "urn:schemas-microsoft-com:office:office", "mc",
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships", "o",
m: "http://schemas.openxmlformats.org/officeDocument/2006/math", "r",
v: "urn:schemas-microsoft-com:vml", "m",
wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing", "v",
wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", "wp14",
w10: "urn:schemas-microsoft-com:office:word", "wp",
w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main", "w10",
w14: "http://schemas.microsoft.com/office/word/2010/wordml", "w",
w15: "http://schemas.microsoft.com/office/word/2012/wordml", "w14",
wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup", "w15",
wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk", "wpg",
wne: "http://schemas.microsoft.com/office/word/2006/wordml", "wpi",
wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape", "wne",
cx: "http://schemas.microsoft.com/office/drawing/2014/chartex", "wps",
cx1: "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex", "cx",
cx2: "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex", "cx1",
cx3: "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex", "cx2",
cx4: "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex", "cx3",
cx5: "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex", "cx4",
cx6: "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex", "cx5",
cx7: "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex", "cx6",
cx8: "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex", "cx7",
aink: "http://schemas.microsoft.com/office/drawing/2016/ink", "cx8",
am3d: "http://schemas.microsoft.com/office/drawing/2017/model3d", "aink",
w16cex: "http://schemas.microsoft.com/office/word/2018/wordml/cex", "am3d",
w16cid: "http://schemas.microsoft.com/office/word/2016/wordml/cid", "w16cex",
w16: "http://schemas.microsoft.com/office/word/2018/wordml", "w16cid",
w16sdtdh: "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash", "w16",
w16se: "http://schemas.microsoft.com/office/word/2015/wordml/symex", "w16sdtdh",
Ignorable: "w14 w15 wp14", "w16se",
}), ],
"w14 w15 wp14",
),
); );
this.body = new Body(); this.body = new Body();
if (options.background) { if (options.background) {

View File

@ -479,4 +479,41 @@ describe("File", () => {
expect(doc.Styles).to.not.be.undefined; expect(doc.Styles).to.not.be.undefined;
}); });
}); });
describe("#features", () => {
it("should work with updateFields", () => {
const doc = new File({
sections: [],
features: {
updateFields: true,
},
});
expect(doc.Styles).to.not.be.undefined;
});
it("should work with trackRevisions", () => {
const doc = new File({
sections: [],
features: {
trackRevisions: true,
},
});
expect(doc.Styles).to.not.be.undefined;
});
});
describe("#hyphenation", () => {
it("should work with autoHyphenation", () => {
const doc = new File({
sections: [],
hyphenation: {
autoHyphenation: true,
},
});
expect(doc.Styles).to.not.be.undefined;
});
});
}); });

View File

@ -80,6 +80,12 @@ export class File {
trackRevisions: options.features?.trackRevisions, trackRevisions: options.features?.trackRevisions,
updateFields: options.features?.updateFields, updateFields: options.features?.updateFields,
defaultTabStop: options.defaultTabStop, defaultTabStop: options.defaultTabStop,
hyphenation: {
autoHyphenation: options.hyphenation?.autoHyphenation,
hyphenationZone: options.hyphenation?.hyphenationZone,
consecutiveHyphenLimit: options.hyphenation?.consecutiveHyphenLimit,
doNotHyphenateCaps: options.hyphenation?.doNotHyphenateCaps,
},
}); });
this.media = new Media(); this.media = new Media();

View File

@ -2,7 +2,7 @@ const obfuscatedStartOffset = 0;
const obfuscatedEndOffset = 32; const obfuscatedEndOffset = 32;
const guidSize = 32; const guidSize = 32;
export const obfuscate = (buf: Buffer, fontKey: string): Buffer => { export const obfuscate = (buf: Uint8Array, fontKey: string): Uint8Array => {
const guid = fontKey.replace(/-/g, ""); const guid = fontKey.replace(/-/g, "");
if (guid.length !== guidSize) { if (guid.length !== guidSize) {
throw new Error(`Error: Cannot extract GUID from font filename: ${fontKey}`); throw new Error(`Error: Cannot extract GUID from font filename: ${fontKey}`);
@ -17,6 +17,9 @@ export const obfuscate = (buf: Buffer, fontKey: string): Buffer => {
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
const obfuscatedBytes = bytesToObfuscate.map((byte, i) => byte ^ hexNumbers[i % hexNumbers.length]); const obfuscatedBytes = bytesToObfuscate.map((byte, i) => byte ^ hexNumbers[i % hexNumbers.length]);
const out = Buffer.concat([buf.slice(0, obfuscatedStartOffset), obfuscatedBytes, buf.slice(obfuscatedEndOffset)]); const out = new Uint8Array(obfuscatedStartOffset + obfuscatedBytes.length + Math.max(0, buf.length - obfuscatedEndOffset));
out.set(buf.slice(0, obfuscatedStartOffset));
out.set(obfuscatedBytes, obfuscatedStartOffset);
out.set(buf.slice(obfuscatedEndOffset), obfuscatedStartOffset + obfuscatedBytes.length);
return out; return out;
}; };

View File

@ -20,3 +20,5 @@ export * from "./border";
export * from "./vertical-align"; export * from "./vertical-align";
export * from "./checkbox"; export * from "./checkbox";
export * from "./fonts"; export * from "./fonts";
export * from "./textbox";
export { type IPropertiesOptions } from "./core-properties";

View File

@ -37,25 +37,10 @@ export class Numbering extends XmlComponent {
public constructor(options: INumberingOptions) { public constructor(options: INumberingOptions) {
super("w:numbering"); super("w:numbering");
this.root.push( this.root.push(
new DocumentAttributes({ new DocumentAttributes(
wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", ["wpc", "mc", "o", "r", "m", "v", "wp14", "wp", "w10", "w", "w14", "w15", "wpg", "wpi", "wne", "wps"],
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006", "w14 w15 wp14",
o: "urn:schemas-microsoft-com:office:office", ),
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
m: "http://schemas.openxmlformats.org/officeDocument/2006/math",
v: "urn:schemas-microsoft-com:vml",
wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
w10: "urn:schemas-microsoft-com:office:word",
w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
w14: "http://schemas.microsoft.com/office/word/2010/wordml",
w15: "http://schemas.microsoft.com/office/word/2012/wordml",
wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
wne: "http://schemas.microsoft.com/office/word/2006/wordml",
wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
Ignorable: "w14 w15 wp14",
}),
); );
const abstractNumbering = new AbstractNumbering(this.abstractNumUniqueNumericId(), [ const abstractNumbering = new AbstractNumbering(this.abstractNumUniqueNumericId(), [

View File

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

View File

@ -0,0 +1,2 @@
export * from "./math-bar";
export * from "./math-bar-properties";

View File

@ -0,0 +1,11 @@
// https://www.datypic.com/sc/ooxml/e-m_pos-1.html
import { Attributes, XmlComponent } from "@file/xml-components";
export class MathBarPos extends XmlComponent {
// TODO: Use correct types rather than any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public constructor(attributes: any) {
super("m:pos");
this.root.push(new Attributes(attributes));
}
}

View File

@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { Formatter } from "@export/formatter";
import { MathBarProperties } from "./math-bar-properties";
describe("MathBarProperties", () => {
describe("#constructor()", () => {
it("should create a MathBarProperties with top key", () => {
const mathBarProperties = new MathBarProperties("top");
const tree = new Formatter().format(mathBarProperties);
expect(tree).to.deep.equal({
"m:barPr": [
{
"m:pos": {
_attr: {
"w:val": "top",
},
},
},
],
});
});
it("should create a MathBarProperties with bottom key", () => {
const mathBarProperties = new MathBarProperties("bot");
const tree = new Formatter().format(mathBarProperties);
expect(tree).to.deep.equal({
"m:barPr": [
{
"m:pos": {
_attr: {
"w:val": "bot",
},
},
},
],
});
});
});
});

View File

@ -0,0 +1,11 @@
// https://www.datypic.com/sc/ooxml/e-m_barPr-1.html
import { XmlComponent } from "@file/xml-components";
import { MathBarPos } from "./math-bar-pos";
export class MathBarProperties extends XmlComponent {
public constructor(type: string) {
super("m:barPr");
this.root.push(new MathBarPos({ val: type }));
}
}

View File

@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { Formatter } from "@export/formatter";
import { MathBar } from "./math-bar";
import { MathRun } from "../math-run";
describe("MathBar", () => {
describe("#constructor()", () => {
it("should create a MathBar with correct root key", () => {
const mathBar = new MathBar({ type: "top", children: [new MathRun("text")] });
const tree = new Formatter().format(mathBar);
expect(tree).to.deep.equal({
"m:bar": [
{
"m:barPr": [
{
"m:pos": {
_attr: {
"w:val": "top",
},
},
},
],
},
{
"m:e": [
{
"m:r": [{ "m:t": ["text"] }],
},
],
},
],
});
});
});
});

View File

@ -0,0 +1,18 @@
// https://www.datypic.com/sc/ooxml/e-m_bar-1.html
import { XmlComponent } from "@file/xml-components";
import { MathBarProperties } from "./math-bar-properties";
import type { MathComponent } from "../math-component";
import { MathBase } from "../n-ary";
type MathBarOption = {
readonly type: "top" | "bot";
readonly children: readonly MathComponent[];
};
export class MathBar extends XmlComponent {
public constructor(options: MathBarOption) {
super("m:bar");
this.root.push(new MathBarProperties(options.type));
this.root.push(new MathBase(options.children));
}
}

View File

@ -2,7 +2,7 @@
import { XmlComponent } from "@file/xml-components"; import { XmlComponent } from "@file/xml-components";
import { MathPreSubSuperScriptProperties } from "./math-pre-sub-super-script-function-properties"; import { MathPreSubSuperScriptProperties } from "./math-pre-sub-super-script-function-properties";
import { MathComponent } from "../../math-component"; import type { MathComponent } from "../../math-component";
import { MathBase, MathSubScriptElement, MathSuperScriptElement } from "../../n-ary"; import { MathBase, MathSubScriptElement, MathSuperScriptElement } from "../../n-ary";
export type IMathPreSubSuperScriptOptions = { export type IMathPreSubSuperScriptOptions = {

View File

@ -38,6 +38,8 @@ export type ILevelParagraphStylePropertiesOptions = {
}; };
export type IParagraphStylePropertiesOptions = { export type IParagraphStylePropertiesOptions = {
readonly border?: IBordersOptions;
readonly shading?: IShadingAttributesProperties;
readonly numbering?: readonly numbering?:
| { | {
readonly reference: string; readonly reference: string;
@ -49,7 +51,6 @@ export type IParagraphStylePropertiesOptions = {
} & ILevelParagraphStylePropertiesOptions; } & ILevelParagraphStylePropertiesOptions;
export type IParagraphPropertiesOptions = { export type IParagraphPropertiesOptions = {
readonly border?: IBordersOptions;
readonly heading?: (typeof HeadingLevel)[keyof typeof HeadingLevel]; readonly heading?: (typeof HeadingLevel)[keyof typeof HeadingLevel];
readonly bidirectional?: boolean; readonly bidirectional?: boolean;
readonly pageBreakBefore?: boolean; readonly pageBreakBefore?: boolean;
@ -58,7 +59,6 @@ export type IParagraphPropertiesOptions = {
readonly bullet?: { readonly bullet?: {
readonly level: number; readonly level: number;
}; };
readonly shading?: IShadingAttributesProperties;
readonly widowControl?: boolean; readonly widowControl?: boolean;
readonly frame?: IFrameOptions; readonly frame?: IFrameOptions;
readonly suppressLineNumbers?: boolean; readonly suppressLineNumbers?: boolean;

View File

@ -155,7 +155,7 @@ export class Run extends XmlComponent {
this.root.push(child); this.root.push(child);
} }
} else if (options.text) { } else if (options.text !== undefined) {
this.root.push(new Text(options.text)); this.root.push(new Text(options.text));
} }
} }

View File

@ -16,6 +16,14 @@ describe("TextRun", () => {
"w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "test"] }], "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "test"] }],
}); });
}); });
it("should add empty text into run", () => {
run = new TextRun({ text: "" });
const f = new Formatter().format(run);
expect(f).to.deep.equal({
"w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, ""] }],
});
});
}); });
describe("#referenceFootnote()", () => { describe("#referenceFootnote()", () => {

View File

@ -1,14 +1,7 @@
import { IRunOptions, Run } from "./run"; import { IRunOptions, Run } from "./run";
import { Text } from "./run-components/text";
export class TextRun extends Run { export class TextRun extends Run {
public constructor(options: IRunOptions | string) { public constructor(options: IRunOptions | string) {
if (typeof options === "string") { super(typeof options === "string" ? { text: options } : options);
super({});
this.root.push(new Text(options));
return this;
}
super(options);
} }
} }

View File

@ -129,6 +129,74 @@ describe("Settings", () => {
}); });
}); });
it("should add autoHyphenation setting", () => {
const options = {
hyphenation: {
autoHyphenation: true,
},
};
const tree = new Formatter().format(new Settings(options));
expect(Object.keys(tree)).has.length(1);
expect(tree["w:settings"]).to.be.an("array");
expect(tree["w:settings"]).to.deep.include({
"w:autoHyphenation": {},
});
});
it("should add doNotHyphenateCaps setting", () => {
const options = {
hyphenation: {
doNotHyphenateCaps: true,
},
};
const tree = new Formatter().format(new Settings(options));
expect(Object.keys(tree)).has.length(1);
expect(tree["w:settings"]).to.be.an("array");
expect(tree["w:settings"]).to.deep.include({
"w:doNotHyphenateCaps": {},
});
});
it("should add hyphenationZone setting", () => {
const options = {
hyphenation: {
hyphenationZone: 200,
},
};
const tree = new Formatter().format(new Settings(options));
expect(Object.keys(tree)).has.length(1);
expect(tree["w:settings"]).to.be.an("array");
expect(tree["w:settings"]).to.deep.include({
"w:hyphenationZone": {
_attr: {
"w:val": 200,
},
},
});
});
it("should add consecutiveHyphenLimit setting", () => {
const options = {
hyphenation: {
consecutiveHyphenLimit: 3,
},
};
const tree = new Formatter().format(new Settings(options));
expect(Object.keys(tree)).has.length(1);
expect(tree["w:settings"]).to.be.an("array");
expect(tree["w:settings"]).to.deep.include({
"w:consecutiveHyphenLimit": {
_attr: {
"w:val": 3,
},
},
});
});
// TODO: Remove when deprecating compatibilityModeVersion // TODO: Remove when deprecating compatibilityModeVersion
it("should add compatibility setting with legacy version", () => { it("should add compatibility setting with legacy version", () => {
const settings = new Settings({ const settings = new Settings({

View File

@ -153,6 +153,18 @@ export type ISettingsOptions = {
readonly updateFields?: boolean; readonly updateFields?: boolean;
readonly compatibility?: ICompatibilityOptions; readonly compatibility?: ICompatibilityOptions;
readonly defaultTabStop?: number; readonly defaultTabStop?: number;
readonly hyphenation?: IHyphenationOptions;
};
export type IHyphenationOptions = {
/** Specifies whether the application automatically hyphenates words as they are typed in the document. */
readonly autoHyphenation?: boolean;
/** Specifies the minimum number of characters at the beginning of a word before a hyphen can be inserted. */
readonly hyphenationZone?: number;
/** Specifies the maximum number of consecutive lines that can end with a hyphenated word. */
readonly consecutiveHyphenLimit?: number;
/** Specifies whether to hyphenate words in all capital letters. */
readonly doNotHyphenateCaps?: boolean;
}; };
export class Settings extends XmlComponent { export class Settings extends XmlComponent {
@ -204,6 +216,26 @@ export class Settings extends XmlComponent {
this.root.push(new NumberValueElement("w:defaultTabStop", options.defaultTabStop)); this.root.push(new NumberValueElement("w:defaultTabStop", options.defaultTabStop));
} }
// https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_autoHyphenation_topic_ID0EFUMX.html
if (options.hyphenation?.autoHyphenation !== undefined) {
this.root.push(new OnOffElement("w:autoHyphenation", options.hyphenation.autoHyphenation));
}
// https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_hyphenationZone_topic_ID0ERI3X.html
if (options.hyphenation?.hyphenationZone !== undefined) {
this.root.push(new NumberValueElement("w:hyphenationZone", options.hyphenation.hyphenationZone));
}
// https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_consecutiveHyphenLim_topic_ID0EQ6RX.html
if (options.hyphenation?.consecutiveHyphenLimit !== undefined) {
this.root.push(new NumberValueElement("w:consecutiveHyphenLimit", options.hyphenation.consecutiveHyphenLimit));
}
// https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_doNotHyphenateCaps_topic_ID0EW4XX.html
if (options.hyphenation?.doNotHyphenateCaps !== undefined) {
this.root.push(new OnOffElement("w:doNotHyphenateCaps", options.hyphenation.doNotHyphenateCaps));
}
this.root.push( this.root.push(
new Compatibility({ new Compatibility({
...(options.compatibility ?? {}), ...(options.compatibility ?? {}),

View File

@ -144,6 +144,10 @@ describe("External styles factory", () => {
expect(() => new ExternalStylesFactory().newInstance(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><foo/>`)).to.throw( expect(() => new ExternalStylesFactory().newInstance(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><foo/>`)).to.throw(
"can not find styles element", "can not find styles element",
); );
expect(() => new ExternalStylesFactory().newInstance(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`)).to.throw(
"can not find styles element",
);
}); });
it("should parse styles elements", () => { it("should parse styles elements", () => {

View File

@ -38,14 +38,7 @@ export type IDefaultStylesOptions = {
export class DefaultStylesFactory { export class DefaultStylesFactory {
public newInstance(options: IDefaultStylesOptions = {}): IStylesOptions { public newInstance(options: IDefaultStylesOptions = {}): IStylesOptions {
const documentAttributes = new DocumentAttributes({ const documentAttributes = new DocumentAttributes(["mc", "r", "w", "w14", "w15"], "w14 w15");
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
w14: "http://schemas.microsoft.com/office/word/2010/wordml",
w15: "http://schemas.microsoft.com/office/word/2012/wordml",
Ignorable: "w14 w15",
});
return { return {
initialStyles: documentAttributes, initialStyles: documentAttributes,
importedStyles: [ importedStyles: [

View File

@ -0,0 +1 @@
export * from "./textbox";

View File

@ -0,0 +1,11 @@
import { BuilderElement, XmlComponent } from "@file/xml-components";
export type IPictElement = {
readonly shape: XmlComponent;
};
export const createPictElement = ({ shape }: IPictElement): XmlComponent =>
new BuilderElement<{ readonly style?: string }>({
name: "w:pict",
children: [shape],
});

View File

@ -0,0 +1,58 @@
import { describe, expect, it } from "vitest";
import { Formatter } from "@export/formatter";
import { Paragraph } from "@file/paragraph";
import { createShape } from "./shape";
describe("createShape", () => {
it("should work", () => {
const tree = new Formatter().format(
createShape({
id: "test-id",
style: {
width: "10pt",
},
children: [new Paragraph("test-content")],
}),
);
expect(tree).toStrictEqual({
"v:shape": [
{ _attr: { id: "test-id", type: "#_x0000_t202", style: "width:10pt" } },
{
"v:textbox": [
{ _attr: { insetmode: "auto", style: "mso-fit-shape-to-text:t;" } },
{
"w:txbxContent": [
{ "w:p": [{ "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "test-content"] }] }] },
],
},
],
},
],
});
});
it("should create default styles", () => {
const tree = new Formatter().format(
createShape({
id: "test-id",
}),
);
expect(tree).toStrictEqual({
"v:shape": [
{ _attr: { id: "test-id", type: "#_x0000_t202" } },
{
"v:textbox": [
{ _attr: { insetmode: "auto", style: "mso-fit-shape-to-text:t;" } },
{
"w:txbxContent": {},
},
],
},
],
});
});
});

View File

@ -0,0 +1,125 @@
// https://c-rex.net/samples/ooxml/e1/Part3/OOXML_P3_Primer_OfficeArt_topic_ID0ELU5O.html
// http://webapp.docx4java.org/OnlineDemo/ecma376/VML/shape.html
import { ParagraphChild } from "@file/paragraph";
import { BuilderElement, XmlComponent } from "@file/xml-components";
import { LengthUnit } from "../types";
import { createVmlTextbox } from "../vml-textbox/vml-texbox";
const SHAPE_TYPE = "#_x0000_t202";
const styleToKeyMap: Record<keyof VmlShapeStyle, string> = {
flip: "flip",
height: "height",
left: "left",
marginBottom: "margin-bottom",
marginLeft: "margin-left",
marginRight: "margin-right",
marginTop: "margin-top",
positionHorizontal: "mso-position-horizontal",
positionHorizontalRelative: "mso-position-horizontal-relative",
positionVertical: "mso-position-vertical",
positionVerticalRelative: "mso-position-vertical-relative",
wrapDistanceBottom: "mso-wrap-distance-bottom",
wrapDistanceLeft: "mso-wrap-distance-left",
wrapDistanceRight: "mso-wrap-distance-right",
wrapDistanceTop: "mso-wrap-distance-top",
wrapEdited: "mso-wrap-edited",
wrapStyle: "mso-wrap-style",
position: "position",
rotation: "rotation",
top: "top",
visibility: "visibility",
width: "width",
zIndex: "z-index",
};
export type VmlShapeStyle = {
/** Specifies that the orientation of a shape is flipped. Default is no value. */
readonly flip?: "x" | "y" | "xy" | "yx";
/** Specifies the height of the containing block of the shape. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. */
readonly height?: LengthUnit;
/** Specifies the position of the left of the containing block of the shape relative to the element left of it in the flow of the page. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. This property shall not be used for shapes anchored inline. */
readonly left?: LengthUnit;
/** Specifies the position of the bottom of the containing block of the shape relative to the shape anchor. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. */
readonly marginBottom?: LengthUnit;
/** Specifies the position of the left of the containing block of the shape relative to the shape anchor. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. */
readonly marginLeft?: LengthUnit;
/** Specifies the position of the right of the containing block of the shape relative to the shape anchor. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. */
readonly marginRight?: LengthUnit;
/** Specifies the position of the top of the containing block of the shape relative to the shape anchor. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. */
readonly marginTop?: LengthUnit;
/** Specifies the horizontal positioning data for objects in WordprocessingML documents. Default is absolute. */
readonly positionHorizontal?: "absolute" | "left" | "center" | "right" | "inside" | "outside";
/** Specifies relative horizontal position data for objects in WordprocessingML documents. This modifies the mso-position-horizontal property. Default is text. */
readonly positionHorizontalRelative?: "margin" | "page" | "text" | "char";
/** Specifies the vertical positioning data for objects in WordprocessingML documents. Default is absolute. */
readonly positionVertical?: "absolute" | "left" | "center" | "right" | "inside" | "outside";
/** Specifies relative vertical position data for objects in WordprocessingML documents. This modifies the mso-position-vertical property. Default is text. */
readonly positionVerticalRelative?: "margin" | "page" | "text" | "char";
/** Specifies the distance from the bottom of the shape to the text that wraps around it. Default is 0 pt. Note that this property is different from the CSS margin property, which changes the origin of the shape to include the margin areas. This property does not change the origin. */
readonly wrapDistanceBottom?: number;
/** Specifies the distance from the left side of the shape to the text that wraps around it. Default is 0 pt. Note that this property is different from the CSS margin property, which changes the origin of the shape to include the margin areas. This property does not change the origin. */
readonly wrapDistanceLeft?: number;
/** Specifies the distance from the right side of the shape to the text that wraps around it. Default is 0 pt. Note that this property is different from the CSS margin property, which changes the origin of the shape to include the margin areas. This property does not change the origin. */
readonly wrapDistanceRight?: number;
/** Specifies the distance from the top of the shape to the text that wraps around it. Default is 0 pt. Note that this property is different from the CSS margin property, which changes the origin of the shape to include the margin areas. This property does not change the origin. */
readonly wrapDistanceTop?: number;
/** Specifies whether the wrap coordinates were customized by the user. If the wrap coordinates are generated by an editor, this property is true; otherwise they were customized by a user. Default is false. */
readonly wrapEdited?: boolean;
/** Specifies the wrapping mode for text in shapes in WordprocessingML documents. Default is square. */
readonly wrapStyle?: "square" | "none";
/** Specifies the type of positioning used to place an element. Default is static. When the element is contained inside a group, this property must be absolute. */
readonly position?: "static" | "absolute" | "relative";
/** Specifies the angle that a shape is rotated, in degrees. Default is 0. Positive angles are clockwise. */
readonly rotation?: number;
/** Specifies the position of the top of the containing block of the shape relative to the element above it in the flow of the page. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. This property shall not be used for shapes anchored inline. */
readonly top?: LengthUnit;
/** Specifies whether a shape is displayed. Only inherit and hidden are used; any other values are mapped to inherit. Default is inherit. */
readonly visibility?: "hidden" | "inherit";
/** Specifies the width of the containing block of the shape. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. */
readonly width: LengthUnit;
/** Specifies the display order of overlapping shapes. Default is 0. This property shall not be used for shapes anchored inline. */
readonly zIndex?: "auto" | number;
};
const formatShapeStyle = (style?: VmlShapeStyle): string | undefined =>
style
? Object.entries(style)
.map(([key, value]) => `${styleToKeyMap[key as keyof VmlShapeStyle]}:${value}`)
.join(";")
: undefined;
export const createShape = ({
id,
children,
type = SHAPE_TYPE,
style,
}: {
readonly id: string;
readonly children?: readonly ParagraphChild[];
readonly type?: string;
readonly style?: VmlShapeStyle;
}): XmlComponent =>
new BuilderElement<{
readonly id: string;
readonly type?: string;
readonly style?: string;
}>({
name: "v:shape",
attributes: {
id: {
key: "id",
value: id,
},
type: {
key: "type",
value: type,
},
style: {
key: "style",
value: formatShapeStyle(style),
},
},
children: [createVmlTextbox({ style: "mso-fit-shape-to-text:t;", children })],
});

View File

@ -0,0 +1,8 @@
import { ParagraphChild } from "@file/paragraph";
import { BuilderElement, XmlComponent } from "@file/xml-components";
export const createTextboxContent = ({ children = [] }: { readonly children?: readonly ParagraphChild[] }): XmlComponent =>
new BuilderElement<{ readonly style?: string }>({
name: "w:txbxContent",
children: children as readonly XmlComponent[],
});

View File

@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";
import { Formatter } from "@export/formatter";
import { Paragraph } from "@file/paragraph";
import { Textbox } from "./textbox";
describe("VmlTextbox", () => {
it("should work", () => {
const tree = new Formatter().format(
new Textbox({
style: {
width: "10pt",
},
children: [new Paragraph("test-content")],
}),
);
expect(tree).toStrictEqual({
"w:p": [
{
"w:pict": [
{
"v:shape": [
{ _attr: { id: expect.any(String), type: "#_x0000_t202", style: "width:10pt" } },
{
"v:textbox": [
{ _attr: { insetmode: "auto", style: "mso-fit-shape-to-text:t;" } },
{
"w:txbxContent": [
{
"w:p": [
{ "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "test-content"] }] },
],
},
],
},
],
},
],
},
],
},
],
});
});
});

View File

@ -0,0 +1,27 @@
import { FileChild } from "@file/file-child";
import { IParagraphOptions, ParagraphProperties } from "@file/paragraph";
import { uniqueId } from "@util/convenience-functions";
import { createPictElement } from "./pict-element/pict-element";
import { VmlShapeStyle, createShape } from "./shape/shape";
type ITextboxOptions = Omit<IParagraphOptions, "style"> & {
readonly style?: VmlShapeStyle;
};
export class Textbox extends FileChild {
public constructor({ style, children, ...rest }: ITextboxOptions) {
super("w:p");
this.root.push(new ParagraphProperties(rest));
this.root.push(
createPictElement({
shape: createShape({
children: children,
id: uniqueId(),
style: style,
}),
}),
);
}
}

View File

@ -0,0 +1,3 @@
import { Percentage, RelativeMeasure, UniversalMeasure } from "@util/values";
export type LengthUnit = "auto" | number | Percentage | UniversalMeasure | RelativeMeasure;

View File

@ -0,0 +1,42 @@
// http://webapp.docx4java.org/OnlineDemo/ecma376/VML/textbox.html
import { ParagraphChild } from "@file/paragraph";
import { BuilderElement, XmlComponent } from "@file/xml-components";
import { InsetMode } from "@util/types";
import { createTextboxContent } from "../texbox-content/textbox-content";
import { LengthUnit } from "../types";
// type VMLTextboxStyle = {
// readonly fontWeight?: "normal" | "lighter" | 100 | 200 | 300 | 400 | "bold" | "bolder" | 500 | 600 | 700 | 800 | 900;
// }
export type IVTextboxOptions = {
readonly style?: string;
readonly children?: readonly ParagraphChild[];
readonly inset?: {
readonly top: LengthUnit;
readonly left: LengthUnit;
readonly bottom: LengthUnit;
readonly right: LengthUnit;
};
};
export const createVmlTextbox = ({ style, children, inset }: IVTextboxOptions): XmlComponent =>
new BuilderElement<{ readonly style?: string; readonly inset?: string; readonly insetMode?: InsetMode }>({
name: "v:textbox",
attributes: {
style: {
key: "style",
value: style,
},
insetMode: {
key: "insetmode",
value: inset ? "custom" : "auto",
},
inset: {
key: "inset",
value: inset ? `${inset.left}, ${inset.top}, ${inset.right}, ${inset.bottom}` : undefined,
},
},
children: [createTextboxContent({ children })],
});

View File

@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { Formatter } from "@export/formatter";
import { Paragraph } from "@file/paragraph";
import { createVmlTextbox } from "./vml-texbox";
describe("VmlTextbox", () => {
it("should work", () => {
const tree = new Formatter().format(
createVmlTextbox({
style: "test-style",
children: [new Paragraph("test-content")],
}),
);
expect(tree).toStrictEqual({
"v:textbox": [
{ _attr: { insetmode: "auto", style: "test-style" } },
{ "w:txbxContent": [{ "w:p": [{ "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "test-content"] }] }] }] },
],
});
});
it("should work with inset", () => {
const tree = new Formatter().format(
createVmlTextbox({
style: "test-style",
children: [new Paragraph("test-content")],
inset: {
top: 0,
left: 0,
bottom: 0,
right: 0,
},
}),
);
expect(tree).toStrictEqual({
"v:textbox": [
{ _attr: { insetmode: "custom", style: "test-style", inset: "0, 0, 0, 0" } },
{ "w:txbxContent": [{ "w:p": [{ "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "test-content"] }] }] }] },
],
});
});
});

View File

@ -1,7 +1,7 @@
import { BaseXmlComponent, IContext } from "./base"; import { BaseXmlComponent, IContext } from "./base";
import { IXmlAttribute, IXmlableObject } from "./xmlable-object"; import { IXmlAttribute, IXmlableObject } from "./xmlable-object";
type AttributeMap<T> = Record<keyof T, string>; export type AttributeMap<T> = Record<keyof T, string>;
export type AttributeData = Record<string, boolean | number | string>; export type AttributeData = Record<string, boolean | number | string>;
export type AttributePayload<T> = { readonly [P in keyof T]: { readonly key: string; readonly value: T[P] } }; export type AttributePayload<T> = { readonly [P in keyof T]: { readonly key: string; readonly value: T[P] } };

View File

@ -2,6 +2,7 @@ import JSZip from "jszip";
import { Element, js2xml } from "xml-js"; import { Element, js2xml } from "xml-js";
import { ImageReplacer } from "@export/packer/image-replacer"; import { ImageReplacer } from "@export/packer/image-replacer";
import { DocumentAttributeNamespaces } from "@file/document";
import { IViewWrapper } from "@file/document-wrapper"; import { IViewWrapper } from "@file/document-wrapper";
import { File } from "@file/file"; import { File } from "@file/file";
import { FileChild } from "@file/file-child"; import { FileChild } from "@file/file-child";
@ -10,6 +11,7 @@ import { ConcreteHyperlink, ExternalHyperlink, ParagraphChild } from "@file/para
import { TargetModeType } from "@file/relationships/relationship/relationship"; import { TargetModeType } from "@file/relationships/relationship/relationship";
import { IContext } from "@file/xml-components"; import { IContext } from "@file/xml-components";
import { uniqueId } from "@util/convenience-functions"; import { uniqueId } from "@util/convenience-functions";
import { OutputByType, OutputType } from "@util/output-type";
import { appendContentType } from "./content-types-manager"; import { appendContentType } from "./content-types-manager";
import { appendRelationship, getNextRelationshipIndex } from "./relationship-manager"; import { appendRelationship, getNextRelationshipIndex } from "./relationship-manager";
@ -46,21 +48,7 @@ type IHyperlinkRelationshipAddition = {
export type IPatch = ParagraphPatch | FilePatch; export type IPatch = ParagraphPatch | FilePatch;
// From JSZip export type PatchDocumentOutputType = OutputType;
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> = { export type PatchDocumentOptions<T extends PatchDocumentOutputType = PatchDocumentOutputType> = {
readonly outputType: T; readonly outputType: T;
@ -100,6 +88,24 @@ export const patchDocument = async <T extends PatchDocumentOutputType = PatchDoc
} }
const json = toJson(await value.async("text")); const json = toJson(await value.async("text"));
if (key === "word/document.xml") {
const document = json.elements?.find((i) => i.name === "w:document");
if (document) {
// We could check all namespaces from Document, but we'll instead
// check only those that may be used by our element types.
// eslint-disable-next-line functional/immutable-data
document.attributes = document.attributes ?? {};
for (const ns of ["mc", "wp", "r", "w15", "m"] as const) {
// eslint-disable-next-line functional/immutable-data
document.attributes[`xmlns:${ns}`] = DocumentAttributeNamespaces[ns];
}
// eslint-disable-next-line functional/immutable-data
document.attributes["mc:Ignorable"] = `${document.attributes["mc:Ignorable"] || ""} w15`.trim();
}
}
if (key.startsWith("word/") && !key.endsWith(".xml.rels")) { if (key.startsWith("word/") && !key.endsWith(".xml.rels")) {
const context: IContext = { const context: IContext = {
file, file,
@ -132,38 +138,37 @@ export const patchDocument = async <T extends PatchDocumentOutputType = PatchDoc
// We need to loop through to catch every occurrence of the patch text // 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 // It is possible that the patch text is in the same run
// This algorithm is limited to one patch per text 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 // We break out of the loop once it cannot find any more occurrences
// https://github.com/dolanmiu/docx/issues/2267 // https://github.com/dolanmiu/docx/issues/2267
while (true) { while (true) {
try { const { didFindOccurrence } = replacer({
replacer({ json,
json, patch: {
patch: { ...patchValue,
...patchValue, children: patchValue.children.map((element) => {
children: patchValue.children.map((element) => { // We need to replace external hyperlinks with concrete hyperlinks
// We need to replace external hyperlinks with concrete hyperlinks if (element instanceof ExternalHyperlink) {
if (element instanceof ExternalHyperlink) { const concreteHyperlink = new ConcreteHyperlink(element.options.children, uniqueId());
const concreteHyperlink = new ConcreteHyperlink(element.options.children, uniqueId()); // eslint-disable-next-line functional/immutable-data
// eslint-disable-next-line functional/immutable-data hyperlinkRelationshipAdditions.push({
hyperlinkRelationshipAdditions.push({ key,
key, hyperlink: {
hyperlink: { id: concreteHyperlink.linkId,
id: concreteHyperlink.linkId, link: element.options.link,
link: element.options.link, },
}, });
}); return concreteHyperlink;
return concreteHyperlink; } else {
} else { return element;
return element; }
} }),
}), // eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any } as any,
} as any, patchText,
patchText, context,
context, keepOriginalStyles,
keepOriginalStyles, });
}); if (!didFindOccurrence) {
} catch {
break; break;
} }
} }

View File

@ -3,7 +3,6 @@ import { describe, expect, it, vi } from "vitest";
import { IViewWrapper } from "@file/document-wrapper"; import { IViewWrapper } from "@file/document-wrapper";
import { File } from "@file/file"; import { File } from "@file/file";
import { Paragraph, TextRun } from "@file/paragraph"; import { Paragraph, TextRun } from "@file/paragraph";
import { IContext } from "@file/xml-components";
import { PatchType } from "./from-docx"; import { PatchType } from "./from-docx";
import { replacer } from "./replacer"; import { replacer } from "./replacer";
@ -62,6 +61,10 @@ export const MOCK_JSON = {
name: "w:t", name: "w:t",
elements: [{ type: "text", text: "What a {{bold}} text!" }], elements: [{ type: "text", text: "What a {{bold}} text!" }],
}, },
{
type: "element",
name: "w:br",
},
], ],
}, },
], ],
@ -73,25 +76,23 @@ export const MOCK_JSON = {
describe("replacer", () => { describe("replacer", () => {
describe("replacer", () => { describe("replacer", () => {
it("should throw an error if nothing is added", () => { it("should return { didFindOccurrence: false } if nothing is added", () => {
expect(() => const { didFindOccurrence } = replacer({
replacer({ json: {
json: { elements: [],
elements: [], },
}, patch: {
patch: { type: PatchType.PARAGRAPH,
type: PatchType.PARAGRAPH, children: [],
children: [], },
}, patchText: "hello",
patchText: "hello", context: vi.fn()(),
// eslint-disable-next-line functional/prefer-readonly-type });
context: vi.fn<[], IContext>()(), expect(didFindOccurrence).toBe(false);
}),
).toThrow();
}); });
it("should replace paragraph type", () => { it("should replace paragraph type", () => {
const output = replacer({ const { element, didFindOccurrence } = replacer({
json: JSON.parse(JSON.stringify(MOCK_JSON)), json: JSON.parse(JSON.stringify(MOCK_JSON)),
patch: { patch: {
type: PatchType.PARAGRAPH, type: PatchType.PARAGRAPH,
@ -107,11 +108,12 @@ describe("replacer", () => {
}, },
}); });
expect(JSON.stringify(output)).to.contain("Delightful Header"); expect(JSON.stringify(element)).to.contain("Delightful Header");
expect(didFindOccurrence).toBe(true);
}); });
it("should replace paragraph type keeping original styling if keepOriginalStyles is true", () => { it("should replace paragraph type keeping original styling if keepOriginalStyles is true", () => {
const output = replacer({ const { element, didFindOccurrence } = replacer({
json: JSON.parse(JSON.stringify(MOCK_JSON)), json: JSON.parse(JSON.stringify(MOCK_JSON)),
patch: { patch: {
type: PatchType.PARAGRAPH, type: PatchType.PARAGRAPH,
@ -128,8 +130,8 @@ describe("replacer", () => {
keepOriginalStyles: true, keepOriginalStyles: true,
}); });
expect(JSON.stringify(output)).to.contain("sweet"); expect(JSON.stringify(element)).to.contain("sweet");
expect(output.elements![0].elements![1].elements).toMatchObject([ expect(element.elements![0].elements![1].elements).toMatchObject([
{ {
type: "element", type: "element",
name: "w:r", name: "w:r",
@ -176,13 +178,18 @@ describe("replacer", () => {
name: "w:t", name: "w:t",
elements: [{ type: "text", text: " text!" }], elements: [{ type: "text", text: " text!" }],
}, },
{
name: "w:br",
type: "element",
},
], ],
}, },
]); ]);
expect(didFindOccurrence).toBe(true);
}); });
it("should replace document type", () => { it("should replace document type", () => {
const output = replacer({ const { element, didFindOccurrence } = replacer({
json: JSON.parse(JSON.stringify(MOCK_JSON)), json: JSON.parse(JSON.stringify(MOCK_JSON)),
patch: { patch: {
type: PatchType.DOCUMENT, type: PatchType.DOCUMENT,
@ -198,12 +205,13 @@ describe("replacer", () => {
}, },
}); });
expect(JSON.stringify(output)).to.contain("Lorem ipsum paragraph"); expect(JSON.stringify(element)).to.contain("Lorem ipsum paragraph");
expect(didFindOccurrence).toBe(true);
}); });
it("should replace", () => { it("should replace", () => {
// cspell:disable // cspell:disable
const output = replacer({ const { element, didFindOccurrence } = replacer({
json: { json: {
elements: [ elements: [
{ {
@ -647,7 +655,74 @@ describe("replacer", () => {
}, },
}); });
expect(JSON.stringify(output)).to.contain("Lorem ipsum paragraph"); expect(JSON.stringify(element)).to.contain("Lorem ipsum paragraph");
expect(didFindOccurrence).toBe(true);
});
it("should handle empty runs in patches", () => {
// cspell:disable
const { element, didFindOccurrence } = replacer({
json: {
elements: [
{
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: "text", text: "\n " },
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "{{empty}}" }],
},
{ type: "text", text: "\n " },
],
},
],
},
],
},
],
},
// cspell:enable
patch: {
type: PatchType.PARAGRAPH,
children: [new TextRun({})],
},
patchText: "{{empty}}",
context: {
file: {} as unknown as File,
viewWrapper: {
Relationships: {},
} as unknown as IViewWrapper,
stack: [],
},
keepOriginalStyles: true,
});
expect(JSON.stringify(element)).not.to.contain("{{empty}}");
expect(didFindOccurrence).toBe(true);
}); });
}); });
}); });

View File

@ -14,6 +14,11 @@ const formatter = new Formatter();
const SPLIT_TOKEN = "ɵ"; const SPLIT_TOKEN = "ɵ";
type IReplacerResult = {
readonly element: Element;
readonly didFindOccurrence: boolean;
};
export const replacer = ({ export const replacer = ({
json, json,
patch, patch,
@ -26,11 +31,11 @@ export const replacer = ({
readonly patchText: string; readonly patchText: string;
readonly context: IContext; readonly context: IContext;
readonly keepOriginalStyles?: boolean; readonly keepOriginalStyles?: boolean;
}): Element => { }): IReplacerResult => {
const renderedParagraphs = findLocationOfText(json, patchText); const renderedParagraphs = findLocationOfText(json, patchText);
if (renderedParagraphs.length === 0) { if (renderedParagraphs.length === 0) {
throw new Error(`Could not find text ${patchText}`); return { element: json, didFindOccurrence: false };
} }
for (const renderedParagraph of renderedParagraphs) { for (const renderedParagraph of renderedParagraphs) {
@ -64,12 +69,12 @@ export const replacer = ({
if (keepOriginalStyles) { if (keepOriginalStyles) {
const runElementNonTextualElements = runElementToBeReplaced.elements!.filter( const runElementNonTextualElements = runElementToBeReplaced.elements!.filter(
(e) => e.type === "element" && e.name !== "w:t" && e.name !== "w:br" && e.name !== "w:tab", (e) => e.type === "element" && e.name === "w:rPr",
); );
newRunElements = textJson.map((e) => ({ newRunElements = textJson.map((e) => ({
...e, ...e,
elements: [...runElementNonTextualElements, ...e.elements!], elements: [...runElementNonTextualElements, ...(e.elements ?? [])],
})); }));
patchedRightElement = { patchedRightElement = {
@ -85,7 +90,7 @@ export const replacer = ({
} }
} }
return json; return { element: json, didFindOccurrence: true };
}; };
const goToElementFromPath = (json: Element, path: readonly number[]): Element => { const goToElementFromPath = (json: Element, path: readonly number[]): Element => {

View File

@ -1,2 +1,3 @@
export * from "./convenience-functions"; export * from "./convenience-functions";
export * from "./values"; export * from "./values";
export type * from "./output-type";

18
src/util/output-type.ts Normal file
View File

@ -0,0 +1,18 @@
/* v8 ignore start */
// Simply type definitions. Can ignore testing and coverage
// From JSZip
export 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 OutputType = keyof OutputByType;
/* v8 ignore stop */

7
src/util/types.ts Normal file
View File

@ -0,0 +1,7 @@
// <xsd:simpleType name="ST_InsetMode">
// <xsd:restriction base="xsd:string">
// <xsd:enumeration value="auto"/>
// <xsd:enumeration value="custom"/>
// </xsd:restriction>
// </xsd:simpleType>
export type InsetMode = "auto" | "custom"; // VML only type

View File

@ -21,6 +21,16 @@ export type PositiveUniversalMeasure = `${number}${"mm" | "cm" | "in" | "pt" | "
// </xsd:simpleType> // </xsd:simpleType>
export type Percentage = `${"-" | ""}${number}%`; export type Percentage = `${"-" | ""}${number}%`;
// <xsd:simpleType name="ST_PositivePercentage">
// <xsd:restriction base="ST_Percentage">
// <xsd:pattern value="[0-9]+(\.[0-9]+)?%"/>
// </xsd:restriction>
// </xsd:simpleType>
export type PositivePercentage = `${number}%`;
// Only applies to VmlTextbox so far
export type RelativeMeasure = `${"-" | ""}${number}${"em" | "ex"}`;
// <xsd:simpleType name="ST_DecimalNumber"> // <xsd:simpleType name="ST_DecimalNumber">
// <xsd:restriction base="xsd:integer"/> // <xsd:restriction base="xsd:integer"/>
// </xsd:simpleType> // </xsd:simpleType>

View File

@ -1,14 +1,20 @@
import { configDefaults, defineConfig } from "vitest/config";
import { resolve } from "path"; import { resolve } from "path";
import tsconfigPaths from "vite-tsconfig-paths";
import dts from "vite-plugin-dts"; import dts from "vite-plugin-dts";
import { nodePolyfills } from "vite-plugin-node-polyfills"; import { nodePolyfills } from "vite-plugin-node-polyfills";
import tsconfigPaths from "vite-tsconfig-paths";
import { configDefaults, defineConfig } from "vitest/config";
import { copyFileSync } from "node:fs";
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
tsconfigPaths(), tsconfigPaths(),
dts({ dts({
rollupTypes: true rollupTypes: true,
afterBuild: () => {
// https://github.com/dolanmiu/docx/pull/2883
// To pass publint - `npx publint@latest`
copyFileSync("dist/index.d.ts", "dist/index.d.cts");
},
}), }),
nodePolyfills({ nodePolyfills({
exclude: [], exclude: [],
@ -34,7 +40,7 @@ export default defineConfig({
name: "docx", name: "docx",
fileName: (d) => { fileName: (d) => {
if (d === "umd") { if (d === "umd") {
return "index.umd.js"; return "index.umd.cjs";
} }
if (d === "cjs") { if (d === "cjs") {
@ -53,7 +59,7 @@ export default defineConfig({
}, },
formats: ["iife", "es", "cjs", "umd"], formats: ["iife", "es", "cjs", "umd"],
}, },
outDir: resolve(__dirname, "build"), outDir: resolve(__dirname, "dist"),
commonjsOptions: { commonjsOptions: {
include: [/node_modules/], include: [/node_modules/],
}, },
@ -65,29 +71,22 @@ export default defineConfig({
reporter: ["text", "json", "html"], reporter: ["text", "json", "html"],
thresholds: { thresholds: {
statements: 100, statements: 100,
branches: 99.35, branches: 99.68,
functions: 100, functions: 100,
lines: 100, lines: 100,
}, },
exclude: [ exclude: [
...configDefaults.exclude, ...configDefaults.exclude,
'**/build/**', "**/dist/**",
'**/demo/**', "**/demo/**",
'**/docs/**', "**/docs/**",
'**/scripts/**', "**/scripts/**",
'**/src/**/index.ts', "**/src/**/index.ts",
"**/src/**/types.ts",
"**/*.spec.ts",
], ],
}, },
include: [ include: ["**/src/**/*.spec.ts", "**/packages/**/*.spec.ts"],
'**/src/**/*.spec.ts', exclude: [...configDefaults.exclude, "**/build/**", "**/demo/**", "**/docs/**", "**/scripts/**"],
'**/packages/**/*.spec.ts'
],
exclude: [
...configDefaults.exclude,
'**/build/**',
'**/demo/**',
'**/docs/**',
'**/scripts/**'
],
}, },
}); });