From 24883141a144460720bc80e68c6ba38d9940255d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20N=C3=B6lke?= Date: Mon, 13 May 2019 19:06:24 +0200 Subject: [PATCH] style: add orphan & widdow control, paragraph background, line height (#75) --- CHANGELOG.md | 1 + README.md | 1 + docs/Features.md | 101 ++++++++++++++ src/api/office/AutomaticStyles.spec.ts | 3 +- src/api/office/AutomaticStyles.ts | 8 ++ src/api/style/IParagraphProperties.ts | 67 +++++++++- src/api/style/ParagraphProperties.spec.ts | 153 ++++++++++++++++++++++ src/api/style/ParagraphProperties.ts | 97 +++++++++++++- src/api/style/ParagraphStyle.ts | 73 +++++++++++ src/api/style/VerticalAlignment.ts | 7 + src/api/style/index.ts | 1 + src/api/util/index.ts | 2 + src/api/util/isNonNegativeNumber.ts | 10 ++ src/api/util/isPercent.ts | 10 ++ src/index.ts | 1 + src/xml/OdfAttributeName.ts | 10 +- src/xml/office/StylesWriter.spec.ts | 65 ++++++++- src/xml/office/StylesWriter.ts | 48 ++++++- test/integration.spec.ts | 61 ++++++++- 19 files changed, 696 insertions(+), 23 deletions(-) create mode 100644 docs/Features.md create mode 100644 src/api/style/VerticalAlignment.ts create mode 100644 src/api/util/index.ts create mode 100644 src/api/util/isNonNegativeNumber.ts create mode 100644 src/api/util/isPercent.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a67571cc..1be7f5ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - **style:** add common styles - **style:** allow page break to be before or after a paragraph, closes [#31](https://github.com/connium/simple-odf/issues/31) - **style:** add keep-with-next flag +- **style:** add orphan & widdow control, paragraph background, line height - **docs:** add a realistic example ### Changed diff --git a/README.md b/README.md index 7d4d2446..9000e134 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ See the [examples](./examples/README.md) for more details on how to use the libr Learn more about the [OASIS Open Document Format](http://docs.oasis-open.org/office/v1.2/OpenDocument-v1.2.html). +- [Features](./docs/Features.md) - [API reference](./docs/API.md) ## Contributing diff --git a/docs/Features.md b/docs/Features.md new file mode 100644 index 00000000..734879f0 --- /dev/null +++ b/docs/Features.md @@ -0,0 +1,101 @@ +# Features + +- Metadata +- Font face declaration +- [Style](#style) + - [Character](#character) + - [Paragraph](#paragraph) +- [Text content](#text-content) + +## Style + +### Character +- Font + - :heavy_check_mark: Family + - :heavy_check_mark: Style + - :heavy_check_mark: Size + - :x: Language +- Font effects + - :heavy_check_mark: Font color + - :heavy_check_mark: Effects + - :x: Relief + - :soon: Overlining + - :soon: Strikethrough + - :soon: Underlining +- Position + - :x: Position + - :x: Rotation & scaling + - :x: Spacing +- Hyperlink + - :heavy_check_mark: Hyperlink + - :x: Events + - :x: Character Styles +- Highlighting + - :x: Color +- Borders + - :x: Line arrangement + - :x: Line + - :x: Spacing to contents + - :x: Shadow style + +### Paragraph +- Indents & spacing + - :soon: Indent + - :soon: Spacing + - :construction: Line spacing +- Alignment + - :heavy_check_mark: horizontal + - :construction: vertical +- Text flow + - :x: Hyphenation + - :heavy_check_mark: Breaks + - :construction: Options +- Outline & numbering + - :x: Outline + - :x: Numbering + - :x: Line numbering +- Tabs + - :heavy_check_mark: Type + - :soon: Fill character +- Drop caps +- Borders + - :soon: Line arrangement + - :soon: Line + - :x: Spacing to contents + - :x: Shadow style +- Area + - :construction: Color + - :x: Gradient + - :x: Hatching + - :x: Bitmap + +## Text content + +- :heavy_check_mark: Heading +- :heavy_check_mark: Paragraph + - :heavy_check_mark: tab + - :heavy_check_mark: line break + - :x: hyphen + - :soon: span + - :construction: hyperlink + - :x: number +- :construction: List +- :x: Section +- :x: Change Tracking +- :x: Bookmark & Reference +- :x: Note +- :x: Text field +- :x: Table +- :x: Graphic content + - :x: Drawing shape + - :x: Frame + - :x: 3D Shape +- :x: Chart +- :x: Form + + diff --git a/src/api/office/AutomaticStyles.spec.ts b/src/api/office/AutomaticStyles.spec.ts index ebd87b1d..ee205184 100644 --- a/src/api/office/AutomaticStyles.spec.ts +++ b/src/api/office/AutomaticStyles.spec.ts @@ -1,4 +1,4 @@ -import { ParagraphStyle } from '../style'; +import { ParagraphStyle, TabStopType } from '../style'; import { AutomaticStyles } from './AutomaticStyles'; describe(AutomaticStyles.name, () => { @@ -9,6 +9,7 @@ describe(AutomaticStyles.name, () => { beforeEach(() => { testStyle1 = new ParagraphStyle(); testStyle2 = new ParagraphStyle().setFontSize(23); + testStyle2.addTabStop(42, TabStopType.Right); automaticStyles = new AutomaticStyles(); }); diff --git a/src/api/office/AutomaticStyles.ts b/src/api/office/AutomaticStyles.ts index 6fa7a76b..ff924ef2 100644 --- a/src/api/office/AutomaticStyles.ts +++ b/src/api/office/AutomaticStyles.ts @@ -114,9 +114,17 @@ export class AutomaticStyles implements IStyles { const paragraphStyle = style as ParagraphStyle; // paragraph properties + const backgroundColor = paragraphStyle.getBackgroundColor(); + hash.update(backgroundColor !== undefined ? backgroundColor.toHex() : ''); hash.update(paragraphStyle.getHorizontalAlignment()); hash.update(paragraphStyle.getKeepTogether() ? 'kt' : ''); + hash.update(paragraphStyle.getKeepWithNext() ? 'kwn' : ''); + hash.update('lh' + (paragraphStyle.getLineHeight() || '')); + hash.update('lhal' + (paragraphStyle.getLineHeightAtLeast() || '')); + hash.update('orphans' + (paragraphStyle.getOrphans() || '')); hash.update(paragraphStyle.getPageBreak().toString()); + hash.update(paragraphStyle.getVerticalAlignment()); + hash.update('widows' + (paragraphStyle.getWidows() || '')); paragraphStyle.getTabStops().forEach((tabStop) => { hash.update(`tab${tabStop.getPosition()}${tabStop.getType()}`); }); diff --git a/src/api/style/IParagraphProperties.ts b/src/api/style/IParagraphProperties.ts index 5040f440..7bd328c9 100644 --- a/src/api/style/IParagraphProperties.ts +++ b/src/api/style/IParagraphProperties.ts @@ -1,7 +1,9 @@ +import { Color } from './Color'; import { HorizontalAlignment } from './HorizontalAlignment'; import { PageBreak } from './PageBreak'; import { TabStop } from './TabStop'; import { TabStopType } from './TabStopType'; +import { VerticalAlignment } from './VerticalAlignment'; /** * This class represents the styling properties of a paragraph. @@ -11,6 +13,16 @@ import { TabStopType } from './TabStopType'; * @since 0.4.0 */ export interface IParagraphProperties { + /** + * @since 0.9.0 + */ + setBackgroundColor (color: Color | undefined): void; + + /** + * @since 0.9.0 + */ + getBackgroundColor (): Color | undefined; + /** * Sets the horizontal alignment setting of this paragraph. * @@ -43,20 +55,45 @@ export interface IParagraphProperties { getKeepTogether (): boolean; /** - * TODO - * * @since 0.9.0 */ setKeepWithNext (keepWithNext?: boolean): void; /** - * TODO - * - * @returns {boolean} `true` TODO, `false` otherwise * @since 0.9.0 */ getKeepWithNext (): boolean; + /** + * @since 0.9.0 + */ + setLineHeight (lineHeight: number | string | undefined): void; + + /** + * @since 0.9.0 + */ + getLineHeight (): number | string | undefined; + + /** + * @since 0.9.0 + */ + setLineHeightAtLeast (minimumLineHeight: number | undefined): void; + + /** + * @since 0.9.0 + */ + getLineHeightAtLeast (): number | undefined; + + /** + * @since 0.9.0 + */ + setOrphans (orphans: number | undefined): void; + + /** + * @since 0.9.0 + */ + getOrphans (): number | undefined; + /** * Sets the page break setting of the paragraph. * @@ -73,6 +110,26 @@ export interface IParagraphProperties { */ getPageBreak (): PageBreak; + /** + * @since 0.9.0 + */ + setVerticalAlignment (verticalAlignment: VerticalAlignment): void; + + /** + * @since 0.9.0 + */ + getVerticalAlignment (): VerticalAlignment; + + /** + * @since 0.9.0 + */ + setWidows (widows: number | undefined): void; + + /** + * @since 0.9.0 + */ + getWidows (): number | undefined; + /** * Adds a new tab stop to this style. * If a tab stop at the same position already exists, the new tab stop will not be added. diff --git a/src/api/style/ParagraphProperties.spec.ts b/src/api/style/ParagraphProperties.spec.ts index b936036b..d2c5fbdd 100644 --- a/src/api/style/ParagraphProperties.spec.ts +++ b/src/api/style/ParagraphProperties.spec.ts @@ -1,8 +1,10 @@ +import { Color } from './Color'; import { HorizontalAlignment } from './HorizontalAlignment'; import { PageBreak } from './PageBreak'; import { ParagraphProperties } from './ParagraphProperties'; import { TabStop } from './TabStop'; import { TabStopType } from './TabStopType'; +import { VerticalAlignment } from './VerticalAlignment'; describe(ParagraphProperties.name, () => { let properties: ParagraphProperties; @@ -11,6 +13,20 @@ describe(ParagraphProperties.name, () => { properties = new ParagraphProperties(); }); + describe('background color', () => { + it('return undefined by default', () => { + expect(properties.getBackgroundColor()).toBeUndefined(); + }); + + it('return previously set alignment', () => { + const testColor = Color.fromRgb(1, 2, 3); + + properties.setBackgroundColor(testColor); + + expect(properties.getBackgroundColor()).toBe(testColor); + }); + }); + describe('horizontal alignment', () => { it('return `Default` by default', () => { expect(properties.getHorizontalAlignment()).toBe(HorizontalAlignment.Default); @@ -55,6 +71,99 @@ describe(ParagraphProperties.name, () => { }); }); + describe('line height', () => { + const testLineHeightNumber = 23; + const testLineHeightPercent = '42%'; + + it('return undefined by default', () => { + expect(properties.getLineHeight()).toBeUndefined(); + }); + + it('return previously set state', () => { + properties.setLineHeight(testLineHeightNumber); + + expect(properties.getLineHeight()).toBe(testLineHeightNumber); + + properties.setLineHeight(testLineHeightPercent); + + expect(properties.getLineHeight()).toBe(testLineHeightPercent); + + properties.setLineHeight(undefined); + + expect(properties.getLineHeight()).toBeUndefined(); + }); + + it('ignore invalid value', () => { + properties.setLineHeight(testLineHeightNumber); + + properties.setLineHeight(0); + + expect(properties.getLineHeight()).toBe(testLineHeightNumber); + + properties.setLineHeight('42$'); + + expect(properties.getLineHeight()).toBe(testLineHeightNumber); + }); + }); + + describe('line height at least', () => { + const testLineHeight = 23; + + it('return undefined by default', () => { + expect(properties.getLineHeightAtLeast()).toBeUndefined(); + }); + + it('return previously set state', () => { + properties.setLineHeightAtLeast(testLineHeight); + + expect(properties.getLineHeightAtLeast()).toBe(testLineHeight); + + properties.setLineHeightAtLeast(undefined); + + expect(properties.getLineHeightAtLeast()).toBeUndefined(); + }); + + it('ignore invalid value', () => { + properties.setLineHeightAtLeast(testLineHeight); + + properties.setLineHeightAtLeast(0); + + expect(properties.getLineHeightAtLeast()).toBe(testLineHeight); + }); + }); + + describe('orphans', () => { + const testOrphans = 23; + + it('return undefined by default', () => { + expect(properties.getOrphans()).toBeUndefined(); + }); + + it('return previously set state', () => { + properties.setOrphans(testOrphans); + + expect(properties.getOrphans()).toBe(testOrphans); + + properties.setOrphans(undefined); + + expect(properties.getOrphans()).toBeUndefined(); + }); + + it('use truncated value', () => { + properties.setOrphans(23.42); + + expect(properties.getOrphans()).toBe(testOrphans); + }); + + it('ignore invalid value', () => { + properties.setOrphans(testOrphans); + + properties.setOrphans(-23); + + expect(properties.getOrphans()).toBe(testOrphans); + }); + }); + describe('page break', () => { it('return None by default', () => { expect(properties.getPageBreak()).toBe(PageBreak.None); @@ -71,6 +180,50 @@ describe(ParagraphProperties.name, () => { }); }); + describe('vertical alignment', () => { + it('return Default by default', () => { + expect(properties.getVerticalAlignment()).toBe(VerticalAlignment.Default); + }); + + it('return previously set alignment', () => { + properties.setVerticalAlignment(VerticalAlignment.Middle); + + expect(properties.getVerticalAlignment()).toBe(VerticalAlignment.Middle); + }); + }); + + describe('widows', () => { + const testWidows = 23; + + it('return undefined by default', () => { + expect(properties.getWidows()).toBeUndefined(); + }); + + it('return previously set state', () => { + properties.setWidows(testWidows); + + expect(properties.getWidows()).toBe(testWidows); + + properties.setWidows(undefined); + + expect(properties.getWidows()).toBeUndefined(); + }); + + it('use truncated value', () => { + properties.setWidows(23.42); + + expect(properties.getWidows()).toBe(testWidows); + }); + + it('ignore invalid value', () => { + properties.setWidows(testWidows); + + properties.setWidows(-23); + + expect(properties.getWidows()).toBe(testWidows); + }); + }); + describe('#addTabStop', () => { it('add new item to the list of tab stops by position and return the added tab stop', () => { const testTabStop = new TabStop(23); diff --git a/src/api/style/ParagraphProperties.ts b/src/api/style/ParagraphProperties.ts index d89b3a5d..7c3dafa5 100644 --- a/src/api/style/ParagraphProperties.ts +++ b/src/api/style/ParagraphProperties.ts @@ -1,19 +1,29 @@ +import { isNonNegativeNumber, isPercent } from '../util'; +import { Color } from './Color'; import { HorizontalAlignment } from './HorizontalAlignment'; import { IParagraphProperties } from './IParagraphProperties'; import { PageBreak } from './PageBreak'; import { TabStopType } from './TabStopType'; import { TabStop } from './TabStop'; +import { VerticalAlignment } from './VerticalAlignment'; const DEFAULT_HORIZONTAL_ALIGNMENT = HorizontalAlignment.Default; -const DEFAULT_PAGE_BREAK = PageBreak.None; const DEFAULT_KEEP_TOGETHER = false; const DEFAULT_KEEP_WITH_NEXT = false; +const DEFAULT_PAGE_BREAK = PageBreak.None; +const DEFAULT_VERTICAL_ALIGNMENT = VerticalAlignment.Default; export class ParagraphProperties implements IParagraphProperties { + private backgroundColor: Color | undefined; private horizontalAlignment: HorizontalAlignment; + private lineHeight: number | string | undefined; + private minimumLineHeight: number | undefined; + private orphans: number | undefined; private pageBreak: PageBreak; private shouldKeepTogether: boolean; private shouldKeepWithNext: boolean; + private verticalAlignment: VerticalAlignment; + private widows: number | undefined; private tabStops: TabStop[] = []; public constructor () { @@ -21,6 +31,17 @@ export class ParagraphProperties implements IParagraphProperties { this.pageBreak = DEFAULT_PAGE_BREAK; this.shouldKeepTogether = DEFAULT_KEEP_TOGETHER; this.shouldKeepWithNext = DEFAULT_KEEP_WITH_NEXT; + this.verticalAlignment = DEFAULT_VERTICAL_ALIGNMENT; + } + + /** @inheritdoc */ + public setBackgroundColor (color: Color | undefined): void { + this.backgroundColor = color; + } + + /** @inheritdoc */ + public getBackgroundColor (): Color | undefined { + return this.backgroundColor; } /** @inheritdoc */ @@ -44,15 +65,58 @@ export class ParagraphProperties implements IParagraphProperties { } /** @inheritdoc */ - setKeepWithNext (keepWithNext = true): void { + public setKeepWithNext (keepWithNext = true): void { this.shouldKeepWithNext = keepWithNext; } /** @inheritdoc */ - getKeepWithNext (): boolean { + public getKeepWithNext (): boolean { return this.shouldKeepWithNext; } + /** @inheritdoc */ + public setLineHeight (lineHeight: number | string | undefined): void { + if (isNonNegativeNumber(lineHeight) || isPercent(lineHeight) || lineHeight === undefined) { + this.lineHeight = lineHeight; + this.minimumLineHeight = undefined; + } + } + + /** @inheritdoc */ + public getLineHeight (): number | string | undefined { + return this.lineHeight; + } + + /** @inheritdoc */ + public setLineHeightAtLeast (minimumLineHeight: number | undefined): void { + if (isNonNegativeNumber(minimumLineHeight) || minimumLineHeight === undefined) { + this.minimumLineHeight = minimumLineHeight; + this.lineHeight = undefined; + } + } + + /** @inheritdoc */ + public getLineHeightAtLeast (): number | undefined { + return this.minimumLineHeight; + } + + /** @inheritdoc */ + public setOrphans (orphans: number | undefined): void { + if (isNonNegativeNumber(orphans)) { + this.orphans = Math.trunc(orphans as number); + return; + } + + if (orphans === undefined) { + this.orphans = orphans; + } + } + + /** @inheritdoc */ + public getOrphans (): number | undefined { + return this.orphans; + } + /** @inheritdoc */ public setPageBreak (pageBreak: PageBreak): void { this.pageBreak = pageBreak; @@ -63,6 +127,33 @@ export class ParagraphProperties implements IParagraphProperties { return this.pageBreak; } + /** @inheritdoc */ + public setVerticalAlignment (verticalAlignment: VerticalAlignment): void { + this.verticalAlignment = verticalAlignment; + } + + /** @inheritdoc */ + public getVerticalAlignment (): VerticalAlignment { + return this.verticalAlignment; + } + + /** @inheritdoc */ + public setWidows (widows: number | undefined): void { + if (isNonNegativeNumber(widows)) { + this.widows = Math.trunc(widows as number); + return; + } + + if (widows === undefined) { + this.widows = widows; + } + } + + /** @inheritdoc */ + public getWidows (): number | undefined { + return this.widows; + } + /** @inheritdoc */ public addTabStop (position: number, type: TabStopType): TabStop | undefined; diff --git a/src/api/style/ParagraphStyle.ts b/src/api/style/ParagraphStyle.ts index a8a721d5..b3a0af55 100644 --- a/src/api/style/ParagraphStyle.ts +++ b/src/api/style/ParagraphStyle.ts @@ -11,6 +11,7 @@ import { TabStopType } from './TabStopType'; import { TextProperties } from './TextProperties'; import { TextTransformation } from './TextTransformation'; import { Typeface } from './Typeface'; +import { VerticalAlignment } from './VerticalAlignment'; export class ParagraphStyle extends Style implements IParagraphProperties, ITextProperties { private paragraphProperties: ParagraphProperties; @@ -25,6 +26,18 @@ export class ParagraphStyle extends Style implements IParagraphProperties, IText // paragraph properties + /** @inheritdoc */ + public setBackgroundColor (color: Color | undefined): ParagraphStyle { + this.paragraphProperties.setBackgroundColor(color); + + return this; + } + + /** @inheritdoc */ + public getBackgroundColor (): Color | undefined { + return this.paragraphProperties.getBackgroundColor(); + } + /** @inheritdoc */ public setHorizontalAlignment (horizontalAlignment: HorizontalAlignment): ParagraphStyle { this.paragraphProperties.setHorizontalAlignment(horizontalAlignment); @@ -61,6 +74,42 @@ export class ParagraphStyle extends Style implements IParagraphProperties, IText return this.paragraphProperties.getKeepWithNext(); } + /** @inheritdoc */ + public setLineHeight (lineHeight: number | string | undefined): ParagraphStyle { + this.paragraphProperties.setLineHeight(lineHeight); + + return this; + } + + /** @inheritdoc */ + public getLineHeight (): number | string | undefined { + return this.paragraphProperties.getLineHeight(); + } + + /** @inheritdoc */ + public setLineHeightAtLeast (minimumLineHeight: number | undefined): ParagraphStyle { + this.paragraphProperties.setLineHeightAtLeast(minimumLineHeight); + + return this; + } + + /** @inheritdoc */ + public getLineHeightAtLeast (): number | undefined { + return this.paragraphProperties.getLineHeightAtLeast(); + } + + /** @inheritdoc */ + public setOrphans (orphans: number | undefined): ParagraphStyle { + this.paragraphProperties.setOrphans(orphans); + + return this; + } + + /** @inheritdoc */ + public getOrphans (): number | undefined { + return this.paragraphProperties.getOrphans(); + } + /** @inheritdoc */ public setPageBreak (pageBreak: PageBreak): ParagraphStyle { this.paragraphProperties.setPageBreak(pageBreak); @@ -73,6 +122,30 @@ export class ParagraphStyle extends Style implements IParagraphProperties, IText return this.paragraphProperties.getPageBreak(); } + /** @inheritdoc */ + public setVerticalAlignment (verticalAlignment: VerticalAlignment): ParagraphStyle { + this.paragraphProperties.setVerticalAlignment(verticalAlignment); + + return this; + } + + /** @inheritdoc */ + public getVerticalAlignment (): VerticalAlignment { + return this.paragraphProperties.getVerticalAlignment(); + } + + /** @inheritdoc */ + public setWidows (widows: number | undefined): ParagraphStyle { + this.paragraphProperties.setWidows(widows); + + return this; + } + + /** @inheritdoc */ + public getWidows (): number | undefined { + return this.paragraphProperties.getWidows(); + } + /** @inheritdoc */ public addTabStop (position: number, type: TabStopType): TabStop | undefined; diff --git a/src/api/style/VerticalAlignment.ts b/src/api/style/VerticalAlignment.ts new file mode 100644 index 00000000..a027d052 --- /dev/null +++ b/src/api/style/VerticalAlignment.ts @@ -0,0 +1,7 @@ +export enum VerticalAlignment { + Default = '', + Top = 'top', + Middle = 'middle', + Bottom = 'bottom', + Baseline = 'baseline' +} diff --git a/src/api/style/index.ts b/src/api/style/index.ts index 79317928..d1b44b89 100644 --- a/src/api/style/index.ts +++ b/src/api/style/index.ts @@ -13,3 +13,4 @@ export { TabStopType } from './TabStopType'; export { TextProperties } from './TextProperties'; export { TextTransformation } from './TextTransformation'; export { Typeface } from './Typeface'; +export { VerticalAlignment } from './VerticalAlignment'; diff --git a/src/api/util/index.ts b/src/api/util/index.ts new file mode 100644 index 00000000..885d4fec --- /dev/null +++ b/src/api/util/index.ts @@ -0,0 +1,2 @@ +export { isNonNegativeNumber } from './isNonNegativeNumber'; +export { isPercent } from './isPercent'; diff --git a/src/api/util/isNonNegativeNumber.ts b/src/api/util/isNonNegativeNumber.ts new file mode 100644 index 00000000..c84b77c8 --- /dev/null +++ b/src/api/util/isNonNegativeNumber.ts @@ -0,0 +1,10 @@ +/** + * The `isNonNegativeNumber` method returns whether the given value is a non-negative number. + * + * @param {any} value The value that is to be checked + * @returns {boolean} `true` if the given value is a non-negative number, `false` otherwise + * @private + */ +export function isNonNegativeNumber (value: any): boolean { + return typeof value === 'number' && value > 0; +} diff --git a/src/api/util/isPercent.ts b/src/api/util/isPercent.ts new file mode 100644 index 00000000..923dd891 --- /dev/null +++ b/src/api/util/isPercent.ts @@ -0,0 +1,10 @@ +/** + * The `isPercent` method returns whether the given value is a percentage. + * + * @param {any} value The value that is to be checked + * @returns {boolean} `true` if the given value is a percentage, `false` otherwise + * @private + */ +export function isPercent (value: any): boolean { + return typeof value === 'string' && /^-?([0-9]+(\.[0-9]*)?|\.[0-9]+)%$/.test(value); +} diff --git a/src/index.ts b/src/index.ts index 5a30f88a..befd2afe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ export { TabStop } from './api/style/TabStop'; export { TabStopType } from './api/style/TabStopType'; export { TextTransformation } from './api/style/TextTransformation'; export { Typeface } from './api/style/Typeface'; +export { VerticalAlignment } from './api/style/VerticalAlignment'; // text export { Heading } from './api/text/Heading'; diff --git a/src/xml/OdfAttributeName.ts b/src/xml/OdfAttributeName.ts index 1cdcd89d..c4e64d33 100644 --- a/src/xml/OdfAttributeName.ts +++ b/src/xml/OdfAttributeName.ts @@ -1,23 +1,29 @@ export enum OdfAttributeName { + FormatBackgroundColor = 'fo:background-color', FormatBreakAfter = 'fo:break-after', FormatBreakBefore = 'fo:break-before', - FormatKeepTogether = 'fo:keep-together', - FormatKeepWithNext = 'fo:keep-with-next', FormatColor = 'fo:color', FormatFontSize = 'fo:font-size', FormatFontStyle = 'fo:font-style', FormatFontWeight = 'fo:font-weight', + FormatKeepTogether = 'fo:keep-together', + FormatKeepWithNext = 'fo:keep-with-next', + FormatLineHeight = 'fo:line-height', + FormatOrphans = 'fo:orphans', FormatTextAlign = 'fo:text-align', FormatTextTransform = 'fo:text-transform', + FormatWidows = 'fo:widows', OfficeMimetype = 'office:mimetype', OfficeVersion = 'office:version', StyleFamily = 'style:family', StyleFontName = 'style:font-name', + StyleLineHeightAtLeast = 'style:line-height-at-least', StyleName = 'style:name', StylePosition = 'style:position', StyleType = 'style:type', + StyleVerticalAlign = 'style:vertical-align', SvgHeight = 'svg:height', SvgWidth = 'svg:width', diff --git a/src/xml/office/StylesWriter.spec.ts b/src/xml/office/StylesWriter.spec.ts index 7c757bf8..d8412927 100644 --- a/src/xml/office/StylesWriter.spec.ts +++ b/src/xml/office/StylesWriter.spec.ts @@ -2,7 +2,7 @@ import { DOMImplementation, XMLSerializer } from 'xmldom'; import { CommonStyles, AutomaticStyles } from '../../api/office'; import { Color, HorizontalAlignment, PageBreak, ParagraphStyle, TextTransformation, Typeface } from '../../api/style'; // tslint:disable-next-line:no-duplicate-imports -import { TabStop, TabStopType } from '../../api/style'; +import { TabStop, TabStopType, VerticalAlignment } from '../../api/style'; import { OdfElementName } from '../OdfElementName'; import { StylesWriter } from './StylesWriter'; @@ -77,6 +77,15 @@ describe(StylesWriter.name, () => { testStyle = commonStyles.createParagraphStyle('Summary'); }); + it('set background color', () => { + testStyle.setBackgroundColor(Color.fromRgb(1, 2, 3)); + + stylesWriter.write(commonStyles, testDocument, testRoot); + const documentAsString = new XMLSerializer().serializeToString(testDocument); + + expect(documentAsString).toMatch(//); + }); + it('set horizontal alignment', () => { testStyle.setHorizontalAlignment(HorizontalAlignment.Center); @@ -104,6 +113,42 @@ describe(StylesWriter.name, () => { expect(documentAsString).toMatch(//); }); + it('set line height as fix value', () => { + testStyle.setLineHeight(23); + + stylesWriter.write(commonStyles, testDocument, testRoot); + const documentAsString = new XMLSerializer().serializeToString(testDocument); + + expect(documentAsString).toMatch(//); + }); + + it('set line height as percentage', () => { + testStyle.setLineHeight('42%'); + + stylesWriter.write(commonStyles, testDocument, testRoot); + const documentAsString = new XMLSerializer().serializeToString(testDocument); + + expect(documentAsString).toMatch(//); + }); + + it('set line height at least', () => { + testStyle.setLineHeightAtLeast(23); + + stylesWriter.write(commonStyles, testDocument, testRoot); + const documentAsString = new XMLSerializer().serializeToString(testDocument); + + expect(documentAsString).toMatch(//); + }); + + it('set orphans', () => { + testStyle.setOrphans(23); + + stylesWriter.write(commonStyles, testDocument, testRoot); + const documentAsString = new XMLSerializer().serializeToString(testDocument); + + expect(documentAsString).toMatch(//); + }); + it('set page break before', () => { testStyle.setPageBreak(PageBreak.Before); @@ -122,6 +167,24 @@ describe(StylesWriter.name, () => { expect(documentAsString).toMatch(//); }); + it('set vertical alignment', () => { + testStyle.setVerticalAlignment(VerticalAlignment.Middle); + + stylesWriter.write(commonStyles, testDocument, testRoot); + const documentAsString = new XMLSerializer().serializeToString(testDocument); + + expect(documentAsString).toMatch(//); + }); + + it('set widows', () => { + testStyle.setWidows(23); + + stylesWriter.write(commonStyles, testDocument, testRoot); + const documentAsString = new XMLSerializer().serializeToString(testDocument); + + expect(documentAsString).toMatch(//); + }); + it('set tab stops', () => { testStyle.addTabStop(new TabStop(2, TabStopType.Center)); testStyle.addTabStop(new TabStop(4, TabStopType.Char)); diff --git a/src/xml/office/StylesWriter.ts b/src/xml/office/StylesWriter.ts index 862ef938..19978bd2 100644 --- a/src/xml/office/StylesWriter.ts +++ b/src/xml/office/StylesWriter.ts @@ -1,13 +1,15 @@ +// tslint:disable:no-duplicate-imports import { AutomaticStyles, CommonStyles, IStyles } from '../../api/office'; import { HorizontalAlignment, ParagraphStyle, Style, StyleFamily, TabStopType, PageBreak } from '../../api/style'; -// tslint:disable-next-line:no-duplicate-imports -import { TextTransformation, Typeface } from '../../api/style'; +import { TextTransformation, Typeface, VerticalAlignment } from '../../api/style'; import { OdfAttributeName } from '../OdfAttributeName'; import { OdfElementName } from '../OdfElementName'; /** * Transforms a {@link StyleManager} object into ODF conform XML * + * NOTE: The properties are set in the order of their appearance in the Realx NG schema. + * * @since 0.9.0 */ export class StylesWriter { @@ -68,6 +70,23 @@ export class StylesWriter { const paragraphPropertiesElement = document.createElement(OdfElementName.StyleParagraphProperties); parent.appendChild(paragraphPropertiesElement); + const lineHeight = style.getLineHeight(); + switch (typeof lineHeight) { + case 'number': + paragraphPropertiesElement.setAttribute(OdfAttributeName.FormatLineHeight, lineHeight + 'mm'); + break; + case 'string': + paragraphPropertiesElement.setAttribute(OdfAttributeName.FormatLineHeight, lineHeight); + break; + default: + break; + } + + const lineHeightAtLeast = style.getLineHeightAtLeast(); + if (lineHeightAtLeast !== undefined) { + paragraphPropertiesElement.setAttribute(OdfAttributeName.StyleLineHeightAtLeast, lineHeightAtLeast + 'mm'); + } + if (style.getHorizontalAlignment() !== HorizontalAlignment.Default) { paragraphPropertiesElement.setAttribute(OdfAttributeName.FormatTextAlign, style.getHorizontalAlignment()); } @@ -76,8 +95,14 @@ export class StylesWriter { paragraphPropertiesElement.setAttribute(OdfAttributeName.FormatKeepTogether, 'always'); } - if (style.getKeepWithNext() === true) { - paragraphPropertiesElement.setAttribute(OdfAttributeName.FormatKeepWithNext, 'always'); + const widows = style.getWidows(); + if (widows !== undefined) { + paragraphPropertiesElement.setAttribute(OdfAttributeName.FormatWidows, widows.toString(10)); + } + + const orphans = style.getOrphans(); + if (orphans !== undefined) { + paragraphPropertiesElement.setAttribute(OdfAttributeName.FormatOrphans, orphans.toString(10)); } switch (style.getPageBreak()) { @@ -91,6 +116,19 @@ export class StylesWriter { break; } + const backgroundColor = style.getBackgroundColor(); + if (backgroundColor !== undefined) { + paragraphPropertiesElement.setAttribute(OdfAttributeName.FormatBackgroundColor, backgroundColor.toHex()); + } + + if (style.getKeepWithNext() === true) { + paragraphPropertiesElement.setAttribute(OdfAttributeName.FormatKeepWithNext, 'always'); + } + + if (style.getVerticalAlignment() !== VerticalAlignment.Default) { + paragraphPropertiesElement.setAttribute(OdfAttributeName.StyleVerticalAlign, style.getVerticalAlignment()); + } + const tabStops = style.getTabStops(); if (tabStops.length === 0) { return paragraphPropertiesElement; @@ -103,7 +141,7 @@ export class StylesWriter { const tabStopElement = document.createElement(OdfElementName.StyleTabStop); parent.appendChild(tabStopElement); - tabStopElement.setAttribute(OdfAttributeName.StylePosition, `${tabStop.getPosition()}mm`); + tabStopElement.setAttribute(OdfAttributeName.StylePosition, tabStop.getPosition() + 'mm'); if (tabStop.getType() !== TabStopType.Left) { tabStopElement.setAttribute(OdfAttributeName.StyleType, tabStop.getType()); } diff --git a/test/integration.spec.ts b/test/integration.spec.ts index bd252161..ed9153e9 100644 --- a/test/integration.spec.ts +++ b/test/integration.spec.ts @@ -1,10 +1,11 @@ +// tslint:disable:no-duplicate-imports import { unlink } from 'fs'; import { join } from 'path'; import { promisify } from 'util'; import { AnchorType } from '../src/api/draw'; import { TextBody, TextDocument } from '../src/api/office'; import { Color, FontPitch, HorizontalAlignment, PageBreak, ParagraphStyle, TabStop } from '../src/api/style'; -import { TabStopType, TextTransformation, Typeface } from '../src/api/style'; +import { TabStopType, TextTransformation, Typeface, VerticalAlignment } from '../src/api/style'; const FILEPATH = './integration.fodt'; @@ -65,14 +66,22 @@ xdescribe('integration', () => { heading.setStyle(style); }); - it('keep with next', () => { + it('background color', () => { const style = new ParagraphStyle(); - style.setKeepWithNext(); + style.setBackgroundColor(Color.fromRgb(0, 255, 0)); - const heading = body.addParagraph('Keep together with next paragraph'); + const heading = body.addParagraph('Some text with green colored background'); heading.setStyle(style); }); + it('align text', () => { + const style = new ParagraphStyle(); + style.setHorizontalAlignment(HorizontalAlignment.Center); + + const paragraph = body.addParagraph('Some centered text'); + paragraph.setStyle(style); + }); + it('keep together', () => { const style = new ParagraphStyle(); style.setKeepTogether(); @@ -81,14 +90,54 @@ xdescribe('integration', () => { heading.setStyle(style); }); - it('align text', () => { + it('keep with next', () => { const style = new ParagraphStyle(); - style.setHorizontalAlignment(HorizontalAlignment.Center); + style.setKeepWithNext(); + + const heading = body.addParagraph('Keep together with next paragraph'); + heading.setStyle(style); + }); + + it('line height', () => { + const style = new ParagraphStyle(); + style.setLineHeight('120%'); + + const heading = body.addParagraph('Some text with 120% line height'); + heading.setStyle(style); + }); + + it('line height at least', () => { + const style = new ParagraphStyle(); + style.setLineHeightAtLeast(40); + + const heading = body.addParagraph('Some text with minimum line height of 40 mm'); + heading.setStyle(style); + }); + + it('orphans', () => { + const style = new ParagraphStyle(); + style.setOrphans(2); + + const heading = body.addParagraph('Break paragraph after 2 lines of text at the earliest'); + heading.setStyle(style); + }); + + it('vertical align text', () => { + const style = new ParagraphStyle(); + style.setVerticalAlignment(VerticalAlignment.Middle); const paragraph = body.addParagraph('Some centered text'); paragraph.setStyle(style); }); + it('widows', () => { + const style = new ParagraphStyle(); + style.setWidows(2); + + const heading = body.addParagraph('Write at least 2 lines of text after a break of the paragraph'); + heading.setStyle(style); + }); + it('tab stops', () => { const style = new ParagraphStyle(); style.addTabStop(new TabStop(40));