From c6bb2556419f49f0ea42e774a20f5fa5f7450350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20T=C3=A1borszki?= <46889694+merewif@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:41:31 +0100 Subject: [PATCH] Add hyphenation support (#2678) * Add hyphenation support * Remove unneeded linebreaks * Add documentation and fix eslint * Add tests --------- Co-authored-by: Dolan Miu --- docs/usage/document.md | 37 ++++++---- src/file/core-properties/properties.ts | 2 + .../properties/line-number.spec.ts | 34 ++++++++++ src/file/file.spec.ts | 37 ++++++++++ src/file/file.ts | 6 ++ src/file/settings/settings.spec.ts | 68 +++++++++++++++++++ src/file/settings/settings.ts | 32 +++++++++ vite.config.ts | 6 +- 8 files changed, 206 insertions(+), 16 deletions(-) create mode 100644 src/file/document/body/section-properties/properties/line-number.spec.ts diff --git a/docs/usage/document.md b/docs/usage/document.md index 818930e00e..5d7aff8d55 100644 --- a/docs/usage/document.md +++ b/docs/usage/document.md @@ -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` | 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 diff --git a/src/file/core-properties/properties.ts b/src/file/core-properties/properties.ts index 97ab062316..270326e7c0 100644 --- a/src/file/core-properties/properties.ts +++ b/src/file/core-properties/properties.ts @@ -1,5 +1,6 @@ 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 { dateTimeValue } from "@util/values"; @@ -44,6 +45,7 @@ export type IPropertiesOptions = { readonly evenAndOddHeaderAndFooters?: boolean; readonly defaultTabStop?: number; readonly fonts?: readonly FontOptions[]; + readonly hyphenation?: IHyphenationOptions; }; // diff --git a/src/file/document/body/section-properties/properties/line-number.spec.ts b/src/file/document/body/section-properties/properties/line-number.spec.ts new file mode 100644 index 0000000000..01bf39b237 --- /dev/null +++ b/src/file/document/body/section-properties/properties/line-number.spec.ts @@ -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: {} }, + }); + }); +}); diff --git a/src/file/file.spec.ts b/src/file/file.spec.ts index 7f4cca4ae2..45d41cb7ce 100644 --- a/src/file/file.spec.ts +++ b/src/file/file.spec.ts @@ -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; + }); + }); }); diff --git a/src/file/file.ts b/src/file/file.ts index cc7c5f51d5..82229a2e75 100644 --- a/src/file/file.ts +++ b/src/file/file.ts @@ -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(); diff --git a/src/file/settings/settings.spec.ts b/src/file/settings/settings.spec.ts index 4b647684f5..65a18d0e98 100644 --- a/src/file/settings/settings.spec.ts +++ b/src/file/settings/settings.spec.ts @@ -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({ diff --git a/src/file/settings/settings.ts b/src/file/settings/settings.ts index 393c84ed72..916b692dfc 100644 --- a/src/file/settings/settings.ts +++ b/src/file/settings/settings.ts @@ -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 ?? {}), diff --git a/vite.config.ts b/vite.config.ts index e36159a0bd..bc666ade1c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,8 @@ -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"; export default defineConfig({ plugins: [ @@ -65,7 +65,7 @@ export default defineConfig({ reporter: ["text", "json", "html"], thresholds: { statements: 100, - branches: 99.35, + branches: 99.68, functions: 100, lines: 100, },