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