diff --git a/CHANGELOG.md b/CHANGELOG.md index 909273da..8a9fe64a 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. - **chore:** Add code coverage analysis with Codecov by @oncletom, closes [#17](https://github.com/connium/simple-odf/issues/17) - **chore:** Add static code analysis with Better Code Hub - **paragraph:** Set color, font size and typeface to the text of a paragraph, closes [#26](https://github.com/connium/simple-odf/issues/26) +- **paragraph:** Set font family to a paragraph, closes [#27](https://github.com/connium/simple-odf/issues/27) ### Changed - **chore:** Run TSLint on production and test code diff --git a/README.md b/README.md index 17cabb3b..6a1b261b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,11 @@ style1.setTypeface(simpleOdf.Typeface.Bold); style1.setHorizontalAlignment(simpleOdf.HorizontalAlignment.Center); style1.setPageBreakBefore(); p1.setStyle(style1); +// font usage +document.declareFont("Open Sans", "Open Sans", simpleOdf.FontPitch.Variable); +const p2 = document.addParagraph("It always seems impossible until it's done."); +const style2 = new simpleOdf.ParagraphStyle(); +style1.setFontName("Open Sans"); document.addHeading("Credits", 2); diff --git a/src/OdfAttributeName.ts b/src/OdfAttributeName.ts index 44a0885d..021658f0 100644 --- a/src/OdfAttributeName.ts +++ b/src/OdfAttributeName.ts @@ -5,11 +5,12 @@ export enum OdfAttributeName { FormatFontStyle = "fo:font-style", FormatFontWeight = "fo:font-weight", FormatTextAlign = "fo:text-align", - + OfficeMimetype = "office:mimetype", OfficeVersion = "office:version", - + StyleFamily = "style:family", + StyleFontName = "style:font-name", StyleName = "style:name", StylePosition = "style:position", StyleType = "style:type", diff --git a/src/OdfElementName.ts b/src/OdfElementName.ts index 3293afd1..3a05a3d9 100644 --- a/src/OdfElementName.ts +++ b/src/OdfElementName.ts @@ -6,16 +6,18 @@ export enum OdfElementName { OfficeBinaryData = "office:binary-data", OfficeBody = "office:body", OfficeDocument = "office:document", + OfficeFontFaceDeclarations = "office:font-face-decls", OfficeText = "office:text", + StyleFontFace = "style:font-face", + StyleParagraphProperties = "style:paragraph-properties", StyleStyle = "style:style", StyleTabStop = "style:tab-stop", StyleTabStops = "style:tab-stops", StyleTextProperties = "style:text-properties", - StyleParagraphProperties = "style:paragraph-properties", - TextHyperlink = "text:a", TextHeading = "text:h", + TextHyperlink = "text:a", TextLineBreak = "text:line-break", TextList = "text:list", TextListItem = "text:list-item", diff --git a/src/TextDocument.ts b/src/TextDocument.ts index c8504633..705615d5 100644 --- a/src/TextDocument.ts +++ b/src/TextDocument.ts @@ -5,6 +5,7 @@ import { DOMImplementation, XMLSerializer } from "xmldom"; import { OdfAttributeName } from "./OdfAttributeName"; import { OdfElement } from "./OdfElement"; import { OdfElementName } from "./OdfElementName"; +import { FontPitch } from "./style/FontPitch"; import { Heading } from "./text/Heading"; import { List } from "./text/List"; import { Paragraph } from "./text/Paragraph"; @@ -13,13 +14,39 @@ export const XML_DECLARATION = '\n'; const OFFICE_VERSION = "1.2"; +/** This interface holds a font font declaration */ +interface IFont { + name: string; + fontFamily: string; + fontPitch: FontPitch; +} + /** * This class represents an empty ODF text document. * @since 0.1.0 */ export class TextDocument extends OdfElement { + private fonts: IFont[]; + public constructor() { super(); + + this.fonts = []; + } + + /** + * Declares a font to be used in the document. + * + * **Note: There is no check whether the font exists. + * In order to be displayed properly, the font must be present on the target system.** + * + * @param {string} name The name of the font; this name must be set to a {@link ParagraphStyle} + * @param {string} fontFamily The name of the font family + * @param {FontPitch} fontPitch The ptich of the fonr + * @since 0.4.0 + */ + public declareFont(name: string, fontFamily: string, fontPitch: FontPitch): void { + this.fonts.push({ name, fontFamily, fontPitch }); } /** @@ -104,6 +131,8 @@ export class TextDocument extends OdfElement { root.setAttribute(OdfAttributeName.OfficeMimetype, "application/vnd.oasis.opendocument.text"); root.setAttribute(OdfAttributeName.OfficeVersion, OFFICE_VERSION); + this.setFontFaceElements(document, root); + const bodyElement = document.createElement(OdfElementName.OfficeBody); root.appendChild(bodyElement); @@ -112,4 +141,30 @@ export class TextDocument extends OdfElement { super.toXml(document, textElement); } + + /** + * Adds the `font-face-decls` element and the font faces if any font needs to bne declared. + * + * @param {Document} document The XML document + * @param {Element} root The element which will be used as parent + */ + private setFontFaceElements(document: Document, root: Element): void { + if (this.fonts.length === 0) { + return; + } + + root.setAttribute("xmlns:svg", "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"); + + const fontFaceDeclsElement = document.createElement(OdfElementName.OfficeFontFaceDeclarations); + root.appendChild(fontFaceDeclsElement); + + this.fonts.forEach((font: IFont) => { + const fontFaceElement = document.createElement(OdfElementName.StyleFontFace); + fontFaceDeclsElement.appendChild(fontFaceElement); + fontFaceElement.setAttribute("style:name", font.name); + const encodedFontFamily = font.fontFamily.includes(" ") === true ? `'${font.fontFamily}'` : font.fontFamily; + fontFaceElement.setAttribute("svg:font-family", encodedFontFamily); + fontFaceElement.setAttribute("style:font-pitch", font.fontPitch); + }); + } } diff --git a/src/index.ts b/src/index.ts index 50a8c526..5f4e5b3e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export { Image } from "./draw/Image"; // style export { Color } from "./style/Color"; +export { FontPitch } from "./style/FontPitch"; export { HorizontalAlignment } from "./style/HorizontalAlignment"; export { IParagraphStyle } from "./style/IParagraphStyle"; export { ParagraphStyle } from "./style/ParagraphStyle"; diff --git a/src/style/FontPitch.ts b/src/style/FontPitch.ts new file mode 100644 index 00000000..1b9d303f --- /dev/null +++ b/src/style/FontPitch.ts @@ -0,0 +1,4 @@ +export enum FontPitch { + Fixed = "fixed", + Variable = "variable", +} diff --git a/src/style/ITextProperties.ts b/src/style/ITextProperties.ts index 64931762..3b6876c4 100644 --- a/src/style/ITextProperties.ts +++ b/src/style/ITextProperties.ts @@ -43,6 +43,23 @@ export interface ITextProperties { */ getColor(): Color | undefined; + /** + * Sets the name of the font that will be applied to the text. + * To reset the font, `undefined` must be given. + * + * @param {string} name The name of the font to apply or `undefined` if the default font should be used + * @since 0.4.0 + */ + setFontName(name: string): void; + + /** + * Returns the name of the font that will be applied to the text or `undefined` if the default font will be used. + * + * @returns {string | undefined} The name of the font to apply or `undefined` if the default font will be used + * @since 0.4.0 + */ + getFontName(): string | undefined; + /** * Sets the font size that will be applied to the text. * diff --git a/src/style/ParagraphProperties.ts b/src/style/ParagraphProperties.ts index 45b348d5..7355953a 100644 --- a/src/style/ParagraphProperties.ts +++ b/src/style/ParagraphProperties.ts @@ -130,17 +130,20 @@ export class ParagraphProperties implements IParagraphProperties { /** * Adds the `tab-stops` element and the tab stop definitions if any tab stop is set. * - * @param {Element} textPropertiesElement The element which will take the attribute + * @param {Document} document The XML document + * @param {Element} textPropertiesElement The element which will be used as parent */ private setTabStopElements(document: Document, paragraphPropertiesElement: Element): void { - if (this.tabStops.length > 0) { - const tabStopsElement = document.createElement(OdfElementName.StyleTabStops); - paragraphPropertiesElement.appendChild(tabStopsElement); - - this.tabStops.forEach((tabStop: TabStop) => { - tabStop.toXml(document, tabStopsElement); - }); + if (this.tabStops.length === 0) { + return; } + + const tabStopsElement = document.createElement(OdfElementName.StyleTabStops); + paragraphPropertiesElement.appendChild(tabStopsElement); + + this.tabStops.forEach((tabStop: TabStop) => { + tabStop.toXml(document, tabStopsElement); + }); } /** diff --git a/src/style/ParagraphStyle.ts b/src/style/ParagraphStyle.ts index 3864447f..73e4f610 100644 --- a/src/style/ParagraphStyle.ts +++ b/src/style/ParagraphStyle.ts @@ -38,6 +38,16 @@ export class ParagraphStyle implements IParagraphStyle { return this.textProperties.getColor(); } + /** @inheritDoc */ + public setFontName(name: string): void { + this.textProperties.setFontName(name); + } + + /** @inheritDoc */ + public getFontName(): string | undefined { + return this.textProperties.getFontName(); + } + /** @inheritDoc */ public setFontSize(size: number): void { return this.textProperties.setFontSize(size); @@ -129,6 +139,7 @@ export class ParagraphStyle implements IParagraphStyle { // text properties const color = this.textProperties.getColor(); hash.update(color !== undefined ? color.toHex() : ""); + hash.update(this.textProperties.getFontName() || ""); hash.update(this.textProperties.getFontSize().toString()); hash.update(this.textProperties.getTypeface().toString()); diff --git a/src/style/StyleHelper.ts b/src/style/StyleHelper.ts index 90e4fa31..fec8f5a5 100644 --- a/src/style/StyleHelper.ts +++ b/src/style/StyleHelper.ts @@ -38,8 +38,10 @@ export class StyleHelper { rootNode.setAttribute("xmlns:style", "urn:oasis:names:tc:opendocument:xmlns:style:1.0"); rootNode.setAttribute("xmlns:fo", "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"); + const officeBodyElement = rootNode.getElementsByTagName(OdfElementName.OfficeBody)[0]; + const automaticStyles = document.createElement(OdfElementName.OfficeAutomaticStyles); - rootNode.insertBefore(automaticStyles, rootNode.firstChild); + rootNode.insertBefore(automaticStyles, officeBodyElement); return automaticStyles; } diff --git a/src/style/TextProperties.ts b/src/style/TextProperties.ts index 56fabf8c..d2a2742e 100644 --- a/src/style/TextProperties.ts +++ b/src/style/TextProperties.ts @@ -16,6 +16,7 @@ const DEFAULT_TYPEFACE = Typeface.Normal; */ export class TextProperties implements ITextProperties { private color: Color | undefined; + private fontName: string | undefined; private fontSize: number; private typeface: Typeface; @@ -39,6 +40,16 @@ export class TextProperties implements ITextProperties { return this.color; } + /** @inheritDoc */ + public setFontName(name: string): void { + this.fontName = name; + } + + /** @inheritDoc */ + public getFontName(): string | undefined { + return this.fontName; + } + /** @inheritDoc */ public setFontSize(size: number): void { this.fontSize = Math.max(size, MINIMAL_FONT_SIZE); @@ -67,6 +78,7 @@ export class TextProperties implements ITextProperties { */ public isDefault(): boolean { return this.color === undefined + && this.fontName === undefined && this.fontSize === DEFAULT_FONT_SIZE && this.typeface === DEFAULT_TYPEFACE; } @@ -87,6 +99,7 @@ export class TextProperties implements ITextProperties { parent.appendChild(textPropertiesElement); this.setColorAttribute(textPropertiesElement); + this.setFontNameAttribute(textPropertiesElement); this.setFontSizeAttribute(textPropertiesElement); this.setFontStyleAttribute(textPropertiesElement); this.setFontWeightAttribute(textPropertiesElement); @@ -105,6 +118,19 @@ export class TextProperties implements ITextProperties { textPropertiesElement.setAttribute(OdfAttributeName.FormatColor, this.color.toHex()); } + /** + * Sets the `font-name` attribute if a font name is set. + * + * @param {Element} textPropertiesElement The element which will take the attribute + */ + private setFontNameAttribute(textPropertiesElement: Element): void { + if (this.fontName === undefined) { + return; + } + + textPropertiesElement.setAttribute(OdfAttributeName.StyleFontName, this.fontName); + } + /** * Sets the `font-size` attribute if the font size is different from the default font size. * diff --git a/test/TextDocument.spec.ts b/test/TextDocument.spec.ts index 769539ac..088f893e 100644 --- a/test/TextDocument.spec.ts +++ b/test/TextDocument.spec.ts @@ -1,5 +1,6 @@ import { readFile, unlink } from "fs"; import { promisify } from "util"; +import { FontPitch } from "../src/style/FontPitch"; import { HorizontalAlignment } from "../src/style/HorizontalAlignment"; import { Heading } from "../src/text/Heading"; import { List } from "../src/text/List"; @@ -25,6 +26,28 @@ describe(TextDocument.name, () => { done(); }); + describe("#declareFont", () => { + it("add svg namespace", () => { + document.declareFont("Springfield", "Springfield", FontPitch.Variable); + + expect(document.toString()).toMatch(/xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"/); + }); + + it("add font declaration to document", () => { + document.declareFont("Springfield", "Springfield", FontPitch.Variable); + + /* tslint:disable-next-line:max-line-length */ + expect(document.toString()).toMatch(/<\/office:font-face-decls>/); + }); + + it("add font declaration to document and wrap font family if it contains spaces", () => { + document.declareFont("Homer Simpson", "Homer Simpson", FontPitch.Variable); + + /* tslint:disable-next-line:max-line-length */ + expect(document.toString()).toMatch(/<\/office:font-face-decls>/); + }); + }); + describe("#addHeading", () => { it("return a heading", () => { const heading = document.addHeading(); diff --git a/test/integration.spec.ts b/test/integration.spec.ts index fa3499f7..041cbba6 100644 --- a/test/integration.spec.ts +++ b/test/integration.spec.ts @@ -2,6 +2,7 @@ import { unlink } from "fs"; import { join } from "path"; import { promisify } from "util"; import { Color } from "../src/style/Color"; +import { FontPitch } from "../src/style/FontPitch"; import { HorizontalAlignment } from "../src/style/HorizontalAlignment"; import { ParagraphStyle } from "../src/style/ParagraphStyle"; import { TabStop } from "../src/style/TabStop"; @@ -72,6 +73,14 @@ xdescribe("integration", () => { paragraph.getStyle().setColor(Color.fromRgb(62, 180, 137)); }); + it("font name", () => { + document.declareFont("Open Sans", "Open Sans", FontPitch.Variable); + + const paragraph = document.addParagraph("Open Sans"); + paragraph.setStyle(new ParagraphStyle()); + paragraph.getStyle().setFontName("Open Sans"); + }); + it("font size", () => { const paragraph = document.addParagraph("Some small text"); paragraph.setStyle(new ParagraphStyle()); diff --git a/test/style/TextProperties.spec.ts b/test/style/TextProperties.spec.ts index 302671b8..9d6657f7 100644 --- a/test/style/TextProperties.spec.ts +++ b/test/style/TextProperties.spec.ts @@ -33,6 +33,20 @@ describe(TextProperties.name, () => { }); }); + describe("#getFontName", () => { + it("return `undefined` as default", () => { + expect(properties.getFontName()).toBeUndefined(); + }); + + it("return the current font name", () => { + const testFontName = "someFont"; + + properties.setFontName(testFontName); + + expect(properties.getFontName()).toBe(testFontName); + }); + }); + describe("#setFontSize", () => { it("set a minimum font size", () => { properties.setFontSize(-42); @@ -72,6 +86,12 @@ describe(TextProperties.name, () => { expect(properties.isDefault()).toBe(false); }); + it("return false if font name was set", () => { + properties.setFontName("someFontName"); + + expect(properties.isDefault()).toBe(false); + }); + it("return false if font size was set", () => { properties.setFontSize(23); @@ -100,6 +120,13 @@ describe(TextProperties.name, () => { expect(document.toString()).toMatch(//); }); + it("set the font name", () => { + testStyle.setFontName("someFontName"); + paragraph.setStyle(testStyle); + + expect(document.toString()).toMatch(//); + }); + it("set the font size", () => { testStyle.setFontSize(23); paragraph.setStyle(testStyle);