1 /** 2 * Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com> 3 * 4 * @licstart 5 * This file is part of WebODF. 6 * 7 * WebODF is free software: you can redistribute it and/or modify it 8 * under the terms of the GNU Affero General Public License (GNU AGPL) 9 * as published by the Free Software Foundation, either version 3 of 10 * the License, or (at your option) any later version. 11 * 12 * WebODF is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU Affero General Public License for more details. 16 * 17 * You should have received a copy of the GNU Affero General Public License 18 * along with WebODF. If not, see <http://www.gnu.org/licenses/>. 19 * @licend 20 * 21 * @source: http://www.webodf.org/ 22 * @source: https://github.com/kogmbh/WebODF/ 23 */ 24 25 /*global Node, NodeFilter, runtime, core, xmldom, odf, DOMParser, document, webodf */ 26 27 (function () { 28 "use strict"; 29 var styleInfo = new odf.StyleInfo(), 30 domUtils = new core.DomUtils(), 31 /**@const 32 @type{!string}*/ 33 officens = "urn:oasis:names:tc:opendocument:xmlns:office:1.0", 34 /**@const 35 @type{!string}*/ 36 manifestns = "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0", 37 /**@const 38 @type{!string}*/ 39 webodfns = "urn:webodf:names:scope", 40 /**@const 41 @type{!string}*/ 42 stylens = odf.Namespaces.stylens, 43 /**@const 44 @type{!Array.<!string>}*/ 45 nodeorder = ['meta', 'settings', 'scripts', 'font-face-decls', 'styles', 46 'automatic-styles', 'master-styles', 'body'], 47 /**@const 48 @type{!string}*/ 49 automaticStylePrefix = Date.now() + "_webodf_", 50 base64 = new core.Base64(), 51 /**@const 52 @type{!string}*/ 53 documentStylesScope = "document-styles", 54 /**@const 55 @type{!string}*/ 56 documentContentScope = "document-content"; 57 58 /** 59 * Return the position the node should get according to the ODF flat format. 60 * @param {!Node} child 61 * @return {!number} 62 */ 63 function getNodePosition(child) { 64 var i, l = nodeorder.length; 65 for (i = 0; i < l; i += 1) { 66 if (child.namespaceURI === officens && 67 child.localName === nodeorder[i]) { 68 return i; 69 } 70 } 71 return -1; 72 } 73 /** 74 * Class that filters runtime specific nodes from the DOM. 75 * Additionally all unused automatic styles are skipped, if a tree 76 * of elements was passed to check the style usage in it. 77 * @constructor 78 * @implements {xmldom.LSSerializerFilter} 79 * @param {!Element} styleUsingElementsRoot root element of tree of elements using styles 80 * @param {?Element=} automaticStyles root element of the automatic style definition tree 81 */ 82 function OdfStylesFilter(styleUsingElementsRoot, automaticStyles) { 83 var usedStyleList = new styleInfo.UsedStyleList(styleUsingElementsRoot, automaticStyles), 84 odfNodeFilter = new odf.OdfNodeFilter(); 85 86 /** 87 * @param {!Node} node 88 * @return {!number} 89 */ 90 this.acceptNode = function (node) { 91 var result = odfNodeFilter.acceptNode(node); 92 if (result === NodeFilter.FILTER_ACCEPT 93 && node.parentNode === automaticStyles 94 && node.nodeType === Node.ELEMENT_NODE) { 95 // skip all automatic styles which are not used 96 if (usedStyleList.uses(/**@type{!Element}*/(node))) { 97 result = NodeFilter.FILTER_ACCEPT; 98 } else { 99 result = NodeFilter.FILTER_REJECT; 100 } 101 } 102 return result; 103 }; 104 } 105 /** 106 * Class that extends OdfStylesFilter 107 * Additionally, filter out ' ' within the <text:s> element and '\t' within the <text:tab> element 108 * @constructor 109 * @implements {xmldom.LSSerializerFilter} 110 * @param {!Element} styleUsingElementsRoot root element of tree of elements using styles 111 * @param {?Element=} automaticStyles root element of the automatic style definition tree 112 */ 113 function OdfContentFilter(styleUsingElementsRoot, automaticStyles) { 114 var odfStylesFilter = new OdfStylesFilter(styleUsingElementsRoot, automaticStyles); 115 116 /** 117 * @param {!Node} node 118 * @return {!number} 119 */ 120 this.acceptNode = function (node) { 121 var result = odfStylesFilter.acceptNode(node); 122 if (result === NodeFilter.FILTER_ACCEPT 123 && node.parentNode 124 && node.parentNode.namespaceURI === odf.Namespaces.textns 125 && (node.parentNode.localName === 's' || node.parentNode.localName === 'tab')) { 126 result = NodeFilter.FILTER_REJECT; 127 } 128 return result; 129 }; 130 } 131 /** 132 * Put the element at the right position in the parent. 133 * The right order is given by the value returned from getNodePosition. 134 * @param {!Node} node 135 * @param {?Node} child 136 * @return {undefined} 137 */ 138 function setChild(node, child) { 139 if (!child) { 140 return; 141 } 142 var childpos = getNodePosition(child), 143 pos, 144 c = node.firstChild; 145 if (childpos === -1) { 146 return; 147 } 148 while (c) { 149 pos = getNodePosition(c); 150 if (pos !== -1 && pos > childpos) { 151 break; 152 } 153 c = c.nextSibling; 154 } 155 node.insertBefore(child, c); 156 } 157 /*jslint emptyblock: true*/ 158 /** 159 * A DOM element that is part of and ODF part of a DOM. 160 * @constructor 161 * @extends {Element} 162 */ 163 odf.ODFElement = function ODFElement() { 164 }; 165 /** 166 * The root element of an ODF document. 167 * @constructor 168 * @extends {odf.ODFElement} 169 */ 170 odf.ODFDocumentElement = function ODFDocumentElement() { 171 }; 172 /*jslint emptyblock: false*/ 173 odf.ODFDocumentElement.prototype = new odf.ODFElement(); 174 odf.ODFDocumentElement.prototype.constructor = odf.ODFDocumentElement; 175 /** 176 * Optional tag <office:automatic-styles/> 177 * If it is missing, it is created. 178 * @type {!Element} 179 */ 180 odf.ODFDocumentElement.prototype.automaticStyles; 181 /** 182 * Required tag <office:body/> 183 * @type {!Element} 184 */ 185 odf.ODFDocumentElement.prototype.body; 186 /** 187 * Optional tag <office:font-face-decls/> 188 * @type {Element} 189 */ 190 odf.ODFDocumentElement.prototype.fontFaceDecls = null; 191 /** 192 * @type {Element} 193 */ 194 odf.ODFDocumentElement.prototype.manifest = null; 195 /** 196 * Optional tag <office:master-styles/> 197 * If it is missing, it is created. 198 * @type {!Element} 199 */ 200 odf.ODFDocumentElement.prototype.masterStyles; 201 /** 202 * Optional tag <office:meta/> 203 * @type {?Element} 204 */ 205 odf.ODFDocumentElement.prototype.meta; 206 /** 207 * Optional tag <office:settings/> 208 * @type {Element} 209 */ 210 odf.ODFDocumentElement.prototype.settings = null; 211 /** 212 * Optional tag <office:styles/> 213 * If it is missing, it is created. 214 * @type {!Element} 215 */ 216 odf.ODFDocumentElement.prototype.styles; 217 odf.ODFDocumentElement.namespaceURI = officens; 218 odf.ODFDocumentElement.localName = 'document'; 219 220 /*jslint emptyblock: true*/ 221 /** 222 * An element that also has a pointer to the optional annotation end 223 * @constructor 224 * @extends {odf.ODFElement} 225 */ 226 odf.AnnotationElement = function AnnotationElement() { 227 }; 228 /*jslint emptyblock: false*/ 229 230 /** 231 * @type {?Element} 232 */ 233 odf.AnnotationElement.prototype.annotationEndElement; 234 235 // private constructor 236 /** 237 * @constructor 238 * @param {string} name 239 * @param {string} mimetype 240 * @param {!odf.OdfContainer} container 241 * @param {core.Zip} zip 242 */ 243 odf.OdfPart = function OdfPart(name, mimetype, container, zip) { 244 var self = this; 245 246 // declare public variables 247 this.size = 0; 248 this.type = null; 249 this.name = name; 250 this.container = container; 251 /**@type{?string}*/ 252 this.url = null; 253 /**@type{string}*/ 254 this.mimetype = mimetype; 255 this.document = null; 256 this.onstatereadychange = null; 257 /**@type{?function(!odf.OdfPart)}*/ 258 this.onchange; 259 this.EMPTY = 0; 260 this.LOADING = 1; 261 this.DONE = 2; 262 this.state = this.EMPTY; 263 this.data = ""; 264 265 // private functions 266 // public functions 267 /** 268 * @return {undefined} 269 */ 270 this.load = function () { 271 if (zip === null) { 272 return; 273 } 274 this.mimetype = mimetype; 275 zip.loadAsDataURL(name, mimetype, function (err, url) { 276 if (err) { 277 runtime.log(err); 278 } 279 self.url = url; 280 if (self.onchange) { 281 self.onchange(self); 282 } 283 if (self.onstatereadychange) { 284 self.onstatereadychange(self); 285 } 286 }); 287 }; 288 }; 289 /*jslint emptyblock: true*/ 290 odf.OdfPart.prototype.load = function () { 291 }; 292 /*jslint emptyblock: false*/ 293 odf.OdfPart.prototype.getUrl = function () { 294 if (this.data) { 295 return 'data:;base64,' + base64.toBase64(this.data); 296 } 297 return null; 298 }; 299 /** 300 * The OdfContainer class manages the various parts that constitues an ODF 301 * document. 302 * The constructor takes a url or a type. If urlOrType is a type, an empty 303 * document of that type is created. Otherwise, urlOrType is interpreted as 304 * a url and loaded from that url. 305 * 306 * @constructor 307 * @param {!string|!odf.OdfContainer.DocumentType} urlOrType 308 * @param {?function(!odf.OdfContainer)=} onstatereadychange 309 * @return {?} 310 */ 311 odf.OdfContainer = function OdfContainer(urlOrType, onstatereadychange) { 312 var self = this, 313 /**@type {!core.Zip}*/ 314 zip, 315 partMimetypes = {}, 316 /**@type {?Element}*/ 317 contentElement, 318 /**@type{!string}*/ 319 url = ""; 320 321 // NOTE each instance of OdfContainer has a copy of the private functions 322 // it would be better to have a class OdfContainerPrivate where the 323 // private functions can be defined via OdfContainerPrivate.prototype 324 // without exposing them 325 326 // declare public variables 327 this.onstatereadychange = onstatereadychange; 328 this.onchange = null; 329 this.state = null; 330 /** 331 * @type {!odf.ODFDocumentElement} 332 */ 333 this.rootElement; 334 335 /** 336 * @param {!Element} element 337 * @return {undefined} 338 */ 339 function removeProcessingInstructions(element) { 340 var n = element.firstChild, next, e; 341 while (n) { 342 next = n.nextSibling; 343 if (n.nodeType === Node.ELEMENT_NODE) { 344 e = /**@type{!Element}*/(n); 345 removeProcessingInstructions(e); 346 } else if (n.nodeType === Node.PROCESSING_INSTRUCTION_NODE) { 347 element.removeChild(n); 348 } 349 n = next; 350 } 351 } 352 353 // private functions 354 /** 355 * Iterates through the subtree of rootElement and adds annotation-end 356 * elements as direct properties of the corresponding annotation elements. 357 * Expects properly used annotation elements, does not try 358 * to do heuristic fixes or drop broken elements. 359 * @param {!Element} rootElement 360 * @return {undefined} 361 */ 362 function linkAnnotationStartAndEndElements(rootElement) { 363 var document = rootElement.ownerDocument, 364 /** @type {!Object.<!string,!Element>} */ 365 annotationStarts = {}, 366 n, name, annotationStart, 367 // TODO: optimize by using a filter rejecting subtrees without annotations possible 368 nodeIterator = document.createNodeIterator(rootElement, NodeFilter.SHOW_ELEMENT, null, false); 369 370 n = /**@type{?Element}*/(nodeIterator.nextNode()); 371 while (n) { 372 if (n.namespaceURI === officens) { 373 if (n.localName === "annotation") { 374 name = n.getAttributeNS(officens, 'name'); 375 if (name) { 376 if (annotationStarts.hasOwnProperty(name)) { 377 runtime.log("Warning: annotation name used more than once with <office:annotation/>: '" + name + "'"); 378 } else { 379 annotationStarts[name] = n; 380 } 381 } 382 } else if (n.localName === "annotation-end") { 383 name = n.getAttributeNS(officens, 'name'); 384 if (name) { 385 if (annotationStarts.hasOwnProperty(name)) { 386 annotationStart = /** @type {!odf.AnnotationElement}*/(annotationStarts[name]); 387 if (!annotationStart.annotationEndElement) { 388 // Linking annotation start & end 389 annotationStart.annotationEndElement = n; 390 } else { 391 runtime.log("Warning: annotation name used more than once with <office:annotation-end/>: '" + name + "'"); 392 } 393 } else { 394 runtime.log("Warning: annotation end without an annotation start, name: '" + name + "'"); 395 } 396 } else { 397 runtime.log("Warning: annotation end without a name found"); 398 } 399 } 400 } 401 n = /**@type{?Element}*/(nodeIterator.nextNode()); 402 } 403 } 404 405 /** 406 * Tags all styles with an attribute noting their scope. 407 * Helper function for the primitive complete backwriting of 408 * the automatic styles. 409 * @param {?Element} stylesRootElement 410 * @param {!string} scope 411 * @return {undefined} 412 */ 413 function setAutomaticStylesScope(stylesRootElement, scope) { 414 var n = stylesRootElement && stylesRootElement.firstChild; 415 while (n) { 416 if (n.nodeType === Node.ELEMENT_NODE) { 417 /**@type{!Element}*/(n).setAttributeNS(webodfns, "scope", scope); 418 } 419 n = n.nextSibling; 420 } 421 } 422 423 /** 424 * Returns the meta element. If it did not exist before, it will be created. 425 * @return {!Element} 426 */ 427 function getEnsuredMetaElement() { 428 var root = self.rootElement, 429 meta = root.meta; 430 431 if (!meta) { 432 root.meta = meta = document.createElementNS(officens, "meta"); 433 setChild(root, meta); 434 } 435 436 return meta; 437 } 438 439 /** 440 * @param {!string} metadataNs 441 * @param {!string} metadataLocalName 442 * @return {?string} 443 */ 444 function getMetadata(metadataNs, metadataLocalName) { 445 var node = self.rootElement.meta, textNode; 446 447 node = node && node.firstChild; 448 while (node && (node.namespaceURI !== metadataNs || node.localName !== metadataLocalName)) { 449 node = node.nextSibling; 450 } 451 node = node && node.firstChild; 452 while (node && node.nodeType !== Node.TEXT_NODE) { 453 node = node.nextSibling; 454 } 455 if (node) { 456 textNode = /**@type{!Text}*/(node); 457 return textNode.data; 458 } 459 return null; 460 } 461 this.getMetadata = getMetadata; 462 463 /** 464 * Returns key with a number postfix or none, as key unused both in map1 and map2. 465 * @param {!string} key 466 * @param {!Object} map1 467 * @param {!Object} map2 468 * @return {!string} 469 */ 470 function unusedKey(key, map1, map2) { 471 var i = 0, postFixedKey; 472 473 // cut any current postfix number 474 key = key.replace(/\d+$/, ''); 475 // start with no postfix, continue with i = 1, aiming for the simpelst unused number or key 476 postFixedKey = key; 477 while (map1.hasOwnProperty(postFixedKey) || map2.hasOwnProperty(postFixedKey)) { 478 i += 1; 479 postFixedKey = key + i; 480 } 481 482 return postFixedKey; 483 } 484 485 /** 486 * Returns a map with the fontface declaration elements, with font-face name as key. 487 * @param {!Element} fontFaceDecls 488 * @return {!Object.<!string,!Element>} 489 */ 490 function mapByFontFaceName(fontFaceDecls) { 491 var fn, result = {}, fontname; 492 // create map of current target decls 493 fn = fontFaceDecls.firstChild; 494 while (fn) { 495 if (fn.nodeType === Node.ELEMENT_NODE 496 && fn.namespaceURI === stylens 497 && fn.localName === "font-face") { 498 fontname = /**@type{!Element}*/(fn).getAttributeNS(stylens, "name"); 499 // assuming existance and uniqueness of style:name here 500 result[fontname] = fn; 501 } 502 fn = fn.nextSibling; 503 } 504 return result; 505 } 506 507 /** 508 * Merges all style:font-face elements from the source into the target. 509 * Skips elements equal to one already in the target. 510 * Elements with the same style:name but different properties get a new 511 * value for style:name. Any name changes are logged and returned as a map 512 * with the old names as keys. 513 * @param {!Element} targetFontFaceDeclsRootElement 514 * @param {!Element} sourceFontFaceDeclsRootElement 515 * @return {!Object.<!string,!string>} mapping of old font-face name to new 516 */ 517 function mergeFontFaceDecls(targetFontFaceDeclsRootElement, sourceFontFaceDeclsRootElement) { 518 var e, s, fontFaceName, newFontFaceName, 519 targetFontFaceDeclsMap, sourceFontFaceDeclsMap, 520 fontFaceNameChangeMap = {}; 521 522 targetFontFaceDeclsMap = mapByFontFaceName(targetFontFaceDeclsRootElement); 523 sourceFontFaceDeclsMap = mapByFontFaceName(sourceFontFaceDeclsRootElement); 524 525 // merge source decls into target 526 e = sourceFontFaceDeclsRootElement.firstElementChild; 527 while (e) { 528 s = e.nextElementSibling; 529 if (e.namespaceURI === stylens && e.localName === "font-face") { 530 fontFaceName = e.getAttributeNS(stylens, "name"); 531 // already such a name used in target? 532 if (targetFontFaceDeclsMap.hasOwnProperty(fontFaceName)) { 533 // skip it if the declarations are equal, otherwise insert with a new, unused name 534 if (!e.isEqualNode(targetFontFaceDeclsMap[fontFaceName])) { 535 newFontFaceName = unusedKey(fontFaceName, targetFontFaceDeclsMap, sourceFontFaceDeclsMap); 536 e.setAttributeNS(stylens, "style:name", newFontFaceName); 537 // copy with a new name 538 targetFontFaceDeclsRootElement.appendChild(e); 539 targetFontFaceDeclsMap[newFontFaceName] = e; 540 delete sourceFontFaceDeclsMap[fontFaceName]; 541 // note name change 542 fontFaceNameChangeMap[fontFaceName] = newFontFaceName; 543 } 544 } else { 545 // move over 546 // perhaps one day it could also be checked if there is an equal declaration 547 // with a different name, but that has yet to be seen in real life 548 targetFontFaceDeclsRootElement.appendChild(e); 549 targetFontFaceDeclsMap[fontFaceName] = e; 550 delete sourceFontFaceDeclsMap[fontFaceName]; 551 } 552 } 553 e = s; 554 } 555 return fontFaceNameChangeMap; 556 } 557 558 /** 559 * Creates a clone of the styles tree containing only styles tagged 560 * with the given scope, or with no specified scope. 561 * Helper function for the primitive complete backwriting of 562 * the automatic styles. 563 * @param {?Element} stylesRootElement 564 * @param {!string} scope 565 * @return {?Element} 566 */ 567 function cloneStylesInScope(stylesRootElement, scope) { 568 var copy = null, e, s, scopeAttrValue; 569 if (stylesRootElement) { 570 copy = stylesRootElement.cloneNode(true); 571 e = copy.firstElementChild; 572 while (e) { 573 s = e.nextElementSibling; 574 scopeAttrValue = e.getAttributeNS(webodfns, "scope"); 575 if (scopeAttrValue && scopeAttrValue !== scope) { 576 copy.removeChild(e); 577 } 578 e = s; 579 } 580 } 581 return copy; 582 } 583 /** 584 * Creates a clone of the font face declaration tree containing only 585 * those declarations which are referenced in the passed styles. 586 * @param {?Element} fontFaceDeclsRootElement 587 * @param {!Array.<!Element>} stylesRootElementList 588 * @return {?Element} 589 */ 590 function cloneFontFaceDeclsUsedInStyles(fontFaceDeclsRootElement, stylesRootElementList) { 591 var e, nextSibling, fontFaceName, 592 copy = null, 593 usedFontFaceDeclMap = {}; 594 595 if (fontFaceDeclsRootElement) { 596 // first collect used font faces 597 stylesRootElementList.forEach(function (stylesRootElement) { 598 styleInfo.collectUsedFontFaces(usedFontFaceDeclMap, stylesRootElement); 599 }); 600 601 // then clone all font face declarations and drop those which are not in the list of used 602 copy = fontFaceDeclsRootElement.cloneNode(true); 603 e = copy.firstElementChild; 604 while (e) { 605 nextSibling = e.nextElementSibling; 606 fontFaceName = e.getAttributeNS(stylens, "name"); 607 if (!usedFontFaceDeclMap[fontFaceName]) { 608 copy.removeChild(e); 609 } 610 e = nextSibling; 611 } 612 } 613 return copy; 614 } 615 616 /** 617 * Import the document elementnode into the DOM of OdfContainer. 618 * Any processing instructions are removed, since importing them 619 * gives an exception. 620 * @param {Document|undefined} xmldoc 621 * @return {!Element|undefined} 622 */ 623 function importRootNode(xmldoc) { 624 var doc = self.rootElement.ownerDocument, 625 node; 626 // remove all processing instructions 627 // TODO: replace cursor processing instruction with an element 628 if (xmldoc) { 629 removeProcessingInstructions(xmldoc.documentElement); 630 try { 631 node = /**@type{!Element}*/(doc.importNode(xmldoc.documentElement, true)); 632 } catch (ignore) { 633 } 634 } 635 return node; 636 } 637 /** 638 * @param {!number} state 639 * @return {undefined} 640 */ 641 function setState(state) { 642 self.state = state; 643 if (self.onchange) { 644 self.onchange(self); 645 } 646 if (self.onstatereadychange) { 647 self.onstatereadychange(self); 648 } 649 } 650 /** 651 * @param {!Element} root 652 * @return {undefined} 653 */ 654 function setRootElement(root) { 655 contentElement = null; 656 self.rootElement = /**@type{!odf.ODFDocumentElement}*/(root); 657 root.fontFaceDecls = domUtils.getDirectChild(root, officens, 'font-face-decls'); 658 root.styles = domUtils.getDirectChild(root, officens, 'styles'); 659 root.automaticStyles = domUtils.getDirectChild(root, officens, 'automatic-styles'); 660 root.masterStyles = domUtils.getDirectChild(root, officens, 'master-styles'); 661 root.body = domUtils.getDirectChild(root, officens, 'body'); 662 root.meta = domUtils.getDirectChild(root, officens, 'meta'); 663 root.settings = domUtils.getDirectChild(root, officens, 'settings'); 664 root.scripts = domUtils.getDirectChild(root, officens, 'scripts'); 665 linkAnnotationStartAndEndElements(root); 666 } 667 /** 668 * @param {Document|undefined} xmldoc 669 * @return {undefined} 670 */ 671 function handleFlatXml(xmldoc) { 672 var root = importRootNode(xmldoc); 673 if (!root || root.localName !== 'document' || 674 root.namespaceURI !== officens) { 675 setState(OdfContainer.INVALID); 676 return; 677 } 678 setRootElement(/**@type{!Element}*/(root)); 679 setState(OdfContainer.DONE); 680 } 681 /** 682 * @param {Document} xmldoc 683 * @return {undefined} 684 */ 685 function handleStylesXml(xmldoc) { 686 var node = importRootNode(xmldoc), 687 root = self.rootElement, 688 n; 689 if (!node || node.localName !== 'document-styles' || 690 node.namespaceURI !== officens) { 691 setState(OdfContainer.INVALID); 692 return; 693 } 694 root.fontFaceDecls = domUtils.getDirectChild(node, officens, 'font-face-decls'); 695 setChild(root, root.fontFaceDecls); 696 n = domUtils.getDirectChild(node, officens, 'styles'); 697 root.styles = n || xmldoc.createElementNS(officens, 'styles'); 698 setChild(root, root.styles); 699 n = domUtils.getDirectChild(node, officens, 'automatic-styles'); 700 root.automaticStyles = n || xmldoc.createElementNS(officens, 'automatic-styles'); 701 setAutomaticStylesScope(root.automaticStyles, documentStylesScope); 702 setChild(root, root.automaticStyles); 703 node = domUtils.getDirectChild(node, officens, 'master-styles'); 704 root.masterStyles = node || xmldoc.createElementNS(officens, 705 'master-styles'); 706 setChild(root, root.masterStyles); 707 // automatic styles from styles.xml could shadow automatic styles 708 // from content.xml, because they could have the same name 709 // so prefix them and their uses with some almost unique string 710 styleInfo.prefixStyleNames(root.automaticStyles, automaticStylePrefix, root.masterStyles); 711 } 712 /** 713 * @param {Document} xmldoc 714 * @return {undefined} 715 */ 716 function handleContentXml(xmldoc) { 717 var node = importRootNode(xmldoc), 718 root, 719 automaticStyles, 720 fontFaceDecls, 721 fontFaceNameChangeMap, 722 c; 723 if (!node || node.localName !== 'document-content' || 724 node.namespaceURI !== officens) { 725 setState(OdfContainer.INVALID); 726 return; 727 } 728 root = self.rootElement; 729 fontFaceDecls = domUtils.getDirectChild(node, officens, 'font-face-decls'); 730 if (root.fontFaceDecls && fontFaceDecls) { 731 fontFaceNameChangeMap = mergeFontFaceDecls(root.fontFaceDecls, fontFaceDecls); 732 } else if (fontFaceDecls) { 733 root.fontFaceDecls = fontFaceDecls; 734 setChild(root, fontFaceDecls); 735 } 736 automaticStyles = domUtils.getDirectChild(node, officens, 'automatic-styles'); 737 setAutomaticStylesScope(automaticStyles, documentContentScope); 738 if (fontFaceNameChangeMap) { 739 styleInfo.changeFontFaceNames(automaticStyles, fontFaceNameChangeMap); 740 } 741 if (root.automaticStyles && automaticStyles) { 742 c = automaticStyles.firstChild; 743 while (c) { 744 root.automaticStyles.appendChild(c); 745 c = automaticStyles.firstChild; // works because node c moved 746 } 747 } else if (automaticStyles) { 748 root.automaticStyles = automaticStyles; 749 setChild(root, automaticStyles); 750 } 751 node = domUtils.getDirectChild(node, officens, 'body'); 752 if (node === null) { 753 throw "<office:body/> tag is mising."; 754 } 755 root.body = node; 756 setChild(root, root.body); 757 } 758 /** 759 * @param {Document} xmldoc 760 * @return {undefined} 761 */ 762 function handleMetaXml(xmldoc) { 763 var node = importRootNode(xmldoc), 764 root; 765 if (!node || node.localName !== 'document-meta' || 766 node.namespaceURI !== officens) { 767 return; 768 } 769 root = self.rootElement; 770 root.meta = domUtils.getDirectChild(node, officens, 'meta'); 771 setChild(root, root.meta); 772 } 773 /** 774 * @param {Document} xmldoc 775 * @return {undefined} 776 */ 777 function handleSettingsXml(xmldoc) { 778 var node = importRootNode(xmldoc), 779 root; 780 if (!node || node.localName !== 'document-settings' || 781 node.namespaceURI !== officens) { 782 return; 783 } 784 root = self.rootElement; 785 root.settings = domUtils.getDirectChild(node, officens, 'settings'); 786 setChild(root, root.settings); 787 } 788 /** 789 * @param {Document} xmldoc 790 * @return {undefined} 791 */ 792 function handleManifestXml(xmldoc) { 793 var node = importRootNode(xmldoc), 794 root, 795 e; 796 if (!node || node.localName !== 'manifest' || 797 node.namespaceURI !== manifestns) { 798 return; 799 } 800 root = self.rootElement; 801 root.manifest = /**@type{!Element}*/(node); 802 e = root.manifest.firstElementChild; 803 while (e) { 804 if (e.localName === "file-entry" && 805 e.namespaceURI === manifestns) { 806 partMimetypes[e.getAttributeNS(manifestns, "full-path")] = 807 e.getAttributeNS(manifestns, "media-type"); 808 } 809 e = e.nextElementSibling; 810 } 811 } 812 /** 813 * @param {!Document} xmldoc 814 * @param {!string} localName 815 * @param {!Object.<!string,!boolean>} allowedNamespaces 816 * @return {undefined} 817 */ 818 function removeElements(xmldoc, localName, allowedNamespaces) { 819 var elements = domUtils.getElementsByTagName(xmldoc, localName), 820 element, 821 i; 822 for (i = 0; i < elements.length; i += 1) { 823 element = elements[i]; 824 if (!allowedNamespaces.hasOwnProperty(element.namespaceURI)) { 825 element.parentNode.removeChild(element); 826 } 827 } 828 } 829 /** 830 * Remove any HTML <script/> tags from the DOM. 831 * The tags need to be removed, because otherwise they would be executed 832 * when the dom is inserted into the document. 833 * To be safe, all elements with localName "script" are removed, unless 834 * they are in a known, allowed namespace. 835 * @param {!Document} xmldoc 836 * @return {undefined} 837 */ 838 function removeDangerousElements(xmldoc) { 839 removeElements(xmldoc, "script", { 840 "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0": true, 841 "urn:oasis:names:tc:opendocument:xmlns:office:1.0": true, 842 "urn:oasis:names:tc:opendocument:xmlns:table:1.0": true, 843 "urn:oasis:names:tc:opendocument:xmlns:text:1.0": true, 844 "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0": true 845 }); 846 removeElements(xmldoc, "style", { 847 "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0": true, 848 "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0": true, 849 "urn:oasis:names:tc:opendocument:xmlns:style:1.0": true 850 }); 851 } 852 853 /** 854 * Remove all attributes that have no namespace and that have 855 * localname like 'on....', the event handler attributes. 856 * @param {!Element} element 857 * @return {undefined} 858 */ 859 function removeDangerousAttributes(element) { 860 var e = element.firstElementChild, as = [], i, n, a, 861 atts = element.attributes, 862 l = atts.length; 863 // collect all dangerous attributes 864 for (i = 0; i < l; i += 1) { 865 a = atts.item(i); 866 n = a.localName.substr(0, 2).toLowerCase(); 867 if (a.namespaceURI === null && n === "on") { 868 as.push(a); 869 } 870 } 871 // remove the dangerous attributes 872 l = as.length; 873 for (i = 0; i < l; i += 1) { 874 element.removeAttributeNode(as[i]); 875 } 876 // recurse into the child elements 877 while (e) { 878 removeDangerousAttributes(e); 879 e = e.nextElementSibling; 880 } 881 } 882 883 /** 884 * @param {!Array.<!{path:string,handler:function(?Document)}>} remainingComponents 885 * @return {undefined} 886 */ 887 function loadNextComponent(remainingComponents) { 888 var component = remainingComponents.shift(); 889 890 if (component) { 891 zip.loadAsDOM(component.path, function (err, xmldoc) { 892 if (xmldoc) { 893 removeDangerousElements(xmldoc); 894 removeDangerousAttributes(xmldoc.documentElement); 895 } 896 component.handler(xmldoc); 897 if (self.state === OdfContainer.INVALID) { 898 if (err) { 899 runtime.log("ERROR: Unable to load " + component.path + " - " + err); 900 } else { 901 runtime.log("ERROR: Unable to load " + component.path); 902 } 903 return; 904 } 905 if (err) { 906 runtime.log("DEBUG: Unable to load " + component.path + " - " + err); 907 } 908 loadNextComponent(remainingComponents); 909 }); 910 } else { 911 linkAnnotationStartAndEndElements(self.rootElement); 912 setState(OdfContainer.DONE); 913 } 914 } 915 /** 916 * @return {undefined} 917 */ 918 function loadComponents() { 919 var componentOrder = [ 920 {path: 'styles.xml', handler: handleStylesXml}, 921 {path: 'content.xml', handler: handleContentXml}, 922 {path: 'meta.xml', handler: handleMetaXml}, 923 {path: 'settings.xml', handler: handleSettingsXml}, 924 {path: 'META-INF/manifest.xml', handler: handleManifestXml} 925 ]; 926 loadNextComponent(componentOrder); 927 } 928 /** 929 * @param {!string} name 930 * @return {!string} 931 */ 932 function createDocumentElement(name) { 933 var /**@type{string}*/ 934 s = ""; 935 936 /** 937 * @param {string} prefix 938 * @param {string} ns 939 */ 940 function defineNamespace(prefix, ns) { 941 s += " xmlns:" + prefix + "=\"" + ns + "\""; 942 } 943 odf.Namespaces.forEachPrefix(defineNamespace); 944 return "<?xml version=\"1.0\" encoding=\"UTF-8\"?><office:" + name + 945 " " + s + " office:version=\"1.2\">"; 946 } 947 /** 948 * @return {!string} 949 */ 950 function serializeMetaXml() { 951 var serializer = new xmldom.LSSerializer(), 952 /**@type{!string}*/ 953 s = createDocumentElement("document-meta"); 954 serializer.filter = new odf.OdfNodeFilter(); 955 s += serializer.writeToString(self.rootElement.meta, odf.Namespaces.namespaceMap); 956 s += "</office:document-meta>"; 957 return s; 958 } 959 /** 960 * Creates a manifest:file-entry node 961 * @param {!string} fullPath Full-path attribute value for the file-entry 962 * @param {!string} mediaType Media-type attribute value for the file-entry 963 * @return {!Node} 964 */ 965 function createManifestEntry(fullPath, mediaType) { 966 var element = document.createElementNS(manifestns, 'manifest:file-entry'); 967 element.setAttributeNS(manifestns, 'manifest:full-path', fullPath); 968 element.setAttributeNS(manifestns, 'manifest:media-type', mediaType); 969 return element; 970 } 971 /** 972 * @return {string} 973 */ 974 function serializeManifestXml() { 975 var header = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n', 976 xml = '<manifest:manifest xmlns:manifest="' + manifestns + '" manifest:version="1.2"></manifest:manifest>', 977 manifest = /**@type{!Document}*/(runtime.parseXML(xml)), 978 manifestRoot = manifest.documentElement, 979 serializer = new xmldom.LSSerializer(), 980 /**@type{string}*/ 981 fullPath; 982 983 for (fullPath in partMimetypes) { 984 if (partMimetypes.hasOwnProperty(fullPath)) { 985 manifestRoot.appendChild(createManifestEntry(fullPath, partMimetypes[fullPath])); 986 } 987 } 988 serializer.filter = new odf.OdfNodeFilter(); 989 return header + serializer.writeToString(manifest, odf.Namespaces.namespaceMap); 990 } 991 /** 992 * @return {!string} 993 */ 994 function serializeSettingsXml() { 995 var serializer, 996 /**@type{!string}*/ 997 s = ""; 998 // <office:settings/> is optional, but if present must have at least one child element 999 if (self.rootElement.settings && self.rootElement.settings.firstElementChild) { 1000 serializer = new xmldom.LSSerializer(); 1001 s = createDocumentElement("document-settings"); 1002 serializer.filter = new odf.OdfNodeFilter(); 1003 s += serializer.writeToString(self.rootElement.settings, odf.Namespaces.namespaceMap); 1004 s += "</office:document-settings>"; 1005 } 1006 return s; 1007 } 1008 /** 1009 * @return {!string} 1010 */ 1011 function serializeStylesXml() { 1012 var fontFaceDecls, automaticStyles, masterStyles, 1013 nsmap = odf.Namespaces.namespaceMap, 1014 serializer = new xmldom.LSSerializer(), 1015 /**@type{!string}*/ 1016 s = createDocumentElement("document-styles"); 1017 1018 // special handling for merged toplevel nodes 1019 automaticStyles = cloneStylesInScope( 1020 self.rootElement.automaticStyles, 1021 documentStylesScope 1022 ); 1023 masterStyles = /**@type{!Element}*/(self.rootElement.masterStyles.cloneNode(true)); 1024 fontFaceDecls = cloneFontFaceDeclsUsedInStyles(self.rootElement.fontFaceDecls, [masterStyles, self.rootElement.styles, automaticStyles]); 1025 1026 // automatic styles from styles.xml could shadow automatic styles from content.xml, 1027 // because they could have the same name 1028 // thus they were prefixed on loading with some almost unique string, which cam be removed 1029 // again before saving 1030 styleInfo.removePrefixFromStyleNames(automaticStyles, 1031 automaticStylePrefix, masterStyles); 1032 serializer.filter = new OdfStylesFilter(masterStyles, automaticStyles); 1033 1034 s += serializer.writeToString(fontFaceDecls, nsmap); 1035 s += serializer.writeToString(self.rootElement.styles, nsmap); 1036 s += serializer.writeToString(automaticStyles, nsmap); 1037 s += serializer.writeToString(masterStyles, nsmap); 1038 s += "</office:document-styles>"; 1039 return s; 1040 } 1041 /** 1042 * @return {!string} 1043 */ 1044 function serializeContentXml() { 1045 var fontFaceDecls, automaticStyles, 1046 nsmap = odf.Namespaces.namespaceMap, 1047 serializer = new xmldom.LSSerializer(), 1048 /**@type{!string}*/ 1049 s = createDocumentElement("document-content"); 1050 1051 // special handling for merged toplevel nodes 1052 automaticStyles = cloneStylesInScope(self.rootElement.automaticStyles, documentContentScope); 1053 fontFaceDecls = cloneFontFaceDeclsUsedInStyles(self.rootElement.fontFaceDecls, [automaticStyles]); 1054 1055 serializer.filter = new OdfContentFilter(self.rootElement.body, automaticStyles); 1056 1057 s += serializer.writeToString(fontFaceDecls, nsmap); 1058 s += serializer.writeToString(automaticStyles, nsmap); 1059 s += serializer.writeToString(self.rootElement.body, nsmap); 1060 s += "</office:document-content>"; 1061 return s; 1062 } 1063 /** 1064 * @param {!{Type:function(new:Object),namespaceURI:string,localName:string}} type 1065 * @return {!Element} 1066 */ 1067 function createElement(type) { 1068 var original = document.createElementNS( 1069 type.namespaceURI, 1070 type.localName 1071 ), 1072 /**@type{string}*/ 1073 method, 1074 iface = new type.Type(); 1075 for (method in iface) { 1076 if (iface.hasOwnProperty(method)) { 1077 original[method] = iface[method]; 1078 } 1079 } 1080 return original; 1081 } 1082 /** 1083 * @param {!string} url 1084 * @param {!function((string)):undefined} callback 1085 * @return {undefined} 1086 */ 1087 function loadFromXML(url, callback) { 1088 /** 1089 * @param {?string} err 1090 * @param {?Document} dom 1091 */ 1092 function handler(err, dom) { 1093 if (err) { 1094 callback(err); 1095 } else if (!dom) { 1096 callback("No DOM was loaded."); 1097 } else { 1098 removeDangerousElements(dom); 1099 removeDangerousAttributes(dom.documentElement); 1100 handleFlatXml(dom); 1101 } 1102 } 1103 runtime.loadXML(url, handler); 1104 } 1105 // public functions 1106 this.setRootElement = setRootElement; 1107 1108 /** 1109 * @return {!Element} 1110 */ 1111 this.getContentElement = function () { 1112 var /**@type{!Element}*/ 1113 body; 1114 if (!contentElement) { 1115 body = self.rootElement.body; 1116 contentElement = domUtils.getDirectChild(body, officens, "text") 1117 || domUtils.getDirectChild(body, officens, "presentation") 1118 || domUtils.getDirectChild(body, officens, "spreadsheet"); 1119 } 1120 if (!contentElement) { 1121 throw "Could not find content element in <office:body/>."; 1122 } 1123 return contentElement; 1124 }; 1125 1126 /** 1127 * Gets the document type as 'text', 'presentation', or 'spreadsheet'. 1128 * @return {!string} 1129 */ 1130 this.getDocumentType = function () { 1131 var content = self.getContentElement(); 1132 return content && content.localName; 1133 }; 1134 1135 /** 1136 * Open file and parse it. Return the XML Node. Return the root node of 1137 * the file or null if this is not possible. 1138 * For 'content.xml', 'styles.xml', 'meta.xml', and 'settings.xml', the 1139 * elements 'document-content', 'document-styles', 'document-meta', or 1140 * 'document-settings' will be returned respectively. 1141 * @param {string} partname 1142 * @return {!odf.OdfPart} 1143 **/ 1144 this.getPart = function (partname) { 1145 return new odf.OdfPart(partname, partMimetypes[partname], self, zip); 1146 }; 1147 /** 1148 * @param {string} url 1149 * @param {function(?string, ?Uint8Array)} callback receiving err and data 1150 * @return {undefined} 1151 */ 1152 this.getPartData = function (url, callback) { 1153 zip.load(url, callback); 1154 }; 1155 1156 /** 1157 * Sets the metadata fields from the given properties map. 1158 * @param {?Object.<!string, !string>} setProperties A flat object that is a string->string map of field name -> value. 1159 * @param {?Array.<!string>} removedPropertyNames An array of metadata field names (prefixed). 1160 * @return {undefined} 1161 */ 1162 function setMetadata(setProperties, removedPropertyNames) { 1163 var metaElement = getEnsuredMetaElement(); 1164 1165 if (setProperties) { 1166 domUtils.mapKeyValObjOntoNode(metaElement, setProperties, odf.Namespaces.lookupNamespaceURI); 1167 } 1168 if (removedPropertyNames) { 1169 domUtils.removeKeyElementsFromNode(metaElement, removedPropertyNames, odf.Namespaces.lookupNamespaceURI); 1170 } 1171 } 1172 this.setMetadata = setMetadata; 1173 1174 /** 1175 * Increment the number of times the document has been edited. 1176 * @return {!number} new number of editing cycles 1177 */ 1178 this.incrementEditingCycles = function () { 1179 var currentValueString = getMetadata(odf.Namespaces.metans, "editing-cycles"), 1180 currentCycles = currentValueString ? parseInt(currentValueString, 10) : 0; 1181 1182 if (isNaN(currentCycles)) { 1183 currentCycles = 0; 1184 } 1185 1186 setMetadata({"meta:editing-cycles": currentCycles + 1}, null); 1187 return currentCycles + 1; 1188 }; 1189 1190 /** 1191 * Write pre-saving metadata to the DOM 1192 * @return {undefined} 1193 */ 1194 function updateMetadataForSaving() { 1195 // set the opendocument provider used to create/ 1196 // last modify the document. 1197 // this string should match the definition for 1198 // user-agents in the http protocol as specified 1199 // in section 14.43 of [RFC2616]. 1200 var generatorString, 1201 window = runtime.getWindow(); 1202 1203 generatorString = "WebODF/" + webodf.Version; 1204 1205 if (window) { 1206 generatorString = generatorString + " " + window.navigator.userAgent; 1207 } 1208 1209 setMetadata({"meta:generator": generatorString}, null); 1210 } 1211 1212 /** 1213 * @param {!string} type 1214 * @return {!core.Zip} 1215 */ 1216 function createEmptyDocument(type) { 1217 var emptyzip = new core.Zip("", null), 1218 data = runtime.byteArrayFromString( 1219 "application/vnd.oasis.opendocument." + type, 1220 "utf8" 1221 ), 1222 root = self.rootElement, 1223 content = document.createElementNS(officens, type); 1224 emptyzip.save("mimetype", data, false, new Date()); 1225 /** 1226 * @param {!string} memberName variant of the real local name which allows dot notation 1227 * @param {!string=} realLocalName 1228 * @return {undefined} 1229 */ 1230 function addToplevelElement(memberName, realLocalName) { 1231 var element; 1232 if (!realLocalName) { 1233 realLocalName = memberName; 1234 } 1235 element = document.createElementNS(officens, realLocalName); 1236 root[memberName] = element; 1237 root.appendChild(element); 1238 } 1239 // add toplevel elements in correct order to the root node 1240 addToplevelElement("meta"); 1241 addToplevelElement("settings"); 1242 addToplevelElement("scripts"); 1243 addToplevelElement("fontFaceDecls", "font-face-decls"); 1244 addToplevelElement("styles"); 1245 addToplevelElement("automaticStyles", "automatic-styles"); 1246 addToplevelElement("masterStyles", "master-styles"); 1247 addToplevelElement("body"); 1248 root.body.appendChild(content); 1249 partMimetypes["/"] = "application/vnd.oasis.opendocument." + type; 1250 partMimetypes["settings.xml"] = "text/xml"; 1251 partMimetypes["meta.xml"] = "text/xml"; 1252 partMimetypes["styles.xml"] = "text/xml"; 1253 partMimetypes["content.xml"] = "text/xml"; 1254 1255 setState(OdfContainer.DONE); 1256 return emptyzip; 1257 } 1258 1259 /** 1260 * Fill the zip with current data. 1261 * @return {undefined} 1262 */ 1263 function fillZip() { 1264 // the assumption so far is that all ODF parts are serialized 1265 // already, but meta, settings, styles and content should be 1266 // refreshed 1267 // update the zip entries with the data from the live ODF DOM 1268 var data, 1269 date = new Date(), 1270 settings; 1271 1272 settings = serializeSettingsXml(); 1273 if (settings) { 1274 // Optional according to package spec 1275 // See http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__440346_826425813 1276 data = runtime.byteArrayFromString(settings, "utf8"); 1277 zip.save("settings.xml", data, true, date); 1278 } else { 1279 zip.remove("settings.xml"); 1280 } 1281 updateMetadataForSaving(); 1282 // Even thought meta-data is optional, it is always created by the previous statement 1283 data = runtime.byteArrayFromString(serializeMetaXml(), "utf8"); 1284 zip.save("meta.xml", data, true, date); 1285 data = runtime.byteArrayFromString(serializeStylesXml(), "utf8"); 1286 zip.save("styles.xml", data, true, date); 1287 data = runtime.byteArrayFromString(serializeContentXml(), "utf8"); 1288 zip.save("content.xml", data, true, date); 1289 data = runtime.byteArrayFromString(serializeManifestXml(), "utf8"); 1290 zip.save("META-INF/manifest.xml", data, true, date); 1291 } 1292 /** 1293 * Create a bytearray from the zipfile. 1294 * @param {!function(!Uint8Array):undefined} successCallback receiving zip as bytearray 1295 * @param {!function(?string):undefined} errorCallback receiving possible err 1296 * @return {undefined} 1297 */ 1298 function createByteArray(successCallback, errorCallback) { 1299 fillZip(); 1300 zip.createByteArray(successCallback, errorCallback); 1301 } 1302 this.createByteArray = createByteArray; 1303 /** 1304 * @param {!string} newurl 1305 * @param {function(?string):undefined} callback 1306 * @return {undefined} 1307 */ 1308 function saveAs(newurl, callback) { 1309 fillZip(); 1310 zip.writeAs(newurl, function (err) { 1311 callback(err); 1312 }); 1313 } 1314 this.saveAs = saveAs; 1315 /** 1316 * @param {function(?string):undefined} callback 1317 * @return {undefined} 1318 */ 1319 this.save = function (callback) { 1320 saveAs(url, callback); 1321 }; 1322 1323 /** 1324 * @return {!string} 1325 */ 1326 this.getUrl = function () { 1327 // TODO: saveAs seems to not update the url, is that wanted? 1328 return url; 1329 }; 1330 /** 1331 * Add a new blob or overwrite any existing blob which has the same filename. 1332 * @param {!string} filename 1333 * @param {!string} mimetype 1334 * @param {!string} content base64 encoded string 1335 */ 1336 this.setBlob = function (filename, mimetype, content) { 1337 var data = base64.convertBase64ToByteArray(content), 1338 date = new Date(); 1339 zip.save(filename, data, false, date); 1340 if (partMimetypes.hasOwnProperty(filename)) { 1341 runtime.log(filename + " has been overwritten."); 1342 } 1343 partMimetypes[filename] = mimetype; 1344 }; 1345 /** 1346 * @param {!string} filename 1347 */ 1348 this.removeBlob = function (filename) { 1349 var foundAndRemoved = zip.remove(filename); 1350 runtime.assert(foundAndRemoved, "file is not found: " + filename); 1351 delete partMimetypes[filename]; 1352 }; 1353 // initialize public variables 1354 this.state = OdfContainer.LOADING; 1355 this.rootElement = /**@type{!odf.ODFDocumentElement}*/( 1356 createElement({ 1357 Type: odf.ODFDocumentElement, 1358 namespaceURI: odf.ODFDocumentElement.namespaceURI, 1359 localName: odf.ODFDocumentElement.localName 1360 }) 1361 ); 1362 1363 // initialize private variables 1364 if (urlOrType === odf.OdfContainer.DocumentType.TEXT) { 1365 zip = createEmptyDocument("text"); 1366 } else if (urlOrType === odf.OdfContainer.DocumentType.PRESENTATION) { 1367 zip = createEmptyDocument("presentation"); 1368 } else if (urlOrType === odf.OdfContainer.DocumentType.SPREADSHEET) { 1369 zip = createEmptyDocument("spreadsheet"); 1370 } else { 1371 url = /**@type{!string}*/(urlOrType); 1372 zip = new core.Zip(url, function (err, zipobject) { 1373 zip = zipobject; 1374 if (err) { 1375 loadFromXML(url, function (xmlerr) { 1376 if (err) { 1377 zip.error = err + "\n" + xmlerr; 1378 setState(OdfContainer.INVALID); 1379 } 1380 }); 1381 } else { 1382 loadComponents(); 1383 } 1384 }); 1385 } 1386 }; 1387 odf.OdfContainer.EMPTY = 0; 1388 odf.OdfContainer.LOADING = 1; 1389 odf.OdfContainer.DONE = 2; 1390 odf.OdfContainer.INVALID = 3; 1391 odf.OdfContainer.SAVING = 4; 1392 odf.OdfContainer.MODIFIED = 5; 1393 /** 1394 * @param {!string} url 1395 * @return {!odf.OdfContainer} 1396 */ 1397 odf.OdfContainer.getContainer = function (url) { 1398 return new odf.OdfContainer(url, null); 1399 }; 1400 }()); 1401 /** 1402 * @enum {number} 1403 */ 1404 odf.OdfContainer.DocumentType = { 1405 TEXT: 1, 1406 PRESENTATION: 2, 1407 SPREADSHEET: 3 1408 }; 1409