UNPKG

node-webodf-ilkkah

Version:

WebODF - JavaScript Document Engine http://webodf.org/

1,264 lines (1,202 loc) 57.5 kB
/** * Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com> * * @licstart * This file is part of WebODF. * * WebODF is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License (GNU AGPL) * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * WebODF is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with WebODF. If not, see <http://www.gnu.org/licenses/>. * @licend * * @source: http://www.webodf.org/ * @source: https://github.com/kogmbh/WebODF/ */ /*global Node, NodeFilter, runtime, core, xmldom, odf, DOMParser, document, webodf */ (function () { "use strict"; var styleInfo = new odf.StyleInfo(), domUtils = core.DomUtils, /**@const @type{!string}*/ officens = "urn:oasis:names:tc:opendocument:xmlns:office:1.0", /**@const @type{!string}*/ manifestns = "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0", /**@const @type{!string}*/ webodfns = "urn:webodf:names:scope", /**@const @type{!string}*/ stylens = odf.Namespaces.stylens, /**@const @type{!Array.<!string>}*/ nodeorder = ['meta', 'settings', 'scripts', 'font-face-decls', 'styles', 'automatic-styles', 'master-styles', 'body'], /**@const @type{!string}*/ automaticStylePrefix = Date.now() + "_webodf_", base64 = new core.Base64(), /**@const @type{!string}*/ documentStylesScope = "document-styles", /**@const @type{!string}*/ documentContentScope = "document-content"; /** * Return the position the node should get according to the ODF flat format. * @param {!Node} child * @return {!number} */ function getNodePosition(child) { var i, l = nodeorder.length; for (i = 0; i < l; i += 1) { if (child.namespaceURI === officens && child.localName === nodeorder[i]) { return i; } } return -1; } /** * Class that filters runtime specific nodes from the DOM. * Additionally all unused automatic styles are skipped, if a tree * of elements was passed to check the style usage in it. * @constructor * @implements {xmldom.LSSerializerFilter} * @param {!Element} styleUsingElementsRoot root element of tree of elements using styles * @param {?Element=} automaticStyles root element of the automatic style definition tree */ function OdfStylesFilter(styleUsingElementsRoot, automaticStyles) { var usedStyleList = new styleInfo.UsedStyleList(styleUsingElementsRoot, automaticStyles), odfNodeFilter = new odf.OdfNodeFilter(); /** * @param {!Node} node * @return {!number} */ this.acceptNode = function (node) { var result = odfNodeFilter.acceptNode(node); if (result === NodeFilter.FILTER_ACCEPT && node.parentNode === automaticStyles && node.nodeType === Node.ELEMENT_NODE) { // skip all automatic styles which are not used if (usedStyleList.uses(/**@type{!Element}*/(node))) { result = NodeFilter.FILTER_ACCEPT; } else { result = NodeFilter.FILTER_REJECT; } } return result; }; } /** * Class that extends OdfStylesFilter * Additionally, filter out ' ' within the <text:s> element and '\t' within the <text:tab> element * @constructor * @implements {xmldom.LSSerializerFilter} * @param {!Element} styleUsingElementsRoot root element of tree of elements using styles * @param {?Element=} automaticStyles root element of the automatic style definition tree */ function OdfContentFilter(styleUsingElementsRoot, automaticStyles) { var odfStylesFilter = new OdfStylesFilter(styleUsingElementsRoot, automaticStyles); /** * @param {!Node} node * @return {!number} */ this.acceptNode = function (node) { var result = odfStylesFilter.acceptNode(node); if (result === NodeFilter.FILTER_ACCEPT && node.parentNode && node.parentNode.namespaceURI === odf.Namespaces.textns && (node.parentNode.localName === 's' || node.parentNode.localName === 'tab')) { result = NodeFilter.FILTER_REJECT; } return result; }; } /** * Put the element at the right position in the parent. * The right order is given by the value returned from getNodePosition. * @param {!Node} node * @param {?Node} child * @return {undefined} */ function setChild(node, child) { if (!child) { return; } var childpos = getNodePosition(child), pos, c = node.firstChild; if (childpos === -1) { return; } while (c) { pos = getNodePosition(c); if (pos !== -1 && pos > childpos) { break; } c = c.nextSibling; } node.insertBefore(child, c); } /*jslint emptyblock: true*/ /** * A DOM element that is part of and ODF part of a DOM. * @constructor * @extends {Element} */ odf.ODFElement = function ODFElement() { }; /** * The root element of an ODF document. * @constructor * @extends {odf.ODFElement} */ odf.ODFDocumentElement = function ODFDocumentElement() { }; /*jslint emptyblock: false*/ odf.ODFDocumentElement.prototype = new odf.ODFElement(); odf.ODFDocumentElement.prototype.constructor = odf.ODFDocumentElement; /** * Optional tag <office:automatic-styles/> * If it is missing, it is created. * @type {!Element} */ odf.ODFDocumentElement.prototype.automaticStyles; /** * Required tag <office:body/> * @type {!Element} */ odf.ODFDocumentElement.prototype.body; /** * Optional tag <office:font-face-decls/> * @type {Element} */ odf.ODFDocumentElement.prototype.fontFaceDecls = null; /** * @type {Element} */ odf.ODFDocumentElement.prototype.manifest = null; /** * Optional tag <office:master-styles/> * If it is missing, it is created. * @type {!Element} */ odf.ODFDocumentElement.prototype.masterStyles; /** * Optional tag <office:meta/> * @type {?Element} */ odf.ODFDocumentElement.prototype.meta; /** * Optional tag <office:settings/> * @type {Element} */ odf.ODFDocumentElement.prototype.settings = null; /** * Optional tag <office:styles/> * If it is missing, it is created. * @type {!Element} */ odf.ODFDocumentElement.prototype.styles; odf.ODFDocumentElement.namespaceURI = officens; odf.ODFDocumentElement.localName = 'document'; /*jslint emptyblock: true*/ /** * An element that also has a pointer to the optional annotation end * @constructor * @extends {odf.ODFElement} */ odf.AnnotationElement = function AnnotationElement() { }; /*jslint emptyblock: false*/ /** * @type {?Element} */ odf.AnnotationElement.prototype.annotationEndElement; // private constructor /** * @constructor * @param {string} name * @param {string} mimetype * @param {!odf.OdfContainer} container * @param {core.Zip} zip */ odf.OdfPart = function OdfPart(name, mimetype, container, zip) { var self = this; // declare public variables this.size = 0; this.type = null; this.name = name; this.container = container; /**@type{?string}*/ this.url = null; /**@type{string}*/ this.mimetype = mimetype; this.document = null; this.onstatereadychange = null; /**@type{?function(!odf.OdfPart)}*/ this.onchange; this.EMPTY = 0; this.LOADING = 1; this.DONE = 2; this.state = this.EMPTY; this.data = ""; // private functions // public functions /** * @return {undefined} */ this.load = function () { if (zip === null) { return; } this.mimetype = mimetype; zip.loadAsDataURL(name, mimetype, function (err, url) { if (err) { runtime.log(err); } self.url = url; if (self.onchange) { self.onchange(self); } if (self.onstatereadychange) { self.onstatereadychange(self); } }); }; }; /*jslint emptyblock: true*/ odf.OdfPart.prototype.load = function () { }; /*jslint emptyblock: false*/ odf.OdfPart.prototype.getUrl = function () { if (this.data) { return 'data:;base64,' + base64.toBase64(this.data); } return null; }; /** * The OdfContainer class manages the various parts that constitues an ODF * document. * The constructor takes a url or a type. If urlOrType is a type, an empty * document of that type is created. Otherwise, urlOrType is interpreted as * a url and loaded from that url. * * @constructor * @param {!string|!odf.OdfContainer.DocumentType} urlOrType * @param {?function(!odf.OdfContainer)=} onstatereadychange * @return {?} */ odf.OdfContainer = function OdfContainer(urlOrType, onstatereadychange) { var self = this, /**@type {!core.Zip}*/ zip, /**@type {!Object.<!string,!string>}*/ partMimetypes = {}, /**@type {?Element}*/ contentElement, /**@type{!string}*/ url = ""; // NOTE each instance of OdfContainer has a copy of the private functions // it would be better to have a class OdfContainerPrivate where the // private functions can be defined via OdfContainerPrivate.prototype // without exposing them // declare public variables this.onstatereadychange = onstatereadychange; this.onchange = null; this.state = null; /** * @type {!odf.ODFDocumentElement} */ this.rootElement; /** * @param {!Element} element * @return {undefined} */ function removeProcessingInstructions(element) { var n = element.firstChild, next, e; while (n) { next = n.nextSibling; if (n.nodeType === Node.ELEMENT_NODE) { e = /**@type{!Element}*/(n); removeProcessingInstructions(e); } else if (n.nodeType === Node.PROCESSING_INSTRUCTION_NODE) { element.removeChild(n); } n = next; } } // private functions /** * Iterates through the subtree of rootElement and adds annotation-end * elements as direct properties of the corresponding annotation elements. * Expects properly used annotation elements, does not try * to do heuristic fixes or drop broken elements. * @param {!Element} rootElement * @return {undefined} */ function linkAnnotationStartAndEndElements(rootElement) { var document = rootElement.ownerDocument, /** @type {!Object.<!string,!Element>} */ annotationStarts = {}, n, name, annotationStart, // TODO: optimize by using a filter rejecting subtrees without annotations possible nodeIterator = document.createNodeIterator(rootElement, NodeFilter.SHOW_ELEMENT, null, false); n = /**@type{?Element}*/(nodeIterator.nextNode()); while (n) { if (n.namespaceURI === officens) { if (n.localName === "annotation") { name = n.getAttributeNS(officens, 'name'); if (name) { if (annotationStarts.hasOwnProperty(name)) { runtime.log("Warning: annotation name used more than once with <office:annotation/>: '" + name + "'"); } else { annotationStarts[name] = n; } } } else if (n.localName === "annotation-end") { name = n.getAttributeNS(officens, 'name'); if (name) { if (annotationStarts.hasOwnProperty(name)) { annotationStart = /** @type {!odf.AnnotationElement}*/(annotationStarts[name]); if (!annotationStart.annotationEndElement) { // Linking annotation start & end annotationStart.annotationEndElement = n; } else { runtime.log("Warning: annotation name used more than once with <office:annotation-end/>: '" + name + "'"); } } else { runtime.log("Warning: annotation end without an annotation start, name: '" + name + "'"); } } else { runtime.log("Warning: annotation end without a name found"); } } } n = /**@type{?Element}*/(nodeIterator.nextNode()); } } /** * Tags all styles with an attribute noting their scope. * Helper function for the primitive complete backwriting of * the automatic styles. * @param {?Element} stylesRootElement * @param {!string} scope * @return {undefined} */ function setAutomaticStylesScope(stylesRootElement, scope) { var n = stylesRootElement && stylesRootElement.firstChild; while (n) { if (n.nodeType === Node.ELEMENT_NODE) { /**@type{!Element}*/(n).setAttributeNS(webodfns, "scope", scope); } n = n.nextSibling; } } /** * Returns the meta element. If it did not exist before, it will be created. * @return {!Element} */ function getEnsuredMetaElement() { var root = self.rootElement, meta = root.meta; if (!meta) { root.meta = meta = document.createElementNS(officens, "meta"); setChild(root, meta); } return meta; } /** * @param {!string} metadataNs * @param {!string} metadataLocalName * @return {?string} */ function getMetadata(metadataNs, metadataLocalName) { var node = self.rootElement.meta, textNode; node = node && node.firstChild; while (node && (node.namespaceURI !== metadataNs || node.localName !== metadataLocalName)) { node = node.nextSibling; } node = node && node.firstChild; while (node && node.nodeType !== Node.TEXT_NODE) { node = node.nextSibling; } if (node) { textNode = /**@type{!Text}*/(node); return textNode.data; } return null; } this.getMetadata = getMetadata; /** * Returns key with a number postfix or none, as key unused both in map1 and map2. * @param {!string} key * @param {!Object} map1 * @param {!Object} map2 * @return {!string} */ function unusedKey(key, map1, map2) { var i = 0, postFixedKey; // cut any current postfix number key = key.replace(/\d+$/, ''); // start with no postfix, continue with i = 1, aiming for the simpelst unused number or key postFixedKey = key; while (map1.hasOwnProperty(postFixedKey) || map2.hasOwnProperty(postFixedKey)) { i += 1; postFixedKey = key + i; } return postFixedKey; } /** * Returns a map with the fontface declaration elements, with font-face name as key. * @param {!Element} fontFaceDecls * @return {!Object.<!string,!Element>} */ function mapByFontFaceName(fontFaceDecls) { var fn, result = {}, fontname; // create map of current target decls fn = fontFaceDecls.firstChild; while (fn) { if (fn.nodeType === Node.ELEMENT_NODE && fn.namespaceURI === stylens && fn.localName === "font-face") { fontname = /**@type{!Element}*/(fn).getAttributeNS(stylens, "name"); // assuming existance and uniqueness of style:name here result[fontname] = fn; } fn = fn.nextSibling; } return result; } /** * Merges all style:font-face elements from the source into the target. * Skips elements equal to one already in the target. * Elements with the same style:name but different properties get a new * value for style:name. Any name changes are logged and returned as a map * with the old names as keys. * @param {!Element} targetFontFaceDeclsRootElement * @param {!Element} sourceFontFaceDeclsRootElement * @return {!Object.<!string,!string>} mapping of old font-face name to new */ function mergeFontFaceDecls(targetFontFaceDeclsRootElement, sourceFontFaceDeclsRootElement) { var e, s, fontFaceName, newFontFaceName, targetFontFaceDeclsMap, sourceFontFaceDeclsMap, fontFaceNameChangeMap = {}; targetFontFaceDeclsMap = mapByFontFaceName(targetFontFaceDeclsRootElement); sourceFontFaceDeclsMap = mapByFontFaceName(sourceFontFaceDeclsRootElement); // merge source decls into target e = sourceFontFaceDeclsRootElement.firstElementChild; while (e) { s = e.nextElementSibling; if (e.namespaceURI === stylens && e.localName === "font-face") { fontFaceName = e.getAttributeNS(stylens, "name"); // already such a name used in target? if (targetFontFaceDeclsMap.hasOwnProperty(fontFaceName)) { // skip it if the declarations are equal, otherwise insert with a new, unused name if (!e.isEqualNode(targetFontFaceDeclsMap[fontFaceName])) { newFontFaceName = unusedKey(fontFaceName, targetFontFaceDeclsMap, sourceFontFaceDeclsMap); e.setAttributeNS(stylens, "style:name", newFontFaceName); // copy with a new name targetFontFaceDeclsRootElement.appendChild(e); targetFontFaceDeclsMap[newFontFaceName] = e; delete sourceFontFaceDeclsMap[fontFaceName]; // note name change fontFaceNameChangeMap[fontFaceName] = newFontFaceName; } } else { // move over // perhaps one day it could also be checked if there is an equal declaration // with a different name, but that has yet to be seen in real life targetFontFaceDeclsRootElement.appendChild(e); targetFontFaceDeclsMap[fontFaceName] = e; delete sourceFontFaceDeclsMap[fontFaceName]; } } e = s; } return fontFaceNameChangeMap; } /** * Creates a clone of the styles tree containing only styles tagged * with the given scope, or with no specified scope. * Helper function for the primitive complete backwriting of * the automatic styles. * @param {?Element} stylesRootElement * @param {!string} scope * @return {?Element} */ function cloneStylesInScope(stylesRootElement, scope) { var copy = null, e, s, scopeAttrValue; if (stylesRootElement) { copy = stylesRootElement.cloneNode(true); e = copy.firstElementChild; while (e) { s = e.nextElementSibling; scopeAttrValue = e.getAttributeNS(webodfns, "scope"); if (scopeAttrValue && scopeAttrValue !== scope) { copy.removeChild(e); } e = s; } } return copy; } /** * Creates a clone of the font face declaration tree containing only * those declarations which are referenced in the passed styles. * @param {?Element} fontFaceDeclsRootElement * @param {!Array.<!Element>} stylesRootElementList * @return {?Element} */ function cloneFontFaceDeclsUsedInStyles(fontFaceDeclsRootElement, stylesRootElementList) { var e, nextSibling, fontFaceName, copy = null, usedFontFaceDeclMap = {}; if (fontFaceDeclsRootElement) { // first collect used font faces stylesRootElementList.forEach(function (stylesRootElement) { styleInfo.collectUsedFontFaces(usedFontFaceDeclMap, stylesRootElement); }); // then clone all font face declarations and drop those which are not in the list of used copy = fontFaceDeclsRootElement.cloneNode(true); e = copy.firstElementChild; while (e) { nextSibling = e.nextElementSibling; fontFaceName = e.getAttributeNS(stylens, "name"); if (!usedFontFaceDeclMap[fontFaceName]) { copy.removeChild(e); } e = nextSibling; } } return copy; } /** * Import the document elementnode into the DOM of OdfContainer. * Any processing instructions are removed, since importing them * gives an exception. * @param {Document|undefined} xmldoc * @return {!Element|undefined} */ function importRootNode(xmldoc) { var doc = self.rootElement.ownerDocument, node; // remove all processing instructions // TODO: replace cursor processing instruction with an element if (xmldoc) { removeProcessingInstructions(xmldoc.documentElement); try { node = /**@type{!Element}*/(doc.importNode(xmldoc.documentElement, true)); } catch (ignore) { } } return node; } /** * @param {!number} state * @return {undefined} */ function setState(state) { self.state = state; if (self.onchange) { self.onchange(self); } if (self.onstatereadychange) { self.onstatereadychange(self); } } /** * @param {!Element} root * @return {undefined} */ function setRootElement(root) { contentElement = null; self.rootElement = /**@type{!odf.ODFDocumentElement}*/(root); root.fontFaceDecls = domUtils.getDirectChild(root, officens, 'font-face-decls'); root.styles = domUtils.getDirectChild(root, officens, 'styles'); root.automaticStyles = domUtils.getDirectChild(root, officens, 'automatic-styles'); root.masterStyles = domUtils.getDirectChild(root, officens, 'master-styles'); root.body = domUtils.getDirectChild(root, officens, 'body'); root.meta = domUtils.getDirectChild(root, officens, 'meta'); root.settings = domUtils.getDirectChild(root, officens, 'settings'); root.scripts = domUtils.getDirectChild(root, officens, 'scripts'); linkAnnotationStartAndEndElements(root); } /** * @param {Document|undefined} xmldoc * @return {undefined} */ function handleFlatXml(xmldoc) { var root = importRootNode(xmldoc); if (!root || root.localName !== 'document' || root.namespaceURI !== officens) { setState(OdfContainer.INVALID); return; } setRootElement(/**@type{!Element}*/(root)); setState(OdfContainer.DONE); } /** * @param {Document} xmldoc * @return {undefined} */ function handleStylesXml(xmldoc) { var node = importRootNode(xmldoc), root = self.rootElement, n; if (!node || node.localName !== 'document-styles' || node.namespaceURI !== officens) { setState(OdfContainer.INVALID); return; } root.fontFaceDecls = domUtils.getDirectChild(node, officens, 'font-face-decls'); setChild(root, root.fontFaceDecls); n = domUtils.getDirectChild(node, officens, 'styles'); root.styles = n || xmldoc.createElementNS(officens, 'styles'); setChild(root, root.styles); n = domUtils.getDirectChild(node, officens, 'automatic-styles'); root.automaticStyles = n || xmldoc.createElementNS(officens, 'automatic-styles'); setAutomaticStylesScope(root.automaticStyles, documentStylesScope); setChild(root, root.automaticStyles); node = domUtils.getDirectChild(node, officens, 'master-styles'); root.masterStyles = node || xmldoc.createElementNS(officens, 'master-styles'); setChild(root, root.masterStyles); // automatic styles from styles.xml could shadow automatic styles // from content.xml, because they could have the same name // so prefix them and their uses with some almost unique string styleInfo.prefixStyleNames(root.automaticStyles, automaticStylePrefix, root.masterStyles); } /** * @param {Document} xmldoc * @return {undefined} */ function handleContentXml(xmldoc) { var node = importRootNode(xmldoc), root, automaticStyles, fontFaceDecls, fontFaceNameChangeMap, c; if (!node || node.localName !== 'document-content' || node.namespaceURI !== officens) { setState(OdfContainer.INVALID); return; } root = self.rootElement; fontFaceDecls = domUtils.getDirectChild(node, officens, 'font-face-decls'); if (root.fontFaceDecls && fontFaceDecls) { fontFaceNameChangeMap = mergeFontFaceDecls(root.fontFaceDecls, fontFaceDecls); } else if (fontFaceDecls) { root.fontFaceDecls = fontFaceDecls; setChild(root, fontFaceDecls); } automaticStyles = domUtils.getDirectChild(node, officens, 'automatic-styles'); setAutomaticStylesScope(automaticStyles, documentContentScope); if (fontFaceNameChangeMap) { styleInfo.changeFontFaceNames(automaticStyles, fontFaceNameChangeMap); } if (root.automaticStyles && automaticStyles) { c = automaticStyles.firstChild; while (c) { root.automaticStyles.appendChild(c); c = automaticStyles.firstChild; // works because node c moved } } else if (automaticStyles) { root.automaticStyles = automaticStyles; setChild(root, automaticStyles); } node = domUtils.getDirectChild(node, officens, 'body'); if (node === null) { throw "<office:body/> tag is mising."; } root.body = node; setChild(root, root.body); } /** * @param {Document} xmldoc * @return {undefined} */ function handleMetaXml(xmldoc) { var node = importRootNode(xmldoc), root; if (!node || node.localName !== 'document-meta' || node.namespaceURI !== officens) { return; } root = self.rootElement; root.meta = domUtils.getDirectChild(node, officens, 'meta'); setChild(root, root.meta); } /** * @param {Document} xmldoc * @return {undefined} */ function handleSettingsXml(xmldoc) { var node = importRootNode(xmldoc), root; if (!node || node.localName !== 'document-settings' || node.namespaceURI !== officens) { return; } root = self.rootElement; root.settings = domUtils.getDirectChild(node, officens, 'settings'); setChild(root, root.settings); } /** * @param {Document} xmldoc * @return {undefined} */ function handleManifestXml(xmldoc) { var node = importRootNode(xmldoc), root, e; if (!node || node.localName !== 'manifest' || node.namespaceURI !== manifestns) { return; } root = self.rootElement; root.manifest = /**@type{!Element}*/(node); e = root.manifest.firstElementChild; while (e) { if (e.localName === "file-entry" && e.namespaceURI === manifestns) { partMimetypes[e.getAttributeNS(manifestns, "full-path")] = e.getAttributeNS(manifestns, "media-type"); } e = e.nextElementSibling; } } /** * @param {!Document} xmldoc * @param {!string} localName * @param {!Object.<!string,!boolean>} allowedNamespaces * @return {undefined} */ function removeElements(xmldoc, localName, allowedNamespaces) { var elements = domUtils.getElementsByTagName(xmldoc, localName), element, i; for (i = 0; i < elements.length; i += 1) { element = elements[i]; if (!allowedNamespaces.hasOwnProperty(element.namespaceURI)) { element.parentNode.removeChild(element); } } } /** * Remove any HTML <script/> tags from the DOM. * The tags need to be removed, because otherwise they would be executed * when the dom is inserted into the document. * To be safe, all elements with localName "script" are removed, unless * they are in a known, allowed namespace. * @param {!Document} xmldoc * @return {undefined} */ function removeDangerousElements(xmldoc) { removeElements(xmldoc, "script", { "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0": true, "urn:oasis:names:tc:opendocument:xmlns:office:1.0": true, "urn:oasis:names:tc:opendocument:xmlns:table:1.0": true, "urn:oasis:names:tc:opendocument:xmlns:text:1.0": true, "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0": true }); removeElements(xmldoc, "style", { "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0": true, "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0": true, "urn:oasis:names:tc:opendocument:xmlns:style:1.0": true }); } /** * Remove all attributes that have no namespace and that have * localname like 'on....', the event handler attributes. * @param {!Element} element * @return {undefined} */ function removeDangerousAttributes(element) { var e = element.firstElementChild, as = [], i, n, a, atts = element.attributes, l = atts.length; // collect all dangerous attributes for (i = 0; i < l; i += 1) { a = atts.item(i); n = a.localName.substr(0, 2).toLowerCase(); if (a.namespaceURI === null && n === "on") { as.push(a); } } // remove the dangerous attributes l = as.length; for (i = 0; i < l; i += 1) { element.removeAttributeNode(as[i]); } // recurse into the child elements while (e) { removeDangerousAttributes(e); e = e.nextElementSibling; } } /** * @param {!Array.<!{path:string,handler:function(?Document)}>} remainingComponents * @return {undefined} */ function loadNextComponent(remainingComponents) { var component = remainingComponents.shift(); if (component) { zip.loadAsDOM(component.path, function (err, xmldoc) { if (xmldoc) { removeDangerousElements(xmldoc); removeDangerousAttributes(xmldoc.documentElement); } component.handler(xmldoc); if (self.state === OdfContainer.INVALID) { if (err) { runtime.log("ERROR: Unable to load " + component.path + " - " + err); } else { runtime.log("ERROR: Unable to load " + component.path); } return; } if (err) { runtime.log("DEBUG: Unable to load " + component.path + " - " + err); } loadNextComponent(remainingComponents); }); } else { linkAnnotationStartAndEndElements(self.rootElement); setState(OdfContainer.DONE); } } /** * @return {undefined} */ function loadComponents() { var componentOrder = [ {path: 'styles.xml', handler: handleStylesXml}, {path: 'content.xml', handler: handleContentXml}, {path: 'meta.xml', handler: handleMetaXml}, {path: 'settings.xml', handler: handleSettingsXml}, {path: 'META-INF/manifest.xml', handler: handleManifestXml} ]; loadNextComponent(componentOrder); } /** * @param {!string} name * @return {!string} */ function createDocumentElement(name) { var /**@type{string}*/ s = ""; /** * @param {string} prefix * @param {string} ns */ function defineNamespace(prefix, ns) { s += " xmlns:" + prefix + "=\"" + ns + "\""; } odf.Namespaces.forEachPrefix(defineNamespace); return "<?xml version=\"1.0\" encoding=\"UTF-8\"?><office:" + name + " " + s + " office:version=\"1.2\">"; } /** * @return {!string} */ function serializeMetaXml() { var serializer = new xmldom.LSSerializer(), /**@type{!string}*/ s = createDocumentElement("document-meta"); serializer.filter = new odf.OdfNodeFilter(); s += serializer.writeToString(self.rootElement.meta, odf.Namespaces.namespaceMap); s += "</office:document-meta>"; return s; } /** * Creates a manifest:file-entry node * @param {!string} fullPath Full-path attribute value for the file-entry * @param {!string} mediaType Media-type attribute value for the file-entry * @return {!Node} */ function createManifestEntry(fullPath, mediaType) { var element = document.createElementNS(manifestns, 'manifest:file-entry'); element.setAttributeNS(manifestns, 'manifest:full-path', fullPath); element.setAttributeNS(manifestns, 'manifest:media-type', mediaType); return element; } /** * @return {string} */ function serializeManifestXml() { var header = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n', xml = '<manifest:manifest xmlns:manifest="' + manifestns + '" manifest:version="1.2"></manifest:manifest>', manifest = /**@type{!Document}*/(runtime.parseXML(xml)), manifestRoot = manifest.documentElement, serializer = new xmldom.LSSerializer(), /**@type{string}*/ fullPath; for (fullPath in partMimetypes) { if (partMimetypes.hasOwnProperty(fullPath)) { manifestRoot.appendChild(createManifestEntry(fullPath, partMimetypes[fullPath])); } } serializer.filter = new odf.OdfNodeFilter(); return header + serializer.writeToString(manifest, odf.Namespaces.namespaceMap); } /** * @return {!string} */ function serializeSettingsXml() { var serializer = new xmldom.LSSerializer(), /**@type{!string}*/ s = createDocumentElement("document-settings"); // <office:settings/> is optional, but if present must have at least one child element if (self.rootElement.settings && self.rootElement.settings.firstElementChild) { serializer.filter = new odf.OdfNodeFilter(); s += serializer.writeToString(self.rootElement.settings, odf.Namespaces.namespaceMap); } return s + "</office:document-settings>"; } /** * @return {!string} */ function serializeStylesXml() { var fontFaceDecls, automaticStyles, masterStyles, nsmap = odf.Namespaces.namespaceMap, serializer = new xmldom.LSSerializer(), /**@type{!string}*/ s = createDocumentElement("document-styles"); // special handling for merged toplevel nodes automaticStyles = cloneStylesInScope( self.rootElement.automaticStyles, documentStylesScope ); masterStyles = /**@type{!Element}*/(self.rootElement.masterStyles.cloneNode(true)); fontFaceDecls = cloneFontFaceDeclsUsedInStyles(self.rootElement.fontFaceDecls, [masterStyles, self.rootElement.styles, automaticStyles]); // automatic styles from styles.xml could shadow automatic styles from content.xml, // because they could have the same name // thus they were prefixed on loading with some almost unique string, which cam be removed // again before saving styleInfo.removePrefixFromStyleNames(automaticStyles, automaticStylePrefix, masterStyles); serializer.filter = new OdfStylesFilter(masterStyles, automaticStyles); s += serializer.writeToString(fontFaceDecls, nsmap); s += serializer.writeToString(self.rootElement.styles, nsmap); s += serializer.writeToString(automaticStyles, nsmap); s += serializer.writeToString(masterStyles, nsmap); s += "</office:document-styles>"; return s; } /** * @return {!string} */ function serializeContentXml() { var fontFaceDecls, automaticStyles, nsmap = odf.Namespaces.namespaceMap, serializer = new xmldom.LSSerializer(), /**@type{!string}*/ s = createDocumentElement("document-content"); // special handling for merged toplevel nodes automaticStyles = cloneStylesInScope(self.rootElement.automaticStyles, documentContentScope); fontFaceDecls = cloneFontFaceDeclsUsedInStyles(self.rootElement.fontFaceDecls, [automaticStyles]); serializer.filter = new OdfContentFilter(self.rootElement.body, automaticStyles); s += serializer.writeToString(fontFaceDecls, nsmap); s += serializer.writeToString(automaticStyles, nsmap); s += serializer.writeToString(self.rootElement.body, nsmap); s += "</office:document-content>"; return s; } /** * @param {!{Type:function(new:Object),namespaceURI:string,localName:string}} type * @return {!Element} */ function createElement(type) { var original = document.createElementNS( type.namespaceURI, type.localName ), /**@type{string}*/ method, iface = new type.Type(); for (method in iface) { if (iface.hasOwnProperty(method)) { original[method] = iface[method]; } } return original; } /** * @param {!string} url * @param {!function((string)):undefined} callback * @return {undefined} */ function loadFromXML(url, callback) { /** * @param {?string} err * @param {?Document} dom */ function handler(err, dom) { if (err) { callback(err); } else if (!dom) { callback("No DOM was loaded."); } else { removeDangerousElements(dom); removeDangerousAttributes(dom.documentElement); handleFlatXml(dom); } } runtime.loadXML(url, handler); } // public functions this.setRootElement = setRootElement; /** * @return {!Element} */ this.getContentElement = function () { var /**@type{!Element}*/ body; if (!contentElement) { body = self.rootElement.body; contentElement = domUtils.getDirectChild(body, officens, "text") || domUtils.getDirectChild(body, officens, "presentation") || domUtils.getDirectChild(body, officens, "spreadsheet"); } if (!contentElement) { throw "Could not find content element in <office:body/>."; } return contentElement; }; /** * Gets the document type as 'text', 'presentation', or 'spreadsheet'. * @return {!string} */ this.getDocumentType = function () { var content = self.getContentElement(); return content && content.localName; }; /** * Returns whether the document is a template. * @return {!boolean} */ this.isTemplate = function () { var docMimetype = partMimetypes["/"]; return (docMimetype.substr(-9) === "-template"); }; /** * Sets whether the document is a template or not. * @param {!boolean} isTemplate * @return {undefined} */ this.setIsTemplate = function (isTemplate) { var docMimetype = partMimetypes["/"], oldIsTemplate = (docMimetype.substr(-9) === "-template"), data; if (isTemplate === oldIsTemplate) { return; } if (isTemplate) { docMimetype = docMimetype + "-template"; } else { docMimetype = docMimetype.substr(0, docMimetype.length-9); } partMimetypes["/"] = docMimetype; data = runtime.byteArrayFromString(docMimetype, "utf8"); zip.save("mimetype", data, false, new Date()); }; /** * Open file and parse it. Return the XML Node. Return the root node of * the file or null if this is not possible. * For 'content.xml', 'styles.xml', 'meta.xml', and 'settings.xml', the * elements 'document-content', 'document-styles', 'document-meta', or * 'document-settings' will be returned respectively. * @param {string} partname * @return {!odf.OdfPart} **/ this.getPart = function (partname) { return new odf.OdfPart(partname, partMimetypes[partname], self, zip); }; /** * @param {string} url * @param {function(?string, ?Uint8Array)} callback receiving err and data * @return {undefined} */ this.getPartData = function (url, callback) { zip.load(url, callback); }; /** * Sets the metadata fields from the given properties map. * @param {?Object.<!string, !string>} setProperties A flat object that is a string->string map of field name -> value. * @param {?Array.<!string>} removedPropertyNames An array of metadata field names (prefixed). * @return {undefined} */ function setMetadata(setProperties, removedPropertyNames) { var metaElement = getEnsuredMetaElement(); if (setProperties) { domUtils.mapKeyValObjOntoNode(metaElement, setProperties, odf.Namespaces.lookupNamespaceURI); } if (removedPropertyNames) { domUtils.removeKeyElementsFromNode(metaElement, removedPropertyNames, odf.Namespaces.lookupNamespaceURI); } } this.setMetadata = setMetadata; /** * Increment the number of times the document has been edited. * @return {!number} new number of editing cycles */ this.incrementEditingCycles = function () { var currentValueString = getMetadata(odf.Namespaces.metans, "editing-cycles"), currentCycles = currentValueString ? parseInt(currentValueString, 10) : 0; if (isNaN(currentCycles)) { currentCycles = 0; } setMetadata({"meta:editing-cycles": currentCycles + 1}, null); return currentCycles + 1; }; /** * Write pre-saving metadata to the DOM * @return {undefined} */ function updateMetadataForSaving() { // set the opendocument provider used to create/ // last modify the document. // this string should match the definition for // user-agents in the http protocol as specified // in section 14.43 of [RFC2616]. var generatorString, window = runtime.getWindow(); generatorString = "WebODF/" + webodf.Version; if (window) { generatorString = generatorString + " " + window.navigator.userAgent; } setMetadata({"meta:generator": generatorString}, null); } /** * @param {!string} type * @param {!boolean=} isTemplate Default value is false. * @return {!core.Zip} */ function createEmptyDocument(type, isTemplate) { var emptyzip = new core.Zip("", null), mimetype = "application/vnd.oasis.opendocument." + type + (isTemplate === true ? "-template" : ""), data = runtime.byteArrayFromString( mimetype, "utf8" ), root = self.rootElement, content = document.createElementNS(officens, type); emptyzip.save("mimetype", data, false, new Date()); /** * @param {!string} memberName variant of the real local name which allows dot notation * @param {!string=} realLocalName * @return {undefined} */