Compare commits
55 Commits
Author | SHA1 | Date | |
---|---|---|---|
a708475539 | |||
9c60cfcbc7 | |||
a5454edc61 | |||
7152abfe48 | |||
075eeb7e3c | |||
f0acb3f3fb | |||
ae048833a1 | |||
ebae72004c | |||
13744abff5 | |||
492ef29d7f | |||
a6e6463a36 | |||
2ef596543b | |||
85005c3c07 | |||
0d2b433446 | |||
b1f67652e9 | |||
4e2befb7ef | |||
a887927968 | |||
ee6db1429a | |||
170309a7ed | |||
05fcf6edd4 | |||
eb2174e566 | |||
2a56360875 | |||
ba862766a6 | |||
71291dab8f | |||
974ba7b450 | |||
8f7df39c07 | |||
ca3d8f0121 | |||
46c517d195 | |||
ff37f3b460 | |||
7b9b474484 | |||
e80a50d36c | |||
df99f96469 | |||
f87ad6a43c | |||
8bf5220e31 | |||
da8148251a | |||
01183ac34d | |||
d553e4763a | |||
213c3d29f3 | |||
697c1d91c6 | |||
b22de4ac4f | |||
0af9d27dcc | |||
b89b571c4e | |||
64505a295f | |||
c247bbf409 | |||
4c60e6a0c0 | |||
086e5ef184 | |||
7e81f7b368 | |||
defa431aa9 | |||
17eb4fe8c4 | |||
3997ce538d | |||
3654eb0800 | |||
c6bb255641 | |||
05a3cf5b43 | |||
dece0f58e1 | |||
7f3c5615c9 |
@ -8,11 +8,14 @@
|
||||
// words - list of words to be always considered correct
|
||||
"words": [
|
||||
"Abjad",
|
||||
"aink",
|
||||
"aiueo",
|
||||
"ATLEAST",
|
||||
"chosung",
|
||||
"clippy",
|
||||
"datas",
|
||||
"dcmitype",
|
||||
"dcterms",
|
||||
"docsify",
|
||||
"dolan",
|
||||
"execa",
|
||||
@ -32,6 +35,7 @@
|
||||
"panose",
|
||||
"rels",
|
||||
"rsid",
|
||||
"sdtdh",
|
||||
"twip",
|
||||
"twips",
|
||||
"Xmlable",
|
||||
|
243
.eslintrc.yml
243
.eslintrc.yml
@ -1,243 +0,0 @@
|
||||
extends: eslint:recommended
|
||||
env:
|
||||
browser: true
|
||||
es6: true
|
||||
node: true
|
||||
parser: "@typescript-eslint/parser"
|
||||
parserOptions:
|
||||
project:
|
||||
- tsconfig.json
|
||||
sourceType: module
|
||||
plugins:
|
||||
- eslint-plugin-import
|
||||
- eslint-plugin-no-null
|
||||
- eslint-plugin-unicorn
|
||||
- eslint-plugin-jsdoc
|
||||
- eslint-plugin-prefer-arrow
|
||||
- "@typescript-eslint"
|
||||
- eslint-plugin-functional
|
||||
root: true
|
||||
rules:
|
||||
no-undef: "off"
|
||||
no-extra-boolean-cast: "off"
|
||||
no-alert: error
|
||||
no-self-compare: error
|
||||
no-unreachable-loop: error
|
||||
no-template-curly-in-string: error
|
||||
no-unused-private-class-members: error
|
||||
no-extend-native: error
|
||||
no-floating-decimal: error
|
||||
no-implied-eval: error
|
||||
no-iterator: error
|
||||
no-lone-blocks: error
|
||||
no-loop-func: error
|
||||
no-new-object: error
|
||||
no-proto: error
|
||||
no-useless-catch: error
|
||||
one-var-declaration-per-line: error
|
||||
prefer-arrow-callback: error
|
||||
prefer-destructuring: error
|
||||
prefer-exponentiation-operator: error
|
||||
prefer-promise-reject-errors: error
|
||||
prefer-regex-literals: error
|
||||
prefer-spread: error
|
||||
prefer-template: error
|
||||
require-await: error
|
||||
"@typescript-eslint/adjacent-overload-signatures": error
|
||||
"@typescript-eslint/array-type":
|
||||
- error
|
||||
- default: array
|
||||
"@typescript-eslint/no-restricted-types":
|
||||
- error
|
||||
- types:
|
||||
Object:
|
||||
message: Avoid using the `Object` type. Did you mean `object`?
|
||||
fixWith: object
|
||||
Function:
|
||||
message: >-
|
||||
Avoid using the `Function` type. Prefer a specific function type,
|
||||
like `() => void`.
|
||||
Boolean:
|
||||
message: Avoid using the `Boolean` type. Did you mean `boolean`?
|
||||
fixWith: boolean
|
||||
Number:
|
||||
message: Avoid using the `Number` type. Did you mean `number`?
|
||||
fixWith: number
|
||||
String:
|
||||
message: Avoid using the `String` type. Did you mean `string`?
|
||||
fixWith: string
|
||||
Symbol:
|
||||
message: Avoid using the `Symbol` type. Did you mean `symbol`?
|
||||
fixWith: symbol
|
||||
"@typescript-eslint/consistent-type-assertions": error
|
||||
"@typescript-eslint/dot-notation": error
|
||||
"@typescript-eslint/explicit-function-return-type":
|
||||
- error
|
||||
- allowExpressions: true
|
||||
allowTypedFunctionExpressions: true
|
||||
allowHigherOrderFunctions: false
|
||||
allowDirectConstAssertionInArrowFunctions: true
|
||||
allowConciseArrowFunctionExpressionsStartingWithVoid: true
|
||||
"@typescript-eslint/explicit-member-accessibility":
|
||||
- error
|
||||
- accessibility: explicit
|
||||
overrides:
|
||||
accessors: explicit
|
||||
"@typescript-eslint/explicit-module-boundary-types":
|
||||
- error
|
||||
- allowArgumentsExplicitlyTypedAsAny: true
|
||||
allowDirectConstAssertionInArrowFunctions: true
|
||||
allowHigherOrderFunctions: false
|
||||
allowTypedFunctionExpressions: false
|
||||
"@typescript-eslint/naming-convention":
|
||||
- error
|
||||
- selector:
|
||||
- objectLiteralProperty
|
||||
leadingUnderscore: allow
|
||||
format:
|
||||
- camelCase
|
||||
- PascalCase
|
||||
- UPPER_CASE # for constants
|
||||
filter:
|
||||
regex: (^[a-z]+:.+)|_attr|[0-9]
|
||||
match: false
|
||||
"@typescript-eslint/no-empty-function": error
|
||||
"@typescript-eslint/no-empty-interface": error
|
||||
"@typescript-eslint/no-explicit-any": error
|
||||
"@typescript-eslint/no-misused-new": error
|
||||
"@typescript-eslint/no-namespace": error
|
||||
"@typescript-eslint/no-parameter-properties": "off"
|
||||
"@typescript-eslint/no-require-imports": error
|
||||
"@typescript-eslint/no-shadow":
|
||||
- error
|
||||
- hoist: all
|
||||
"@typescript-eslint/no-this-alias": error
|
||||
"@typescript-eslint/no-unused-expressions": error
|
||||
"@typescript-eslint/no-use-before-define": "off"
|
||||
"@typescript-eslint/no-var-requires": error
|
||||
"@typescript-eslint/prefer-for-of": error
|
||||
"@typescript-eslint/prefer-function-type": error
|
||||
"@typescript-eslint/prefer-namespace-keyword": error
|
||||
"@typescript-eslint/prefer-readonly": error
|
||||
"@typescript-eslint/triple-slash-reference":
|
||||
- error
|
||||
- path: always
|
||||
types: prefer-import
|
||||
lib: always
|
||||
"@typescript-eslint/typedef":
|
||||
- error
|
||||
- parameter: true
|
||||
propertyDeclaration: true
|
||||
"@typescript-eslint/unified-signatures": error
|
||||
arrow-body-style: error
|
||||
complexity: "off"
|
||||
consistent-return: error
|
||||
constructor-super: error
|
||||
curly: error
|
||||
dot-notation: "off"
|
||||
eqeqeq:
|
||||
- error
|
||||
- smart
|
||||
guard-for-in: error
|
||||
id-denylist:
|
||||
- error
|
||||
- any
|
||||
- Number
|
||||
- number
|
||||
- String
|
||||
- string
|
||||
- Boolean
|
||||
- boolean
|
||||
- Undefined
|
||||
- undefined
|
||||
id-match: error
|
||||
import/no-default-export: error
|
||||
import/no-extraneous-dependencies: "off"
|
||||
import/no-internal-modules: "off"
|
||||
import/order: error
|
||||
indent: "off"
|
||||
jsdoc/check-alignment: error
|
||||
jsdoc/check-indentation: "off"
|
||||
max-classes-per-file: "off"
|
||||
max-len: "off"
|
||||
new-parens: error
|
||||
no-bitwise: error
|
||||
no-caller: error
|
||||
no-cond-assign: error
|
||||
no-console: error
|
||||
no-debugger: error
|
||||
no-duplicate-case: error
|
||||
no-duplicate-imports: error
|
||||
no-empty: error
|
||||
no-empty-function: "off"
|
||||
no-eval: error
|
||||
no-extra-bind: error
|
||||
no-fallthrough: "off"
|
||||
no-invalid-this: "off"
|
||||
no-multiple-empty-lines: error
|
||||
no-new-func: error
|
||||
no-new-wrappers: error
|
||||
no-null/no-null: error
|
||||
no-param-reassign: error
|
||||
no-redeclare: error
|
||||
no-return-await: error
|
||||
no-sequences: error
|
||||
no-shadow: "off"
|
||||
no-sparse-arrays: error
|
||||
no-throw-literal: error
|
||||
no-trailing-spaces: error
|
||||
no-undef-init: error
|
||||
no-underscore-dangle:
|
||||
- error
|
||||
- allow:
|
||||
- _attr
|
||||
no-unsafe-finally: error
|
||||
no-unused-expressions: "off"
|
||||
no-unused-labels: error
|
||||
no-use-before-define: "off"
|
||||
no-useless-constructor: error
|
||||
no-var: error
|
||||
object-shorthand: "off"
|
||||
one-var:
|
||||
- error
|
||||
- never
|
||||
prefer-arrow/prefer-arrow-functions: error
|
||||
prefer-const: error
|
||||
prefer-object-spread: error
|
||||
radix: error
|
||||
space-in-parens:
|
||||
- error
|
||||
- never
|
||||
spaced-comment:
|
||||
- error
|
||||
- always
|
||||
- markers:
|
||||
- /
|
||||
unicorn/filename-case: error
|
||||
unicorn/prefer-ternary: error
|
||||
use-isnan: error
|
||||
valid-typeof: "off"
|
||||
functional/immutable-data:
|
||||
- error
|
||||
- ignoreImmediateMutation: true
|
||||
ignoreAccessorPattern:
|
||||
- "**.root*"
|
||||
- "**.numberingReferences*"
|
||||
- "**.sections*"
|
||||
- "**.properties*"
|
||||
functional/prefer-property-signatures: error
|
||||
functional/no-mixed-types: error
|
||||
functional/prefer-readonly-type: error
|
||||
no-unused-vars:
|
||||
- error
|
||||
- argsIgnorePattern: ^[_]+$
|
||||
ignorePatterns:
|
||||
- vite.config.ts
|
||||
overrides:
|
||||
- files:
|
||||
- "*.spec.ts"
|
||||
rules:
|
||||
"@typescript-eslint/no-unused-expressions": "off"
|
||||
"@typescript-eslint/dot-notation": "off"
|
||||
prefer-destructuring: "off"
|
||||
"@typescript-eslint/explicit-function-return-type": "off"
|
2
.github/workflows/default.yml
vendored
2
.github/workflows/default.yml
vendored
@ -65,5 +65,5 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Dependencies
|
||||
run: npm ci --force
|
||||
- name: Prettier
|
||||
- name: CSpell
|
||||
run: npm run cspell
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -33,8 +33,7 @@ node_modules
|
||||
.node_repl_history
|
||||
|
||||
# build
|
||||
build
|
||||
build-tests
|
||||
dist
|
||||
|
||||
# Documentation
|
||||
docs/api/
|
||||
|
@ -14,6 +14,7 @@
|
||||
[![Known Vulnerabilities][snky-image]][snky-url]
|
||||
[![PRs Welcome][pr-image]][pr-url]
|
||||
[![codecov][codecov-image]][codecov-url]
|
||||
[![Docx.js Editor][docxjs-editor-image]][docxjs-editor-url]
|
||||
|
||||
<p align="center">
|
||||
<img src="https://i.imgur.com/QeL1HuU.png" alt="drawing"/>
|
||||
@ -64,6 +65,10 @@ More [here](https://github.com/dolanmiu/docx/tree/master/demo)
|
||||
|
||||
Please refer to the [documentation at https://docx.js.org/](https://docx.js.org/) for details on how to use this library, examples and much more!
|
||||
|
||||
# Playground
|
||||
|
||||
Experience `docx` in action through [Docx.js Editor][docxjs-editor-url], an interactive playground where you can code and preview the results in real-time.
|
||||
|
||||
# Examples
|
||||
|
||||
Check the [demo folder](https://github.com/dolanmiu/docx/tree/master/demo) for examples.
|
||||
@ -115,3 +120,5 @@ Made with 💖
|
||||
[patreon-url]: https://www.patreon.com/dolanmiu
|
||||
[browserstack-image]: https://user-images.githubusercontent.com/2917613/54233552-128e9d00-4505-11e9-88fb-025a4e04007c.png
|
||||
[browserstack-url]: https://www.browserstack.com
|
||||
[docxjs-editor-image]: https://img.shields.io/badge/Docx.js%20Editor-2b579a.svg?style=flat&logo=javascript&logoColor=white
|
||||
[docxjs-editor-url]: https://docxjs-editor.vercel.app/
|
||||
|
43
demo/94-texbox.ts
Normal file
43
demo/94-texbox.ts
Normal file
@ -0,0 +1,43 @@
|
||||
// Simple example to add textbox to a document
|
||||
import { Document, Packer, Paragraph, Textbox, TextRun } from "docx";
|
||||
import * as fs from "fs";
|
||||
|
||||
const doc = new Document({
|
||||
sections: [
|
||||
{
|
||||
properties: {},
|
||||
children: [
|
||||
new Textbox({
|
||||
alignment: "center",
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [new TextRun("Hi i'm a textbox!")],
|
||||
}),
|
||||
],
|
||||
style: {
|
||||
width: "200pt",
|
||||
height: "auto",
|
||||
},
|
||||
}),
|
||||
new Textbox({
|
||||
alignment: "center",
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [new TextRun("Hi i'm a textbox with a hidden box!")],
|
||||
}),
|
||||
],
|
||||
style: {
|
||||
width: "300pt",
|
||||
height: 400,
|
||||
visibility: "hidden",
|
||||
zIndex: "auto",
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Packer.toBuffer(doc).then((buffer) => {
|
||||
fs.writeFileSync("My Document.docx", buffer);
|
||||
});
|
60
demo/95-paragraph-style-with-shading-and-borders.ts
Normal file
60
demo/95-paragraph-style-with-shading-and-borders.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import * as fs from "fs";
|
||||
import { BorderStyle, Document, Packer, Paragraph, TextRun } from "docx";
|
||||
|
||||
const doc = new Document({
|
||||
styles: {
|
||||
paragraphStyles: [
|
||||
{
|
||||
id: "withSingleBlackBordersAndYellowShading",
|
||||
name: "Paragraph Style with Black Borders and Yellow Shading",
|
||||
basedOn: "Normal",
|
||||
paragraph: {
|
||||
shading: {
|
||||
color: "#fff000",
|
||||
type: "solid",
|
||||
},
|
||||
border: {
|
||||
top: {
|
||||
style: BorderStyle.SINGLE,
|
||||
color: "#000000",
|
||||
size: 4,
|
||||
},
|
||||
bottom: {
|
||||
style: BorderStyle.SINGLE,
|
||||
color: "#000000",
|
||||
size: 4,
|
||||
},
|
||||
left: {
|
||||
style: BorderStyle.SINGLE,
|
||||
color: "#000000",
|
||||
size: 4,
|
||||
},
|
||||
right: {
|
||||
style: BorderStyle.SINGLE,
|
||||
color: "#000000",
|
||||
size: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
children: [
|
||||
new Paragraph({
|
||||
style: "withSingleBlackBordersAndYellowShading",
|
||||
children: [
|
||||
new TextRun({
|
||||
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Packer.toBuffer(doc).then((buffer) => {
|
||||
fs.writeFileSync("My Document.docx", buffer);
|
||||
});
|
@ -22,19 +22,30 @@ const doc = new docx.Document({
|
||||
|
||||
### Full list of options:
|
||||
|
||||
- creator
|
||||
- description
|
||||
- title
|
||||
- subject
|
||||
- keywords
|
||||
- lastModifiedBy
|
||||
- revision
|
||||
- externalStyles
|
||||
- styles
|
||||
- numbering
|
||||
- footnotes
|
||||
- hyperlinks
|
||||
- background
|
||||
| Property | Type | Notes |
|
||||
| -------------------------- | -------------------------------------------------------- | -------- |
|
||||
| sections | `ISectionOptions[]` | Optional |
|
||||
| title | `string` | Optional |
|
||||
| subject | `string` | Optional |
|
||||
| creator | `string` | Optional |
|
||||
| keywords | `string` | Optional |
|
||||
| description | `string` | Optional |
|
||||
| lastModifiedBy | `string` | Optional |
|
||||
| revision | `number` | Optional |
|
||||
| externalStyles | `string` | Optional |
|
||||
| styles | `IStylesOptions` | Optional |
|
||||
| numbering | `INumberingOptions` | Optional |
|
||||
| comments | `ICommentsOptions` | Optional |
|
||||
| footnotes | `Record<string, { children: Paragraph[] }>` | Optional |
|
||||
| background | `IDocumentBackgroundOptions` | Optional |
|
||||
| features | `{ trackRevisions?: boolean; updateFields?: boolean; }` | Optional |
|
||||
| compatabilityModeVersion | `number` | Optional |
|
||||
| compatibility | `ICompatibilityOptions` | Optional |
|
||||
| customProperties | ` ICustomPropertyOptions`[] | Optional |
|
||||
| evenAndOddHeaderAndFooters | `boolean` | Optional |
|
||||
| defaultTabStop | `number` | Optional |
|
||||
| fonts | ` FontOptions[]` | Optional |
|
||||
| hyphenation | `IHyphenationOptions` | Optional |
|
||||
|
||||
### Change background color of Document
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
> Packers are the way in which `docx` turns your code into `.docx` format. It is completely decoupled from the `docx.Document`.
|
||||
|
||||
Packers works in both a node and browser environment (Angular etc). Now, the packer returns a `Buffer`, `Blob` or `base64 string`. It is up to you to take that and persist it with node's `fs`, send it down as a downloadable file, or anything else you wish. As of `version 4+`, this library will not have options to export to PDF.
|
||||
Packers works in both a node and browser environment (Angular etc). Now, the packer returns a `Buffer`, `Blob`, `string`, `base64 string`, `ArrayBuffer`, or `Stream`. It is up to you to take that and persist it with node's `fs`, send it down as a downloadable file, or anything else you wish. As of `version 4+`, this library will not have options to export to PDF.
|
||||
|
||||
### Export as Buffer
|
||||
|
||||
@ -14,6 +14,14 @@ Packer.toBuffer(doc).then((buffer) => {
|
||||
});
|
||||
```
|
||||
|
||||
### Export as string
|
||||
|
||||
```ts
|
||||
Packer.toString(doc).then((string) => {
|
||||
console.log(string);
|
||||
});
|
||||
```
|
||||
|
||||
### Export as a `base64` string
|
||||
|
||||
```ts
|
||||
@ -32,3 +40,46 @@ Packer.toBlob(doc).then((blob) => {
|
||||
saveAs(blob, "example.docx");
|
||||
});
|
||||
```
|
||||
|
||||
### Export as ArrayBuffer
|
||||
|
||||
This may be useful when working in a Node.js worker.
|
||||
|
||||
```ts
|
||||
Packer.toArrayBuffer(doc).then((arrayBuffer) => {
|
||||
port.postMessage(arrayBuffer, [arrayBuffer]);
|
||||
});
|
||||
```
|
||||
|
||||
### Export as a Stream
|
||||
|
||||
```ts
|
||||
Packer.toStream(doc).then((stream) => {
|
||||
// read from stream
|
||||
});
|
||||
```
|
||||
|
||||
### Export using optional arguments
|
||||
|
||||
The `Packer` methods support 2 optional arguments.
|
||||
|
||||
The first is for controlling the indentation of the xml and should be a `boolean` or `keyof typeof PrettifyType`.
|
||||
|
||||
The second is an array of subfile overrides (`{path: string, data: string}[]`). These overrides can be used to write additional subfiles to the result or even override default subfiles in the case that the default handling of these subfiles does not meet your needs.
|
||||
|
||||
```ts
|
||||
const overrides = [{ path: "word/commentsExtended.xml", data: "string_data" }];
|
||||
Packer.toString(doc, true, overrides).then((string) => {
|
||||
console.log(string);
|
||||
});
|
||||
```
|
||||
|
||||
### Export to arbitrary formats
|
||||
|
||||
You can also use the lower-level `Packer.pack` method to export to any specified type.
|
||||
|
||||
```ts
|
||||
Packer.pack(doc, 'string').then((string) => {
|
||||
console.log(string);
|
||||
});
|
||||
```
|
||||
|
26
docs/usage/text-box.md
Normal file
26
docs/usage/text-box.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Text Box
|
||||
|
||||
Similar `Text Frames`, but the difference being that it is `VML` `Shape` based.
|
||||
|
||||
!> `Text Boxes` requires an understanding of [Paragraphs](usage/paragraph.md).
|
||||
|
||||
> `Text boxes` are paragraphs of text in a document which are positioned in a separate region or frame in the document, and can be positioned with a specific size and position relative to non-frame paragraphs in the current document.
|
||||
|
||||
## Intro
|
||||
|
||||
To make a `Text Box`, simply create a `Textbox` object inside the `Document`:
|
||||
|
||||
```ts
|
||||
new Textbox({
|
||||
alignment: "center",
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [new TextRun("Hi i'm a textbox!")],
|
||||
}),
|
||||
],
|
||||
style: {
|
||||
width: "200pt",
|
||||
height: "auto",
|
||||
},
|
||||
});
|
||||
```
|
@ -1,6 +1,6 @@
|
||||
# Text Frames
|
||||
|
||||
Also known as `Text Boxes`
|
||||
> Similar to `Text Boxes`!
|
||||
|
||||
!> Text Frames requires an understanding of [Paragraphs](usage/paragraph.md).
|
||||
|
||||
|
@ -10,7 +10,7 @@ import tsEslint from "typescript-eslint";
|
||||
|
||||
const config: Linter.Config<Linter.RulesRecord>[] = [
|
||||
{
|
||||
ignores: ["**/vite.config.ts", "**/build/**", "**/coverage/**", "**/*.js", "eslint.config.ts", "**/demo/**", "**/scripts/**"],
|
||||
ignores: ["**/vite.config.ts", "**/dist/**", "**/coverage/**", "**/*.js", "eslint.config.ts", "**/demo/**", "**/scripts/**"],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
importPlugin.flatConfigs.recommended,
|
||||
|
8634
package-lock.json
generated
8634
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@ -1,21 +1,25 @@
|
||||
{
|
||||
"name": "docx",
|
||||
"version": "9.0.2",
|
||||
"version": "9.2.0",
|
||||
"description": "Easily generate .docx files with JS/TS with a nice declarative API. Works for Node and on the Browser.",
|
||||
"type": "module",
|
||||
"main": "build/index.umd.js",
|
||||
"module": "./build/index.mjs",
|
||||
"types": "./build/index.d.ts",
|
||||
"main": "dist/index.umd.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./build/index.cjs",
|
||||
"types": "./build/index.d.ts",
|
||||
"import": "./build/index.mjs",
|
||||
"default": "./build/index.mjs"
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"build"
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && vite build",
|
||||
@ -57,7 +61,7 @@
|
||||
"@types/node": "^22.7.5",
|
||||
"hash.js": "^1.1.7",
|
||||
"jszip": "^3.10.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"nanoid": "^5.1.3",
|
||||
"xml": "^1.0.1",
|
||||
"xml-js": "^1.6.8"
|
||||
},
|
||||
@ -68,7 +72,6 @@
|
||||
},
|
||||
"homepage": "https://docx.js.org",
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.1",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/inquirer": "^9.0.3",
|
||||
"@types/prompt": "^1.1.1",
|
||||
@ -76,8 +79,8 @@
|
||||
"@types/xml": "^1.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.8.1",
|
||||
"@typescript-eslint/parser": "^8.8.1",
|
||||
"@vitest/coverage-v8": "^1.1.0",
|
||||
"@vitest/ui": "^2.1.2",
|
||||
"@vitest/coverage-v8": "^3.0.8",
|
||||
"@vitest/ui": "^3.0.8",
|
||||
"cspell": "^8.2.3",
|
||||
"docsify-cli": "^4.3.0",
|
||||
"eslint": "^9.13.0",
|
||||
@ -92,20 +95,20 @@
|
||||
"glob": "^11.0.0",
|
||||
"inquirer": "^12.0.0",
|
||||
"jiti": "^2.3.3",
|
||||
"jsdom": "^25.0.1",
|
||||
"jsdom": "^26.0.0",
|
||||
"pre-commit": "^1.2.2",
|
||||
"prettier": "^3.1.1",
|
||||
"tsconfig-paths": "^4.0.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typedoc": "^0.26.9",
|
||||
"typedoc": "^0.27.3",
|
||||
"typescript": "5.3.3",
|
||||
"typescript-eslint": "^8.10.0",
|
||||
"unzipper": "^0.12.3",
|
||||
"vite": "^5.0.10",
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-dts": "^4.2.4",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^3.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
|
@ -112,6 +112,41 @@ describe("Compiler", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should pack subfile overrides",
|
||||
async () => {
|
||||
const file = new File({
|
||||
sections: [],
|
||||
comments: {
|
||||
children: [],
|
||||
},
|
||||
});
|
||||
const subfileData1 = "comments";
|
||||
const subfileData2 = "commentsExtended";
|
||||
const overrides = [
|
||||
{ path: "word/comments.xml", data: subfileData1 },
|
||||
{ path: "word/commentsExtended.xml", data: subfileData2 },
|
||||
];
|
||||
const zipFile = compiler.compile(file, "", overrides);
|
||||
const fileNames = Object.keys(zipFile.files).map((f) => zipFile.files[f].name);
|
||||
|
||||
expect(fileNames).is.an.instanceof(Array);
|
||||
expect(fileNames).has.length(20);
|
||||
|
||||
expect(fileNames).to.include("word/comments.xml");
|
||||
expect(fileNames).to.include("word/commentsExtended.xml");
|
||||
|
||||
const commentsText = await zipFile.file("word/comments.xml")?.async("text");
|
||||
const commentsExtendedText = await zipFile.file("word/commentsExtended.xml")?.async("text");
|
||||
|
||||
expect(commentsText).toBe(subfileData1);
|
||||
expect(commentsExtendedText).toBe(subfileData2);
|
||||
},
|
||||
{
|
||||
timeout: 99999999,
|
||||
},
|
||||
);
|
||||
|
||||
it("should call the format method X times equalling X files to be formatted", () => {
|
||||
// This test is required because before, there was a case where Document was formatted twice, which was inefficient
|
||||
// This also caused issues such as running prepForXml multiple times as format() was ran multiple times.
|
||||
|
@ -9,7 +9,7 @@ import { ImageReplacer } from "./image-replacer";
|
||||
import { NumberingReplacer } from "./numbering-replacer";
|
||||
import { PrettifyType } from "./packer";
|
||||
|
||||
type IXmlifyedFile = {
|
||||
export type IXmlifyedFile = {
|
||||
readonly data: string;
|
||||
readonly path: string;
|
||||
};
|
||||
@ -47,7 +47,11 @@ export class Compiler {
|
||||
this.numberingReplacer = new NumberingReplacer();
|
||||
}
|
||||
|
||||
public compile(file: File, prettifyXml?: (typeof PrettifyType)[keyof typeof PrettifyType]): JSZip {
|
||||
public compile(
|
||||
file: File,
|
||||
prettifyXml?: (typeof PrettifyType)[keyof typeof PrettifyType],
|
||||
overrides: readonly IXmlifyedFile[] = [],
|
||||
): JSZip {
|
||||
const zip = new JSZip();
|
||||
const xmlifiedFileMapping = this.xmlifyFile(file, prettifyXml);
|
||||
const map = new Map<string, IXmlifyedFile | readonly IXmlifyedFile[]>(Object.entries(xmlifiedFileMapping));
|
||||
@ -62,6 +66,10 @@ export class Compiler {
|
||||
}
|
||||
}
|
||||
|
||||
for (const subFile of overrides) {
|
||||
zip.file(subFile.path, subFile.data);
|
||||
}
|
||||
|
||||
for (const data of file.Media.Array) {
|
||||
if (data.type !== "svg") {
|
||||
zip.file(`word/media/${data.fileName}`, data.data);
|
||||
@ -109,6 +117,12 @@ export class Compiler {
|
||||
);
|
||||
});
|
||||
|
||||
file.Document.Relationships.createRelationship(
|
||||
file.Document.Relationships.RelationshipCount + 1,
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable",
|
||||
"fontTable.xml",
|
||||
);
|
||||
|
||||
return xml(
|
||||
this.formatter.format(file.Document.Relationships, {
|
||||
viewWrapper: file.Document,
|
||||
|
@ -46,7 +46,7 @@ describe("Packer", () => {
|
||||
|
||||
await Packer.toString(file, true);
|
||||
|
||||
expect(spy).toBeCalledWith(expect.anything(), PrettifyType.WITH_2_BLANKS);
|
||||
expect(spy).toBeCalledWith(expect.anything(), PrettifyType.WITH_2_BLANKS, expect.anything());
|
||||
});
|
||||
|
||||
it("should use a prettify value", async () => {
|
||||
@ -55,7 +55,7 @@ describe("Packer", () => {
|
||||
|
||||
await Packer.toString(file, PrettifyType.WITH_4_BLANKS);
|
||||
|
||||
expect(spy).toBeCalledWith(expect.anything(), PrettifyType.WITH_4_BLANKS);
|
||||
expect(spy).toBeCalledWith(expect.anything(), PrettifyType.WITH_4_BLANKS, expect.anything());
|
||||
});
|
||||
|
||||
it("should use an undefined prettify value", async () => {
|
||||
@ -64,7 +64,32 @@ describe("Packer", () => {
|
||||
|
||||
await Packer.toString(file, false);
|
||||
|
||||
expect(spy).toBeCalledWith(expect.anything(), undefined);
|
||||
expect(spy).toBeCalledWith(expect.anything(), undefined, expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
describe("overrides", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should use an overrides value", async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const spy = vi.spyOn((Packer as any).compiler, "compile");
|
||||
const overrides = [{ path: "word/comments.xml", data: "comments" }];
|
||||
|
||||
await Packer.toString(file, true, overrides);
|
||||
|
||||
expect(spy).toBeCalledWith(expect.anything(), expect.anything(), overrides);
|
||||
});
|
||||
|
||||
it("should use a default overrides value", async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const spy = vi.spyOn((Packer as any).compiler, "compile");
|
||||
|
||||
await Packer.toString(file);
|
||||
|
||||
expect(spy).toBeCalledWith(expect.anything(), undefined, []);
|
||||
});
|
||||
});
|
||||
|
||||
@ -162,6 +187,33 @@ describe("Packer", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#toArrayBuffer()", () => {
|
||||
it("should create a standard docx file", async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
vi.spyOn((Packer as any).compiler, "compile").mockReturnValue({
|
||||
generateAsync: () => vi.fn(),
|
||||
});
|
||||
const str = await Packer.toArrayBuffer(file);
|
||||
|
||||
assert.isDefined(str);
|
||||
});
|
||||
|
||||
it("should handle exception if it throws any", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
vi.spyOn((Packer as any).compiler, "compile").mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
|
||||
return Packer.toArrayBuffer(file).catch((error) => {
|
||||
assert.isDefined(error);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#toStream()", () => {
|
||||
it("should create a standard docx file", async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Stream } from "stream";
|
||||
|
||||
import { File } from "@file/file";
|
||||
import { OutputByType, OutputType } from "@util/output-type";
|
||||
|
||||
import { Compiler } from "./next-compiler";
|
||||
import { Compiler, IXmlifyedFile } from "./next-compiler";
|
||||
|
||||
/**
|
||||
* Use blanks to prettify
|
||||
@ -21,53 +22,68 @@ const convertPrettifyType = (
|
||||
prettify === true ? PrettifyType.WITH_2_BLANKS : prettify === false ? undefined : prettify;
|
||||
|
||||
export class Packer {
|
||||
public static async toString(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Promise<string> {
|
||||
const zip = this.compiler.compile(file, convertPrettifyType(prettify));
|
||||
const zipData = await zip.generateAsync({
|
||||
type: "string",
|
||||
// eslint-disable-next-line require-await
|
||||
public static async pack<T extends OutputType>(
|
||||
file: File,
|
||||
type: T,
|
||||
prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
|
||||
overrides: readonly IXmlifyedFile[] = [],
|
||||
): Promise<OutputByType[T]> {
|
||||
const zip = this.compiler.compile(file, convertPrettifyType(prettify), overrides);
|
||||
return zip.generateAsync({
|
||||
type,
|
||||
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
compression: "DEFLATE",
|
||||
});
|
||||
|
||||
return zipData;
|
||||
}
|
||||
|
||||
public static async toBuffer(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Promise<Buffer> {
|
||||
const zip = this.compiler.compile(file, convertPrettifyType(prettify));
|
||||
const zipData = await zip.generateAsync({
|
||||
type: "nodebuffer",
|
||||
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
compression: "DEFLATE",
|
||||
});
|
||||
|
||||
return zipData;
|
||||
public static toString(
|
||||
file: File,
|
||||
prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
|
||||
overrides: readonly IXmlifyedFile[] = [],
|
||||
): Promise<string> {
|
||||
return Packer.pack(file, "string", prettify, overrides);
|
||||
}
|
||||
|
||||
public static async toBase64String(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Promise<string> {
|
||||
const zip = this.compiler.compile(file, convertPrettifyType(prettify));
|
||||
const zipData = await zip.generateAsync({
|
||||
type: "base64",
|
||||
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
compression: "DEFLATE",
|
||||
});
|
||||
|
||||
return zipData;
|
||||
public static toBuffer(
|
||||
file: File,
|
||||
prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
|
||||
overrides: readonly IXmlifyedFile[] = [],
|
||||
): Promise<Buffer> {
|
||||
return Packer.pack(file, "nodebuffer", prettify, overrides);
|
||||
}
|
||||
|
||||
public static async toBlob(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Promise<Blob> {
|
||||
const zip = this.compiler.compile(file, convertPrettifyType(prettify));
|
||||
const zipData = await zip.generateAsync({
|
||||
type: "blob",
|
||||
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
compression: "DEFLATE",
|
||||
});
|
||||
|
||||
return zipData;
|
||||
public static toBase64String(
|
||||
file: File,
|
||||
prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
|
||||
overrides: readonly IXmlifyedFile[] = [],
|
||||
): Promise<string> {
|
||||
return Packer.pack(file, "base64", prettify, overrides);
|
||||
}
|
||||
|
||||
public static toStream(file: File, prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType]): Stream {
|
||||
public static toBlob(
|
||||
file: File,
|
||||
prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
|
||||
overrides: readonly IXmlifyedFile[] = [],
|
||||
): Promise<Blob> {
|
||||
return Packer.pack(file, "blob", prettify, overrides);
|
||||
}
|
||||
|
||||
public static toArrayBuffer(
|
||||
file: File,
|
||||
prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
|
||||
overrides: readonly IXmlifyedFile[] = [],
|
||||
): Promise<ArrayBuffer> {
|
||||
return Packer.pack(file, "arraybuffer", prettify, overrides);
|
||||
}
|
||||
|
||||
public static toStream(
|
||||
file: File,
|
||||
prettify?: boolean | (typeof PrettifyType)[keyof typeof PrettifyType],
|
||||
overrides: readonly IXmlifyedFile[] = [],
|
||||
): Stream {
|
||||
const stream = new Stream();
|
||||
const zip = this.compiler.compile(file, convertPrettifyType(prettify));
|
||||
const zip = this.compiler.compile(file, convertPrettifyType(prettify), overrides);
|
||||
|
||||
zip.generateAsync({
|
||||
type: "nodebuffer",
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { FontOptions } from "@file/fonts/font-table";
|
||||
import { ICommentsOptions } from "@file/paragraph/run/comment-run";
|
||||
import { IHyphenationOptions } from "@file/settings";
|
||||
import { ICompatibilityOptions } from "@file/settings/compatibility";
|
||||
import { StringContainer, XmlComponent } from "@file/xml-components";
|
||||
import { StringContainer, XmlAttributeComponent, XmlComponent } from "@file/xml-components";
|
||||
import { dateTimeValue } from "@util/values";
|
||||
|
||||
import { ICustomPropertyOptions } from "../custom-properties";
|
||||
@ -44,6 +45,7 @@ export type IPropertiesOptions = {
|
||||
readonly evenAndOddHeaderAndFooters?: boolean;
|
||||
readonly defaultTabStop?: number;
|
||||
readonly fonts?: readonly FontOptions[];
|
||||
readonly hyphenation?: IHyphenationOptions;
|
||||
};
|
||||
|
||||
// <xs:element name="coreProperties" type="CT_CoreProperties"/>
|
||||
@ -73,15 +75,7 @@ export type IPropertiesOptions = {
|
||||
export class CoreProperties extends XmlComponent {
|
||||
public constructor(options: Omit<IPropertiesOptions, "sections">) {
|
||||
super("cp:coreProperties");
|
||||
this.root.push(
|
||||
new DocumentAttributes({
|
||||
cp: "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
|
||||
dc: "http://purl.org/dc/elements/1.1/",
|
||||
dcterms: "http://purl.org/dc/terms/",
|
||||
dcmitype: "http://purl.org/dc/dcmitype/",
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
}),
|
||||
);
|
||||
this.root.push(new DocumentAttributes(["cp", "dc", "dcterms", "dcmitype", "xsi"]));
|
||||
if (options.title) {
|
||||
this.root.push(new StringContainer("dc:title", options.title));
|
||||
}
|
||||
@ -108,11 +102,15 @@ export class CoreProperties extends XmlComponent {
|
||||
}
|
||||
}
|
||||
|
||||
class TimestampElementProperties extends XmlAttributeComponent<{ readonly type: string }> {
|
||||
protected readonly xmlKeys = { type: "xsi:type" };
|
||||
}
|
||||
|
||||
class TimestampElement extends XmlComponent {
|
||||
public constructor(name: string) {
|
||||
super(name);
|
||||
this.root.push(
|
||||
new DocumentAttributes({
|
||||
new TimestampElementProperties({
|
||||
type: "dcterms:W3CDTF",
|
||||
}),
|
||||
);
|
||||
|
@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { Formatter } from "@export/formatter";
|
||||
|
||||
import { createLineNumberType } from "./line-number";
|
||||
|
||||
describe("createLineNumberType", () => {
|
||||
it("should work", () => {
|
||||
const textDirection = createLineNumberType({ countBy: 0, start: 0, restart: "newPage", distance: 10 });
|
||||
|
||||
const tree = new Formatter().format(textDirection);
|
||||
expect(tree).to.deep.equal({
|
||||
"w:lnNumType": { _attr: { "w:countBy": 0, "w:start": 0, "w:restart": "newPage", "w:distance": 10 } },
|
||||
});
|
||||
});
|
||||
|
||||
it("should work with string measures for distance", () => {
|
||||
const textDirection = createLineNumberType({ countBy: 0, start: 0, restart: "newPage", distance: "10mm" });
|
||||
|
||||
const tree = new Formatter().format(textDirection);
|
||||
expect(tree).to.deep.equal({
|
||||
"w:lnNumType": { _attr: { "w:countBy": 0, "w:start": 0, "w:restart": "newPage", "w:distance": "10mm" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("should work with blank entries", () => {
|
||||
const textDirection = createLineNumberType({});
|
||||
|
||||
const tree = new Formatter().format(textDirection);
|
||||
expect(tree).to.deep.equal({
|
||||
"w:lnNumType": { _attr: {} },
|
||||
});
|
||||
});
|
||||
});
|
@ -1,89 +1,60 @@
|
||||
import { XmlAttributeComponent } from "@file/xml-components";
|
||||
import { AttributeMap, XmlAttributeComponent } from "@file/xml-components";
|
||||
|
||||
/* cSpell:disable */
|
||||
export type IDocumentAttributesProperties = {
|
||||
readonly wpc?: string;
|
||||
readonly mc?: string;
|
||||
readonly o?: string;
|
||||
readonly r?: string;
|
||||
readonly m?: string;
|
||||
readonly v?: string;
|
||||
readonly wp14?: string;
|
||||
readonly wp?: string;
|
||||
readonly w10?: string;
|
||||
readonly w?: string;
|
||||
readonly w14?: string;
|
||||
readonly w15?: string;
|
||||
readonly wpg?: string;
|
||||
readonly wpi?: string;
|
||||
readonly wne?: string;
|
||||
readonly wps?: string;
|
||||
readonly Ignorable?: string;
|
||||
readonly cp?: string;
|
||||
readonly dc?: string;
|
||||
readonly dcterms?: string;
|
||||
readonly dcmitype?: string;
|
||||
readonly xsi?: string;
|
||||
readonly type?: string;
|
||||
readonly cx?: string;
|
||||
readonly cx1?: string;
|
||||
readonly cx2?: string;
|
||||
readonly cx3?: string;
|
||||
readonly cx4?: string;
|
||||
readonly cx5?: string;
|
||||
readonly cx6?: string;
|
||||
readonly cx7?: string;
|
||||
readonly cx8?: string;
|
||||
readonly aink?: string;
|
||||
readonly am3d?: string;
|
||||
readonly w16cex?: string;
|
||||
readonly w16cid?: string;
|
||||
readonly w16?: string;
|
||||
readonly w16sdtdh?: string;
|
||||
readonly w16se?: string;
|
||||
export const DocumentAttributeNamespaces = {
|
||||
wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas",
|
||||
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
||||
o: "urn:schemas-microsoft-com:office:office",
|
||||
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
||||
m: "http://schemas.openxmlformats.org/officeDocument/2006/math",
|
||||
v: "urn:schemas-microsoft-com:vml",
|
||||
wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
|
||||
wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
|
||||
w10: "urn:schemas-microsoft-com:office:word",
|
||||
w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
||||
w14: "http://schemas.microsoft.com/office/word/2010/wordml",
|
||||
w15: "http://schemas.microsoft.com/office/word/2012/wordml",
|
||||
wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
|
||||
wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
|
||||
wne: "http://schemas.microsoft.com/office/word/2006/wordml",
|
||||
wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
|
||||
cp: "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
|
||||
dc: "http://purl.org/dc/elements/1.1/",
|
||||
dcterms: "http://purl.org/dc/terms/",
|
||||
dcmitype: "http://purl.org/dc/dcmitype/",
|
||||
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
cx: "http://schemas.microsoft.com/office/drawing/2014/chartex",
|
||||
cx1: "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex",
|
||||
cx2: "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex",
|
||||
cx3: "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex",
|
||||
cx4: "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex",
|
||||
cx5: "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex",
|
||||
cx6: "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex",
|
||||
cx7: "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex",
|
||||
cx8: "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex",
|
||||
aink: "http://schemas.microsoft.com/office/drawing/2016/ink",
|
||||
am3d: "http://schemas.microsoft.com/office/drawing/2017/model3d",
|
||||
w16cex: "http://schemas.microsoft.com/office/word/2018/wordml/cex",
|
||||
w16cid: "http://schemas.microsoft.com/office/word/2016/wordml/cid",
|
||||
w16: "http://schemas.microsoft.com/office/word/2018/wordml",
|
||||
w16sdtdh: "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash",
|
||||
w16se: "http://schemas.microsoft.com/office/word/2015/wordml/symex",
|
||||
};
|
||||
/* cSpell:enable */
|
||||
|
||||
export type DocumentAttributeNamespace = keyof typeof DocumentAttributeNamespaces;
|
||||
|
||||
export type IDocumentAttributesProperties = Partial<Record<DocumentAttributeNamespace, string>> & {
|
||||
readonly Ignorable?: string;
|
||||
};
|
||||
|
||||
export class DocumentAttributes extends XmlAttributeComponent<IDocumentAttributesProperties> {
|
||||
protected readonly xmlKeys = {
|
||||
wpc: "xmlns:wpc",
|
||||
mc: "xmlns:mc",
|
||||
o: "xmlns:o",
|
||||
r: "xmlns:r",
|
||||
m: "xmlns:m",
|
||||
v: "xmlns:v",
|
||||
wp14: "xmlns:wp14",
|
||||
wp: "xmlns:wp",
|
||||
w10: "xmlns:w10",
|
||||
w: "xmlns:w",
|
||||
w14: "xmlns:w14",
|
||||
w15: "xmlns:w15",
|
||||
wpg: "xmlns:wpg",
|
||||
wpi: "xmlns:wpi",
|
||||
wne: "xmlns:wne",
|
||||
wps: "xmlns:wps",
|
||||
Ignorable: "mc:Ignorable",
|
||||
cp: "xmlns:cp",
|
||||
dc: "xmlns:dc",
|
||||
dcterms: "xmlns:dcterms",
|
||||
dcmitype: "xmlns:dcmitype",
|
||||
xsi: "xmlns:xsi",
|
||||
type: "xsi:type",
|
||||
cx: "xmlns:cx",
|
||||
cx1: "xmlns:cx1",
|
||||
cx2: "xmlns:cx2",
|
||||
cx3: "xmlns:cx3",
|
||||
cx4: "xmlns:cx4",
|
||||
cx5: "xmlns:cx5",
|
||||
cx6: "xmlns:cx6",
|
||||
cx7: "xmlns:cx7",
|
||||
cx8: "xmlns:cx8",
|
||||
aink: "xmlns:aink",
|
||||
am3d: "xmlns:am3d",
|
||||
w16cex: "xmlns:w16cex",
|
||||
w16cid: "xmlns:w16cid",
|
||||
w16: "xmlns:w16",
|
||||
w16sdtdh: "xmlns:w16sdtdh",
|
||||
w16se: "xmlns:w16se",
|
||||
};
|
||||
...Object.fromEntries(Object.keys(DocumentAttributeNamespaces).map((key) => [key, `xmlns:${key}`])),
|
||||
} as AttributeMap<IDocumentAttributesProperties>;
|
||||
|
||||
public constructor(ns: readonly DocumentAttributeNamespace[], Ignorable?: string) {
|
||||
super({ Ignorable, ...Object.fromEntries(ns.map((n) => [n, DocumentAttributeNamespaces[n]])) });
|
||||
}
|
||||
}
|
||||
|
@ -37,41 +37,43 @@ export class Document extends XmlComponent {
|
||||
public constructor(options: IDocumentOptions) {
|
||||
super("w:document");
|
||||
this.root.push(
|
||||
new DocumentAttributes({
|
||||
wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas",
|
||||
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
||||
o: "urn:schemas-microsoft-com:office:office",
|
||||
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
||||
m: "http://schemas.openxmlformats.org/officeDocument/2006/math",
|
||||
v: "urn:schemas-microsoft-com:vml",
|
||||
wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
|
||||
wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
|
||||
w10: "urn:schemas-microsoft-com:office:word",
|
||||
w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
||||
w14: "http://schemas.microsoft.com/office/word/2010/wordml",
|
||||
w15: "http://schemas.microsoft.com/office/word/2012/wordml",
|
||||
wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
|
||||
wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
|
||||
wne: "http://schemas.microsoft.com/office/word/2006/wordml",
|
||||
wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
|
||||
cx: "http://schemas.microsoft.com/office/drawing/2014/chartex",
|
||||
cx1: "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex",
|
||||
cx2: "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex",
|
||||
cx3: "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex",
|
||||
cx4: "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex",
|
||||
cx5: "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex",
|
||||
cx6: "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex",
|
||||
cx7: "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex",
|
||||
cx8: "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex",
|
||||
aink: "http://schemas.microsoft.com/office/drawing/2016/ink",
|
||||
am3d: "http://schemas.microsoft.com/office/drawing/2017/model3d",
|
||||
w16cex: "http://schemas.microsoft.com/office/word/2018/wordml/cex",
|
||||
w16cid: "http://schemas.microsoft.com/office/word/2016/wordml/cid",
|
||||
w16: "http://schemas.microsoft.com/office/word/2018/wordml",
|
||||
w16sdtdh: "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash",
|
||||
w16se: "http://schemas.microsoft.com/office/word/2015/wordml/symex",
|
||||
Ignorable: "w14 w15 wp14",
|
||||
}),
|
||||
new DocumentAttributes(
|
||||
[
|
||||
"wpc",
|
||||
"mc",
|
||||
"o",
|
||||
"r",
|
||||
"m",
|
||||
"v",
|
||||
"wp14",
|
||||
"wp",
|
||||
"w10",
|
||||
"w",
|
||||
"w14",
|
||||
"w15",
|
||||
"wpg",
|
||||
"wpi",
|
||||
"wne",
|
||||
"wps",
|
||||
"cx",
|
||||
"cx1",
|
||||
"cx2",
|
||||
"cx3",
|
||||
"cx4",
|
||||
"cx5",
|
||||
"cx6",
|
||||
"cx7",
|
||||
"cx8",
|
||||
"aink",
|
||||
"am3d",
|
||||
"w16cex",
|
||||
"w16cid",
|
||||
"w16",
|
||||
"w16sdtdh",
|
||||
"w16se",
|
||||
],
|
||||
"w14 w15 wp14",
|
||||
),
|
||||
);
|
||||
this.body = new Body();
|
||||
if (options.background) {
|
||||
|
@ -479,4 +479,41 @@ describe("File", () => {
|
||||
expect(doc.Styles).to.not.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe("#features", () => {
|
||||
it("should work with updateFields", () => {
|
||||
const doc = new File({
|
||||
sections: [],
|
||||
features: {
|
||||
updateFields: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(doc.Styles).to.not.be.undefined;
|
||||
});
|
||||
|
||||
it("should work with trackRevisions", () => {
|
||||
const doc = new File({
|
||||
sections: [],
|
||||
features: {
|
||||
trackRevisions: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(doc.Styles).to.not.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe("#hyphenation", () => {
|
||||
it("should work with autoHyphenation", () => {
|
||||
const doc = new File({
|
||||
sections: [],
|
||||
hyphenation: {
|
||||
autoHyphenation: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(doc.Styles).to.not.be.undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -80,6 +80,12 @@ export class File {
|
||||
trackRevisions: options.features?.trackRevisions,
|
||||
updateFields: options.features?.updateFields,
|
||||
defaultTabStop: options.defaultTabStop,
|
||||
hyphenation: {
|
||||
autoHyphenation: options.hyphenation?.autoHyphenation,
|
||||
hyphenationZone: options.hyphenation?.hyphenationZone,
|
||||
consecutiveHyphenLimit: options.hyphenation?.consecutiveHyphenLimit,
|
||||
doNotHyphenateCaps: options.hyphenation?.doNotHyphenateCaps,
|
||||
},
|
||||
});
|
||||
|
||||
this.media = new Media();
|
||||
|
@ -2,7 +2,7 @@ const obfuscatedStartOffset = 0;
|
||||
const obfuscatedEndOffset = 32;
|
||||
const guidSize = 32;
|
||||
|
||||
export const obfuscate = (buf: Buffer, fontKey: string): Buffer => {
|
||||
export const obfuscate = (buf: Uint8Array, fontKey: string): Uint8Array => {
|
||||
const guid = fontKey.replace(/-/g, "");
|
||||
if (guid.length !== guidSize) {
|
||||
throw new Error(`Error: Cannot extract GUID from font filename: ${fontKey}`);
|
||||
@ -17,6 +17,9 @@ export const obfuscate = (buf: Buffer, fontKey: string): Buffer => {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const obfuscatedBytes = bytesToObfuscate.map((byte, i) => byte ^ hexNumbers[i % hexNumbers.length]);
|
||||
|
||||
const out = Buffer.concat([buf.slice(0, obfuscatedStartOffset), obfuscatedBytes, buf.slice(obfuscatedEndOffset)]);
|
||||
const out = new Uint8Array(obfuscatedStartOffset + obfuscatedBytes.length + Math.max(0, buf.length - obfuscatedEndOffset));
|
||||
out.set(buf.slice(0, obfuscatedStartOffset));
|
||||
out.set(obfuscatedBytes, obfuscatedStartOffset);
|
||||
out.set(buf.slice(obfuscatedEndOffset), obfuscatedStartOffset + obfuscatedBytes.length);
|
||||
return out;
|
||||
};
|
||||
|
@ -20,3 +20,5 @@ export * from "./border";
|
||||
export * from "./vertical-align";
|
||||
export * from "./checkbox";
|
||||
export * from "./fonts";
|
||||
export * from "./textbox";
|
||||
export { type IPropertiesOptions } from "./core-properties";
|
||||
|
@ -37,25 +37,10 @@ export class Numbering extends XmlComponent {
|
||||
public constructor(options: INumberingOptions) {
|
||||
super("w:numbering");
|
||||
this.root.push(
|
||||
new DocumentAttributes({
|
||||
wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas",
|
||||
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
||||
o: "urn:schemas-microsoft-com:office:office",
|
||||
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
||||
m: "http://schemas.openxmlformats.org/officeDocument/2006/math",
|
||||
v: "urn:schemas-microsoft-com:vml",
|
||||
wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
|
||||
wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
|
||||
w10: "urn:schemas-microsoft-com:office:word",
|
||||
w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
||||
w14: "http://schemas.microsoft.com/office/word/2010/wordml",
|
||||
w15: "http://schemas.microsoft.com/office/word/2012/wordml",
|
||||
wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
|
||||
wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
|
||||
wne: "http://schemas.microsoft.com/office/word/2006/wordml",
|
||||
wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
|
||||
Ignorable: "w14 w15 wp14",
|
||||
}),
|
||||
new DocumentAttributes(
|
||||
["wpc", "mc", "o", "r", "m", "v", "wp14", "wp", "w10", "w", "w14", "w15", "wpg", "wpi", "wne", "wps"],
|
||||
"w14 w15 wp14",
|
||||
),
|
||||
);
|
||||
|
||||
const abstractNumbering = new AbstractNumbering(this.abstractNumUniqueNumericId(), [
|
||||
|
@ -24,7 +24,7 @@ class SpacingAttributes extends XmlAttributeComponent<ISpacingProperties> {
|
||||
line: "w:line",
|
||||
lineRule: "w:lineRule",
|
||||
beforeAutoSpacing: "w:beforeAutospacing",
|
||||
afterAutoSpacing: "w:afterAutoSpacing",
|
||||
afterAutoSpacing: "w:afterAutospacing",
|
||||
};
|
||||
}
|
||||
|
||||
|
2
src/file/paragraph/math/bar/index.ts
Normal file
2
src/file/paragraph/math/bar/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./math-bar";
|
||||
export * from "./math-bar-properties";
|
11
src/file/paragraph/math/bar/math-bar-pos.ts
Normal file
11
src/file/paragraph/math/bar/math-bar-pos.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// https://www.datypic.com/sc/ooxml/e-m_pos-1.html
|
||||
import { Attributes, XmlComponent } from "@file/xml-components";
|
||||
|
||||
export class MathBarPos extends XmlComponent {
|
||||
// TODO: Use correct types rather than any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public constructor(attributes: any) {
|
||||
super("m:pos");
|
||||
this.root.push(new Attributes(attributes));
|
||||
}
|
||||
}
|
43
src/file/paragraph/math/bar/math-bar-properties.spec.ts
Normal file
43
src/file/paragraph/math/bar/math-bar-properties.spec.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { Formatter } from "@export/formatter";
|
||||
|
||||
import { MathBarProperties } from "./math-bar-properties";
|
||||
describe("MathBarProperties", () => {
|
||||
describe("#constructor()", () => {
|
||||
it("should create a MathBarProperties with top key", () => {
|
||||
const mathBarProperties = new MathBarProperties("top");
|
||||
|
||||
const tree = new Formatter().format(mathBarProperties);
|
||||
|
||||
expect(tree).to.deep.equal({
|
||||
"m:barPr": [
|
||||
{
|
||||
"m:pos": {
|
||||
_attr: {
|
||||
"w:val": "top",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
it("should create a MathBarProperties with bottom key", () => {
|
||||
const mathBarProperties = new MathBarProperties("bot");
|
||||
|
||||
const tree = new Formatter().format(mathBarProperties);
|
||||
|
||||
expect(tree).to.deep.equal({
|
||||
"m:barPr": [
|
||||
{
|
||||
"m:pos": {
|
||||
_attr: {
|
||||
"w:val": "bot",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
11
src/file/paragraph/math/bar/math-bar-properties.ts
Normal file
11
src/file/paragraph/math/bar/math-bar-properties.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// https://www.datypic.com/sc/ooxml/e-m_barPr-1.html
|
||||
import { XmlComponent } from "@file/xml-components";
|
||||
|
||||
import { MathBarPos } from "./math-bar-pos";
|
||||
|
||||
export class MathBarProperties extends XmlComponent {
|
||||
public constructor(type: string) {
|
||||
super("m:barPr");
|
||||
this.root.push(new MathBarPos({ val: type }));
|
||||
}
|
||||
}
|
38
src/file/paragraph/math/bar/math-bar.spec.ts
Normal file
38
src/file/paragraph/math/bar/math-bar.spec.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { Formatter } from "@export/formatter";
|
||||
|
||||
import { MathBar } from "./math-bar";
|
||||
import { MathRun } from "../math-run";
|
||||
|
||||
describe("MathBar", () => {
|
||||
describe("#constructor()", () => {
|
||||
it("should create a MathBar with correct root key", () => {
|
||||
const mathBar = new MathBar({ type: "top", children: [new MathRun("text")] });
|
||||
const tree = new Formatter().format(mathBar);
|
||||
|
||||
expect(tree).to.deep.equal({
|
||||
"m:bar": [
|
||||
{
|
||||
"m:barPr": [
|
||||
{
|
||||
"m:pos": {
|
||||
_attr: {
|
||||
"w:val": "top",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"m:e": [
|
||||
{
|
||||
"m:r": [{ "m:t": ["text"] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
18
src/file/paragraph/math/bar/math-bar.ts
Normal file
18
src/file/paragraph/math/bar/math-bar.ts
Normal file
@ -0,0 +1,18 @@
|
||||
// https://www.datypic.com/sc/ooxml/e-m_bar-1.html
|
||||
import { XmlComponent } from "@file/xml-components";
|
||||
|
||||
import { MathBarProperties } from "./math-bar-properties";
|
||||
import type { MathComponent } from "../math-component";
|
||||
import { MathBase } from "../n-ary";
|
||||
|
||||
type MathBarOption = {
|
||||
readonly type: "top" | "bot";
|
||||
readonly children: readonly MathComponent[];
|
||||
};
|
||||
export class MathBar extends XmlComponent {
|
||||
public constructor(options: MathBarOption) {
|
||||
super("m:bar");
|
||||
this.root.push(new MathBarProperties(options.type));
|
||||
this.root.push(new MathBase(options.children));
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
import { XmlComponent } from "@file/xml-components";
|
||||
|
||||
import { MathPreSubSuperScriptProperties } from "./math-pre-sub-super-script-function-properties";
|
||||
import { MathComponent } from "../../math-component";
|
||||
import type { MathComponent } from "../../math-component";
|
||||
import { MathBase, MathSubScriptElement, MathSuperScriptElement } from "../../n-ary";
|
||||
|
||||
export type IMathPreSubSuperScriptOptions = {
|
||||
|
@ -38,6 +38,8 @@ export type ILevelParagraphStylePropertiesOptions = {
|
||||
};
|
||||
|
||||
export type IParagraphStylePropertiesOptions = {
|
||||
readonly border?: IBordersOptions;
|
||||
readonly shading?: IShadingAttributesProperties;
|
||||
readonly numbering?:
|
||||
| {
|
||||
readonly reference: string;
|
||||
@ -49,7 +51,6 @@ export type IParagraphStylePropertiesOptions = {
|
||||
} & ILevelParagraphStylePropertiesOptions;
|
||||
|
||||
export type IParagraphPropertiesOptions = {
|
||||
readonly border?: IBordersOptions;
|
||||
readonly heading?: (typeof HeadingLevel)[keyof typeof HeadingLevel];
|
||||
readonly bidirectional?: boolean;
|
||||
readonly pageBreakBefore?: boolean;
|
||||
@ -58,7 +59,6 @@ export type IParagraphPropertiesOptions = {
|
||||
readonly bullet?: {
|
||||
readonly level: number;
|
||||
};
|
||||
readonly shading?: IShadingAttributesProperties;
|
||||
readonly widowControl?: boolean;
|
||||
readonly frame?: IFrameOptions;
|
||||
readonly suppressLineNumbers?: boolean;
|
||||
|
@ -155,7 +155,7 @@ export class Run extends XmlComponent {
|
||||
|
||||
this.root.push(child);
|
||||
}
|
||||
} else if (options.text) {
|
||||
} else if (options.text !== undefined) {
|
||||
this.root.push(new Text(options.text));
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,14 @@ describe("TextRun", () => {
|
||||
"w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "test"] }],
|
||||
});
|
||||
});
|
||||
|
||||
it("should add empty text into run", () => {
|
||||
run = new TextRun({ text: "" });
|
||||
const f = new Formatter().format(run);
|
||||
expect(f).to.deep.equal({
|
||||
"w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, ""] }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#referenceFootnote()", () => {
|
||||
|
@ -1,14 +1,7 @@
|
||||
import { IRunOptions, Run } from "./run";
|
||||
import { Text } from "./run-components/text";
|
||||
|
||||
export class TextRun extends Run {
|
||||
public constructor(options: IRunOptions | string) {
|
||||
if (typeof options === "string") {
|
||||
super({});
|
||||
this.root.push(new Text(options));
|
||||
return this;
|
||||
}
|
||||
|
||||
super(options);
|
||||
super(typeof options === "string" ? { text: options } : options);
|
||||
}
|
||||
}
|
||||
|
@ -129,6 +129,74 @@ describe("Settings", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should add autoHyphenation setting", () => {
|
||||
const options = {
|
||||
hyphenation: {
|
||||
autoHyphenation: true,
|
||||
},
|
||||
};
|
||||
|
||||
const tree = new Formatter().format(new Settings(options));
|
||||
expect(Object.keys(tree)).has.length(1);
|
||||
expect(tree["w:settings"]).to.be.an("array");
|
||||
expect(tree["w:settings"]).to.deep.include({
|
||||
"w:autoHyphenation": {},
|
||||
});
|
||||
});
|
||||
|
||||
it("should add doNotHyphenateCaps setting", () => {
|
||||
const options = {
|
||||
hyphenation: {
|
||||
doNotHyphenateCaps: true,
|
||||
},
|
||||
};
|
||||
|
||||
const tree = new Formatter().format(new Settings(options));
|
||||
expect(Object.keys(tree)).has.length(1);
|
||||
expect(tree["w:settings"]).to.be.an("array");
|
||||
expect(tree["w:settings"]).to.deep.include({
|
||||
"w:doNotHyphenateCaps": {},
|
||||
});
|
||||
});
|
||||
|
||||
it("should add hyphenationZone setting", () => {
|
||||
const options = {
|
||||
hyphenation: {
|
||||
hyphenationZone: 200,
|
||||
},
|
||||
};
|
||||
|
||||
const tree = new Formatter().format(new Settings(options));
|
||||
expect(Object.keys(tree)).has.length(1);
|
||||
expect(tree["w:settings"]).to.be.an("array");
|
||||
expect(tree["w:settings"]).to.deep.include({
|
||||
"w:hyphenationZone": {
|
||||
_attr: {
|
||||
"w:val": 200,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should add consecutiveHyphenLimit setting", () => {
|
||||
const options = {
|
||||
hyphenation: {
|
||||
consecutiveHyphenLimit: 3,
|
||||
},
|
||||
};
|
||||
|
||||
const tree = new Formatter().format(new Settings(options));
|
||||
expect(Object.keys(tree)).has.length(1);
|
||||
expect(tree["w:settings"]).to.be.an("array");
|
||||
expect(tree["w:settings"]).to.deep.include({
|
||||
"w:consecutiveHyphenLimit": {
|
||||
_attr: {
|
||||
"w:val": 3,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Remove when deprecating compatibilityModeVersion
|
||||
it("should add compatibility setting with legacy version", () => {
|
||||
const settings = new Settings({
|
||||
|
@ -153,6 +153,18 @@ export type ISettingsOptions = {
|
||||
readonly updateFields?: boolean;
|
||||
readonly compatibility?: ICompatibilityOptions;
|
||||
readonly defaultTabStop?: number;
|
||||
readonly hyphenation?: IHyphenationOptions;
|
||||
};
|
||||
|
||||
export type IHyphenationOptions = {
|
||||
/** Specifies whether the application automatically hyphenates words as they are typed in the document. */
|
||||
readonly autoHyphenation?: boolean;
|
||||
/** Specifies the minimum number of characters at the beginning of a word before a hyphen can be inserted. */
|
||||
readonly hyphenationZone?: number;
|
||||
/** Specifies the maximum number of consecutive lines that can end with a hyphenated word. */
|
||||
readonly consecutiveHyphenLimit?: number;
|
||||
/** Specifies whether to hyphenate words in all capital letters. */
|
||||
readonly doNotHyphenateCaps?: boolean;
|
||||
};
|
||||
|
||||
export class Settings extends XmlComponent {
|
||||
@ -204,6 +216,26 @@ export class Settings extends XmlComponent {
|
||||
this.root.push(new NumberValueElement("w:defaultTabStop", options.defaultTabStop));
|
||||
}
|
||||
|
||||
// https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_autoHyphenation_topic_ID0EFUMX.html
|
||||
if (options.hyphenation?.autoHyphenation !== undefined) {
|
||||
this.root.push(new OnOffElement("w:autoHyphenation", options.hyphenation.autoHyphenation));
|
||||
}
|
||||
|
||||
// https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_hyphenationZone_topic_ID0ERI3X.html
|
||||
if (options.hyphenation?.hyphenationZone !== undefined) {
|
||||
this.root.push(new NumberValueElement("w:hyphenationZone", options.hyphenation.hyphenationZone));
|
||||
}
|
||||
|
||||
// https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_consecutiveHyphenLim_topic_ID0EQ6RX.html
|
||||
if (options.hyphenation?.consecutiveHyphenLimit !== undefined) {
|
||||
this.root.push(new NumberValueElement("w:consecutiveHyphenLimit", options.hyphenation.consecutiveHyphenLimit));
|
||||
}
|
||||
|
||||
// https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_doNotHyphenateCaps_topic_ID0EW4XX.html
|
||||
if (options.hyphenation?.doNotHyphenateCaps !== undefined) {
|
||||
this.root.push(new OnOffElement("w:doNotHyphenateCaps", options.hyphenation.doNotHyphenateCaps));
|
||||
}
|
||||
|
||||
this.root.push(
|
||||
new Compatibility({
|
||||
...(options.compatibility ?? {}),
|
||||
|
@ -144,6 +144,10 @@ describe("External styles factory", () => {
|
||||
expect(() => new ExternalStylesFactory().newInstance(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><foo/>`)).to.throw(
|
||||
"can not find styles element",
|
||||
);
|
||||
|
||||
expect(() => new ExternalStylesFactory().newInstance(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`)).to.throw(
|
||||
"can not find styles element",
|
||||
);
|
||||
});
|
||||
|
||||
it("should parse styles elements", () => {
|
||||
|
@ -38,14 +38,7 @@ export type IDefaultStylesOptions = {
|
||||
|
||||
export class DefaultStylesFactory {
|
||||
public newInstance(options: IDefaultStylesOptions = {}): IStylesOptions {
|
||||
const documentAttributes = new DocumentAttributes({
|
||||
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
||||
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
||||
w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
||||
w14: "http://schemas.microsoft.com/office/word/2010/wordml",
|
||||
w15: "http://schemas.microsoft.com/office/word/2012/wordml",
|
||||
Ignorable: "w14 w15",
|
||||
});
|
||||
const documentAttributes = new DocumentAttributes(["mc", "r", "w", "w14", "w15"], "w14 w15");
|
||||
return {
|
||||
initialStyles: documentAttributes,
|
||||
importedStyles: [
|
||||
|
1
src/file/textbox/index.ts
Normal file
1
src/file/textbox/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./textbox";
|
11
src/file/textbox/pict-element/pict-element.ts
Normal file
11
src/file/textbox/pict-element/pict-element.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { BuilderElement, XmlComponent } from "@file/xml-components";
|
||||
|
||||
export type IPictElement = {
|
||||
readonly shape: XmlComponent;
|
||||
};
|
||||
|
||||
export const createPictElement = ({ shape }: IPictElement): XmlComponent =>
|
||||
new BuilderElement<{ readonly style?: string }>({
|
||||
name: "w:pict",
|
||||
children: [shape],
|
||||
});
|
58
src/file/textbox/shape/shape.spec.ts
Normal file
58
src/file/textbox/shape/shape.spec.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { Formatter } from "@export/formatter";
|
||||
import { Paragraph } from "@file/paragraph";
|
||||
|
||||
import { createShape } from "./shape";
|
||||
|
||||
describe("createShape", () => {
|
||||
it("should work", () => {
|
||||
const tree = new Formatter().format(
|
||||
createShape({
|
||||
id: "test-id",
|
||||
style: {
|
||||
width: "10pt",
|
||||
},
|
||||
children: [new Paragraph("test-content")],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(tree).toStrictEqual({
|
||||
"v:shape": [
|
||||
{ _attr: { id: "test-id", type: "#_x0000_t202", style: "width:10pt" } },
|
||||
{
|
||||
"v:textbox": [
|
||||
{ _attr: { insetmode: "auto", style: "mso-fit-shape-to-text:t;" } },
|
||||
{
|
||||
"w:txbxContent": [
|
||||
{ "w:p": [{ "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "test-content"] }] }] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should create default styles", () => {
|
||||
const tree = new Formatter().format(
|
||||
createShape({
|
||||
id: "test-id",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(tree).toStrictEqual({
|
||||
"v:shape": [
|
||||
{ _attr: { id: "test-id", type: "#_x0000_t202" } },
|
||||
{
|
||||
"v:textbox": [
|
||||
{ _attr: { insetmode: "auto", style: "mso-fit-shape-to-text:t;" } },
|
||||
{
|
||||
"w:txbxContent": {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
125
src/file/textbox/shape/shape.ts
Normal file
125
src/file/textbox/shape/shape.ts
Normal file
@ -0,0 +1,125 @@
|
||||
// https://c-rex.net/samples/ooxml/e1/Part3/OOXML_P3_Primer_OfficeArt_topic_ID0ELU5O.html
|
||||
// http://webapp.docx4java.org/OnlineDemo/ecma376/VML/shape.html
|
||||
import { ParagraphChild } from "@file/paragraph";
|
||||
import { BuilderElement, XmlComponent } from "@file/xml-components";
|
||||
|
||||
import { LengthUnit } from "../types";
|
||||
import { createVmlTextbox } from "../vml-textbox/vml-texbox";
|
||||
|
||||
const SHAPE_TYPE = "#_x0000_t202";
|
||||
|
||||
const styleToKeyMap: Record<keyof VmlShapeStyle, string> = {
|
||||
flip: "flip",
|
||||
height: "height",
|
||||
left: "left",
|
||||
marginBottom: "margin-bottom",
|
||||
marginLeft: "margin-left",
|
||||
marginRight: "margin-right",
|
||||
marginTop: "margin-top",
|
||||
positionHorizontal: "mso-position-horizontal",
|
||||
positionHorizontalRelative: "mso-position-horizontal-relative",
|
||||
positionVertical: "mso-position-vertical",
|
||||
positionVerticalRelative: "mso-position-vertical-relative",
|
||||
wrapDistanceBottom: "mso-wrap-distance-bottom",
|
||||
wrapDistanceLeft: "mso-wrap-distance-left",
|
||||
wrapDistanceRight: "mso-wrap-distance-right",
|
||||
wrapDistanceTop: "mso-wrap-distance-top",
|
||||
wrapEdited: "mso-wrap-edited",
|
||||
wrapStyle: "mso-wrap-style",
|
||||
position: "position",
|
||||
rotation: "rotation",
|
||||
top: "top",
|
||||
visibility: "visibility",
|
||||
width: "width",
|
||||
zIndex: "z-index",
|
||||
};
|
||||
|
||||
export type VmlShapeStyle = {
|
||||
/** Specifies that the orientation of a shape is flipped. Default is no value. */
|
||||
readonly flip?: "x" | "y" | "xy" | "yx";
|
||||
/** Specifies the height of the containing block of the shape. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. */
|
||||
readonly height?: LengthUnit;
|
||||
/** Specifies the position of the left of the containing block of the shape relative to the element left of it in the flow of the page. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. This property shall not be used for shapes anchored inline. */
|
||||
readonly left?: LengthUnit;
|
||||
/** Specifies the position of the bottom of the containing block of the shape relative to the shape anchor. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. */
|
||||
readonly marginBottom?: LengthUnit;
|
||||
/** Specifies the position of the left of the containing block of the shape relative to the shape anchor. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. */
|
||||
readonly marginLeft?: LengthUnit;
|
||||
/** Specifies the position of the right of the containing block of the shape relative to the shape anchor. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. */
|
||||
readonly marginRight?: LengthUnit;
|
||||
/** Specifies the position of the top of the containing block of the shape relative to the shape anchor. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. */
|
||||
readonly marginTop?: LengthUnit;
|
||||
/** Specifies the horizontal positioning data for objects in WordprocessingML documents. Default is absolute. */
|
||||
readonly positionHorizontal?: "absolute" | "left" | "center" | "right" | "inside" | "outside";
|
||||
/** Specifies relative horizontal position data for objects in WordprocessingML documents. This modifies the mso-position-horizontal property. Default is text. */
|
||||
readonly positionHorizontalRelative?: "margin" | "page" | "text" | "char";
|
||||
/** Specifies the vertical positioning data for objects in WordprocessingML documents. Default is absolute. */
|
||||
readonly positionVertical?: "absolute" | "left" | "center" | "right" | "inside" | "outside";
|
||||
/** Specifies relative vertical position data for objects in WordprocessingML documents. This modifies the mso-position-vertical property. Default is text. */
|
||||
readonly positionVerticalRelative?: "margin" | "page" | "text" | "char";
|
||||
/** Specifies the distance from the bottom of the shape to the text that wraps around it. Default is 0 pt. Note that this property is different from the CSS margin property, which changes the origin of the shape to include the margin areas. This property does not change the origin. */
|
||||
readonly wrapDistanceBottom?: number;
|
||||
/** Specifies the distance from the left side of the shape to the text that wraps around it. Default is 0 pt. Note that this property is different from the CSS margin property, which changes the origin of the shape to include the margin areas. This property does not change the origin. */
|
||||
readonly wrapDistanceLeft?: number;
|
||||
/** Specifies the distance from the right side of the shape to the text that wraps around it. Default is 0 pt. Note that this property is different from the CSS margin property, which changes the origin of the shape to include the margin areas. This property does not change the origin. */
|
||||
readonly wrapDistanceRight?: number;
|
||||
/** Specifies the distance from the top of the shape to the text that wraps around it. Default is 0 pt. Note that this property is different from the CSS margin property, which changes the origin of the shape to include the margin areas. This property does not change the origin. */
|
||||
readonly wrapDistanceTop?: number;
|
||||
/** Specifies whether the wrap coordinates were customized by the user. If the wrap coordinates are generated by an editor, this property is true; otherwise they were customized by a user. Default is false. */
|
||||
readonly wrapEdited?: boolean;
|
||||
/** Specifies the wrapping mode for text in shapes in WordprocessingML documents. Default is square. */
|
||||
readonly wrapStyle?: "square" | "none";
|
||||
/** Specifies the type of positioning used to place an element. Default is static. When the element is contained inside a group, this property must be absolute. */
|
||||
readonly position?: "static" | "absolute" | "relative";
|
||||
/** Specifies the angle that a shape is rotated, in degrees. Default is 0. Positive angles are clockwise. */
|
||||
readonly rotation?: number;
|
||||
/** Specifies the position of the top of the containing block of the shape relative to the element above it in the flow of the page. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. This property shall not be used for shapes anchored inline. */
|
||||
readonly top?: LengthUnit;
|
||||
/** Specifies whether a shape is displayed. Only inherit and hidden are used; any other values are mapped to inherit. Default is inherit. */
|
||||
readonly visibility?: "hidden" | "inherit";
|
||||
/** Specifies the width of the containing block of the shape. Default is 0. It is specified in CSS units or, for elements in a group, in the coordinate system of the parent element. */
|
||||
readonly width: LengthUnit;
|
||||
/** Specifies the display order of overlapping shapes. Default is 0. This property shall not be used for shapes anchored inline. */
|
||||
readonly zIndex?: "auto" | number;
|
||||
};
|
||||
|
||||
const formatShapeStyle = (style?: VmlShapeStyle): string | undefined =>
|
||||
style
|
||||
? Object.entries(style)
|
||||
.map(([key, value]) => `${styleToKeyMap[key as keyof VmlShapeStyle]}:${value}`)
|
||||
.join(";")
|
||||
: undefined;
|
||||
|
||||
export const createShape = ({
|
||||
id,
|
||||
children,
|
||||
type = SHAPE_TYPE,
|
||||
style,
|
||||
}: {
|
||||
readonly id: string;
|
||||
readonly children?: readonly ParagraphChild[];
|
||||
readonly type?: string;
|
||||
readonly style?: VmlShapeStyle;
|
||||
}): XmlComponent =>
|
||||
new BuilderElement<{
|
||||
readonly id: string;
|
||||
readonly type?: string;
|
||||
readonly style?: string;
|
||||
}>({
|
||||
name: "v:shape",
|
||||
attributes: {
|
||||
id: {
|
||||
key: "id",
|
||||
value: id,
|
||||
},
|
||||
type: {
|
||||
key: "type",
|
||||
value: type,
|
||||
},
|
||||
style: {
|
||||
key: "style",
|
||||
value: formatShapeStyle(style),
|
||||
},
|
||||
},
|
||||
children: [createVmlTextbox({ style: "mso-fit-shape-to-text:t;", children })],
|
||||
});
|
8
src/file/textbox/texbox-content/textbox-content.ts
Normal file
8
src/file/textbox/texbox-content/textbox-content.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { ParagraphChild } from "@file/paragraph";
|
||||
import { BuilderElement, XmlComponent } from "@file/xml-components";
|
||||
|
||||
export const createTextboxContent = ({ children = [] }: { readonly children?: readonly ParagraphChild[] }): XmlComponent =>
|
||||
new BuilderElement<{ readonly style?: string }>({
|
||||
name: "w:txbxContent",
|
||||
children: children as readonly XmlComponent[],
|
||||
});
|
47
src/file/textbox/textbox.spec.ts
Normal file
47
src/file/textbox/textbox.spec.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { Formatter } from "@export/formatter";
|
||||
import { Paragraph } from "@file/paragraph";
|
||||
|
||||
import { Textbox } from "./textbox";
|
||||
|
||||
describe("VmlTextbox", () => {
|
||||
it("should work", () => {
|
||||
const tree = new Formatter().format(
|
||||
new Textbox({
|
||||
style: {
|
||||
width: "10pt",
|
||||
},
|
||||
children: [new Paragraph("test-content")],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(tree).toStrictEqual({
|
||||
"w:p": [
|
||||
{
|
||||
"w:pict": [
|
||||
{
|
||||
"v:shape": [
|
||||
{ _attr: { id: expect.any(String), type: "#_x0000_t202", style: "width:10pt" } },
|
||||
{
|
||||
"v:textbox": [
|
||||
{ _attr: { insetmode: "auto", style: "mso-fit-shape-to-text:t;" } },
|
||||
{
|
||||
"w:txbxContent": [
|
||||
{
|
||||
"w:p": [
|
||||
{ "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "test-content"] }] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
27
src/file/textbox/textbox.ts
Normal file
27
src/file/textbox/textbox.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { FileChild } from "@file/file-child";
|
||||
import { IParagraphOptions, ParagraphProperties } from "@file/paragraph";
|
||||
import { uniqueId } from "@util/convenience-functions";
|
||||
|
||||
import { createPictElement } from "./pict-element/pict-element";
|
||||
import { VmlShapeStyle, createShape } from "./shape/shape";
|
||||
|
||||
type ITextboxOptions = Omit<IParagraphOptions, "style"> & {
|
||||
readonly style?: VmlShapeStyle;
|
||||
};
|
||||
|
||||
export class Textbox extends FileChild {
|
||||
public constructor({ style, children, ...rest }: ITextboxOptions) {
|
||||
super("w:p");
|
||||
this.root.push(new ParagraphProperties(rest));
|
||||
|
||||
this.root.push(
|
||||
createPictElement({
|
||||
shape: createShape({
|
||||
children: children,
|
||||
id: uniqueId(),
|
||||
style: style,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
3
src/file/textbox/types.ts
Normal file
3
src/file/textbox/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { Percentage, RelativeMeasure, UniversalMeasure } from "@util/values";
|
||||
|
||||
export type LengthUnit = "auto" | number | Percentage | UniversalMeasure | RelativeMeasure;
|
42
src/file/textbox/vml-textbox/vml-texbox.ts
Normal file
42
src/file/textbox/vml-textbox/vml-texbox.ts
Normal file
@ -0,0 +1,42 @@
|
||||
// http://webapp.docx4java.org/OnlineDemo/ecma376/VML/textbox.html
|
||||
import { ParagraphChild } from "@file/paragraph";
|
||||
import { BuilderElement, XmlComponent } from "@file/xml-components";
|
||||
import { InsetMode } from "@util/types";
|
||||
|
||||
import { createTextboxContent } from "../texbox-content/textbox-content";
|
||||
import { LengthUnit } from "../types";
|
||||
|
||||
// type VMLTextboxStyle = {
|
||||
// readonly fontWeight?: "normal" | "lighter" | 100 | 200 | 300 | 400 | "bold" | "bolder" | 500 | 600 | 700 | 800 | 900;
|
||||
// }
|
||||
|
||||
export type IVTextboxOptions = {
|
||||
readonly style?: string;
|
||||
readonly children?: readonly ParagraphChild[];
|
||||
readonly inset?: {
|
||||
readonly top: LengthUnit;
|
||||
readonly left: LengthUnit;
|
||||
readonly bottom: LengthUnit;
|
||||
readonly right: LengthUnit;
|
||||
};
|
||||
};
|
||||
|
||||
export const createVmlTextbox = ({ style, children, inset }: IVTextboxOptions): XmlComponent =>
|
||||
new BuilderElement<{ readonly style?: string; readonly inset?: string; readonly insetMode?: InsetMode }>({
|
||||
name: "v:textbox",
|
||||
attributes: {
|
||||
style: {
|
||||
key: "style",
|
||||
value: style,
|
||||
},
|
||||
insetMode: {
|
||||
key: "insetmode",
|
||||
value: inset ? "custom" : "auto",
|
||||
},
|
||||
inset: {
|
||||
key: "inset",
|
||||
value: inset ? `${inset.left}, ${inset.top}, ${inset.right}, ${inset.bottom}` : undefined,
|
||||
},
|
||||
},
|
||||
children: [createTextboxContent({ children })],
|
||||
});
|
46
src/file/textbox/vml-textbox/vml-textbox.spec.ts
Normal file
46
src/file/textbox/vml-textbox/vml-textbox.spec.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { Formatter } from "@export/formatter";
|
||||
import { Paragraph } from "@file/paragraph";
|
||||
|
||||
import { createVmlTextbox } from "./vml-texbox";
|
||||
|
||||
describe("VmlTextbox", () => {
|
||||
it("should work", () => {
|
||||
const tree = new Formatter().format(
|
||||
createVmlTextbox({
|
||||
style: "test-style",
|
||||
children: [new Paragraph("test-content")],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(tree).toStrictEqual({
|
||||
"v:textbox": [
|
||||
{ _attr: { insetmode: "auto", style: "test-style" } },
|
||||
{ "w:txbxContent": [{ "w:p": [{ "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "test-content"] }] }] }] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should work with inset", () => {
|
||||
const tree = new Formatter().format(
|
||||
createVmlTextbox({
|
||||
style: "test-style",
|
||||
children: [new Paragraph("test-content")],
|
||||
inset: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(tree).toStrictEqual({
|
||||
"v:textbox": [
|
||||
{ _attr: { insetmode: "custom", style: "test-style", inset: "0, 0, 0, 0" } },
|
||||
{ "w:txbxContent": [{ "w:p": [{ "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "test-content"] }] }] }] },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
import { BaseXmlComponent, IContext } from "./base";
|
||||
import { IXmlAttribute, IXmlableObject } from "./xmlable-object";
|
||||
|
||||
type AttributeMap<T> = Record<keyof T, string>;
|
||||
export type AttributeMap<T> = Record<keyof T, string>;
|
||||
|
||||
export type AttributeData = Record<string, boolean | number | string>;
|
||||
export type AttributePayload<T> = { readonly [P in keyof T]: { readonly key: string; readonly value: T[P] } };
|
||||
|
@ -2,6 +2,7 @@ import JSZip from "jszip";
|
||||
import { Element, js2xml } from "xml-js";
|
||||
|
||||
import { ImageReplacer } from "@export/packer/image-replacer";
|
||||
import { DocumentAttributeNamespaces } from "@file/document";
|
||||
import { IViewWrapper } from "@file/document-wrapper";
|
||||
import { File } from "@file/file";
|
||||
import { FileChild } from "@file/file-child";
|
||||
@ -10,6 +11,7 @@ import { ConcreteHyperlink, ExternalHyperlink, ParagraphChild } from "@file/para
|
||||
import { TargetModeType } from "@file/relationships/relationship/relationship";
|
||||
import { IContext } from "@file/xml-components";
|
||||
import { uniqueId } from "@util/convenience-functions";
|
||||
import { OutputByType, OutputType } from "@util/output-type";
|
||||
|
||||
import { appendContentType } from "./content-types-manager";
|
||||
import { appendRelationship, getNextRelationshipIndex } from "./relationship-manager";
|
||||
@ -46,21 +48,7 @@ type IHyperlinkRelationshipAddition = {
|
||||
|
||||
export type IPatch = ParagraphPatch | FilePatch;
|
||||
|
||||
// From JSZip
|
||||
type OutputByType = {
|
||||
readonly base64: string;
|
||||
// eslint-disable-next-line id-denylist
|
||||
readonly string: string;
|
||||
readonly text: string;
|
||||
readonly binarystring: string;
|
||||
readonly array: readonly number[];
|
||||
readonly uint8array: Uint8Array;
|
||||
readonly arraybuffer: ArrayBuffer;
|
||||
readonly blob: Blob;
|
||||
readonly nodebuffer: Buffer;
|
||||
};
|
||||
|
||||
export type PatchDocumentOutputType = keyof OutputByType;
|
||||
export type PatchDocumentOutputType = OutputType;
|
||||
|
||||
export type PatchDocumentOptions<T extends PatchDocumentOutputType = PatchDocumentOutputType> = {
|
||||
readonly outputType: T;
|
||||
@ -100,6 +88,24 @@ export const patchDocument = async <T extends PatchDocumentOutputType = PatchDoc
|
||||
}
|
||||
|
||||
const json = toJson(await value.async("text"));
|
||||
|
||||
if (key === "word/document.xml") {
|
||||
const document = json.elements?.find((i) => i.name === "w:document");
|
||||
if (document) {
|
||||
// We could check all namespaces from Document, but we'll instead
|
||||
// check only those that may be used by our element types.
|
||||
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
document.attributes = document.attributes ?? {};
|
||||
for (const ns of ["mc", "wp", "r", "w15", "m"] as const) {
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
document.attributes[`xmlns:${ns}`] = DocumentAttributeNamespaces[ns];
|
||||
}
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
document.attributes["mc:Ignorable"] = `${document.attributes["mc:Ignorable"] || ""} w15`.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (key.startsWith("word/") && !key.endsWith(".xml.rels")) {
|
||||
const context: IContext = {
|
||||
file,
|
||||
@ -132,38 +138,37 @@ export const patchDocument = async <T extends PatchDocumentOutputType = PatchDoc
|
||||
// We need to loop through to catch every occurrence of the patch text
|
||||
// It is possible that the patch text is in the same run
|
||||
// This algorithm is limited to one patch per text run
|
||||
// Once it cannot find any more occurrences, it will throw an error, and then we break out of the loop
|
||||
// We break out of the loop once it cannot find any more occurrences
|
||||
// https://github.com/dolanmiu/docx/issues/2267
|
||||
while (true) {
|
||||
try {
|
||||
replacer({
|
||||
json,
|
||||
patch: {
|
||||
...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;
|
||||
}
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
patchText,
|
||||
context,
|
||||
keepOriginalStyles,
|
||||
});
|
||||
} catch {
|
||||
const { didFindOccurrence } = replacer({
|
||||
json,
|
||||
patch: {
|
||||
...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;
|
||||
}
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
patchText,
|
||||
context,
|
||||
keepOriginalStyles,
|
||||
});
|
||||
if (!didFindOccurrence) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { IViewWrapper } from "@file/document-wrapper";
|
||||
import { File } from "@file/file";
|
||||
import { Paragraph, TextRun } from "@file/paragraph";
|
||||
import { IContext } from "@file/xml-components";
|
||||
|
||||
import { PatchType } from "./from-docx";
|
||||
import { replacer } from "./replacer";
|
||||
@ -62,6 +61,10 @@ export const MOCK_JSON = {
|
||||
name: "w:t",
|
||||
elements: [{ type: "text", text: "What a {{bold}} text!" }],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:br",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -73,25 +76,23 @@ export const MOCK_JSON = {
|
||||
|
||||
describe("replacer", () => {
|
||||
describe("replacer", () => {
|
||||
it("should throw an error if nothing is added", () => {
|
||||
expect(() =>
|
||||
replacer({
|
||||
json: {
|
||||
elements: [],
|
||||
},
|
||||
patch: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [],
|
||||
},
|
||||
patchText: "hello",
|
||||
// eslint-disable-next-line functional/prefer-readonly-type
|
||||
context: vi.fn<[], IContext>()(),
|
||||
}),
|
||||
).toThrow();
|
||||
it("should return { didFindOccurrence: false } if nothing is added", () => {
|
||||
const { didFindOccurrence } = replacer({
|
||||
json: {
|
||||
elements: [],
|
||||
},
|
||||
patch: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [],
|
||||
},
|
||||
patchText: "hello",
|
||||
context: vi.fn()(),
|
||||
});
|
||||
expect(didFindOccurrence).toBe(false);
|
||||
});
|
||||
|
||||
it("should replace paragraph type", () => {
|
||||
const output = replacer({
|
||||
const { element, didFindOccurrence } = replacer({
|
||||
json: JSON.parse(JSON.stringify(MOCK_JSON)),
|
||||
patch: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
@ -107,11 +108,12 @@ describe("replacer", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(JSON.stringify(output)).to.contain("Delightful Header");
|
||||
expect(JSON.stringify(element)).to.contain("Delightful Header");
|
||||
expect(didFindOccurrence).toBe(true);
|
||||
});
|
||||
|
||||
it("should replace paragraph type keeping original styling if keepOriginalStyles is true", () => {
|
||||
const output = replacer({
|
||||
const { element, didFindOccurrence } = replacer({
|
||||
json: JSON.parse(JSON.stringify(MOCK_JSON)),
|
||||
patch: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
@ -128,8 +130,8 @@ describe("replacer", () => {
|
||||
keepOriginalStyles: true,
|
||||
});
|
||||
|
||||
expect(JSON.stringify(output)).to.contain("sweet");
|
||||
expect(output.elements![0].elements![1].elements).toMatchObject([
|
||||
expect(JSON.stringify(element)).to.contain("sweet");
|
||||
expect(element.elements![0].elements![1].elements).toMatchObject([
|
||||
{
|
||||
type: "element",
|
||||
name: "w:r",
|
||||
@ -176,13 +178,18 @@ describe("replacer", () => {
|
||||
name: "w:t",
|
||||
elements: [{ type: "text", text: " text!" }],
|
||||
},
|
||||
{
|
||||
name: "w:br",
|
||||
type: "element",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(didFindOccurrence).toBe(true);
|
||||
});
|
||||
|
||||
it("should replace document type", () => {
|
||||
const output = replacer({
|
||||
const { element, didFindOccurrence } = replacer({
|
||||
json: JSON.parse(JSON.stringify(MOCK_JSON)),
|
||||
patch: {
|
||||
type: PatchType.DOCUMENT,
|
||||
@ -198,12 +205,13 @@ describe("replacer", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(JSON.stringify(output)).to.contain("Lorem ipsum paragraph");
|
||||
expect(JSON.stringify(element)).to.contain("Lorem ipsum paragraph");
|
||||
expect(didFindOccurrence).toBe(true);
|
||||
});
|
||||
|
||||
it("should replace", () => {
|
||||
// cspell:disable
|
||||
const output = replacer({
|
||||
const { element, didFindOccurrence } = replacer({
|
||||
json: {
|
||||
elements: [
|
||||
{
|
||||
@ -647,7 +655,74 @@ describe("replacer", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(JSON.stringify(output)).to.contain("Lorem ipsum paragraph");
|
||||
expect(JSON.stringify(element)).to.contain("Lorem ipsum paragraph");
|
||||
expect(didFindOccurrence).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle empty runs in patches", () => {
|
||||
// cspell:disable
|
||||
const { element, didFindOccurrence } = replacer({
|
||||
json: {
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:hdr",
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:p",
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:r",
|
||||
elements: [
|
||||
{ type: "text", text: "\n " },
|
||||
{
|
||||
type: "element",
|
||||
name: "w:rPr",
|
||||
elements: [
|
||||
{ type: "text", text: "\n " },
|
||||
{
|
||||
type: "element",
|
||||
name: "w:rFonts",
|
||||
attributes: { "w:eastAsia": "Times New Roman" },
|
||||
},
|
||||
{ type: "text", text: "\n " },
|
||||
],
|
||||
},
|
||||
{ type: "text", text: "\n " },
|
||||
{
|
||||
type: "element",
|
||||
name: "w:t",
|
||||
elements: [{ type: "text", text: "{{empty}}" }],
|
||||
},
|
||||
{ type: "text", text: "\n " },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// cspell:enable
|
||||
patch: {
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [new TextRun({})],
|
||||
},
|
||||
patchText: "{{empty}}",
|
||||
context: {
|
||||
file: {} as unknown as File,
|
||||
viewWrapper: {
|
||||
Relationships: {},
|
||||
} as unknown as IViewWrapper,
|
||||
stack: [],
|
||||
},
|
||||
keepOriginalStyles: true,
|
||||
});
|
||||
|
||||
expect(JSON.stringify(element)).not.to.contain("{{empty}}");
|
||||
expect(didFindOccurrence).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -14,6 +14,11 @@ const formatter = new Formatter();
|
||||
|
||||
const SPLIT_TOKEN = "ɵ";
|
||||
|
||||
type IReplacerResult = {
|
||||
readonly element: Element;
|
||||
readonly didFindOccurrence: boolean;
|
||||
};
|
||||
|
||||
export const replacer = ({
|
||||
json,
|
||||
patch,
|
||||
@ -26,11 +31,11 @@ export const replacer = ({
|
||||
readonly patchText: string;
|
||||
readonly context: IContext;
|
||||
readonly keepOriginalStyles?: boolean;
|
||||
}): Element => {
|
||||
}): IReplacerResult => {
|
||||
const renderedParagraphs = findLocationOfText(json, patchText);
|
||||
|
||||
if (renderedParagraphs.length === 0) {
|
||||
throw new Error(`Could not find text ${patchText}`);
|
||||
return { element: json, didFindOccurrence: false };
|
||||
}
|
||||
|
||||
for (const renderedParagraph of renderedParagraphs) {
|
||||
@ -64,12 +69,12 @@ export const replacer = ({
|
||||
|
||||
if (keepOriginalStyles) {
|
||||
const runElementNonTextualElements = runElementToBeReplaced.elements!.filter(
|
||||
(e) => e.type === "element" && e.name !== "w:t" && e.name !== "w:br" && e.name !== "w:tab",
|
||||
(e) => e.type === "element" && e.name === "w:rPr",
|
||||
);
|
||||
|
||||
newRunElements = textJson.map((e) => ({
|
||||
...e,
|
||||
elements: [...runElementNonTextualElements, ...e.elements!],
|
||||
elements: [...runElementNonTextualElements, ...(e.elements ?? [])],
|
||||
}));
|
||||
|
||||
patchedRightElement = {
|
||||
@ -85,7 +90,7 @@ export const replacer = ({
|
||||
}
|
||||
}
|
||||
|
||||
return json;
|
||||
return { element: json, didFindOccurrence: true };
|
||||
};
|
||||
|
||||
const goToElementFromPath = (json: Element, path: readonly number[]): Element => {
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from "./convenience-functions";
|
||||
export * from "./values";
|
||||
export type * from "./output-type";
|
||||
|
18
src/util/output-type.ts
Normal file
18
src/util/output-type.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/* v8 ignore start */
|
||||
// Simply type definitions. Can ignore testing and coverage
|
||||
// From JSZip
|
||||
export type OutputByType = {
|
||||
readonly base64: string;
|
||||
// eslint-disable-next-line id-denylist
|
||||
readonly string: string;
|
||||
readonly text: string;
|
||||
readonly binarystring: string;
|
||||
readonly array: readonly number[];
|
||||
readonly uint8array: Uint8Array;
|
||||
readonly arraybuffer: ArrayBuffer;
|
||||
readonly blob: Blob;
|
||||
readonly nodebuffer: Buffer;
|
||||
};
|
||||
|
||||
export type OutputType = keyof OutputByType;
|
||||
/* v8 ignore stop */
|
7
src/util/types.ts
Normal file
7
src/util/types.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// <xsd:simpleType name="ST_InsetMode">
|
||||
// <xsd:restriction base="xsd:string">
|
||||
// <xsd:enumeration value="auto"/>
|
||||
// <xsd:enumeration value="custom"/>
|
||||
// </xsd:restriction>
|
||||
// </xsd:simpleType>
|
||||
export type InsetMode = "auto" | "custom"; // VML only type
|
@ -21,6 +21,16 @@ export type PositiveUniversalMeasure = `${number}${"mm" | "cm" | "in" | "pt" | "
|
||||
// </xsd:simpleType>
|
||||
export type Percentage = `${"-" | ""}${number}%`;
|
||||
|
||||
// <xsd:simpleType name="ST_PositivePercentage">
|
||||
// <xsd:restriction base="ST_Percentage">
|
||||
// <xsd:pattern value="[0-9]+(\.[0-9]+)?%"/>
|
||||
// </xsd:restriction>
|
||||
// </xsd:simpleType>
|
||||
export type PositivePercentage = `${number}%`;
|
||||
|
||||
// Only applies to VmlTextbox so far
|
||||
export type RelativeMeasure = `${"-" | ""}${number}${"em" | "ex"}`;
|
||||
|
||||
// <xsd:simpleType name="ST_DecimalNumber">
|
||||
// <xsd:restriction base="xsd:integer"/>
|
||||
// </xsd:simpleType>
|
||||
|
@ -1,14 +1,20 @@
|
||||
import { configDefaults, defineConfig } from "vitest/config";
|
||||
import { resolve } from "path";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import dts from "vite-plugin-dts";
|
||||
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { configDefaults, defineConfig } from "vitest/config";
|
||||
import { copyFileSync } from "node:fs";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths(),
|
||||
dts({
|
||||
rollupTypes: true
|
||||
rollupTypes: true,
|
||||
afterBuild: () => {
|
||||
// https://github.com/dolanmiu/docx/pull/2883
|
||||
// To pass publint - `npx publint@latest`
|
||||
copyFileSync("dist/index.d.ts", "dist/index.d.cts");
|
||||
},
|
||||
}),
|
||||
nodePolyfills({
|
||||
exclude: [],
|
||||
@ -34,7 +40,7 @@ export default defineConfig({
|
||||
name: "docx",
|
||||
fileName: (d) => {
|
||||
if (d === "umd") {
|
||||
return "index.umd.js";
|
||||
return "index.umd.cjs";
|
||||
}
|
||||
|
||||
if (d === "cjs") {
|
||||
@ -53,7 +59,7 @@ export default defineConfig({
|
||||
},
|
||||
formats: ["iife", "es", "cjs", "umd"],
|
||||
},
|
||||
outDir: resolve(__dirname, "build"),
|
||||
outDir: resolve(__dirname, "dist"),
|
||||
commonjsOptions: {
|
||||
include: [/node_modules/],
|
||||
},
|
||||
@ -65,29 +71,22 @@ export default defineConfig({
|
||||
reporter: ["text", "json", "html"],
|
||||
thresholds: {
|
||||
statements: 100,
|
||||
branches: 99.35,
|
||||
branches: 99.68,
|
||||
functions: 100,
|
||||
lines: 100,
|
||||
},
|
||||
exclude: [
|
||||
...configDefaults.exclude,
|
||||
'**/build/**',
|
||||
'**/demo/**',
|
||||
'**/docs/**',
|
||||
'**/scripts/**',
|
||||
'**/src/**/index.ts',
|
||||
"**/dist/**",
|
||||
"**/demo/**",
|
||||
"**/docs/**",
|
||||
"**/scripts/**",
|
||||
"**/src/**/index.ts",
|
||||
"**/src/**/types.ts",
|
||||
"**/*.spec.ts",
|
||||
],
|
||||
},
|
||||
include: [
|
||||
'**/src/**/*.spec.ts',
|
||||
'**/packages/**/*.spec.ts'
|
||||
],
|
||||
exclude: [
|
||||
...configDefaults.exclude,
|
||||
'**/build/**',
|
||||
'**/demo/**',
|
||||
'**/docs/**',
|
||||
'**/scripts/**'
|
||||
],
|
||||
include: ["**/src/**/*.spec.ts", "**/packages/**/*.spec.ts"],
|
||||
exclude: [...configDefaults.exclude, "**/build/**", "**/demo/**", "**/docs/**", "**/scripts/**"],
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user