diff --git a/src/document-parser.ts b/src/document-parser.ts index 50e614d..1842929 100644 --- a/src/document-parser.ts +++ b/src/document-parser.ts @@ -1,6 +1,12 @@ -import { IDomStyle, IDomDocument, DomType, IDomTable, IDomStyleValues, IDomNumbering, IDomRun, - IDomHyperlink, IDomParagraph, IDomImage, IDomElement, IDomTableColumn, IDomTableCell, - IDomRelationship, IDomSubStyle, IDomTableRow, NumberingPicBullet, DocxTab, DomRelationshipType } from './dom'; +import { + IDomStyle, DomType, IDomTable, IDomStyleValues, IDomNumbering, IDomRun, + IDomHyperlink, IDomParagraph, IDomImage, OpenXmlElement, IDomTableColumn, IDomTableCell, + IDomRelationship, IDomSubStyle, IDomTableRow, NumberingPicBullet, DocxTab, DomRelationshipType +} from './dom/dom'; +import * as utils from './utils'; +import { SectionProperties, WordDocument } from './dom/document'; +import { namespaces, Columns } from './dom/common'; +import { forEachElementNS, getAttributeLengthValue, getAttributeIntValue, getAttributeBoolValue } from './parser/common'; export var autos = { shd: "white", @@ -17,7 +23,7 @@ export class DocumentParser { ignoreHeight: boolean = true; debug: boolean = false; - parseDocumentRelationsFile(xmlString) { + parseDocumentRelationsFile(xmlString: string) { var xrels = xml.parse(xmlString, this.skipDeclaration); return xml.elements(xrels).map(c => { @@ -27,11 +33,12 @@ export class DocumentParser { }); } - parseDocumentFile(xmlString) { - var result: IDomDocument = { + parseDocumentFile(xmlString: string) { + var result: WordDocument = { domType: DomType.Document, children: [], - style: {} + style: {}, + section: null }; var xbody = xml.byTagName(xml.parse(xmlString, this.skipDeclaration), "body"); @@ -47,7 +54,7 @@ export class DocumentParser { break; case "sectPr": - this.parseSectionProperties(elem, result); + result.section = this.parseSectionProperties(elem); break; } }); @@ -189,8 +196,8 @@ export class DocumentParser { var selector = ""; switch (type) { - case "firstRow": selector = "tr.first-row"; break; - case "lastRow": selector = "tr.last-row"; break; + case "firstRow": selector = "tr.first-row td"; break; + case "lastRow": selector = "tr.last-row td"; break; case "firstCol": selector = "td.first-col"; break; case "lastCol": selector = "td.last-col"; break; case "band1Vert": selector = "td.odd-col"; break; @@ -318,28 +325,63 @@ export class DocumentParser { return result; } - parseSectionProperties(elem: Element, domElem: IDomElement) { - xml.foreach(elem, n => { - switch (n.localName) { - case "pgMar": - domElem.style["padding-left"] = xml.sizeAttr(n, "left"); - domElem.style["padding-right"] = xml.sizeAttr(n, "right"); - domElem.style["padding-top"] = xml.sizeAttr(n, "top"); - domElem.style["padding-bottom"] = xml.sizeAttr(n, "bottom"); - break; + parseSectionProperties(elem: Element): SectionProperties { + var section = {}; + forEachElementNS(elem, namespaces.wordml, e => { + switch(e.localName) { case "pgSz": - if (!this.ignoreWidth) - domElem.style["width"] = xml.sizeAttr(n, "w"); + section.pageSize = { + width: getAttributeLengthValue(e, namespaces.wordml, "w"), + height: getAttributeLengthValue(e, namespaces.wordml, "h"), + orientation: e.getAttributeNS(namespaces.wordml, "orient") + } + break; + + case "pgMar": + section.pageMargins = { + left: getAttributeLengthValue(e, namespaces.wordml, "left"), + right: getAttributeLengthValue(e, namespaces.wordml, "right"), + top: getAttributeLengthValue(e, namespaces.wordml, "top"), + bottom: getAttributeLengthValue(e, namespaces.wordml, "bottom"), + header: getAttributeLengthValue(e, namespaces.wordml, "header"), + footer: getAttributeLengthValue(e, namespaces.wordml, "footer"), + gutter: getAttributeLengthValue(e, namespaces.wordml, "gutter"), + }; + break; - if (!this.ignoreHeight) - domElem.style["height"] = xml.sizeAttr(n, "h"); + case "cols": + section.columns = this.parseColumns(e); break; } }); + + return section; } - parseParagraph(node: Element): IDomElement { + parseColumns(elem: Element): Columns { + var result = { + numberOfColumns: getAttributeIntValue(elem, namespaces.wordml, "num"), + space: getAttributeLengthValue(elem, namespaces.wordml, "space"), + separator: getAttributeBoolValue(elem, namespaces.wordml, "sep"), + equalWidth: getAttributeBoolValue(elem, namespaces.wordml, "equalWidth", true), + columns: [] + }; + + forEachElementNS(elem, namespaces.wordml, e => { + if(e.localName != "col") + return; + + result.columns.push({ + width: getAttributeLengthValue(elem, namespaces.wordml, "w"), + space: getAttributeLengthValue(elem, namespaces.wordml, "space") + }); + }); + + return result; + } + + parseParagraph(node: Element): OpenXmlElement { var result = { domType: DomType.Paragraph, children: [] }; xml.foreach(node, c => { @@ -369,7 +411,11 @@ export class DocumentParser { this.parseDefaultProperties(elem, paragraph.style = {}, null, c => { switch (c.localName) { case "pStyle": - paragraph.className = xml.className(c, "val"); + utils.addElementClass(paragraph, xml.className(c, "val")); + break; + + case "cnfStyle": + utils.addElementClass(paragraph, values.classNameOfCnfStyle(c)); break; case "numPr": @@ -417,7 +463,7 @@ export class DocumentParser { paragraph.style["float"] = "left"; } - parseBookmark(node: Element): IDomElement { + parseBookmark(node: Element): OpenXmlElement { var result: IDomRun = { domType: DomType.Run }; result.id = xml.stringAttr(node, "name"); @@ -425,7 +471,7 @@ export class DocumentParser { return result; } - parseHyperlink(node: Element, parent?: IDomElement): IDomRun { + parseHyperlink(node: Element, parent?: OpenXmlElement): IDomRun { var result: IDomHyperlink = { domType: DomType.Hyperlink, parent: parent, children: [] }; var anchor = xml.stringAttr(node, "anchor"); @@ -443,7 +489,7 @@ export class DocumentParser { return result; } - parseRun(node: Element, parent?: IDomElement): IDomRun { + parseRun(node: Element, parent?: OpenXmlElement): IDomRun { var result: IDomRun = { domType: DomType.Run, parent: parent }; xml.foreach(node, c => { @@ -499,7 +545,7 @@ export class DocumentParser { }); } - parseDrawing(node: Element): IDomElement { + parseDrawing(node: Element): OpenXmlElement { for (var n of xml.elements(node)) { switch (n.localName) { case "inline": @@ -509,8 +555,8 @@ export class DocumentParser { } } - parseDrawingWrapper(node: Element): IDomElement { - var result = { domType: DomType.Drawing, children: [], style: {} }; + parseDrawingWrapper(node: Element): OpenXmlElement { + var result = { domType: DomType.Drawing, children: [], style: {} }; var isAnchor = node.localName == "anchor"; //TODO @@ -582,7 +628,7 @@ export class DocumentParser { return result; } - parseGraphic(elem: Element): IDomElement { + parseGraphic(elem: Element): OpenXmlElement { var graphicData = xml.byTagName(elem, "graphicData"); for (let n of xml.elements(graphicData)) { @@ -670,6 +716,10 @@ export class DocumentParser { table.className = xml.className(c, "val"); break; + case "tblLook": + utils.addElementClass(table, values.classNameOftblLook(c)); + break; + case "tblpPr": this.parseTablePosition(c, table); break; @@ -747,7 +797,7 @@ export class DocumentParser { }); } - parseTableCell(node: Element): IDomElement { + parseTableCell(node: Element): OpenXmlElement { var result: IDomTableCell = { domType: DomType.Cell, children: [] }; xml.foreach(node, c => { @@ -1313,18 +1363,14 @@ class values { } static classNameOftblLook(c: Element) { - let val = xml.stringAttr(c, "val"); - let num = parseInt(val, 16); let className = ""; - //FirstRow, LastRow, FirstColumn, LastColumn, Band1Vertical, Band2Vertical, Band1Horizontal, Band2Horizontal, NE Cell, NW Cell, SE Cell, SW Cell. - - if (values.checkMask(num, 0x0020)) className += " first-row"; - if (values.checkMask(num, 0x0040)) className += " last-row"; - if (values.checkMask(num, 0x0080)) className += " first-col"; - if (values.checkMask(num, 0x0100)) className += " last-col"; - if (!values.checkMask(num, 0x0200)) className += " odd-row even-row"; - if (!values.checkMask(num, 0x0400)) className += " odd-col even-col"; + if (xml.boolAttr(c, "firstColumn")) className += " first-col"; + if (xml.boolAttr(c, "firstRow")) className += " first-row"; + if (xml.boolAttr(c, "lastColumn")) className += " lat-col"; + if (xml.boolAttr(c, "lastRow")) className += " last-row"; + if (xml.boolAttr(c, "noHBand")) className += " no-hband"; + if (xml.boolAttr(c, "noVBand")) className += " no-vband"; return className.trim(); } diff --git a/src/document.ts b/src/document.ts index 51ddfef..2d4261f 100644 --- a/src/document.ts +++ b/src/document.ts @@ -1,5 +1,7 @@ import { DocumentParser } from './document-parser'; -import { IDomRelationship, IDomStyle, IDomFont, IDomNumbering, IDomDocument } from './dom'; +import { IDomRelationship, IDomStyle, IDomNumbering } from './dom/dom'; +import { Font } from './dom/common'; +import { WordDocument } from './dom/document'; enum PartType { Document = "word/document.xml", @@ -18,9 +20,9 @@ export class Document { numRelations: IDomRelationship[] = null; styles: IDomStyle[] = null; - fonts: IDomFont[] = null; + fonts: Font[] = null; numbering: IDomNumbering[] = null; - document: IDomDocument = null; + document: WordDocument = null; static load(blob, parser: DocumentParser): PromiseLike { var d = new Document(); diff --git a/src/docx-preview.ts b/src/docx-preview.ts index 9358082..5ea97e6 100644 --- a/src/docx-preview.ts +++ b/src/docx-preview.ts @@ -14,9 +14,18 @@ export function renderAsync(data: Blob | any, bodyContainer: HTMLElement, styleC var parser = new DocumentParser(); var renderer = new HtmlRenderer(window.document); + options = { + ignoreHeight: true, + ignoreWidth: false, + debug: false, + className: "docx", + inWrapper: true, + ... options + }; + if (options) { - parser.ignoreWidth = options.ignoreWidth || parser.ignoreWidth; - parser.ignoreHeight = options.ignoreHeight || parser.ignoreHeight; + options.ignoreWidth = options.ignoreWidth || parser.ignoreWidth; + options.ignoreHeight = options.ignoreHeight || parser.ignoreHeight; parser.debug = options.debug || parser.debug; renderer.className = options.className || "docx"; @@ -25,7 +34,7 @@ export function renderAsync(data: Blob | any, bodyContainer: HTMLElement, styleC return Document.load(data, parser) .then(doc => { - renderer.render(doc, bodyContainer, styleContainer); + renderer.render(doc, bodyContainer, styleContainer, options); return doc; }); } \ No newline at end of file diff --git a/src/dom/common.ts b/src/dom/common.ts new file mode 100644 index 0000000..ecd030a --- /dev/null +++ b/src/dom/common.ts @@ -0,0 +1,26 @@ +export const namespaces = { + wordml: "http://schemas.openxmlformats.org/wordprocessingml/2006/main" +} + +export interface Length { + value: number; + type: "px" | "pt" | "%" +} + +export interface Font { + name: string; + family: string; +} + +export interface Column { + space: Length; + width: Length; +} + +export interface Columns { + space: Length; + numberOfColumns: number; + separator: boolean; + equalWidth: boolean; + columns: Column[]; +} \ No newline at end of file diff --git a/src/dom/document.ts b/src/dom/document.ts new file mode 100644 index 0000000..0bbd17d --- /dev/null +++ b/src/dom/document.ts @@ -0,0 +1,28 @@ +import { Length, Columns } from "./common"; +import { OpenXmlElement } from "./dom"; + +export interface PageSize { + width: Length, + height: Length, + orientation: "landscape" | string +} + +export interface PageMargins { + top: Length; + right: Length; + bottom: Length; + left: Length; + header: Length; + footer: Length; + gutter: Length; +} + +export interface SectionProperties { + pageSize: PageSize, + pageMargins: PageMargins, + columns: Columns; +} + +export interface WordDocument extends OpenXmlElement { + section: SectionProperties; +} \ No newline at end of file diff --git a/src/dom.ts b/src/dom/dom.ts similarity index 73% rename from src/dom.ts rename to src/dom/dom.ts index 805ae32..0104be5 100644 --- a/src/dom.ts +++ b/src/dom/dom.ts @@ -1,3 +1,5 @@ +import { SectionProperties } from "./document"; + export enum DomType { Document, Paragraph, @@ -28,15 +30,15 @@ export interface IDomRelationship { target: string; } -export interface IDomElement { +export interface OpenXmlElement { domType: DomType; - children?: IDomElement[]; + children?: OpenXmlElement[]; style?: IDomStyleValues; className?: string; - parent?: IDomElement; + parent?: OpenXmlElement; } -export interface IDomParagraph extends IDomElement { +export interface IDomParagraph extends OpenXmlElement { numberingId?: string; numberingLevel?: number; tabs: DocxTab[]; @@ -48,11 +50,11 @@ export interface DocxTab { position: string; } -export interface IDomHyperlink extends IDomElement { +export interface IDomHyperlink extends OpenXmlElement { href?: string; } -export interface IDomRun extends IDomElement { +export interface IDomRun extends OpenXmlElement { id?: string; break?: string; wrapper?: string; @@ -61,22 +63,19 @@ export interface IDomRun extends IDomElement { tab?: boolean; } -export interface IDomTable extends IDomElement { +export interface IDomTable extends OpenXmlElement { columns?: IDomTableColumn[]; cellStyle?: IDomStyleValues; } -export interface IDomTableRow extends IDomElement { +export interface IDomTableRow extends OpenXmlElement { } -export interface IDomTableCell extends IDomElement { +export interface IDomTableCell extends OpenXmlElement { span?: number; } -export interface IDomDocument extends IDomElement { -} - -export interface IDomImage extends IDomElement { +export interface IDomImage extends OpenXmlElement { src: string; } @@ -116,8 +115,3 @@ export interface NumberingPicBullet { export interface IDomStyleValues { [name: string]: string; } - -export interface IDomFont { - name: string; - family: string; -} diff --git a/src/html-renderer.ts b/src/html-renderer.ts index 197c493..313d386 100644 --- a/src/html-renderer.ts +++ b/src/html-renderer.ts @@ -1,20 +1,25 @@ import { Document } from './document'; -import { IDomStyle, IDomDocument, DomType, IDomTable, IDomStyleValues, IDomNumbering, IDomRun, - IDomHyperlink, IDomParagraph, IDomImage, IDomElement, IDomTableColumn, IDomTableCell } from './dom'; +import { IDomStyle, DomType, IDomTable, IDomStyleValues, IDomNumbering, IDomRun, + IDomHyperlink, IDomParagraph, IDomImage, OpenXmlElement, IDomTableColumn, IDomTableCell } from './dom/dom'; +import { Length } from './dom/common'; +import { Options } from './docx-preview'; +import { WordDocument } from './dom/document'; export class HtmlRenderer { inWrapper: boolean = true; className: string = "docx"; document: Document; + options: Partial; private digitTest = /^[0-9]/.test; constructor(public htmlDocument: HTMLDocument) { } - render(document: Document, bodyContainer: HTMLElement, styleContainer: HTMLElement = null) { + render(document: Document, bodyContainer: HTMLElement, styleContainer: HTMLElement = null, options: Partial) { this.document = document; + this.options = options; styleContainer = styleContainer || bodyContainer; @@ -80,7 +85,7 @@ export class HtmlRenderer { } } - processElement(element: IDomDocument) { + processElement(element: OpenXmlElement) { if (element.children) { for (var e of element.children) { e.className = this.processClassName(e.className); @@ -123,7 +128,7 @@ export class HtmlRenderer { return output; } - renderDocument(document: IDomDocument): HTMLElement { + renderDocument(document: WordDocument): HTMLElement { var bodyElement = this.htmlDocument.createElement("section"); bodyElement.className = this.className; @@ -133,9 +138,40 @@ export class HtmlRenderer { this.renderStyleValues(document.style, bodyElement); + if(document.section) { + var props = document.section; + + if(props.pageMargins) { + bodyElement.style.paddingLeft = this.renderLength(props.pageMargins.left); + bodyElement.style.paddingRight = this.renderLength(props.pageMargins.right); + bodyElement.style.paddingTop = this.renderLength(props.pageMargins.top); + bodyElement.style.paddingBottom = this.renderLength(props.pageMargins.bottom); + } + + if(props.pageSize) { + if(!this.options.ignoreWidth) + bodyElement.style.width = this.renderLength(props.pageSize.width); + if(!this.options.ignoreHeight) + bodyElement.style.height = this.renderLength(props.pageSize.height); + } + + if(props.columns && props.columns.numberOfColumns) { + bodyElement.style.columnCount = props.columns.numberOfColumns; + bodyElement.style.columnGap = this.renderLength(props.columns.space); + + if(props.columns.separator) { + bodyElement.style.columnRule = "1px solid black"; + } + } + } + return bodyElement; } + renderLength(l: Length): string { + return !l ? null : `${l.value}${l.type}`; + } + renderWrapper() { var wrapper = document.createElement("div"); @@ -256,7 +292,7 @@ export class HtmlRenderer { return this.renderStyle(styleText); } - renderElement(elem: IDomElement, parent: IDomElement): HTMLElement { + renderElement(elem: OpenXmlElement, parent: OpenXmlElement): HTMLElement { switch (elem.domType) { case DomType.Paragraph: return this.renderParagraph(elem); @@ -286,7 +322,7 @@ export class HtmlRenderer { return null; } - renderChildren(elem: IDomElement, into?: HTMLElement): HTMLElement[] { + renderChildren(elem: OpenXmlElement, into?: HTMLElement): HTMLElement[] { var result: HTMLElement[] = null; if (elem.children != null) @@ -441,7 +477,7 @@ export class HtmlRenderer { return result; } - renderTableRow(elem: IDomElement) { + renderTableRow(elem: OpenXmlElement) { let result = this.htmlDocument.createElement("tr"); this.renderClass(elem, result); @@ -474,7 +510,7 @@ export class HtmlRenderer { } } - renderClass(input: IDomElement, ouput: HTMLElement) { + renderClass(input: OpenXmlElement, ouput: HTMLElement) { if (input.className) ouput.className = input.className; } diff --git a/src/parser/common.ts b/src/parser/common.ts new file mode 100644 index 0000000..62f8520 --- /dev/null +++ b/src/parser/common.ts @@ -0,0 +1,51 @@ +import { Length } from "../dom/common"; + +export function forEachElementNS(elem: Element, namespaceURI: string, callback: (elem: Element) => any) { + elem.childNodes.forEach(n => { + if(n.nodeType == 1 && n.namespaceURI == namespaceURI) + callback(n); + }); +} + +export function getAttributeIntValue(elem: Element, namespaceURI: string, name: string): number { + var val = elem.getAttributeNS(namespaceURI, name); + return val ? parseInt(val) : null; +} + +export function getAttributeBoolValue(elem: Element, namespaceURI: string, name: string, defaultValue: boolean = false): boolean { + var val = elem.getAttributeNS(namespaceURI, name); + + if(val == null) + return defaultValue; + + return val === "true" || val === "1"; +} + +export function getAttributeLengthValue(elem: Element, namespaceURI: string, name: string, usage: LengthUsage = LengthUsage.Dxa): Length { + return parseLength(elem.getAttributeNS(namespaceURI, name), usage); +} + +export enum LengthUsage { + Dxa, + Emu, + FontSize, + Border, + Percent +} + +export function parseLength(val: string | null, usage: LengthUsage = LengthUsage.Dxa): Length { + if (!val) + return null; + + var num = parseInt(val); + + switch (usage) { + case LengthUsage.Dxa: return { value: 0.05 * num, type: "pt" }; + case LengthUsage.Emu: return { value: num / 12700, type: "pt" }; + case LengthUsage.FontSize: return { value: 0.5 * num, type: "pt" }; + case LengthUsage.Border: return { value: 0.125 * num, type: "pt" }; + case LengthUsage.Percent: return { value: 0.02 * num, type: "%" }; + } + + return null; +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..2ec6ecf --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,9 @@ +import { OpenXmlElement } from "./dom/dom"; + +export function addElementClass(element: OpenXmlElement, className: string): string { + return element.className = appendClass(element.className, className); +} + +export function appendClass(classList: string, className: string): string { + return (!classList) ? className : `${classList} ${className}` +} \ No newline at end of file