From 3af88316770516c66f00df1a0f5891e1f5fea253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20N=C3=B6lke?= Date: Sun, 17 Feb 2019 21:58:58 +0100 Subject: [PATCH] refactor: split API and serialization logic (#62) BREAKING CHANGE: `addHeading`, `addList` and `addParagraph` are no longer available on `TextDocument` but are added to the `TextBody` which can be retrieved from the document (`getBody`). The code was largely restructured: The domain model classes (except for the style stuff) were moved to `/api` and the DOM generation is placed in `/xml`. The styles will get some extra love as soon as common styles will be implemented and thus are excluded for now. All tests are placed next to their corresponding classes. Except for the styles, the public API now is fluent and its documentation follows the same pattern and includes examples. Fixes: #60 --- .gitignore | 2 +- CHANGELOG.md | 1 + README.md | 15 +- docs/API.md | 1640 ++++++++++------- package.json | 4 +- src/TextDocument.spec.ts | 150 -- src/TextDocument.ts | 206 --- src/{ => api}/OdfElement.ts | 36 +- src/api/draw/Image.spec.ts | 46 + src/api/draw/Image.ts | 85 + src/api/draw/index.ts | 1 + src/{ => api}/meta/Meta.spec.ts | 71 - src/{ => api}/meta/Meta.ts | 240 +-- src/api/meta/index.ts | 1 + src/api/office/TextBody.spec.ts | 34 + src/api/office/TextBody.ts | 59 + src/api/office/TextDocument.spec.ts | 84 + src/api/office/TextDocument.ts | 132 ++ src/api/office/index.ts | 2 + src/api/style/FontFace.spec.ts | 32 + src/api/style/FontFace.ts | 25 + src/{ => api}/style/FontPitch.ts | 0 src/api/style/index.ts | 2 + src/api/text/Heading.spec.ts | 52 + src/api/text/Heading.ts | 64 + src/api/text/Hyperlink.spec.ts | 40 + src/api/text/Hyperlink.ts | 64 + src/{ => api}/text/List.spec.ts | 18 +- src/api/text/List.ts | 179 ++ src/{ => api}/text/ListItem.ts | 21 +- src/api/text/OdfTextElement.ts | 39 + src/api/text/Paragraph.spec.ts | 94 + src/api/text/Paragraph.ts | 195 ++ src/api/text/index.ts | 6 + src/draw/Image.spec.ts | 59 - src/draw/Image.ts | 81 - src/index.ts | 25 +- src/style/Color.ts | 4 +- src/style/ImageStyle.spec.ts | 6 +- src/style/ImageStyle.ts | 2 +- src/style/ParagraphProperties.spec.ts | 6 +- src/style/ParagraphProperties.ts | 4 +- src/style/ParagraphStyle.spec.ts | 8 +- src/style/ParagraphStyle.ts | 4 +- src/style/StyleHelper.ts | 2 +- src/style/TabStop.spec.ts | 4 +- src/style/TabStop.ts | 4 +- src/style/TextProperties.spec.ts | 6 +- src/style/TextProperties.ts | 4 +- src/text/Heading.spec.ts | 66 - src/text/Heading.ts | 53 - src/text/HyperLink.ts | 64 - src/text/Hyperlink.spec.ts | 63 - src/text/List.ts | 130 -- src/text/Paragraph.spec.ts | 152 -- src/text/Paragraph.ts | 158 -- src/xml/DomVisitor.spec.ts | 185 ++ src/xml/DomVisitor.ts | 141 ++ src/{draw => xml}/DrawElementName.ts | 0 src/{ => xml}/OdfAttributeName.ts | 0 src/{ => xml}/OdfElementName.ts | 0 .../OdfTextElementWriter.ts} | 52 +- src/xml/TextDocumentWriter.spec.ts | 81 + src/xml/TextDocumentWriter.ts | 89 + src/{text => xml}/TextElementName.ts | 0 src/{ => xml}/meta/MetaElementName.ts | 0 src/xml/meta/MetaWriter.spec.ts | 91 + src/xml/meta/MetaWriter.ts | 265 +++ test/integration.spec.ts | 42 +- tsconfig.json | 7 +- tslint.json | 24 +- 71 files changed, 3218 insertions(+), 2304 deletions(-) delete mode 100644 src/TextDocument.spec.ts delete mode 100644 src/TextDocument.ts rename src/{ => api}/OdfElement.ts (82%) create mode 100644 src/api/draw/Image.spec.ts create mode 100644 src/api/draw/Image.ts create mode 100644 src/api/draw/index.ts rename src/{ => api}/meta/Meta.spec.ts (75%) rename src/{ => api}/meta/Meta.ts (65%) create mode 100644 src/api/meta/index.ts create mode 100644 src/api/office/TextBody.spec.ts create mode 100644 src/api/office/TextBody.ts create mode 100644 src/api/office/TextDocument.spec.ts create mode 100644 src/api/office/TextDocument.ts create mode 100644 src/api/office/index.ts create mode 100644 src/api/style/FontFace.spec.ts create mode 100644 src/api/style/FontFace.ts rename src/{ => api}/style/FontPitch.ts (100%) create mode 100644 src/api/style/index.ts create mode 100644 src/api/text/Heading.spec.ts create mode 100644 src/api/text/Heading.ts create mode 100644 src/api/text/Hyperlink.spec.ts create mode 100644 src/api/text/Hyperlink.ts rename src/{ => api}/text/List.spec.ts (88%) create mode 100644 src/api/text/List.ts rename src/{ => api}/text/ListItem.ts (50%) create mode 100644 src/api/text/OdfTextElement.ts create mode 100644 src/api/text/Paragraph.spec.ts create mode 100644 src/api/text/Paragraph.ts create mode 100644 src/api/text/index.ts delete mode 100644 src/draw/Image.spec.ts delete mode 100644 src/draw/Image.ts delete mode 100644 src/text/Heading.spec.ts delete mode 100644 src/text/Heading.ts delete mode 100644 src/text/HyperLink.ts delete mode 100644 src/text/Hyperlink.spec.ts delete mode 100644 src/text/List.ts delete mode 100644 src/text/Paragraph.spec.ts delete mode 100644 src/text/Paragraph.ts create mode 100644 src/xml/DomVisitor.spec.ts create mode 100644 src/xml/DomVisitor.ts rename src/{draw => xml}/DrawElementName.ts (100%) rename src/{ => xml}/OdfAttributeName.ts (100%) rename src/{ => xml}/OdfElementName.ts (100%) rename src/{text/OdfTextElement.ts => xml/OdfTextElementWriter.ts} (77%) create mode 100644 src/xml/TextDocumentWriter.spec.ts create mode 100644 src/xml/TextDocumentWriter.ts rename src/{text => xml}/TextElementName.ts (100%) rename src/{ => xml}/meta/MetaElementName.ts (100%) create mode 100644 src/xml/meta/MetaWriter.spec.ts create mode 100644 src/xml/meta/MetaWriter.ts diff --git a/.gitignore b/.gitignore index ddaef248..316981ca 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,6 @@ coverage/ examples/ lib/ node_modules/ -test/example +example.spec.ts TODO.md diff --git a/CHANGELOG.md b/CHANGELOG.md index accfdf85..748a7b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Changed - **meta:** Use `Date` instead of `number` when dealing with dates - **chore:** Update dev dependencies, place test code next to production code, mention contributors in package.json +- **refactor:** Split API and serialization logic, closes [#60](https://github.com/connium/simple-odf/issues/60) ## [0.6.0] (2018-10-12) ### Added diff --git a/README.md b/README.md index 454b7664..1d84b785 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,15 @@ Create your first document. const simpleOdf = require("simple-odf"); const document = new simpleOdf.TextDocument(); +const body = document.getBody(); -const image = document.addParagraph().addImage("/home/homer/myself.png"); +const image = body.addParagraph().addImage("/home/homer/myself.png"); image.getStyle().setAnchorType(simpleOdf.AnchorType.AsChar); image.getStyle().setSize(29.4, 36.5); -document.addHeading("Welcome to simple-odf"); +body.addHeading("Welcome to simple-odf"); -const p1 = document.addParagraph("The quick, brown fox jumps over a lazy dog."); +const p1 = body.addParagraph("The quick, brown fox jumps over a lazy dog."); p1.addText("\nThe five boxing wizards jump quickly.\n\n"); p1.addHyperlink("Visit me", "http://example.org/"); const style1 = new simpleOdf.ParagraphStyle(); @@ -45,15 +46,15 @@ style1.setKeepTogether(); 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 p2 = body.addParagraph("It always seems impossible until it's done."); const style2 = new simpleOdf.ParagraphStyle(); style1.setFontName("Open Sans"); -document.addHeading("Credits", 2); +body.addHeading("Credits", 2); -document.addParagraph("This was quite easy. Do you want to know why?"); +body.addParagraph("This was quite easy. Do you want to know why?"); -const list = document.addList(); +const list = body.addList(); list.addItem("one-liner setup"); list.addItem("just write like you would do in a full-blown editor"); diff --git a/docs/API.md b/docs/API.md index dcb6d0af..50805390 100644 --- a/docs/API.md +++ b/docs/API.md @@ -3,11 +3,35 @@
Image

This class represents an image in a paragraph.

+

It is used to embed image data in BASE64 encoding.

Meta

This class represents the metadata of a document.

It is used to set descriptive information about the document.

+
TextBody
+

This class represents the content of a text document.

+
+
TextDocument
+

This class represents a text document in OpenDocument format.

+
+
HeadingParagraph
+

This class represents a heading in a document.

+

It is used to structure a document into multiple sections. +A chapter or section begins with a heading and extends to the next heading at the same or higher level.

+
+
Hyperlink
+

This class represents a hyperlink in a paragraph.

+
+
List
+

This class represents a list and may contain any number list items.

+
+
ListItem
+

This class represents an item in a list.

+
+
Paragraph
+

This class represents a paragraph.

+
Color

This class represents a Color.

@@ -25,25 +49,6 @@

Tab stops are used to align text in a paragraph. To become effective they must be set to the style of the respective paragraph.

-
Heading
-

This class represents a heading.

-
-
Hyperlink
-

This class represents a hyperlink in a paragraph.

-
-
List
-

This class represents a list. -It can contain multiple list items.

-
-
ListItem
-

This class represents an item in a list.

-
-
Paragraph
-

This class represents a paragraph.

-
-
TextDocument
-

This class represents an empty ODF text document.

-
@@ -51,13 +56,15 @@ It can contain multiple list items.

## Image This class represents an image in a paragraph. +It is used to embed image data in BASE64 encoding. + **Since**: 0.3.0 * [Image](#Image) * [`new Image(path)`](#new_Image_new) - * [`.setStyle(style)`](#Image+setStyle) + * [`.getPath()`](#Image+getPath) ⇒ string + * [`.setStyle(style)`](#Image+setStyle) ⇒ [Image](#Image) * [`.getStyle()`](#Image+getStyle) ⇒ IImageStyle - * [`.toXml()`](#Image+toXml) * * * @@ -71,18 +78,51 @@ Creates an image - path string Path to the image file that should be embedded +**Example** +```js +document.getBody() + .addParagraph() + .addImage("/home/homer/myself.png") + .getStyle() + .setSize(42, 23); +``` + +* * * + + + +### `image.getPath()` ⇒ string +The `getPath()` method returns the path to the image file that should be embedded. + +**Return value** +string - The path to the image file + +**Example** +```js +const image = new Image("/home/homer/myself.png"); +image.getPath(); // '/home/homer/myself.png' +``` +**Since**: 0.7.0 * * * -### `image.setStyle(style)` +### `image.setStyle(style)` ⇒ [Image](#Image) Sets the new style of this image. #### Parameters - style IImageStyle The new style +**Return value** +[Image](#Image) - The `Image` object + +**Example** +```js +const image = new Image("/home/homer/myself.png"); +image.setStyle(new ImageStyle()); +``` **Since**: 0.5.0 * * * @@ -95,16 +135,17 @@ Returns the style of this image. **Return value** IImageStyle - The style of the image +**Example** +```js +const image = new Image("/home/homer/myself.png"); +image.getStyle(); // default style +image.setStyle(new ImageStyle()); +image.getStyle(); // previously set style +``` **Since**: 0.5.0 * * * - - -### `image.toXml()` - -* * * - ## Meta @@ -118,9 +159,9 @@ It is used to set descriptive information about the document. * [`new Meta()`](#new_Meta_new) * [`.setCreator(creator)`](#Meta+setCreator) ⇒ [Meta](#Meta) * [`.getCreator()`](#Meta+getCreator) ⇒ string \| undefined - * [`.getCreationDate()`](#Meta+getCreationDate) ⇒ number + * [`.getCreationDate()`](#Meta+getCreationDate) ⇒ Date * [`.setDate(date)`](#Meta+setDate) ⇒ [Meta](#Meta) - * [`.getDate()`](#Meta+getDate) ⇒ number \| undefined + * [`.getDate()`](#Meta+getDate) ⇒ Date \| undefined * [`.setDescription(description)`](#Meta+setDescription) ⇒ [Meta](#Meta) * [`.getDescription()`](#Meta+getDescription) ⇒ string \| undefined * [`.getEditingCycles()`](#Meta+getEditingCycles) ⇒ number @@ -134,14 +175,13 @@ It is used to set descriptive information about the document. * [`.setLanguage(language)`](#Meta+setLanguage) ⇒ [Meta](#Meta) * [`.getLanguage()`](#Meta+getLanguage) ⇒ string \| undefined * [`.setPrintDate(printDate)`](#Meta+setPrintDate) ⇒ [Meta](#Meta) - * [`.getPrintDate()`](#Meta+getPrintDate) ⇒ number \| undefined + * [`.getPrintDate()`](#Meta+getPrintDate) ⇒ Date \| undefined * [`.setPrintedBy(printedBy)`](#Meta+setPrintedBy) ⇒ [Meta](#Meta) * [`.getPrintedBy()`](#Meta+getPrintedBy) ⇒ string \| undefined * [`.setSubject(subject)`](#Meta+setSubject) ⇒ [Meta](#Meta) * [`.getSubject()`](#Meta+getSubject) ⇒ string \| undefined * [`.setTitle(title)`](#Meta+setTitle) ⇒ [Meta](#Meta) * [`.getTitle()`](#Meta+getTitle) ⇒ string \| undefined - * [`.toXml(document, root)`](#Meta+toXml) * * * @@ -213,13 +253,13 @@ meta.getCreator(); // 'Lisa Simpson' -### `meta.getCreationDate()` ⇒ number +### `meta.getCreationDate()` ⇒ Date The `getCreationDate()` method returns the UTC timestamp specifying the date and time when a document was created. The creation date is initialized with the UTC timestamp of the moment the `Meta` instance was created. **Return value** -number - The UTC timestamp specifying the date and time when a document was created +Date - A `Date` instance specifying the date and time when a document was created **Example** ```js @@ -236,9 +276,9 @@ meta.getCreationDate(); // 1585742400000 The `setDate()` method sets the date and time when the document was last modified. #### Parameters -- date number | undefined -The UTC timestamp specifying the date and time when the document was last modified - or `undefined` to unset the date +- date Date | undefined +A `Date` instance specifying the date and time when the document was last modified + or `undefined` to unset the date **Return value** [Meta](#Meta) - The `Meta` object @@ -246,7 +286,7 @@ The UTC timestamp specifying the date and time when the document was last modifi **Example** ```js const meta = new Meta(); -meta.setDate(Date.now()); // 2020-07-23 13:37:00 +meta.setDate(new Date()); // 2020-07-23 13:37:00 ``` **Since**: 0.6.0 @@ -254,18 +294,18 @@ meta.setDate(Date.now()); // 2020-07-23 13:37:00 -### `meta.getDate()` ⇒ number \| undefined +### `meta.getDate()` ⇒ Date \| undefined The `getDate()` method returns the date and time when the document was last modified. **Return value** -number \| undefined - The UTC timestamp specifying the date and time when the document was last modified - or `undefined` if the date is not set +Date \| undefined - A `Date` instance specifying the date and time when the document was last modified + or `undefined` if the date is not set **Example** ```js const meta = new Meta(); meta.getDate(); // undefined -meta.setDate(Date.now()); // 2020-07-23 13:37:00 +meta.setDate(new Date()); // 2020-07-23 13:37:00 meta.getDate(); // 1595511420000 ``` **Since**: 0.6.0 @@ -528,9 +568,9 @@ meta.getLanguage(); // 'en-US' The `setPrintDate()` method sets the date and time when the document was last printed. #### Parameters -- printDate number | undefined -The UTC timestamp specifying the date and time when the document was last - printed or `undefined` to unset the print date +- printDate Date | undefined +A `Date` instance specifying the date and time when the document was last + printed or `undefined` to unset the print date **Return value** [Meta](#Meta) - The `Meta` object @@ -538,7 +578,7 @@ The UTC timestamp specifying the date and time when the document was last **Example** ```js const meta = new Meta(); -meta.setPrintDate(Date.now()); // 2020-07-23 13:37:00 +meta.setPrintDate(new Date()); // 2020-07-23 13:37:00 ``` **Since**: 0.6.0 @@ -546,18 +586,18 @@ meta.setPrintDate(Date.now()); // 2020-07-23 13:37:00 -### `meta.getPrintDate()` ⇒ number \| undefined +### `meta.getPrintDate()` ⇒ Date \| undefined The `getPrintDate()` method returns the date and time when the document was last printed. **Return value** -number \| undefined - The UTC timestamp specifying the date and time when the document was last printed - or `undefined` if the print date is not set +Date \| undefined - A `Date` instance specifying the date and time when the document was last printed + or `undefined` if the print date is not set **Example** ```js const meta = new Meta(); meta.getPrintDate(); // undefined -meta.setPrintDate(Date.now()); // 2020-07-23 13:37:00 +meta.setPrintDate(new Date()); // 2020-07-23 13:37:00 meta.getPrintDate(); // 1595511420000 ``` **Since**: 0.6.0 @@ -689,1077 +729,1393 @@ meta.getTitle(); // 'Memoirs of Homer Simpson' * * * - - -### `meta.toXml(document, root)` -Transforms the text style into Open Document Format. - -#### Parameters -- document Document -The XML document -- root Element -The root node in the DOM - -**Since**: 0.6.0 - -* * * - - + -## Color -This class represents a Color. +## TextBody +This class represents the content of a text document. -**Since**: 0.4.0 +**Since**: 0.7.0 -* [Color](#Color) - * [`new Color(red, green, blue)`](#new_Color_new) - * _instance_ - * [`.toHex()`](#Color+toHex) ⇒ string - * _static_ - * [`.fromHex(value)`](#Color.fromHex) ⇒ [Color](#Color) \| undefined - * [`.fromRgb(red, green, blue)`](#Color.fromRgb) ⇒ [Color](#Color) \| undefined +* [TextBody](#TextBody) + * [`.addHeading([text], [level])`](#TextBody+addHeading) ⇒ [Heading](#Heading) + * [`.addList()`](#TextBody+addList) ⇒ [List](#List) + * [`.addParagraph([text])`](#TextBody+addParagraph) ⇒ [Paragraph](#Paragraph) * * * - + -### `new Color(red, green, blue)` -Creates a new color with the specified channel values. +### `textBody.addHeading([text], [level])` ⇒ [Heading](#Heading) +Adds a heading at the end of the document. +If a text is given, this will be set as text content of the heading. #### Parameters -- red number -The red channel of the color -- green number -The green channel of the color -- blue number -The blue channel of the color - - -* * * - - - -### `color.toHex()` ⇒ string -The toHex() method returns a string representing the color as hex string. +- [text] string +The text content of the heading +- [level] number = 1 +The heading level; defaults to 1 if omitted **Return value** -string - A hex string representing the color +[Heading](#Heading) - The newly added heading -**Since**: 0.4.0 +**Since**: 0.7.0 * * * - - -### `Color.fromHex(value)` ⇒ [Color](#Color) \| undefined -The `Color.fromHex()` method parses a string argument and returns a color. -The string is expected to be in `#rrggbb` or `rrggbb` format. + -#### Parameters -- value string -The value you want to parse +### `textBody.addList()` ⇒ [List](#List) +Adds an empty list at the end of the document. **Return value** -[Color](#Color) \| undefined - A color parsed from the given value. -If the value cannot be converted to a color, `undefined` is returned. +[List](#List) - The newly added list -**Since**: 0.4.0 +**Since**: 0.7.0 * * * - + -### `Color.fromRgb(red, green, blue)` ⇒ [Color](#Color) \| undefined -The `Color.fromRgb()` method returns a color with the channel arguments. +### `textBody.addParagraph([text])` ⇒ [Paragraph](#Paragraph) +Adds a paragraph at the end of the document. +If a text is given, this will be set as text content of the paragraph. #### Parameters -- red number -The red channel of the color with a range of 0...255 -- green number -The green channel of the color with a range of 0...255 -- blue number -The blue channel of the color with a range of 0...255 +- [text] string +The text content of the paragraph **Return value** -[Color](#Color) \| undefined - A color parsed from the given value. -If any channel is outside the allowable range, `undefined` is returned. +[Paragraph](#Paragraph) - The newly added paragraph -**Since**: 0.4.0 +**Since**: 0.7.0 * * * - + -## ImageStyle -This class represents the style of an image +## TextDocument +This class represents a text document in OpenDocument format. -**Since**: 0.5.0 +**Since**: 0.1.0 -* [ImageStyle](#ImageStyle) - * [`new ImageStyle()`](#new_ImageStyle_new) - * [`.setAnchorType()`](#ImageStyle+setAnchorType) - * [`.getAnchorType()`](#ImageStyle+getAnchorType) - * [`.setHeight(height)`](#ImageStyle+setHeight) - * [`.getHeight()`](#ImageStyle+getHeight) ⇒ number \| undefined - * [`.setWidth(width)`](#ImageStyle+setWidth) - * [`.getWidth()`](#ImageStyle+getWidth) ⇒ number \| undefined - * [`.setSize(width, height)`](#ImageStyle+setSize) - * [`.toXml()`](#ImageStyle+toXml) +* [TextDocument](#TextDocument) + * [`.getBody()`](#TextDocument+getBody) ⇒ [TextBody](#TextBody) + * [`.declareFont(name, fontFamily, fontPitch)`](#TextDocument+declareFont) ⇒ FontFace + * [`.getFonts()`](#TextDocument+getFonts) ⇒ Array.<FontFace> + * [`.getMeta()`](#TextDocument+getMeta) ⇒ [Meta](#Meta) + * [`.saveFlat(filePath)`](#TextDocument+saveFlat) ⇒ Promise.<void> + * ~~[`.toString()`](#TextDocument+toString) ⇒ string~~ * * * - + -### `new ImageStyle()` -Constructor. +### `textDocument.getBody()` ⇒ [TextBody](#TextBody) +The `getBody()` method returns the content of the document. +**Return value** +[TextBody](#TextBody) - A `TextBody` object that holds the content of the document + +**Example** +```js +new TextDocument() + .getBody() + .addHeading('My first document'); +``` +**Since**: 0.7.0 * * * - + -### `imageStyle.setAnchorType()` +### `textDocument.declareFont(name, fontFamily, fontPitch)` ⇒ FontFace +The `declareFont` method creates a font face 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.** - +#### Parameters +- name string +The name of the font; this name must be set to a [ParagraphStyle](#ParagraphStyle) +- fontFamily string +The name of the font family +- fontPitch FontPitch +The pitch of the font -### `imageStyle.getAnchorType()` +**Return value** +FontFace - The declared `FontFace` object + +**Example** +```js +new TextDocument() + .declareFont("FreeSans", "FreeSans", FontPitch.Variable); +``` +**Since**: 0.4.0 * * * - + -### `imageStyle.setHeight(height)` -Sets the target height of the image. +### `textDocument.getFonts()` ⇒ Array.<FontFace> +The `getFonts()` method returns all font face declarations for the document. -#### Parameters -- height number -The target height of the image in millimeter +**Return value** +Array.<FontFace> - A copy of the list of font face declarations for the document -**Since**: 0.5.0 +**Example** +```js +const document = new TextDocument(); +document.declareFont("FreeSans", "FreeSans", FontPitch.Variable); +document.getFonts(); +``` +**Since**: 0.7.0 * * * - + -### `imageStyle.getHeight()` ⇒ number \| undefined -Returns the target height of the image or `undefined` if no height was set. +### `textDocument.getMeta()` ⇒ [Meta](#Meta) +The `getMeta()` method returns the metadata of the document. **Return value** -number \| undefined - The target height of the image in millimeter or `undefined` if no height was set +[Meta](#Meta) - An object holding the metadata of the document -**Since**: 0.5.0 +**See**: [Meta](#Meta) +**Example** +```js +new TextDocument.getMeta() + .setCreator('Homer Simpson'); +``` +**Since**: 0.6.0 * * * - + -### `imageStyle.setWidth(width)` -Sets the target width of the image. +### `textDocument.saveFlat(filePath)` ⇒ Promise.<void> +The `saveFlat()` method converts the document into an XML string and stores it in flat open document xml format. #### Parameters -- width number -The target width of the image in millimeter +- filePath string +The file path to write to -**Since**: 0.5.0 +**Example** +```js +new TextDocument() + .saveFlat("/home/homer/document.fodt"); +``` +**Since**: 0.1.0 * * * - + -### `imageStyle.getWidth()` ⇒ number \| undefined -Returns the target width of the image or `undefined` if no width was set. +### ~~`textDocument.toString()` ⇒ string~~ +***Deprecated*** + +Returns the string representation of this document in flat open document xml format. **Return value** -number \| undefined - The target width of the image in millimeter or `undefined` if no width was set +string - The string representation of this document -**Since**: 0.5.0 +**Since**: 0.1.0 * * * - - -### `imageStyle.setSize(width, height)` -Sets the target size of the image. + -#### Parameters -- width number -The target width of the image in millimeter -- height number -The target height of the image in millimeter +## Heading ⇐ [Paragraph](#Paragraph) +This class represents a heading in a document. -**Since**: 0.5.0 +It is used to structure a document into multiple sections. +A chapter or section begins with a heading and extends to the next heading at the same or higher level. -* * * +**Extends**: [Paragraph](#Paragraph) +**Since**: 0.1.0 - +* [Heading](#Heading) ⇐ [Paragraph](#Paragraph) + * [`new Heading([text], [level])`](#new_Heading_new) + * [`.setLevel(level)`](#Heading+setLevel) ⇒ [Heading](#Heading) + * [`.getLevel()`](#Heading+getLevel) ⇒ number + * [`.addText(text)`](#Paragraph+addText) ⇒ [Paragraph](#Paragraph) + * [`.getText()`](#Paragraph+getText) ⇒ string + * [`.setText(text)`](#Paragraph+setText) ⇒ [Paragraph](#Paragraph) + * [`.addHyperlink(text, uri)`](#Paragraph+addHyperlink) ⇒ [Hyperlink](#Hyperlink) + * [`.addImage(path)`](#Paragraph+addImage) ⇒ [Image](#Image) + * [`.setStyle(style)`](#Paragraph+setStyle) ⇒ [Paragraph](#Paragraph) + * [`.getStyle()`](#Paragraph+getStyle) ⇒ IParagraphStyle \| undefined -### `imageStyle.toXml()` * * * - + -## ParagraphStyle -This class represents the style of a paragraph +### `new Heading([text], [level])` +Creates a `Heading` instance that represents a heading in a document. -**Since**: 0.4.0 - -* [ParagraphStyle](#ParagraphStyle) - * [`new ParagraphStyle()`](#new_ParagraphStyle_new) - * [`.setColor()`](#ParagraphStyle+setColor) - * [`.getColor()`](#ParagraphStyle+getColor) - * [`.setFontName()`](#ParagraphStyle+setFontName) - * [`.getFontName()`](#ParagraphStyle+getFontName) - * [`.setFontSize()`](#ParagraphStyle+setFontSize) - * [`.getFontSize()`](#ParagraphStyle+getFontSize) - * [`.setTextTransformation()`](#ParagraphStyle+setTextTransformation) - * [`.getTextTransformation()`](#ParagraphStyle+getTextTransformation) - * [`.setTypeface()`](#ParagraphStyle+setTypeface) - * [`.getTypeface()`](#ParagraphStyle+getTypeface) - * [`.setHorizontalAlignment()`](#ParagraphStyle+setHorizontalAlignment) - * [`.getHorizontalAlignment()`](#ParagraphStyle+getHorizontalAlignment) - * [`.setPageBreakBefore()`](#ParagraphStyle+setPageBreakBefore) - * [`.setKeepTogether()`](#ParagraphStyle+setKeepTogether) - * [`.getTabStops()`](#ParagraphStyle+getTabStops) - * [`.clearTabStops()`](#ParagraphStyle+clearTabStops) - * [`.toXml()`](#ParagraphStyle+toXml) +#### Parameters +- [text] string = "''" +The text content of the heading; defaults to an empty string if omitted +- [level] number = 1 +The level of the heading, starting with `1`; defaults to `1` if omitted +**Example** +```js +document.getBody().addHeading("First Headline", 1); +``` +**Example** +```js +document.getBody().addHeading() + .setText("Second Headline") + .setLevel(2); +``` * * * - + -### `new ParagraphStyle()` -Constructor. +### `heading.setLevel(level)` ⇒ [Heading](#Heading) +The `setLevel()` method sets the level of the heading, starting with `1`. +If an illegal value is provided, then the heading is assumed to be at level `1`. +#### Parameters +- level number +The level of the heading, starting with `1` + +**Return value** +[Heading](#Heading) - The `Heading` object + +**Since**: 0.1.0 * * * - + -### `paragraphStyle.setColor()` +### `heading.getLevel()` ⇒ number +The `getLevel()` method returns the level of the heading. + +**Return value** +number - The level of the heading + +**Since**: 0.1.0 * * * - + -### `paragraphStyle.getColor()` +### `heading.addText(text)` ⇒ [Paragraph](#Paragraph) +Appends the specified text to the end of the paragraph. + +#### Parameters +- text string +The additional text content + +**Return value** +[Paragraph](#Paragraph) - The `Paragraph` object + +**Example** +```js +new Paragraph("Some text") // Some text + .addText("\nEven more text"); // Some text\nEven more text +``` +**Since**: 0.1.0 * * * - + -### `paragraphStyle.setFontName()` +### `heading.getText()` ⇒ string +Returns the text content of the paragraph. +Note: This will only return the text; other elements and markup will be omitted. + +**Return value** +string - The text content of the paragraph + +**Example** +```js +const paragraph = new Paragraph("Some text, "); +paragraph.addHyperlink("some linked text"); +paragraph.addText(", even more text"); +paragraph.getText(); // Some text, some linked text, even more text +``` +**Since**: 0.1.0 * * * - + -### `paragraphStyle.getFontName()` +### `heading.setText(text)` ⇒ [Paragraph](#Paragraph) +Sets the text content of the paragraph. +Note: This will replace any existing content of the paragraph. + +#### Parameters +- text string +The new text content + +**Return value** +[Paragraph](#Paragraph) - The `Paragraph` object + +**Example** +```js +new Paragraph("Some text") // Some text + .setText("Some other text"); // Some other text +``` +**Since**: 0.1.0 * * * - + -### `paragraphStyle.setFontSize()` +### `heading.addHyperlink(text, uri)` ⇒ [Hyperlink](#Hyperlink) +Appends the specified text as hyperlink to the end of the paragraph. + +#### Parameters +- text string +The text content of the hyperlink +- uri string +The target URI of the hyperlink + +**Return value** +[Hyperlink](#Hyperlink) - The added `Hyperlink` object + +**Example** +```js +new Paragraph("Some text, ") // Some text, + .addHyperlink("some linked text"); // Some text, some linked text +``` +**Since**: 0.3.0 * * * - + -### `paragraphStyle.getFontSize()` +### `heading.addImage(path)` ⇒ [Image](#Image) +Appends the image of the denoted path to the end of the paragraph. +The current paragraph will be set as anchor for the image. + +#### Parameters +- path string +The path to the image file + +**Return value** +[Image](#Image) - The added `Image` object + +**Example** +```js +new Paragraph("Some text") + .addImage("/home/homer/myself.png"); +``` +**Since**: 0.3.0 * * * - + -### `paragraphStyle.setTextTransformation()` +### `heading.setStyle(style)` ⇒ [Paragraph](#Paragraph) +Sets the new style of the paragraph. +To reset the style, `undefined` must be given. + +#### Parameters +- style IParagraphStyle | undefined +The new style or `undefined` to reset the style + +**Return value** +[Paragraph](#Paragraph) - The `Paragraph` object + +**Example** +```js +new Paragraph("Some text") + .setStyle(new ParagraphStyle()); +``` +**Since**: 0.3.0 * * * - + -### `paragraphStyle.getTextTransformation()` +### `heading.getStyle()` ⇒ IParagraphStyle \| undefined +Returns the style of the paragraph. + +**Return value** +IParagraphStyle \| undefined - The style of the paragraph or `undefined` if no style was set + +**Example** +```js +const paragraph = new Paragraph("Some text"); +paragraph.getStyle(); // undefined +paragraph.setStyle(new ParagraphStyle()); +paragraph.getStyle(); // previously set style +``` +**Since**: 0.3.0 * * * - + + +## Hyperlink +This class represents a hyperlink in a paragraph. + +**Since**: 0.3.0 + +* [Hyperlink](#Hyperlink) + * [`new Hyperlink(text, uri)`](#new_Hyperlink_new) + * [`.setURI(uri)`](#Hyperlink+setURI) ⇒ [Hyperlink](#Hyperlink) + * [`.getURI()`](#Hyperlink+getURI) ⇒ string -### `paragraphStyle.setTypeface()` * * * - + -### `paragraphStyle.getTypeface()` +### `new Hyperlink(text, uri)` +Creates a hyperlink + +#### Parameters +- text string +The text content of the hyperlink +- uri string +The target URI of the hyperlink + +**Example** +```js +document.getBody() + .addParagraph('This is a ') + .addHyperlink('link', 'https://example.com/'); +``` * * * - + -### `paragraphStyle.setHorizontalAlignment()` +### `hyperlink.setURI(uri)` ⇒ [Hyperlink](#Hyperlink) +The `setURI()` method sets the target URI for this hyperlink. +If an illegal value is provided, the value will be ignored. + +#### Parameters +- uri string +The target URI of this hyperlink + +**Return value** +[Hyperlink](#Hyperlink) - The `Hyperlink` object + +**Example** +```js +const hyperlink = new Hyperlink('My website', 'https://example.com/'); +hyperlink.setURI('https://github.com'); // https://github.com +hyperlink.setURI(''); // https://github.com +``` +**Since**: 0.3.0 * * * - + -### `paragraphStyle.getHorizontalAlignment()` +### `hyperlink.getURI()` ⇒ string +The `getURI()` method returns the target URI of this hyperlink. + +**Return value** +string - The target URI of this hyperlink + +**Example** +```js +const hyperlink = new Hyperlink('My website', 'https://example.com/'); +hyperlink.getURI(); // https://example.com +hyperlink.setURI('https://github.com'); +hyperlink.getURI(); // https://github.com +``` +**Since**: 0.3.0 * * * - + + +## List +This class represents a list and may contain any number list items. + +**Since**: 0.2.0 + +* [List](#List) + * [`new List()`](#new_List_new) + * [`.addItem([item])`](#List+addItem) ⇒ [ListItem](#ListItem) + * [`.insertItem(position, item)`](#List+insertItem) ⇒ [ListItem](#ListItem) + * [`.getItem(position)`](#List+getItem) ⇒ [ListItem](#ListItem) \| undefined + * [`.getItems()`](#List+getItems) ⇒ [Array.<ListItem>](#ListItem) + * [`.removeItemAt(position)`](#List+removeItemAt) ⇒ [ListItem](#ListItem) \| undefined + * [`.clear()`](#List+clear) ⇒ [List](#List) + * [`.size()`](#List+size) ⇒ number -### `paragraphStyle.setPageBreakBefore()` * * * - + -### `paragraphStyle.setKeepTogether()` +### `new List()` +Creates a `List` instance that represents a list. + +**Example** +```js +const list = document.getBody().addList(); +list.addItem("First item"); +list.addItem("Second item"); +list.insertItem(1, "After first item") +list.removeItemAt(2); +``` * * * - + -### `paragraphStyle.getTabStops()` +### `list.addItem([item])` ⇒ [ListItem](#ListItem) +The `addItem()` method adds a new list item with the specified text or adds the specified item to the list. + +#### Parameters +- [item] string | [ListItem](#ListItem) +The text content of the new item or the item to add + +**Return value** +[ListItem](#ListItem) - The added `ListItem` object + +**Example** +```js +const list = new List(); +list.addItem("First item"); +list.addItem(new ListItem("Second item")); +``` +**Since**: 0.2.0 * * * - + -### `paragraphStyle.clearTabStops()` +### `list.insertItem(position, item)` ⇒ [ListItem](#ListItem) +The `insertItem` method inserts a new list item with the specified text +or inserts the specified item at the specified position. +The item is inserted before the item at the specified position. + +If the position is greater than the current number items, the new item is appended at the end of the list. +If the position is negative, the new item is inserted as first element. + +#### Parameters +- position number +The index at which to insert the list item (starting from 0). +- item string | [ListItem](#ListItem) +The text content of the new item or the item to insert + +**Return value** +[ListItem](#ListItem) - The inserted `ListItem` object + +**Example** +```js +const list = new List(); +list.addItem("First item"); // "First item" +list.addItem("Second item"); // "First item", "Second item" +list.insertItem(1, "After first item"); // "First item", "After first item", "Second item" +``` +**Since**: 0.2.0 * * * - + -### `paragraphStyle.toXml()` +### `list.getItem(position)` ⇒ [ListItem](#ListItem) \| undefined +The `getItem()` method returns the item at the specified position in the list. +If an invalid position is given, undefined is returned. + +#### Parameters +- position number +The index of the requested list item (starting from 0). + +**Return value** +[ListItem](#ListItem) \| undefined - The `ListItem` object at the specified position +or `undefined` if there is no list item at the specified position + +**Example** +```js +const list = new List(); +list.addItem("First item"); +list.addItem("Second item"); +list.getItem(1); // "Second item" +list.getItem(2); // undefined +``` +**Since**: 0.2.0 * * * - + -## StyleHelper -Utility class for dealing with styles. +### `list.getItems()` ⇒ [Array.<ListItem>](#ListItem) +The `getItems()` method returns all list items. -**Since**: 0.4.0 +**Return value** +[Array.<ListItem>](#ListItem) - A copy of the list of `ListItem` objects + +**Example** +```js +const list = new List(); +list.getItems(); // [] +list.addItem("First item"); +list.addItem("Second item"); +list.getItems(); // ["First item", "Second item"] +``` +**Since**: 0.2.0 * * * - + -### `StyleHelper.getAutomaticStylesElement(document)` ⇒ Element -Returns the `automatic-styles` element of the document. -If there is no such element yet, it will be created. +### `list.removeItemAt(position)` ⇒ [ListItem](#ListItem) \| undefined +The `removeItemAt()` method removes the list item from the specified position. #### Parameters -- document Document -The XML document +- position number +The index of the list item to remove (starting from 0). **Return value** -Element - The documents `automatic-styles` element +[ListItem](#ListItem) \| undefined - The removed `ListItem` object +or undefined if there is no list item at the specified position -**Since**: 0.4.0 +**Example** +```js +const list = new List(); +list.addItem("First item"); +list.addItem("Second item"); +list.removeItemAt(0); // "First item" +list.getItems(); // ["Second item"] +list.removeItemAt(2); // undefined +``` +**Since**: 0.2.0 * * * - + -## TabStop -This class represents a tab stop. +### `list.clear()` ⇒ [List](#List) +The `clear()` method removes all items from the list. -Tab stops are used to align text in a paragraph. -To become effective they must be set to the style of the respective paragraph. +**Return value** +[List](#List) - The `List` object -**Since**: 0.3.0 +**Example** +```js +const list = new List(); +list.addItem("First item"); // "First item" +list.addItem("Second item"); // "First item", "Second item" +list.clear(); // - +``` +**Since**: 0.2.0 -* [TabStop](#TabStop) - * [`new TabStop([position], [type])`](#new_TabStop_new) - * [`.setPosition(position)`](#TabStop+setPosition) - * [`.getPosition()`](#TabStop+getPosition) ⇒ number - * [`.setType(type)`](#TabStop+setType) - * [`.getType()`](#TabStop+getType) ⇒ TabStopType - * [`.toXml(document, parent)`](#TabStop+toXml) +* * * + + +### `list.size()` ⇒ number +The `size()` method returns the number of items in the list. + +**Return value** +number - The number of items in this list + +**Example** +```js +const list = new List(); +list.size(); // 0 +list.addItem("First item"); +list.addItem("Second item"); +list.size(); // 2 +``` +**Since**: 0.2.0 * * * - + -### `new TabStop([position], [type])` -Creates a tab stop to be set to the style of a paragraph. +## ListItem +This class represents an item in a list. + +**Since**: 0.2.0 + +* * * + + + +### `new ListItem([text])` +Creates a `ListItem` instance that represents an item in a list. #### Parameters -- [position] number -The position of the tab stop in centimeters relative to the left margin. -If a negative value is given, the `position` will be set to `0`. -- [type] TabStopType -The type of the tab stop. Defaults to `TabStopType.Left`. +- [text] string = "\"\"" +The text content of the list item; defaults to an empty string if omitted **Example** ```js -// creates a right aligned tab stop with a distance of 4 cm from the left margin -const tabStop4 = new TabStop(4, TabStopType.Right); -paragraph.getStyle().addTabStop(tabStop4); +const list = document.getBody() + .addList() + .addItem("First item"); ``` * * * - + -### `tabStop.setPosition(position)` -Sets the position of this tab stop. +## Paragraph +This class represents a paragraph. -#### Parameters -- position number -The position of the tab stop in centimeters relative to the left margin. -If a negative value is given, the `position` will be set to `0`. +**Since**: 0.1.0 + +* [Paragraph](#Paragraph) + * [`new Paragraph([text])`](#new_Paragraph_new) + * [`.addText(text)`](#Paragraph+addText) ⇒ [Paragraph](#Paragraph) + * [`.getText()`](#Paragraph+getText) ⇒ string + * [`.setText(text)`](#Paragraph+setText) ⇒ [Paragraph](#Paragraph) + * [`.addHyperlink(text, uri)`](#Paragraph+addHyperlink) ⇒ [Hyperlink](#Hyperlink) + * [`.addImage(path)`](#Paragraph+addImage) ⇒ [Image](#Image) + * [`.setStyle(style)`](#Paragraph+setStyle) ⇒ [Paragraph](#Paragraph) + * [`.getStyle()`](#Paragraph+getStyle) ⇒ IParagraphStyle \| undefined -**Since**: 0.3.0 * * * - + -### `tabStop.getPosition()` ⇒ number -Returns the position of this tab stop. +### `new Paragraph([text])` +Creates a `Paragraph` instance. -**Return value** -number - The position of this tab stop in centimeters +#### Parameters +- [text] string +The text content of the paragraph; defaults to an empty string if omitted -**Since**: 0.3.0 +**Example** +```js +document.getBody().addParagraph("Some text") + .addText("\nEven more text") + .addImage("/home/homer/myself.png"); +``` * * * - + -### `tabStop.setType(type)` -Sets the type of this tab stop. +### `paragraph.addText(text)` ⇒ [Paragraph](#Paragraph) +Appends the specified text to the end of the paragraph. #### Parameters -- type TabStopType -The type of the tab stop +- text string +The additional text content -**Since**: 0.3.0 +**Return value** +[Paragraph](#Paragraph) - The `Paragraph` object + +**Example** +```js +new Paragraph("Some text") // Some text + .addText("\nEven more text"); // Some text\nEven more text +``` +**Since**: 0.1.0 * * * - + -### `tabStop.getType()` ⇒ TabStopType -Returns the type of this tab stop. +### `paragraph.getText()` ⇒ string +Returns the text content of the paragraph. +Note: This will only return the text; other elements and markup will be omitted. **Return value** -TabStopType - The type of this tab stop +string - The text content of the paragraph -**Since**: 0.3.0 +**Example** +```js +const paragraph = new Paragraph("Some text, "); +paragraph.addHyperlink("some linked text"); +paragraph.addText(", even more text"); +paragraph.getText(); // Some text, some linked text, even more text +``` +**Since**: 0.1.0 * * * - + -### `tabStop.toXml(document, parent)` -Transforms the tab stop into Open Document Format. +### `paragraph.setText(text)` ⇒ [Paragraph](#Paragraph) +Sets the text content of the paragraph. +Note: This will replace any existing content of the paragraph. #### Parameters -- document Document -The XML document -- parent Element -The parent node in the DOM (`style:tab-stops`) +- text string +The new text content -**Since**: 0.3.0 +**Return value** +[Paragraph](#Paragraph) - The `Paragraph` object + +**Example** +```js +new Paragraph("Some text") // Some text + .setText("Some other text"); // Some other text +``` +**Since**: 0.1.0 * * * - + -## Heading -This class represents a heading. +### `paragraph.addHyperlink(text, uri)` ⇒ [Hyperlink](#Hyperlink) +Appends the specified text as hyperlink to the end of the paragraph. -**Since**: 0.1.0 +#### Parameters +- text string +The text content of the hyperlink +- uri string +The target URI of the hyperlink -* [Heading](#Heading) - * [`new Heading([text], [level])`](#new_Heading_new) - * [`.setLevel(level)`](#Heading+setLevel) - * [`.getLevel()`](#Heading+getLevel) ⇒ number - * [`.createElement()`](#Heading+createElement) +**Return value** +[Hyperlink](#Hyperlink) - The added `Hyperlink` object +**Example** +```js +new Paragraph("Some text, ") // Some text, + .addHyperlink("some linked text"); // Some text, some linked text +``` +**Since**: 0.3.0 * * * - + -### `new Heading([text], [level])` -Creates a heading +### `paragraph.addImage(path)` ⇒ [Image](#Image) +Appends the image of the denoted path to the end of the paragraph. +The current paragraph will be set as anchor for the image. #### Parameters -- [text] string -The text content of the heading -- [level] number -The heading level; defaults to 1 if omitted +- path string +The path to the image file + +**Return value** +[Image](#Image) - The added `Image` object +**Example** +```js +new Paragraph("Some text") + .addImage("/home/homer/myself.png"); +``` +**Since**: 0.3.0 * * * - + -### `heading.setLevel(level)` -Sets the level of this heading. +### `paragraph.setStyle(style)` ⇒ [Paragraph](#Paragraph) +Sets the new style of the paragraph. +To reset the style, `undefined` must be given. #### Parameters -- level number -The heading level +- style IParagraphStyle | undefined +The new style or `undefined` to reset the style -**Since**: 0.1.0 +**Return value** +[Paragraph](#Paragraph) - The `Paragraph` object + +**Example** +```js +new Paragraph("Some text") + .setStyle(new ParagraphStyle()); +``` +**Since**: 0.3.0 * * * - + -### `heading.getLevel()` ⇒ number -Returns the level of this heading. +### `paragraph.getStyle()` ⇒ IParagraphStyle \| undefined +Returns the style of the paragraph. **Return value** -number - The heading level - -**Since**: 0.1.0 - -* * * - - +IParagraphStyle \| undefined - The style of the paragraph or `undefined` if no style was set -### `heading.createElement()` +**Example** +```js +const paragraph = new Paragraph("Some text"); +paragraph.getStyle(); // undefined +paragraph.setStyle(new ParagraphStyle()); +paragraph.getStyle(); // previously set style +``` +**Since**: 0.3.0 * * * - + -## Hyperlink -This class represents a hyperlink in a paragraph. +## Color +This class represents a Color. -**Since**: 0.3.0 +**Since**: 0.4.0 -* [Hyperlink](#Hyperlink) - * [`new Hyperlink(text, uri)`](#new_Hyperlink_new) - * [`.setURI(uri)`](#Hyperlink+setURI) - * [`.getURI()`](#Hyperlink+getURI) ⇒ string - * [`.toXml()`](#Hyperlink+toXml) +* [Color](#Color) + * [`new Color(red, green, blue)`](#new_Color_new) + * _instance_ + * [`.toHex()`](#Color+toHex) ⇒ string + * _static_ + * [`.fromHex(value)`](#Color.fromHex) ⇒ [Color](#Color) \| never + * [`.fromRgb(red, green, blue)`](#Color.fromRgb) ⇒ [Color](#Color) \| never * * * - + -### `new Hyperlink(text, uri)` -Creates a hyperlink +### `new Color(red, green, blue)` +Creates a new color with the specified channel values. #### Parameters -- text string -The text content of the hyperlink -- uri string -The target URI of the hyperlink +- red number +The red channel of the color +- green number +The green channel of the color +- blue number +The blue channel of the color * * * - + -### `hyperlink.setURI(uri)` -Sets the target URI for this hyperlink. +### `color.toHex()` ⇒ string +The toHex() method returns a string representing the color as hex string. -#### Parameters -- uri string -The new target URI +**Return value** +string - A hex string representing the color -**Since**: 0.3.0 +**Since**: 0.4.0 * * * - + -### `hyperlink.getURI()` ⇒ string -Returns the target URI of this hyperlink. +### `Color.fromHex(value)` ⇒ [Color](#Color) \| never +The `Color.fromHex()` method parses a string argument and returns a color. +The string is expected to be in `#rrggbb` or `rrggbb` format. -**Return value** -string - The target URI +#### Parameters +- value string +The value you want to parse -**Since**: 0.3.0 +**Return value** +[Color](#Color) \| never - A color parsed from the given value -* * * +**Throws**: - +- Error If the value cannot be converted to a color -### `hyperlink.toXml()` +**Since**: 0.4.0 * * * - + -## List -This class represents a list. -It can contain multiple list items. +### `Color.fromRgb(red, green, blue)` ⇒ [Color](#Color) \| never +The `Color.fromRgb()` method returns a color with the channel arguments. -**Since**: 0.2.0 +#### Parameters +- red number +The red channel of the color with a range of 0...255 +- green number +The green channel of the color with a range of 0...255 +- blue number +The blue channel of the color with a range of 0...255 -* [List](#List) - * [`new List()`](#new_List_new) - * [`.addItem([item])`](#List+addItem) ⇒ [ListItem](#ListItem) - * [`.insertItem(position, item)`](#List+insertItem) ⇒ [ListItem](#ListItem) - * [`.getItem(position)`](#List+getItem) ⇒ [ListItem](#ListItem) \| undefined - * [`.getItems()`](#List+getItems) ⇒ [Array.<ListItem>](#ListItem) - * [`.removeItemAt(position)`](#List+removeItemAt) ⇒ [ListItem](#ListItem) \| undefined - * [`.clear()`](#List+clear) - * [`.size()`](#List+size) ⇒ number - * [`.toXml()`](#List+toXml) +**Return value** +[Color](#Color) \| never - A color parsed from the given value + +**Throws**: +- Error If any channel is outside the allowable range + +**Since**: 0.4.0 * * * - + -### `new List()` -Creates a list +## ImageStyle +This class represents the style of an image +**Since**: 0.5.0 -* * * +* [ImageStyle](#ImageStyle) + * [`new ImageStyle()`](#new_ImageStyle_new) + * [`.setAnchorType()`](#ImageStyle+setAnchorType) + * [`.getAnchorType()`](#ImageStyle+getAnchorType) + * [`.setHeight(height)`](#ImageStyle+setHeight) + * [`.getHeight()`](#ImageStyle+getHeight) ⇒ number \| undefined + * [`.setWidth(width)`](#ImageStyle+setWidth) + * [`.getWidth()`](#ImageStyle+getWidth) ⇒ number \| undefined + * [`.setSize(width, height)`](#ImageStyle+setSize) + * [`.toXml()`](#ImageStyle+toXml) - -### `list.addItem([item])` ⇒ [ListItem](#ListItem) -Adds a new list item with the specified text or adds the specified item to the list. +* * * -#### Parameters -- [item] string | [ListItem](#ListItem) -The text content of the new item or the item to add + -**Return value** -[ListItem](#ListItem) - The newly added list item +### `new ImageStyle()` +Constructor. -**Since**: 0.2.0 * * * - + -### `list.insertItem(position, item)` ⇒ [ListItem](#ListItem) -Inserts a new list item with the specified text or inserts the specified item at the specified position. -The item is inserted before the item at the specified position. +### `imageStyle.setAnchorType()` -#### Parameters -- position number -The index at which to insert the list item (starting from 0). -- item string | [ListItem](#ListItem) -The text content of the new item or the item to insert +* * * -**Return value** -[ListItem](#ListItem) - The newly added list item + -**Since**: 0.2.0 +### `imageStyle.getAnchorType()` * * * - + -### `list.getItem(position)` ⇒ [ListItem](#ListItem) \| undefined -Returns the item at the specified position in this list. -If an invalid position is given, undefined is returned. +### `imageStyle.setHeight(height)` +Sets the target height of the image. #### Parameters -- position number -The index of the requested the list item (starting from 0). - -**Return value** -[ListItem](#ListItem) \| undefined - The list item at the specified position -or undefined if there is no list item at the specified position +- height number +The target height of the image in millimeter -**Since**: 0.2.0 +**Since**: 0.5.0 * * * - + -### `list.getItems()` ⇒ [Array.<ListItem>](#ListItem) -Returns all list items. +### `imageStyle.getHeight()` ⇒ number \| undefined +Returns the target height of the image or `undefined` if no height was set. **Return value** -[Array.<ListItem>](#ListItem) - A copy of the list of list items +number \| undefined - The target height of the image in millimeter or `undefined` if no height was set -**Since**: 0.2.0 +**Since**: 0.5.0 * * * - + -### `list.removeItemAt(position)` ⇒ [ListItem](#ListItem) \| undefined -Removes the list item from the specified position. +### `imageStyle.setWidth(width)` +Sets the target width of the image. #### Parameters -- position number -The index of the list item to remove (starting from 0). - -**Return value** -[ListItem](#ListItem) \| undefined - The removed list item -or undefined if there is no list item at the specified position +- width number +The target width of the image in millimeter -**Since**: 0.2.0 +**Since**: 0.5.0 * * * - + + +### `imageStyle.getWidth()` ⇒ number \| undefined +Returns the target width of the image or `undefined` if no width was set. -### `list.clear()` -Removes all items from this list. +**Return value** +number \| undefined - The target width of the image in millimeter or `undefined` if no width was set -**Since**: 0.2.0 +**Since**: 0.5.0 * * * - + -### `list.size()` ⇒ number -Returns the number of items in this list. +### `imageStyle.setSize(width, height)` +Sets the target size of the image. -**Return value** -number - The number of items in this list +#### Parameters +- width number +The target width of the image in millimeter +- height number +The target height of the image in millimeter -**Since**: 0.2.0 +**Since**: 0.5.0 * * * - + -### `list.toXml()` +### `imageStyle.toXml()` * * * - + -## ListItem -This class represents an item in a list. +## ParagraphStyle +This class represents the style of a paragraph -**Since**: 0.2.0 +**Since**: 0.4.0 -* [ListItem](#ListItem) - * [`new ListItem([text])`](#new_ListItem_new) - * [`.toXml()`](#ListItem+toXml) +* [ParagraphStyle](#ParagraphStyle) + * [`new ParagraphStyle()`](#new_ParagraphStyle_new) + * [`.setColor()`](#ParagraphStyle+setColor) + * [`.getColor()`](#ParagraphStyle+getColor) + * [`.setFontName()`](#ParagraphStyle+setFontName) + * [`.getFontName()`](#ParagraphStyle+getFontName) + * [`.setFontSize()`](#ParagraphStyle+setFontSize) + * [`.getFontSize()`](#ParagraphStyle+getFontSize) + * [`.setTextTransformation()`](#ParagraphStyle+setTextTransformation) + * [`.getTextTransformation()`](#ParagraphStyle+getTextTransformation) + * [`.setTypeface()`](#ParagraphStyle+setTypeface) + * [`.getTypeface()`](#ParagraphStyle+getTypeface) + * [`.setHorizontalAlignment()`](#ParagraphStyle+setHorizontalAlignment) + * [`.getHorizontalAlignment()`](#ParagraphStyle+getHorizontalAlignment) + * [`.setPageBreakBefore()`](#ParagraphStyle+setPageBreakBefore) + * [`.setKeepTogether()`](#ParagraphStyle+setKeepTogether) + * [`.getTabStops()`](#ParagraphStyle+getTabStops) + * [`.clearTabStops()`](#ParagraphStyle+clearTabStops) + * [`.toXml()`](#ParagraphStyle+toXml) * * * - - -### `new ListItem([text])` -Creates a list item + -#### Parameters -- [text] string -The text content of the list item +### `new ParagraphStyle()` +Constructor. * * * - + -### `listItem.toXml()` +### `paragraphStyle.setColor()` * * * - + -## Paragraph -This class represents a paragraph. +### `paragraphStyle.getColor()` -**Since**: 0.1.0 +* * * -* [Paragraph](#Paragraph) - * [`new Paragraph([text])`](#new_Paragraph_new) - * [`.addText(text)`](#Paragraph+addText) - * [`.getText()`](#Paragraph+getText) ⇒ string - * [`.setText(text)`](#Paragraph+setText) - * [`.addHyperlink(text, uri)`](#Paragraph+addHyperlink) ⇒ [Hyperlink](#Hyperlink) - * [`.addImage(path)`](#Paragraph+addImage) ⇒ [Image](#Image) - * [`.setStyle(style)`](#Paragraph+setStyle) - * [`.getStyle()`](#Paragraph+getStyle) ⇒ IParagraphStyle \| undefined - * [`.createElement(document)`](#Paragraph+createElement) ⇒ Element - * [`.toXml()`](#Paragraph+toXml) + +### `paragraphStyle.setFontName()` * * * - + -### `new Paragraph([text])` -Creates a paragraph +### `paragraphStyle.getFontName()` -#### Parameters -- [text] string -The text content of the paragraph +* * * + + +### `paragraphStyle.setFontSize()` * * * - + + +### `paragraphStyle.getFontSize()` -### `paragraph.addText(text)` -Appends the specified text to the end of this paragraph. +* * * -#### Parameters -- text string -The additional text content + -**Since**: 0.1.0 +### `paragraphStyle.setTextTransformation()` * * * - + -### `paragraph.getText()` ⇒ string -Returns the text content of this paragraph. -Note: This will only return the text; other elements and markup will be omitted. +### `paragraphStyle.getTextTransformation()` -**Return value** -string - The text content of this paragraph +* * * -**Since**: 0.1.0 + + +### `paragraphStyle.setTypeface()` * * * - + -### `paragraph.setText(text)` -Sets the text content of this paragraph. -Note: This will replace any existing content of the paragraph. +### `paragraphStyle.getTypeface()` -#### Parameters -- text string -The new text content +* * * -**Since**: 0.1.0 + + +### `paragraphStyle.setHorizontalAlignment()` * * * - + -### `paragraph.addHyperlink(text, uri)` ⇒ [Hyperlink](#Hyperlink) -Appends the specified text as hyperlink to the end of this paragraph. +### `paragraphStyle.getHorizontalAlignment()` -#### Parameters -- text string -The text content of the hyperlink -- uri string -The target URI of the hyperlink +* * * -**Return value** -[Hyperlink](#Hyperlink) - The newly added hyperlink + -**Since**: 0.3.0 +### `paragraphStyle.setPageBreakBefore()` * * * - + -### `paragraph.addImage(path)` ⇒ [Image](#Image) -Appends the image of the denoted path to the end of this paragraph. -The current paragraph will be set as anchor for the image. +### `paragraphStyle.setKeepTogether()` -#### Parameters -- path string -The path to the image file +* * * -**Return value** -[Image](#Image) - The newly added image + -**Since**: 0.3.0 +### `paragraphStyle.getTabStops()` * * * - + -### `paragraph.setStyle(style)` -Sets the new style of this paragraph. -To reset the style, `undefined` must be given. +### `paragraphStyle.clearTabStops()` -#### Parameters -- style IParagraphStyle | undefined -The new style or `undefined` to reset the style +* * * -**Since**: 0.3.0 + -* * * +### `paragraphStyle.toXml()` - +* * * -### `paragraph.getStyle()` ⇒ IParagraphStyle \| undefined -Returns the style of this paragraph. + -**Return value** -IParagraphStyle \| undefined - The style of the paragraph or `undefined` if no style was set +## StyleHelper +Utility class for dealing with styles. -**Since**: 0.3.0 +**Since**: 0.4.0 * * * - + -### `paragraph.createElement(document)` ⇒ Element -Creates the paragraph element. +### `StyleHelper.getAutomaticStylesElement(document)` ⇒ Element +Returns the `automatic-styles` element of the document. +If there is no such element yet, it will be created. #### Parameters - document Document The XML document **Return value** -Element - The DOM element representing this paragraph +Element - The documents `automatic-styles` element -**Since**: 0.1.0 +**Since**: 0.4.0 * * * - - -### `paragraph.toXml()` - -* * * + - +## TabStop +This class represents a tab stop. -## TextDocument -This class represents an empty ODF text document. +Tab stops are used to align text in a paragraph. +To become effective they must be set to the style of the respective paragraph. -**Since**: 0.1.0 +**Since**: 0.3.0 -* [TextDocument](#TextDocument) - * [`.getMeta()`](#TextDocument+getMeta) ⇒ [Meta](#Meta) - * [`.declareFont(name, fontFamily, fontPitch)`](#TextDocument+declareFont) - * [`.addHeading([text], [level])`](#TextDocument+addHeading) ⇒ [Heading](#Heading) - * [`.addList()`](#TextDocument+addList) ⇒ [List](#List) - * [`.addParagraph([text])`](#TextDocument+addParagraph) ⇒ [Paragraph](#Paragraph) - * [`.saveFlat(filePath)`](#TextDocument+saveFlat) ⇒ Promise.<void> - * ~~[`.toString()`](#TextDocument+toString) ⇒ string~~ - * [`.toXml()`](#TextDocument+toXml) +* [TabStop](#TabStop) + * [`new TabStop([position], [type])`](#new_TabStop_new) + * [`.setPosition(position)`](#TabStop+setPosition) + * [`.getPosition()`](#TabStop+getPosition) ⇒ number + * [`.setType(type)`](#TabStop+setType) + * [`.getType()`](#TabStop+getType) ⇒ TabStopType + * [`.toXml(document, parent)`](#TabStop+toXml) * * * - + -### `textDocument.getMeta()` ⇒ [Meta](#Meta) -The `getMeta()` method returns the metadata of the document. +### `new TabStop([position], [type])` +Creates a tab stop to be set to the style of a paragraph. -**Return value** -[Meta](#Meta) - An object holding the metadata of the document +#### Parameters +- [position] number +The position of the tab stop in centimeters relative to the left margin. +If a negative value is given, the `position` will be set to `0`. +- [type] TabStopType +The type of the tab stop. Defaults to `TabStopType.Left`. -**See**: [Meta](#Meta) **Example** ```js -document.getMeta().setCreator('Lisa Simpson'); +// creates a right aligned tab stop with a distance of 4 cm from the left margin +const tabStop4 = new TabStop(4, TabStopType.Right); +paragraph.getStyle().addTabStop(tabStop4); ``` -**Since**: 0.6.0 * * * - - -### `textDocument.declareFont(name, fontFamily, fontPitch)` -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.** +### `tabStop.setPosition(position)` +Sets the position of this tab stop. #### Parameters -- name string -The name of the font; this name must be set to a [ParagraphStyle](#ParagraphStyle) -- fontFamily string -The name of the font family -- fontPitch FontPitch -The ptich of the fonr +- position number +The position of the tab stop in centimeters relative to the left margin. +If a negative value is given, the `position` will be set to `0`. -**Since**: 0.4.0 +**Since**: 0.3.0 * * * - - -### `textDocument.addHeading([text], [level])` ⇒ [Heading](#Heading) -Adds a heading at the end of the document. -If a text is given, this will be set as text content of the heading. + -#### Parameters -- [text] string -The text content of the heading -- [level] number = 1 -The heading level; defaults to 1 if omitted +### `tabStop.getPosition()` ⇒ number +Returns the position of this tab stop. **Return value** -[Heading](#Heading) - The newly added heading +number - The position of this tab stop in centimeters -**Since**: 0.1.0 +**Since**: 0.3.0 * * * - + -### `textDocument.addList()` ⇒ [List](#List) -Adds an empty list at the end of the document. +### `tabStop.setType(type)` +Sets the type of this tab stop. -**Return value** -[List](#List) - The newly added list +#### Parameters +- type TabStopType +The type of the tab stop -**Since**: 0.2.0 +**Since**: 0.3.0 * * * - - -### `textDocument.addParagraph([text])` ⇒ [Paragraph](#Paragraph) -Adds a paragraph at the end of the document. -If a text is given, this will be set as text content of the paragraph. + -#### Parameters -- [text] string -The text content of the paragraph +### `tabStop.getType()` ⇒ TabStopType +Returns the type of this tab stop. **Return value** -[Paragraph](#Paragraph) - The newly added paragraph +TabStopType - The type of this tab stop -**Since**: 0.1.0 +**Since**: 0.3.0 * * * - + -### `textDocument.saveFlat(filePath)` ⇒ Promise.<void> -Saves the document in flat open document xml format. +### `tabStop.toXml(document, parent)` +Transforms the tab stop into Open Document Format. #### Parameters -- filePath string -The file path to write to - -**Since**: 0.1.0 - -* * * - - - -### ~~`textDocument.toString()` ⇒ string~~ -***Deprecated*** - -Returns the string representation of this document in flat open document xml format. - -**Return value** -string - The string representation of this document - -**Since**: 0.1.0 - -* * * - - +- document Document +The XML document +- parent Element +The parent node in the DOM (`style:tab-stops`) -### `textDocument.toXml()` +**Since**: 0.3.0 * * * diff --git a/package.json b/package.json index ca7e1d58..d10e5aaf 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,8 @@ "test": "jest", "watch-test": "jest --watch", "coverage": "jest --coverage", - "lint": "tslint -c tslint.json 'src/**/*.ts', 'test/**/*.ts'", - "docs": "rm -r ./lib && tsc && jsdoc2md --name-format --param-list-format list --separators --partial ./jsdoc2md/body.hbs ./jsdoc2md/params-list.hbs ./jsdoc2md/returns.hbs ./jsdoc2md/scope.hbs --files ./lib/**/*.js ./lib/*.js > ./docs/API.md" + "lint": "tslint -c tslint.json 'src/**/*.ts'", + "docs": "rm -r ./lib && tsc && jsdoc2md --name-format --param-list-format list --separators --partial ./jsdoc2md/body.hbs ./jsdoc2md/params-list.hbs ./jsdoc2md/returns.hbs ./jsdoc2md/scope.hbs --files ./lib/api/**/*.js ./lib/style/**/*.js > ./docs/API.md" }, "dependencies": { "xmldom": "^0.1.27" diff --git a/src/TextDocument.spec.ts b/src/TextDocument.spec.ts deleted file mode 100644 index 53f23d2e..00000000 --- a/src/TextDocument.spec.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { readFile, unlink } from "fs"; -import { promisify } from "util"; -import { Meta } from "./meta/Meta"; -import { FontPitch } from "./style/FontPitch"; -import { Heading } from "./text/Heading"; -import { List } from "./text/List"; -import { Paragraph } from "./text/Paragraph"; -import { TextDocument, XML_DECLARATION } from "./TextDocument"; - -const FILEPATH = "./test.fodt"; - -jest.mock("../src/meta/Meta"); - -describe(TextDocument.name, () => { - - /* tslint:disable-next-line:max-line-length */ - const baseDocument = ''; - let document: TextDocument; - - beforeEach(() => { - document = new TextDocument(); - }); - - afterAll(async (done) => { - const unlinkAsync = promisify(unlink); - - await unlinkAsync(FILEPATH); - - done(); - }); - - describe("namespace declaration", () => { - it("add dc namespace", () => { - expect(document.toString()).toMatch(/xmlns:dc="http:\/\/purl.org\/dc\/elements\/1.1"/); - }); - - it("add draw namespace", () => { - expect(document.toString()).toMatch(/xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"/); - }); - - it("add fo namespace", () => { - expect(document.toString()).toMatch(/xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"/); - }); - - it("add meta namespace", () => { - expect(document.toString()).toMatch(/xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"/); - }); - - it("add style namespace", () => { - expect(document.toString()).toMatch(/xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"/); - }); - - it("add svg namespace", () => { - expect(document.toString()).toMatch(/xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"/); - }); - - it("add text namespace", () => { - expect(document.toString()).toMatch(/xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"/); - }); - - it("add xlink namespace", () => { - expect(document.toString()).toMatch(/xmlns:xlink="http:\/\/www.w3.org\/1999\/xlink"/); - }); - }); - - describe("#getMeta", () => { - it("return a meta object", () => { - expect(document.getMeta()).toBeInstanceOf(Meta); - }); - }); - - describe("#declareFont", () => { - 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(); - - expect(heading).toBeInstanceOf(Heading); - }); - - it("add heading to document", () => { - document.addHeading(); - - expect(document.toString()).toMatch(/ { - it("return a list", () => { - const list = document.addList(); - - expect(list).toBeInstanceOf(List); - }); - - it("add list to document", () => { - document.addList().addItem(); - - expect(document.toString()).toMatch(//); - }); - }); - - describe("#addParagraph", () => { - it("return a paragraph", () => { - const paragraph = document.addParagraph(); - - expect(paragraph).toBeInstanceOf(Paragraph); - }); - - it("add paragraph to document", () => { - document.addParagraph(); - - expect(document.toString()).toMatch(/ { - it("write a flat document", async (done) => { - const readFileAsync = promisify(readFile); - - await document.saveFlat(FILEPATH); - - const fileContents = await readFileAsync(FILEPATH, "utf8"); - - expect(fileContents).toEqual(XML_DECLARATION + baseDocument); - done(); - }); - }); - - describe("#toString", () => { - it("return the basis document", () => { - const result = document.toString(); - - expect(result).toEqual(XML_DECLARATION + baseDocument); - }); - }); -}); diff --git a/src/TextDocument.ts b/src/TextDocument.ts deleted file mode 100644 index 06e009ab..00000000 --- a/src/TextDocument.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { writeFile } from "fs"; -import { promisify } from "util"; -import { DOMImplementation, XMLSerializer } from "xmldom"; - -import { Meta } from "./meta/Meta"; -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"; - -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 meta: Meta; - private fonts: IFont[]; - - public constructor() { - super(); - - this.meta = new Meta(); - this.fonts = []; - } - - /** - * The `getMeta()` method returns the metadata of the document. - * - * @example - * document.getMeta().setCreator('Lisa Simpson'); - * - * @returns {Meta} An object holding the metadata of the document - * @see {@link Meta} - * @since 0.6.0 - */ - public getMeta(): Meta { - return this.meta; - } - - /** - * 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 }); - } - - /** - * Adds a heading at the end of the document. - * If a text is given, this will be set as text content of the heading. - * - * @param {string} [text] The text content of the heading - * @param {number} [level=1] The heading level; defaults to 1 if omitted - * @returns {Heading} The newly added heading - * @since 0.1.0 - */ - public addHeading(text?: string, level = 1): Heading { - const heading = new Heading(text, level); - this.append(heading); - - return heading; - } - - /** - * Adds an empty list at the end of the document. - * - * @returns {List} The newly added list - * @since 0.2.0 - */ - public addList(): List { - const list = new List(); - this.append(list); - - return list; - } - - /** - * Adds a paragraph at the end of the document. - * If a text is given, this will be set as text content of the paragraph. - * - * @param {string} [text] The text content of the paragraph - * @returns {Paragraph} The newly added paragraph - * @since 0.1.0 - */ - public addParagraph(text?: string): Paragraph { - const paragraph = new Paragraph(text); - this.append(paragraph); - - return paragraph; - } - - /** - * Saves the document in flat open document xml format. - * - * @param {string} filePath The file path to write to - * @returns {Promise} - * @since 0.1.0 - */ - public saveFlat(filePath: string): Promise { - const writeFileAsync = promisify(writeFile); - const xml = this.toString(); - - return writeFileAsync(filePath, xml); - } - - /** - * Returns the string representation of this document in flat open document xml format. - * - * @returns {string} The string representation of this document - * @since 0.1.0 - * @deprecated since version 0.3.0; use {@link TextDocument#saveFlat} instead - */ - public toString(): string { - const document = new DOMImplementation().createDocument( - "urn:oasis:names:tc:opendocument:xmlns:office:1.0", - OdfElementName.OfficeDocument, - null); - const root = document.firstChild; - - this.toXml(document, root as Element); - - return XML_DECLARATION + new XMLSerializer().serializeToString(document); - } - - /** @inheritDoc */ - protected toXml(document: Document, root: Element): void { - this.setXmlNamespaces(root); - - root.setAttribute(OdfAttributeName.OfficeMimetype, "application/vnd.oasis.opendocument.text"); - root.setAttribute(OdfAttributeName.OfficeVersion, OFFICE_VERSION); - - this.meta.toXml(document, root); - - this.setFontFaceElements(document, root); - - const bodyElement = document.createElement(OdfElementName.OfficeBody); - root.appendChild(bodyElement); - - const textElement = document.createElement(OdfElementName.OfficeText); - bodyElement.appendChild(textElement); - - super.toXml(document, textElement); - } - - /** - * Declares the used XML namespaces. - * - * @param {Element} root The root element of the document which will be used as parent - * @private - */ - private setXmlNamespaces(root: Element): void { - root.setAttribute("xmlns:dc", "http://purl.org/dc/elements/1.1"); - root.setAttribute("xmlns:draw", "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"); - root.setAttribute("xmlns:fo", "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"); - root.setAttribute("xmlns:meta", "urn:oasis:names:tc:opendocument:xmlns:meta:1.0"); - root.setAttribute("xmlns:style", "urn:oasis:names:tc:opendocument:xmlns:style:1.0"); - root.setAttribute("xmlns:svg", "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"); - root.setAttribute("xmlns:text", "urn:oasis:names:tc:opendocument:xmlns:text:1.0"); - root.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); - } - - /** - * Adds the `font-face-decls` element and the font faces if any font needs to be declared. - * - * @param {Document} document The XML document - * @param {Element} root The element which will be used as parent - * @private - */ - private setFontFaceElements(document: Document, root: Element): void { - if (this.fonts.length === 0) { - return; - } - - 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/OdfElement.ts b/src/api/OdfElement.ts similarity index 82% rename from src/OdfElement.ts rename to src/api/OdfElement.ts index b599a162..5eb869d1 100644 --- a/src/OdfElement.ts +++ b/src/api/OdfElement.ts @@ -14,13 +14,23 @@ export class OdfElement { this.children = []; } + /** + * Returns all child elements. + * + * @returns {OdfElement[]} A copy of the list of child elements + * @since 0.2.0 + */ + public getAll(): OdfElement[] { + return Array.from(this.children); + } + /** * Appends the element as a child element to this element. * * @param {OdfElement} element The element to append * @since 0.1.0 */ - public append(element: OdfElement): void { + protected append(element: OdfElement): void { this.children.push(element); } @@ -64,16 +74,6 @@ export class OdfElement { return this.children[position]; } - /** - * Returns all child elements. - * - * @returns {OdfElement[]} A copy of the list of child elements - * @since 0.2.0 - */ - protected getAll(): OdfElement[] { - return Array.from(this.children); - } - /** * Removes the child element from the specified position. * @@ -101,18 +101,4 @@ export class OdfElement { protected hasChildren(): boolean { return this.children.length > 0; } - - /** - * Transforms the element into Open Document Format. - * Implementors of this class must add themselves to the document and afterwards call super.toXML(...). - * - * @param {Document} document The XML document - * @param {Element} parent The parent node - * @since 0.1.0 - */ - protected toXml(document: Document, parent: Element): void { - this.children.forEach((child: OdfElement) => { - child.toXml(document, parent); - }); - } } diff --git a/src/api/draw/Image.spec.ts b/src/api/draw/Image.spec.ts new file mode 100644 index 00000000..d8c37803 --- /dev/null +++ b/src/api/draw/Image.spec.ts @@ -0,0 +1,46 @@ +import { AnchorType } from "../../style/AnchorType"; +import { ImageStyle } from "../../style/ImageStyle"; +import { Image } from "./Image"; + +describe(Image.name, () => { + const testImagePath = "/some/image.path.png"; + + let image: Image; + + beforeEach(() => { + image = new Image(testImagePath); + }); + + describe("path", () => { + it("return initial path", () => { + expect(image.getPath()).toBe(testImagePath); + }); + }); + + describe("style", () => { + let testStyle: ImageStyle; + + beforeEach(() => { + testStyle = new ImageStyle(); + }); + + it("return style by default", () => { + expect(image.getStyle()).toBeInstanceOf(ImageStyle); + }); + + it("return previous set style", () => { + testStyle.setAnchorType(AnchorType.AsChar); + + image.setStyle(testStyle); + + expect(image.getStyle()).toBe(testStyle); + }); + + it("ignore invalid input", () => { + image.setStyle(testStyle); + image.setStyle(null); + + expect(image.getStyle()).toBe(testStyle); + }); + }); +}); diff --git a/src/api/draw/Image.ts b/src/api/draw/Image.ts new file mode 100644 index 00000000..0ac83910 --- /dev/null +++ b/src/api/draw/Image.ts @@ -0,0 +1,85 @@ +import { IImageStyle } from "../../style/IImageStyle"; +import { ImageStyle } from "../../style/ImageStyle"; +import { OdfElement } from "../OdfElement"; + +/** + * This class represents an image in a paragraph. + * + * It is used to embed image data in BASE64 encoding. + * + * @example + * document.getBody() + * .addParagraph() + * .addImage("/home/homer/myself.png") + * .getStyle() + * .setSize(42, 23); + * + * @since 0.3.0 + */ +export class Image extends OdfElement { + private style: IImageStyle; + + /** + * Creates an image + * + * @example + * const image = new Image("/home/homer/myself.png"); + * + * @param {string} path Path to the image file that should be embedded + * @since 0.3.0 + */ + public constructor(private path: string) { + super(); + + this.style = new ImageStyle(); + } + + /** + * The `getPath()` method returns the path to the image file that should be embedded. + * + * @example + * const image = new Image("/home/homer/myself.png"); + * image.getPath(); // '/home/homer/myself.png' + * + * @returns {string} The path to the image file + * @since 0.7.0 + */ + public getPath(): string { + return this.path; + } + + /** + * Sets the new style of this image. + * + * @example + * const image = new Image("/home/homer/myself.png"); + * image.setStyle(new ImageStyle()); + * + * @param {IImageStyle} style The new style + * @returns {Image} The `Image` object + * @since 0.5.0 + */ + public setStyle(style: IImageStyle): Image { + if (style instanceof ImageStyle) { + this.style = style; + } + + return this; + } + + /** + * Returns the style of this image. + * + * @example + * const image = new Image("/home/homer/myself.png"); + * image.getStyle(); // default style + * image.setStyle(new ImageStyle()); + * image.getStyle(); // previously set style + * + * @returns {IImageStyle} The style of the image + * @since 0.5.0 + */ + public getStyle(): IImageStyle { + return this.style; + } +} diff --git a/src/api/draw/index.ts b/src/api/draw/index.ts new file mode 100644 index 00000000..d88656f5 --- /dev/null +++ b/src/api/draw/index.ts @@ -0,0 +1 @@ +export { Image } from "./Image"; diff --git a/src/meta/Meta.spec.ts b/src/api/meta/Meta.spec.ts similarity index 75% rename from src/meta/Meta.spec.ts rename to src/api/meta/Meta.spec.ts index 2abd8ba6..645a2bac 100644 --- a/src/meta/Meta.spec.ts +++ b/src/api/meta/Meta.spec.ts @@ -1,6 +1,5 @@ import { userInfo } from "os"; import { Meta } from "./Meta"; -import { TextDocument } from "../TextDocument"; describe(Meta.name, () => { const timeOffset = 100; @@ -357,74 +356,4 @@ describe(Meta.name, () => { expect(meta.getTitle()).toBe(testTitle); }); }); - - describe("#toXml", () => { - let document: TextDocument; - - beforeEach(() => { - document = new TextDocument(); - }); - - it("append creator, date, creation-date, editing-cycles and generator as default properties", () => { - const regex = new RegExp("" - + "simple-odf/\\d\\.\\d+\\.\\d+" - + "" + userInfo().username + "" - + "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z" - + "1" - + ""); - expect(document.toString()).toMatch(regex); - }); - - it("ignore description, language, subject, title if they are empty", () => { - document.getMeta() - .setCreator("") - .setDate(undefined) - .setDescription("") - .setInitialCreator("") - .setLanguage("") - .setSubject("") - .setTitle(""); - - const regex = new RegExp("" - + "simple-odf/\\d\\.\\d+\\.\\d+" - + "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z" - + "1" - + ""); - expect(document.toString()).toMatch(regex); - }); - - it("append elements if they are set", () => { - document.getMeta() - .setCreator("Homer Simpson") - .setDate(new Date(Date.UTC(2020, 11, 24, 13, 37, 23, 42))) - .setDescription("some test description") - .setInitialCreator("Marge Simpson") - .addKeyword("some keyword") - .addKeyword("some other keyword") - .setLanguage("zu") - .setPrintDate(new Date(Date.UTC(2021, 3, 1))) - .setPrintedBy("Maggie Simpson") - .setSubject("some test subject") - .setTitle("some test title") - ; - - const regex = new RegExp("" - + "simple-odf/\\d\\.\\d+\\.\\d+" - + "some test title" - + "some test description" - + "some test subject" - + "some keyword" - + "some other keyword" - + "Marge Simpson" - + "Homer Simpson" - + "Maggie Simpson" - + "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z" - + "2020-12-24T13:37:23.042Z" - + "2021-04-01T00:00:00.000Z" - + "zu" - + "1" - + ""); - expect(document.toString()).toMatch(regex); - }); - }); }); diff --git a/src/meta/Meta.ts b/src/api/meta/Meta.ts similarity index 65% rename from src/meta/Meta.ts rename to src/api/meta/Meta.ts index 729e7659..be165746 100644 --- a/src/meta/Meta.ts +++ b/src/api/meta/Meta.ts @@ -1,6 +1,4 @@ import { userInfo } from "os"; -import { OdfElementName } from "../OdfElementName"; -import { MetaElementName } from "./MetaElementName"; /** * This class represents the metadata of a document. @@ -52,7 +50,7 @@ export class Meta { * @since 0.6.0 */ public constructor() { - const packageJson = require("../../package.json"); + const packageJson = require("../../../package.json"); this.generator = `${packageJson.name}/${packageJson.version}`; this.keywords = []; @@ -517,240 +515,4 @@ export class Meta { public getTitle(): string | undefined { return this.title; } - - /** - * Transforms the text style into Open Document Format. - * - * @param {Document} document The XML document - * @param {Element} root The root node in the DOM - * @since 0.6.0 - */ - public toXml(document: Document, root: Element): void { - const metaElement = document.createElement(OdfElementName.OfficeMeta); - root.appendChild(metaElement); - - this.setGeneratorElement(document, metaElement); - this.setTitleElement(document, metaElement); - this.setDescriptionElement(document, metaElement); - this.setSubjectElement(document, metaElement); - this.setKeywordElements(document, metaElement); - this.setInitialCreatorElement(document, metaElement); - this.setCreatorElement(document, metaElement); - this.setPrintedByElement(document, metaElement); - this.setCreationDateElement(document, metaElement); - this.setDateElement(document, metaElement); - this.setPrintDateElement(document, metaElement); - this.setLanguageElement(document, metaElement); - this.setEditingCyclesElement(document, metaElement); - } - - /** - * Sets the `meta:creation-date` element to the date and time this class was constructed. - * - * @param {Document} document The XML document - * @param {Element} metaElement The meta element which will act as parent - * @private - */ - private setCreationDateElement(document: Document, metaElement: Element): void { - const creationDateElement = document.createElement(MetaElementName.MetaCreationDate); - metaElement.appendChild(creationDateElement); - creationDateElement.appendChild(document.createTextNode(this.creationDate.toISOString())); - } - - /** - * Sets the `dc:creator` element if creator is set. - * - * @param {Document} document The XML document - * @param {Element} metaElement The meta element which will act as parent - * @private - */ - private setCreatorElement(document: Document, metaElement: Element): void { - if (this.creator === undefined || this.creator.length === 0) { - return; - } - - const creatorElement = document.createElement(MetaElementName.DcCreator); - metaElement.appendChild(creatorElement); - creatorElement.appendChild(document.createTextNode(this.creator)); - } - - /** - * Sets the `dc:date` element if date is set. - * - * @param {Document} document The XML document - * @param {Element} metaElement The meta element which will act as parent - * @private - */ - private setDateElement(document: Document, metaElement: Element): void { - if (this.date === undefined) { - return; - } - - const dateElement = document.createElement(MetaElementName.DcDate); - metaElement.appendChild(dateElement); - dateElement.appendChild(document.createTextNode(this.date.toISOString())); - } - - /** - * Sets the `dc:description` element if description is set. - * - * @param {Document} document The XML document - * @param {Element} metaElement The meta element which will act as parent - * @private - */ - private setDescriptionElement(document: Document, metaElement: Element): void { - if (this.description === undefined || this.description.length === 0) { - return; - } - - const descriptionElement = document.createElement(MetaElementName.DcDescription); - metaElement.appendChild(descriptionElement); - descriptionElement.appendChild(document.createTextNode(this.description)); - } - - /** - * Sets the `meta:editing-cycles` element to 1. - * - * @param {Document} document The XML document - * @param {Element} metaElement The meta element which will act as parent - * @private - */ - private setEditingCyclesElement(document: Document, metaElement: Element): void { - const editingCyclesElement = document.createElement(MetaElementName.MetaEditingCycles); - metaElement.appendChild(editingCyclesElement); - editingCyclesElement.appendChild(document.createTextNode(this.editingCycles.toString())); - } - - /** - * Sets the `meta:generator` element to the name and version of this library (`simple-odf/x.y.z`). - * - * @param {Document} document The XML document - * @param {Element} metaElement The meta element which will act as parent - * @private - */ - private setGeneratorElement(document: Document, metaElement: Element): void { - const generatorElement = document.createElement(MetaElementName.MetaGenerator); - metaElement.appendChild(generatorElement); - generatorElement.appendChild(document.createTextNode(this.generator)); - } - - /** - * Sets the `meta:initial-creator` element to the current user. - * - * @param {Document} document The XML document - * @param {Element} metaElement The meta element which will act as parent - * @private - */ - private setInitialCreatorElement(document: Document, metaElement: Element): void { - if (this.initialCreator === undefined || this.initialCreator.length === 0) { - return; - } - - const creatorElement = document.createElement(MetaElementName.MetaInitialCreator); - metaElement.appendChild(creatorElement); - creatorElement.appendChild(document.createTextNode(this.initialCreator)); - } - - /** - * Sets the `meta:keyword` elements if any keyword is set. - * - * @param {Document} document The XML document - * @param {Element} metaElement The meta element which will act as parent - * @private - */ - private setKeywordElements(document: Document, metaElement: Element): void { - if (this.keywords.length === 0) { - return; - } - - this.keywords.forEach((keyword: string) => { - const subjectElement = document.createElement(MetaElementName.MetaKeyword); - metaElement.appendChild(subjectElement); - subjectElement.appendChild(document.createTextNode(keyword)); - }); - } - - /** - * Sets the `dc:language` element if language is set. - * - * @param {Document} document The XML document - * @param {Element} metaElement The meta element which will act as parent - * @private - */ - private setLanguageElement(document: Document, metaElement: Element): void { - if (this.language === undefined || this.language.length === 0) { - return; - } - - const languageElement = document.createElement(MetaElementName.DcLanguage); - metaElement.appendChild(languageElement); - languageElement.appendChild(document.createTextNode(this.language)); - } - - /** - * Sets the `meta:print-date` element if print date is set. - * - * @param {Document} document The XML document - * @param {Element} metaElement The meta element which will act as parent - * @private - */ - private setPrintDateElement(document: Document, metaElement: Element): void { - if (this.printDate === undefined) { - return; - } - - const printDateElement = document.createElement(MetaElementName.MetaPrintDate); - metaElement.appendChild(printDateElement); - printDateElement.appendChild(document.createTextNode(this.printDate.toISOString())); - } - - /** - * Sets the `meta:printed-by` element if printing name is set. - * - * @param {Document} document The XML document - * @param {Element} metaElement The meta element which will act as parent - * @private - */ - private setPrintedByElement(document: Document, metaElement: Element): void { - if (this.printedBy === undefined || this.printedBy.length === 0) { - return; - } - const printedByElement = document.createElement(MetaElementName.MetaPrintedBy); - metaElement.appendChild(printedByElement); - printedByElement.appendChild(document.createTextNode(this.printedBy)); - } - - /** - * Sets the `dc:subject` element if subject is set. - * - * @param {Document} document The XML document - * @param {Element} metaElement The meta element which will act as parent - * @private - */ - private setSubjectElement(document: Document, metaElement: Element): void { - if (this.subject === undefined || this.subject.length === 0) { - return; - } - - const subjectElement = document.createElement(MetaElementName.DcSubject); - metaElement.appendChild(subjectElement); - subjectElement.appendChild(document.createTextNode(this.subject)); - } - - /** - * Sets the `dc:title` element if title is set. - * - * @param {Document} document The XML document - * @param {Element} metaElement The meta element which will act as parent - * @private - */ - private setTitleElement(document: Document, metaElement: Element): void { - if (this.title === undefined || this.title.length === 0) { - return; - } - - const titleElement = document.createElement(MetaElementName.DcTitle); - metaElement.appendChild(titleElement); - titleElement.appendChild(document.createTextNode(this.title)); - } } diff --git a/src/api/meta/index.ts b/src/api/meta/index.ts new file mode 100644 index 00000000..f7500524 --- /dev/null +++ b/src/api/meta/index.ts @@ -0,0 +1 @@ +export { Meta } from "./Meta"; diff --git a/src/api/office/TextBody.spec.ts b/src/api/office/TextBody.spec.ts new file mode 100644 index 00000000..931f3fcf --- /dev/null +++ b/src/api/office/TextBody.spec.ts @@ -0,0 +1,34 @@ +import { Heading, List, Paragraph } from "../text"; +import { TextBody } from "./TextBody"; + +describe(TextBody.name, () => { + let textBody: TextBody; + + beforeEach(() => { + textBody = new TextBody(); + }); + + describe("#addHeading", () => { + it("return a heading", () => { + const heading = textBody.addHeading(); + + expect(heading).toBeInstanceOf(Heading); + }); + }); + + describe("#addList", () => { + it("return a list", () => { + const list = textBody.addList(); + + expect(list).toBeInstanceOf(List); + }); + }); + + describe("#addParagraph", () => { + it("return a paragraph", () => { + const paragraph = textBody.addParagraph(); + + expect(paragraph).toBeInstanceOf(Paragraph); + }); + }); +}); diff --git a/src/api/office/TextBody.ts b/src/api/office/TextBody.ts new file mode 100644 index 00000000..30e6319f --- /dev/null +++ b/src/api/office/TextBody.ts @@ -0,0 +1,59 @@ +import { OdfElement } from "../OdfElement"; +import { Heading, List, Paragraph } from "../text"; + +/** + * This class represents the content of a text document. + * + * @example + * const body = document.getBody(); + * body.addHeading("My document"); + * body.addParagraph("This is the first paragraph"); + * body.addHeading("Subheadline", 2); + * + * @since 0.7.0 + */ +export class TextBody extends OdfElement { + /** + * Adds a heading at the end of the document. + * If a text is given, this will be set as text content of the heading. + * + * @param {string} [text] The text content of the heading + * @param {number} [level=1] The heading level; defaults to 1 if omitted + * @returns {Heading} The newly added heading + * @since 0.7.0 + */ + public addHeading(text?: string, level = 1): Heading { + const heading = new Heading(text, level); + this.append(heading); + + return heading; + } + + /** + * Adds an empty list at the end of the document. + * + * @returns {List} The newly added list + * @since 0.7.0 + */ + public addList(): List { + const list = new List(); + this.append(list); + + return list; + } + + /** + * Adds a paragraph at the end of the document. + * If a text is given, this will be set as text content of the paragraph. + * + * @param {string} [text] The text content of the paragraph + * @returns {Paragraph} The newly added paragraph + * @since 0.7.0 + */ + public addParagraph(text?: string): Paragraph { + const paragraph = new Paragraph(text); + this.append(paragraph); + + return paragraph; + } +} diff --git a/src/api/office/TextDocument.spec.ts b/src/api/office/TextDocument.spec.ts new file mode 100644 index 00000000..6e9b61ba --- /dev/null +++ b/src/api/office/TextDocument.spec.ts @@ -0,0 +1,84 @@ +import { readFile, unlink } from "fs"; +import { promisify } from "util"; +import { Meta } from "../meta/Meta"; +import { FontFace } from "../style"; +import { FontPitch } from "../style/FontPitch"; +import { TextBody } from "./TextBody"; +import { TextDocument, XML_DECLARATION } from "./TextDocument"; + +const FILEPATH = "./test.fodt"; + +jest.mock("../../xml/TextDocumentWriter"); + +describe(TextDocument.name, () => { + let document: TextDocument; + + beforeEach(() => { + document = new TextDocument(); + }); + + describe("body", () => { + it("return a text body object", () => { + const body = document.getBody(); + + expect(body).toBeInstanceOf(TextBody); + }); + }); + + describe("font", () => { + it("return an empty list of fonts by default", () => { + const fonts = document.getFonts(); + + expect(fonts).toEqual([]); + }); + + it("return a font face object", () => { + const font = document.declareFont("Springfield", "Springfield", FontPitch.Variable); + + expect(font).toBeInstanceOf(FontFace); + }); + + it("add font face to list of fonts", () => { + document.declareFont("Springfield", "Springfield", FontPitch.Variable); + + const fonts = document.getFonts(); + + expect(fonts).toEqual([new FontFace("Springfield", "Springfield", FontPitch.Variable)]); + }); + }); + + describe("meta", () => { + it("return a meta object", () => { + expect(document.getMeta()).toBeInstanceOf(Meta); + }); + }); + + describe("#saveFlat", () => { + afterEach(async (done) => { + const unlinkAsync = promisify(unlink); + + await unlinkAsync(FILEPATH); + + done(); + }); + + it("write a flat document", async (done) => { + const readFileAsync = promisify(readFile); + + await document.saveFlat(FILEPATH); + + const fileContents = await readFileAsync(FILEPATH, "utf8"); + + expect(fileContents).toEqual(XML_DECLARATION + "??"); + done(); + }); + }); + + describe("#toString", () => { + it("return the basis document", () => { + const result = document.toString(); + + expect(result).toEqual(XML_DECLARATION + "??"); + }); + }); +}); diff --git a/src/api/office/TextDocument.ts b/src/api/office/TextDocument.ts new file mode 100644 index 00000000..248c7d9b --- /dev/null +++ b/src/api/office/TextDocument.ts @@ -0,0 +1,132 @@ +import { writeFile } from "fs"; +import { promisify } from "util"; +import { XMLSerializer } from "xmldom"; +import { TextDocumentWriter } from "../../xml/TextDocumentWriter"; +import { Meta } from "../meta"; +import { FontFace, FontPitch } from "../style"; +import { TextBody } from "./TextBody"; + +export const XML_DECLARATION = '\n'; + +/** + * This class represents a text document in OpenDocument format. + * + * @example + * const document = new TextDocument(); + * document.getMeta().setCreator("Homer Simpson"); + * document.declareFont("FreeSans", "FreeSans", FontPitch.Variable); + * document.getBody().addHeading("My first document"); + * document.saveFlat("/home/homer/document.fodt"); + * + * @since 0.1.0 + */ +export class TextDocument { + private meta: Meta; + private fonts: FontFace[]; + private body: TextBody; + + public constructor() { + this.meta = new Meta(); + this.fonts = []; + this.body = new TextBody(); + } + + /** + * The `getBody()` method returns the content of the document. + * + * @example + * new TextDocument() + * .getBody() + * .addHeading('My first document'); + * + * @returns {TextBody} A `TextBody` object that holds the content of the document + * @since 0.7.0 + */ + public getBody(): TextBody { + return this.body; + } + + /** + * The `declareFont` method creates a font face 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.** + * + * @example + * new TextDocument() + * .declareFont("FreeSans", "FreeSans", FontPitch.Variable); + * + * @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 pitch of the font + * @returns {FontFace} The declared `FontFace` object + * @since 0.4.0 + */ + public declareFont(name: string, fontFamily: string, fontPitch: FontPitch): FontFace { + const fontFace = new FontFace(name, fontFamily, fontPitch); + this.fonts.push(fontFace); + + return fontFace; + } + + /** + * The `getFonts()` method returns all font face declarations for the document. + * + * @example + * const document = new TextDocument(); + * document.declareFont("FreeSans", "FreeSans", FontPitch.Variable); + * document.getFonts(); + * + * @returns {FontFace[]} A copy of the list of font face declarations for the document + * @since 0.7.0 + */ + public getFonts(): FontFace[] { + return Array.from(this.fonts); + } + + /** + * The `getMeta()` method returns the metadata of the document. + * + * @example + * new TextDocument.getMeta() + * .setCreator('Homer Simpson'); + * + * @returns {Meta} An object holding the metadata of the document + * @see {@link Meta} + * @since 0.6.0 + */ + public getMeta(): Meta { + return this.meta; + } + + /** + * The `saveFlat()` method converts the document into an XML string and stores it in flat open document xml format. + * + * @example + * new TextDocument() + * .saveFlat("/home/homer/document.fodt"); + * + * @param {string} filePath The file path to write to + * @returns {Promise} + * @since 0.1.0 + */ + public saveFlat(filePath: string): Promise { + const writeFileAsync = promisify(writeFile); + const xml = this.toString(); + + return writeFileAsync(filePath, xml); + } + + /** + * Returns the string representation of this document in flat open document xml format. + * + * @returns {string} The string representation of this document + * @since 0.1.0 + * @deprecated since version 0.3.0; use {@link TextDocument#saveFlat} instead + */ + public toString(): string { + const document = new TextDocumentWriter().write(this); + + return XML_DECLARATION + new XMLSerializer().serializeToString(document); + } +} diff --git a/src/api/office/index.ts b/src/api/office/index.ts new file mode 100644 index 00000000..a0b107ae --- /dev/null +++ b/src/api/office/index.ts @@ -0,0 +1,2 @@ +export { TextBody } from "./TextBody"; +export { TextDocument } from "./TextDocument"; diff --git a/src/api/style/FontFace.spec.ts b/src/api/style/FontFace.spec.ts new file mode 100644 index 00000000..3cc11c94 --- /dev/null +++ b/src/api/style/FontFace.spec.ts @@ -0,0 +1,32 @@ +import { FontFace } from "./FontFace"; +import { FontPitch } from "./FontPitch"; + +describe(FontFace.name, () => { + const testFamily = "someFontFamily"; + const testFontPitch = FontPitch.Variable; + const testName = "someFontName"; + + let fontFace: FontFace; + + beforeEach(() => { + fontFace = new FontFace(testName, testFamily, testFontPitch); + }); + + describe("font family", () => { + it("return initial font family", () => { + expect(fontFace.getFontFamily()).toBe(testFamily); + }); + }); + + describe("font pitch", () => { + it("return initial font pitch", () => { + expect(fontFace.getFontPitch()).toBe(testFontPitch); + }); + }); + + describe("name", () => { + it("return initial name", () => { + expect(fontFace.getName()).toBe(testName); + }); + }); +}); diff --git a/src/api/style/FontFace.ts b/src/api/style/FontFace.ts new file mode 100644 index 00000000..5a61e429 --- /dev/null +++ b/src/api/style/FontFace.ts @@ -0,0 +1,25 @@ +import { FontPitch } from "./FontPitch"; + +export class FontFace { + private name: string; + private fontFamily: string; + private fontPitch: FontPitch; + + public constructor(name: string, fontFamily: string, fontPitch: FontPitch) { + this.name = name; + this.fontFamily = fontFamily; + this.fontPitch = fontPitch; + } + + public getFontFamily(): string { + return this.fontFamily; + } + + public getFontPitch(): FontPitch { + return this.fontPitch; + } + + public getName(): string { + return this.name; + } +} diff --git a/src/style/FontPitch.ts b/src/api/style/FontPitch.ts similarity index 100% rename from src/style/FontPitch.ts rename to src/api/style/FontPitch.ts diff --git a/src/api/style/index.ts b/src/api/style/index.ts new file mode 100644 index 00000000..0f68a6fa --- /dev/null +++ b/src/api/style/index.ts @@ -0,0 +1,2 @@ +export { FontFace } from "./FontFace"; +export { FontPitch } from "./FontPitch"; diff --git a/src/api/text/Heading.spec.ts b/src/api/text/Heading.spec.ts new file mode 100644 index 00000000..da1a427b --- /dev/null +++ b/src/api/text/Heading.spec.ts @@ -0,0 +1,52 @@ +import { Heading } from "./Heading"; + +describe(Heading.name, () => { + const testLevel = 2; + const testText = "some text"; + + let heading: Heading; + + beforeEach(() => { + heading = new Heading(testText, testLevel); + }); + + describe("level", () => { + it("return initial level", () => { + expect(heading.getLevel()).toBe(testLevel); + }); + + it("return default level if initial level is not set", () => { + heading = new Heading(testText); + + expect(heading.getLevel()).toBe(Heading.DEFAULT_LEVEL); + }); + + it("return previous set level", () => { + heading.setLevel(3); + + expect(heading.getLevel()).toBe(3); + }); + + it("use default level if invalid level is given", () => { + heading.setLevel(-2); + + expect(heading.getLevel()).toBe(Heading.DEFAULT_LEVEL); + + heading.setLevel(null); + + expect(heading.getLevel()).toBe(Heading.DEFAULT_LEVEL); + }); + }); + + describe("text", () => { + it("return initial text", () => { + expect(heading.getText()).toBe(testText); + }); + + it("return empty text if initial text is not set", () => { + heading = new Heading(); + + expect(heading.getText()).toBe(""); + }); + }); +}); diff --git a/src/api/text/Heading.ts b/src/api/text/Heading.ts new file mode 100644 index 00000000..edf345f0 --- /dev/null +++ b/src/api/text/Heading.ts @@ -0,0 +1,64 @@ +import { Paragraph } from "./Paragraph"; + +/** + * This class represents a heading in a document. + * + * It is used to structure a document into multiple sections. + * A chapter or section begins with a heading and extends to the next heading at the same or higher level. + * + * @example + * document.getBody().addHeading("First Headline", 1); + * + * @example + * document.getBody().addHeading() + * .setText("Second Headline") + * .setLevel(2); + * + * @extends {Paragraph} + * @since 0.1.0 + */ +export class Heading extends Paragraph { + public static DEFAULT_LEVEL = 1; + + /** + * Creates a `Heading` instance that represents a heading in a document. + * + * @example + * new Heading("First Headline", 1); + * new Heading("First Headline"); + * new Heading(); + * + * @param {string} [text=''] The text content of the heading; defaults to an empty string if omitted + * @param {number} [level=1] The level of the heading, starting with `1`; defaults to `1` if omitted + * @since 0.1.0 + */ + public constructor(text?: string, private level = Heading.DEFAULT_LEVEL) { + super(text); + + this.setLevel(level); + } + + /** + * The `setLevel()` method sets the level of the heading, starting with `1`. + * If an illegal value is provided, then the heading is assumed to be at level `1`. + * + * @param {number} level The level of the heading, starting with `1` + * @returns {Heading} The `Heading` object + * @since 0.1.0 + */ + public setLevel(level: number): Heading { + this.level = level > Heading.DEFAULT_LEVEL ? level : Heading.DEFAULT_LEVEL; + + return this; + } + + /** + * The `getLevel()` method returns the level of the heading. + * + * @returns {number} The level of the heading + * @since 0.1.0 + */ + public getLevel(): number { + return this.level; + } +} diff --git a/src/api/text/Hyperlink.spec.ts b/src/api/text/Hyperlink.spec.ts new file mode 100644 index 00000000..fb71b2eb --- /dev/null +++ b/src/api/text/Hyperlink.spec.ts @@ -0,0 +1,40 @@ +import { Hyperlink } from "./Hyperlink"; + +describe(Hyperlink.name, () => { + const testText = "some text"; + const testUri = "http://example.org/"; + + let hyperlink: Hyperlink; + + beforeEach(() => { + hyperlink = new Hyperlink(testText, testUri); + }); + + describe("text", () => { + it("return initial text", () => { + expect(hyperlink.getText()).toBe(testText); + }); + }); + + describe("URI", () => { + it("return initial URI", () => { + expect(hyperlink.getURI()).toBe(testUri); + }); + + it("return previous set URI", () => { + hyperlink.setURI("localhost"); + + expect(hyperlink.getURI()).toBe("localhost"); + }); + + it("ignore invalid input", () => { + hyperlink.setURI(""); + + expect(hyperlink.getURI()).toBe(testUri); + + hyperlink.setURI(null); + + expect(hyperlink.getURI()).toBe(testUri); + }); + }); +}); diff --git a/src/api/text/Hyperlink.ts b/src/api/text/Hyperlink.ts new file mode 100644 index 00000000..417ae87f --- /dev/null +++ b/src/api/text/Hyperlink.ts @@ -0,0 +1,64 @@ +import { OdfTextElement } from "./OdfTextElement"; + +/** + * This class represents a hyperlink in a paragraph. + * + * @example + * document.getBody() + * .addParagraph('This is a ') + * .addHyperlink('link', 'https://example.com/'); + * + * @since 0.3.0 + */ +export class Hyperlink extends OdfTextElement { + /** + * Creates a hyperlink + * + * @example + * new Hyperlink('My website', 'https://example.com/'); + * + * @param {string} text The text content of the hyperlink + * @param {string} uri The target URI of the hyperlink + * @since 0.3.0 + */ + public constructor(text: string, private uri: string) { + super(text); + } + + /** + * The `setURI()` method sets the target URI for this hyperlink. + * If an illegal value is provided, the value will be ignored. + * + * @example + * const hyperlink = new Hyperlink('My website', 'https://example.com/'); + * hyperlink.setURI('https://github.com'); // https://github.com + * hyperlink.setURI(''); // https://github.com + * + * @param {string} uri The target URI of this hyperlink + * @returns {Hyperlink} The `Hyperlink` object + * @since 0.3.0 + */ + public setURI(uri: string): Hyperlink { + if (typeof uri === "string" && uri.trim().length > 0) { + this.uri = uri; + } + + return this; + } + + /** + * The `getURI()` method returns the target URI of this hyperlink. + * + * @example + * const hyperlink = new Hyperlink('My website', 'https://example.com/'); + * hyperlink.getURI(); // https://example.com + * hyperlink.setURI('https://github.com'); + * hyperlink.getURI(); // https://github.com + * + * @returns {string} The target URI of this hyperlink + * @since 0.3.0 + */ + public getURI(): string { + return this.uri; + } +} diff --git a/src/text/List.spec.ts b/src/api/text/List.spec.ts similarity index 88% rename from src/text/List.spec.ts rename to src/api/text/List.spec.ts index 92f59d6a..5086b081 100644 --- a/src/text/List.spec.ts +++ b/src/api/text/List.spec.ts @@ -1,36 +1,20 @@ import { List } from "./List"; import { ListItem } from "./ListItem"; -import { TextDocument } from "../TextDocument"; describe(List.name, () => { - let document: TextDocument; let list: List; let testItem1: ListItem; let testItem2: ListItem; let testItem3: ListItem; beforeEach(() => { - document = new TextDocument(); - list = document.addList(); + list = new List(); testItem1 = new ListItem("first"); testItem2 = new ListItem("second"); testItem3 = new ListItem("third"); }); - it("NOT insert an empty list", () => { - const documentAsString = document.toString(); - expect(documentAsString).not.toMatch(/ { - list.addItem("first"); - - const documentAsString = document.toString(); - /* tslint:disable-next-line:max-line-length */ - expect(documentAsString).toMatch(/first<\/text:p><\/text:list-item><\/text:list>/); - }); - describe("#addItem", () => { beforeEach(() => { list.addItem("first"); diff --git a/src/api/text/List.ts b/src/api/text/List.ts new file mode 100644 index 00000000..6bf647e8 --- /dev/null +++ b/src/api/text/List.ts @@ -0,0 +1,179 @@ +import { OdfElement } from "../OdfElement"; +import { ListItem } from "./ListItem"; + +/** + * This class represents a list and may contain any number list items. + * + * @example + * const list = document.getBody().addList(); + * list.addItem("First item"); + * list.addItem("Second item"); + * list.insertItem(1, "After first item") + * list.removeItemAt(2); + * + * @since 0.2.0 + */ +export class List extends OdfElement { + /** + * Creates a `List` instance that represents a list. + * + * @example + * new List(); + * + * @since 0.2.0 + */ + public constructor() { + super(); + } + + /** + * The `addItem()` method adds a new list item with the specified text or adds the specified item to the list. + * + * @example + * const list = new List(); + * list.addItem("First item"); + * list.addItem(new ListItem("Second item")); + * + * @param {string | ListItem} [item] The text content of the new item or the item to add + * @returns {ListItem} The added `ListItem` object + * @since 0.2.0 + */ + public addItem(item?: string | ListItem): ListItem { + if (item instanceof ListItem) { + this.append(item); + return item; + } + + const listItem = new ListItem(item); + this.append(listItem); + + return listItem; + } + + /** + * The `insertItem` method inserts a new list item with the specified text + * or inserts the specified item at the specified position. + * The item is inserted before the item at the specified position. + * + * If the position is greater than the current number items, the new item is appended at the end of the list. + * If the position is negative, the new item is inserted as first element. + * + * @example + * const list = new List(); + * list.addItem("First item"); // "First item" + * list.addItem("Second item"); // "First item", "Second item" + * list.insertItem(1, "After first item"); // "First item", "After first item", "Second item" + * + * @param {number} position The index at which to insert the list item (starting from 0). + * @param {string | ListItem} item The text content of the new item or the item to insert + * @returns {ListItem} The inserted `ListItem` object + * @since 0.2.0 + */ + public insertItem(position: number, item: string | ListItem): ListItem { + if (item instanceof ListItem) { + this.insert(position, item); + return item; + } + + const listItem = new ListItem(item); + this.insert(position, listItem); + + return listItem; + } + + /** + * The `getItem()` method returns the item at the specified position in the list. + * If an invalid position is given, undefined is returned. + * + * @example + * const list = new List(); + * list.addItem("First item"); + * list.addItem("Second item"); + * list.getItem(1); // "Second item" + * list.getItem(2); // undefined + * + * @param {number} position The index of the requested list item (starting from 0). + * @returns {ListItem | undefined} The `ListItem` object at the specified position + * or `undefined` if there is no list item at the specified position + * @since 0.2.0 + */ + public getItem(position: number): ListItem | undefined { + return this.get(position) as ListItem; + } + + /** + * The `getItems()` method returns all list items. + * + * @example + * const list = new List(); + * list.getItems(); // [] + * list.addItem("First item"); + * list.addItem("Second item"); + * list.getItems(); // ["First item", "Second item"] + * + * @returns {ListItem[]} A copy of the list of `ListItem` objects + * @since 0.2.0 + */ + public getItems(): ListItem[] { + return this.getAll() as ListItem[]; + } + + /** + * The `removeItemAt()` method removes the list item from the specified position. + * + * @example + * const list = new List(); + * list.addItem("First item"); + * list.addItem("Second item"); + * list.removeItemAt(0); // "First item" + * list.getItems(); // ["Second item"] + * list.removeItemAt(2); // undefined + * + * @param {number} position The index of the list item to remove (starting from 0). + * @returns {ListItem | undefined} The removed `ListItem` object + * or undefined if there is no list item at the specified position + * @since 0.2.0 + */ + public removeItemAt(position: number): ListItem | undefined { + return this.removeAt(position) as ListItem; + } + + /** + * The `clear()` method removes all items from the list. + * + * @example + * const list = new List(); + * list.addItem("First item"); // "First item" + * list.addItem("Second item"); // "First item", "Second item" + * list.clear(); // - + * + * @returns {List} The `List` object + * @since 0.2.0 + */ + public clear(): List { + let removedElement; + + do { + removedElement = this.removeAt(0); + } while (removedElement !== undefined); + + return this; + } + + /** + * The `size()` method returns the number of items in the list. + * + * @example + * const list = new List(); + * list.size(); // 0 + * list.addItem("First item"); + * list.addItem("Second item"); + * list.size(); // 2 + * + * @returns {number} The number of items in this list + * @since 0.2.0 + */ + public size(): number { + return this.getAll().length; + } +} diff --git a/src/text/ListItem.ts b/src/api/text/ListItem.ts similarity index 50% rename from src/text/ListItem.ts rename to src/api/text/ListItem.ts index 451ad15a..ad9648eb 100644 --- a/src/text/ListItem.ts +++ b/src/api/text/ListItem.ts @@ -1,19 +1,26 @@ import { OdfElement } from "../OdfElement"; import { Paragraph } from "./Paragraph"; -import { TextElementName } from "./TextElementName"; /** * This class represents an item in a list. * + * @example + * const list = document.getBody() + * .addList() + * .addItem("First item"); + * * @since 0.2.0 */ export class ListItem extends OdfElement { private paragraph: Paragraph; /** - * Creates a list item + * Creates a `ListItem` instance that represents an item in a list. + * + * @example + * new ListItem("First item"); * - * @param {string} [text] The text content of the list item + * @param {string} [text=""] The text content of the list item; defaults to an empty string if omitted * @since 0.2.0 */ public constructor(text?: string) { @@ -22,12 +29,4 @@ export class ListItem extends OdfElement { this.paragraph = new Paragraph(text); this.append(this.paragraph); } - - /** @inheritDoc */ - protected toXml(document: Document, parent: Element): void { - const listItemElement = document.createElement(TextElementName.TextListItem); - parent.appendChild(listItemElement); - - super.toXml(document, listItemElement); - } } diff --git a/src/api/text/OdfTextElement.ts b/src/api/text/OdfTextElement.ts new file mode 100644 index 00000000..1dcffb2c --- /dev/null +++ b/src/api/text/OdfTextElement.ts @@ -0,0 +1,39 @@ +import { OdfElement } from "../OdfElement"; + +/** + * This class represents text in a paragraph. + * + * @since 0.3.0 + * @private + */ +export class OdfTextElement extends OdfElement { + /** + * Creates a text + * + * @param {string} text The text content + * @since 0.3.0 + */ + public constructor(private text: string) { + super(); + } + + /** + * Sets the new text content. + * + * @param {string} text The new text content + * @since 0.3.0 + */ + public setText(text: string): void { + this.text = text; + } + + /** + * Returns the text content. + * + * @returns {string} The text content + * @since 0.3.0 + */ + public getText(): string { + return this.text; + } +} diff --git a/src/api/text/Paragraph.spec.ts b/src/api/text/Paragraph.spec.ts new file mode 100644 index 00000000..6d6a57d2 --- /dev/null +++ b/src/api/text/Paragraph.spec.ts @@ -0,0 +1,94 @@ +import { ParagraphStyle } from "../../style/ParagraphStyle"; +import { TextTransformation } from "../../style/TextTransformation"; +import { Image } from "../draw"; +import { Hyperlink } from "./Hyperlink"; +import { Paragraph } from "./Paragraph"; + +describe(Paragraph.name, () => { + const testText = "some text"; + + let paragraph: Paragraph; + + beforeEach(() => { + paragraph = new Paragraph(testText); + }); + + describe("text", () => { + it("return initial text", () => { + expect(paragraph.getText()).toBe(testText); + }); + + it("return empty text if initial text is not set", () => { + paragraph = new Paragraph(); + + expect(paragraph.getText()).toBe(""); + }); + + it("append the text", () => { + paragraph.addText(" some more text"); + + expect(paragraph.getText()).toEqual("some text some more text"); + }); + + it("replace existing text with specified text", () => { + paragraph.setText("some other text"); + + expect(paragraph.getText()).toEqual("some other text"); + }); + + it("return the text", () => { + paragraph.setText("some text"); + paragraph.addText(" some\nmore text"); + paragraph.addHyperlink(" link", "http://example.org/"); + paragraph.addText(" even more text"); + + expect(paragraph.getText()).toEqual("some text some\nmore text link even more text"); + }); + }); + + describe("hyperlink", () => { + it("return a hyperlink", () => { + const hyperlink = paragraph.addHyperlink("some linked text", "http://example.org/"); + + expect(hyperlink).toBeInstanceOf(Hyperlink); + expect(hyperlink.getText()).toEqual("some linked text"); + expect(hyperlink.getURI()).toEqual("http://example.org/"); + }); + }); + + describe("image", () => { + it("return an image", () => { + const image = paragraph.addImage("someImagePath"); + + expect(image).toBeInstanceOf(Image); + expect(image.getPath()).toEqual("someImagePath"); + }); + }); + + describe("style", () => { + let testStyle: ParagraphStyle; + + beforeEach(() => { + testStyle = new ParagraphStyle(); + }); + + it("return undefined by default", () => { + expect(paragraph.getStyle()).toBeUndefined(); + }); + + it("return previous set style", () => { + testStyle.setTextTransformation(TextTransformation.Uppercase); + + paragraph.setStyle(testStyle); + + expect(paragraph.getStyle()).toBe(testStyle); + }); + + it("ignore invalid input", () => { + paragraph.setStyle(testStyle); + paragraph.setStyle(null); + + expect(paragraph.getStyle()).toBe(testStyle); + }); + }); +}); diff --git a/src/api/text/Paragraph.ts b/src/api/text/Paragraph.ts new file mode 100644 index 00000000..681ce7ce --- /dev/null +++ b/src/api/text/Paragraph.ts @@ -0,0 +1,195 @@ +import { IParagraphStyle } from "../../style/IParagraphStyle"; +import { ParagraphStyle } from "../../style/ParagraphStyle"; +import { Image } from "../draw"; +import { OdfElement } from "../OdfElement"; +import { Hyperlink } from "./Hyperlink"; +import { OdfTextElement } from "./OdfTextElement"; + +/** + * This class represents a paragraph. + * + * @example + * document.getBody().addParagraph("Some text") + * .addText("\nEven more text") + * .addImage("/home/homer/myself.png"); + * + * @since 0.1.0 + */ +export class Paragraph extends OdfElement { + private style: IParagraphStyle | undefined; + + /** + * Creates a `Paragraph` instance. + * + * @example + * new Paragraph("Some text"); + * new Paragraph(); + * + * @param {string} [text] The text content of the paragraph; defaults to an empty string if omitted + * @since 0.1.0 + */ + public constructor(text?: string) { + super(); + + this.addText(text || ""); + } + + /** + * Appends the specified text to the end of the paragraph. + * + * @example + * new Paragraph("Some text") // Some text + * .addText("\nEven more text"); // Some text\nEven more text + * + * @param {string} text The additional text content + * @returns {Paragraph} The `Paragraph` object + * @since 0.1.0 + */ + public addText(text: string): Paragraph { + const elements = this.getAll(); + + if (elements.length > 0 && elements[elements.length - 1].constructor.name === OdfTextElement.name) { + const lastElement = elements[elements.length - 1] as OdfTextElement; + lastElement.setText(lastElement.getText() + text); + return this; + } + + this.append(new OdfTextElement(text)); + + return this; + } + + /** + * Returns the text content of the paragraph. + * Note: This will only return the text; other elements and markup will be omitted. + * + * @example + * const paragraph = new Paragraph("Some text, "); + * paragraph.addHyperlink("some linked text"); + * paragraph.addText(", even more text"); + * paragraph.getText(); // Some text, some linked text, even more text + * + * @returns {string} The text content of the paragraph + * @since 0.1.0 + */ + public getText(): string { + return this.getAll() + .map((value: OdfElement) => { + return value instanceof OdfTextElement ? value.getText() : ""; + }) + .join(""); + } + + /** + * Sets the text content of the paragraph. + * Note: This will replace any existing content of the paragraph. + * + * @example + * new Paragraph("Some text") // Some text + * .setText("Some other text"); // Some other text + * + * @param {string} text The new text content + * @returns {Paragraph} The `Paragraph` object + * @since 0.1.0 + */ + public setText(text: string): Paragraph { + this.removeText(); + this.addText(text || ""); + + return this; + } + + /** + * Appends the specified text as hyperlink to the end of the paragraph. + * + * @example + * new Paragraph("Some text, ") // Some text, + * .addHyperlink("some linked text"); // Some text, some linked text + * + * @param {string} text The text content of the hyperlink + * @param {string} uri The target URI of the hyperlink + * @returns {Hyperlink} The added `Hyperlink` object + * @since 0.3.0 + */ + public addHyperlink(text: string, uri: string): Hyperlink { + const hyperlink = new Hyperlink(text, uri); + this.append(hyperlink); + + return hyperlink; + } + + /** + * Appends the image of the denoted path to the end of the paragraph. + * The current paragraph will be set as anchor for the image. + * + * @example + * new Paragraph("Some text") + * .addImage("/home/homer/myself.png"); + * + * @param {string} path The path to the image file + * @returns {Image} The added `Image` object + * @since 0.3.0 + */ + public addImage(path: string): Image { + const image = new Image(path); + this.append(image); + + return image; + } + + /** + * Sets the new style of the paragraph. + * To reset the style, `undefined` must be given. + * + * @example + * new Paragraph("Some text") + * .setStyle(new ParagraphStyle()); + * + * @param {IParagraphStyle | undefined} style The new style or `undefined` to reset the style + * @returns {Paragraph} The `Paragraph` object + * @since 0.3.0 + */ + public setStyle(style: IParagraphStyle | undefined): Paragraph { + if (style instanceof ParagraphStyle) { + this.style = style; + } + + return this; + } + + /** + * Returns the style of the paragraph. + * + * @example + * const paragraph = new Paragraph("Some text"); + * paragraph.getStyle(); // undefined + * paragraph.setStyle(new ParagraphStyle()); + * paragraph.getStyle(); // previously set style + * + * @returns {IParagraphStyle | undefined} The style of the paragraph or `undefined` if no style was set + * @since 0.3.0 + */ + public getStyle(): IParagraphStyle | undefined { + return this.style; + } + + /** + * Removes the text content of the paragraph. + * + * @example + * new Paragraph("Some text") // Some text + * .removeText(); // "" + * + * @returns {Paragraph} The `Paragraph` object + * @private + */ + private removeText(): Paragraph { + const elements = this.getAll(); + + for (let index = elements.length - 1; index >= 0; index--) { + this.removeAt(index); + } + + return this; + } +} diff --git a/src/api/text/index.ts b/src/api/text/index.ts new file mode 100644 index 00000000..4265e7ae --- /dev/null +++ b/src/api/text/index.ts @@ -0,0 +1,6 @@ +export { Heading } from "./Heading"; +export { Hyperlink } from "./Hyperlink"; +export { List } from "./List"; +export { ListItem } from "./ListItem"; +export { Paragraph } from "./Paragraph"; +export { OdfTextElement } from "./OdfTextElement"; diff --git a/src/draw/Image.spec.ts b/src/draw/Image.spec.ts deleted file mode 100644 index 7e4c9396..00000000 --- a/src/draw/Image.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { join } from "path"; -import { Image } from "./Image"; -import { AnchorType } from "../style/AnchorType"; -import { ImageStyle } from "../style/ImageStyle"; -import { TextDocument } from "../TextDocument"; - -describe(Image.name, () => { - let document: TextDocument; - - beforeEach(() => { - document = new TextDocument(); - }); - - describe("#setStyle", () => { - it("set text anchor attribute on frame", () => { - document.addParagraph().addImage(join(__dirname, "..", "..", "test", "data", "ODF.png")); - - expect(document.toString()).toMatch(//); - }); - }); - - describe("#getStyle", () => { - let image: Image; - - beforeEach(() => { - image = new Image("somePath"); - }); - - it("return style by default", () => { - expect(image.getStyle()).toBeInstanceOf(ImageStyle); - }); - - it("return previous set style", () => { - const testStyle = new ImageStyle(); - testStyle.setAnchorType(AnchorType.AsChar); - - image.setStyle(testStyle); - - expect(image.getStyle()).toBe(testStyle); - }); - }); - - describe("#toXml", () => { - beforeEach(() => { - document.addParagraph().addImage(join(__dirname, "..", "..", "test", "data", "ODF.png")); - }); - - it("append a draw frame with image and base64 encoded image", () => { - const regex = new RegExp("" - + "" - + "" - + ".*" - + "" - + "" - + ""); - expect(document.toString()).toMatch(regex); - }); - }); -}); diff --git a/src/draw/Image.ts b/src/draw/Image.ts deleted file mode 100644 index 0bcc0826..00000000 --- a/src/draw/Image.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { readFileSync } from "fs"; -import { OdfElement } from "../OdfElement"; -import { OdfElementName } from "../OdfElementName"; -import { IImageStyle } from "../style/IImageStyle"; -import { ImageStyle } from "../style/ImageStyle"; -import { DrawElementName } from "./DrawElementName"; - -const ENCODING = "base64"; - -/** - * This class represents an image in a paragraph. - * - * @since 0.3.0 - */ -export class Image extends OdfElement { - private style: IImageStyle; - - /** - * Creates an image - * - * @param {string} path Path to the image file that should be embedded - * @since 0.3.0 - */ - public constructor(private path: string) { - super(); - - this.style = new ImageStyle(); - } - - /** - * Sets the new style of this image. - * - * @param {IImageStyle} style The new style - * @since 0.5.0 - */ - public setStyle(style: IImageStyle): void { - this.style = style; - } - - /** - * Returns the style of this image. - * - * @returns {IImageStyle} The style of the image - * @since 0.5.0 - */ - public getStyle(): IImageStyle { - return this.style; - } - - /** @inheritDoc */ - protected toXml(document: Document, parent: Element): void { - const frameElement = document.createElement(DrawElementName.DrawFrame); - parent.appendChild(frameElement); - - this.embedImage(document, frameElement); - - this.style.toXml(frameElement); - - super.toXml(document, frameElement); - } - - /** - * Creates the image element and embeds the denoted image base64 encoded binary data. - * - * @param {Document} document The XML document - * @param {Element} frameElement The parent node in the DOM (`draw:frame`) - * @private - */ - private embedImage(document: Document, frameElement: Element): void { - const image = document.createElement(DrawElementName.DrawImage); - frameElement.appendChild(image); - - const binaryData = document.createElement(OdfElementName.OfficeBinaryData); - image.appendChild(binaryData); - - const rawImage = readFileSync(this.path); - const base64Image = rawImage.toString(ENCODING); - const textNode = document.createTextNode(base64Image); - binaryData.appendChild(textNode); - } -} diff --git a/src/index.ts b/src/index.ts index 0b3fab98..26b0d7a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,20 @@ -export { TextDocument } from "./TextDocument"; - // draw -export { Image } from "./draw/Image"; +export { Image } from "./api/draw/Image"; // meta -export { Meta } from "./meta/Meta"; +export { Meta } from "./api/meta/Meta"; + +// office +export { TextBody } from "./api/office/TextBody"; +export { TextDocument } from "./api/office/TextDocument"; // style +export { FontFace } from "./api/style/FontFace"; +export { FontPitch } from "./api/style/FontPitch"; + +// style (legacy) export { AnchorType } from "./style/AnchorType"; export { Color } from "./style/Color"; -export { FontPitch } from "./style/FontPitch"; export { HorizontalAlignment } from "./style/HorizontalAlignment"; export { IImageStyle } from "./style/IImageStyle"; export { ImageStyle } from "./style/ImageStyle"; @@ -21,8 +26,8 @@ export { TextTransformation } from "./style/TextTransformation"; export { Typeface } from "./style/Typeface"; // text -export { Heading } from "./text/Heading"; -export { Hyperlink } from "./text/HyperLink"; -export { List } from "./text/List"; -export { ListItem } from "./text/ListItem"; -export { Paragraph } from "./text/Paragraph"; +export { Heading } from "./api/text/Heading"; +export { Hyperlink } from "./api/text/Hyperlink"; +export { List } from "./api/text/List"; +export { ListItem } from "./api/text/ListItem"; +export { Paragraph } from "./api/text/Paragraph"; diff --git a/src/style/Color.ts b/src/style/Color.ts index 5fb94fef..3da9f786 100644 --- a/src/style/Color.ts +++ b/src/style/Color.ts @@ -16,7 +16,7 @@ export class Color { public static fromHex(value: string): Color | never { const matches = value.match(/^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/); if (matches === null) { - throw new Error('Invalid color value'); + throw new Error("Invalid color value"); } return new Color(parseInt(matches[1], 16), parseInt(matches[2], 16), parseInt(matches[3], 16)); @@ -36,7 +36,7 @@ export class Color { if (Color.checkRange(red) && Color.checkRange(green) && Color.checkRange(blue)) { return new Color(red, green, blue); } - throw new Error('Invalid value for a color channel'); + throw new Error("Invalid value for a color channel"); } /** diff --git a/src/style/ImageStyle.spec.ts b/src/style/ImageStyle.spec.ts index 9139bb73..04d48bad 100644 --- a/src/style/ImageStyle.spec.ts +++ b/src/style/ImageStyle.spec.ts @@ -1,8 +1,8 @@ import { join } from "path"; -import { Image } from "../draw/Image"; +import { Image } from "../api/draw"; +import { TextDocument } from "../api/office"; import { AnchorType } from "./AnchorType"; import { ImageStyle } from "./ImageStyle"; -import { TextDocument } from "../TextDocument"; describe(ImageStyle.name, () => { let document: TextDocument; @@ -78,7 +78,7 @@ describe(ImageStyle.name, () => { let image: Image; beforeEach(() => { - image = document.addParagraph().addImage(join(__dirname, "..", "..", "test", "data", "ODF.png")); + image = document.getBody().addParagraph().addImage(join(__dirname, "..", "..", "test", "data", "ODF.png")); }); it("set the anchor type", () => { diff --git a/src/style/ImageStyle.ts b/src/style/ImageStyle.ts index 07de5a41..d720036b 100644 --- a/src/style/ImageStyle.ts +++ b/src/style/ImageStyle.ts @@ -1,4 +1,4 @@ -import { OdfAttributeName } from "../OdfAttributeName"; +import { OdfAttributeName } from "../xml/OdfAttributeName"; import { AnchorType } from "./AnchorType"; import { IImageStyle } from "./IImageStyle"; diff --git a/src/style/ParagraphProperties.spec.ts b/src/style/ParagraphProperties.spec.ts index 7fff9312..6d9ad5a1 100644 --- a/src/style/ParagraphProperties.spec.ts +++ b/src/style/ParagraphProperties.spec.ts @@ -1,10 +1,10 @@ +import { TextDocument } from "../api/office"; +import { Paragraph } from "../api/text"; import { HorizontalAlignment } from "./HorizontalAlignment"; import { ParagraphProperties } from "./ParagraphProperties"; import { ParagraphStyle } from "./ParagraphStyle"; import { TabStop } from "./TabStop"; import { TabStopType } from "./TabStopType"; -import { Paragraph } from "../text/Paragraph"; -import { TextDocument } from "../TextDocument"; describe(ParagraphProperties.name, () => { let properties: ParagraphProperties; @@ -16,7 +16,7 @@ describe(ParagraphProperties.name, () => { properties = new ParagraphProperties(); document = new TextDocument(); - paragraph = document.addParagraph(); + paragraph = document.getBody().addParagraph(); testStyle = new ParagraphStyle(); }); diff --git a/src/style/ParagraphProperties.ts b/src/style/ParagraphProperties.ts index f40c8fc8..a0337cdf 100644 --- a/src/style/ParagraphProperties.ts +++ b/src/style/ParagraphProperties.ts @@ -1,5 +1,5 @@ -import { OdfAttributeName } from "../OdfAttributeName"; -import { OdfElementName } from "../OdfElementName"; +import { OdfAttributeName } from "../xml/OdfAttributeName"; +import { OdfElementName } from "../xml/OdfElementName"; import { HorizontalAlignment } from "./HorizontalAlignment"; import { IParagraphProperties } from "./IParagraphProperties"; import { TabStop } from "./TabStop"; diff --git a/src/style/ParagraphStyle.spec.ts b/src/style/ParagraphStyle.spec.ts index 56581630..2d247092 100644 --- a/src/style/ParagraphStyle.spec.ts +++ b/src/style/ParagraphStyle.spec.ts @@ -1,6 +1,6 @@ +import { TextDocument } from "../api/office"; +import { Paragraph } from "../api/text"; import { ParagraphStyle } from "./ParagraphStyle"; -import { Paragraph } from "../text/Paragraph"; -import { TextDocument } from "../TextDocument"; describe(ParagraphStyle.name, () => { let document: TextDocument; @@ -9,7 +9,7 @@ describe(ParagraphStyle.name, () => { beforeEach(() => { document = new TextDocument(); - paragraph = document.addParagraph("test"); + paragraph = document.getBody().addParagraph("test"); testStyle = new ParagraphStyle(); }); @@ -40,7 +40,7 @@ describe(ParagraphStyle.name, () => { testStyle.setPageBreakBefore(); paragraph.setStyle(testStyle); - document.addParagraph().setStyle(testStyle); + document.getBody().addParagraph().setStyle(testStyle); /* tslint:disable-next-line:max-line-length */ expect(document.toString()).toMatch(/<\/style:style><\/office:automatic-styles>/); diff --git a/src/style/ParagraphStyle.ts b/src/style/ParagraphStyle.ts index e694af64..1a10ef50 100644 --- a/src/style/ParagraphStyle.ts +++ b/src/style/ParagraphStyle.ts @@ -1,6 +1,6 @@ import { createHash } from "crypto"; -import { OdfAttributeName } from "../OdfAttributeName"; -import { OdfElementName } from "../OdfElementName"; +import { OdfAttributeName } from "../xml/OdfAttributeName"; +import { OdfElementName } from "../xml/OdfElementName"; import { Color } from "./Color"; import { HorizontalAlignment } from "./HorizontalAlignment"; import { IParagraphStyle } from "./IParagraphStyle"; diff --git a/src/style/StyleHelper.ts b/src/style/StyleHelper.ts index 006f82ec..c4b48a36 100644 --- a/src/style/StyleHelper.ts +++ b/src/style/StyleHelper.ts @@ -1,4 +1,4 @@ -import { OdfElementName } from "../OdfElementName"; +import { OdfElementName } from "../xml/OdfElementName"; /** * Utility class for dealing with styles. diff --git a/src/style/TabStop.spec.ts b/src/style/TabStop.spec.ts index d8ec2eb8..85e9abfa 100644 --- a/src/style/TabStop.spec.ts +++ b/src/style/TabStop.spec.ts @@ -1,7 +1,7 @@ +import { TextDocument } from "../api/office"; import { ParagraphStyle } from "./ParagraphStyle"; import { TabStop } from "./TabStop"; import { TabStopType } from "./TabStopType"; -import { TextDocument } from "../TextDocument"; describe(TabStop.name, () => { describe("#constructor", () => { @@ -87,7 +87,7 @@ describe(TabStop.name, () => { describe("#toXml", () => { it("return the current position", () => { const document = new TextDocument(); - const paragraph = document.addParagraph(); + const paragraph = document.getBody().addParagraph(); const style = new ParagraphStyle(); style.addTabStop(new TabStop(2, TabStopType.Center)); diff --git a/src/style/TabStop.ts b/src/style/TabStop.ts index 5fc416e2..583cf158 100644 --- a/src/style/TabStop.ts +++ b/src/style/TabStop.ts @@ -1,5 +1,5 @@ -import { OdfAttributeName } from "../OdfAttributeName"; -import { OdfElementName } from "../OdfElementName"; +import { OdfAttributeName } from "../xml/OdfAttributeName"; +import { OdfElementName } from "../xml/OdfElementName"; import { TabStopType } from "./TabStopType"; /** diff --git a/src/style/TextProperties.spec.ts b/src/style/TextProperties.spec.ts index 92164038..063a2067 100644 --- a/src/style/TextProperties.spec.ts +++ b/src/style/TextProperties.spec.ts @@ -1,10 +1,10 @@ +import { TextDocument } from "../api/office"; +import { Paragraph } from "../api/text"; import { Color } from "./Color"; import { ParagraphStyle } from "./ParagraphStyle"; import { TextProperties } from "./TextProperties"; import { TextTransformation } from "./TextTransformation"; import { Typeface } from "./Typeface"; -import { Paragraph } from "../text/Paragraph"; -import { TextDocument } from "../TextDocument"; describe(TextProperties.name, () => { let properties: TextProperties; @@ -16,7 +16,7 @@ describe(TextProperties.name, () => { properties = new TextProperties(); document = new TextDocument(); - paragraph = document.addParagraph("test"); + paragraph = document.getBody().addParagraph("test"); testStyle = new ParagraphStyle(); }); diff --git a/src/style/TextProperties.ts b/src/style/TextProperties.ts index 77d85e02..294ebef5 100644 --- a/src/style/TextProperties.ts +++ b/src/style/TextProperties.ts @@ -1,5 +1,5 @@ -import { OdfAttributeName } from "../OdfAttributeName"; -import { OdfElementName } from "../OdfElementName"; +import { OdfAttributeName } from "../xml/OdfAttributeName"; +import { OdfElementName } from "../xml/OdfElementName"; import { Color } from "./Color"; import { ITextProperties } from "./ITextProperties"; import { TextTransformation } from "./TextTransformation"; diff --git a/src/text/Heading.spec.ts b/src/text/Heading.spec.ts deleted file mode 100644 index d339f4d3..00000000 --- a/src/text/Heading.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Heading } from "./Heading"; -import { TextDocument } from "../TextDocument"; - -describe(Heading.name, () => { - let document: TextDocument; - let heading: Heading; - - beforeEach(() => { - document = new TextDocument(); - }); - - describe("#addHeading", () => { - it("insert an empty heading with default level 1", () => { - document.addHeading(); - - const documentAsString = document.toString(); - expect(documentAsString).toMatch(//); - }); - - it("insert a heading with given text and default level 1", () => { - document.addHeading("heading"); - - const documentAsString = document.toString(); - expect(documentAsString).toMatch(/heading<\/text:h>/); - }); - - it("insert a heading with given text and given level", () => { - document.addHeading("heading", 2); - - const documentAsString = document.toString(); - expect(documentAsString).toMatch(/heading<\/text:h>/); - }); - }); - - describe("#setLevel", () => { - beforeEach(() => { - heading = document.addHeading("Heading", 2); - }); - - it("change the current level to the given value", () => { - heading.setLevel(3); - const headingLevel = heading.getLevel(); - - expect(headingLevel).toBe(3); - }); - - it("change the current level to the default value, if the given value is invalid", () => { - heading.setLevel(-2); - const headingLevel = heading.getLevel(); - - expect(headingLevel).toBe(Heading.DEFAULT_LEVEL); - }); - }); - - describe("#getLevel", () => { - beforeEach(() => { - heading = document.addHeading("heading", 2); - }); - - it("return the current level", () => { - const headingLevel = heading.getLevel(); - - expect(headingLevel).toBe(2); - }); - }); -}); diff --git a/src/text/Heading.ts b/src/text/Heading.ts deleted file mode 100644 index 5246d7dc..00000000 --- a/src/text/Heading.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { OdfAttributeName } from "../OdfAttributeName"; -import { Paragraph } from "./Paragraph"; -import { TextElementName } from "./TextElementName"; - -/** - * This class represents a heading. - * - * @since 0.1.0 - */ -export class Heading extends Paragraph { - public static DEFAULT_LEVEL = 1; - - /** - * Creates a heading - * - * @param {string} [text] The text content of the heading - * @param {number} [level] The heading level; defaults to 1 if omitted - * @since 0.1.0 - */ - public constructor(text?: string, private level = Heading.DEFAULT_LEVEL) { - super(text); - - this.setLevel(level); - } - - /** - * Sets the level of this heading. - * - * @param {number} level The heading level - * @since 0.1.0 - */ - public setLevel(level: number): void { - this.level = level > Heading.DEFAULT_LEVEL ? level : Heading.DEFAULT_LEVEL; - } - - /** - * Returns the level of this heading. - * - * @returns {number} The heading level - * @since 0.1.0 - */ - public getLevel(): number { - return this.level; - } - - /** @inheritDoc */ - protected createElement(document: Document): Element { - const heading = document.createElement(TextElementName.TextHeading); - heading.setAttribute(OdfAttributeName.TextOutlineLevel, this.level.toString(10)); - - return heading; - } -} diff --git a/src/text/HyperLink.ts b/src/text/HyperLink.ts deleted file mode 100644 index 67d0d4e1..00000000 --- a/src/text/HyperLink.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { OdfAttributeName } from "../OdfAttributeName"; -import { OdfTextElement } from "./OdfTextElement"; -import { TextElementName } from "./TextElementName"; - -const LINK_TYPE = "simple"; - -/** - * This class represents a hyperlink in a paragraph. - * - * @since 0.3.0 - */ -export class Hyperlink extends OdfTextElement { - /** - * Creates a hyperlink - * - * @param {string} text The text content of the hyperlink - * @param {string} uri The target URI of the hyperlink - * @since 0.3.0 - */ - public constructor(text: string, private uri: string) { - super(text); - } - - /** - * Sets the target URI for this hyperlink. - * - * @param {string} uri The new target URI - * @since 0.3.0 - */ - public setURI(uri: string): void { - this.uri = uri; - } - - /** - * Returns the target URI of this hyperlink. - * - * @returns {string} The target URI - * @since 0.3.0 - */ - public getURI(): string { - return this.uri; - } - - /** @inheritDoc */ - protected toXml(document: Document, parent: Element): void { - const text = this.getText(); - - if (text === undefined || text === "") { - return; - } - - if (this.uri === undefined || this.uri === "") { - return super.toXml(document, parent); - } - - const hyperlink = document.createElement(TextElementName.TextHyperlink); - parent.appendChild(hyperlink); - hyperlink.setAttribute(OdfAttributeName.XlinkType, LINK_TYPE); - hyperlink.setAttribute(OdfAttributeName.XlinkHref, this.uri); - - const textNode = document.createTextNode(text); - hyperlink.appendChild(textNode); - } -} diff --git a/src/text/Hyperlink.spec.ts b/src/text/Hyperlink.spec.ts deleted file mode 100644 index 9bbdd4a6..00000000 --- a/src/text/Hyperlink.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Hyperlink } from "./HyperLink"; -import { TextDocument } from "../TextDocument"; - -describe(Hyperlink.name, () => { - const testText = "some text"; - const testUri = "http://example.org/"; - - let document: TextDocument; - - beforeEach(() => { - document = new TextDocument(); - }); - - describe("#addHyperlink", () => { - it("append a linked text", () => { - document.addParagraph(testText).addHyperlink(" some linked text", testUri); - - 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", () => { - document.addParagraph(testText).addHyperlink("", testUri); - - const documentAsString = document.toString(); - expect(documentAsString).toMatch(/some text<\/text:p>/); - }); - - it("not create a hyperlink but add the text if URI is empty", () => { - document.addParagraph(testText).addHyperlink(" some linked text", ""); - - const documentAsString = document.toString(); - expect(documentAsString).toMatch(/some text some linked text<\/text:p>/); - }); - }); - - describe("#setURI", () => { - let hyperlink: Hyperlink; - - beforeEach(() => { - hyperlink = document.addParagraph().addHyperlink(testText, testUri); - }); - - it("change the current URI to the given value", () => { - hyperlink.setURI("localhost"); - - expect(hyperlink.getURI()).toBe("localhost"); - }); - }); - - describe("#getURI", () => { - let hyperlink: Hyperlink; - - beforeEach(() => { - hyperlink = document.addParagraph().addHyperlink(testText, testUri); - }); - - it("return the current URI", () => { - expect(hyperlink.getURI()).toBe(testUri); - }); - }); -}); diff --git a/src/text/List.ts b/src/text/List.ts deleted file mode 100644 index a742778d..00000000 --- a/src/text/List.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { OdfElement } from "../OdfElement"; -import { ListItem } from "./ListItem"; -import { TextElementName } from "./TextElementName"; - -/** - * This class represents a list. - * It can contain multiple list items. - * - * @since 0.2.0 - */ -export class List extends OdfElement { - /** - * Creates a list - * - * @since 0.2.0 - */ - public constructor() { - super(); - } - - /** - * Adds a new list item with the specified text or adds the specified item to the list. - * - * @param {string | ListItem} [item] The text content of the new item or the item to add - * @returns {ListItem} The newly added list item - * @since 0.2.0 - */ - public addItem(item?: string | ListItem): ListItem { - if (item instanceof ListItem) { - this.append(item); - return item; - } - - const listItem = new ListItem(item); - this.append(listItem); - - return listItem; - } - - /** - * Inserts a new list item with the specified text or inserts the specified item at the specified position. - * The item is inserted before the item at the specified position. - * - * @param {number} position The index at which to insert the list item (starting from 0). - * @param {string | ListItem} item The text content of the new item or the item to insert - * @returns {ListItem} The newly added list item - * @since 0.2.0 - */ - public insertItem(position: number, item: string | ListItem): ListItem { - if (item instanceof ListItem) { - this.insert(position, item); - return item; - } - - const listItem = new ListItem(item); - this.insert(position, listItem); - - return listItem; - } - - /** - * Returns the item at the specified position in this list. - * If an invalid position is given, undefined is returned. - * - * @param {number} position The index of the requested the list item (starting from 0). - * @returns {ListItem | undefined} The list item at the specified position - * or undefined if there is no list item at the specified position - * @since 0.2.0 - */ - public getItem(position: number): ListItem | undefined { - return this.get(position) as ListItem; - } - - /** - * Returns all list items. - * - * @returns {ListItem[]} A copy of the list of list items - * @since 0.2.0 - */ - public getItems(): ListItem[] { - return this.getAll() as ListItem[]; - } - - /** - * Removes the list item from the specified position. - * - * @param {number} position The index of the list item to remove (starting from 0). - * @returns {ListItem | undefined} The removed list item - * or undefined if there is no list item at the specified position - * @since 0.2.0 - */ - public removeItemAt(position: number): ListItem | undefined { - return this.removeAt(position) as ListItem; - } - - /** - * Removes all items from this list. - * - * @since 0.2.0 - */ - public clear(): void { - let removedElement; - - do { - removedElement = this.removeAt(0); - } while (removedElement !== undefined); - } - - /** - * Returns the number of items in this list. - * - * @returns {number} The number of items in this list - * @since 0.2.0 - */ - public size(): number { - return this.getAll().length; - } - - /** @inheritDoc */ - protected toXml(document: Document, parent: Element): void { - if (this.hasChildren() === false) { - return; - } - - const listElement = document.createElement(TextElementName.TextList); - parent.appendChild(listElement); - - super.toXml(document, listElement); - } -} diff --git a/src/text/Paragraph.spec.ts b/src/text/Paragraph.spec.ts deleted file mode 100644 index 0df5da81..00000000 --- a/src/text/Paragraph.spec.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { join } from "path"; -import { ParagraphStyle } from "../style/ParagraphStyle"; -import { Paragraph } from "./Paragraph"; -import { TextDocument } from "../TextDocument"; - -describe(Paragraph.name, () => { - let document: TextDocument; - - beforeEach(() => { - document = new TextDocument(); - }); - - describe("#addParagraph", () => { - it("insert an empty paragraph", () => { - document.addParagraph(); - - expect(document.toString()).toMatch(//); - }); - - it("insert a paragraph with specified text", () => { - document.addParagraph("some text"); - - expect(document.toString()).toMatch(/some text<\/text:p>/); - }); - }); - - describe("#addText", () => { - it("set the text if element is empty", () => { - document.addParagraph().addText("some text"); - - expect(document.toString()).toMatch(/some text<\/text:p>/); - }); - - it("append the text", () => { - document.addParagraph("some text").addText(" some more text"); - - expect(document.toString()).toMatch(/some text some more text<\/text:p>/); - }); - }); - - describe("#getText", () => { - it("return the text", () => { - const paragraph = document.addParagraph("some text"); - paragraph.addText(" some\nmore text"); - paragraph.addHyperlink(" link", "http://example.org/"); - paragraph.addText(" even more text"); - - expect(paragraph.getText()).toEqual("some text some\nmore text link even more text"); - }); - }); - - describe("#setText", () => { - it("replace existing text with specified text", () => { - document.addParagraph("some text").setText("some other text"); - - expect(document.toString()).toMatch(/some other text<\/text:p>/); - }); - }); - - it("replace newline with line break", () => { - document.addParagraph("some text\nsome more text"); - - expect(document.toString()).toMatch(/some textsome more text<\/text:p>/); - }); - - it("replace tab with tabulation", () => { - document.addParagraph("some\ttabbed\t\ttext"); - - expect(document.toString()).toMatch(/sometabbedtext<\/text:p>/); - }); - - it("replace sequence of spaces with space node", () => { - document.addParagraph(" some spacey text "); - - /* tslint:disable-next-line:max-line-length */ - expect(document.toString()).toMatch(/ some spacey text <\/text:p>/); - }); - - it("ignore carriage return character", () => { - document.addParagraph("some text\r\nsome\r more text"); - - expect(document.toString()).toMatch(/some textsome more text<\/text:p>/); - }); - - describe("#addHyperlink", () => { - it("append a linked text", () => { - document.addParagraph("some text").addHyperlink(" some linked text", "http://example.org/"); - - /* tslint:disable-next-line:max-line-length */ - expect(document.toString()).toMatch(/some text some linked text<\/text:a><\/text:p>/); - }); - }); - - describe("#addImage", () => { - it("append a draw frame with image and binary data", () => { - document.addParagraph().addImage(join(__dirname, "..", "..", "test", "data", "ODF.png")); - - const regex = new RegExp("" - + "" - + "" - + ".*" - + "" - + "" - + ""); - expect(document.toString()).toMatch(regex); - }); - }); - - describe("#setStyle", () => { - let paragraph: Paragraph; - let testStyle: ParagraphStyle; - - beforeEach(() => { - paragraph = document.addParagraph("some text"); - testStyle = new ParagraphStyle(); - }); - - it("set style-name attribute on paragraph if any style property was set", () => { - testStyle.setPageBreakBefore(); - paragraph.setStyle(testStyle); - - expect(document.toString()).toMatch(/some text<\/text:p>/); - }); - - it("not set style-name attribute if default style is set", () => { - paragraph.setStyle(testStyle); - - expect(document.toString()).toMatch(/some text<\/text:p>/); - }); - }); - - describe("#getStyle", () => { - let paragraph: Paragraph; - - beforeEach(() => { - paragraph = document.addParagraph("some text"); - }); - - it("return undefined if no style was set", () => { - expect(paragraph.getStyle()).toBeUndefined(); - }); - - it("return previous set style", () => { - const testStyle = new ParagraphStyle(); - testStyle.setPageBreakBefore(); - - paragraph.setStyle(testStyle); - - expect(paragraph.getStyle()).toBe(testStyle); - }); - }); -}); diff --git a/src/text/Paragraph.ts b/src/text/Paragraph.ts deleted file mode 100644 index fbe9717d..00000000 --- a/src/text/Paragraph.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { Image } from "../draw/Image"; -import { OdfElement } from "../OdfElement"; -import { IParagraphStyle } from "../style/IParagraphStyle"; -import { Hyperlink } from "./HyperLink"; -import { OdfTextElement } from "./OdfTextElement"; -import { TextElementName } from "./TextElementName"; - -/** - * This class represents a paragraph. - * - * @since 0.1.0 - */ -export class Paragraph extends OdfElement { - private style: IParagraphStyle | undefined; - - /** - * Creates a paragraph - * - * @param {string} [text] The text content of the paragraph - * @since 0.1.0 - */ - public constructor(text?: string) { - super(); - - this.addText(text || ""); - } - - /** - * Appends the specified text to the end of this paragraph. - * - * @param {string} text The additional text content - * @since 0.1.0 - */ - public addText(text: string): void { - const elements = this.getAll(); - - if (elements.length > 0 && elements[elements.length - 1].constructor.name === OdfTextElement.name) { - const lastElement = elements[elements.length - 1] as OdfTextElement; - lastElement.setText(lastElement.getText() + text); - return; - } - - this.append(new OdfTextElement(text)); - } - - /** - * Returns the text content of this paragraph. - * Note: This will only return the text; other elements and markup will be omitted. - * - * @returns {string} The text content of this paragraph - * @since 0.1.0 - */ - public getText(): string { - return this.getAll() - .map((value: OdfElement) => { - return value instanceof OdfTextElement ? value.getText() : ""; - }) - .join(""); - } - - /** - * Sets the text content of this paragraph. - * Note: This will replace any existing content of the paragraph. - * - * @param {string} text The new text content - * @since 0.1.0 - */ - public setText(text: string): void { - this.removeText(); - this.addText(text || ""); - } - - /** - * 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 target URI of the hyperlink - * @returns {Hyperlink} The newly added hyperlink - * @since 0.3.0 - */ - public addHyperlink(text: string, uri: string): Hyperlink { - const hyperlink = new Hyperlink(text, uri); - this.append(hyperlink); - - return hyperlink; - } - - /** - * Appends the image of the denoted path to the end of this paragraph. - * The current paragraph will be set as anchor for the image. - * - * @param {string} path The path to the image file - * @returns {Image} The newly added image - * @since 0.3.0 - */ - public addImage(path: string): Image { - const image = new Image(path); - this.append(image); - - return image; - } - - /** - * Sets the new style of this paragraph. - * To reset the style, `undefined` must be given. - * - * @param {IParagraphStyle | undefined} style The new style or `undefined` to reset the style - * @since 0.3.0 - */ - public setStyle(style: IParagraphStyle | undefined): void { - this.style = style; - } - - /** - * Returns the style of this paragraph. - * - * @returns {IParagraphStyle | undefined} The style of the paragraph or `undefined` if no style was set - * @since 0.3.0 - */ - public getStyle(): IParagraphStyle | undefined { - return this.style; - } - - /** - * Creates the paragraph element. - * - * @param {Document} document The XML document - * @returns {Element} The DOM element representing this paragraph - * @since 0.1.0 - */ - protected createElement(document: Document): Element { - return document.createElement(TextElementName.TextParagraph); - } - - /** @inheritDoc */ - protected toXml(document: Document, parent: Element): void { - const paragraph = this.createElement(document); - parent.appendChild(paragraph); - - if (this.style !== undefined) { - this.style.toXml(document, paragraph); - } - - super.toXml(document, paragraph); - } - - /** - * Removes the text content of this paragraph. - * @private - */ - private removeText(): void { - const elements = this.getAll(); - - for (let index = elements.length - 1; index >= 0; index--) { - this.removeAt(index); - } - } -} diff --git a/src/xml/DomVisitor.spec.ts b/src/xml/DomVisitor.spec.ts new file mode 100644 index 00000000..7c124a14 --- /dev/null +++ b/src/xml/DomVisitor.spec.ts @@ -0,0 +1,185 @@ +/* tslint:disable:max-line-length */ +import { join } from "path"; +import { DOMImplementation, XMLSerializer } from "xmldom"; +import { Image } from "../api/draw"; +import { Heading, Hyperlink, List, Paragraph } from "../api/text"; +import { ParagraphStyle } from "../style/ParagraphStyle"; +import { DomVisitor } from "./DomVisitor"; +import { OdfElementName } from "./OdfElementName"; + +fdescribe(DomVisitor.name, () => { + describe("#visit", () => { + const testText = "some text"; + + let domVisitor: DomVisitor; + let testDocument: Document; + let testRoot: Element; + + beforeEach(() => { + testDocument = new DOMImplementation().createDocument("someNameSpace", OdfElementName.OfficeDocument, null); + testRoot = testDocument.firstChild as Element; + + domVisitor = new DomVisitor(); + }); + + describe("#visitHeading", () => { + let heading: Heading; + + beforeEach(() => { + heading = new Heading(testText, 2); + }); + + it("add a heading with level 2 and the text", () => { + domVisitor.visit(heading, testDocument, testRoot); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + expect(documentAsString).toMatch(/some text<\/text:h>/); + }); + + it("call `toXml` on a style if a style is set", () => { + const testStyle = new ParagraphStyle(); + const styleToXmlSpy = jest.spyOn(testStyle, "toXml"); + + heading.setStyle(testStyle); + + domVisitor.visit(heading, testDocument, testRoot); + + expect(styleToXmlSpy).toHaveBeenCalledWith(testDocument, expect.any(Object)); + }); + }); + + describe("#visitHyperlink", () => { + let hyperlink: Hyperlink; + + beforeEach(() => { + hyperlink = new Hyperlink(testText, "http://example.org/"); + }); + + it("add a linked text", () => { + domVisitor.visit(hyperlink, testDocument, testRoot); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + expect(documentAsString).toMatch(/some text<\/text:a>/); + }); + }); + + describe("#visitImage", () => { + let image: Image; + + beforeEach(() => { + image = new Image(join(__dirname, "..", "..", "test", "data", "ODF.png")); + }); + + it("add a draw frame with image and base64 encoded image", () => { + domVisitor.visit(image, testDocument, testRoot); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + expect(documentAsString).toMatch(/.*<\/office:binary-data><\/draw:image><\/draw:frame>/); + }); + }); + + describe("#visitList", () => { + let list: List; + + beforeEach(() => { + list = new List(); + }); + + it("NOT insert an empty list", () => { + domVisitor.visit(list, testDocument, testRoot); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + expect(documentAsString).not.toMatch(/ { + list.addItem("first"); + + domVisitor.visit(list, testDocument, testRoot); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + expect(documentAsString).toMatch(/first<\/text:p><\/text:list-item><\/text:list>/); + }); + }); + + describe("visitOdfText", () => { + let paragraph: Paragraph; + + beforeEach(() => { + paragraph = new Paragraph(); + }); + + it("replace newline with line break", () => { + paragraph.setText("some text\nsome more text"); + + domVisitor.visit(paragraph, testDocument, testRoot); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + expect(documentAsString).toMatch(/some textsome more text<\/text:p>/); + }); + + it("replace tab with tabulation", () => { + paragraph.setText("some\ttabbed\t\ttext"); + + domVisitor.visit(paragraph, testDocument, testRoot); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + expect(documentAsString).toMatch(/sometabbedtext<\/text:p>/); + }); + + it("replace sequence of spaces with space node", () => { + paragraph.setText(" some spacey text "); + + domVisitor.visit(paragraph, testDocument, testRoot); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + /* tslint:disable-next-line:max-line-length */ + expect(documentAsString).toMatch(/ some spacey text <\/text:p>/); + }); + + it("ignore carriage return character", () => { + paragraph.setText("some text\r\nsome\r more text"); + + domVisitor.visit(paragraph, testDocument, testRoot); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + expect(documentAsString).toMatch(/some textsome more text<\/text:p>/); + }); + }); + + describe("#visitParagraph", () => { + let paragraph: Paragraph; + + beforeEach(() => { + paragraph = new Paragraph(testText); + }); + + it("add an empty paragraph", () => { + paragraph.setText(""); + + domVisitor.visit(paragraph, testDocument, testRoot); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + expect(documentAsString).toMatch(//); + }); + + it("add a paragraph with specified text", () => { + domVisitor.visit(paragraph, testDocument, testRoot); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + expect(documentAsString).toMatch(/some text<\/text:p>/); + }); + + it("call `toXml` on a style if a style is set", () => { + const testStyle = new ParagraphStyle(); + const styleToXmlSpy = jest.spyOn(testStyle, "toXml"); + + paragraph.setStyle(testStyle); + + domVisitor.visit(paragraph, testDocument, testRoot); + + expect(styleToXmlSpy).toHaveBeenCalledWith(testDocument, expect.any(Object)); + }); + }); + }); +}); diff --git a/src/xml/DomVisitor.ts b/src/xml/DomVisitor.ts new file mode 100644 index 00000000..81c0fac9 --- /dev/null +++ b/src/xml/DomVisitor.ts @@ -0,0 +1,141 @@ +import { readFileSync } from "fs"; +import { Image } from "../api/draw"; +import { OdfElement } from "../api/OdfElement"; +import { TextBody } from "../api/office"; +import { Heading, Hyperlink, List, ListItem, OdfTextElement, Paragraph } from "../api/text"; +import { DrawElementName } from "./DrawElementName"; +import { OdfAttributeName } from "./OdfAttributeName"; +import { OdfElementName } from "./OdfElementName"; +import { OdfTextElementWriter } from "./OdfTextElementWriter"; +import { TextElementName } from "./TextElementName"; + +const IMAGE_ENCODING = "base64"; +const HYPERLINK_LINK_TYPE = "simple"; + +export class DomVisitor { + public visit(odfElement: OdfElement, document: Document, parent: Element): void { + let currentElement: Element; + if (odfElement instanceof Heading) { + currentElement = this.visitHeading(odfElement, document, parent); + } else if (odfElement instanceof Hyperlink) { + currentElement = this.visitHyperlink(odfElement, document, parent); + } else if (odfElement instanceof Image) { + currentElement = this.visitImage(odfElement, document, parent); + } else if (odfElement instanceof List) { + currentElement = this.visitList(odfElement, document, parent); + } else if (odfElement instanceof ListItem) { + currentElement = this.visitListItem(document, parent); + } else if (odfElement instanceof OdfTextElement) { + currentElement = this.visitOdfText(odfElement, document, parent); + } else if (odfElement instanceof Paragraph) { + currentElement = this.visitParagraph(odfElement, document, parent); + } else if (odfElement instanceof TextBody) { + currentElement = this.visitTextBody(document, parent); + } + + odfElement.getAll().forEach((odfChildElement) => { + this.visit(odfChildElement, document, currentElement); + }); + } + + private visitHeading(heading: Heading, document: Document, parent: Element): Element { + const headingElement = document.createElement(TextElementName.TextHeading); + headingElement.setAttribute(OdfAttributeName.TextOutlineLevel, heading.getLevel().toString(10)); + parent.appendChild(headingElement); + + const style = heading.getStyle(); + if (style !== undefined) { + style.toXml(document, headingElement); + } + + return headingElement; + } + + private visitHyperlink(hyperlink: Hyperlink, document: Document, parent: Element): Element { + const hyperlinkElement = document.createElement(TextElementName.TextHyperlink); + hyperlinkElement.setAttribute(OdfAttributeName.XlinkType, HYPERLINK_LINK_TYPE); + hyperlinkElement.setAttribute(OdfAttributeName.XlinkHref, hyperlink.getURI()); + parent.appendChild(hyperlinkElement); + + this.visitOdfText(hyperlink, document, hyperlinkElement); + + return hyperlinkElement; + } + + private visitImage(image: Image, document: Document, parent: Element): Element { + const frameElement = document.createElement(DrawElementName.DrawFrame); + parent.appendChild(frameElement); + + this.embedImage(document, frameElement, image); + + image.getStyle().toXml(frameElement); + + return frameElement; + } + + private visitList(list: List, document: Document, parent: Element): Element { + if (list.size() === 0) { + return parent; + } + + const listElement = document.createElement(TextElementName.TextList); + parent.appendChild(listElement); + + return listElement; + } + + private visitListItem(document: Document, parent: Element): Element { + const listItemElement = document.createElement(TextElementName.TextListItem); + parent.appendChild(listItemElement); + + return listItemElement; + } + + private visitOdfText(odfText: OdfTextElement, document: Document, parent: Element): Element { + new OdfTextElementWriter().write(odfText, document, parent); + return parent; + } + + private visitParagraph(paragraph: Paragraph, document: Document, parent: Element): Element { + const paragraphElement = document.createElement(TextElementName.TextParagraph); + parent.appendChild(paragraphElement); + + const style = paragraph.getStyle(); + if (style !== undefined) { + style.toXml(document, paragraphElement); + } + + return paragraphElement; + } + + private visitTextBody(document: Document, parent: Element): Element { + const bodyElement = document.createElement(OdfElementName.OfficeBody); + parent.appendChild(bodyElement); + + const textElement = document.createElement(OdfElementName.OfficeText); + bodyElement.appendChild(textElement); + + return textElement; + } + + /** + * Creates the image element and embeds the denoted image base64 encoded binary data. + * + * @param {Document} document The XML document + * @param {Element} frameElement The parent node in the DOM (`draw:frame`) + * @param {Image} image The image + * @private + */ + private embedImage(document: Document, frameElement: Element, image: Image): void { + const imageElement = document.createElement(DrawElementName.DrawImage); + frameElement.appendChild(imageElement); + + const binaryData = document.createElement(OdfElementName.OfficeBinaryData); + imageElement.appendChild(binaryData); + + const rawImage = readFileSync(image.getPath()); + const base64Image = rawImage.toString(IMAGE_ENCODING); + const textNode = document.createTextNode(base64Image); + binaryData.appendChild(textNode); + } +} diff --git a/src/draw/DrawElementName.ts b/src/xml/DrawElementName.ts similarity index 100% rename from src/draw/DrawElementName.ts rename to src/xml/DrawElementName.ts diff --git a/src/OdfAttributeName.ts b/src/xml/OdfAttributeName.ts similarity index 100% rename from src/OdfAttributeName.ts rename to src/xml/OdfAttributeName.ts diff --git a/src/OdfElementName.ts b/src/xml/OdfElementName.ts similarity index 100% rename from src/OdfElementName.ts rename to src/xml/OdfElementName.ts diff --git a/src/text/OdfTextElement.ts b/src/xml/OdfTextElementWriter.ts similarity index 77% rename from src/text/OdfTextElement.ts rename to src/xml/OdfTextElementWriter.ts index d38996b7..08daa155 100644 --- a/src/text/OdfTextElement.ts +++ b/src/xml/OdfTextElementWriter.ts @@ -1,59 +1,27 @@ -import { OdfElement } from "../OdfElement"; +import { OdfTextElement } from "../api/text/OdfTextElement"; import { TextElementName } from "./TextElementName"; const SPACE = " "; -/** - * This class represents text in a paragraph. - * - * @since 0.3.0 - * @private - */ -export class OdfTextElement extends OdfElement { +export class OdfTextElementWriter { /** - * Creates a text - * - * @param {string} text The text content - * @since 0.3.0 - */ - public constructor(private text: string) { - super(); - } - - /** - * Sets the new text content. - * - * @param {string} text The new text content - * @since 0.3.0 + * @inheritdoc + * @since 0.7.0 */ - public setText(text: string): void { - this.text = text; - } - - /** - * Returns the text content. - * - * @returns {string} The text content - * @since 0.3.0 - */ - public getText(): string { - return this.text; - } - - /** @inheritDoc */ - protected toXml(document: Document, parent: Element): void { - if (this.text === undefined || this.text === "") { + public write(odfText: OdfTextElement, document: Document, parent: Element): void { + const text = odfText.getText(); + if (text === undefined || text === "") { return; } let str = ""; - for (let index = 0; index < this.text.length; index++) { - const currentChar = this.text.charAt(index); + for (let index = 0; index < text.length; index++) { + const currentChar = text.charAt(index); switch (currentChar) { case SPACE: str += currentChar; - const count = this.findNextNonSpaceCharacter(this.text, index) - 1; + const count = this.findNextNonSpaceCharacter(text, index) - 1; if (count > 0) { this.appendTextNode(document, parent, str); this.appendSpaceNode(document, parent, count); diff --git a/src/xml/TextDocumentWriter.spec.ts b/src/xml/TextDocumentWriter.spec.ts new file mode 100644 index 00000000..2d99b97a --- /dev/null +++ b/src/xml/TextDocumentWriter.spec.ts @@ -0,0 +1,81 @@ +import { XMLSerializer } from "xmldom"; +import { TextDocument } from "../api/office"; +import { FontPitch } from "../api/style"; +import { TextDocumentWriter } from "./TextDocumentWriter"; + +jest.mock("./meta/MetaWriter"); +jest.mock("./DomVisitor"); + +describe(TextDocumentWriter.name, () => { + let documentWriter: TextDocumentWriter; + let textDocument: TextDocument; + + beforeEach(() => { + textDocument = new TextDocument(); + + documentWriter = new TextDocumentWriter(); + }); + + describe("namespace declaration", () => { + let documentAsString: string; + + beforeEach(() => { + const document = documentWriter.write(textDocument); + documentAsString = new XMLSerializer().serializeToString(document); + }); + + it("add dc namespace", () => { + expect(documentAsString).toMatch(/xmlns:dc="http:\/\/purl.org\/dc\/elements\/1.1"/); + }); + + it("add draw namespace", () => { + expect(documentAsString).toMatch(/xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"/); + }); + + it("add fo namespace", () => { + expect(documentAsString).toMatch(/xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"/); + }); + + it("add meta namespace", () => { + expect(documentAsString).toMatch(/xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"/); + }); + + it("add style namespace", () => { + expect(documentAsString).toMatch(/xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"/); + }); + + it("add svg namespace", () => { + expect(documentAsString).toMatch(/xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"/); + }); + + it("add text namespace", () => { + expect(documentAsString).toMatch(/xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"/); + }); + + it("add xlink namespace", () => { + expect(documentAsString).toMatch(/xmlns:xlink="http:\/\/www.w3.org\/1999\/xlink"/); + }); + }); + + describe("font face declarations", () => { + it("add font declaration to document", () => { + textDocument.declareFont("Springfield", "Springfield", FontPitch.Variable); + + const document = documentWriter.write(textDocument); + const documentAsString = new XMLSerializer().serializeToString(document); + + /* tslint:disable-next-line:max-line-length */ + expect(documentAsString).toMatch(/<\/office:font-face-decls>/); + }); + + it("add font declaration to document and wrap font family if it contains spaces", () => { + textDocument.declareFont("Homer Simpson", "Homer Simpson", FontPitch.Fixed); + + const document = documentWriter.write(textDocument); + const documentAsString = new XMLSerializer().serializeToString(document); + + /* tslint:disable-next-line:max-line-length */ + expect(documentAsString).toMatch(/<\/office:font-face-decls>/); + }); + }); +}); diff --git a/src/xml/TextDocumentWriter.ts b/src/xml/TextDocumentWriter.ts new file mode 100644 index 00000000..6500b2f9 --- /dev/null +++ b/src/xml/TextDocumentWriter.ts @@ -0,0 +1,89 @@ +import { DOMImplementation } from "xmldom"; +import { TextDocument } from "../api/office/TextDocument"; +import { FontFace } from "../api/style"; +import { DomVisitor } from "./DomVisitor"; +import { MetaWriter } from "./meta/MetaWriter"; +import { OdfAttributeName } from "./OdfAttributeName"; +import { OdfElementName } from "./OdfElementName"; + +const OFFICE_VERSION = "1.2"; + +/** + * Transforms a {@link TextDocument} object into ODF conform XML + * + * @since 0.7.0 + */ +export class TextDocumentWriter { + /** + * Transforms the given {@link TextDocument} into Open Document Format. + * + * @param {TextDocument} textDocument The text document to serialize + * @returns {Document} The XML document + * @since 0.7.0 + */ + public write(textDocument: TextDocument): Document { + const document = new DOMImplementation().createDocument( + "urn:oasis:names:tc:opendocument:xmlns:office:1.0", + OdfElementName.OfficeDocument, + null); + const root = document.firstChild as Element; + + this.setXmlNamespaces(root); + + root.setAttribute(OdfAttributeName.OfficeMimetype, "application/vnd.oasis.opendocument.text"); + root.setAttribute(OdfAttributeName.OfficeVersion, OFFICE_VERSION); + + new MetaWriter().write(document, root, textDocument.getMeta()); + + this.setFontFaceElements(textDocument.getFonts(), document, root); + + new DomVisitor().visit(textDocument.getBody(), document, root); + + return document; + } + + /** + * Declares the used XML namespaces. + * + * @param {Element} root The root element of the document which will be used as parent + * @private + */ + private setXmlNamespaces(root: Element): void { + root.setAttribute("xmlns:dc", "http://purl.org/dc/elements/1.1"); + root.setAttribute("xmlns:draw", "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"); + root.setAttribute("xmlns:fo", "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"); + root.setAttribute("xmlns:meta", "urn:oasis:names:tc:opendocument:xmlns:meta:1.0"); + root.setAttribute("xmlns:style", "urn:oasis:names:tc:opendocument:xmlns:style:1.0"); + root.setAttribute("xmlns:svg", "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"); + root.setAttribute("xmlns:text", "urn:oasis:names:tc:opendocument:xmlns:text:1.0"); + root.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); + } + + /** + * Adds the `font-face-decls` element and the font faces if any font needs to be declared. + * + * @param {Document} document The XML document + * @param {Element} root The element which will be used as parent + * @private + */ + private setFontFaceElements(fonts: FontFace[], document: Document, root: Element): void { + if (fonts.length === 0) { + return; + } + + const fontFaceDeclsElement = document.createElement(OdfElementName.OfficeFontFaceDeclarations); + root.appendChild(fontFaceDeclsElement); + + fonts.forEach((font: FontFace) => { + const fontFaceElement = document.createElement(OdfElementName.StyleFontFace); + fontFaceDeclsElement.appendChild(fontFaceElement); + fontFaceElement.setAttribute("style:name", font.getName()); + + const fontFamily = font.getFontFamily(); + const encodedFontFamily = fontFamily.includes(" ") === true ? `'${fontFamily}'` : fontFamily; + fontFaceElement.setAttribute("svg:font-family", encodedFontFamily); + + fontFaceElement.setAttribute("style:font-pitch", font.getFontPitch()); + }); + } +} diff --git a/src/text/TextElementName.ts b/src/xml/TextElementName.ts similarity index 100% rename from src/text/TextElementName.ts rename to src/xml/TextElementName.ts diff --git a/src/meta/MetaElementName.ts b/src/xml/meta/MetaElementName.ts similarity index 100% rename from src/meta/MetaElementName.ts rename to src/xml/meta/MetaElementName.ts diff --git a/src/xml/meta/MetaWriter.spec.ts b/src/xml/meta/MetaWriter.spec.ts new file mode 100644 index 00000000..650003a6 --- /dev/null +++ b/src/xml/meta/MetaWriter.spec.ts @@ -0,0 +1,91 @@ +import { userInfo } from "os"; +import { DOMImplementation, XMLSerializer } from "xmldom"; +import { Meta } from "../../api/meta"; +import { OdfElementName } from "../OdfElementName"; +import { MetaWriter } from "./MetaWriter"; + +describe(MetaWriter.name, () => { + describe("#write", () => { + let metaWriter: MetaWriter; + let testDocument: Document; + let testRoot: Element; + let meta: Meta; + + beforeEach(() => { + testDocument = new DOMImplementation().createDocument("someNameSpace", OdfElementName.OfficeDocument, null); + testRoot = testDocument.firstChild as Element; + meta = new Meta(); + + metaWriter = new MetaWriter(); + }); + + it("append creator, date, creation-date, editing-cycles and generator as default properties", () => { + metaWriter.write(testDocument, testRoot, meta); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + const regex = new RegExp("" + + "simple-odf/\\d\\.\\d+\\.\\d+" + + "" + userInfo().username + "" + + "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z" + + "1" + + ""); + expect(documentAsString).toMatch(regex); + }); + + it("ignore description, language, subject, title if they are empty", () => { + meta.setCreator("") + .setDate(undefined) + .setDescription("") + .setInitialCreator("") + .setLanguage("") + .setSubject("") + .setTitle(""); + + metaWriter.write(testDocument, testRoot, meta); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + const regex = new RegExp("" + + "simple-odf/\\d\\.\\d+\\.\\d+" + + "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z" + + "1" + + ""); + expect(documentAsString).toMatch(regex); + }); + + it("append elements if they are set", () => { + meta.setCreator("Homer Simpson") + .setDate(new Date(Date.UTC(2020, 11, 24, 13, 37, 23, 42))) + .setDescription("some test description") + .setInitialCreator("Marge Simpson") + .addKeyword("some keyword") + .addKeyword("some other keyword") + .setLanguage("zu") + .setPrintDate(new Date(Date.UTC(2021, 3, 1))) + .setPrintedBy("Maggie Simpson") + .setSubject("some test subject") + .setTitle("some test title") + ; + + metaWriter.write(testDocument, testRoot, meta); + + const documentAsString = new XMLSerializer().serializeToString(testDocument); + const regex = new RegExp("" + + "simple-odf/\\d\\.\\d+\\.\\d+" + + "some test title" + + "some test description" + + "some test subject" + + "some keyword" + + "some other keyword" + + "Marge Simpson" + + "Homer Simpson" + + "Maggie Simpson" + + "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z" + + "2020-12-24T13:37:23.042Z" + + "2021-04-01T00:00:00.000Z" + + "zu" + + "1" + + ""); + expect(documentAsString).toMatch(regex); + }); + }); +}); diff --git a/src/xml/meta/MetaWriter.ts b/src/xml/meta/MetaWriter.ts new file mode 100644 index 00000000..96f00901 --- /dev/null +++ b/src/xml/meta/MetaWriter.ts @@ -0,0 +1,265 @@ +import { Meta } from "../../api/meta"; +import { OdfElementName } from "../OdfElementName"; +import { MetaElementName } from "./MetaElementName"; + +/** + * Transforms a {@link Meta} object into ODF conform XML + * + * @since 0.7.0 + */ +export class MetaWriter { + /** + * Transforms the given {@link Meta} into Open Document Format. + * + * @param {Document} document The XML document + * @param {Element} parent The parent node in the DOM + * @param {Meta} meta The Meta to serialize + * @since 0.7.0 + */ + public write(document: Document, root: Element, meta: Meta): void { + const metaElement = document.createElement(OdfElementName.OfficeMeta); + root.appendChild(metaElement); + + this.setGeneratorElement(document, metaElement, meta); + this.setTitleElement(document, metaElement, meta); + this.setDescriptionElement(document, metaElement, meta); + this.setSubjectElement(document, metaElement, meta); + this.setKeywordElements(document, metaElement, meta); + this.setInitialCreatorElement(document, metaElement, meta); + this.setCreatorElement(document, metaElement, meta); + this.setPrintedByElement(document, metaElement, meta); + this.setCreationDateElement(document, metaElement, meta); + this.setDateElement(document, metaElement, meta); + this.setPrintDateElement(document, metaElement, meta); + this.setLanguageElement(document, metaElement, meta); + this.setEditingCyclesElement(document, metaElement, meta); + } + + /** + * Sets the `meta:creation-date` element to the date and time this class was constructed. + * + * @param {Document} document The XML document + * @param {Element} metaElement The meta element which will act as parent + * @param {Meta} meta The metadata + * @private + */ + private setCreationDateElement(document: Document, metaElement: Element, meta: Meta): void { + const creationDateElement = document.createElement(MetaElementName.MetaCreationDate); + metaElement.appendChild(creationDateElement); + creationDateElement.appendChild(document.createTextNode(meta.getCreationDate().toISOString())); + } + + /** + * Sets the `dc:creator` element if creator is set. + * + * @param {Document} document The XML document + * @param {Element} metaElement The meta element which will act as parent + * @param {Meta} meta The metadata + * @private + */ + private setCreatorElement(document: Document, metaElement: Element, meta: Meta): void { + const creator = meta.getCreator(); + if (creator === undefined || creator.length === 0) { + return; + } + + const creatorElement = document.createElement(MetaElementName.DcCreator); + metaElement.appendChild(creatorElement); + creatorElement.appendChild(document.createTextNode(creator)); + } + + /** + * Sets the `dc:date` element if date is set. + * + * @param {Document} document The XML document + * @param {Element} metaElement The meta element which will act as parent + * @param {Meta} meta The metadata + * @private + */ + private setDateElement(document: Document, metaElement: Element, meta: Meta): void { + const date = meta.getDate(); + if (date === undefined) { + return; + } + + const dateElement = document.createElement(MetaElementName.DcDate); + metaElement.appendChild(dateElement); + dateElement.appendChild(document.createTextNode(date.toISOString())); + } + + /** + * Sets the `dc:description` element if description is set. + * + * @param {Document} document The XML document + * @param {Element} metaElement The meta element which will act as parent + * @param {Meta} meta The metadata + * @private + */ + private setDescriptionElement(document: Document, metaElement: Element, meta: Meta): void { + const description = meta.getDescription(); + if (description === undefined || description.length === 0) { + return; + } + + const descriptionElement = document.createElement(MetaElementName.DcDescription); + metaElement.appendChild(descriptionElement); + descriptionElement.appendChild(document.createTextNode(description)); + } + + /** + * Sets the `meta:editing-cycles` element to 1. + * + * @param {Document} document The XML document + * @param {Element} metaElement The meta element which will act as parent + * @param {Meta} meta The metadata + * @private + */ + private setEditingCyclesElement(document: Document, metaElement: Element, meta: Meta): void { + const editingCyclesElement = document.createElement(MetaElementName.MetaEditingCycles); + metaElement.appendChild(editingCyclesElement); + editingCyclesElement.appendChild(document.createTextNode(meta.getEditingCycles().toString())); + } + + /** + * Sets the `meta:generator` element to the name and version of this library (`simple-odf/x.y.z`). + * + * @param {Document} document The XML document + * @param {Element} metaElement The meta element which will act as parent + * @param {Meta} meta The metadata + * @private + */ + private setGeneratorElement(document: Document, metaElement: Element, meta: Meta): void { + const generatorElement = document.createElement(MetaElementName.MetaGenerator); + metaElement.appendChild(generatorElement); + generatorElement.appendChild(document.createTextNode(meta.getGenerator())); + } + + /** + * Sets the `meta:initial-creator` element to the current user. + * + * @param {Document} document The XML document + * @param {Element} metaElement The meta element which will act as parent + * @param {Meta} meta The metadata + * @private + */ + private setInitialCreatorElement(document: Document, metaElement: Element, meta: Meta): void { + const initialCreator = meta.getInitialCreator(); + if (initialCreator === undefined || initialCreator.length === 0) { + return; + } + + const creatorElement = document.createElement(MetaElementName.MetaInitialCreator); + metaElement.appendChild(creatorElement); + creatorElement.appendChild(document.createTextNode(initialCreator)); + } + + /** + * Sets the `meta:keyword` elements if any keyword is set. + * + * @param {Document} document The XML document + * @param {Element} metaElement The meta element which will act as parent + * @param {Meta} meta The metadata + * @private + */ + private setKeywordElements(document: Document, metaElement: Element, meta: Meta): void { + meta.getKeywords().forEach((keyword: string) => { + const subjectElement = document.createElement(MetaElementName.MetaKeyword); + metaElement.appendChild(subjectElement); + subjectElement.appendChild(document.createTextNode(keyword)); + }); + } + + /** + * Sets the `dc:language` element if language is set. + * + * @param {Document} document The XML document + * @param {Element} metaElement The meta element which will act as parent + * @param {Meta} meta The metadata + * @private + */ + private setLanguageElement(document: Document, metaElement: Element, meta: Meta): void { + const language = meta.getLanguage(); + if (language === undefined || language.length === 0) { + return; + } + + const languageElement = document.createElement(MetaElementName.DcLanguage); + metaElement.appendChild(languageElement); + languageElement.appendChild(document.createTextNode(language)); + } + + /** + * Sets the `meta:print-date` element if print date is set. + * + * @param {Document} document The XML document + * @param {Element} metaElement The meta element which will act as parent + * @param {Meta} meta The metadata + * @private + */ + private setPrintDateElement(document: Document, metaElement: Element, meta: Meta): void { + const printDate = meta.getPrintDate(); + if (printDate === undefined) { + return; + } + + const printDateElement = document.createElement(MetaElementName.MetaPrintDate); + metaElement.appendChild(printDateElement); + printDateElement.appendChild(document.createTextNode(printDate.toISOString())); + } + + /** + * Sets the `meta:printed-by` element if printing name is set. + * + * @param {Document} document The XML document + * @param {Element} metaElement The meta element which will act as parent + * @param {Meta} meta The metadata + * @private + */ + private setPrintedByElement(document: Document, metaElement: Element, meta: Meta): void { + const printedBy = meta.getPrintedBy(); + if (printedBy === undefined || printedBy.length === 0) { + return; + } + const printedByElement = document.createElement(MetaElementName.MetaPrintedBy); + metaElement.appendChild(printedByElement); + printedByElement.appendChild(document.createTextNode(printedBy)); + } + + /** + * Sets the `dc:subject` element if subject is set. + * + * @param {Document} document The XML document + * @param {Element} metaElement The meta element which will act as parent + * @param {Meta} meta The metadata + * @private + */ + private setSubjectElement(document: Document, metaElement: Element, meta: Meta): void { + const subject = meta.getSubject(); + if (subject === undefined || subject.length === 0) { + return; + } + + const subjectElement = document.createElement(MetaElementName.DcSubject); + metaElement.appendChild(subjectElement); + subjectElement.appendChild(document.createTextNode(subject)); + } + + /** + * Sets the `dc:title` element if title is set. + * + * @param {Document} document The XML document + * @param {Element} metaElement The meta element which will act as parent + * @param {Meta} meta The metadata + * @private + */ + private setTitleElement(document: Document, metaElement: Element, meta: Meta): void { + const title = meta.getTitle(); + if (title === undefined || title.length === 0) { + return; + } + + const titleElement = document.createElement(MetaElementName.DcTitle); + metaElement.appendChild(titleElement); + titleElement.appendChild(document.createTextNode(title)); + } +} diff --git a/test/integration.spec.ts b/test/integration.spec.ts index 639c4d5c..6b995a93 100644 --- a/test/integration.spec.ts +++ b/test/integration.spec.ts @@ -1,24 +1,26 @@ import { unlink } from "fs"; import { join } from "path"; import { promisify } from "util"; +import { TextBody, TextDocument } from "../src/api/office"; +import { FontPitch } from "../src/api/style"; import { AnchorType } from "../src/style/AnchorType"; 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"; import { TabStopType } from "../src/style/TabStopType"; import { TextTransformation } from "../src/style/TextTransformation"; import { Typeface } from "../src/style/Typeface"; -import { TextDocument } from "../src/TextDocument"; const FILEPATH = "./integration.fodt"; xdescribe("integration", () => { let document: TextDocument; + let body: TextBody; beforeAll(() => { document = new TextDocument(); + body = document.getBody(); }); afterAll(async (done) => { @@ -39,7 +41,7 @@ xdescribe("integration", () => { }); it("image", () => { - const paragraph = document.addParagraph(); + const paragraph = body.addParagraph(); paragraph.setStyle(new ParagraphStyle()); paragraph.getStyle().setHorizontalAlignment(HorizontalAlignment.Center); @@ -49,34 +51,34 @@ xdescribe("integration", () => { }); it("add heading", () => { - document.addHeading("First heading"); - document.addHeading("Second heading", 2); + body.addHeading("First heading"); + body.addHeading("Second heading", 2); - const para = document.addParagraph("The quick, brown fox jumps over a lazy dog."); + const para = body.addParagraph("The quick, brown fox jumps over a lazy dog."); para.addText("\nSome more text"); }); describe("paragraph formatting", () => { it("page break", () => { - const heading = document.addHeading("Paragraph Formatting", 2); + const heading = body.addHeading("Paragraph Formatting", 2); heading.setStyle(new ParagraphStyle()); heading.getStyle().setPageBreakBefore(); }); it("keep together", () => { - const heading = document.addParagraph("Paragraph Formatting"); + const heading = body.addParagraph("Paragraph Formatting"); heading.setStyle(new ParagraphStyle()); heading.getStyle().setKeepTogether(); }); it("align text", () => { - const paragraph = document.addParagraph("Some centered text"); + const paragraph = body.addParagraph("Some centered text"); paragraph.setStyle(new ParagraphStyle()); paragraph.getStyle().setHorizontalAlignment(HorizontalAlignment.Center); }); it("tab stops", () => { - const paragraph = document.addParagraph("first\tsecond\tthird"); + const paragraph = body.addParagraph("first\tsecond\tthird"); paragraph.setStyle(new ParagraphStyle()); paragraph.getStyle().addTabStop(new TabStop(4)); paragraph.getStyle().addTabStop(new TabStop(12, TabStopType.Right)); @@ -85,13 +87,13 @@ xdescribe("integration", () => { describe("text formatting", () => { beforeAll(() => { - const heading = document.addHeading("Text Formatting", 2); + const heading = body.addHeading("Text Formatting", 2); heading.setStyle(new ParagraphStyle()); heading.getStyle().setPageBreakBefore(); }); it("color", () => { - const paragraph = document.addParagraph("Some mint-colored text"); + const paragraph = body.addParagraph("Some mint-colored text"); paragraph.setStyle(new ParagraphStyle()); paragraph.getStyle().setColor(Color.fromRgb(62, 180, 137)); }); @@ -99,46 +101,46 @@ xdescribe("integration", () => { it("font name", () => { document.declareFont("Open Sans", "Open Sans", FontPitch.Variable); - const paragraph = document.addParagraph("Open Sans"); + const paragraph = body.addParagraph("Open Sans"); paragraph.setStyle(new ParagraphStyle()); paragraph.getStyle().setFontName("Open Sans"); }); it("font size", () => { - const paragraph = document.addParagraph("Some small text"); + const paragraph = body.addParagraph("Some small text"); paragraph.setStyle(new ParagraphStyle()); paragraph.getStyle().setFontSize(8); }); it("text transformation", () => { - const paragraph = document.addParagraph("Some uppercase text"); + const paragraph = body.addParagraph("Some uppercase text"); paragraph.setStyle(new ParagraphStyle()); paragraph.getStyle().setTextTransformation(TextTransformation.Uppercase); }); it("typeface", () => { - const paragraph = document.addParagraph("Some bold text"); + const paragraph = body.addParagraph("Some bold text"); paragraph.setStyle(new ParagraphStyle()); paragraph.getStyle().setTypeface(Typeface.Bold); }); }); it("hyperlink", () => { - const heading = document.addHeading("Hyperlink", 2); + const heading = body.addHeading("Hyperlink", 2); heading.setStyle(new ParagraphStyle()); heading.getStyle().setPageBreakBefore(); - const paragraph = document.addParagraph("This is just an "); + const paragraph = body.addParagraph("This is just an "); paragraph.addHyperlink("example", "http://example.org"); paragraph.addText("."); }); it("list", () => { - const heading = document.addHeading("List", 2); + const heading = body.addHeading("List", 2); heading.setStyle(new ParagraphStyle()); heading.getStyle().setPageBreakBefore(); - const list = document.addList(); + const list = body.addList(); list.addItem("first item"); list.addItem("second item"); }); diff --git a/tsconfig.json b/tsconfig.json index 87d7a22e..89d60a0e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -41,7 +41,7 @@ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ @@ -51,6 +51,11 @@ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "plugins": [ + { + "name": "typescript-tslint-plugin" + } + ] }, "include": [ "src/**/*" diff --git a/tslint.json b/tslint.json index 32fa6e5e..b6d5a329 100644 --- a/tslint.json +++ b/tslint.json @@ -1,9 +1,19 @@ { - "defaultSeverity": "error", - "extends": [ - "tslint:recommended" - ], - "jsRules": {}, - "rules": {}, - "rulesDirectory": [] + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "whitespace": { + "options": [ + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } + }, + "rulesDirectory": [] } \ No newline at end of file