diff --git a/Font.ts b/Font.ts new file mode 100644 index 00000000..d7a4caa5 --- /dev/null +++ b/Font.ts @@ -0,0 +1,30 @@ +export enum FontName { + ARIAL = 'Arial', +} + +export enum FontStyle { + NORMAL = 'normal', + BOLD = 'bold', + ITALIC = 'italic', +} + +export enum Color { + BLACK = '#000000', + GRAY = '#808080', + MC_BLUE = '#0098da', +} + +export class Font { + // TODO add toXML() + public name: string; + public style: FontStyle; + public size: number; + public color: string; + + public constructor(name: string, style: FontStyle, size: number, color: string) { + this.name = name; + this.style = style; + this.size = size; + this.color = color; + } +} diff --git a/Links.md b/Links.md new file mode 100644 index 00000000..71f0a278 --- /dev/null +++ b/Links.md @@ -0,0 +1,7 @@ +https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=office +http://docs.oasis-open.org/office/v1.2/OpenDocument-v1.2-part1.html + +http://incubator.apache.org/odftoolkit/simple/index.html +http://incubator.apache.org/odftoolkit/simple/gettingstartguide.html + +http://incubator.apache.org/odftoolkit/simple/demo/demo10.html \ No newline at end of file diff --git a/OdfElement.ts b/OdfElement.ts new file mode 100644 index 00000000..84cae93d --- /dev/null +++ b/OdfElement.ts @@ -0,0 +1,35 @@ +/** + * Base element in Open Document Format + */ +export class OdfElement { + private children: Array; + + /** + * Constructor. + */ + public constructor() { + this.children = []; + } + + /** + * Appends a child element to this element. + * + * @param {OdfElement} element The element to append + */ + public appendElement(element: OdfElement): void { + this.children.push(element); + } + + /** + * 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 + */ + protected toXML(document: Document, parent: Element): void { + this.children.forEach((child: OdfElement) => { + child.toXML(document, parent); + }); + } +} diff --git a/OdfElementName.ts b/OdfElementName.ts new file mode 100644 index 00000000..78e6ce33 --- /dev/null +++ b/OdfElementName.ts @@ -0,0 +1,8 @@ +export enum OdfElementName { + OFFICE_AUTOMATIC_STYLES = 'office:automatic-styles', + OFFICE_BODY = 'office:body', + OFFICE_TEXT = 'office:text', + STYLE_STYLE = 'style:style', + STYLE_TEXT_PROPERTIES = 'style:text-properties', + STYLE_PARAGRAPH_PROPERTIES = 'style:paragraph-properties', +} diff --git a/Paragraph.ts b/Paragraph.ts new file mode 100644 index 00000000..625307fc --- /dev/null +++ b/Paragraph.ts @@ -0,0 +1,186 @@ +import { createHash } from 'crypto'; +import { Font } from './Font'; +import { OdfElement } from './OdfElement'; +import { OdfElementName } from './OdfElementName'; + +interface Style { + // TODO add toXML() + // TODO add getStyleName() + font: Font | undefined; + shouldBreakPage: boolean; +} + +/** + * This class represents an empty ODF text document. + */ +export class Paragraph extends OdfElement { + private text: string; + private headingLevel: number; + private style: Style; + + public constructor(text?: string) { + super(); + + this.text = text; + this.headingLevel = 0; + this.style = { + font: undefined, + shouldBreakPage: false, + }; + } + + public appendTextContent(text: string): void { + if (this.text === undefined) { + this.text = text; + return; + } + + this.text += text; + } + + /** + * Returns whether the paragraph is heading. + * + * @returns {boolean} TRUE if the paragraph is a heading, FALSE otherwise + */ + public isHeading(): boolean { + return this.headingLevel > 0; + } + + /** + * + * @returns {number} + */ + public getHeadingLevel(): number { + return this.headingLevel; + } + + public applyHeading(isHeading = true, level = 1): void { + if (isHeading === false) { + this.headingLevel = 0; + return; + } + + this.headingLevel = level > 0 ? level : 0; + } + + public setFont(font: Font): void { + this.style.font = font; + } + + public addPageBreak(): void { + this.style.shouldBreakPage = true; + } + + /** @inheritDoc */ + protected toXML(document: Document, parent: Element): void { + let paragraph: Element; + if (this.isHeading() === true) { + paragraph = document.createElement('text:h'); + paragraph.setAttribute('text:outline-level', this.getHeadingLevel().toString(10)); + } else { + paragraph = document.createElement('text:p'); + } + + this.appendStyle(document, paragraph); + this.appendText(document, paragraph); + + parent.appendChild(paragraph); + + super.toXML(document, paragraph); + } + + private appendStyle(document: Document, paragraph: Element): void { + if (this.style.font === undefined && this.style.shouldBreakPage === false) { + return; + } + + const styleName = Paragraph.getStyleName(this.style); + + const rootNode = document.firstChild; + rootNode.setAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'); + rootNode.setAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'); + const automaticStyles = rootNode.getElementsByTagName(OdfElementName.OFFICE_AUTOMATIC_STYLES)[0]; + + for (let i = 0; i < automaticStyles.childNodes.length; i++) { + const style = automaticStyles.childNodes[i]; + const name = style.attributes.getNamedItem('style:name'); + if (name.name === styleName) { + console.log('found style with name', styleName); + return; + } + } + + // TODO declare fonts + // TODO http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#property-style_font-name + // + // + // + // + // + // + + const style = document.createElement(OdfElementName.STYLE_STYLE); + style.setAttribute('style:family', 'paragraph'); + style.setAttribute('style:name', styleName); + automaticStyles.appendChild(style); + + if (this.style.shouldBreakPage === true) { + const paragraphProperties = document.createElement(OdfElementName.STYLE_PARAGRAPH_PROPERTIES); + paragraphProperties.setAttribute('fo:break-before', 'page'); + style.appendChild(paragraphProperties); + } + + if (this.style.font !== undefined) { + const textProperties = document.createElement(OdfElementName.STYLE_TEXT_PROPERTIES); + textProperties.setAttribute('style:font-name', this.style.font.name); + textProperties.setAttribute('fo:font-weight', this.style.font.style.toString()); + textProperties.setAttribute('fo:font-size', `${this.style.font.size} pt`); + textProperties.setAttribute('fo:color', this.style.font.color); + style.appendChild(textProperties); + } + + paragraph.setAttribute('text:style-name', styleName); + } + + /** + * Appends the text of the paragraph. + * Newlines will be replaced with line breaks. + * + * @param {Document} document The XML document + * @param {Element} paragraph The paragraph the text belongs to + */ + private appendText(document: Document, paragraph: Element): void { + if (this.text === undefined) { + return; + } + + (document.firstChild).setAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'); + + const lines = this.text.split('\n'); + + for (let i = 0; i < lines.length; i++) { + if (i > 0) { + const lineBreak = document.createElement('text:line-break'); + paragraph.appendChild(lineBreak); + } + + const textNode = document.createTextNode(lines[i]); + paragraph.appendChild(textNode); + } + } + + private static getStyleName(style: Style): string { + const hash = createHash('md5'); + + if (style.font !== undefined) { + hash.update(style.font.name); + hash.update(style.font.style); + hash.update(style.font.size.toString()); + hash.update(style.font.color); + } + + hash.update(style.shouldBreakPage ? '1' : '0'); + return hash.digest('hex'); + } +} diff --git a/TextDocument.ts b/TextDocument.ts new file mode 100644 index 00000000..831ab8e9 --- /dev/null +++ b/TextDocument.ts @@ -0,0 +1,81 @@ +import { writeFile } from 'fs'; +import { DOMImplementation, XMLSerializer } from 'xmldom'; + +import { OdfElement } from './OdfElement'; +import { Paragraph } from './Paragraph'; +import { OdfElementName } from './OdfElementName'; + +const XML_DECLARATION = '', + NEWLINE = '\n'; + +/** + * This class represents an empty ODF text document. + */ +export class TextDocument extends OdfElement { + // load file from File, InputStream, OdfPackageDocument, String + // public static load(filePath: string): Promise { + // return Promise.resolve(undefined); + // } + + public constructor() { + super(); + } + + /** + * Saves the document in flat open document xml format. + * + * @param {string} filePath The file path to write to + * @returns {Promise} + */ + public save(filePath: string): Promise { + const document = new DOMImplementation().createDocument('urn:oasis:names:tc:opendocument:xmlns:office:1.0', 'office:document', undefined); + const root = document.firstChild; + + this.toXML(document, root); + + const xml = new XMLSerializer().serializeToString(document); + + return new Promise((resolve, reject) => { + writeFile(filePath, XML_DECLARATION + NEWLINE + xml, (error) => { + if (error !== null) { + console.error(`Failed to save document to ${filePath}`, error); + reject(error); + return; + } + + resolve(); + }); + }); + } + + /** + * Appends a paragraph to the document. + * If a text is given, this will be set as content of the paragraph. + * + * @param {string} [text] The optional text of the paragraph + * @returns {Paragraph} The newly added paragraph + */ + public addParagraph(text?: string): Paragraph { + const paragraph = new Paragraph(text); + this.appendElement(paragraph); + + return paragraph; + } + + /** @inheritDoc */ + protected toXML(document: Document, root: Element): void { + root.setAttribute('office:mimetype', 'application/vnd.oasis.opendocument.text'); + root.setAttribute('office:version', '1.2'); + + const automaticStyles = document.createElement(OdfElementName.OFFICE_AUTOMATIC_STYLES); + root.appendChild(automaticStyles); + + const body = document.createElement(OdfElementName.OFFICE_BODY); + root.appendChild(body); + + const text = document.createElement(OdfElementName.OFFICE_TEXT); + body.appendChild(text); + + super.toXML(document, text); + } +}