Compare commits
116 Commits
Author | SHA1 | Date | |
---|---|---|---|
3eb98533ae | |||
f5f021834e | |||
35702c3f77 | |||
8fbbd571ad | |||
2dd228be69 | |||
562835cfe7 | |||
6bac06e84d | |||
88436168ee | |||
0c3206d2e2 | |||
033debd339 | |||
d4f160732a | |||
4238fc9ab4 | |||
0d042b8dd1 | |||
afdd5f2d8f | |||
70c7b3d1b3 | |||
7b1cd5fe86 | |||
09ab169ffd | |||
35a82cf12e | |||
8d33cb01dd | |||
438d11dd86 | |||
d23b0d0789 | |||
1fa8c7ac82 | |||
3d6ead0359 | |||
379050dccd | |||
f9d1c197cf | |||
a89ee463e6 | |||
036caaacc8 | |||
80c37afe2b | |||
3a36d912fe | |||
c741d5d8ac | |||
0fa7dd13ad | |||
d5d80550e7 | |||
23a0a59454 | |||
066aa56f6a | |||
3997b7a5d6 | |||
ccc391607a | |||
9577192d41 | |||
968c3aed0f | |||
9c7a80729b | |||
5f26ca1c94 | |||
f2b1587bff | |||
3b9f80fb1a | |||
8da57bec51 | |||
63db9f290c | |||
e8bd4bd6c6 | |||
28d93b0c42 | |||
95f8e37006 | |||
87083cb264 | |||
1e8dc95c9c | |||
abfa242c28 | |||
56b5414152 | |||
8f46060be2 | |||
93b17ca2af | |||
66008115b8 | |||
55697a7c32 | |||
e9b259db6b | |||
55c51f7af1 | |||
4e7fd6a6dc | |||
6294ec448f | |||
668198b5d1 | |||
56b2ffe706 | |||
2ab06ffe36 | |||
0c51082bb9 | |||
9a9a2019f6 | |||
d94348f5ca | |||
4c10741862 | |||
222a25e4e2 | |||
5fd4490c4f | |||
e1cc65cb97 | |||
991f837bc1 | |||
87b3035465 | |||
4498305a6c | |||
cac7abba91 | |||
b9b55b2829 | |||
11365d5be5 | |||
47a5aff40c | |||
721fbbac67 | |||
bcca50fb40 | |||
229d2eb689 | |||
3ef80cc7b3 | |||
f1f11f36e4 | |||
28f187064e | |||
c391ca533a | |||
0ca92b80f9 | |||
84501b6038 | |||
dcd3e90b19 | |||
be7d84dfa0 | |||
b0d60109c9 | |||
b4659f9901 | |||
25f657b842 | |||
dfffb066f3 | |||
4f06d760a3 | |||
aea2531111 | |||
16707201b4 | |||
c7c9652095 | |||
05378f58ae | |||
d7549a1140 | |||
e3ee455186 | |||
210d9c58f2 | |||
db85c3dd2f | |||
414d0248f5 | |||
fa400bcf39 | |||
9e9ca526fe | |||
41c0fb5fc0 | |||
58e7dbf445 | |||
20e0213c7d | |||
b8f83fd6ad | |||
d1f75e3a42 | |||
337ff464cf | |||
cad4a5510b | |||
aa8438d8bd | |||
79363c2c2c | |||
f706d8e62d | |||
e7ee2a0fdf | |||
bd17df8e98 | |||
e237326319 |
@ -12,7 +12,6 @@
|
||||
[![Downloads per month][downloads-image]][downloads-url]
|
||||
[![GitHub Action Workflow Status][github-actions-workflow-image]][github-actions-workflow-url]
|
||||
[![Known Vulnerabilities][snky-image]][snky-url]
|
||||
[![Chat on Gitter][gitter-image]][gitter-url]
|
||||
[![PRs Welcome][pr-image]][pr-url]
|
||||
[![codecov][codecov-image]][codecov-url]
|
||||
|
||||
@ -107,8 +106,6 @@ Made with 💖
|
||||
[github-actions-workflow-url]: https://github.com/dolanmiu/docx/actions
|
||||
[snky-image]: https://snyk.io/test/github/dolanmiu/docx/badge.svg
|
||||
[snky-url]: https://snyk.io/test/github/dolanmiu/docx
|
||||
[gitter-image]: https://badges.gitter.im/dolanmiu/docx.svg
|
||||
[gitter-url]: https://gitter.im/docx-lib/Lobby
|
||||
[pr-image]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg
|
||||
[pr-url]: http://makeapullrequest.com
|
||||
[codecov-image]: https://codecov.io/gh/dolanmiu/docx/branch/master/graph/badge.svg
|
||||
|
@ -43,6 +43,15 @@ const doc = new Document({
|
||||
color: "#FF0000",
|
||||
},
|
||||
},
|
||||
document: {
|
||||
run: {
|
||||
size: "11pt",
|
||||
font: "Calibri",
|
||||
},
|
||||
paragraph: {
|
||||
alignment: AlignmentType.RIGHT,
|
||||
},
|
||||
},
|
||||
},
|
||||
paragraphStyles: [
|
||||
{
|
||||
|
@ -4,6 +4,18 @@ import * as fs from "fs";
|
||||
import { Document, ExternalHyperlink, Footer, FootnoteReferenceRun, ImageRun, Packer, Paragraph, TextRun } from "docx";
|
||||
|
||||
const doc = new Document({
|
||||
styles: {
|
||||
default: {
|
||||
hyperlink: {
|
||||
run: {
|
||||
color: "FF0000",
|
||||
underline: {
|
||||
color: "0000FF",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
footnotes: {
|
||||
1: {
|
||||
children: [
|
||||
|
44
demo/90-check-boxes.ts
Normal file
44
demo/90-check-boxes.ts
Normal file
@ -0,0 +1,44 @@
|
||||
// Simple example to add check boxes to a document
|
||||
import * as fs from "fs";
|
||||
import { Document, Packer, Paragraph, TextRun, CheckBox } from "docx";
|
||||
|
||||
const doc = new Document({
|
||||
sections: [
|
||||
{
|
||||
properties: {},
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun("Hello World"),
|
||||
new TextRun({ break: 1 }),
|
||||
new CheckBox(),
|
||||
new TextRun({ break: 1 }),
|
||||
new CheckBox({ checked: true }),
|
||||
new TextRun({ break: 1 }),
|
||||
new CheckBox({ checked: true, checkedState: { value: "2611" } }),
|
||||
new TextRun({ break: 1 }),
|
||||
new CheckBox({ checked: true, checkedState: { value: "2611", font: "MS Gothic" } }),
|
||||
new TextRun({ break: 1 }),
|
||||
new CheckBox({
|
||||
checked: true,
|
||||
checkedState: { value: "2611", font: "MS Gothic" },
|
||||
uncheckedState: { value: "2610", font: "MS Gothic" },
|
||||
}),
|
||||
new TextRun({ break: 1 }),
|
||||
new CheckBox({
|
||||
checked: true,
|
||||
checkedState: { value: "2611", font: "MS Gothic" },
|
||||
uncheckedState: { value: "2610", font: "MS Gothic" },
|
||||
}),
|
||||
new TextRun({ text: "Are you ok?", break: 1 }),
|
||||
new CheckBox({ checked: true, alias: "Are you ok?" }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Packer.toBuffer(doc).then((buffer) => {
|
||||
fs.writeFileSync("My Document.docx", buffer);
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="../build/index.umd.cjs"></script>
|
||||
<script src="../build/index.umd.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.js"></script>
|
||||
</head>
|
||||
|
||||
|
@ -5,9 +5,9 @@ import inquirer from "inquirer";
|
||||
import { $ } from "execa";
|
||||
|
||||
export type Answers = {
|
||||
type: "list" | "number";
|
||||
demoNumber?: number;
|
||||
demoFile?: number;
|
||||
readonly type: "list" | "number";
|
||||
readonly demoNumber?: number;
|
||||
readonly demoFile?: number;
|
||||
};
|
||||
|
||||
const dir = "./demo";
|
||||
@ -15,8 +15,7 @@ const fileNames = fs.readdirSync(dir);
|
||||
|
||||
const keys = fileNames.map((f) => path.parse(f).name);
|
||||
const getFileNumber = (file: string): number => {
|
||||
const nameParts = file.split("-");
|
||||
const firstPart = nameParts[0];
|
||||
const [firstPart] = file.split("-");
|
||||
|
||||
return Number(firstPart);
|
||||
};
|
||||
@ -35,15 +34,15 @@ const answers = await inquirer.prompt<Answers>([
|
||||
name: "demoFile",
|
||||
message: "What demo do you wish to run?",
|
||||
choices: demoFiles,
|
||||
filter: (input) => parseInt(input.split("-")[0]),
|
||||
when: (answers) => answers.type === "list",
|
||||
filter: (input) => parseInt(input.split("-")[0], 10),
|
||||
when: (a) => a.type === "list",
|
||||
},
|
||||
{
|
||||
type: "number",
|
||||
name: "demoNumber",
|
||||
message: "What demo do you wish to run? (Enter a number)",
|
||||
default: 1,
|
||||
when: (answers) => answers.type === "number",
|
||||
when: (a) => a.type === "number",
|
||||
},
|
||||
]);
|
||||
|
||||
@ -56,6 +55,7 @@ if (files.length === 0) {
|
||||
const filePath = path.join(dir, files[0]);
|
||||
|
||||
console.log(`Running demo ${demoNumber}: ${files[0]}`);
|
||||
await $`ts-node --project demo/tsconfig.json ${filePath}`;
|
||||
const { stdout } = await $`ts-node --project demo/tsconfig.json ${filePath}`;
|
||||
console.log(stdout);
|
||||
console.log("Successfully created document!");
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
# Bullets and Numbering
|
||||
|
||||
!> Bullets and Numbering requires an understanding of [Sections](usage/sections.md) and [Paragraphs](usage/paragraph.md).
|
||||
|
||||
`docx` is quite flexible in its bullets and numbering system, allowing
|
||||
the user great freedom in how bullets and numbers are to be styled and
|
||||
displayed. E.g., numbers can be shown using Arabic numerals, roman
|
||||
@ -8,112 +10,128 @@ format also supports re-using bullets/numbering styles throughout the
|
||||
document, so that different lists using the same style need not
|
||||
redefine them.
|
||||
|
||||
Because of this flexibility, bullets and numbering in DOCX involves a
|
||||
couple of moving pieces:
|
||||
## Configuration
|
||||
|
||||
1. Document-level bullets/numbering definitions (abstract)
|
||||
2. Document-level bullets/numbering definitions (concrete)
|
||||
3. Paragraph-level bullets/numbering selection
|
||||
|
||||
## Document-level bullets/numbering definitions (abstract)
|
||||
|
||||
Every document contains a set of abstract bullets/numbering
|
||||
definitions which define the formatting and layout of paragraphs using
|
||||
those bullets/numbering. An abstract numbering system defines how
|
||||
bullets/numbers are to be shown for lists, including any sublists that
|
||||
may be used. Thus each abstract definition includes a series of
|
||||
_levels_ which form a sequence starting at 0 indicating the top-level
|
||||
list look and increasing from there to describe the sublists, then
|
||||
sub-sublists, etc. Each level includes the following properties:
|
||||
|
||||
* **level**: This is its 0-based index in the definition stack
|
||||
* **numberFormat**: This indicates how the bullet or number should be
|
||||
generated. Options include `bullet` (meaning don't count), `decimal`
|
||||
(arabic numerals), `upperRoman`, `lowerRoman`, `hex`, and many
|
||||
more.
|
||||
* **levelText**: This is a format string using the output of the
|
||||
`numberFormat` function and generating a string to insert before
|
||||
every item in the list. You may use `%1`, `%2`, ... to reference the
|
||||
numbers from each numbering level before this one. Thus a level
|
||||
text of `%d)` with a number format of `lowerLetter` would result in
|
||||
the sequence "a)", "b)", ...
|
||||
* and a few others, which you can see in the OOXML spec section 17.9.6
|
||||
|
||||
## Document-level bullets/numbering definitions (concrete)
|
||||
|
||||
Concrete definitions are sort of like concrete subclasses of the
|
||||
abstract definitions. They indicate their parent and are allowed to
|
||||
override certain level definitions. Thus two lists that differ only in
|
||||
how sub-sub-lists are to be displayed can share the same abstract
|
||||
numbering definition and have slightly different concrete definitions.
|
||||
|
||||
## Paragraph-level bullets/numbering selection
|
||||
|
||||
In order to use a bullets/numbering definition (which must be
|
||||
concrete), paragraphs need to select it, similar to applying a CSS
|
||||
class to an element, using both the concrete numbering definition ID
|
||||
and the level number that the paragraph should be at. Additionally, MS
|
||||
Word and LibreOffice typically apply a "ListParagraph" style to
|
||||
paragraphs that are being numbered.
|
||||
|
||||
## Using bullets/numbering in `docx`
|
||||
|
||||
`docx` includes a pre-defined bullet style which you can add to your
|
||||
paragraphs using `para.bullets()`. If you require different bullet
|
||||
styles or numbering of any kind, you'll have to use the
|
||||
`docx.Numbering` class.
|
||||
|
||||
First you need to create a new numbering container class and use it to
|
||||
create your abstract numbering style, define your levels, and create
|
||||
your concrete numbering style:
|
||||
Numbering is configured by adding config into `Document`:
|
||||
|
||||
```ts
|
||||
const numbering = new docx.Numbering();
|
||||
|
||||
const abstractNum = numbering.createAbstractNumbering();
|
||||
abstractNum.createLevel(0, "upperRoman", "%1", "start").addParagraphProperty(new Indent(720, 260));
|
||||
abstractNum.createLevel(1, "decimal", "%2.", "start").addParagraphProperty(new Indent(1440, 980));
|
||||
abstractNum.createLevel(2, "lowerLetter", "%3)", "start").addParagraphProperty(new Indent(2160, 1700));
|
||||
|
||||
const concrete = numbering.createConcreteNumbering(abstractNum);
|
||||
```
|
||||
|
||||
You can then apply your concrete style to paragraphs using the
|
||||
`setNumbering` method:
|
||||
|
||||
```ts
|
||||
topLevelP.setNumbering(concrete, 0);
|
||||
subP.setNumbering(concrete, 1);
|
||||
subSubP.setNumbering(concrete, 2);
|
||||
```
|
||||
|
||||
## Unindent numbering
|
||||
|
||||
Default:1. test
|
||||
|
||||
After:1.test
|
||||
|
||||
Use default numbering have indent,If you want unindent numbering
|
||||
|
||||
How to custom number see the demo:
|
||||
https://runkit.com/dolanmiu/docx-demo3
|
||||
|
||||
```ts
|
||||
|
||||
enum LevelSuffix {
|
||||
NOTHING = "nothing",
|
||||
SPACE = "space",
|
||||
TAB = "tab"
|
||||
new Document({
|
||||
numbering: {
|
||||
config: [...]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
// custom numbering
|
||||
const levels=[
|
||||
Each `config` entry includes the following properties:
|
||||
|
||||
. Each level includes the following properties:
|
||||
|
||||
| Property | Type | Notes | Possible Values |
|
||||
| --------- | ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| reference | `string` | Required | A unique `string` |
|
||||
| levels | `ILevelOptions[]` | Required | a series of _levels_ which form a sequence starting at 0 indicating the top-level list look and increasing from there to describe the sublists, then sub-sublists, etc |
|
||||
|
||||
### Level Options
|
||||
|
||||
Levels define the numbering definition itself, what it looks like, the indention, the alignment and the style. The reason why it is an array is because it allows the ability to create sub-lists. A sub list will have a different configuration because you may want the sub-list to have a different indentation or different bullet.
|
||||
|
||||
| Property | Type | Notes | Possible Values |
|
||||
| --------- | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| level | `number` | Required | The list level this definition is for. `0` is for the root level, `1` is for a sub list, `2` is for a sub-sub-list etc. |
|
||||
| format | `LevelFormat` | Optional | `DECIMAL`, `UPPER_ROMAN`, `LOWER_ROMAN`, `UPPER_LETTER`, `LOWER_LETTER`, `ORDINAL`, `CARDINAL_TEXT`, `ORDINAL_TEXT`, `HEX`, `CHICAGO`, `IDEOGRAPH__DIGITAL`, `JAPANESE_COUNTING`, `AIUEO`, `IROHA`, `DECIMAL_FULL_WIDTH`, `DECIMAL_HALF_WIDTH`, `JAPANESE_LEGAL`, `JAPANESE_DIGITAL_TEN_THOUSAND`, `DECIMAL_ENCLOSED_CIRCLE`, `DECIMAL_FULL_WIDTH2`, `AIUEO_FULL_WIDTH`, `IROHA_FULL_WIDTH`, `DECIMAL_ZERO`, `BULLET`, `GANADA`, `CHOSUNG`, `DECIMAL_ENCLOSED_FULLSTOP`, `DECIMAL_ENCLOSED_PARENTHESES`, `DECIMAL_ENCLOSED_CIRCLE_CHINESE`, `IDEOGRAPH_ENCLOSED_CIRCLE`, `IDEOGRAPH_TRADITIONAL`, `IDEOGRAPH_ZODIAC`, `IDEOGRAPH_ZODIAC_TRADITIONAL`, `TAIWANESE_COUNTING`, `IDEOGRAPH_LEGAL_TRADITIONAL`, `TAIWANESE_COUNTING_THOUSAND`, `TAIWANESE_DIGITAL`, `CHINESE_COUNTING`, `CHINESE_LEGAL_SIMPLIFIED`, `CHINESE_COUNTING_THOUSAND`, `KOREAN_DIGITAL`, `KOREAN_COUNTING`, `KOREAN_LEGAL`, `KOREAN_DIGITAL2`, `VIETNAMESE_COUNTING`, `RUSSIAN_LOWER`, `RUSSIAN_UPPER`, `NONE`, `NUMBER_IN_DASH`, `HEBREW1`, `HEBREW2`, `ARABIC_ALPHA`, `ARABIC_ABJAD`, `HINDI_VOWELS`, `HINDI_CONSONANTS`, `HINDI_NUMBERS`, `HINDI_COUNTING`, `THAI_LETTERS`, `THAI_NUMBERS`, `THAI_COUNTING`, `BAHT_TEXT`, `DOLLAR_TEXT`, `CUSTOM` |
|
||||
| text | `string` | Optional | A unique `string` to describe the shape of the bullet |
|
||||
| alignment | `string` | Required | `START`, `CENTER`, `END`, `BOTH`, `MEDIUM_KASHIDA`, `DISTRIBUTE`, `NUM_TAB`, `HIGH_KASHIDA`, `LOW_KASHIDA`, `THAI_DISTRIBUTE`, `LEFT`, `RIGHT`, `JUSTIFIED` |
|
||||
| style | `string` | Optional | [Sections](usage/styling-with-js.md) |
|
||||
|
||||
## Using ordered lists in `docx`
|
||||
|
||||
Add a `numbering` section to the `Document` to numbering style, define your levels. Use `LevelFormat.UPPER_ROMAN` for the `format` in `levels`:
|
||||
|
||||
```ts
|
||||
const doc = new Document({
|
||||
...
|
||||
numbering: {
|
||||
config: [
|
||||
{
|
||||
reference: "my-numbering",
|
||||
levels: [
|
||||
{
|
||||
level: 0,
|
||||
format: "decimal",
|
||||
text: "%1.",
|
||||
format: LevelFormat.UPPER_ROMAN,
|
||||
text: "%1",
|
||||
alignment: AlignmentType.START,
|
||||
suffix: LevelSuffix.NOTHING, // Cancel intent
|
||||
}]
|
||||
|
||||
style: {
|
||||
paragraph: {
|
||||
indent: { left: 2880, hanging: 2420 },
|
||||
},
|
||||
},
|
||||
},
|
||||
...
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
And then on a `Paragraph`, we can add use the numbering created:
|
||||
|
||||
```ts
|
||||
new Paragraph({
|
||||
text: "Hey you!",
|
||||
numbering: {
|
||||
reference: "my-numbering",
|
||||
level: 0,
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
## Un-ordered lists / Bullet points
|
||||
|
||||
Add a `numbering` section to the `Document` to numbering style, define your levels. Use `LevelFormat.BULLET` for the `format` in `levels`:
|
||||
|
||||
```ts
|
||||
const doc = new Document({
|
||||
...
|
||||
numbering: {
|
||||
config: [
|
||||
{
|
||||
reference: "my-bullet-points",
|
||||
levels: [
|
||||
{
|
||||
level: 0,
|
||||
format: LevelFormat.BULLET,
|
||||
text: "\u1F60",
|
||||
alignment: AlignmentType.LEFT,
|
||||
style: {
|
||||
paragraph: {
|
||||
indent: { left: convertInchesToTwip(0.5), hanging: convertInchesToTwip(0.25) },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
And then on a `Paragraph`, we can add use the numbering created:
|
||||
|
||||
```ts
|
||||
new Paragraph({
|
||||
text: "Hey you!",
|
||||
numbering: {
|
||||
reference: "my-bullet-points",
|
||||
level: 0,
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
## Full Example
|
||||
|
||||
[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/3-numbering-and-bullet-points.ts ":include")
|
||||
|
||||
_Source: https://github.com/dolanmiu/docx/blob/master/demo/3-numbering-and-bullet-points.ts_
|
||||
|
@ -9,11 +9,16 @@
|
||||

|
||||

|
||||
|
||||
*Note*: Font and color selection from the theme are currently not supported.
|
||||
|
||||
3. You can even create a totally new `Style`:
|
||||
|
||||

|
||||

|
||||
|
||||
*Note*: When selecting the style type, it is important to consider the component being used.
|
||||
|
||||
|
||||
4. Save
|
||||
5. Re-name the saved `.docx` file to `.zip` and un-zip
|
||||
6. Find `styles.xml`
|
||||
|
5039
package-lock.json
generated
5039
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@ -1,20 +1,17 @@
|
||||
{
|
||||
"name": "docx",
|
||||
"version": "8.1.0",
|
||||
"version": "8.2.4",
|
||||
"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.cjs",
|
||||
"module": "./build/index.js",
|
||||
"main": "build/index.umd.js",
|
||||
"module": "./build/index.mjs",
|
||||
"types": "./build/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"browser": {
|
||||
"default": "./build/index.umd.cjs"
|
||||
},
|
||||
"require": "./build/index.cjs",
|
||||
"types": "./build/index.d.ts",
|
||||
"import": "./build/index.js",
|
||||
"default": "./build/index.js"
|
||||
"import": "./build/index.mjs",
|
||||
"default": "./build/index.mjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@ -58,7 +55,6 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@types/node": "^20.3.1",
|
||||
"fflate": "^0.8.0",
|
||||
"jszip": "^3.10.1",
|
||||
"nanoid": "^4.0.2",
|
||||
"xml": "^1.0.1",
|
||||
@ -71,16 +67,15 @@
|
||||
},
|
||||
"homepage": "https://docx.js.org",
|
||||
"devDependencies": {
|
||||
"@esbuild/win32-x64": "^0.18.3",
|
||||
"@types/inquirer": "^9.0.3",
|
||||
"@types/prompt": "^1.1.1",
|
||||
"@types/unzipper": "^0.10.4",
|
||||
"@types/xml": "^1.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.36.1",
|
||||
"@typescript-eslint/parser": "^5.36.1",
|
||||
"@vitest/coverage-v8": "^0.32.0",
|
||||
"@vitest/ui": "^0.32.0",
|
||||
"cspell": "^6.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.9.1",
|
||||
"@typescript-eslint/parser": "^6.9.1",
|
||||
"@vitest/coverage-v8": "^0.33.0",
|
||||
"@vitest/ui": "^0.33.0",
|
||||
"cspell": "^7.3.8",
|
||||
"docsify-cli": "^4.3.0",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-plugin-functional": "^5.0.8",
|
||||
@ -88,23 +83,23 @@
|
||||
"eslint-plugin-jsdoc": "^46.2.6",
|
||||
"eslint-plugin-no-null": "^1.0.2",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||
"eslint-plugin-unicorn": "^47.0.0",
|
||||
"execa": "^7.1.1",
|
||||
"eslint-plugin-unicorn": "^48.0.1",
|
||||
"execa": "^8.0.1",
|
||||
"glob": "^10.2.7",
|
||||
"inquirer": "^9.2.7",
|
||||
"jsdom": "^22.1.0",
|
||||
"pre-commit": "^1.2.2",
|
||||
"prettier": "^2.3.1",
|
||||
"prettier": "^3.0.0",
|
||||
"ts-node": "^10.2.1",
|
||||
"tsconfig-paths": "^4.0.0",
|
||||
"typedoc": "^0.24.8",
|
||||
"typescript": "5.1.3",
|
||||
"typescript": "5.1.6",
|
||||
"unzipper": "^0.10.11",
|
||||
"vite": "^4.3.2",
|
||||
"vite-plugin-dts": "^2.3.0",
|
||||
"vite-plugin-dts": "^3.3.1",
|
||||
"vite-plugin-node-polyfills": "^0.9.0",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"vitest": "^0.32.0"
|
||||
"vitest": "^0.33.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
|
29
src/file/checkbox/checkbox-symbol.ts
Normal file
29
src/file/checkbox/checkbox-symbol.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// This represents element type CT_SdtCheckboxSymbol element
|
||||
// <xsd:complexType name="CT_SdtCheckboxSymbol">
|
||||
// <xsd:attribute name="font" type="w:ST_String"/>
|
||||
// <xsd:attribute name="val" type="w:ST_ShortHexNumber"/>
|
||||
// </xsd:complexType>
|
||||
|
||||
import { XmlAttributeComponent, XmlComponent } from "@file/xml-components";
|
||||
import { shortHexNumber } from "@util/values";
|
||||
|
||||
class CheckboxSymbolAttributes extends XmlAttributeComponent<{
|
||||
readonly val?: string | number | boolean;
|
||||
readonly symbolfont?: string;
|
||||
}> {
|
||||
protected readonly xmlKeys = {
|
||||
val: "w14:val",
|
||||
symbolfont: "w14:font",
|
||||
};
|
||||
}
|
||||
|
||||
export class CheckBoxSymbolElement extends XmlComponent {
|
||||
public constructor(name: string, val: string, font?: string) {
|
||||
super(name);
|
||||
if (font) {
|
||||
this.root.push(new CheckboxSymbolAttributes({ val: shortHexNumber(val), symbolfont: font }));
|
||||
} else {
|
||||
this.root.push(new CheckboxSymbolAttributes({ val }));
|
||||
}
|
||||
}
|
||||
}
|
85
src/file/checkbox/checkbox-util.spec.ts
Normal file
85
src/file/checkbox/checkbox-util.spec.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Formatter } from "@export/formatter";
|
||||
import { CheckBoxUtil } from ".";
|
||||
|
||||
describe("CheckBoxUtil", () => {
|
||||
describe("#constructor()", () => {
|
||||
it("should create a CheckBoxUtil with proper root and default values", () => {
|
||||
const checkBoxUtil = new CheckBoxUtil();
|
||||
|
||||
const tree = new Formatter().format(checkBoxUtil);
|
||||
|
||||
expect(tree).to.deep.equal({
|
||||
"w14:checkbox": [
|
||||
{
|
||||
"w14:checked": {
|
||||
_attr: {
|
||||
"w14:val": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w14:checkedState": {
|
||||
_attr: {
|
||||
"w14:font": "MS Gothic",
|
||||
"w14:val": "2612",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w14:uncheckedState": {
|
||||
_attr: {
|
||||
"w14:font": "MS Gothic",
|
||||
"w14:val": "2610",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a CheckBoxUtil with proper structure and custom values", () => {
|
||||
const checkBoxUtil = new CheckBoxUtil({
|
||||
checked: true,
|
||||
checkedState: {
|
||||
value: "2713",
|
||||
font: "Segoe UI Symbol",
|
||||
},
|
||||
uncheckedState: {
|
||||
value: "2705",
|
||||
font: "Segoe UI Symbol",
|
||||
},
|
||||
});
|
||||
|
||||
const tree = new Formatter().format(checkBoxUtil);
|
||||
|
||||
expect(tree).to.deep.equal({
|
||||
"w14:checkbox": [
|
||||
{
|
||||
"w14:checked": {
|
||||
_attr: {
|
||||
"w14:val": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w14:checkedState": {
|
||||
_attr: {
|
||||
"w14:font": "Segoe UI Symbol",
|
||||
"w14:val": "2713",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w14:uncheckedState": {
|
||||
_attr: {
|
||||
"w14:font": "Segoe UI Symbol",
|
||||
"w14:val": "2705",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
45
src/file/checkbox/checkbox-util.ts
Normal file
45
src/file/checkbox/checkbox-util.ts
Normal file
@ -0,0 +1,45 @@
|
||||
// <xsd:complexType name="CT_SdtCheckbox">
|
||||
// <xsd:sequence>
|
||||
// <xsd:element name="checked" type="CT_OnOff" minOccurs="0"/>
|
||||
// <xsd:element name="checkedState" type="CT_SdtCheckboxSymbol" minOccurs="0"/>
|
||||
// <xsd:element name="uncheckedState" type="CT_SdtCheckboxSymbol" minOccurs="0"/>
|
||||
// </xsd:sequence>
|
||||
// </xsd:complexType>
|
||||
// <xsd:element name="checkbox" type="CT_SdtCheckbox"/>
|
||||
|
||||
import { XmlComponent } from "@file/xml-components";
|
||||
import { CheckBoxSymbolElement } from "@file/checkbox/checkbox-symbol";
|
||||
|
||||
export interface ICheckboxSymbolProperties {
|
||||
readonly value?: string;
|
||||
readonly font?: string;
|
||||
}
|
||||
|
||||
export interface ICheckboxSymbolOptions {
|
||||
readonly alias?: string;
|
||||
readonly checked?: boolean;
|
||||
readonly checkedState?: ICheckboxSymbolProperties;
|
||||
readonly uncheckedState?: ICheckboxSymbolProperties;
|
||||
}
|
||||
|
||||
export class CheckBoxUtil extends XmlComponent {
|
||||
private readonly DEFAULT_UNCHECKED_SYMBOL: string = "2610";
|
||||
private readonly DEFAULT_CHECKED_SYMBOL: string = "2612";
|
||||
private readonly DEFAULT_FONT: string = "MS Gothic";
|
||||
public constructor(options?: ICheckboxSymbolOptions) {
|
||||
super("w14:checkbox");
|
||||
|
||||
const value = options?.checked ? "1" : "0";
|
||||
let symbol: string;
|
||||
let font: string;
|
||||
this.root.push(new CheckBoxSymbolElement("w14:checked", value));
|
||||
|
||||
symbol = options?.checkedState?.value ? options?.checkedState?.value : this.DEFAULT_CHECKED_SYMBOL;
|
||||
font = options?.checkedState?.font ? options?.checkedState?.font : this.DEFAULT_FONT;
|
||||
this.root.push(new CheckBoxSymbolElement("w14:checkedState", symbol, font));
|
||||
|
||||
symbol = options?.uncheckedState?.value ? options?.uncheckedState?.value : this.DEFAULT_UNCHECKED_SYMBOL;
|
||||
font = options?.uncheckedState?.font ? options?.uncheckedState?.font : this.DEFAULT_FONT;
|
||||
this.root.push(new CheckBoxSymbolElement("w14:uncheckedState", symbol, font));
|
||||
}
|
||||
}
|
213
src/file/checkbox/checkbox.spec.ts
Normal file
213
src/file/checkbox/checkbox.spec.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Formatter } from "@export/formatter";
|
||||
|
||||
import { CheckBox } from "./checkbox";
|
||||
|
||||
describe("CheckBox", () => {
|
||||
describe("#constructor()", () => {
|
||||
it("should create a CheckBox with proper root and default values (no alias, no custom state)", () => {
|
||||
const checkBox = new CheckBox();
|
||||
|
||||
const tree = new Formatter().format(checkBox);
|
||||
|
||||
expect(tree).to.deep.equal({
|
||||
"w:sdt": [
|
||||
{
|
||||
"w:sdtPr": [
|
||||
{
|
||||
"w14:checkbox": [
|
||||
{
|
||||
"w14:checked": {
|
||||
_attr: {
|
||||
"w14:val": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w14:checkedState": {
|
||||
_attr: {
|
||||
"w14:font": "MS Gothic",
|
||||
"w14:val": "2612",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w14:uncheckedState": {
|
||||
_attr: {
|
||||
"w14:font": "MS Gothic",
|
||||
"w14:val": "2610",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"w:sdtContent": [
|
||||
{
|
||||
"w:r": [
|
||||
{
|
||||
"w:sym": {
|
||||
_attr: {
|
||||
"w:char": "2610",
|
||||
"w:font": "MS Gothic",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["2713", "Segoe UI Symbol", "2713", "Segoe UI Symbol"],
|
||||
[undefined, undefined, "2612", "MS Gothic"],
|
||||
])("should create a CheckBox with proper root and custom values", (inputChar, inputFont, actualChar, actualFont) => {
|
||||
const checkBox = new CheckBox({
|
||||
alias: "Custom Checkbox",
|
||||
checked: true,
|
||||
checkedState: {
|
||||
value: inputChar,
|
||||
font: inputFont,
|
||||
},
|
||||
uncheckedState: {
|
||||
value: "2705",
|
||||
font: "Segoe UI Symbol",
|
||||
},
|
||||
});
|
||||
|
||||
const tree = new Formatter().format(checkBox);
|
||||
|
||||
expect(tree).to.deep.equal({
|
||||
"w:sdt": [
|
||||
{
|
||||
"w:sdtPr": [
|
||||
{
|
||||
"w:alias": {
|
||||
_attr: {
|
||||
"w:val": "Custom Checkbox",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w14:checkbox": [
|
||||
{
|
||||
"w14:checked": {
|
||||
_attr: {
|
||||
"w14:val": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w14:checkedState": {
|
||||
_attr: {
|
||||
"w14:font": actualFont,
|
||||
"w14:val": actualChar,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w14:uncheckedState": {
|
||||
_attr: {
|
||||
"w14:font": "Segoe UI Symbol",
|
||||
"w14:val": "2705",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"w:sdtContent": [
|
||||
{
|
||||
"w:r": [
|
||||
{
|
||||
"w:sym": {
|
||||
_attr: {
|
||||
"w:char": actualChar,
|
||||
"w:font": actualFont,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a CheckBox with proper root, custom state, and no alias", () => {
|
||||
const checkBox = new CheckBox({
|
||||
checked: false,
|
||||
checkedState: {
|
||||
value: "2713",
|
||||
font: "Segoe UI Symbol",
|
||||
},
|
||||
uncheckedState: {
|
||||
value: "2705",
|
||||
font: "Segoe UI Symbol",
|
||||
},
|
||||
});
|
||||
|
||||
const tree = new Formatter().format(checkBox);
|
||||
|
||||
expect(tree).to.deep.equal({
|
||||
"w:sdt": [
|
||||
{
|
||||
"w:sdtPr": [
|
||||
{
|
||||
"w14:checkbox": [
|
||||
{
|
||||
"w14:checked": {
|
||||
_attr: {
|
||||
"w14:val": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w14:checkedState": {
|
||||
_attr: {
|
||||
"w14:font": "Segoe UI Symbol",
|
||||
"w14:val": "2713",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w14:uncheckedState": {
|
||||
_attr: {
|
||||
"w14:font": "Segoe UI Symbol",
|
||||
"w14:val": "2705",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"w:sdtContent": [
|
||||
{
|
||||
"w:r": [
|
||||
{
|
||||
"w:sym": {
|
||||
_attr: {
|
||||
"w:char": "2705",
|
||||
"w:font": "Segoe UI Symbol",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
43
src/file/checkbox/checkbox.ts
Normal file
43
src/file/checkbox/checkbox.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { SymbolRun } from "@file/paragraph/run/symbol-run";
|
||||
import { StructuredDocumentTagProperties } from "@file/table-of-contents/sdt-properties";
|
||||
import { StructuredDocumentTagContent } from "@file/table-of-contents/sdt-content";
|
||||
import { XmlComponent } from "@file/xml-components";
|
||||
import { CheckBoxUtil, ICheckboxSymbolOptions } from "./checkbox-util";
|
||||
|
||||
export class CheckBox extends XmlComponent {
|
||||
// default values per Microsoft
|
||||
private readonly DEFAULT_UNCHECKED_SYMBOL: string = "2610";
|
||||
private readonly DEFAULT_CHECKED_SYMBOL: string = "2612";
|
||||
private readonly DEFAULT_FONT: string = "MS Gothic";
|
||||
public constructor(options?: ICheckboxSymbolOptions) {
|
||||
super("w:sdt");
|
||||
|
||||
const properties = new StructuredDocumentTagProperties(options?.alias);
|
||||
properties.addChildElement(new CheckBoxUtil(options));
|
||||
this.root.push(properties);
|
||||
|
||||
const content = new StructuredDocumentTagContent();
|
||||
const checkedFont: string | undefined = options?.checkedState?.font;
|
||||
const checkedText: string | undefined = options?.checkedState?.value;
|
||||
const uncheckedFont: string | undefined = options?.uncheckedState?.font;
|
||||
const uncheckedText: string | undefined = options?.uncheckedState?.value;
|
||||
let symbolFont: string;
|
||||
let char: string;
|
||||
|
||||
if (options?.checked) {
|
||||
symbolFont = checkedFont ? checkedFont : this.DEFAULT_FONT;
|
||||
char = checkedText ? checkedText : this.DEFAULT_CHECKED_SYMBOL;
|
||||
} else {
|
||||
symbolFont = uncheckedFont ? uncheckedFont : this.DEFAULT_FONT;
|
||||
char = uncheckedText ? uncheckedText : this.DEFAULT_UNCHECKED_SYMBOL;
|
||||
}
|
||||
|
||||
const initialRenderedChar = new SymbolRun({
|
||||
char: char,
|
||||
symbolfont: symbolFont,
|
||||
});
|
||||
|
||||
content.addChildElement(initialRenderedChar);
|
||||
this.root.push(content);
|
||||
}
|
||||
}
|
3
src/file/checkbox/index.ts
Normal file
3
src/file/checkbox/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./checkbox-util";
|
||||
export * from "./checkbox-symbol";
|
||||
export * from "./checkbox";
|
@ -5,7 +5,6 @@ import { FooterWrapper } from "@file/footer-wrapper";
|
||||
import { HeaderWrapper } from "@file/header-wrapper";
|
||||
import { VerticalAlign, VerticalAlignElement } from "@file/vertical-align";
|
||||
import { OnOffElement, XmlComponent } from "@file/xml-components";
|
||||
import { PositiveUniversalMeasure, UniversalMeasure } from "@util/values";
|
||||
|
||||
import { HeaderFooterReference, HeaderFooterReferenceType, HeaderFooterType } from "./properties/header-footer-reference";
|
||||
import { Columns, IColumnsAttributes } from "./properties/columns";
|
||||
@ -76,10 +75,10 @@ export interface ISectionPropertiesOptions {
|
||||
// </xsd:group>
|
||||
|
||||
export const sectionMarginDefaults = {
|
||||
TOP: "1in" as UniversalMeasure,
|
||||
RIGHT: "1in" as PositiveUniversalMeasure,
|
||||
BOTTOM: "1in" as UniversalMeasure,
|
||||
LEFT: "1in" as PositiveUniversalMeasure,
|
||||
TOP: 1440,
|
||||
RIGHT: 1440,
|
||||
BOTTOM: 1440,
|
||||
LEFT: 1440,
|
||||
HEADER: 708,
|
||||
FOOTER: 708,
|
||||
GUTTER: 0,
|
||||
|
@ -80,6 +80,7 @@ export class Document extends XmlComponent {
|
||||
}
|
||||
|
||||
public add(item: Paragraph | Table | TableOfContents | ConcreteHyperlink): Document {
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
this.body.push(item);
|
||||
return this;
|
||||
}
|
||||
|
@ -17,7 +17,11 @@ export class FooterWrapper implements IViewWrapper {
|
||||
private readonly footer: Footer;
|
||||
private readonly relationships: Relationships;
|
||||
|
||||
public constructor(private readonly media: Media, referenceId: number, initContent?: XmlComponent) {
|
||||
public constructor(
|
||||
private readonly media: Media,
|
||||
referenceId: number,
|
||||
initContent?: XmlComponent,
|
||||
) {
|
||||
this.footer = new Footer(referenceId, initContent);
|
||||
this.relationships = new Relationships();
|
||||
}
|
||||
|
@ -17,7 +17,11 @@ export class HeaderWrapper implements IViewWrapper {
|
||||
private readonly header: Header;
|
||||
private readonly relationships: Relationships;
|
||||
|
||||
public constructor(private readonly media: Media, referenceId: number, initContent?: XmlComponent) {
|
||||
public constructor(
|
||||
private readonly media: Media,
|
||||
referenceId: number,
|
||||
initContent?: XmlComponent,
|
||||
) {
|
||||
this.header = new Header(referenceId, initContent);
|
||||
this.relationships = new Relationships();
|
||||
}
|
||||
|
@ -17,3 +17,4 @@ export * from "./track-revision";
|
||||
export * from "./shared";
|
||||
export * from "./border";
|
||||
export * from "./vertical-align";
|
||||
export * from "./checkbox";
|
||||
|
@ -216,7 +216,7 @@ export class Numbering extends XmlComponent {
|
||||
abstractNumId: abstractNumbering.id,
|
||||
reference,
|
||||
instance,
|
||||
overrideLevel:
|
||||
overrideLevels: [
|
||||
firstLevelStartNumber && Number.isInteger(firstLevelStartNumber)
|
||||
? {
|
||||
num: 0,
|
||||
@ -226,6 +226,7 @@ export class Numbering extends XmlComponent {
|
||||
num: 0,
|
||||
start: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
this.concreteNumberingMap.set(fullReference, new ConcreteNumbering(concreteNumberingSettings));
|
||||
|
@ -6,6 +6,7 @@ import { FileChild } from "@file/file-child";
|
||||
|
||||
import { TargetModeType } from "../relationships/relationship/relationship";
|
||||
import { DeletedTextRun, InsertedTextRun } from "../track-revision";
|
||||
import { CheckBox } from "../checkbox";
|
||||
import { ColumnBreak, PageBreak } from "./formatting/break";
|
||||
import { Bookmark, ConcreteHyperlink, ExternalHyperlink, InternalHyperlink } from "./links";
|
||||
import { Math } from "./math";
|
||||
@ -33,7 +34,8 @@ export type ParagraphChild =
|
||||
| Comment
|
||||
| CommentRangeStart
|
||||
| CommentRangeEnd
|
||||
| CommentReference;
|
||||
| CommentReference
|
||||
| CheckBox;
|
||||
|
||||
export interface IParagraphOptions extends IParagraphPropertiesOptions {
|
||||
readonly text?: string;
|
||||
|
@ -1,5 +1,6 @@
|
||||
// http://officeopenxml.com/WPparagraphProperties.php
|
||||
// https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_suppressLineNumbers_topic_ID0ECJAO.html
|
||||
/* eslint-disable functional/immutable-data */
|
||||
import { IContext, IgnoreIfEmptyXmlComponent, IXmlableObject, OnOffElement, XmlComponent } from "@file/xml-components";
|
||||
import { DocumentWrapper } from "../document-wrapper";
|
||||
import { IShadingAttributesProperties, Shading } from "../shading";
|
||||
|
@ -1,4 +1,5 @@
|
||||
// https://www.ecma-international.org/wp-content/uploads/ECMA-376-1_5th_edition_december_2016.zip page 297, section 17.3.2.21
|
||||
/* eslint-disable functional/immutable-data */
|
||||
import { BorderElement, IBorderOptions } from "@file/border";
|
||||
import { IShadingAttributesProperties, Shading } from "@file/shading";
|
||||
import { ChangeAttributes, IChangedAttributesProperties } from "@file/track-revision/track-revision";
|
||||
|
@ -318,4 +318,45 @@ describe("Default Styles", () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("HyperlinkStyle#constructor", () => {
|
||||
const style = new defaultStyles.HyperlinkStyle({
|
||||
run: {
|
||||
color: "FF0000",
|
||||
underline: {
|
||||
color: "0000FF",
|
||||
},
|
||||
},
|
||||
});
|
||||
const tree = new Formatter().format(style);
|
||||
expect(tree).to.deep.equal({
|
||||
"w:style": [
|
||||
{ _attr: { "w:type": "character", "w:styleId": "Hyperlink" } },
|
||||
{ "w:name": { _attr: { "w:val": "Hyperlink" } } },
|
||||
{ "w:basedOn": { _attr: { "w:val": "DefaultParagraphFont" } } },
|
||||
{
|
||||
"w:uiPriority": {
|
||||
_attr: {
|
||||
"w:val": 99,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"w:unhideWhenUsed": EMPTY_OBJECT,
|
||||
},
|
||||
{
|
||||
"w:rPr": [
|
||||
{ "w:u": { _attr: { "w:color": "0000FF", "w:val": "single" } } },
|
||||
{
|
||||
"w:color": {
|
||||
_attr: {
|
||||
"w:val": "FF0000",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -8,10 +8,10 @@ import { IBaseParagraphStyleOptions, IParagraphStyleOptions, StyleForParagraph }
|
||||
export class HeadingStyle extends StyleForParagraph {
|
||||
public constructor(options: IParagraphStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
basedOn: "Normal",
|
||||
next: "Normal",
|
||||
quickFormat: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -19,9 +19,9 @@ export class HeadingStyle extends StyleForParagraph {
|
||||
export class TitleStyle extends HeadingStyle {
|
||||
public constructor(options: IBaseParagraphStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
id: "Title",
|
||||
name: "Title",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -29,9 +29,9 @@ export class TitleStyle extends HeadingStyle {
|
||||
export class Heading1Style extends HeadingStyle {
|
||||
public constructor(options: IBaseParagraphStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
id: "Heading1",
|
||||
name: "Heading 1",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -39,9 +39,9 @@ export class Heading1Style extends HeadingStyle {
|
||||
export class Heading2Style extends HeadingStyle {
|
||||
public constructor(options: IBaseParagraphStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
id: "Heading2",
|
||||
name: "Heading 2",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -49,9 +49,9 @@ export class Heading2Style extends HeadingStyle {
|
||||
export class Heading3Style extends HeadingStyle {
|
||||
public constructor(options: IBaseParagraphStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
id: "Heading3",
|
||||
name: "Heading 3",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -59,9 +59,9 @@ export class Heading3Style extends HeadingStyle {
|
||||
export class Heading4Style extends HeadingStyle {
|
||||
public constructor(options: IBaseParagraphStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
id: "Heading4",
|
||||
name: "Heading 4",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -69,9 +69,9 @@ export class Heading4Style extends HeadingStyle {
|
||||
export class Heading5Style extends HeadingStyle {
|
||||
public constructor(options: IBaseParagraphStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
id: "Heading5",
|
||||
name: "Heading 5",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -79,9 +79,9 @@ export class Heading5Style extends HeadingStyle {
|
||||
export class Heading6Style extends HeadingStyle {
|
||||
public constructor(options: IBaseParagraphStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
id: "Heading6",
|
||||
name: "Heading 6",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -89,9 +89,9 @@ export class Heading6Style extends HeadingStyle {
|
||||
export class StrongStyle extends HeadingStyle {
|
||||
public constructor(options: IBaseParagraphStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
id: "Strong",
|
||||
name: "Strong",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -99,11 +99,11 @@ export class StrongStyle extends HeadingStyle {
|
||||
export class ListParagraph extends StyleForParagraph {
|
||||
public constructor(options: IBaseParagraphStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
id: "ListParagraph",
|
||||
name: "List Paragraph",
|
||||
basedOn: "Normal",
|
||||
quickFormat: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -111,7 +111,6 @@ export class ListParagraph extends StyleForParagraph {
|
||||
export class FootnoteText extends StyleForParagraph {
|
||||
public constructor(options: IBaseParagraphStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
id: "FootnoteText",
|
||||
name: "footnote text",
|
||||
link: "FootnoteTextChar",
|
||||
@ -129,6 +128,7 @@ export class FootnoteText extends StyleForParagraph {
|
||||
run: {
|
||||
size: 20,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -136,7 +136,6 @@ export class FootnoteText extends StyleForParagraph {
|
||||
export class FootnoteReferenceStyle extends StyleForCharacter {
|
||||
public constructor(options: IBaseCharacterStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
id: "FootnoteReference",
|
||||
name: "footnote reference",
|
||||
basedOn: "DefaultParagraphFont",
|
||||
@ -144,6 +143,7 @@ export class FootnoteReferenceStyle extends StyleForCharacter {
|
||||
run: {
|
||||
superScript: true,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -151,7 +151,6 @@ export class FootnoteReferenceStyle extends StyleForCharacter {
|
||||
export class FootnoteTextChar extends StyleForCharacter {
|
||||
public constructor(options: IBaseCharacterStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
id: "FootnoteTextChar",
|
||||
name: "Footnote Text Char",
|
||||
basedOn: "DefaultParagraphFont",
|
||||
@ -160,6 +159,7 @@ export class FootnoteTextChar extends StyleForCharacter {
|
||||
run: {
|
||||
size: 20,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -167,7 +167,6 @@ export class FootnoteTextChar extends StyleForCharacter {
|
||||
export class HyperlinkStyle extends StyleForCharacter {
|
||||
public constructor(options: IBaseCharacterStyleOptions) {
|
||||
super({
|
||||
...options,
|
||||
id: "Hyperlink",
|
||||
name: "Hyperlink",
|
||||
basedOn: "DefaultParagraphFont",
|
||||
@ -177,6 +176,7 @@ export class HyperlinkStyle extends StyleForCharacter {
|
||||
type: UnderlineType.SINGLE,
|
||||
},
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,11 @@
|
||||
import { StringValueElement, XmlComponent } from "@file/xml-components";
|
||||
|
||||
export class StructuredDocumentTagProperties extends XmlComponent {
|
||||
public constructor(alias: string) {
|
||||
public constructor(alias?: string) {
|
||||
super("w:sdtPr");
|
||||
|
||||
if (alias) {
|
||||
this.root.push(new StringValueElement("w:alias", alias));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ describe("ImportedXmlComponent", () => {
|
||||
otherAttr: "2",
|
||||
};
|
||||
importedXmlComponent = new ImportedXmlComponent("w:test", attributes);
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
importedXmlComponent.push(new ImportedXmlComponent("w:child"));
|
||||
});
|
||||
|
||||
|
@ -21,6 +21,7 @@ export const convertToXmlComponent = (element: XmlElement): ImportedXmlComponent
|
||||
for (const childElm of childElements) {
|
||||
const child = convertToXmlComponent(childElm);
|
||||
if (child !== undefined) {
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
xmlComponent.push(child);
|
||||
}
|
||||
}
|
||||
@ -60,6 +61,7 @@ export class ImportedXmlComponent extends XmlComponent {
|
||||
public constructor(rootKey: string, _attr?: any) {
|
||||
super(rootKey);
|
||||
if (_attr) {
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
this.root.push(new ImportedXmlComponentAttributes(_attr));
|
||||
}
|
||||
}
|
||||
|
@ -20,11 +20,13 @@ describe("XmlComponent", () => {
|
||||
});
|
||||
it("should handle children elements", () => {
|
||||
const xmlComponent = new TestComponent("w:test");
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
xmlComponent.push(
|
||||
new Attributes({
|
||||
val: "test",
|
||||
}),
|
||||
);
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
xmlComponent.push(new TestComponent("innerTest"));
|
||||
|
||||
const tree = new Formatter().format(xmlComponent);
|
||||
@ -43,6 +45,7 @@ describe("XmlComponent", () => {
|
||||
});
|
||||
it("should hoist attrs if only attrs are present", () => {
|
||||
const xmlComponent = new TestComponent("w:test");
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
xmlComponent.push(
|
||||
new Attributes({
|
||||
val: "test",
|
||||
|
@ -1,7 +1,7 @@
|
||||
export interface IXmlAttribute {
|
||||
readonly [key: string]: string | number | boolean;
|
||||
}
|
||||
export interface IXmlableObject extends Object {
|
||||
export interface IXmlableObject extends Record<string, unknown> {
|
||||
// readonly _attr?: IXmlAttribute;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly [key: string]: any;
|
||||
|
@ -47,5 +47,31 @@ describe("content-types-manager", () => {
|
||||
type: "element",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not append duplicate content type", () => {
|
||||
const element = {
|
||||
type: "element",
|
||||
name: "xml",
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "Types",
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "Default",
|
||||
attributes: {
|
||||
ContentType: "image/png",
|
||||
Extension: "png",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
appendContentType(element, "image/png", "png");
|
||||
|
||||
expect(element.elements.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,6 +4,18 @@ import { getFirstLevelElements } from "./util";
|
||||
|
||||
export const appendContentType = (element: Element, contentType: string, extension: string): void => {
|
||||
const relationshipElements = getFirstLevelElements(element, "Types");
|
||||
|
||||
const exist = relationshipElements.some(
|
||||
(el) =>
|
||||
el.type === "element" &&
|
||||
el.name === "Default" &&
|
||||
el?.attributes?.ContentType === contentType &&
|
||||
el?.attributes?.Extension === extension,
|
||||
);
|
||||
if (exist) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
relationshipElements.push({
|
||||
attributes: {
|
||||
|
@ -49,11 +49,12 @@ export type IPatch = ParagraphPatch | FilePatch;
|
||||
|
||||
export interface PatchDocumentOptions {
|
||||
readonly patches: { readonly [key: string]: IPatch };
|
||||
readonly keepOriginalStyles?: boolean;
|
||||
}
|
||||
|
||||
const imageReplacer = new ImageReplacer();
|
||||
|
||||
export const patchDocument = async (data: InputDataType, options: PatchDocumentOptions): Promise<Buffer> => {
|
||||
export const patchDocument = async (data: InputDataType, options: PatchDocumentOptions): Promise<Uint8Array> => {
|
||||
const zipContent = await JSZip.loadAsync(data);
|
||||
const contexts = new Map<string, IContext>();
|
||||
const file = {
|
||||
@ -68,11 +69,11 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
|
||||
const hyperlinkRelationshipAdditions: IHyperlinkRelationshipAddition[] = [];
|
||||
let hasMedia = false;
|
||||
|
||||
const binaryContentMap = new Map<string, Buffer>();
|
||||
const binaryContentMap = new Map<string, Uint8Array>();
|
||||
|
||||
for (const [key, value] of Object.entries(zipContent.files)) {
|
||||
if (!key.endsWith(".xml") && !key.endsWith(".rels")) {
|
||||
binaryContentMap.set(key, await value.async("nodebuffer"));
|
||||
binaryContentMap.set(key, await value.async("uint8array"));
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -128,6 +129,7 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
|
||||
patchText,
|
||||
renderedParagraphs,
|
||||
context,
|
||||
options.keepOriginalStyles,
|
||||
);
|
||||
}
|
||||
|
||||
@ -213,7 +215,7 @@ export const patchDocument = async (data: InputDataType, options: PatchDocumentO
|
||||
}
|
||||
|
||||
return zip.generateAsync({
|
||||
type: "nodebuffer",
|
||||
type: "uint8array",
|
||||
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
compression: "DEFLATE",
|
||||
});
|
||||
|
@ -43,6 +43,7 @@ export const replaceTokenInParagraphElement = ({
|
||||
patchTextElement(paragraphElement.elements![run.index].elements![index], firstPart);
|
||||
replaceMode = ReplaceMode.MIDDLE;
|
||||
continue;
|
||||
/* c8 ignore next 2 */
|
||||
}
|
||||
break;
|
||||
case ReplaceMode.MIDDLE:
|
||||
@ -59,6 +60,7 @@ export const replaceTokenInParagraphElement = ({
|
||||
patchTextElement(paragraphElement.elements![run.index].elements![index], "");
|
||||
}
|
||||
break;
|
||||
/* c8 ignore next */
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,28 @@ const MOCK_JSON = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:p",
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:r",
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:rPr",
|
||||
elements: [{ type: "element", name: "w:b", attributes: { "w:val": "1" } }],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:t",
|
||||
elements: [{ type: "text", text: "What a {{bold}} text!" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -115,6 +137,93 @@ describe("replacer", () => {
|
||||
expect(JSON.stringify(output)).to.contain("Delightful Header");
|
||||
});
|
||||
|
||||
it("should replace paragraph type keeping original styling if keepOriginalStyles is true", () => {
|
||||
const output = replacer(
|
||||
MOCK_JSON,
|
||||
{
|
||||
type: PatchType.PARAGRAPH,
|
||||
children: [new TextRun("sweet")],
|
||||
},
|
||||
"{{bold}}",
|
||||
[
|
||||
{
|
||||
text: "What a {{bold}} text!",
|
||||
runs: [
|
||||
{
|
||||
text: "What a {{bold}} text!",
|
||||
parts: [{ text: "What a {{bold}} text!", index: 1, start: 0, end: 21 }],
|
||||
index: 0,
|
||||
start: 0,
|
||||
end: 21,
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
path: [0, 0, 1],
|
||||
},
|
||||
],
|
||||
{
|
||||
file: {} as unknown as File,
|
||||
viewWrapper: {
|
||||
Relationships: {},
|
||||
} as unknown as IViewWrapper,
|
||||
stack: [],
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(JSON.stringify(output)).to.contain("sweet");
|
||||
expect(output.elements![0].elements![1].elements).toMatchObject([
|
||||
{
|
||||
type: "element",
|
||||
name: "w:r",
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:rPr",
|
||||
elements: [{ type: "element", name: "w:b", attributes: { "w:val": "1" } }],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:t",
|
||||
elements: [{ type: "text", text: "What a " }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:r",
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:rPr",
|
||||
elements: [{ type: "element", name: "w:b", attributes: { "w:val": "1" } }],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:t",
|
||||
elements: [{ type: "text", text: "sweet" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:r",
|
||||
elements: [
|
||||
{
|
||||
type: "element",
|
||||
name: "w:rPr",
|
||||
elements: [{ type: "element", name: "w:b", attributes: { "w:val": "1" } }],
|
||||
},
|
||||
{
|
||||
type: "element",
|
||||
name: "w:t",
|
||||
elements: [{ type: "text", text: " text!" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should replace document type", () => {
|
||||
const output = replacer(
|
||||
MOCK_JSON,
|
||||
|
@ -20,6 +20,7 @@ export const replacer = (
|
||||
patchText: string,
|
||||
renderedParagraphs: readonly IRenderedParagraphNode[],
|
||||
context: IContext,
|
||||
keepOriginalStyles: boolean = false,
|
||||
): Element => {
|
||||
for (const renderedParagraph of renderedParagraphs) {
|
||||
const textJson = patch.children
|
||||
@ -47,9 +48,30 @@ export const replacer = (
|
||||
|
||||
const index = findRunElementIndexWithToken(paragraphElement, SPLIT_TOKEN);
|
||||
|
||||
const { left, right } = splitRunElement(paragraphElement.elements![index], SPLIT_TOKEN);
|
||||
const runElementToBeReplaced = paragraphElement.elements![index];
|
||||
const { left, right } = splitRunElement(runElementToBeReplaced, SPLIT_TOKEN);
|
||||
|
||||
let newRunElements = textJson;
|
||||
let patchedRightElement = right;
|
||||
|
||||
if (keepOriginalStyles) {
|
||||
const runElementNonTextualElements = runElementToBeReplaced.elements!.filter(
|
||||
(e) => e.type === "element" && e.name !== "w:t",
|
||||
);
|
||||
|
||||
newRunElements = textJson.map((e) => ({
|
||||
...e,
|
||||
elements: [...runElementNonTextualElements, ...e.elements!],
|
||||
}));
|
||||
|
||||
patchedRightElement = {
|
||||
...right,
|
||||
elements: [...runElementNonTextualElements, ...right.elements!],
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line functional/immutable-data
|
||||
paragraphElement.elements!.splice(index, 1, left, ...textJson, right);
|
||||
paragraphElement.elements!.splice(index, 1, left, ...newRunElements, patchedRightElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,10 @@ export default defineConfig({
|
||||
tsconfigPaths(),
|
||||
dts(),
|
||||
nodePolyfills({
|
||||
exclude: ["fs"],
|
||||
exclude: [],
|
||||
globals: {
|
||||
Buffer: false,
|
||||
},
|
||||
protocolImports: true,
|
||||
}),
|
||||
],
|
||||
@ -26,7 +29,25 @@ export default defineConfig({
|
||||
lib: {
|
||||
entry: [resolve(__dirname, "src/index.ts")],
|
||||
name: "docx",
|
||||
fileName: "index",
|
||||
fileName: (d) => {
|
||||
if (d === "umd") {
|
||||
return "index.umd.js";
|
||||
}
|
||||
|
||||
if (d === "cjs") {
|
||||
return "index.cjs";
|
||||
}
|
||||
|
||||
if (d === "es") {
|
||||
return "index.mjs";
|
||||
}
|
||||
|
||||
if (d === "iife") {
|
||||
return "index.iife.js";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
},
|
||||
formats: ["iife", "es", "cjs", "umd"],
|
||||
},
|
||||
outDir: resolve(__dirname, "build"),
|
||||
|
Reference in New Issue
Block a user