Add hyphenation support (#2678)
* Add hyphenation support * Remove unneeded linebreaks * Add documentation and fix eslint * Add tests --------- Co-authored-by: Dolan Miu <dolan_miu@hotmail.com>
This commit is contained in:
@ -22,19 +22,30 @@ const doc = new docx.Document({
|
|||||||
|
|
||||||
### Full list of options:
|
### Full list of options:
|
||||||
|
|
||||||
- creator
|
| Property | Type | Notes |
|
||||||
- description
|
| -------------------------- | -------------------------------------------------------- | -------- |
|
||||||
- title
|
| sections | `ISectionOptions[]` | Optional |
|
||||||
- subject
|
| title | `string` | Optional |
|
||||||
- keywords
|
| subject | `string` | Optional |
|
||||||
- lastModifiedBy
|
| creator | `string` | Optional |
|
||||||
- revision
|
| keywords | `string` | Optional |
|
||||||
- externalStyles
|
| description | `string` | Optional |
|
||||||
- styles
|
| lastModifiedBy | `string` | Optional |
|
||||||
- numbering
|
| revision | `number` | Optional |
|
||||||
- footnotes
|
| externalStyles | `string` | Optional |
|
||||||
- hyperlinks
|
| styles | `IStylesOptions` | Optional |
|
||||||
- background
|
| numbering | `INumberingOptions` | Optional |
|
||||||
|
| comments | `ICommentsOptions` | Optional |
|
||||||
|
| footnotes | `Record<string, { children: Paragraph[] }>` | Optional |
|
||||||
|
| background | `IDocumentBackgroundOptions` | Optional |
|
||||||
|
| features | `{ trackRevisions?: boolean; updateFields?: boolean; }` | Optional |
|
||||||
|
| compatabilityModeVersion | `number` | Optional |
|
||||||
|
| compatibility | `ICompatibilityOptions` | Optional |
|
||||||
|
| customProperties | ` ICustomPropertyOptions`[] | Optional |
|
||||||
|
| evenAndOddHeaderAndFooters | `boolean` | Optional |
|
||||||
|
| defaultTabStop | `number` | Optional |
|
||||||
|
| fonts | ` FontOptions[]` | Optional |
|
||||||
|
| hyphenation | `IHyphenationOptions` | Optional |
|
||||||
|
|
||||||
### Change background color of Document
|
### Change background color of Document
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { FontOptions } from "@file/fonts/font-table";
|
import { FontOptions } from "@file/fonts/font-table";
|
||||||
import { ICommentsOptions } from "@file/paragraph/run/comment-run";
|
import { ICommentsOptions } from "@file/paragraph/run/comment-run";
|
||||||
|
import { IHyphenationOptions } from "@file/settings";
|
||||||
import { ICompatibilityOptions } from "@file/settings/compatibility";
|
import { ICompatibilityOptions } from "@file/settings/compatibility";
|
||||||
import { StringContainer, XmlComponent } from "@file/xml-components";
|
import { StringContainer, XmlComponent } from "@file/xml-components";
|
||||||
import { dateTimeValue } from "@util/values";
|
import { dateTimeValue } from "@util/values";
|
||||||
@ -44,6 +45,7 @@ export type IPropertiesOptions = {
|
|||||||
readonly evenAndOddHeaderAndFooters?: boolean;
|
readonly evenAndOddHeaderAndFooters?: boolean;
|
||||||
readonly defaultTabStop?: number;
|
readonly defaultTabStop?: number;
|
||||||
readonly fonts?: readonly FontOptions[];
|
readonly fonts?: readonly FontOptions[];
|
||||||
|
readonly hyphenation?: IHyphenationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
// <xs:element name="coreProperties" type="CT_CoreProperties"/>
|
// <xs:element name="coreProperties" type="CT_CoreProperties"/>
|
||||||
|
@ -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: {} },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -479,4 +479,41 @@ describe("File", () => {
|
|||||||
expect(doc.Styles).to.not.be.undefined;
|
expect(doc.Styles).to.not.be.undefined;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("#features", () => {
|
||||||
|
it("should work with updateFields", () => {
|
||||||
|
const doc = new File({
|
||||||
|
sections: [],
|
||||||
|
features: {
|
||||||
|
updateFields: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(doc.Styles).to.not.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with trackRevisions", () => {
|
||||||
|
const doc = new File({
|
||||||
|
sections: [],
|
||||||
|
features: {
|
||||||
|
trackRevisions: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(doc.Styles).to.not.be.undefined;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("#hyphenation", () => {
|
||||||
|
it("should work with autoHyphenation", () => {
|
||||||
|
const doc = new File({
|
||||||
|
sections: [],
|
||||||
|
hyphenation: {
|
||||||
|
autoHyphenation: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(doc.Styles).to.not.be.undefined;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -80,6 +80,12 @@ export class File {
|
|||||||
trackRevisions: options.features?.trackRevisions,
|
trackRevisions: options.features?.trackRevisions,
|
||||||
updateFields: options.features?.updateFields,
|
updateFields: options.features?.updateFields,
|
||||||
defaultTabStop: options.defaultTabStop,
|
defaultTabStop: options.defaultTabStop,
|
||||||
|
hyphenation: {
|
||||||
|
autoHyphenation: options.hyphenation?.autoHyphenation,
|
||||||
|
hyphenationZone: options.hyphenation?.hyphenationZone,
|
||||||
|
consecutiveHyphenLimit: options.hyphenation?.consecutiveHyphenLimit,
|
||||||
|
doNotHyphenateCaps: options.hyphenation?.doNotHyphenateCaps,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.media = new Media();
|
this.media = new Media();
|
||||||
|
@ -129,6 +129,74 @@ describe("Settings", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should add autoHyphenation setting", () => {
|
||||||
|
const options = {
|
||||||
|
hyphenation: {
|
||||||
|
autoHyphenation: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tree = new Formatter().format(new Settings(options));
|
||||||
|
expect(Object.keys(tree)).has.length(1);
|
||||||
|
expect(tree["w:settings"]).to.be.an("array");
|
||||||
|
expect(tree["w:settings"]).to.deep.include({
|
||||||
|
"w:autoHyphenation": {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add doNotHyphenateCaps setting", () => {
|
||||||
|
const options = {
|
||||||
|
hyphenation: {
|
||||||
|
doNotHyphenateCaps: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tree = new Formatter().format(new Settings(options));
|
||||||
|
expect(Object.keys(tree)).has.length(1);
|
||||||
|
expect(tree["w:settings"]).to.be.an("array");
|
||||||
|
expect(tree["w:settings"]).to.deep.include({
|
||||||
|
"w:doNotHyphenateCaps": {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add hyphenationZone setting", () => {
|
||||||
|
const options = {
|
||||||
|
hyphenation: {
|
||||||
|
hyphenationZone: 200,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tree = new Formatter().format(new Settings(options));
|
||||||
|
expect(Object.keys(tree)).has.length(1);
|
||||||
|
expect(tree["w:settings"]).to.be.an("array");
|
||||||
|
expect(tree["w:settings"]).to.deep.include({
|
||||||
|
"w:hyphenationZone": {
|
||||||
|
_attr: {
|
||||||
|
"w:val": 200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add consecutiveHyphenLimit setting", () => {
|
||||||
|
const options = {
|
||||||
|
hyphenation: {
|
||||||
|
consecutiveHyphenLimit: 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tree = new Formatter().format(new Settings(options));
|
||||||
|
expect(Object.keys(tree)).has.length(1);
|
||||||
|
expect(tree["w:settings"]).to.be.an("array");
|
||||||
|
expect(tree["w:settings"]).to.deep.include({
|
||||||
|
"w:consecutiveHyphenLimit": {
|
||||||
|
_attr: {
|
||||||
|
"w:val": 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: Remove when deprecating compatibilityModeVersion
|
// TODO: Remove when deprecating compatibilityModeVersion
|
||||||
it("should add compatibility setting with legacy version", () => {
|
it("should add compatibility setting with legacy version", () => {
|
||||||
const settings = new Settings({
|
const settings = new Settings({
|
||||||
|
@ -153,6 +153,18 @@ export type ISettingsOptions = {
|
|||||||
readonly updateFields?: boolean;
|
readonly updateFields?: boolean;
|
||||||
readonly compatibility?: ICompatibilityOptions;
|
readonly compatibility?: ICompatibilityOptions;
|
||||||
readonly defaultTabStop?: number;
|
readonly defaultTabStop?: number;
|
||||||
|
readonly hyphenation?: IHyphenationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IHyphenationOptions = {
|
||||||
|
/** Specifies whether the application automatically hyphenates words as they are typed in the document. */
|
||||||
|
readonly autoHyphenation?: boolean;
|
||||||
|
/** Specifies the minimum number of characters at the beginning of a word before a hyphen can be inserted. */
|
||||||
|
readonly hyphenationZone?: number;
|
||||||
|
/** Specifies the maximum number of consecutive lines that can end with a hyphenated word. */
|
||||||
|
readonly consecutiveHyphenLimit?: number;
|
||||||
|
/** Specifies whether to hyphenate words in all capital letters. */
|
||||||
|
readonly doNotHyphenateCaps?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Settings extends XmlComponent {
|
export class Settings extends XmlComponent {
|
||||||
@ -204,6 +216,26 @@ export class Settings extends XmlComponent {
|
|||||||
this.root.push(new NumberValueElement("w:defaultTabStop", options.defaultTabStop));
|
this.root.push(new NumberValueElement("w:defaultTabStop", options.defaultTabStop));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_autoHyphenation_topic_ID0EFUMX.html
|
||||||
|
if (options.hyphenation?.autoHyphenation !== undefined) {
|
||||||
|
this.root.push(new OnOffElement("w:autoHyphenation", options.hyphenation.autoHyphenation));
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_hyphenationZone_topic_ID0ERI3X.html
|
||||||
|
if (options.hyphenation?.hyphenationZone !== undefined) {
|
||||||
|
this.root.push(new NumberValueElement("w:hyphenationZone", options.hyphenation.hyphenationZone));
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_consecutiveHyphenLim_topic_ID0EQ6RX.html
|
||||||
|
if (options.hyphenation?.consecutiveHyphenLimit !== undefined) {
|
||||||
|
this.root.push(new NumberValueElement("w:consecutiveHyphenLimit", options.hyphenation.consecutiveHyphenLimit));
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_doNotHyphenateCaps_topic_ID0EW4XX.html
|
||||||
|
if (options.hyphenation?.doNotHyphenateCaps !== undefined) {
|
||||||
|
this.root.push(new OnOffElement("w:doNotHyphenateCaps", options.hyphenation.doNotHyphenateCaps));
|
||||||
|
}
|
||||||
|
|
||||||
this.root.push(
|
this.root.push(
|
||||||
new Compatibility({
|
new Compatibility({
|
||||||
...(options.compatibility ?? {}),
|
...(options.compatibility ?? {}),
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { configDefaults, defineConfig } from "vitest/config";
|
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
|
||||||
import dts from "vite-plugin-dts";
|
import dts from "vite-plugin-dts";
|
||||||
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
import { configDefaults, defineConfig } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
@ -65,7 +65,7 @@ export default defineConfig({
|
|||||||
reporter: ["text", "json", "html"],
|
reporter: ["text", "json", "html"],
|
||||||
thresholds: {
|
thresholds: {
|
||||||
statements: 100,
|
statements: 100,
|
||||||
branches: 99.35,
|
branches: 99.68,
|
||||||
functions: 100,
|
functions: 100,
|
||||||
lines: 100,
|
lines: 100,
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user