diff --git a/src/file/index.ts b/src/file/index.ts index 24317b8598..d0c4b62282 100644 --- a/src/file/index.ts +++ b/src/file/index.ts @@ -17,3 +17,4 @@ export * from "./footnotes"; export * from "./track-revision"; export * from "./shared"; export * from "./border"; +export * from "./values"; diff --git a/src/file/paragraph/run/formatting.spec.ts b/src/file/paragraph/run/formatting.spec.ts index 43a1de1fd0..cf590d8cf6 100644 --- a/src/file/paragraph/run/formatting.spec.ts +++ b/src/file/paragraph/run/formatting.spec.ts @@ -2,18 +2,35 @@ import { expect } from "chai"; import { Formatter } from "export/formatter"; -import { Bold } from "./formatting"; +import { CharacterSpacing, Color } from "./formatting"; -describe("Bold", () => { +describe("CharacterSpacing", () => { describe("#constructor()", () => { it("should create", () => { - const currentBold = new Bold(); + const element = new CharacterSpacing(32); - const tree = new Formatter().format(currentBold); + const tree = new Formatter().format(element); expect(tree).to.deep.equal({ - "w:b": { + "w:spacing": { _attr: { - "w:val": true, + "w:val": 32, + }, + }, + }); + }); + }); +}); + +describe("Color", () => { + describe("#constructor()", () => { + it("should create", () => { + const element = new Color("#FFFFFF"); + + const tree = new Formatter().format(element); + expect(tree).to.deep.equal({ + "w:color": { + _attr: { + "w:val": "FFFFFF", }, }, }); diff --git a/src/file/paragraph/run/formatting.ts b/src/file/paragraph/run/formatting.ts index 3f560be6a1..5f735fdfbb 100644 --- a/src/file/paragraph/run/formatting.ts +++ b/src/file/paragraph/run/formatting.ts @@ -1,170 +1,55 @@ +import { hexColorValue, signedTwipsMeasureValue } from "file/values"; import { Attributes, XmlComponent } from "file/xml-components"; -export class Bold extends XmlComponent { - constructor() { - super("w:b"); - this.root.push( - new Attributes({ - val: true, - }), - ); - } -} - -export class BoldComplexScript extends XmlComponent { - constructor() { - super("w:bCs"); - this.root.push( - new Attributes({ - val: true, - }), - ); - } -} - export class CharacterSpacing extends XmlComponent { - constructor(value: number) { + constructor(value: number | string) { super("w:spacing"); this.root.push( new Attributes({ - val: value, - }), - ); - } -} - -export class Italics extends XmlComponent { - constructor() { - super("w:i"); - this.root.push( - new Attributes({ - val: true, - }), - ); - } -} - -export class ItalicsComplexScript extends XmlComponent { - constructor() { - super("w:iCs"); - this.root.push( - new Attributes({ - val: true, - }), - ); - } -} - -export class Caps extends XmlComponent { - constructor() { - super("w:caps"); - this.root.push( - new Attributes({ - val: true, + val: signedTwipsMeasureValue(value), }), ); } } +// +// +// +// +// +// export class Color extends XmlComponent { constructor(color: string) { super("w:color"); this.root.push( new Attributes({ - val: color, - }), - ); - } -} - -export class DoubleStrike extends XmlComponent { - constructor() { - super("w:dstrike"); - this.root.push( - new Attributes({ - val: true, - }), - ); - } -} - -export class Emboss extends XmlComponent { - constructor() { - super("w:emboss"); - this.root.push( - new Attributes({ - val: true, - }), - ); - } -} - -export class Imprint extends XmlComponent { - constructor() { - super("w:imprint"); - this.root.push( - new Attributes({ - val: true, - }), - ); - } -} - -export class SmallCaps extends XmlComponent { - constructor() { - super("w:smallCaps"); - this.root.push( - new Attributes({ - val: true, - }), - ); - } -} - -export class Strike extends XmlComponent { - constructor() { - super("w:strike"); - this.root.push( - new Attributes({ - val: true, - }), - ); - } -} - -export class Size extends XmlComponent { - constructor(size: number) { - super("w:sz"); - this.root.push( - new Attributes({ - val: size, - }), - ); - } -} - -export class SizeComplexScript extends XmlComponent { - constructor(size: number) { - super("w:szCs"); - this.root.push( - new Attributes({ - val: size, - }), - ); - } -} - -export class RightToLeft extends XmlComponent { - constructor() { - super("w:rtl"); - this.root.push( - new Attributes({ - val: true, + val: hexColorValue(color), }), ); } } +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// export class Highlight extends XmlComponent { constructor(color: string) { super("w:highlight"); diff --git a/src/file/paragraph/run/properties.ts b/src/file/paragraph/run/properties.ts index 73f5cb531e..277c540619 100644 --- a/src/file/paragraph/run/properties.ts +++ b/src/file/paragraph/run/properties.ts @@ -1,25 +1,7 @@ import { IShadingAttributesProperties, Shading } from "file/shading"; -import { IgnoreIfEmptyXmlComponent, XmlComponent } from "file/xml-components"; +import { HpsMeasureElement, IgnoreIfEmptyXmlComponent, OnOffElement, XmlComponent } from "file/xml-components"; import { EmphasisMark, EmphasisMarkType } from "./emphasis-mark"; -import { - Bold, - BoldComplexScript, - Caps, - CharacterSpacing, - Color, - DoubleStrike, - Emboss, - Highlight, - HighlightComplexScript, - Imprint, - Italics, - ItalicsComplexScript, - RightToLeft, - Size, - SizeComplexScript, - SmallCaps, - Strike, -} from "./formatting"; +import { CharacterSpacing, Color, Highlight, HighlightComplexScript } from "./formatting"; import { IFontAttributesProperties, RunFonts } from "./run-fonts"; import { SubScript, SuperScript } from "./script"; import { Style } from "./style"; @@ -43,8 +25,8 @@ export interface IRunStylePropertiesOptions { readonly type?: EmphasisMarkType; }; readonly color?: string; - readonly size?: number; - readonly sizeComplexScript?: boolean | number; + readonly size?: number | string; + readonly sizeComplexScript?: boolean | number | string; readonly rightToLeft?: boolean; readonly smallCaps?: boolean; readonly allCaps?: boolean; @@ -65,6 +47,49 @@ export interface IRunPropertiesOptions extends IRunStylePropertiesOptions { readonly style?: string; } +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// export class RunProperties extends IgnoreIfEmptyXmlComponent { constructor(options?: IRunPropertiesOptions) { super("w:rPr"); @@ -74,17 +99,17 @@ export class RunProperties extends IgnoreIfEmptyXmlComponent { } if (options.bold) { - this.push(new Bold()); + this.push(new OnOffElement("w:b")); } if ((options.boldComplexScript === undefined && options.bold) || options.boldComplexScript) { - this.push(new BoldComplexScript()); + this.push(new OnOffElement("w:bCs")); } if (options.italics) { - this.push(new Italics()); + this.push(new OnOffElement("w:i")); } if ((options.italicsComplexScript === undefined && options.italics) || options.italicsComplexScript) { - this.push(new ItalicsComplexScript()); + this.push(new OnOffElement("w:iCs")); } if (options.underline) { @@ -100,32 +125,31 @@ export class RunProperties extends IgnoreIfEmptyXmlComponent { } if (options.size) { - this.push(new Size(options.size)); + this.push(new HpsMeasureElement("w:sz", options.size)); } const szCs = options.sizeComplexScript === undefined || options.sizeComplexScript === true ? options.size : options.sizeComplexScript; if (szCs) { - this.push(new SizeComplexScript(szCs)); + this.push(new HpsMeasureElement("w:szCs", szCs)); } if (options.rightToLeft) { - this.push(new RightToLeft()); + this.push(new OnOffElement("w:rtl")); } + // These two are mutually exclusive if (options.smallCaps) { - this.push(new SmallCaps()); - } - - if (options.allCaps) { - this.push(new Caps()); + this.push(new OnOffElement("w:smallCaps")); + } else if (options.allCaps) { + this.push(new OnOffElement("w:caps")); } if (options.strike) { - this.push(new Strike()); + this.push(new OnOffElement("w:strike")); } if (options.doubleStrike) { - this.push(new DoubleStrike()); + this.push(new OnOffElement("w:dstrike")); } if (options.subScript) { @@ -166,11 +190,11 @@ export class RunProperties extends IgnoreIfEmptyXmlComponent { } if (options.emboss) { - this.push(new Emboss()); + this.push(new OnOffElement("w:emboss")); } if (options.imprint) { - this.push(new Imprint()); + this.push(new OnOffElement("w:imprint")); } if (options.shading) { diff --git a/src/file/paragraph/run/strike.spec.ts b/src/file/paragraph/run/strike.spec.ts deleted file mode 100644 index 73c7e853c8..0000000000 --- a/src/file/paragraph/run/strike.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { expect } from "chai"; - -import { Formatter } from "export/formatter"; - -import { DoubleStrike, Strike } from "./formatting"; - -describe("Strike", () => { - let strike: Strike; - - beforeEach(() => { - strike = new Strike(); - }); - - describe("#constructor()", () => { - it("should create a Strike with correct root key", () => { - const tree = new Formatter().format(strike); - expect(tree).to.deep.equal({ - "w:strike": { - _attr: { - "w:val": true, - }, - }, - }); - }); - }); -}); - -describe("DoubleStrike", () => { - let strike: DoubleStrike; - - beforeEach(() => { - strike = new DoubleStrike(); - }); - - describe("#constructor()", () => { - it("should create a Double Strike with correct root key", () => { - const tree = new Formatter().format(strike); - expect(tree).to.deep.equal({ - "w:dstrike": { - _attr: { - "w:val": true, - }, - }, - }); - }); - }); -}); diff --git a/src/file/paragraph/run/symbol-run.spec.ts b/src/file/paragraph/run/symbol-run.spec.ts index c41e6e1d51..8c6eb8e63c 100644 --- a/src/file/paragraph/run/symbol-run.spec.ts +++ b/src/file/paragraph/run/symbol-run.spec.ts @@ -49,7 +49,7 @@ describe("SymbolRun", () => { emphasisMark: { type: EmphasisMarkType.DOT, }, - color: "green", + color: "00FF00", size: 40, highlight: "yellow", }); @@ -65,7 +65,7 @@ describe("SymbolRun", () => { { "w:iCs": { _attr: { "w:val": true } } }, { "w:u": { _attr: { "w:val": "double", "w:color": "red" } } }, { "w:em": { _attr: { "w:val": "dot" } } }, - { "w:color": { _attr: { "w:val": "green" } } }, + { "w:color": { _attr: { "w:val": "00FF00" } } }, { "w:sz": { _attr: { "w:val": 40 } } }, { "w:szCs": { _attr: { "w:val": 40 } } }, { "w:highlight": { _attr: { "w:val": "yellow" } } }, diff --git a/src/file/paragraph/run/underline.ts b/src/file/paragraph/run/underline.ts index 9cdb7b25a1..9433989d8f 100644 --- a/src/file/paragraph/run/underline.ts +++ b/src/file/paragraph/run/underline.ts @@ -21,7 +21,7 @@ export enum UnderlineType { } export abstract class BaseUnderline extends XmlComponent { - constructor(underlineType: string, color?: string) { + constructor(underlineType: UnderlineType, color?: string) { super("w:u"); this.root.push( new Attributes({ @@ -40,96 +40,96 @@ export class Underline extends BaseUnderline { export class DashUnderline extends BaseUnderline { constructor() { - super("dash"); + super(UnderlineType.DASH); } } export class DashDotDotHeavyUnderline extends BaseUnderline { constructor() { - super("dashDotDotHeavy"); + super(UnderlineType.DASHDOTDOTHEAVY); } } export class DashDotHeavyUnderline extends BaseUnderline { constructor() { - super("dashDotHeavy"); + super(UnderlineType.DASHDOTHEAVY); } } export class DashLongUnderline extends BaseUnderline { constructor() { - super("dashLong"); + super(UnderlineType.DASHLONG); } } export class DashLongHeavyUnderline extends BaseUnderline { constructor() { - super("dashLongHeavy"); + super(UnderlineType.DASHLONGHEAVY); } } export class DotDashUnderline extends BaseUnderline { constructor() { - super("dotDash"); + super(UnderlineType.DOTDASH); } } export class DotDotDashUnderline extends BaseUnderline { constructor() { - super("dotDotDash"); + super(UnderlineType.DOTDOTDASH); } } export class DottedUnderline extends BaseUnderline { constructor() { - super("dotted"); + super(UnderlineType.DOTTED); } } export class DottedHeavyUnderline extends BaseUnderline { constructor() { - super("dottedHeavy"); + super(UnderlineType.DOTTEDHEAVY); } } export class DoubleUnderline extends BaseUnderline { constructor() { - super("double"); + super(UnderlineType.DOUBLE); } } export class SingleUnderline extends BaseUnderline { constructor() { - super("single"); + super(UnderlineType.SINGLE); } } export class ThickUnderline extends BaseUnderline { constructor() { - super("thick"); + super(UnderlineType.THICK); } } export class WaveUnderline extends BaseUnderline { constructor() { - super("wave"); + super(UnderlineType.WAVE); } } export class WavyDoubleUnderline extends BaseUnderline { constructor() { - super("wavyDouble"); + super(UnderlineType.WAVYDOUBLE); } } export class WavyHeavyUnderline extends BaseUnderline { constructor() { - super("wavyHeavy"); + super(UnderlineType.WAVYHEAVY); } } export class WordsUnderline extends BaseUnderline { constructor() { - super("words"); + super(UnderlineType.WORDS); } } diff --git a/src/file/shading/shading.ts b/src/file/shading/shading.ts index e00cedd5de..89f1cb49e0 100644 --- a/src/file/shading/shading.ts +++ b/src/file/shading/shading.ts @@ -18,6 +18,7 @@ // // import { XmlAttributeComponent, XmlComponent } from "file/xml-components"; +import { hexColorValue } from "../values"; export interface IShadingAttributesProperties { readonly fill?: string; @@ -34,9 +35,15 @@ class ShadingAttributes extends XmlAttributeComponent { it("sets shading", () => { const properties = new TableCellProperties({ shading: { - fill: "test", - color: "000", + fill: "ffffff", + color: "000000", }, }); const tree = new Formatter().format(properties); - expect(tree).to.deep.equal({ "w:tcPr": [{ "w:shd": { _attr: { "w:fill": "test", "w:color": "000" } } }] }); + expect(tree).to.deep.equal({ "w:tcPr": [{ "w:shd": { _attr: { "w:fill": "ffffff", "w:color": "000000" } } }] }); }); it("should set the TableCellBorders", () => { diff --git a/src/file/table/table-cell/table-cell.spec.ts b/src/file/table/table-cell/table-cell.spec.ts index 51b452f347..f5ed48b97a 100644 --- a/src/file/table/table-cell/table-cell.spec.ts +++ b/src/file/table/table-cell/table-cell.spec.ts @@ -432,8 +432,8 @@ describe("TableCell", () => { const cell = new TableCell({ children: [], shading: { - fill: "red", - color: "blue", + fill: "FF0000", + color: "0000ff", val: ShadingType.PERCENT_10, }, }); @@ -447,8 +447,8 @@ describe("TableCell", () => { { "w:shd": { _attr: { - "w:color": "blue", - "w:fill": "red", + "w:color": "0000ff", + "w:fill": "FF0000", "w:val": "pct10", }, }, diff --git a/src/file/values.ts b/src/file/values.ts new file mode 100644 index 0000000000..a13b92c43d --- /dev/null +++ b/src/file/values.ts @@ -0,0 +1,94 @@ +// Runtime checks and cleanup for value types in the spec that aren't easily expressed through our type system. +// These will help us to prevent silent failures and corrupted documents. + +// +// +// +// +// +export function universalMeasureValue(val: string): string { + const unit = val.slice(-2); + if (!universalMeasureUnits.includes(unit)) { + throw new Error(`Invalid unit '${unit}' specified. Valid units are ${universalMeasureUnits.join(", ")}`); + } + const amount = val.substring(0, val.length - 2); + if (isNaN(Number(amount))) { + throw new Error(`Invalid value '${amount}' specified. Expected a valid number.`); + } + return val; +} +const universalMeasureUnits = ["mm", "cm", "in", "pt", "pc", "pi"]; + +// +// +// +// +// +export function positiveUniversalMeasureValue(val: string): string { + const value = universalMeasureValue(val); + if (parseInt(value, 10) < 0) { + throw new Error(`Invalid value '${value}' specified. Expected a positive number.`); + } + return value; +} + +// +// +// +// +// +// +// +// + +// The xsd:hexBinary type represents binary data as a sequence of binary octets. +// It uses hexadecimal encoding, where each binary octet is a two-character hexadecimal number. +// Lowercase and uppercase letters A through F are permitted. For example, 0FB8 and 0fb8 are two +// equal xsd:hexBinary representations consisting of two octets. +// +// +// +// +// +export function hexColorValue(val: string): string { + if (val === "auto") { + return val; + } + // It's super common to see colors prefixed with a pound, but technically invalid here. + // Most clients work with it, but strip it off anyway for strict compliance. + const color = val.charAt(0) === "#" ? val.substring(1) : val; + if (color.length !== 6 || isNaN(Number("0x" + color))) { + throw new Error(`Invalid color value '${color}'. Expected six digit hex value (eg FF9900)`); + } + return color; +} + +// +// +// +export function unsignedDecimalNumber(val: number): number { + if (isNaN(val) || val < 0) { + throw new Error(`Invalid value '${val}' specified. Must be a positive base10 integer.`); + } + return Math.floor(val); +} + +// +// +// +export function signedTwipsMeasureValue(val: string | number): string | number { + if (typeof val === "string") { + return universalMeasureValue(val); + } + if (isNaN(val)) { + throw new Error(`Invalid value '${val}' specified. Expected a valid number.`); + } + return Math.floor(val); +} + +// +// +// +export function hpsMeasureValue(val: string | number): string | number { + return typeof val === "string" ? positiveUniversalMeasureValue(val) : unsignedDecimalNumber(val); +} diff --git a/src/file/xml-components/index.ts b/src/file/xml-components/index.ts index 295161b395..9ad7572069 100644 --- a/src/file/xml-components/index.ts +++ b/src/file/xml-components/index.ts @@ -4,4 +4,5 @@ export * from "./default-attributes"; export * from "./imported-xml-component"; export * from "./xmlable-object"; export * from "./initializable-xml-component"; +export * from "./simple-elements"; export * from "./base"; diff --git a/src/file/xml-components/simple-elements.ts b/src/file/xml-components/simple-elements.ts new file mode 100644 index 0000000000..8fa665a832 --- /dev/null +++ b/src/file/xml-components/simple-elements.ts @@ -0,0 +1,27 @@ +import { Attributes, XmlComponent } from "file/xml-components"; + +import { hpsMeasureValue } from "../values"; + +// This represents element type CT_OnOff, which indicate a boolean value. +// +// +// +// +export class OnOffElement extends XmlComponent { + constructor(name: string, val: boolean | undefined = true) { + super(name); + this.root.push(new Attributes({ val })); + } +} + +// This represents element type CT_HpsMeasure, which indicate an unsigned int or a measurement with unit. +// +// +// +// +export class HpsMeasureElement extends XmlComponent { + constructor(name: string, val: number | string) { + super(name); + this.root.push(new Attributes({ val: hpsMeasureValue(val) })); + } +}