Compare commits

..

156 Commits

Author SHA1 Message Date
168e5e6e11 Version bump 2023-03-23 23:20:14 +00:00
ce71f19826 Merge pull request #2030 from dolanmiu/dependabot/npm_and_yarn/rimraf-4.4.1
build(deps-dev): bump rimraf from 4.4.0 to 4.4.1
2023-03-23 23:17:58 +00:00
4fd3f3a6b8 Merge pull request #2019 from dolanmiu/dependabot/npm_and_yarn/cspell-6.30.2
build(deps-dev): bump cspell from 6.30.0 to 6.30.2
2023-03-23 23:17:46 +00:00
39763d3869 Merge pull request #2023 from dolanmiu/dependabot/npm_and_yarn/typescript-eslint/parser-5.56.0
build(deps-dev): bump @typescript-eslint/parser from 5.55.0 to 5.56.0
2023-03-23 23:17:35 +00:00
36f90edc67 Merge pull request #2021 from dolanmiu/dependabot/npm_and_yarn/types/node-18.15.5
build(deps): bump @types/node from 18.15.3 to 18.15.5
2023-03-23 23:17:27 +00:00
b0cbd6f609 Merge pull request #2017 from dolanmiu/dependabot/npm_and_yarn/typedoc-0.23.28
build(deps-dev): bump typedoc from 0.23.27 to 0.23.28
2023-03-23 23:17:18 +00:00
d23d4a4557 Merge pull request #2020 from dolanmiu/dependabot/npm_and_yarn/prettier-2.8.6
build(deps-dev): bump prettier from 2.8.4 to 2.8.6
2023-03-23 23:16:58 +00:00
07c58ee43c Merge pull request #2022 from dolanmiu/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-5.56.0
build(deps-dev): bump @typescript-eslint/eslint-plugin from 5.55.0 to 5.56.0
2023-03-23 23:16:46 +00:00
1037e8f1b9 Merge pull request #2025 from dolanmiu/dependabot/npm_and_yarn/eslint-plugin-functional-5.0.7
build(deps-dev): bump eslint-plugin-functional from 5.0.6 to 5.0.7
2023-03-23 23:16:39 +00:00
f0ae8396f7 Merge pull request #2031 from dolanmiu/dependabot/npm_and_yarn/glob-9.3.2
build(deps-dev): bump glob from 9.3.0 to 9.3.2
2023-03-23 23:16:31 +00:00
1ae5995f9c Merge pull request #2033 from dolanmiu/feat/remove-redundant-keys
Remove unused type keys
2023-03-23 23:14:59 +00:00
751ad9d304 Remove unused type keys 2023-03-23 22:59:34 +00:00
0bd1514826 Merge pull request #2032 from dolanmiu/feat/fix-patcher-issue
#2028 Fix patcher offset indexes
2023-03-23 22:52:34 +00:00
16bfc78d8d #2028 Fix patcher offset indexes 2023-03-23 21:01:24 +00:00
528008406a Merge pull request #2016 from ewoutkleinsmann/master
Images in templates
2023-03-23 19:08:54 +00:00
9e3010ac6a Reduce coverage temporarily 2023-03-23 19:02:22 +00:00
7c1fa6f4bf build(deps-dev): bump glob from 9.3.0 to 9.3.2
Bumps [glob](https://github.com/isaacs/node-glob) from 9.3.0 to 9.3.2.
- [Release notes](https://github.com/isaacs/node-glob/releases)
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v9.3.0...v9.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-23 11:57:04 +00:00
f58d31cc3d build(deps-dev): bump rimraf from 4.4.0 to 4.4.1
Bumps [rimraf](https://github.com/isaacs/rimraf) from 4.4.0 to 4.4.1.
- [Release notes](https://github.com/isaacs/rimraf/releases)
- [Changelog](https://github.com/isaacs/rimraf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/rimraf/compare/v4.4.0...v4.4.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-23 11:56:45 +00:00
286c742ece build(deps-dev): bump eslint-plugin-functional from 5.0.6 to 5.0.7
Bumps [eslint-plugin-functional](https://github.com/eslint-functional/eslint-plugin-functional) from 5.0.6 to 5.0.7.
- [Release notes](https://github.com/eslint-functional/eslint-plugin-functional/releases)
- [Changelog](https://github.com/eslint-functional/eslint-plugin-functional/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint-functional/eslint-plugin-functional/compare/v5.0.6...v5.0.7)

---
updated-dependencies:
- dependency-name: eslint-plugin-functional
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-21 11:58:57 +00:00
3edc51d5a0 build(deps-dev): bump @typescript-eslint/parser from 5.55.0 to 5.56.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.55.0 to 5.56.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.56.0/packages/parser)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-21 11:58:10 +00:00
05d4c9688e build(deps-dev): bump @typescript-eslint/eslint-plugin
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.55.0 to 5.56.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.56.0/packages/eslint-plugin)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-21 11:57:39 +00:00
d4401d1597 build(deps): bump @types/node from 18.15.3 to 18.15.5
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.15.3 to 18.15.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-21 11:57:03 +00:00
90358e73f6 build(deps-dev): bump prettier from 2.8.4 to 2.8.6
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.4 to 2.8.6.
- [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/2.8.4...2.8.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-21 11:56:43 +00:00
a77ca02ea9 Set homepage to docx.js.org 2023-03-21 04:31:21 +00:00
7785f0df02 Add some tests to boost coverage 2023-03-21 04:30:20 +00:00
0e6ed6e5ea build(deps-dev): bump cspell from 6.30.0 to 6.30.2
Bumps [cspell](https://github.com/streetsidesoftware/cspell) from 6.30.0 to 6.30.2.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell/compare/v6.30.0...v6.30.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-20 11:57:45 +00:00
70e84c8d38 build(deps-dev): bump typedoc from 0.23.27 to 0.23.28
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.23.27 to 0.23.28.
- [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.23.27...v0.23.28)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-20 11:56:49 +00:00
11c3886659 Merge branch 'dolanmiu:master' into master 2023-03-20 12:08:28 +01:00
6104e5bc35 Update shelljs definitions 2023-03-19 19:46:06 +00:00
c748b9a7fc Remove src and template from files 2023-03-19 19:42:40 +00:00
714dbc0179 Add horizontal rule to border example 2023-03-19 15:47:45 +00:00
7f16cfc359 Remove unessesary glob types and fix typedocs 2023-03-19 04:22:52 +00:00
78757753c8 Version bump 2023-03-19 03:27:29 +00:00
8343edcdf1 Update documentation 2023-03-19 03:26:02 +00:00
1ab9e08eb9 Bump coverage 2023-03-19 01:35:29 +00:00
e1bc7f34c9 Merge pull request #2015 from dolanmiu/feat/allow-for-children-in-comments
#1988 Allow for children in comments
2023-03-19 01:00:22 +00:00
d5b495df5b #1988 Allow for children in comments 2023-03-19 00:18:38 +00:00
d6256d2acb Merge pull request #2012 from dolanmiu/dependabot/npm_and_yarn/eslint-plugin-jsdoc-40.1.0
build(deps-dev): bump eslint-plugin-jsdoc from 40.0.3 to 40.1.0
2023-03-18 23:46:34 +00:00
f1c7e448ae Merge pull request #2014 from dolanmiu/feat/regex-match-for-types-fixer
Add fix to allow for different types of slash to be supported
2023-03-18 23:46:25 +00:00
b600fd9324 Add fix to allow for different types of slash to be supported
Maybe updating glob broke it for Windows
2023-03-18 23:16:41 +00:00
3bf40ecb33 Merge pull request #2013 from dolanmiu/feat/add-src-to-npmignore
#2002 Add src to .npmignore
2023-03-18 23:11:59 +00:00
ffb650daa9 #2002 Add src to .npmignore 2023-03-18 21:27:08 +00:00
d2122e7806 build(deps-dev): bump eslint-plugin-jsdoc from 40.0.3 to 40.1.0
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 40.0.3 to 40.1.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/v40.0.3...v40.1.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>
2023-03-18 21:21:06 +00:00
6b6eb1d7a2 Merge pull request #2008 from dolanmiu/dependabot/npm_and_yarn/typedoc-0.23.27
build(deps-dev): bump typedoc from 0.23.26 to 0.23.27
2023-03-18 21:20:52 +00:00
c80d881105 Merge pull request #2009 from dolanmiu/dependabot/npm_and_yarn/sinon-15.0.2
build(deps-dev): bump sinon from 15.0.1 to 15.0.2
2023-03-18 21:20:45 +00:00
3c223c69e9 Merge pull request #2010 from dolanmiu/dependabot/npm_and_yarn/typescript-5.0.2
build(deps-dev): bump typescript from 4.9.5 to 5.0.2
2023-03-18 21:20:38 +00:00
2420321008 Merge pull request #2011 from dolanmiu/dependabot/npm_and_yarn/eslint-plugin-jsdoc-40.0.3
build(deps-dev): bump eslint-plugin-jsdoc from 40.0.1 to 40.0.3
2023-03-18 21:20:30 +00:00
29d5421cea Merge pull request #2003 from dolanmiu/dependabot/npm_and_yarn/glob-9.3.0
build(deps-dev): bump glob from 8.1.0 to 9.3.0
2023-03-18 21:20:22 +00:00
71953cf45a Fixes patchDocument for files with binary content 2023-03-17 16:30:35 +01:00
325866d9c3 build(deps-dev): bump eslint-plugin-jsdoc from 40.0.1 to 40.0.3
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 40.0.1 to 40.0.3.
- [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/v40.0.1...v40.0.3)

---
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>
2023-03-17 11:57:54 +00:00
457e9c12e3 build(deps-dev): bump typescript from 4.9.5 to 5.0.2
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.5 to 5.0.2.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.9.5...v5.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-17 11:57:34 +00:00
ff4a66466a build(deps-dev): bump sinon from 15.0.1 to 15.0.2
Bumps [sinon](https://github.com/sinonjs/sinon) from 15.0.1 to 15.0.2.
- [Release notes](https://github.com/sinonjs/sinon/releases)
- [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md)
- [Commits](https://github.com/sinonjs/sinon/compare/v15.0.1...v15.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-17 11:57:12 +00:00
cc142cc052 build(deps-dev): bump typedoc from 0.23.26 to 0.23.27
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.23.26 to 0.23.27.
- [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.23.26...v0.23.27)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-17 11:56:43 +00:00
6e0dc69217 build(deps-dev): bump glob from 8.1.0 to 9.3.0
Bumps [glob](https://github.com/isaacs/node-glob) from 8.1.0 to 9.3.0.
- [Release notes](https://github.com/isaacs/node-glob/releases)
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v8.1.0...v9.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-17 02:55:01 +00:00
33332bcf66 Merge pull request #1993 from dolanmiu/dependabot/npm_and_yarn/eslint-plugin-functional-5.0.6
build(deps-dev): bump eslint-plugin-functional from 5.0.4 to 5.0.6
2023-03-17 02:54:26 +00:00
b94f9ac25b Merge pull request #1920 from dolanmiu/feat/float-options
Use next attribute component
2023-03-17 02:42:49 +00:00
0a7be48dcb build(deps-dev): bump eslint-plugin-functional from 5.0.4 to 5.0.6
Bumps [eslint-plugin-functional](https://github.com/eslint-functional/eslint-plugin-functional) from 5.0.4 to 5.0.6.
- [Release notes](https://github.com/eslint-functional/eslint-plugin-functional/releases)
- [Changelog](https://github.com/eslint-functional/eslint-plugin-functional/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint-functional/eslint-plugin-functional/compare/v5.0.4...v5.0.6)

---
updated-dependencies:
- dependency-name: eslint-plugin-functional
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-17 02:41:12 +00:00
3f3397620e Merge pull request #2005 from dolanmiu/dependabot/npm_and_yarn/cspell-6.30.0
build(deps-dev): bump cspell from 6.28.0 to 6.30.0
2023-03-17 02:40:17 +00:00
154bc1df95 Merge pull request #2004 from dolanmiu/dependabot/npm_and_yarn/webpack-5.76.2
build(deps-dev): bump webpack from 5.75.0 to 5.76.2
2023-03-17 02:40:09 +00:00
715e436c4c Merge pull request #2000 from dolanmiu/dependabot/npm_and_yarn/types/node-18.15.3
build(deps): bump @types/node from 18.14.5 to 18.15.3
2023-03-17 02:39:59 +00:00
f5a9bcf839 Merge pull request #1999 from dolanmiu/dependabot/npm_and_yarn/typescript-eslint/parser-5.55.0
build(deps-dev): bump @typescript-eslint/parser from 5.54.0 to 5.55.0
2023-03-17 02:39:50 +00:00
31d2da86ff Merge pull request #1998 from dolanmiu/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-5.55.0
build(deps-dev): bump @typescript-eslint/eslint-plugin from 5.54.0 to 5.55.0
2023-03-17 02:39:21 +00:00
9dad3ca356 Merge pull request #1994 from dolanmiu/dependabot/npm_and_yarn/eslint-8.36.0
build(deps-dev): bump eslint from 8.35.0 to 8.36.0
2023-03-17 02:39:08 +00:00
355e01edc6 Merge pull request #1979 from dolanmiu/dependabot/npm_and_yarn/eslint-plugin-unicorn-46.0.0
build(deps-dev): bump eslint-plugin-unicorn from 45.0.2 to 46.0.0
2023-03-17 02:38:58 +00:00
c19bf1c404 Merge pull request #2007 from dolanmiu/feat/autoSpaceDN
#1949 - Add auto space DN feature
2023-03-17 02:34:53 +00:00
970450074d #1949 - Add auto space DN feature 2023-03-17 02:20:51 +00:00
91e8295648 Merge pull request #2006 from dolanmiu/feat/deprecate-templates
Deprecate import dotx
2023-03-17 01:26:26 +00:00
3e40ae862b Remove demo 30 from CI 2023-03-17 01:15:45 +00:00
236cce604f Fix tests 2023-03-17 00:54:29 +00:00
0388a564b5 Deprecate import dotx 2023-03-17 00:20:55 +00:00
cd77ceba69 build(deps-dev): bump cspell from 6.28.0 to 6.30.0
Bumps [cspell](https://github.com/streetsidesoftware/cspell) from 6.28.0 to 6.30.0.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell/compare/v6.28.0...v6.30.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-16 23:38:36 +00:00
5ae8731abe build(deps-dev): bump @typescript-eslint/parser from 5.54.0 to 5.55.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.54.0 to 5.55.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.55.0/packages/parser)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-16 23:38:28 +00:00
7bb90c55d1 build(deps-dev): bump eslint from 8.35.0 to 8.36.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.35.0 to 8.36.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/v8.35.0...v8.36.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-16 23:38:24 +00:00
a07d6378e8 Merge pull request #1960 from dolanmiu/feat/new-template
Document Patcher - Re-write / Re-vamp template feature
2023-03-16 23:37:31 +00:00
4fb8d277b4 Change demo import from src to build 2023-03-16 20:33:54 +00:00
73186ce920 build(deps-dev): bump webpack from 5.75.0 to 5.76.2
Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.76.2.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.76.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-16 11:57:58 +00:00
d02f98956f Fix spelling errors 2023-03-16 03:24:51 +00:00
d186d42bb6 Merge branch 'master' into feat/new-template
# Conflicts:
#	package-lock.json
2023-03-16 03:18:50 +00:00
338f7be967 Add chai as promised and fix async test 2023-03-16 03:14:01 +00:00
b63a6e6e16 Update coverage stats 2023-03-16 02:02:02 +00:00
7e9884081e Add tests to patcher 2023-03-16 01:55:18 +00:00
262f6323d0 Write more tests and simplify code 2023-03-15 03:14:38 +00:00
811dd61562 Add tests 2023-03-15 02:46:39 +00:00
9801879063 build(deps): bump @types/node from 18.14.5 to 18.15.3
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.14.5 to 18.15.3.
- [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>
2023-03-14 11:58:04 +00:00
e30836c935 build(deps-dev): bump @typescript-eslint/eslint-plugin
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.54.0 to 5.55.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.55.0/packages/eslint-plugin)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-14 11:56:58 +00:00
352511bb55 Add tests 2023-03-14 03:13:23 +00:00
8ce057e25c Context per view wrapper 2023-03-13 21:35:16 +00:00
0775ecb08b Merge pull request #1990 from dolanmiu/dependabot/npm_and_yarn/tsconfig-paths-webpack-plugin-4.0.1
build(deps-dev): bump tsconfig-paths-webpack-plugin from 4.0.0 to 4.0.1
2023-03-10 02:33:07 +00:00
e909ab9886 Merge pull request #1991 from dolanmiu/dependabot/npm_and_yarn/rimraf-4.4.0
build(deps-dev): bump rimraf from 4.2.0 to 4.4.0
2023-03-10 02:32:57 +00:00
e03bb155b9 build(deps-dev): bump rimraf from 4.2.0 to 4.4.0
Bumps [rimraf](https://github.com/isaacs/rimraf) from 4.2.0 to 4.4.0.
- [Release notes](https://github.com/isaacs/rimraf/releases)
- [Changelog](https://github.com/isaacs/rimraf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/rimraf/compare/v4.2.0...v4.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-09 11:57:17 +00:00
91620d61d8 build(deps-dev): bump tsconfig-paths-webpack-plugin from 4.0.0 to 4.0.1
Bumps [tsconfig-paths-webpack-plugin](https://github.com/dividab/tsconfig-paths-webpack-plugin) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/dividab/tsconfig-paths-webpack-plugin/releases)
- [Changelog](https://github.com/dividab/tsconfig-paths-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dividab/tsconfig-paths-webpack-plugin/compare/v4.0.0...v4.0.1)

---
updated-dependencies:
- dependency-name: tsconfig-paths-webpack-plugin
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-09 11:56:45 +00:00
0fba450c9a Allow patching of ExternalHyperlinks 2023-03-08 23:30:51 +00:00
ca604fb004 Merge pull request #1982 from dolanmiu/dependabot/npm_and_yarn/cspell-6.28.0
build(deps-dev): bump cspell from 6.27.0 to 6.28.0
2023-03-06 16:13:02 +00:00
9e998d233c build(deps-dev): bump cspell from 6.27.0 to 6.28.0
Bumps [cspell](https://github.com/streetsidesoftware/cspell) from 6.27.0 to 6.28.0.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell/compare/v6.27.0...v6.28.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-06 11:57:29 +00:00
6c0fd3adec build(deps-dev): bump eslint-plugin-unicorn from 45.0.2 to 46.0.0
Bumps [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) from 45.0.2 to 46.0.0.
- [Release notes](https://github.com/sindresorhus/eslint-plugin-unicorn/releases)
- [Commits](https://github.com/sindresorhus/eslint-plugin-unicorn/compare/v45.0.2...v46.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-06 11:56:43 +00:00
6ad18420e5 Add hyperlink to demo 2023-03-05 18:46:02 +00:00
ef95a16f39 Merge pull request #1975 from dolanmiu/dependabot/npm_and_yarn/rimraf-4.2.0
build(deps-dev): bump rimraf from 4.1.2 to 4.2.0
2023-03-05 04:44:07 +00:00
53861ec18d Merge pull request #1976 from dolanmiu/dependabot/npm_and_yarn/eslint-plugin-jsdoc-40.0.1
build(deps-dev): bump eslint-plugin-jsdoc from 40.0.0 to 40.0.1
2023-03-05 04:44:00 +00:00
81b1569ad2 Merge pull request #1977 from dolanmiu/dependabot/npm_and_yarn/types/node-18.14.5
build(deps): bump @types/node from 18.14.2 to 18.14.5
2023-03-05 04:43:52 +00:00
66a1992da0 Add ImageRun feature 2023-03-03 23:47:50 +00:00
100356c344 build(deps): bump @types/node from 18.14.2 to 18.14.5
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.14.2 to 18.14.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-03 11:57:17 +00:00
31905a27b1 build(deps-dev): bump eslint-plugin-jsdoc from 40.0.0 to 40.0.1
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 40.0.0 to 40.0.1.
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Changelog](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.releaserc)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v40.0.0...v40.0.1)

---
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>
2023-03-03 11:57:05 +00:00
08916176a2 build(deps-dev): bump rimraf from 4.1.2 to 4.2.0
Bumps [rimraf](https://github.com/isaacs/rimraf) from 4.1.2 to 4.2.0.
- [Release notes](https://github.com/isaacs/rimraf/releases)
- [Changelog](https://github.com/isaacs/rimraf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/rimraf/compare/v4.1.2...v4.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-03 11:56:40 +00:00
ffb3d63c92 Merge pull request #1968 from dolanmiu/dependabot/npm_and_yarn/typescript-eslint/parser-5.54.0
build(deps-dev): bump @typescript-eslint/parser from 5.53.0 to 5.54.0
2023-03-01 03:32:54 +00:00
22005428ba Merge pull request #1967 from dolanmiu/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-5.54.0
build(deps-dev): bump @typescript-eslint/eslint-plugin from 5.52.0 to 5.54.0
2023-02-28 19:35:29 +00:00
a70acd6d63 build(deps-dev): bump @typescript-eslint/parser from 5.53.0 to 5.54.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.53.0 to 5.54.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.54.0/packages/parser)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-28 11:56:53 +00:00
af9e0b0d3f build(deps-dev): bump @typescript-eslint/eslint-plugin
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.52.0 to 5.54.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.54.0/packages/eslint-plugin)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-28 10:59:29 +00:00
a0c2be8af2 Merge pull request #1955 from dolanmiu/dependabot/npm_and_yarn/typescript-eslint/parser-5.53.0
build(deps-dev): bump @typescript-eslint/parser from 5.52.0 to 5.53.0
2023-02-28 10:58:30 +00:00
df9d4b4219 Merge pull request #1961 from dolanmiu/dependabot/npm_and_yarn/cspell-6.27.0
build(deps-dev): bump cspell from 6.26.3 to 6.27.0
2023-02-28 02:55:00 +00:00
cdf5015920 Merge pull request #1962 from dolanmiu/dependabot/npm_and_yarn/eslint-8.35.0
build(deps-dev): bump eslint from 8.34.0 to 8.35.0
2023-02-28 02:52:54 +00:00
fe9b438a51 Update documentation 2023-02-27 21:06:54 +00:00
59cbf8c9cb build(deps-dev): bump cspell from 6.26.3 to 6.27.0
Bumps [cspell](https://github.com/streetsidesoftware/cspell) from 6.26.3 to 6.27.0.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell/compare/v6.26.3...v6.27.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-27 16:54:32 +00:00
7af8175034 build(deps-dev): bump eslint from 8.34.0 to 8.35.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.34.0 to 8.35.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/v8.34.0...v8.35.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-27 16:53:42 +00:00
bf0393b485 build(deps-dev): bump @typescript-eslint/parser from 5.52.0 to 5.53.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.52.0 to 5.53.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.53.0/packages/parser)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-27 16:53:31 +00:00
83a8822cf7 Merge pull request #1957 from dolanmiu/dependabot/npm_and_yarn/types/glob-8.1.0
build(deps-dev): bump @types/glob from 8.0.1 to 8.1.0
2023-02-27 16:53:03 +00:00
eada41b8a1 Merge pull request #1964 from dolanmiu/dependabot/npm_and_yarn/typedoc-0.23.26
build(deps-dev): bump typedoc from 0.23.25 to 0.23.26
2023-02-27 16:52:55 +00:00
2c03377c47 Merge pull request #1963 from dolanmiu/dependabot/npm_and_yarn/types/node-18.14.2
build(deps): bump @types/node from 18.13.0 to 18.14.2
2023-02-27 16:52:45 +00:00
484fc4aa2d build(deps-dev): bump typedoc from 0.23.25 to 0.23.26
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.23.25 to 0.23.26.
- [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.23.25...v0.23.26)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-27 11:58:30 +00:00
2846014db0 build(deps): bump @types/node from 18.13.0 to 18.14.2
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.13.0 to 18.14.2.
- [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>
2023-02-27 11:58:10 +00:00
c9d86619de Clean up 2023-02-25 22:18:56 +00:00
ce485dbc29 Simplify patcher
Use map notation
2023-02-25 22:18:31 +00:00
4e875b4744 Re-name templater to patcher 2023-02-25 20:18:00 +00:00
3f6c006716 Allow for it to work with all xml files under word/ 2023-02-25 19:34:04 +00:00
f8f5d43b0c Allow for Paragraph and array patching 2023-02-25 19:33:12 +00:00
8c0fe00c6f Add launch json 2023-02-24 02:55:53 +00:00
c37d9ca5b3 Add ts-node to tsconfig 2023-02-24 01:44:53 +00:00
f3dc1f0712 Work on replacer logic 2023-02-24 01:05:43 +00:00
a4d96bbf6e Add paragraph token replacer 2023-02-23 19:43:19 +00:00
90c40178aa build(deps-dev): bump @types/glob from 8.0.1 to 8.1.0
Bumps [@types/glob](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/glob) from 8.0.1 to 8.1.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/glob)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-23 11:11:19 +00:00
2ee96b419a Merge branch 'master' into feat/new-template 2023-02-22 20:39:59 +00:00
773900b620 Merge branch 'master' of github.com:dolanmiu/docx 2023-02-22 20:17:58 +00:00
ece440340f Add footnotes documentation 2023-02-22 20:16:23 +00:00
5233b4b5e6 Simple patcher working 2023-02-18 20:36:24 +00:00
86de252a52 Extract runs and text 2023-02-17 10:38:03 +00:00
c206d23480 work on docx patcher 2023-02-16 20:17:48 +00:00
5a53a138d9 Merge pull request #1946 from dolanmiu/dependabot/npm_and_yarn/cspell-6.26.3
build(deps-dev): bump cspell from 6.22.0 to 6.26.3
2023-02-16 16:36:43 +00:00
5b5deb198e Merge pull request #1941 from dolanmiu/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-5.52.0
build(deps-dev): bump @typescript-eslint/eslint-plugin from 5.51.0 to 5.52.0
2023-02-16 16:36:30 +00:00
88a30e5af7 Merge pull request #1940 from dolanmiu/dependabot/npm_and_yarn/typescript-eslint/parser-5.52.0
build(deps-dev): bump @typescript-eslint/parser from 5.51.0 to 5.52.0
2023-02-16 16:36:18 +00:00
0b9ffa9a7b Merge pull request #1938 from dolanmiu/dependabot/npm_and_yarn/typedoc-0.23.25
build(deps-dev): bump typedoc from 0.23.24 to 0.23.25
2023-02-16 16:36:07 +00:00
5052bcde4f Merge pull request #1937 from dolanmiu/dependabot/npm_and_yarn/eslint-8.34.0
build(deps-dev): bump eslint from 8.33.0 to 8.34.0
2023-02-16 16:35:54 +00:00
d5409678ee Merge pull request #1945 from cledoux95/readme/docx-used-by-novelpad
Add NovelPad to Companies
2023-02-16 16:35:24 +00:00
72c39a9eb2 build(deps-dev): bump cspell from 6.22.0 to 6.26.3
Bumps [cspell](https://github.com/streetsidesoftware/cspell) from 6.22.0 to 6.26.3.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell/blob/main/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell/compare/v6.22.0...v6.26.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-16 11:57:22 +00:00
2ed7a28d1e Fixed Whitespace 2023-02-15 18:36:28 -05:00
7b2ff00c83 Add NovelPad to Companies 2023-02-15 18:34:40 -05:00
d5b0083d77 build(deps-dev): bump eslint from 8.33.0 to 8.34.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.33.0 to 8.34.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/v8.33.0...v8.34.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-15 20:26:23 +00:00
406183d104 Merge pull request #1935 from dolanmiu/dependabot/npm_and_yarn/eslint-plugin-jsdoc-40.0.0
build(deps-dev): bump eslint-plugin-jsdoc from 39.8.0 to 40.0.0
2023-02-15 20:25:17 +00:00
c6d3e60314 build(deps-dev): bump @typescript-eslint/eslint-plugin
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.51.0 to 5.52.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.52.0/packages/eslint-plugin)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-14 11:57:37 +00:00
be71037a13 build(deps-dev): bump @typescript-eslint/parser from 5.51.0 to 5.52.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.51.0 to 5.52.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.52.0/packages/parser)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-14 11:56:54 +00:00
c8c64b320b build(deps-dev): bump typedoc from 0.23.24 to 0.23.25
Bumps [typedoc](https://github.com/TypeStrong/TypeDoc) from 0.23.24 to 0.23.25.
- [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.23.24...v0.23.25)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-13 11:59:09 +00:00
8514e8ad31 build(deps-dev): bump eslint-plugin-jsdoc from 39.8.0 to 40.0.0
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 39.8.0 to 40.0.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/v39.8.0...v40.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-13 11:57:05 +00:00
ffd998cbf5 Simple import and export of document 2023-02-08 03:18:11 +00:00
8cbc6c15fe Fix linting 2023-02-04 23:53:37 +00:00
2c21f64b9f Remove only 2023-02-04 23:50:59 +00:00
7ec9cff433 Add tests for BuilderElement 2023-02-04 23:50:37 +00:00
3f979b9981 Remove unused code 2023-02-04 22:48:33 +00:00
3077ca96a7 Use next attribute component 2023-02-04 22:11:47 +00:00
66 changed files with 4599 additions and 1455 deletions

View File

@ -301,15 +301,6 @@ jobs:
with: with:
xml-file: build/extracted-doc/word/document.xml xml-file: build/extracted-doc/word/document.xml
xml-schema-file: ooxml-schemas/microsoft/wml-2010.xsd xml-schema-file: ooxml-schemas/microsoft/wml-2010.xsd
- name: Run Demo
run: npm run ts-node -- ./demo/30-template-document.ts
- name: Extract Word Document
run: npm run extract
- name: Validate XML
uses: ChristophWurst/xmllint-action@v1
with:
xml-file: build/extracted-doc/word/document.xml
xml-schema-file: ooxml-schemas/microsoft/wml-2010.xsd
- name: Run Demo - name: Run Demo
run: npm run ts-node -- ./demo/31-tables.ts run: npm run ts-node -- ./demo/31-tables.ts
- name: Extract Word Document - name: Extract Word Document

View File

@ -40,4 +40,7 @@ build-tests
.vscode .vscode
# docs # docs
docs docs
# src
src

6
.nycrc
View File

@ -1,9 +1,9 @@
{ {
"check-coverage": true, "check-coverage": true,
"statements": 99.79, "statements": 99.87,
"branches": 98.41, "branches": 98.21,
"functions": 100, "functions": 100,
"lines": 99.73, "lines": 99.86,
"include": [ "include": [
"src/**/*.ts" "src/**/*.ts"
], ],

18
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Demo",
"type": "node",
"request": "launch",
"runtimeArgs": [
"-r",
"${workspaceFolder}/node_modules/ts-node/register",
"-r",
"${workspaceFolder}/node_modules/tsconfig-paths/register"
],
"cwd": "${workspaceRoot}",
"program": "${workspaceFolder}/demo/85-template-document.ts"
}
]
}

View File

@ -3,7 +3,7 @@
</p> </p>
<p align="center"> <p align="center">
Easily generate .docx files with JS/TS. Works for Node and on the Browser. Easily generate and modify .docx files with JS/TS. Works for Node and on the Browser.
</p> </p>
--- ---
@ -88,6 +88,7 @@ Read the contribution guidelines [here](https://docx.js.org/#/contribution-guide
[<img src="https://i.imgur.com/cmykN7c.png" alt="drawing"/>](https://www.arity.co/) [<img src="https://i.imgur.com/cmykN7c.png" alt="drawing"/>](https://www.arity.co/)
[<img src="https://i.imgur.com/PXo25um.png" alt="drawing" height="50"/>](https://www.circadianrisk.com/) [<img src="https://i.imgur.com/PXo25um.png" alt="drawing" height="50"/>](https://www.circadianrisk.com/)
[<img src="https://i.imgur.com/AKGhtlh.png" alt="drawing"/>](https://lexense.com/) [<img src="https://i.imgur.com/AKGhtlh.png" alt="drawing"/>](https://lexense.com/)
[<img src="https://i.imgur.com/9tqJaHw.png" alt="drawing" height="50"/>](https://novelpad.co/)
...and many more! ...and many more!

View File

@ -25,6 +25,17 @@ const doc = new Document({
}, },
}, },
}), }),
new Paragraph({
text: "",
border: {
top: {
color: "auto",
space: 1,
style: BorderStyle.SINGLE,
size: 6,
},
},
}),
new Paragraph({ new Paragraph({
children: [ children: [
new TextRun({ new TextRun({

View File

@ -1,35 +0,0 @@
// Example on how to use a template document
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import { Document, ImportDotx, Packer, Paragraph } from "../build";
const importDotx = new ImportDotx();
const filePath = "./demo/dotx/template.dotx";
fs.readFile(filePath, (err, data) => {
if (err) {
throw new Error(`Failed to read file ${filePath}.`);
}
importDotx.extract(data).then((templateDocument) => {
const doc = new Document(
{
sections: [
{
properties: {
titlePage: templateDocument.titlePageIsDefined,
},
children: [new Paragraph("Hello World")],
},
],
},
{
template: templateDocument,
},
);
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});
});
});

View File

@ -5,7 +5,34 @@ import { Document, Packer, Paragraph, TextRun, CommentRangeStart, CommentRangeEn
const doc = new Document({ const doc = new Document({
comments: { comments: {
children: [{ id: 0, author: "Ray Chen", date: new Date(), text: "comment text content" }], children: [
{
id: 0,
author: "Ray Chen",
date: new Date(),
children: [
new Paragraph({
children: [
new TextRun({
text: "some initial text content",
}),
],
}),
new Paragraph({
children: [
new TextRun({
text: "comment text content",
}),
new TextRun({ text: "", break: 1 }),
new TextRun({
text: "More text here",
bold: true,
}),
],
}),
],
},
],
}, },
sections: [ sections: [
{ {

View File

@ -0,0 +1,155 @@
// Patch a document with patches
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import {
ExternalHyperlink,
HeadingLevel,
ImageRun,
Paragraph,
patchDocument,
PatchType,
Table,
TableCell,
TableRow,
TextDirection,
TextRun,
VerticalAlign,
} from "../build";
patchDocument(fs.readFileSync("demo/assets/simple-template.docx"), {
patches: {
name: {
type: PatchType.PARAGRAPH,
children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")],
},
table_heading_1: {
type: PatchType.PARAGRAPH,
children: [new TextRun("Heading wow!")],
},
item_1: {
type: PatchType.PARAGRAPH,
children: [
new TextRun("#657"),
new ExternalHyperlink({
children: [
new TextRun({
text: "BBC News Link",
}),
],
link: "https://www.bbc.co.uk/news",
}),
],
},
paragraph_replace: {
type: PatchType.DOCUMENT,
children: [
new Paragraph("Lorem ipsum paragraph"),
new Paragraph("Another paragraph"),
new Paragraph({
children: [
new TextRun("This is a "),
new ExternalHyperlink({
children: [
new TextRun({
text: "Google Link",
}),
],
link: "https://www.google.co.uk",
}),
new ImageRun({ data: fs.readFileSync("./demo/images/dog.png"), transformation: { width: 100, height: 100 } }),
],
}),
],
},
header_adjective: {
type: PatchType.PARAGRAPH,
children: [new TextRun("Delightful Header")],
},
footer_text: {
type: PatchType.PARAGRAPH,
children: [
new TextRun("replaced just as"),
new TextRun(" well"),
new ExternalHyperlink({
children: [
new TextRun({
text: "BBC News Link",
}),
],
link: "https://www.bbc.co.uk/news",
}),
],
},
image_test: {
type: PatchType.PARAGRAPH,
children: [new ImageRun({ data: fs.readFileSync("./demo/images/image1.jpeg"), transformation: { width: 100, height: 100 } })],
},
table: {
type: PatchType.DOCUMENT,
children: [
new Table({
rows: [
new TableRow({
children: [
new TableCell({
children: [new Paragraph({}), new Paragraph({})],
verticalAlign: VerticalAlign.CENTER,
}),
new TableCell({
children: [new Paragraph({}), new Paragraph({})],
verticalAlign: VerticalAlign.CENTER,
}),
new TableCell({
children: [new Paragraph({ text: "bottom to top" }), new Paragraph({})],
textDirection: TextDirection.BOTTOM_TO_TOP_LEFT_TO_RIGHT,
}),
new TableCell({
children: [new Paragraph({ text: "top to bottom" }), new Paragraph({})],
textDirection: TextDirection.TOP_TO_BOTTOM_RIGHT_TO_LEFT,
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [
new Paragraph({
text: "Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah Blah",
heading: HeadingLevel.HEADING_1,
}),
],
}),
new TableCell({
children: [
new Paragraph({
text: "This text should be in the middle of the cell",
}),
],
verticalAlign: VerticalAlign.CENTER,
}),
new TableCell({
children: [
new Paragraph({
text: "Text above should be vertical from bottom to top",
}),
],
verticalAlign: VerticalAlign.CENTER,
}),
new TableCell({
children: [
new Paragraph({
text: "Text above should be vertical from top to bottom",
}),
],
verticalAlign: VerticalAlign.CENTER,
}),
],
}),
],
}),
],
},
},
}).then((doc) => {
fs.writeFileSync("My Document.docx", doc);
});

View File

@ -0,0 +1,20 @@
// Generate a template document
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import { Document, Packer, Paragraph, TextRun } from "../build";
const doc = new Document({
sections: [
{
children: [
new Paragraph({
children: [new TextRun("{{template}}")],
}),
],
},
],
});
Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});

View File

@ -0,0 +1,15 @@
// Patch a document with patches
// Import from 'docx' rather than '../build' if you install from npm
import * as fs from "fs";
import { patchDocument, PatchType, TextRun } from "../build";
patchDocument(fs.readFileSync("demo/assets/simple-template-2.docx"), {
patches: {
name: {
type: PatchType.PARAGRAPH,
children: [new TextRun("Max")],
},
},
}).then((doc) => {
fs.writeFileSync("My Document.docx", doc);
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,10 +1,10 @@
<img src="https://i.imgur.com/37uBGhO.gif" alt="drawing" style="width:200px;"/> <img src="https://i.imgur.com/37uBGhO.gif" alt="drawing" style="width:200px;"/>
> Easily generate .docx files with JS/TS. Works for Node and on the Browser. :100: > Easily generate and modify .docx files with JS/TS. Works for Node and on the Browser. :100:
- Simple, declarative API - Simple, declarative API
- 60+ usage examples - 80+ usage examples
- Battle tested, mature, 99%+ coverage - Battle tested, mature, 99.9%+ coverage
[GitHub](https://github.com/dolanmiu/docx) [GitHub](https://github.com/dolanmiu/docx)
[Get Started](#Welcome) [Get Started](#Welcome)

View File

@ -1,6 +1,8 @@
- [Getting Started](/) - [Getting Started](/)
- [Examples](https://github.com/dolanmiu/docx/tree/master/demo) - Examples
- [Demos](https://github.com/dolanmiu/docx/tree/master/demo)
- API - API
@ -36,6 +38,10 @@
- [Packers](usage/packers.md) - [Packers](usage/packers.md)
- Modifying Existing Documents
- [Patcher](usage/patcher.md)
- Utility - Utility
- [Convenience functions](usage/convenience-functions.md) - [Convenience functions](usage/convenience-functions.md)

View File

@ -1,11 +1,21 @@
# Contribution Guidelines # Contribution Guidelines
- Include documentation reference(s) at the top of each file: - Include documentation reference(s) at the top of each file as a comment. For example:
```ts ```ts
// http://officeopenxml.com/WPdocument.php // http://officeopenxml.com/WPdocument.php
``` ```
<!-- cSpell:ignore datypic -->
It can be a link to `officeopenxml.com` or `datypic.com` etc.
It could also be a reference to the official ECMA-376 standard: https://www.ecma-international.org/publications-and-standards/standards/ecma-376/
- Include a portion of the schema as a comment for cross reference. For example:
```ts
// <xsd:element name="tbl" type="CT_Tbl" minOccurs="0" maxOccurs="1"/>
```
- Follow Prettier standards, and consider using the [Prettier VSCode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) plugin. - Follow Prettier standards, and consider using the [Prettier VSCode](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) plugin.
- Follow the `ESLint` rules - Follow the `ESLint` rules

42
docs/usage/footnotes.md Normal file
View File

@ -0,0 +1,42 @@
# Footnotes
!> Footnotes requires an understanding of [Sections](usage/sections.md).
Use footnotes and endnotes to explain, comment on, or provide references to something in a document. Usually, footnotes appear at the bottom of the page.
## Example
```ts
const doc = new Document({
footnotes: {
1: { children: [new Paragraph("Foo"), new Paragraph("Bar")] },
2: { children: [new Paragraph("Test")] },
},
sections: [
{
children: [
new Paragraph({
children: [
new TextRun({
children: ["Hello"],
}),
new FootnoteReferenceRun(1),
new TextRun({
children: [" World!"],
}),
new FootnoteReferenceRun(2),
],
}),
],
},
],
});
```
## Usage
Footnotes requires an entry into the `footnotes` array in the `Document` constructor, and a `FootnoteReferenceRun` in the `Paragraph` constructor.
`footnotes` is an object of number to `Footnote` objects. The number is the reference number, and the `Footnote` object is the content of the footnote. The `Footnote` object has a `children` property, which is an array of `Paragraph` objects.
`FootnoteReferenceRun` is a `Run` object, which are added to `Paragraph`s. It takes a number as a parameter, which is the reference number of the footnote.

94
docs/usage/patcher.md Normal file
View File

@ -0,0 +1,94 @@
# Patcher
The patcher allows you to modify existing documents, and add new content to them.
!> The Patcher requires an understanding of [Paragraphs](usage/paragraph.md).
---
## Usage
```ts
import * as fs from "fs";
import { patchDocument } from "docx";
patchDocument(fs.readFileSync("My Document.docx"), {
patches: {
// Patches here
},
});
```
## Patches
The patcher takes in a `patches` object, which is a map of `string` to `Patch`:
```ts
interface Patch {
type: PatchType;
children: FileChild[] | ParagraphChild[];
}
```
| Property | Type | Notes | Possible Values |
| -------- | --------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| type | `PatchType` | Required | `DOCUMENT`, `PARAGRAPH` |
| children | `FileChild[] or ParagraphChild[]` | Required | The contents to replace with. A `FileChild` is a `Paragraph` or `Table`, whereas a `ParagraphChild` is typical `Paragraph` children. |
### How to patch existing document
1. Open your existing word document in your favorite Word Processor
2. Write tags in the document where you want to patch in a mustache style notation. For example, `{{my_patch}}` and `{{my_second_patch}}`.
3. Run the patcher with the patches as a key value pair.
## Example
### Word Document
![Word Document screenshot](https://i.imgur.com/ybkvw6Z.png)
### Patcher
?> Notice how there is no handlebar notation in the key.
The patch can be as simple as a string, or as complex as a table. Images, hyperlinks, and other complex elements within the `docx` library are also supported.
```ts
patchDocument(fs.readFileSync("My Document.docx"), {
patches: {
my_patch: {
type: PatchType.PARAGRAPH,
children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")],
},
my_second_patch: {
type: PatchType.DOCUMENT,
children: [
new Paragraph("Lorem ipsum paragraph"),
new Paragraph("Another paragraph"),
new Paragraph({
children: [
new TextRun("This is a "),
new ExternalHyperlink({
children: [
new TextRun({
text: "Google Link",
}),
],
link: "https://www.google.co.uk",
}),
new ImageRun({ data: fs.readFileSync("./demo/images/dog.png"), transformation: { width: 100, height: 100 } }),
],
}),
],
},
},
});
```
---
## Demo
_Source: https://github.com/dolanmiu/docx/blob/master/demo/85-template-document.ts_
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/85-template-document.ts ":include :type=code typescript")

2072
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "docx", "name": "docx",
"version": "7.8.2", "version": "8.0.1",
"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.",
"main": "build/index.js", "main": "build/index.js",
"scripts": { "scripts": {
@ -28,9 +28,7 @@
"lint" "lint"
], ],
"files": [ "files": [
"src", "build"
"build",
"template"
], ],
"repository": { "repository": {
"type": "git", "type": "git",
@ -61,14 +59,14 @@
"bugs": { "bugs": {
"url": "https://github.com/dolanmiu/docx/issues" "url": "https://github.com/dolanmiu/docx/issues"
}, },
"homepage": "https://github.com/dolanmiu/docx#readme", "homepage": "https://docx.js.org",
"devDependencies": { "devDependencies": {
"@types/chai": "^4.2.15", "@types/chai": "^4.2.15",
"@types/glob": "^8.0.0", "@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^10.0.0", "@types/mocha": "^10.0.0",
"@types/prompt": "^1.1.1", "@types/prompt": "^1.1.1",
"@types/request-promise": "^4.1.42", "@types/request-promise": "^4.1.42",
"@types/shelljs": "^0.8.9", "@types/shelljs": "^0.8.11",
"@types/sinon": "^10.0.0", "@types/sinon": "^10.0.0",
"@types/unzipper": "^0.10.4", "@types/unzipper": "^0.10.4",
"@types/webpack": "^5.0.0", "@types/webpack": "^5.0.0",
@ -76,16 +74,17 @@
"@typescript-eslint/parser": "^5.36.1", "@typescript-eslint/parser": "^5.36.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"chai": "^4.3.6", "chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"cspell": "^6.2.2", "cspell": "^6.2.2",
"docsify-cli": "^4.3.0", "docsify-cli": "^4.3.0",
"eslint": "^8.23.0", "eslint": "^8.23.0",
"eslint-plugin-functional": "^5.0.1", "eslint-plugin-functional": "^5.0.1",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^39.3.6", "eslint-plugin-jsdoc": "^40.0.0",
"eslint-plugin-no-null": "^1.0.2", "eslint-plugin-no-null": "^1.0.2",
"eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-unicorn": "^45.0.0", "eslint-plugin-unicorn": "^46.0.0",
"glob": "^8.0.1", "glob": "^9.3.0",
"jszip": "^3.1.5", "jszip": "^3.1.5",
"mocha": "^10.0.0", "mocha": "^10.0.0",
"nyc": "^15.1.0", "nyc": "^15.1.0",
@ -105,7 +104,7 @@
"tsconfig-paths": "^4.0.0", "tsconfig-paths": "^4.0.0",
"tsconfig-paths-webpack-plugin": "^4.0.0", "tsconfig-paths-webpack-plugin": "^4.0.0",
"typedoc": "^0.23.2", "typedoc": "^0.23.2",
"typescript": "4.9.5", "typescript": "5.0.2",
"unzipper": "^0.10.11", "unzipper": "^0.10.11",
"webpack": "^5.28.0", "webpack": "^5.28.0",
"webpack-cli": "^5.0.0" "webpack-cli": "^5.0.0"

View File

@ -9,7 +9,7 @@ for (const file of files) {
from: /"@[a-z/-]*"/gi, from: /"@[a-z/-]*"/gi,
to: (match) => { to: (match) => {
const matchSlug = match.replace(/['"]+/g, "").replace(/[@]+/g, "").trim(); const matchSlug = match.replace(/['"]+/g, "").replace(/[@]+/g, "").trim();
const levelCount = file.split("/").length - 2; const levelCount = file.split(/[\/\\]/).length - 2;
const backLevels = Array(levelCount).fill("../").join(""); const backLevels = Array(levelCount).fill("../").join("");
return `"${backLevels}${matchSlug}"`; return `"${backLevels}${matchSlug}"`;

View File

@ -59,9 +59,8 @@ export class Compiler {
} }
} }
for (const data of file.Media.Array) { for (const { stream, fileName } of file.Media.Array) {
const mediaData = data.stream; zip.file(`word/media/${fileName}`, stream);
zip.file(`word/media/${data.fileName}`, mediaData);
} }
return zip; return zip;

View File

@ -1,11 +0,0 @@
import { IDocumentTemplate } from "../import-dotx";
export interface IFileProperties {
readonly template?: IDocumentTemplate;
}
// Needed because of: https://github.com/s-panferov/awesome-typescript-loader/issues/432
/**
* @ignore
*/
export const WORKAROUND = "";

View File

@ -1,12 +1,11 @@
import { expect } from "chai"; import { expect } from "chai";
import { Formatter } from "@export/formatter"; import { Formatter } from "@export/formatter";
import { sectionMarginDefaults, sectionPageSizeDefaults } from "./document";
import { sectionMarginDefaults, sectionPageSizeDefaults } from "./document";
import { File } from "./file"; import { File } from "./file";
import { Footer, Header } from "./header"; import { Footer, Header } from "./header";
import { Paragraph } from "./paragraph"; import { Paragraph } from "./paragraph";
import { Media } from "./media";
const PAGE_SIZE_DEFAULTS = { const PAGE_SIZE_DEFAULTS = {
"w:h": sectionPageSizeDefaults.HEIGHT, "w:h": sectionPageSizeDefaults.HEIGHT,
@ -433,29 +432,6 @@ describe("File", () => {
}); });
}); });
describe("#templates", () => {
// Test will be deprecated when import-dotx and templates are deprecated
it("should work with template", () => {
const doc = new File(
{
sections: [],
},
{
template: {
currentRelationshipId: 1,
headers: [],
footers: [],
styles: "",
titlePageIsDefined: true,
media: new Media(),
},
},
);
expect(doc).to.not.be.undefined;
});
});
describe("#externalStyles", () => { describe("#externalStyles", () => {
it("should work with external styles", () => { it("should work with external styles", () => {
const doc = new File({ const doc = new File({

View File

@ -4,7 +4,6 @@ import { CoreProperties, IPropertiesOptions } from "./core-properties";
import { CustomProperties } from "./custom-properties"; import { CustomProperties } from "./custom-properties";
import { DocumentWrapper } from "./document-wrapper"; import { DocumentWrapper } from "./document-wrapper";
import { HeaderFooterReferenceType, ISectionPropertiesOptions } from "./document/body/section-properties"; import { HeaderFooterReferenceType, ISectionPropertiesOptions } from "./document/body/section-properties";
import { IFileProperties } from "./file-properties";
import { FooterWrapper, IDocumentFooter } from "./footer-wrapper"; import { FooterWrapper, IDocumentFooter } from "./footer-wrapper";
import { FootnotesWrapper } from "./footnotes-wrapper"; import { FootnotesWrapper } from "./footnotes-wrapper";
import { Footer, Header } from "./header"; import { Footer, Header } from "./header";
@ -55,7 +54,7 @@ export class File {
private readonly styles: Styles; private readonly styles: Styles;
private readonly comments: Comments; private readonly comments: Comments;
public constructor(options: IPropertiesOptions, fileProperties: IFileProperties = {}) { public constructor(options: IPropertiesOptions) {
this.coreProperties = new CoreProperties({ this.coreProperties = new CoreProperties({
...options, ...options,
creator: options.creator ?? "Un-named", creator: options.creator ?? "Un-named",
@ -80,20 +79,9 @@ export class File {
updateFields: options.features?.updateFields, updateFields: options.features?.updateFields,
}); });
this.media = fileProperties.template && fileProperties.template.media ? fileProperties.template.media : new Media(); this.media = new Media();
if (fileProperties.template) { if (options.externalStyles) {
this.currentRelationshipId = fileProperties.template.currentRelationshipId + 1;
}
// set up styles
if (fileProperties.template && options.externalStyles) {
throw Error("can not use both template and external styles");
}
if (fileProperties.template && fileProperties.template.styles) {
const stylesFactory = new ExternalStylesFactory();
this.styles = stylesFactory.newInstance(fileProperties.template.styles);
} else if (options.externalStyles) {
const stylesFactory = new ExternalStylesFactory(); const stylesFactory = new ExternalStylesFactory();
this.styles = stylesFactory.newInstance(options.externalStyles); this.styles = stylesFactory.newInstance(options.externalStyles);
} else if (options.styles) { } else if (options.styles) {
@ -110,18 +98,6 @@ export class File {
this.addDefaultRelationships(); this.addDefaultRelationships();
if (fileProperties.template && fileProperties.template.headers) {
for (const templateHeader of fileProperties.template.headers) {
this.addHeaderToDocument(templateHeader.header, templateHeader.type);
}
}
if (fileProperties.template && fileProperties.template.footers) {
for (const templateFooter of fileProperties.template.footers) {
this.addFooterToDocument(templateFooter.footer, templateFooter.type);
}
}
for (const section of options.sections) { for (const section of options.sections) {
this.addSection(section); this.addSection(section);
} }

View File

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

View File

@ -15,96 +15,30 @@ describe("Media", () => {
(convenienceFunctions.uniqueId as SinonStub).restore(); (convenienceFunctions.uniqueId as SinonStub).restore();
}); });
describe("#addMedia", () => { describe("#Array", () => {
it("should add media", () => { it("Get images as array", () => {
const image = new Media().addMedia("", {
width: 100,
height: 100,
});
expect(image.fileName).to.equal("test.png");
expect(image.transformation).to.deep.equal({
pixels: {
x: 100,
y: 100,
},
flip: undefined,
emus: {
x: 952500,
y: 952500,
},
rotation: undefined,
});
});
it("should return UInt8Array if atob is present", () => {
// eslint-disable-next-line functional/immutable-data
global.atob = () => "atob result";
const image = new Media().addMedia("", {
width: 100,
height: 100,
});
expect(image.stream).to.be.an.instanceof(Uint8Array);
// eslint-disable-next-line @typescript-eslint/no-explicit-any, functional/immutable-data
(global as any).atob = undefined;
});
it("should use data as is if its not a string", () => {
// eslint-disable-next-line functional/immutable-data
global.atob = () => "atob result";
const image = new Media().addMedia(Buffer.from(""), {
width: 100,
height: 100,
});
expect(image.stream).to.be.an.instanceof(Uint8Array);
// eslint-disable-next-line @typescript-eslint/no-explicit-any, functional/immutable-data
(global as any).atob = undefined;
});
});
describe("#addImage", () => {
it("should add media", () => {
const media = new Media(); const media = new Media();
media.addMedia("", {
width: 100,
height: 100,
});
media.addImage("test2.png", { media.addImage("test2.png", {
stream: Buffer.from(""), stream: Buffer.from(""),
fileName: "", fileName: "test.png",
transformation: { transformation: {
pixels: { pixels: {
x: Math.round(1), x: Math.round(100),
y: Math.round(1), y: Math.round(100),
},
flip: {
vertical: true,
horizontal: true,
}, },
emus: { emus: {
x: Math.round(1 * 9525), x: Math.round(1 * 9525),
y: Math.round(1 * 9525), y: Math.round(1 * 9525),
}, },
rotation: 90,
}, },
}); });
expect(media.Array).to.be.lengthOf(2);
});
});
describe("#Array", () => {
it("Get images as array", () => {
const media = new Media();
media.addMedia("", {
width: 100,
height: 100,
flip: {
vertical: true,
horizontal: true,
},
rotation: 90,
});
const array = media.Array; const array = media.Array;
expect(array).to.be.an.instanceof(Array); expect(array).to.be.an.instanceof(Array);
expect(array.length).to.equal(1); expect(array.length).to.equal(1);
@ -121,10 +55,10 @@ describe("Media", () => {
horizontal: true, horizontal: true,
}, },
emus: { emus: {
x: 952500, x: 9525,
y: 952500, y: 9525,
}, },
rotation: 5400000, rotation: 90,
}); });
}); });
}); });

View File

@ -1,5 +1,3 @@
import { uniqueId } from "@util/convenience-functions";
import { IMediaData } from "./data"; import { IMediaData } from "./data";
export interface IMediaTransformation { export interface IMediaTransformation {
@ -20,33 +18,6 @@ export class Media {
this.map = new Map<string, IMediaData>(); this.map = new Map<string, IMediaData>();
} }
public addMedia(data: Buffer | string | Uint8Array | ArrayBuffer, transformation: IMediaTransformation): IMediaData {
const key = `${uniqueId()}.png`;
const newData = typeof data === "string" ? this.convertDataURIToBinary(data) : data;
const imageData: IMediaData = {
stream: newData,
fileName: key,
transformation: {
pixels: {
x: Math.round(transformation.width),
y: Math.round(transformation.height),
},
emus: {
x: Math.round(transformation.width * 9525),
y: Math.round(transformation.height * 9525),
},
flip: transformation.flip,
rotation: transformation.rotation ? transformation.rotation * 60000 : undefined,
},
};
this.map.set(key, imageData);
return imageData;
}
public addImage(key: string, mediaData: IMediaData): void { public addImage(key: string, mediaData: IMediaData): void {
this.map.set(key, mediaData); this.map.set(key, mediaData);
} }
@ -54,24 +25,4 @@ export class Media {
public get Array(): readonly IMediaData[] { public get Array(): readonly IMediaData[] {
return Array.from(this.map.values()); return Array.from(this.map.values());
} }
private convertDataURIToBinary(dataURI: string): Uint8Array {
// https://gist.github.com/borismus/1032746
// https://github.com/mafintosh/base64-to-uint8array
const BASE64_MARKER = ";base64,";
const base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length;
if (typeof atob === "function") {
return new Uint8Array(
atob(dataURI.substring(base64Index))
.split("")
.map((c) => c.charCodeAt(0)),
);
} else {
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
const b = require("buf" + "fer");
return new b.Buffer(dataURI, "base64");
}
}
} }

View File

@ -10,10 +10,8 @@ import { ColumnBreak, PageBreak } from "./formatting/break";
import { Bookmark, ConcreteHyperlink, ExternalHyperlink, InternalHyperlink } from "./links"; import { Bookmark, ConcreteHyperlink, ExternalHyperlink, InternalHyperlink } from "./links";
import { Math } from "./math"; import { Math } from "./math";
import { IParagraphPropertiesOptions, ParagraphProperties } from "./properties"; import { IParagraphPropertiesOptions, ParagraphProperties } from "./properties";
import { ImageRun, PageNumber, Run, SequentialIdentifier, SimpleField, SimpleMailMergeField, SymbolRun, TextRun } from "./run"; import { ImageRun, Run, SequentialIdentifier, SimpleField, SimpleMailMergeField, SymbolRun, TextRun } from "./run";
import { Comment, CommentRangeEnd, CommentRangeStart, CommentReference, Comments } from "./run/comment-run"; import { Comment, CommentRangeEnd, CommentRangeStart, CommentReference, Comments } from "./run/comment-run";
import { Begin, End, Separate } from "./run/field";
import { NumberOfPages, NumberOfPagesSection, Page } from "./run/page-number";
export type ParagraphChild = export type ParagraphChild =
| TextRun | TextRun
@ -35,8 +33,7 @@ export type ParagraphChild =
| Comment | Comment
| CommentRangeStart | CommentRangeStart
| CommentRangeEnd | CommentRangeEnd
| CommentReference | CommentReference;
| PageNumber;
export interface IParagraphOptions extends IParagraphPropertiesOptions { export interface IParagraphOptions extends IParagraphPropertiesOptions {
readonly text?: string; readonly text?: string;
@ -66,34 +63,6 @@ export class Paragraph extends FileChild {
if (options.children) { if (options.children) {
for (const child of options.children) { for (const child of options.children) {
if (typeof child === "string") {
switch (child) {
case PageNumber.CURRENT:
this.root.push(new TextRun({ children: [new Begin()] }));
this.root.push(new TextRun({ children: [new Page()] }));
this.root.push(new TextRun({ children: [new Separate()] }));
this.root.push(new TextRun({ children: [new End()] }));
break;
case PageNumber.TOTAL_PAGES:
this.root.push(new TextRun({ children: [new Begin()] }));
this.root.push(new TextRun({ children: [new NumberOfPages()] }));
this.root.push(new TextRun({ children: [new Separate()] }));
this.root.push(new TextRun("0"));
this.root.push(new TextRun({ children: [new End()] }));
break;
case PageNumber.TOTAL_PAGES_IN_SECTION:
this.root.push(new TextRun({ children: [new Begin()] }));
this.root.push(new TextRun({ children: [new NumberOfPagesSection()] }));
this.root.push(new TextRun({ children: [new Separate()] }));
this.root.push(new TextRun({ children: [new End()] }));
break;
default:
this.root.push(new TextRun(child));
break;
}
continue;
}
if (child instanceof Bookmark) { if (child instanceof Bookmark) {
this.root.push(child.start); this.root.push(child.start);
for (const textRun of child.children) { for (const textRun of child.children) {

View File

@ -125,6 +125,21 @@ describe("ParagraphProperties", () => {
}); });
}); });
it("should create with the autoSpaceEastAsianText property", () => {
const properties = new ParagraphProperties({
autoSpaceEastAsianText: true,
});
const tree = new Formatter().format(properties);
expect(tree).to.deep.equal({
"w:pPr": [
{
"w:autoSpaceDN": {},
},
],
});
});
it("should create with the wordWrap property", () => { it("should create with the wordWrap property", () => {
const properties = new ParagraphProperties({ const properties = new ParagraphProperties({
wordWrap: true, wordWrap: true,

View File

@ -53,6 +53,11 @@ export interface IParagraphPropertiesOptions extends IParagraphStylePropertiesOp
readonly suppressLineNumbers?: boolean; readonly suppressLineNumbers?: boolean;
readonly wordWrap?: boolean; readonly wordWrap?: boolean;
readonly scale?: number; readonly scale?: number;
/**
* This element specifies whether inter-character spacing shall automatically be adjusted between regions of numbers and regions of East Asian text in the current paragraph. These regions shall be determined by the Unicode character values of the text content within the paragraph.
* This only works in Microsoft Word. It is not part of the ECMA-376 OOXML standard.
*/
readonly autoSpaceEastAsianText?: boolean;
} }
export class ParagraphProperties extends IgnoreIfEmptyXmlComponent { export class ParagraphProperties extends IgnoreIfEmptyXmlComponent {
@ -179,6 +184,10 @@ export class ParagraphProperties extends IgnoreIfEmptyXmlComponent {
if (options.suppressLineNumbers !== undefined) { if (options.suppressLineNumbers !== undefined) {
this.push(new OnOffElement("w:suppressLineNumbers", options.suppressLineNumbers)); this.push(new OnOffElement("w:suppressLineNumbers", options.suppressLineNumbers));
} }
if (options.autoSpaceEastAsianText !== undefined) {
this.push(new OnOffElement("w:autoSpaceDN", options.autoSpaceEastAsianText));
}
} }
public push(item: XmlComponent): void { public push(item: XmlComponent): void {

View File

@ -2,6 +2,8 @@ import { expect } from "chai";
import * as sinon from "sinon"; import * as sinon from "sinon";
import { Formatter } from "@export/formatter"; import { Formatter } from "@export/formatter";
import { Paragraph } from "../paragraph";
import { Comment, CommentRangeEnd, CommentRangeStart, CommentReference, Comments } from "./comment-run"; import { Comment, CommentRangeEnd, CommentRangeStart, CommentReference, Comments } from "./comment-run";
describe("CommentRangeStart", () => { describe("CommentRangeStart", () => {
@ -56,7 +58,7 @@ describe("Comment", () => {
it("should create", () => { it("should create", () => {
const component = new Comment({ const component = new Comment({
id: 0, id: 0,
text: "test-comment", children: [new Paragraph("test-comment")],
date: new Date("1999-01-01T00:00:00.000Z"), date: new Date("1999-01-01T00:00:00.000Z"),
}); });
const tree = new Formatter().format(component); const tree = new Formatter().format(component);
@ -88,7 +90,7 @@ describe("Comment", () => {
it("should create by using default date", () => { it("should create by using default date", () => {
const component = new Comment({ const component = new Comment({
id: 0, id: 0,
text: "test-comment", children: [new Paragraph("test-comment")],
}); });
const tree = new Formatter().format(component); const tree = new Formatter().format(component);
expect(tree).to.deep.equal({ expect(tree).to.deep.equal({
@ -125,12 +127,12 @@ describe("Comments", () => {
children: [ children: [
{ {
id: 0, id: 0,
text: "test-comment", children: [new Paragraph("test-comment")],
date: new Date("1999-01-01T00:00:00.000Z"), date: new Date("1999-01-01T00:00:00.000Z"),
}, },
{ {
id: 1, id: 1,
text: "test-comment-2", children: [new Paragraph("test-comment-2")],
date: new Date("1999-01-01T00:00:00.000Z"), date: new Date("1999-01-01T00:00:00.000Z"),
}, },
], ],

View File

@ -1,11 +1,9 @@
import { Paragraph } from "@file/paragraph"; import { FileChild } from "@file/file-child";
import { XmlAttributeComponent, XmlComponent } from "@file/xml-components"; import { XmlAttributeComponent, XmlComponent } from "@file/xml-components";
import { TextRun } from "./text-run";
export interface ICommentOptions { export interface ICommentOptions {
readonly id: number; readonly id: number;
readonly text: string; readonly children: readonly FileChild[];
readonly initials?: string; readonly initials?: string;
readonly author?: string; readonly author?: string;
readonly date?: Date; readonly date?: Date;
@ -120,7 +118,7 @@ export class CommentReference extends XmlComponent {
} }
export class Comment extends XmlComponent { export class Comment extends XmlComponent {
public constructor({ id, initials, author, date = new Date(), text }: ICommentOptions) { public constructor({ id, initials, author, date = new Date(), children }: ICommentOptions) {
super("w:comment"); super("w:comment");
this.root.push( this.root.push(
@ -132,7 +130,9 @@ export class Comment extends XmlComponent {
}), }),
); );
this.root.push(new Paragraph({ children: [new TextRun(text)] })); for (const child of children) {
this.root.push(child);
}
} }
} }
export class Comments extends XmlComponent { export class Comments extends XmlComponent {

View File

@ -1,29 +1,13 @@
// http://www.datypic.com/sc/ooxml/e-w_fldChar-1.html
import { XmlAttributeComponent, XmlComponent } from "@file/xml-components"; import { XmlAttributeComponent, XmlComponent } from "@file/xml-components";
// <xsd:complexType name="CT_FldChar">
// <xsd:choice>
// <xsd:element name="fldData" type="CT_Text" minOccurs="0" maxOccurs="1" />
// <xsd:element name="ffData" type="CT_FFData" minOccurs="0" maxOccurs="1" />
// <xsd:element name="numberingChange" type="CT_TrackChangeNumbering" minOccurs="0" />
// </xsd:choice>
// <xsd:attribute name="fldCharType" type="ST_FldCharType" use="required" />
// <xsd:attribute name="fldLock" type="s:ST_OnOff" />
// <xsd:attribute name="dirty" type="s:ST_OnOff" />
// </xsd:complexType>
enum FieldCharacterType { enum FieldCharacterType {
BEGIN = "begin", BEGIN = "begin",
END = "end", END = "end",
SEPARATE = "separate", SEPARATE = "separate",
} }
class FidCharAttrs extends XmlAttributeComponent<{ class FidCharAttrs extends XmlAttributeComponent<{ readonly type: FieldCharacterType; readonly dirty?: boolean }> {
readonly type: FieldCharacterType; protected readonly xmlKeys = { type: "w:fldCharType", dirty: "w:dirty" };
readonly dirty?: boolean;
readonly fieldLock?: boolean;
}> {
protected readonly xmlKeys = { type: "w:fldCharType", dirty: "w:dirty", fieldLock: "w:fldLock" };
} }
export class Begin extends XmlComponent { export class Begin extends XmlComponent {

View File

@ -8,7 +8,6 @@ export interface IBaseCharacterStyleOptions extends IStyleOptions {
export interface ICharacterStyleOptions extends IBaseCharacterStyleOptions { export interface ICharacterStyleOptions extends IBaseCharacterStyleOptions {
readonly id: string; readonly id: string;
readonly name?: string;
} }
export class StyleForCharacter extends Style { export class StyleForCharacter extends Style {

View File

@ -9,7 +9,6 @@ export interface IBaseParagraphStyleOptions extends IStyleOptions {
export interface IParagraphStyleOptions extends IBaseParagraphStyleOptions { export interface IParagraphStyleOptions extends IBaseParagraphStyleOptions {
readonly id: string; readonly id: string;
readonly name?: string;
} }
export class StyleForParagraph extends Style { export class StyleForParagraph extends Style {

View File

@ -38,9 +38,7 @@ describe("Table Float Properties", () => {
expect(tree).to.deep.equal({ expect(tree).to.deep.equal({
"w:tblpPr": [ "w:tblpPr": [
{ {
_attr: { _attr: {},
overlap: "never",
},
}, },
{ {
"w:tblOverlap": { "w:tblOverlap": {

View File

@ -1,4 +1,4 @@
import { StringEnumValueElement, XmlAttributeComponent, XmlComponent } from "@file/xml-components"; import { NextAttributeComponent, StringEnumValueElement, XmlComponent } from "@file/xml-components";
import { PositiveUniversalMeasure, signedTwipsMeasureValue, twipsMeasureValue, UniversalMeasure } from "@util/values"; import { PositiveUniversalMeasure, signedTwipsMeasureValue, twipsMeasureValue, UniversalMeasure } from "@util/values";
export enum TableAnchorType { export enum TableAnchorType {
@ -35,7 +35,7 @@ export enum OverlapType {
OVERLAP = "overlap", OVERLAP = "overlap",
} }
export interface ITableFloatOptions { export type ITableFloatOptions = {
/* cSpell:disable */ /* cSpell:disable */
/** /**
* Specifies the horizontal anchor or the base object from which the horizontal positioning in the * Specifies the horizontal anchor or the base object from which the horizontal positioning in the
@ -124,7 +124,7 @@ export interface ITableFloatOptions {
*/ */
readonly rightFromText?: number | PositiveUniversalMeasure; readonly rightFromText?: number | PositiveUniversalMeasure;
readonly overlap?: OverlapType; readonly overlap?: OverlapType;
} };
// <xsd:complexType name="CT_TblPPr"> // <xsd:complexType name="CT_TblPPr">
// <xsd:attribute name="leftFromText" type="s:ST_TwipsMeasure"/> // <xsd:attribute name="leftFromText" type="s:ST_TwipsMeasure"/>
@ -139,51 +139,65 @@ export interface ITableFloatOptions {
// <xsd:attribute name="tblpY" type="ST_SignedTwipsMeasure"/> // <xsd:attribute name="tblpY" type="ST_SignedTwipsMeasure"/>
// </xsd:complexType> // </xsd:complexType>
export class TableFloatOptionsAttributes extends XmlAttributeComponent<ITableFloatOptions> {
protected readonly xmlKeys = {
horizontalAnchor: "w:horzAnchor",
verticalAnchor: "w:vertAnchor",
absoluteHorizontalPosition: "w:tblpX",
relativeHorizontalPosition: "w:tblpXSpec",
absoluteVerticalPosition: "w:tblpY",
relativeVerticalPosition: "w:tblpYSpec",
bottomFromText: "w:bottomFromText",
topFromText: "w:topFromText",
leftFromText: "w:leftFromText",
rightFromText: "w:rightFromText",
};
}
export class TableFloatProperties extends XmlComponent { export class TableFloatProperties extends XmlComponent {
public constructor({ public constructor({
horizontalAnchor,
verticalAnchor,
absoluteHorizontalPosition,
relativeHorizontalPosition,
absoluteVerticalPosition,
relativeVerticalPosition,
bottomFromText,
topFromText,
leftFromText, leftFromText,
rightFromText, rightFromText,
topFromText, overlap,
bottomFromText,
absoluteHorizontalPosition,
absoluteVerticalPosition,
...options
}: ITableFloatOptions) { }: ITableFloatOptions) {
super("w:tblpPr"); super("w:tblpPr");
this.root.push( this.root.push(
new TableFloatOptionsAttributes({ new NextAttributeComponent<Omit<ITableFloatOptions, "overlap">>({
leftFromText: leftFromText === undefined ? undefined : twipsMeasureValue(leftFromText), leftFromText: { key: "w:leftFromText", value: leftFromText === undefined ? undefined : twipsMeasureValue(leftFromText) },
rightFromText: rightFromText === undefined ? undefined : twipsMeasureValue(rightFromText), rightFromText: {
topFromText: topFromText === undefined ? undefined : twipsMeasureValue(topFromText), key: "w:rightFromText",
bottomFromText: bottomFromText === undefined ? undefined : twipsMeasureValue(bottomFromText), value: rightFromText === undefined ? undefined : twipsMeasureValue(rightFromText),
absoluteHorizontalPosition: },
absoluteHorizontalPosition === undefined ? undefined : signedTwipsMeasureValue(absoluteHorizontalPosition), topFromText: { key: "w:topFromText", value: topFromText === undefined ? undefined : twipsMeasureValue(topFromText) },
absoluteVerticalPosition: bottomFromText: {
absoluteVerticalPosition === undefined ? undefined : signedTwipsMeasureValue(absoluteVerticalPosition), key: "w:bottomFromText",
...options, value: bottomFromText === undefined ? undefined : twipsMeasureValue(bottomFromText),
},
absoluteHorizontalPosition: {
key: "w:tblpX",
value: absoluteHorizontalPosition === undefined ? undefined : signedTwipsMeasureValue(absoluteHorizontalPosition),
},
absoluteVerticalPosition: {
key: "w:tblpY",
value: absoluteVerticalPosition === undefined ? undefined : signedTwipsMeasureValue(absoluteVerticalPosition),
},
horizontalAnchor: {
key: "w:horzAnchor",
value: horizontalAnchor === undefined ? undefined : horizontalAnchor,
},
relativeHorizontalPosition: {
key: "w:tblpXSpec",
value: relativeHorizontalPosition,
},
relativeVerticalPosition: {
key: "w:tblpYSpec",
value: relativeVerticalPosition,
},
verticalAnchor: {
key: "w:vertAnchor",
value: verticalAnchor,
},
}), }),
); );
if (options.overlap) { if (overlap) {
// <xsd:complexType name="CT_TblOverlap"> // <xsd:complexType name="CT_TblOverlap">
// <xsd:attribute name="val" type="ST_TblOverlap" use="required"/> // <xsd:attribute name="val" type="ST_TblOverlap" use="required"/>
// </xsd:complexType> // </xsd:complexType>
this.root.push(new StringEnumValueElement<OverlapType>("w:tblOverlap", options.overlap)); this.root.push(new StringEnumValueElement<OverlapType>("w:tblOverlap", overlap));
} }
} }
} }

View File

@ -0,0 +1,41 @@
import { expect } from "chai";
import { Formatter } from "@export/formatter";
import { BuilderElement } from "./simple-elements";
describe("BuilderElement", () => {
describe("#constructor()", () => {
it("should create a simple BuilderElement", () => {
const element = new BuilderElement({
name: "test",
});
const tree = new Formatter().format(element);
expect(tree).to.deep.equal({
test: {},
});
});
it("should create a simple BuilderElement with attributes", () => {
const element = new BuilderElement<{ readonly testAttr: string }>({
name: "test",
attributes: {
testAttr: {
key: "w:testAttr",
value: "test",
},
},
});
const tree = new Formatter().format(element);
expect(tree).to.deep.equal({
test: {
_attr: {
"w:testAttr": "test",
},
},
});
});
});
});

View File

@ -92,5 +92,7 @@ export class BuilderElement<T extends AttributeData> extends XmlComponent {
if (options.attributes) { if (options.attributes) {
this.root.push(new NextAttributeComponent(options.attributes)); this.root.push(new NextAttributeComponent(options.attributes));
} }
// TODO: Children
} }
} }

View File

@ -1,26 +0,0 @@
import { expect } from "chai";
import { ImportDotx } from "./import-dotx";
describe("ImportDotx", () => {
describe("#constructor", () => {
it("should create", () => {
const file = new ImportDotx();
expect(file).to.deep.equal({});
});
});
// describe("#extract", () => {
// it("should create", async () => {
// const file = new ImportDotx();
// const filePath = "./demo/dotx/template.dotx";
// const templateDocument = await file.extract(data);
// await file.extract(data);
// expect(templateDocument).to.be.equal({ currentRelationshipId: 1 });
// });
// });
});

View File

@ -1,266 +0,0 @@
/* eslint-disable */
// This will be deprecated soon
import * as JSZip from "jszip";
import { Element as XMLElement, ElementCompact as XMLElementCompact, xml2js } from "xml-js";
import { HeaderFooterReferenceType } from "@file/document/body/section-properties";
import { FooterWrapper, IDocumentFooter } from "@file/footer-wrapper";
import { HeaderWrapper, IDocumentHeader } from "@file/header-wrapper";
import { Media } from "@file/media";
import { TargetModeType } from "@file/relationships/relationship/relationship";
import { convertToXmlComponent, ImportedXmlComponent } from "@file/xml-components";
const schemeToType = {
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header": "header",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer": "footer",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image": "image",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink": "hyperlink",
};
interface IDocumentRefs {
readonly headers: { readonly id: number; readonly type: HeaderFooterReferenceType }[];
readonly footers: { readonly id: number; readonly type: HeaderFooterReferenceType }[];
}
enum RelationshipType {
HEADER = "header",
FOOTER = "footer",
IMAGE = "image",
HYPERLINK = "hyperlink",
}
interface IRelationshipFileInfo {
readonly id: number;
readonly target: string;
readonly type: RelationshipType;
}
// Document Template
// https://fileinfo.com/extension/dotx
export interface IDocumentTemplate {
readonly currentRelationshipId: number;
readonly headers: IDocumentHeader[];
readonly footers: IDocumentFooter[];
readonly styles: string;
readonly titlePageIsDefined: boolean;
readonly media: Media;
}
export class ImportDotx {
public async extract(
data: Buffer | string | number[] | Uint8Array | ArrayBuffer | Blob | NodeJS.ReadableStream,
): Promise<IDocumentTemplate> {
const zipContent = await JSZip.loadAsync(data);
const documentContent = await zipContent.files["word/document.xml"].async("text");
const relationshipContent = await zipContent.files["word/_rels/document.xml.rels"].async("text");
const documentRefs = this.extractDocumentRefs(documentContent);
const documentRelationships = this.findReferenceFiles(relationshipContent);
const media = new Media();
const templateDocument: IDocumentTemplate = {
headers: await this.createHeaders(zipContent, documentRefs, documentRelationships, media, 0),
footers: await this.createFooters(zipContent, documentRefs, documentRelationships, media, documentRefs.headers.length),
currentRelationshipId: documentRefs.footers.length + documentRefs.headers.length,
styles: await zipContent.files["word/styles.xml"].async("text"),
titlePageIsDefined: this.checkIfTitlePageIsDefined(documentContent),
media: media,
};
return templateDocument;
}
private async createFooters(
zipContent: JSZip,
documentRefs: IDocumentRefs,
documentRelationships: IRelationshipFileInfo[],
media: Media,
startingRelationshipId: number,
): Promise<IDocumentFooter[]> {
const result = documentRefs.footers
.map(async (reference, i) => {
const relationshipFileInfo = documentRelationships.find((rel) => rel.id === reference.id);
if (relationshipFileInfo === null || !relationshipFileInfo) {
throw new Error(`Can not find target file for id ${reference.id}`);
}
const xmlData = await zipContent.files[`word/${relationshipFileInfo.target}`].async("text");
const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as XMLElement;
if (!xmlObj.elements) {
return undefined;
}
const xmlElement = xmlObj.elements.reduce((acc, current) => (current.name === "w:ftr" ? current : acc));
const importedComp = convertToXmlComponent(xmlElement) as ImportedXmlComponent;
const wrapper = new FooterWrapper(media, startingRelationshipId + i, importedComp);
await this.addRelationshipToWrapper(relationshipFileInfo, zipContent, wrapper, media);
return { type: reference.type, footer: wrapper };
})
.filter((x) => !!x) as Promise<IDocumentFooter>[];
return Promise.all(result);
}
private async createHeaders(
zipContent: JSZip,
documentRefs: IDocumentRefs,
documentRelationships: IRelationshipFileInfo[],
media: Media,
startingRelationshipId: number,
): Promise<IDocumentHeader[]> {
const result = documentRefs.headers
.map(async (reference, i) => {
const relationshipFileInfo = documentRelationships.find((rel) => rel.id === reference.id);
if (relationshipFileInfo === null || !relationshipFileInfo) {
throw new Error(`Can not find target file for id ${reference.id}`);
}
const xmlData = await zipContent.files[`word/${relationshipFileInfo.target}`].async("text");
const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as XMLElement;
if (!xmlObj.elements) {
return undefined;
}
const xmlElement = xmlObj.elements.reduce((acc, current) => (current.name === "w:hdr" ? current : acc));
const importedComp = convertToXmlComponent(xmlElement) as ImportedXmlComponent;
const wrapper = new HeaderWrapper(media, startingRelationshipId + i, importedComp);
await this.addRelationshipToWrapper(relationshipFileInfo, zipContent, wrapper, media);
return { type: reference.type, header: wrapper };
})
.filter((x) => !!x) as Promise<IDocumentHeader>[];
return Promise.all(result);
}
private async addRelationshipToWrapper(
relationshipFile: IRelationshipFileInfo,
zipContent: JSZip,
wrapper: HeaderWrapper | FooterWrapper,
media: Media,
): Promise<void> {
const refFile = zipContent.files[`word/_rels/${relationshipFile.target}.rels`];
if (!refFile) {
return;
}
const xmlRef = await refFile.async("text");
const wrapperImagesReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === RelationshipType.IMAGE);
const hyperLinkReferences = this.findReferenceFiles(xmlRef).filter((r) => r.type === RelationshipType.HYPERLINK);
for (const r of wrapperImagesReferences) {
const bufferType = JSZip.support.arraybuffer ? "arraybuffer" : "nodebuffer";
const buffer = await zipContent.files[`word/${r.target}`].async(bufferType);
const mediaData = media.addMedia(buffer, {
width: 100,
height: 100,
});
wrapper.Relationships.createRelationship(
r.id,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${mediaData.fileName}`,
);
}
for (const r of hyperLinkReferences) {
wrapper.Relationships.createRelationship(
r.id,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
r.target,
TargetModeType.EXTERNAL,
);
}
}
private findReferenceFiles(xmlData: string): IRelationshipFileInfo[] {
const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact;
const relationXmlArray = Array.isArray(xmlObj.Relationships.Relationship)
? xmlObj.Relationships.Relationship
: [xmlObj.Relationships.Relationship];
const relationships: IRelationshipFileInfo[] = relationXmlArray
.map((item: XMLElementCompact) => {
if (item._attributes === undefined) {
throw Error("relationship element has no attributes");
}
return {
id: this.parseRefId(item._attributes.Id as string),
type: schemeToType[item._attributes.Type as string],
target: item._attributes.Target as string,
};
})
.filter((item) => item.type !== null);
return relationships;
}
private extractDocumentRefs(xmlData: string): IDocumentRefs {
const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact;
const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"];
const headerProps: XMLElementCompact = sectionProp["w:headerReference"];
let headersXmlArray: XMLElementCompact[];
if (headerProps === undefined) {
headersXmlArray = [];
} else if (Array.isArray(headerProps)) {
headersXmlArray = headerProps;
} else {
headersXmlArray = [headerProps];
}
const headers = headersXmlArray.map((item) => {
if (item._attributes === undefined) {
throw Error("header reference element has no attributes");
}
return {
type: item._attributes["w:type"] as HeaderFooterReferenceType,
id: this.parseRefId(item._attributes["r:id"] as string),
};
});
const footerProps: XMLElementCompact = sectionProp["w:footerReference"];
let footersXmlArray: XMLElementCompact[];
if (footerProps === undefined) {
footersXmlArray = [];
} else if (Array.isArray(footerProps)) {
footersXmlArray = footerProps;
} else {
footersXmlArray = [footerProps];
}
const footers = footersXmlArray.map((item) => {
if (item._attributes === undefined) {
throw Error("footer reference element has no attributes");
}
return {
type: item._attributes["w:type"] as HeaderFooterReferenceType,
id: this.parseRefId(item._attributes["r:id"] as string),
};
});
return { headers, footers };
}
private checkIfTitlePageIsDefined(xmlData: string): boolean {
const xmlObj = xml2js(xmlData, { compact: true }) as XMLElementCompact;
const sectionProp = xmlObj["w:document"]["w:body"]["w:sectPr"];
return sectionProp["w:titlePg"] !== undefined;
}
private parseRefId(str: string): number {
const match = /^rId(\d+)$/.exec(str);
if (match === null) {
throw new Error("Invalid ref id");
}
return parseInt(match[1], 10);
}
}

View File

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

View File

@ -3,5 +3,5 @@
export { File as Document } from "./file"; export { File as Document } from "./file";
export * from "./file"; export * from "./file";
export * from "./export"; export * from "./export";
export * from "./import-dotx";
export * from "./util"; export * from "./util";
export * from "./patcher";

View File

@ -0,0 +1,51 @@
import { expect } from "chai";
import { appendContentType } from "./content-types-manager";
describe("content-types-manager", () => {
describe("appendContentType", () => {
it("should append a content type", () => {
const element = {
type: "element",
name: "xml",
elements: [
{
type: "element",
name: "Types",
elements: [
{
type: "element",
name: "Default",
},
],
},
],
};
appendContentType(element, "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", "docx");
expect(element).to.deep.equal({
elements: [
{
elements: [
{
name: "Default",
type: "element",
},
{
attributes: {
ContentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
Extension: "docx",
},
name: "Default",
type: "element",
},
],
name: "Types",
type: "element",
},
],
name: "xml",
type: "element",
});
});
});
});

View File

@ -0,0 +1,16 @@
import { Element } from "xml-js";
import { getFirstLevelElements } from "./util";
export const appendContentType = (element: Element, contentType: string, extension: string): void => {
const relationshipElements = getFirstLevelElements(element, "Types");
// eslint-disable-next-line functional/immutable-data
relationshipElements.push({
attributes: {
ContentType: contentType,
Extension: extension,
},
name: "Default",
type: "element",
});
};

View File

@ -0,0 +1,413 @@
import * as chai from "chai";
import * as sinon from "sinon";
import * as JSZip from "jszip";
import * as chaiAsPromised from "chai-as-promised";
import { ExternalHyperlink, ImageRun, Paragraph, TextRun } from "@file/paragraph";
import { patchDocument, PatchType } from "./from-docx";
chai.use(chaiAsPromised);
const { expect } = chai;
const MOCK_XML = `
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex"
xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex"
xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex"
xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex"
xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex"
xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex"
xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex"
xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex"
xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink"
xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:oel="http://schemas.microsoft.com/office/2019/extlst"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
xmlns:w10="urn:schemas-microsoft-com:office:word"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex"
xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid"
xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml"
xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash"
xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex"
xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
<w:body>
<w:p w14:paraId="2499FE9F" w14:textId="0A3D130F" w:rsidR="00B51233"
w:rsidRDefault="007B52ED" w:rsidP="007B52ED">
<w:pPr>
<w:pStyle w:val="Title" />
</w:pPr>
<w:r>
<w:t>Hello World</w:t>
</w:r>
</w:p>
<w:p w14:paraId="6410D9A0" w14:textId="7579AB49" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED" />
<w:p w14:paraId="57ACF964" w14:textId="315D7A05" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED">
<w:r>
<w:t>Hello {{name}},</w:t>
</w:r>
<w:r w:rsidR="008126CB">
<w:t xml:space="preserve"> how are you?</w:t>
</w:r>
</w:p>
<w:p w14:paraId="38C7DF4A" w14:textId="66CDEC9A" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED" />
<w:p w14:paraId="04FABE2B" w14:textId="3DACA001" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED">
<w:r>
<w:t>{{paragraph_replace}}</w:t>
</w:r>
</w:p>
<w:p w14:paraId="7AD7975D" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
<w:p w14:paraId="3BD6D75A" w14:textId="19AE3121" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F">
<w:r>
<w:t>{{table}}</w:t>
</w:r>
</w:p>
<w:p w14:paraId="76023962" w14:textId="4E606AB9" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED" />
<w:tbl>
<w:tblPr>
<w:tblStyle w:val="TableGrid" />
<w:tblW w:w="0" w:type="auto" />
<w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1"
w:lastColumn="0" w:noHBand="0" w:noVBand="1" />
</w:tblPr>
<w:tblGrid>
<w:gridCol w:w="3003" />
<w:gridCol w:w="3003" />
<w:gridCol w:w="3004" />
</w:tblGrid>
<w:tr w:rsidR="00EF161F" w14:paraId="1DEC5955" w14:textId="77777777" w:rsidTr="00EF161F">
<w:tc>
<w:tcPr>
<w:tcW w:w="3003" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="54DA5587" w14:textId="625BAC60" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F">
<w:r>
<w:t>{{table_heading_1}}</w:t>
</w:r>
</w:p>
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3003" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="57100910" w14:textId="71FD5616" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3004" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="1D388FAB" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
</w:tr>
<w:tr w:rsidR="00EF161F" w14:paraId="0F53D2DC" w14:textId="77777777" w:rsidTr="00EF161F">
<w:tc>
<w:tcPr>
<w:tcW w:w="3003" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="0F2BCCED" w14:textId="3C3B6706" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F">
<w:r>
<w:t>Item: {{item_1}}</w:t>
</w:r>
</w:p>
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3003" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="1E6158AC" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3004" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="17937748" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
</w:tr>
<w:tr w:rsidR="00EF161F" w14:paraId="781DAC1A" w14:textId="77777777" w:rsidTr="00EF161F">
<w:tc>
<w:tcPr>
<w:tcW w:w="3003" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="1DCD0343" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3003" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="5D02E3CD" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3004" w:type="dxa" />
</w:tcPr>
<w:p w14:paraId="52EA0DBB" w14:textId="77777777" w:rsidR="00EF161F"
w:rsidRDefault="00EF161F" />
</w:tc>
</w:tr>
</w:tbl>
<w:p w14:paraId="47CD1FBC" w14:textId="23474CBC" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED" />
<w:p w14:paraId="0ACCEE90" w14:textId="67907499" w:rsidR="00EF161F"
w:rsidRDefault="0077578F">
<w:r>
<w:t>{{image_test}}</w:t>
</w:r>
</w:p>
<w:p w14:paraId="23FA9862" w14:textId="77777777" w:rsidR="0077578F"
w:rsidRDefault="0077578F" />
<w:p w14:paraId="01578F2F" w14:textId="3BDC6C85" w:rsidR="007B52ED"
w:rsidRDefault="007B52ED">
<w:r>
<w:t>Thank you</w:t>
</w:r>
</w:p>
<w:sectPr w:rsidR="007B52ED" w:rsidSect="0072043F">
<w:headerReference w:type="default" r:id="rId6" />
<w:footerReference w:type="default" r:id="rId7" />
<w:pgSz w:w="11900" w:h="16840" />
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="708"
w:footer="708" w:gutter="0" />
<w:cols w:space="708" />
<w:docGrid w:linePitch="360" />
</w:sectPr>
</w:body>
</w:document>
`;
describe("from-docx", () => {
describe("patchDocument", () => {
describe("document.xml and [Content_Types].xml", () => {
beforeEach(() => {
sinon.stub(JSZip, "loadAsync").callsFake(
() =>
new Promise<JSZip>((resolve) => {
const zip = new JSZip();
zip.file("word/document.xml", MOCK_XML);
zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`);
resolve(zip);
}),
);
});
afterEach(() => {
(JSZip.loadAsync as unknown as sinon.SinonStub).restore();
});
it("should patch the document", async () => {
const output = await patchDocument(Buffer.from(""), {
patches: {
name: {
type: PatchType.PARAGRAPH,
children: [new TextRun("Sir. "), new TextRun("John Doe"), new TextRun("(The Conqueror)")],
},
item_1: {
type: PatchType.PARAGRAPH,
children: [
new TextRun("#657"),
new ExternalHyperlink({
children: [
new TextRun({
text: "BBC News Link",
}),
],
link: "https://www.bbc.co.uk/news",
}),
],
},
// eslint-disable-next-line @typescript-eslint/naming-convention
paragraph_replace: {
type: PatchType.DOCUMENT,
children: [
new Paragraph({
children: [
new TextRun("This is a "),
new ExternalHyperlink({
children: [
new TextRun({
text: "Google Link",
}),
],
link: "https://www.google.co.uk",
}),
new ImageRun({
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
],
}),
],
},
// eslint-disable-next-line @typescript-eslint/naming-convention
image_test: {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
],
},
},
});
expect(output).to.not.be.undefined;
});
it("should patch the document", async () => {
const output = await patchDocument(Buffer.from(""), {
patches: {},
});
expect(output).to.not.be.undefined;
});
});
describe("document.xml and [Content_Types].xml with relationships", () => {
beforeEach(() => {
sinon.stub(JSZip, "loadAsync").callsFake(
() =>
new Promise<JSZip>((resolve) => {
const zip = new JSZip();
zip.file("word/document.xml", MOCK_XML);
zip.file("word/_rels/document.xml.rels", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`);
zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`);
resolve(zip);
}),
);
});
afterEach(() => {
(JSZip.loadAsync as unknown as sinon.SinonStub).restore();
});
it("should use the relationships file rather than create one", async () => {
const output = await patchDocument(Buffer.from(""), {
patches: {
// eslint-disable-next-line @typescript-eslint/naming-convention
image_test: {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
new ExternalHyperlink({
children: [
new TextRun({
text: "Google Link",
}),
],
link: "https://www.google.co.uk",
}),
],
},
},
});
expect(output).to.not.be.undefined;
});
});
describe("document.xml", () => {
beforeEach(() => {
sinon.stub(JSZip, "loadAsync").callsFake(
() =>
new Promise<JSZip>((resolve) => {
const zip = new JSZip();
zip.file("word/document.xml", MOCK_XML);
resolve(zip);
}),
);
});
afterEach(() => {
(JSZip.loadAsync as unknown as sinon.SinonStub).restore();
});
it("should throw an error if the content types is not found", () =>
expect(
patchDocument(Buffer.from(""), {
patches: {
// eslint-disable-next-line @typescript-eslint/naming-convention
image_test: {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
],
},
},
}),
).to.eventually.be.rejected);
});
describe("Images", () => {
beforeEach(() => {
sinon.stub(JSZip, "loadAsync").callsFake(
() =>
new Promise<JSZip>((resolve) => {
const zip = new JSZip();
zip.file("word/document.xml", MOCK_XML);
zip.file("word/document.bmp", "");
resolve(zip);
}),
);
});
afterEach(() => {
(JSZip.loadAsync as unknown as sinon.SinonStub).restore();
});
it("should throw an error if the content types is not found", () =>
expect(
patchDocument(Buffer.from(""), {
patches: {
// eslint-disable-next-line @typescript-eslint/naming-convention
image_test: {
type: PatchType.PARAGRAPH,
children: [
new ImageRun({
data: Buffer.from(""),
transformation: { width: 100, height: 100 },
}),
],
},
},
}),
).to.eventually.be.rejected);
});
});
});

244
src/patcher/from-docx.ts Normal file
View File

@ -0,0 +1,244 @@
import * as JSZip from "jszip";
import { Element, js2xml } from "xml-js";
import { ConcreteHyperlink, ExternalHyperlink, ParagraphChild } from "@file/paragraph";
import { FileChild } from "@file/file-child";
import { IMediaData, Media } from "@file/media";
import { IViewWrapper } from "@file/document-wrapper";
import { File } from "@file/file";
import { IContext } from "@file/xml-components";
import { ImageReplacer } from "@export/packer/image-replacer";
import { TargetModeType } from "@file/relationships/relationship/relationship";
import { uniqueId } from "@util/convenience-functions";
import { replacer } from "./replacer";
import { findLocationOfText } from "./traverser";
import { toJson } from "./util";
import { appendRelationship, getNextRelationshipIndex } from "./relationship-manager";
import { appendContentType } from "./content-types-manager";
// eslint-disable-next-line functional/prefer-readonly-type
type InputDataType = Buffer | string | number[] | Uint8Array | ArrayBuffer | Blob | NodeJS.ReadableStream;
export enum PatchType {
DOCUMENT = "file",
PARAGRAPH = "paragraph",
}
type ParagraphPatch = {
readonly type: PatchType.PARAGRAPH;
readonly children: readonly ParagraphChild[];
};
type FilePatch = {
readonly type: PatchType.DOCUMENT;
readonly children: readonly FileChild[];
};
interface IImageRelationshipAddition {
readonly key: string;
readonly mediaDatas: readonly IMediaData[];
}
interface IHyperlinkRelationshipAddition {
readonly key: string;
readonly hyperlink: { readonly id: string; readonly link: string };
}
export type IPatch = ParagraphPatch | FilePatch;
export interface PatchDocumentOptions {
readonly patches: { readonly [key: string]: IPatch };
}
const imageReplacer = new ImageReplacer();
export const patchDocument = async (data: InputDataType, options: PatchDocumentOptions): Promise<Buffer> => {
const zipContent = await JSZip.loadAsync(data);
const contexts = new Map<string, IContext>();
const file = {
Media: new Media(),
} as unknown as File;
const map = new Map<string, Element>();
// eslint-disable-next-line functional/prefer-readonly-type
const imageRelationshipAdditions: IImageRelationshipAddition[] = [];
// eslint-disable-next-line functional/prefer-readonly-type
const hyperlinkRelationshipAdditions: IHyperlinkRelationshipAddition[] = [];
let hasMedia = false;
const binaryContentMap = new Map<string, Buffer>();
for (const [key, value] of Object.entries(zipContent.files)) {
if (!key.endsWith(".xml") && !key.endsWith(".rels")) {
binaryContentMap.set(key, await value.async("nodebuffer"));
continue;
}
const json = toJson(await value.async("text"));
if (key.startsWith("word/") && !key.endsWith(".xml.rels")) {
const context: IContext = {
file,
viewWrapper: {
Relationships: {
createRelationship: (linkId: string, _: string, target: string, __: TargetModeType) => {
// eslint-disable-next-line functional/immutable-data
hyperlinkRelationshipAdditions.push({
key,
hyperlink: {
id: linkId,
link: target,
},
});
},
},
} as unknown as IViewWrapper,
stack: [],
};
contexts.set(key, context);
for (const [patchKey, patchValue] of Object.entries(options.patches)) {
const patchText = `{{${patchKey}}}`;
const renderedParagraphs = findLocationOfText(json, patchText);
// TODO: mutates json. Make it immutable
replacer(
json,
{
...patchValue,
children: patchValue.children.map((element) => {
// We need to replace external hyperlinks with concrete hyperlinks
if (element instanceof ExternalHyperlink) {
const concreteHyperlink = new ConcreteHyperlink(element.options.children, uniqueId());
// eslint-disable-next-line functional/immutable-data
hyperlinkRelationshipAdditions.push({
key,
hyperlink: {
id: concreteHyperlink.linkId,
link: element.options.link,
},
});
return concreteHyperlink;
} else {
return element;
}
}),
},
patchText,
renderedParagraphs,
context,
);
}
const mediaDatas = imageReplacer.getMediaData(JSON.stringify(json), context.file.Media);
if (mediaDatas.length > 0) {
hasMedia = true;
// eslint-disable-next-line functional/immutable-data
imageRelationshipAdditions.push({
key,
mediaDatas,
});
}
}
map.set(key, json);
}
for (const { key, mediaDatas } of imageRelationshipAdditions) {
// eslint-disable-next-line functional/immutable-data
const relationshipKey = `word/_rels/${key.split("/").pop()}.rels`;
const relationshipsJson = map.get(relationshipKey) ?? createRelationshipFile();
map.set(relationshipKey, relationshipsJson);
const index = getNextRelationshipIndex(relationshipsJson);
const newJson = imageReplacer.replace(JSON.stringify(map.get(key)), mediaDatas, index);
map.set(key, JSON.parse(newJson) as Element);
for (let i = 0; i < mediaDatas.length; i++) {
const { fileName } = mediaDatas[i];
appendRelationship(
relationshipsJson,
index + i,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
`media/${fileName}`,
);
}
}
for (const { key, hyperlink } of hyperlinkRelationshipAdditions) {
// eslint-disable-next-line functional/immutable-data
const relationshipKey = `word/_rels/${key.split("/").pop()}.rels`;
const relationshipsJson = map.get(relationshipKey) ?? createRelationshipFile();
map.set(relationshipKey, relationshipsJson);
appendRelationship(
relationshipsJson,
hyperlink.id,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
hyperlink.link,
TargetModeType.EXTERNAL,
);
}
if (hasMedia) {
const contentTypesJson = map.get("[Content_Types].xml");
if (!contentTypesJson) {
throw new Error("Could not find content types file");
}
appendContentType(contentTypesJson, "image/png", "png");
appendContentType(contentTypesJson, "image/jpeg", "jpeg");
appendContentType(contentTypesJson, "image/jpeg", "jpg");
appendContentType(contentTypesJson, "image/bmp", "bmp");
appendContentType(contentTypesJson, "image/gif", "gif");
}
const zip = new JSZip();
for (const [key, value] of map) {
const output = toXml(value);
zip.file(key, output);
}
for (const [key, value] of binaryContentMap) {
zip.file(key, value);
}
for (const { stream, fileName } of file.Media.Array) {
zip.file(`word/media/${fileName}`, stream);
}
return zip.generateAsync({
type: "nodebuffer",
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
compression: "DEFLATE",
});
};
const toXml = (jsonObj: Element): string => {
const output = js2xml(jsonObj);
return output;
};
const createRelationshipFile = (): Element => ({
declaration: {
attributes: {
version: "1.0",
encoding: "UTF-8",
standalone: "yes",
},
},
elements: [
{
type: "element",
name: "Relationships",
attributes: {
xmlns: "http://schemas.openxmlformats.org/package/2006/relationships",
},
elements: [],
},
],
});

1
src/patcher/index.ts Normal file
View File

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

View File

@ -0,0 +1,224 @@
import { expect } from "chai";
import { findRunElementIndexWithToken, splitRunElement } from "./paragraph-split-inject";
describe("paragraph-split-inject", () => {
describe("findRunElementIndexWithToken", () => {
it("should find the index of a run element with a token", () => {
const output = findRunElementIndexWithToken(
{
name: "w:p",
type: "element",
elements: [
{
name: "w:r",
type: "element",
elements: [
{
name: "w:t",
type: "element",
elements: [
{
type: "text",
text: "hello world",
},
],
},
],
},
],
},
"hello",
);
expect(output).to.deep.equal(0);
});
it("should throw an exception when ran with empty elements", () => {
expect(() =>
findRunElementIndexWithToken(
{
name: "w:p",
type: "element",
},
"hello",
),
).to.throw();
});
it("should throw an exception when ran with empty elements", () => {
expect(() =>
findRunElementIndexWithToken(
{
name: "w:p",
type: "element",
elements: [
{
name: "w:r",
type: "element",
},
],
},
"hello",
),
).to.throw();
});
it("should throw an exception when ran with empty elements", () => {
expect(() =>
findRunElementIndexWithToken(
{
name: "w:p",
type: "element",
elements: [
{
name: "w:r",
type: "element",
elements: [
{
name: "w:t",
type: "element",
},
],
},
],
},
"hello",
),
).to.throw();
});
});
describe("splitRunElement", () => {
it("should split a run element", () => {
const output = splitRunElement(
{
name: "w:r",
type: "element",
elements: [
{
name: "w:t",
type: "element",
elements: [
{
type: "text",
text: "hello*world",
},
],
},
{
name: "w:x",
type: "element",
},
],
},
"*",
);
expect(output).to.deep.equal({
left: {
elements: [
{
attributes: {
"xml:space": "preserve",
},
elements: [
{
text: "hello",
type: "text",
},
],
name: "w:t",
type: "element",
},
],
name: "w:r",
type: "element",
},
right: {
elements: [
{
attributes: {
"xml:space": "preserve",
},
elements: [
{
text: "world",
type: "text",
},
],
name: "w:t",
type: "element",
},
{
name: "w:x",
type: "element",
},
],
name: "w:r",
type: "element",
},
});
});
it("should try to split even if elements is empty for text", () => {
const output = splitRunElement(
{
name: "w:r",
type: "element",
elements: [
{
name: "w:t",
type: "element",
},
],
},
"*",
);
expect(output).to.deep.equal({
left: {
elements: [
{
attributes: {
"xml:space": "preserve",
},
elements: [],
name: "w:t",
type: "element",
},
],
name: "w:r",
type: "element",
},
right: {
elements: [],
name: "w:r",
type: "element",
},
});
});
it("should return empty elements", () => {
const output = splitRunElement(
{
name: "w:r",
type: "element",
},
"*",
);
expect(output).to.deep.equal({
left: {
elements: [],
name: "w:r",
type: "element",
},
right: {
elements: [],
name: "w:r",
type: "element",
},
});
});
});
});

View File

@ -0,0 +1,54 @@
import { Element } from "xml-js";
import { createTextElementContents, patchSpaceAttribute } from "./util";
export const findRunElementIndexWithToken = (paragraphElement: Element, token: string): number => {
for (let i = 0; i < (paragraphElement.elements ?? []).length; i++) {
const element = paragraphElement.elements![i];
if (element.type === "element" && element.name === "w:r") {
const textElement = (element.elements ?? []).filter((e) => e.type === "element" && e.name === "w:t");
for (const text of textElement) {
if ((text.elements?.[0].text as string)?.includes(token)) {
return i;
}
}
}
}
throw new Error("Token not found");
};
export const splitRunElement = (runElement: Element, token: string): { readonly left: Element; readonly right: Element } => {
let splitIndex = 0;
const splitElements =
runElement.elements
?.map((e, i) => {
if (e.type === "element" && e.name === "w:t") {
const text = (e.elements?.[0].text as string) ?? "";
const splitText = text.split(token);
const newElements = splitText.map((t) => ({
...e,
...patchSpaceAttribute(e),
elements: createTextElementContents(t),
}));
splitIndex = i;
return newElements;
} else {
return e;
}
})
.flat() ?? [];
const leftRunElement: Element = {
...JSON.parse(JSON.stringify(runElement)),
elements: splitElements.slice(0, splitIndex + 1),
};
const rightRunElement: Element = {
...JSON.parse(JSON.stringify(runElement)),
elements: splitElements.slice(splitIndex + 1),
};
return { left: leftRunElement, right: rightRunElement };
};

View File

@ -0,0 +1,315 @@
import { expect } from "chai";
import { replaceTokenInParagraphElement } from "./paragraph-token-replacer";
describe("paragraph-token-replacer", () => {
describe("replaceTokenInParagraphElement", () => {
it("should replace token in paragraph", () => {
const output = replaceTokenInParagraphElement({
paragraphElement: {
name: "w:p",
elements: [
{
name: "w:r",
elements: [
{
name: "w:t",
elements: [
{
type: "text",
text: "hello",
},
],
},
],
},
],
},
renderedParagraph: {
index: 0,
path: [0],
runs: [
{
end: 4,
index: 0,
parts: [
{
end: 4,
index: 0,
start: 0,
text: "hello",
},
],
start: 0,
text: "hello",
},
],
text: "hello",
},
originalText: "hello",
replacementText: "world",
});
expect(output).to.deep.equal({
elements: [
{
elements: [
{
elements: [
{
text: "world",
type: "text",
},
],
name: "w:t",
},
],
name: "w:r",
},
],
name: "w:p",
});
});
it("should handle case where it cannot find any text to replace", () => {
const output = replaceTokenInParagraphElement({
paragraphElement: {
name: "w:p",
attributes: {
"w14:paraId": "2499FE9F",
"w14:textId": "27B4FBC2",
"w:rsidR": "00B51233",
"w:rsidRDefault": "007B52ED",
"w:rsidP": "007B52ED",
},
elements: [
{
type: "element",
name: "w:pPr",
elements: [{ type: "element", name: "w:pStyle", attributes: { "w:val": "Title" } }],
},
{
type: "element",
name: "w:r",
elements: [
{
type: "element",
name: "w:t",
attributes: { "xml:space": "preserve" },
elements: [{ type: "text", text: "Hello " }],
},
],
},
{
type: "element",
name: "w:r",
attributes: { "w:rsidR": "007F116B" },
elements: [
{
type: "element",
name: "w:t",
attributes: { "xml:space": "preserve" },
elements: [{ type: "text", text: "{{name}} " }],
},
],
},
{
type: "element",
name: "w:r",
elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "World" }] }],
},
],
},
renderedParagraph: {
text: "Hello {{name}} World",
runs: [
{ text: "Hello ", parts: [{ text: "Hello ", index: 0, start: 0, end: 5 }], index: 1, start: 0, end: 5 },
{ text: "{{name}} ", parts: [{ text: "{{name}} ", index: 0, start: 6, end: 14 }], index: 2, start: 6, end: 14 },
{ text: "World", parts: [{ text: "World", index: 0, start: 15, end: 19 }], index: 3, start: 15, end: 19 },
],
index: 0,
path: [0, 1, 0, 0],
},
originalText: "{{name}}",
replacementText: "John",
});
expect(output).to.deep.equal({
attributes: {
"w14:paraId": "2499FE9F",
"w14:textId": "27B4FBC2",
"w:rsidP": "007B52ED",
"w:rsidR": "00B51233",
"w:rsidRDefault": "007B52ED",
},
elements: [
{
elements: [
{
attributes: {
"w:val": "Title",
},
name: "w:pStyle",
type: "element",
},
],
name: "w:pPr",
type: "element",
},
{
elements: [
{
attributes: {
"xml:space": "preserve",
},
elements: [
{
text: "Hello ",
type: "text",
},
],
name: "w:t",
type: "element",
},
],
name: "w:r",
type: "element",
},
{
attributes: {
"w:rsidR": "007F116B",
},
elements: [
{
attributes: {
"xml:space": "preserve",
},
elements: [
{
text: "John ",
type: "text",
},
],
name: "w:t",
type: "element",
},
],
name: "w:r",
type: "element",
},
{
elements: [
{
attributes: {
"xml:space": "preserve",
},
elements: [
{
text: "World",
type: "text",
},
],
name: "w:t",
type: "element",
},
],
name: "w:r",
type: "element",
},
],
name: "w:p",
});
});
// Try to fill rest of test coverage
// it("should replace token in paragraph", () => {
// const output = replaceTokenInParagraphElement({
// paragraphElement: {
// name: "w:p",
// elements: [
// {
// name: "w:r",
// elements: [
// {
// name: "w:t",
// elements: [
// {
// type: "text",
// text: "test ",
// },
// ],
// },
// {
// name: "w:t",
// elements: [
// {
// type: "text",
// text: " hello ",
// },
// ],
// },
// ],
// },
// ],
// },
// renderedParagraph: {
// index: 0,
// path: [0],
// runs: [
// {
// end: 4,
// index: 0,
// parts: [
// {
// end: 4,
// index: 0,
// start: 0,
// text: "test ",
// },
// ],
// start: 0,
// text: "test ",
// },
// {
// end: 10,
// index: 0,
// parts: [
// {
// end: 10,
// index: 0,
// start: 5,
// text: "hello ",
// },
// ],
// start: 5,
// text: "hello ",
// },
// ],
// text: "test hello ",
// },
// originalText: "hello",
// replacementText: "world",
// });
// expect(output).to.deep.equal({
// elements: [
// {
// elements: [
// {
// elements: [
// {
// text: "test world ",
// type: "text",
// },
// ],
// name: "w:t",
// },
// ],
// name: "w:r",
// },
// ],
// name: "w:p",
// });
// });
});
});

View File

@ -0,0 +1,75 @@
import { Element } from "xml-js";
import { createTextElementContents, patchSpaceAttribute } from "./util";
import { IRenderedParagraphNode } from "./run-renderer";
enum ReplaceMode {
START,
MIDDLE,
END,
}
export const replaceTokenInParagraphElement = ({
paragraphElement,
renderedParagraph,
originalText,
replacementText,
}: {
readonly paragraphElement: Element;
readonly renderedParagraph: IRenderedParagraphNode;
readonly originalText: string;
readonly replacementText: string;
}): Element => {
const startIndex = renderedParagraph.text.indexOf(originalText);
const endIndex = startIndex + originalText.length - 1;
let replaceMode = ReplaceMode.START;
for (const run of renderedParagraph.runs) {
for (const { text, index, start, end } of run.parts) {
switch (replaceMode) {
case ReplaceMode.START:
if (startIndex >= start) {
const offsetStartIndex = startIndex - start;
const offsetEndIndex = Math.min(endIndex, end) - start;
const partToReplace = run.text.substring(offsetStartIndex, offsetEndIndex + 1);
// We use a token to split the text if the replacement is within the same run
// If not, we just add text to the middle of the run later
if (partToReplace === "") {
continue;
}
const firstPart = text.replace(partToReplace, replacementText);
patchTextElement(paragraphElement.elements![run.index].elements![index], firstPart);
replaceMode = ReplaceMode.MIDDLE;
continue;
}
break;
case ReplaceMode.MIDDLE:
if (endIndex <= end) {
const lastPart = text.substring(endIndex - start + 1);
patchTextElement(paragraphElement.elements![run.index].elements![index], lastPart);
const currentElement = paragraphElement.elements![run.index].elements![index];
// We need to add xml:space="preserve" to the last element to preserve the whitespace
// Otherwise, the text will be merged with the next element
// eslint-disable-next-line functional/immutable-data
paragraphElement.elements![run.index].elements![index] = patchSpaceAttribute(currentElement);
replaceMode = ReplaceMode.END;
} else {
patchTextElement(paragraphElement.elements![run.index].elements![index], "");
}
break;
default:
}
}
}
return paragraphElement;
};
const patchTextElement = (element: Element, text: string): Element => {
// eslint-disable-next-line functional/immutable-data
element.elements = createTextElementContents(text);
return element;
};

View File

@ -0,0 +1,87 @@
import { TargetModeType } from "@file/relationships/relationship/relationship";
import { expect } from "chai";
import { appendRelationship, getNextRelationshipIndex } from "./relationship-manager";
describe("relationship-manager", () => {
describe("getNextRelationshipIndex", () => {
it("should get next relationship index", () => {
const output = getNextRelationshipIndex({
elements: [
{
type: "element",
name: "Relationships",
elements: [
{ type: "element", attributes: { Id: "rId1" }, name: "Relationship" },
{ type: "element", attributes: { Id: "rId1" }, name: "Relationship" },
],
},
],
});
expect(output).to.deep.equal(2);
});
it("should work with an empty relationship Id", () => {
const output = getNextRelationshipIndex({
elements: [
{
type: "element",
name: "Relationships",
elements: [{ type: "element", name: "Relationship" }],
},
],
});
expect(output).to.deep.equal(1);
});
it("should work with no relationships", () => {
const output = getNextRelationshipIndex({
elements: [
{
type: "element",
name: "Relationships",
elements: [],
},
],
});
expect(output).to.deep.equal(1);
});
});
describe("appendRelationship", () => {
it("should append a relationship", () => {
const output = appendRelationship(
{
elements: [
{
type: "element",
name: "Relationships",
elements: [
{ type: "element", attributes: { Id: "rId1" }, name: "Relationship" },
{ type: "element", attributes: { Id: "rId1" }, name: "Relationship" },
],
},
],
},
1,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
"test",
TargetModeType.EXTERNAL,
);
expect(output).to.deep.equal([
{ type: "element", attributes: { Id: "rId1" }, name: "Relationship" },
{ type: "element", attributes: { Id: "rId1" }, name: "Relationship" },
{
attributes: {
Id: "rId1",
Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
TargetMode: TargetModeType.EXTERNAL,
Target: "test",
},
name: "Relationship",
type: "element",
},
]);
});
});
});

View File

@ -0,0 +1,42 @@
import { Element } from "xml-js";
import { RelationshipType, TargetModeType } from "@file/relationships/relationship/relationship";
import { getFirstLevelElements } from "./util";
const getIdFromRelationshipId = (relationshipId: string): number => {
const output = parseInt(relationshipId.substring(3), 10);
return isNaN(output) ? 0 : output;
};
export const getNextRelationshipIndex = (relationships: Element): number => {
const relationshipElements = getFirstLevelElements(relationships, "Relationships");
return (
relationshipElements
.map((e) => getIdFromRelationshipId(e.attributes?.Id?.toString() ?? ""))
.reduce((acc, curr) => Math.max(acc, curr), 0) + 1
);
};
export const appendRelationship = (
relationships: Element,
id: number | string,
type: RelationshipType,
target: string,
targetMode?: TargetModeType,
): readonly Element[] => {
const relationshipElements = getFirstLevelElements(relationships, "Relationships");
// eslint-disable-next-line functional/immutable-data
relationshipElements.push({
attributes: {
Id: `rId${id}`,
Type: type,
Target: target,
TargetMode: targetMode,
},
name: "Relationship",
type: "element",
});
return relationshipElements;
};

View File

@ -0,0 +1,206 @@
import { IViewWrapper } from "@file/document-wrapper";
import { File } from "@file/file";
import { Paragraph, TextRun } from "@file/paragraph";
import { IContext } from "@file/xml-components";
import { expect } from "chai";
import * as sinon from "sinon";
import { PatchType } from "./from-docx";
import { replacer } from "./replacer";
const MOCK_JSON = {
elements: [
{
type: "element",
name: "w:hdr",
elements: [
{
type: "element",
name: "w:p",
attributes: { "w14:paraId": "3BE1A671", "w14:textId": "74E856C4", "w:rsidR": "000D38A7", "w:rsidRDefault": "000D38A7" },
elements: [
{
type: "element",
name: "w:pPr",
elements: [{ type: "element", name: "w:pStyle", attributes: { "w:val": "Header" } }],
},
{
type: "element",
name: "w:r",
elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "This is a {{head" }] }],
},
{
type: "element",
name: "w:r",
attributes: { "w:rsidR": "004A3A99" },
elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "er" }] }],
},
{
type: "element",
name: "w:r",
elements: [
{ type: "element", name: "w:t", elements: [{ type: "text", text: "_adjective}} dont you think?" }] },
],
},
],
},
],
},
],
};
describe("replacer", () => {
describe("replacer", () => {
it("should return the same object if nothing is added", () => {
const output = replacer(
{
elements: [],
},
{
type: PatchType.PARAGRAPH,
children: [],
},
"hello",
[],
sinon.mock() as unknown as IContext,
);
expect(output).to.deep.equal({
elements: [],
});
});
it("should replace paragraph type", () => {
const output = replacer(
MOCK_JSON,
{
type: PatchType.PARAGRAPH,
children: [new TextRun("Delightful Header")],
},
"{{header_adjective}}",
[
{
text: "This is a {{header_adjective}} dont you think?",
runs: [
{
text: "This is a {{head",
parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }],
index: 1,
start: 0,
end: 15,
},
{ text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 },
{
text: "_adjective}} dont you think?",
parts: [{ text: "_adjective}} dont you think?", index: 0, start: 18, end: 46 }],
index: 3,
start: 18,
end: 46,
},
],
index: 0,
path: [0, 0, 0],
},
],
{
file: {} as unknown as File,
viewWrapper: {
Relationships: {},
} as unknown as IViewWrapper,
stack: [],
},
);
expect(JSON.stringify(output)).to.contain("Delightful Header");
});
it("should replace document type", () => {
const output = replacer(
MOCK_JSON,
{
type: PatchType.DOCUMENT,
children: [new Paragraph("Lorem ipsum paragraph")],
},
"{{header_adjective}}",
[
{
text: "This is a {{header_adjective}} dont you think?",
runs: [
{
text: "This is a {{head",
parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }],
index: 1,
start: 0,
end: 15,
},
{ text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 },
{
text: "_adjective}} dont you think?",
parts: [{ text: "_adjective}} dont you think?", index: 0, start: 18, end: 46 }],
index: 3,
start: 18,
end: 46,
},
],
index: 0,
path: [0, 0, 0],
},
],
{
file: {} as unknown as File,
viewWrapper: {
Relationships: {},
} as unknown as IViewWrapper,
stack: [],
},
);
expect(JSON.stringify(output)).to.contain("Lorem ipsum paragraph");
});
it("should throw an error if the type is not supported", () => {
expect(() =>
replacer(
{},
{
type: PatchType.DOCUMENT,
children: [new Paragraph("Lorem ipsum paragraph")],
},
"{{header_adjective}}",
[
{
text: "This is a {{header_adjective}} dont you think?",
runs: [
{
text: "This is a {{head",
parts: [{ text: "This is a {{head", index: 0, start: 0, end: 15 }],
index: 1,
start: 0,
end: 15,
},
{ text: "er", parts: [{ text: "er", index: 0, start: 16, end: 17 }], index: 2, start: 16, end: 17 },
{
text: "_adjective}} dont you think?",
parts: [{ text: "_adjective}} dont you think?", index: 0, start: 18, end: 46 }],
index: 3,
start: 18,
end: 46,
},
],
index: 0,
path: [0, 0, 0],
},
],
{
file: {} as unknown as File,
viewWrapper: {
Relationships: {},
} as unknown as IViewWrapper,
stack: [],
},
),
).to.throw();
});
});
});

83
src/patcher/replacer.ts Normal file
View File

@ -0,0 +1,83 @@
import { Element } from "xml-js";
import * as xml from "xml";
import { Formatter } from "@export/formatter";
import { IContext, XmlComponent } from "@file/xml-components";
import { IPatch, PatchType } from "./from-docx";
import { toJson } from "./util";
import { IRenderedParagraphNode } from "./run-renderer";
import { replaceTokenInParagraphElement } from "./paragraph-token-replacer";
import { findRunElementIndexWithToken, splitRunElement } from "./paragraph-split-inject";
const formatter = new Formatter();
const SPLIT_TOKEN = "ɵ";
export const replacer = (
json: Element,
patch: IPatch,
patchText: string,
renderedParagraphs: readonly IRenderedParagraphNode[],
context: IContext,
): Element => {
for (const renderedParagraph of renderedParagraphs) {
const textJson = patch.children
// eslint-disable-next-line no-loop-func
.map((c) => toJson(xml(formatter.format(c as XmlComponent, context))))
.map((c) => c.elements![0]);
switch (patch.type) {
case PatchType.DOCUMENT: {
const parentElement = goToParentElementFromPath(json, renderedParagraph.path);
const elementIndex = getLastElementIndexFromPath(renderedParagraph.path);
// eslint-disable-next-line functional/immutable-data, prefer-destructuring
parentElement.elements!.splice(elementIndex, 1, ...textJson);
break;
}
case PatchType.PARAGRAPH:
default: {
const paragraphElement = goToElementFromPath(json, renderedParagraph.path);
replaceTokenInParagraphElement({
paragraphElement,
renderedParagraph,
originalText: patchText,
replacementText: SPLIT_TOKEN,
});
const index = findRunElementIndexWithToken(paragraphElement, SPLIT_TOKEN);
const { left, right } = splitRunElement(paragraphElement.elements![index], SPLIT_TOKEN);
// eslint-disable-next-line functional/immutable-data
paragraphElement.elements!.splice(index, 1, left, ...textJson, right);
break;
}
}
}
return json;
};
const goToElementFromPath = (json: Element, path: readonly number[]): Element => {
let element = json;
// We start from 1 because the first element is the root element
// Which we do not want to double count
for (let i = 1; i < path.length; i++) {
const index = path[i];
const nextElements = element.elements;
if (!nextElements) {
throw new Error("Could not find element");
}
element = nextElements[index];
}
return element;
};
const goToParentElementFromPath = (json: Element, path: readonly number[]): Element =>
goToElementFromPath(json, path.slice(0, path.length - 1));
const getLastElementIndexFromPath = (path: readonly number[]): number => path[path.length - 1];

View File

@ -0,0 +1,96 @@
import { expect } from "chai";
import { renderParagraphNode } from "./run-renderer";
describe("run-renderer", () => {
describe("renderParagraphNode", () => {
it("should return a rendered paragraph node if theres no elements", () => {
const output = renderParagraphNode({ element: { name: "w:p" }, index: 0, parent: undefined });
expect(output).to.deep.equal({
index: -1,
path: [],
runs: [],
text: "",
});
});
it("should return a rendered paragraph node if there are elements", () => {
const output = renderParagraphNode({
element: {
name: "w:p",
elements: [
{
name: "w:r",
elements: [
{
name: "w:t",
elements: [
{
type: "text",
text: "hello",
},
],
},
],
},
],
},
index: 0,
parent: undefined,
});
expect(output).to.deep.equal({
index: 0,
path: [0],
runs: [
{
end: 4,
index: 0,
parts: [
{
end: 4,
index: 0,
start: 0,
text: "hello",
},
],
start: 0,
text: "hello",
},
],
text: "hello",
});
});
it("should throw an error if the element is not a paragraph", () => {
expect(() => renderParagraphNode({ element: { name: "w:r" }, index: 0, parent: undefined })).to.throw();
});
it("should return blank defaults if run is empty", () => {
const output = renderParagraphNode({
element: {
name: "w:p",
elements: [
{
name: "w:r",
},
],
},
index: 0,
parent: undefined,
});
expect(output).to.deep.equal({
index: 0,
path: [0],
runs: [
{
end: 0,
index: -1,
parts: [],
start: 0,
text: "",
},
],
text: "",
});
});
});
});

109
src/patcher/run-renderer.ts Normal file
View File

@ -0,0 +1,109 @@
import { Element } from "xml-js";
import { ElementWrapper } from "./traverser";
export interface IRenderedParagraphNode {
readonly text: string;
readonly runs: readonly IRenderedRunNode[];
readonly index: number;
readonly path: readonly number[];
}
interface StartAndEnd {
readonly start: number;
readonly end: number;
}
type IParts = {
readonly text: string;
readonly index: number;
} & StartAndEnd;
export type IRenderedRunNode = {
readonly text: string;
readonly parts: readonly IParts[];
readonly index: number;
} & StartAndEnd;
export const renderParagraphNode = (node: ElementWrapper): IRenderedParagraphNode => {
if (node.element.name !== "w:p") {
throw new Error(`Invalid node type: ${node.element.name}`);
}
if (!node.element.elements) {
return {
text: "",
runs: [],
index: -1,
path: [],
};
}
let currentRunStringLength = 0;
const runs = node.element.elements
.map((element, i) => ({ element, i }))
.filter(({ element }) => element.name === "w:r")
.map(({ element, i }) => {
const renderedRunNode = renderRunNode(element, i, currentRunStringLength);
currentRunStringLength += renderedRunNode.text.length;
return renderedRunNode;
})
.filter((e) => !!e)
.map((e) => e as IRenderedRunNode);
const text = runs.reduce((acc, curr) => acc + curr.text, "");
return {
text,
runs,
index: node.index,
path: buildNodePath(node),
};
};
const renderRunNode = (node: Element, index: number, currentRunStringIndex: number): IRenderedRunNode => {
if (!node.elements) {
return {
text: "",
parts: [],
index: -1,
start: currentRunStringIndex,
end: currentRunStringIndex,
};
}
let currentTextStringIndex = currentRunStringIndex;
const parts = node.elements
.map((element, i: number) =>
element.name === "w:t" && element.elements && element.elements.length > 0
? {
text: element.elements[0].text?.toString() ?? "",
index: i,
start: currentTextStringIndex,
end: (() => {
// Side effect
currentTextStringIndex += (element.elements[0].text?.toString() ?? "").length - 1;
return currentTextStringIndex;
})(),
}
: undefined,
)
.filter((e) => !!e)
.map((e) => e as IParts);
const text = parts.reduce((acc, curr) => acc + curr.text, "");
return {
text,
parts,
index,
start: currentRunStringIndex,
end: currentTextStringIndex,
};
};
const buildNodePath = (node: ElementWrapper): readonly number[] =>
node.parent ? [...buildNodePath(node.parent), node.index] : [node.index];

View File

@ -0,0 +1,599 @@
import { expect } from "chai";
import { findLocationOfText } from "./traverser";
const MOCK_JSON = {
elements: [
{
type: "element",
name: "w:document",
attributes: {
"xmlns:wpc": "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas",
"xmlns:cx": "http://schemas.microsoft.com/office/drawing/2014/chartex",
"xmlns:cx1": "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex",
"xmlns:cx2": "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex",
"xmlns:cx3": "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex",
"xmlns:cx4": "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex",
"xmlns:cx5": "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex",
"xmlns:cx6": "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex",
"xmlns:cx7": "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex",
"xmlns:cx8": "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex",
"xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
"xmlns:aink": "http://schemas.microsoft.com/office/drawing/2016/ink",
"xmlns:am3d": "http://schemas.microsoft.com/office/drawing/2017/model3d",
"xmlns:o": "urn:schemas-microsoft-com:office:office",
"xmlns:oel": "http://schemas.microsoft.com/office/2019/extlst",
"xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
"xmlns:m": "http://schemas.openxmlformats.org/officeDocument/2006/math",
"xmlns:v": "urn:schemas-microsoft-com:vml",
"xmlns:wp14": "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
"xmlns:wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
"xmlns:w10": "urn:schemas-microsoft-com:office:word",
"xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"xmlns:w14": "http://schemas.microsoft.com/office/word/2010/wordml",
"xmlns:w15": "http://schemas.microsoft.com/office/word/2012/wordml",
"xmlns:w16cex": "http://schemas.microsoft.com/office/word/2018/wordml/cex",
"xmlns:w16cid": "http://schemas.microsoft.com/office/word/2016/wordml/cid",
"xmlns:w16": "http://schemas.microsoft.com/office/word/2018/wordml",
"xmlns:w16sdtdh": "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash",
"xmlns:w16se": "http://schemas.microsoft.com/office/word/2015/wordml/symex",
"xmlns:wpg": "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
"xmlns:wpi": "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
"xmlns:wne": "http://schemas.microsoft.com/office/word/2006/wordml",
"xmlns:wps": "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
},
elements: [
{
type: "element",
name: "w:body",
elements: [
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "2499FE9F",
"w14:textId": "0A3D130F",
"w:rsidR": "00B51233",
"w:rsidRDefault": "007B52ED",
"w:rsidP": "007B52ED",
},
elements: [
{
type: "element",
name: "w:pPr",
elements: [{ type: "element", name: "w:pStyle", attributes: { "w:val": "Title" } }],
},
{
type: "element",
name: "w:r",
elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "Hello World" }] }],
},
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "6410D9A0",
"w14:textId": "7579AB49",
"w:rsidR": "007B52ED",
"w:rsidRDefault": "007B52ED",
},
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "57ACF964",
"w14:textId": "315D7A05",
"w:rsidR": "007B52ED",
"w:rsidRDefault": "007B52ED",
},
elements: [
{
type: "element",
name: "w:r",
elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "Hello {{name}}," }] }],
},
{
type: "element",
name: "w:r",
attributes: { "w:rsidR": "008126CB" },
elements: [
{
type: "element",
name: "w:t",
attributes: { "xml:space": "preserve" },
elements: [{ type: "text", text: " how are you?" }],
},
],
},
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "38C7DF4A",
"w14:textId": "66CDEC9A",
"w:rsidR": "007B52ED",
"w:rsidRDefault": "007B52ED",
},
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "04FABE2B",
"w14:textId": "3DACA001",
"w:rsidR": "007B52ED",
"w:rsidRDefault": "007B52ED",
},
elements: [
{
type: "element",
name: "w:r",
elements: [
{ type: "element", name: "w:t", elements: [{ type: "text", text: "{{paragraph_replace}}" }] },
],
},
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "7AD7975D",
"w14:textId": "77777777",
"w:rsidR": "00EF161F",
"w:rsidRDefault": "00EF161F",
},
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "3BD6D75A",
"w14:textId": "19AE3121",
"w:rsidR": "00EF161F",
"w:rsidRDefault": "00EF161F",
},
elements: [
{
type: "element",
name: "w:r",
elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "{{table}}" }] }],
},
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "76023962",
"w14:textId": "4E606AB9",
"w:rsidR": "007B52ED",
"w:rsidRDefault": "007B52ED",
},
},
{
type: "element",
name: "w:tbl",
elements: [
{
type: "element",
name: "w:tblPr",
elements: [
{ type: "element", name: "w:tblStyle", attributes: { "w:val": "TableGrid" } },
{ type: "element", name: "w:tblW", attributes: { "w:w": "0", "w:type": "auto" } },
{
type: "element",
name: "w:tblLook",
attributes: {
"w:val": "04A0",
"w:firstRow": "1",
"w:lastRow": "0",
"w:firstColumn": "1",
"w:lastColumn": "0",
"w:noHBand": "0",
"w:noVBand": "1",
},
},
],
},
{
type: "element",
name: "w:tblGrid",
elements: [
{ type: "element", name: "w:gridCol", attributes: { "w:w": "3003" } },
{ type: "element", name: "w:gridCol", attributes: { "w:w": "3003" } },
{ type: "element", name: "w:gridCol", attributes: { "w:w": "3004" } },
],
},
{
type: "element",
name: "w:tr",
attributes: {
"w:rsidR": "00EF161F",
"w14:paraId": "1DEC5955",
"w14:textId": "77777777",
"w:rsidTr": "00EF161F",
},
elements: [
{
type: "element",
name: "w:tc",
elements: [
{
type: "element",
name: "w:tcPr",
elements: [
{ type: "element", name: "w:tcW", attributes: { "w:w": "3003", "w:type": "dxa" } },
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "54DA5587",
"w14:textId": "625BAC60",
"w:rsidR": "00EF161F",
"w:rsidRDefault": "00EF161F",
},
elements: [
{
type: "element",
name: "w:r",
elements: [
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "{{table_heading_1}}" }],
},
],
},
],
},
],
},
{
type: "element",
name: "w:tc",
elements: [
{
type: "element",
name: "w:tcPr",
elements: [
{ type: "element", name: "w:tcW", attributes: { "w:w": "3003", "w:type": "dxa" } },
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "57100910",
"w14:textId": "71FD5616",
"w:rsidR": "00EF161F",
"w:rsidRDefault": "00EF161F",
},
},
],
},
{
type: "element",
name: "w:tc",
elements: [
{
type: "element",
name: "w:tcPr",
elements: [
{ type: "element", name: "w:tcW", attributes: { "w:w": "3004", "w:type": "dxa" } },
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "1D388FAB",
"w14:textId": "77777777",
"w:rsidR": "00EF161F",
"w:rsidRDefault": "00EF161F",
},
},
],
},
],
},
{
type: "element",
name: "w:tr",
attributes: {
"w:rsidR": "00EF161F",
"w14:paraId": "0F53D2DC",
"w14:textId": "77777777",
"w:rsidTr": "00EF161F",
},
elements: [
{
type: "element",
name: "w:tc",
elements: [
{
type: "element",
name: "w:tcPr",
elements: [
{ type: "element", name: "w:tcW", attributes: { "w:w": "3003", "w:type": "dxa" } },
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "0F2BCCED",
"w14:textId": "3C3B6706",
"w:rsidR": "00EF161F",
"w:rsidRDefault": "00EF161F",
},
elements: [
{
type: "element",
name: "w:r",
elements: [
{
type: "element",
name: "w:t",
elements: [{ type: "text", text: "Item: {{item_1}}" }],
},
],
},
],
},
],
},
{
type: "element",
name: "w:tc",
elements: [
{
type: "element",
name: "w:tcPr",
elements: [
{ type: "element", name: "w:tcW", attributes: { "w:w": "3003", "w:type": "dxa" } },
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "1E6158AC",
"w14:textId": "77777777",
"w:rsidR": "00EF161F",
"w:rsidRDefault": "00EF161F",
},
},
],
},
{
type: "element",
name: "w:tc",
elements: [
{
type: "element",
name: "w:tcPr",
elements: [
{ type: "element", name: "w:tcW", attributes: { "w:w": "3004", "w:type": "dxa" } },
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "17937748",
"w14:textId": "77777777",
"w:rsidR": "00EF161F",
"w:rsidRDefault": "00EF161F",
},
},
],
},
],
},
{
type: "element",
name: "w:tr",
attributes: {
"w:rsidR": "00EF161F",
"w14:paraId": "781DAC1A",
"w14:textId": "77777777",
"w:rsidTr": "00EF161F",
},
elements: [
{
type: "element",
name: "w:tc",
elements: [
{
type: "element",
name: "w:tcPr",
elements: [
{ type: "element", name: "w:tcW", attributes: { "w:w": "3003", "w:type": "dxa" } },
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "1DCD0343",
"w14:textId": "77777777",
"w:rsidR": "00EF161F",
"w:rsidRDefault": "00EF161F",
},
},
],
},
{
type: "element",
name: "w:tc",
elements: [
{
type: "element",
name: "w:tcPr",
elements: [
{ type: "element", name: "w:tcW", attributes: { "w:w": "3003", "w:type": "dxa" } },
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "5D02E3CD",
"w14:textId": "77777777",
"w:rsidR": "00EF161F",
"w:rsidRDefault": "00EF161F",
},
},
],
},
{
type: "element",
name: "w:tc",
elements: [
{
type: "element",
name: "w:tcPr",
elements: [
{ type: "element", name: "w:tcW", attributes: { "w:w": "3004", "w:type": "dxa" } },
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "52EA0DBB",
"w14:textId": "77777777",
"w:rsidR": "00EF161F",
"w:rsidRDefault": "00EF161F",
},
},
],
},
],
},
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "47CD1FBC",
"w14:textId": "23474CBC",
"w:rsidR": "007B52ED",
"w:rsidRDefault": "007B52ED",
},
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "0ACCEE90",
"w14:textId": "67907499",
"w:rsidR": "00EF161F",
"w:rsidRDefault": "0077578F",
},
elements: [
{
type: "element",
name: "w:r",
elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "{{image_test}}" }] }],
},
],
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "23FA9862",
"w14:textId": "77777777",
"w:rsidR": "0077578F",
"w:rsidRDefault": "0077578F",
},
},
{
type: "element",
name: "w:p",
attributes: {
"w14:paraId": "01578F2F",
"w14:textId": "3BDC6C85",
"w:rsidR": "007B52ED",
"w:rsidRDefault": "007B52ED",
},
elements: [
{
type: "element",
name: "w:r",
elements: [{ type: "element", name: "w:t", elements: [{ type: "text", text: "Thank you" }] }],
},
],
},
{
type: "element",
name: "w:sectPr",
attributes: { "w:rsidR": "007B52ED", "w:rsidSect": "0072043F" },
elements: [
{ type: "element", name: "w:headerReference", attributes: { "w:type": "default", "r:id": "rId6" } },
{ type: "element", name: "w:footerReference", attributes: { "w:type": "default", "r:id": "rId7" } },
{ type: "element", name: "w:pgSz", attributes: { "w:w": "11900", "w:h": "16840" } },
{
type: "element",
name: "w:pgMar",
attributes: {
"w:top": "1440",
"w:right": "1440",
"w:bottom": "1440",
"w:left": "1440",
"w:header": "708",
"w:footer": "708",
"w:gutter": "0",
},
},
{ type: "element", name: "w:cols", attributes: { "w:space": "708" } },
{ type: "element", name: "w:docGrid", attributes: { "w:linePitch": "360" } },
],
},
],
},
],
},
],
};
describe("traverser", () => {
describe("findLocationOfText", () => {
it("should find the location of text", () => {
const output = findLocationOfText(MOCK_JSON, "{{table_heading_1}}");
expect(output).to.deep.equal([
{
index: 1,
path: [0, 0, 0, 8, 2, 0, 1],
runs: [
{
end: 18,
index: 0,
parts: [
{
end: 18,
index: 0,
start: 0,
text: "{{table_heading_1}}",
},
],
start: 0,
text: "{{table_heading_1}}",
},
],
text: "{{table_heading_1}}",
},
]);
});
});
});

45
src/patcher/traverser.ts Normal file
View File

@ -0,0 +1,45 @@
import { Element } from "xml-js";
import { IRenderedParagraphNode, renderParagraphNode } from "./run-renderer";
export interface ElementWrapper {
readonly element: Element;
readonly index: number;
readonly parent: ElementWrapper | undefined;
}
const elementsToWrapper = (wrapper: ElementWrapper): readonly ElementWrapper[] =>
wrapper.element.elements?.map((e, i) => ({
element: e,
index: i,
parent: wrapper,
})) ?? [];
export const findLocationOfText = (node: Element, text: string): readonly IRenderedParagraphNode[] => {
let renderedParagraphs: readonly IRenderedParagraphNode[] = [];
// eslint-disable-next-line functional/prefer-readonly-type
const queue: ElementWrapper[] = [
...elementsToWrapper({
element: node,
index: 0,
parent: undefined,
}),
];
// eslint-disable-next-line functional/immutable-data
let currentNode: ElementWrapper | undefined;
while (queue.length > 0) {
// eslint-disable-next-line functional/immutable-data
currentNode = queue.shift()!; // This is safe because we check the length of the queue
if (currentNode.element.name === "w:p") {
renderedParagraphs = [...renderedParagraphs, renderParagraphNode(currentNode)];
} else {
// eslint-disable-next-line functional/immutable-data
queue.push(...elementsToWrapper(currentNode));
}
}
return renderedParagraphs.filter((p) => p.text.includes(text));
};

50
src/patcher/util.spec.ts Normal file
View File

@ -0,0 +1,50 @@
import { expect } from "chai";
import { createTextElementContents, getFirstLevelElements, patchSpaceAttribute, toJson } from "./util";
describe("util", () => {
describe("toJson", () => {
it("should return an Element", () => {
const output = toJson("<xml></xml>");
expect(output).to.be.an("object");
});
});
describe("createTextElementContents", () => {
it("should return an array of elements", () => {
const output = createTextElementContents("hello");
expect(output).to.deep.equal([{ type: "text", text: "hello" }]);
});
});
describe("patchSpaceAttribute", () => {
it("should return an element with the xml:space attribute", () => {
const output = patchSpaceAttribute({ type: "element", name: "xml" });
expect(output).to.deep.equal({
type: "element",
name: "xml",
attributes: {
"xml:space": "preserve",
},
});
});
});
describe("getFirstLevelElements", () => {
it("should return an empty array if no elements are found", () => {
const elements = getFirstLevelElements(
{ elements: [{ type: "element", name: "Relationships", elements: [] }] },
"Relationships",
);
expect(elements).to.deep.equal([]);
});
it("should return an array if elements are found", () => {
const elements = getFirstLevelElements(
{ elements: [{ type: "element", name: "Relationships", elements: [{ type: "element", name: "Relationship" }] }] },
"Relationships",
);
expect(elements).to.deep.equal([{ type: "element", name: "Relationship" }]);
});
});
});

30
src/patcher/util.ts Normal file
View File

@ -0,0 +1,30 @@
import { xml2js, Element } from "xml-js";
import * as xml from "xml";
import { Formatter } from "@export/formatter";
import { Text } from "@file/paragraph/run/run-components/text";
const formatter = new Formatter();
export const toJson = (xmlData: string): Element => {
const xmlObj = xml2js(xmlData, { compact: false, captureSpacesBetweenElements: true }) as Element;
return xmlObj;
};
// eslint-disable-next-line functional/prefer-readonly-type
export const createTextElementContents = (text: string): Element[] => {
const textJson = toJson(xml(formatter.format(new Text({ text }))));
return textJson.elements![0].elements ?? [];
};
export const patchSpaceAttribute = (element: Element): Element => ({
...element,
attributes: {
"xml:space": "preserve",
},
});
// eslint-disable-next-line functional/prefer-readonly-type
export const getFirstLevelElements = (relationships: Element, id: string): Element[] =>
relationships.elements?.filter((e) => e.name === id)[0].elements ?? [];

View File

@ -20,5 +20,10 @@
"@shared": ["./shared/index.ts"] "@shared": ["./shared/index.ts"]
} }
}, },
"ts-node": {
"compilerOptions": {
"module": "commonjs"
}
},
"include": ["src"] "include": ["src"]
} }

View File

@ -1,5 +1,8 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": {
"allowSyntheticDefaultImports": true
},
"typedocOptions": { "typedocOptions": {
"out": "docs/api", "out": "docs/api",
"exclude": "test", "exclude": "test",