diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce3688e..913345b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,33 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## 0.1.0 (2017-06-20) +## [Unreleased] (2018-??-??) +### Added +- **paragraph:** Add hyperlinks to a paragraph (), closes [#5](https://github.com/connium/simple-odf/issues/5) +## [0.2.0] (2018-01-12) ### Added -* **heading:** add headings to a document and modify their outline level -* **paragraph:** add paragraphs to a document and modify their text content -* **paragraph:** set page break before paragraph -* **paragraph:** set horizontal alignment -* **text-document:** create text documents and save them as flat XML ODF document +- **docs:** Add CHANGELOG +- **docs:** Improve and extend README +- **docs:** Add badges (dependencies, known vulnerabilities, version) to README +- **list:** Add basic list support (add/insert/get/set/remove item) +- **paragraph:** Overwrite text content +- **style:** Get horizontal alignment +- **test:** Add integration test + +### Changed +- **general:** Export public API from / (no namespaces) +- **paragraph:** Rename text related functions in paragraph +- **heading:** Rename headline to heading + +## 0.1.0 (2018-01-08) +### Added +- **heading:** Add headings to a document and modify their outline level +- **paragraph:** Add paragraphs to a document and modify their text content +- **paragraph:** Set page break before paragraph +- **paragraph:** Set horizontal alignment +- **text-document:** Create text documents and save them as flat XML ODF document + +[Unreleased]: https://github.com/connium/simple-odf/compare/v0.2.0...HEAD +[0.3.0]: https://github.com/connium/simple-odf/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/connium/simple-odf/compare/v0.1.0...v0.2.0 diff --git a/README.md b/README.md index 5d8677de..05846d68 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ const document = new simpleOdf.TextDocument(); document.addHeadline("My First Document"); const p1 = document.addParagraph("The quick, brown fox jumps over a lazy dog."); -p1.appendTextContent("\nThe five boxing wizards jump quickly"); +p1.appendTextContent("\nThe five boxing wizards jump quickly\n\n"); +p1.appendHyperlink("Visit me", "http://example.org/"); document.addHeadline("Credits", 2); diff --git a/src/OdfAttributeName.ts b/src/OdfAttributeName.ts index 118038a5..75e25b03 100644 --- a/src/OdfAttributeName.ts +++ b/src/OdfAttributeName.ts @@ -5,6 +5,9 @@ export enum OdfAttributeName { StyleFamily = "style:family", StyleName = "style:name", - TextStyleName = "text:style-name", TextOutlineLevel = "text:outline-level", + TextStyleName = "text:style-name", + + XlinkHref = "xlink:href", + XlinkType = "xlink:type", } diff --git a/src/OdfElementName.ts b/src/OdfElementName.ts index 550abd4f..c1f67d2c 100644 --- a/src/OdfElementName.ts +++ b/src/OdfElementName.ts @@ -8,6 +8,7 @@ export enum OdfElementName { StyleTextProperties = "style:text-properties", StyleParagraphProperties = "style:paragraph-properties", + TextHyperlink = "text:a", TextHeading = "text:h", TextLineBreak = "text:line-break", TextList = "text:list", diff --git a/src/text/HyperLink.ts b/src/text/HyperLink.ts new file mode 100644 index 00000000..46da737e --- /dev/null +++ b/src/text/HyperLink.ts @@ -0,0 +1,61 @@ +import { OdfAttributeName } from "../OdfAttributeName"; +import { OdfElementName } from "../OdfElementName"; +import { Text } from "./Text"; + +/** + * This class represents a hyperlink in a paragraph. + * + * @since 0.3.0 + */ +export class Hyperlink extends Text { + /** + * Creates a hyperlink + * + * @param {string} text The text content of the hyperlink + * @param {string} uri The URI of the hyperlink + * @since 0.3.0 + */ + public constructor(text: string, private uri: string) { + super(text); + } + + /** + * Returns the URI of this hyperlink. + * + * @returns {string} The URI of this hyperlink + * @since 0.3.0 + */ + public getURI(): string { + return this.uri; + } + + /** + * Sets the URI for this hyperlink. + * + * @param {string} uri The new URI of this hyperlink + * @since 0.3.0 + */ + public setURI(uri: string): void { + this.uri = uri; + } + + /** @inheritDoc */ + protected toXML(document: Document, parent: Element): void { + const text = this.getText(); + + if (text === undefined || text === "") { + return; + } + + (document.firstChild as Element).setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); + + const hyperlink = document.createElement(OdfElementName.TextHyperlink); + hyperlink.setAttribute(OdfAttributeName.XlinkType, "simple"); + hyperlink.setAttribute(OdfAttributeName.XlinkHref, this.uri); + + const textNode = document.createTextNode(text); + hyperlink.appendChild(textNode); + + parent.appendChild(hyperlink); + } +} diff --git a/src/text/Paragraph.ts b/src/text/Paragraph.ts index 094390da..c5f2d642 100644 --- a/src/text/Paragraph.ts +++ b/src/text/Paragraph.ts @@ -3,6 +3,8 @@ import { OdfElement } from "../OdfElement"; import { OdfElementName } from "../OdfElementName"; import { HorizontalAlignment } from "../style/HorizontalAlignment"; import { Style } from "../style/Style"; +import { Hyperlink } from "./HyperLink"; +import { Text } from "./Text"; /** * This class represents a paragraph. @@ -11,7 +13,6 @@ import { Style } from "../style/Style"; * @since 0.1.0 */ export class Paragraph extends OdfElement { - private text: string | undefined; private style: Style; /** @@ -23,18 +24,24 @@ export class Paragraph extends OdfElement { public constructor(text?: string) { super(); - this.text = text; + this.appendText(text || ""); + this.style = new Style(); } /** * Returns the text content of this paragraph. + * Note: This will only return the text; other elements and markup will be omitted. * - * @returns {string | undefined} The text content of this paragraph + * @returns {string} The text content of this paragraph * @since 0.1.0 */ - public getText(): string | undefined { - return this.text; + public getText(): string { + return this.getElements() + .map((value: OdfElement) => { + return value instanceof Text ? value.getText() : ""; + }) + .join(""); } /** @@ -44,22 +51,27 @@ export class Paragraph extends OdfElement { * @since 0.1.0 */ public appendText(text: string): void { - if (this.text === undefined) { - this.text = text; + const elements = this.getElements(); + + if (elements.length > 0 && elements[elements.length - 1].constructor.name === Text.name) { + const lastElement = elements[elements.length - 1] as Text; + lastElement.setText(lastElement.getText() + text); return; } - this.text += text; + this.appendElement(new Text(text)); } /** * Sets the text content of this paragraph. + * Note: This will replace any existing content of the paragraph. * * @param {string} text The text content * @since 0.1.0 */ public setText(text: string): void { - this.text = text; + this.removeText(); + this.appendText(text || ""); } /** @@ -68,7 +80,22 @@ export class Paragraph extends OdfElement { * @since 0.1.0 */ public removeText(): void { - this.text = undefined; + const elements = this.getElements(); + + for (let i = elements.length - 1; i >= 0; i--) { + this.removeElement(i); + } + } + + /** + * Appends the specified text as hyperlink to the end of this paragraph. + * + * @param {string} text The text content of the hyperlink + * @param {string} uri The URI of the hyperlink + * @since 0.3.0 + */ + public appendHyperlink(text: string, uri: string): void { + this.appendElement(new Hyperlink(text, uri)); } /** @@ -112,10 +139,11 @@ export class Paragraph extends OdfElement { /** @inheritDoc */ protected toXML(document: Document, parent: Element): void { + (document.firstChild as Element).setAttribute("xmlns:text", "urn:oasis:names:tc:opendocument:xmlns:text:1.0"); + const paragraph = this.createElement(document); this.appendStyle(document, paragraph); - this.appendTextContent(document, paragraph); parent.appendChild(paragraph); @@ -135,31 +163,4 @@ export class Paragraph extends OdfElement { paragraph.setAttribute(OdfAttributeName.TextStyleName, this.style.getName()); } } - - /** - * Appends the text of the paragraph to the paragraph element. - * Newlines will be replaced with line breaks. - * - * @param {Document} document The XML document - * @param {Element} paragraph The paragraph the text belongs to - */ - private appendTextContent(document: Document, paragraph: Element): void { - if (this.text === undefined) { - return; - } - - (document.firstChild as Element).setAttribute("xmlns:text", "urn:oasis:names:tc:opendocument:xmlns:text:1.0"); - - const lines = this.text.split("\n"); - - for (let i = 0; i < lines.length; i++) { - if (i > 0) { - const lineBreak = document.createElement(OdfElementName.TextLineBreak); - paragraph.appendChild(lineBreak); - } - - const textNode = document.createTextNode(lines[i]); - paragraph.appendChild(textNode); - } - } } diff --git a/src/text/Text.ts b/src/text/Text.ts new file mode 100644 index 00000000..3b986908 --- /dev/null +++ b/src/text/Text.ts @@ -0,0 +1,58 @@ +import { OdfElement } from "../OdfElement"; +import { OdfElementName } from "../OdfElementName"; + +/** + * This class represents text in a paragraph. + * + * @since 0.3.0 + */ +export class Text extends OdfElement { + /** + * Creates a text + * + * @param {string} text The text content + * @since 0.3.0 + */ + public constructor(private text: string) { + super(); + } + + /** + * Returns the text content. + * + * @returns {string} The text content + * @since 0.3.0 + */ + public getText(): string { + return this.text; + } + + /** + * Sets the new text content. + * + * @param {string} text The new text content + * @since 0.3.0 + */ + public setText(text: string): void { + this.text = text; + } + + /** @inheritDoc */ + protected toXML(document: Document, parent: Element): void { + if (this.text === undefined || this.text === "") { + return; + } + + const lines = this.text.split("\n"); + + for (let i = 0; i < lines.length; i++) { + if (i > 0) { + const lineBreak = document.createElement(OdfElementName.TextLineBreak); + parent.appendChild(lineBreak); + } + + const textNode = document.createTextNode(lines[i]); + parent.appendChild(textNode); + } + } +} diff --git a/test/integration.spec.ts b/test/integration.spec.ts index be455833..86acf500 100644 --- a/test/integration.spec.ts +++ b/test/integration.spec.ts @@ -34,6 +34,10 @@ describe(TextDocument.name, () => { const heading30 = document.addHeading("Another chapter"); heading30.setPageBreak(); + const para2 = document.addParagraph("This is just an "); + para2.appendHyperlink("example", "http://example.org"); + para2.appendText("."); + await document.saveFlat(FILEPATH); done(); }); diff --git a/test/text/Heading.spec.ts b/test/text/Heading.spec.ts index 36a717d8..e9b8e3f1 100644 --- a/test/text/Heading.spec.ts +++ b/test/text/Heading.spec.ts @@ -9,12 +9,18 @@ describe(Heading.name, () => { document = new TextDocument(); }); + it("add text namespace", () => { + document.addHeading(); + + const documentAsString = document.toString(); + expect(documentAsString).toMatch(/xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"/); + }); + it("insert an empty heading with default level 1", () => { document.addHeading(); const documentAsString = document.toString(); expect(documentAsString).toMatch(//); - expect(documentAsString).not.toMatch(/xmlns:text/); }); it("insert a heading with given text and default level 1", () => { @@ -22,7 +28,6 @@ describe(Heading.name, () => { const documentAsString = document.toString(); expect(documentAsString).toMatch(/heading<\/text:h>/); - expect(documentAsString).toMatch(/xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"/); }); it("insert a heading with given text and given level", () => { diff --git a/test/text/Paragraph.spec.ts b/test/text/Paragraph.spec.ts index 0c19beb5..ebac79a8 100644 --- a/test/text/Paragraph.spec.ts +++ b/test/text/Paragraph.spec.ts @@ -1,6 +1,6 @@ +import { HorizontalAlignment } from "../../src/style/HorizontalAlignment"; import { Paragraph } from "../../src/text/Paragraph"; import { TextDocument } from "../../src/TextDocument"; -import { HorizontalAlignment } from "../../src/style/HorizontalAlignment"; describe(Paragraph.name, () => { let document: TextDocument; @@ -9,26 +9,34 @@ describe(Paragraph.name, () => { document = new TextDocument(); }); + it("add text namespace", () => { + document.addParagraph(); + + const documentAsString = document.toString(); + expect(documentAsString).toMatch(/xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"/); + }); + it("insert an empty paragraph", () => { document.addParagraph(); const documentAsString = document.toString(); expect(documentAsString).toMatch(//); - expect(documentAsString).not.toMatch(/xmlns:text/); }); - it("insert a paragraph with specified text and add text namespace", () => { + it("insert a paragraph with specified text", () => { document.addParagraph("some text"); const documentAsString = document.toString(); expect(documentAsString).toMatch(/some text<\/text:p>/); - expect(documentAsString).toMatch(/xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"/); }); it("return the text", () => { const paragraph = document.addParagraph("some text"); + paragraph.appendText(" some\nmore text"); + paragraph.appendHyperlink(" link", "http://example.org/"); + paragraph.appendText(" even more text"); - expect(paragraph.getText()).toEqual("some text"); + expect(paragraph.getText()).toEqual("some text some\nmore text link even more text"); }); describe("#appendText", () => { @@ -57,13 +65,12 @@ describe(Paragraph.name, () => { expect(documentAsString).toMatch(/some other text<\/text:p>/); }); - it("remove text from paragraph and not add text namespace", () => { + it("remove text from paragraph", () => { const paragraph = document.addParagraph("some text"); paragraph.removeText(); const documentAsString = document.toString(); expect(documentAsString).toMatch(//); - expect(documentAsString).not.toMatch(/xmlns:text/); }); it("replace newline with line break", () => { @@ -73,6 +80,34 @@ describe(Paragraph.name, () => { expect(documentAsString).toMatch(/some textsome more text<\/text:p>/); }); + describe("#appendHyperlink", () => { + it("add xlink namespace", () => { + const paragraph = document.addParagraph(); + paragraph.appendHyperlink("some linked text", "http://example.org/"); + + const documentAsString = document.toString(); + expect(documentAsString).toMatch(/xmlns:xlink="http:\/\/www.w3.org\/1999\/xlink"/); + }); + + it("append a linked text", () => { + const paragraph = document.addParagraph("some text"); + paragraph.appendHyperlink(" some linked text", "http://example.org/"); + + const documentAsString = document.toString(); + /* tslint:disable-next-line:max-line-length */ + expect(documentAsString).toMatch(/some text some linked text<\/text:a><\/text:p>/); + }); + + it("not create a hyperlink if text is empty", () => { + const paragraph = document.addParagraph("some text"); + paragraph.appendHyperlink("", "http://example.org/"); + + const documentAsString = document.toString(); + /* tslint:disable-next-line:max-line-length */ + expect(documentAsString).toMatch(/some text<\/text:p>/); + }); + }); + describe("style", () => { let paragraph: Paragraph;