node-webodf
Version:
WebODF - JavaScript Document Engine http://webodf.org/
714 lines (710 loc) • 25.7 kB
JavaScript
/**
* Copyright (C) 2014 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 runtime, odf*/
/*jslint emptyblock: false, unparam: false*/
/**
* @constructor
* @param {?Element} element
* @param {!odf.PageLayoutCache} pageLayoutCache
*/
odf.MasterPage = function (element, pageLayoutCache) {
"use strict";
var self = this;
/**
* @type {!odf.PageLayout}
*/
this.pageLayout;
function init() {
var pageLayoutName;
if (element) {
pageLayoutName = element.getAttributeNS(odf.Namespaces.stylens,
"page-layout-name");
self.pageLayout = pageLayoutCache.getPageLayout(pageLayoutName);
} else {
self.pageLayout = pageLayoutCache.getDefaultPageLayout();
}
}
init();
};
/*jslint emptyblock: true, unparam: true*/
/**
* @interface
*/
odf.MasterPageCache = function () {"use strict"; };
/**
* @param {!string} name
* @return {?odf.MasterPage}
*/
odf.MasterPageCache.prototype.getMasterPage = function (name) {"use strict"; };
/*jslint emptyblock: false, unparam: false*/
/**
* @constructor
* @param {!Element} element
* @param {!odf.StyleParseUtils} styleParseUtils
* @param {!odf.MasterPageCache} masterPageCache
* @param {!odf.StylePileEntry=} parent
*/
odf.StylePileEntry = function (element, styleParseUtils, masterPageCache, parent) {
"use strict";
/**
* @type {!odf.TextProperties|undefined}
*/
this.text;
/**
* @type {!odf.ParagraphProperties|undefined}
*/
this.paragraph;
/**
* @type {!odf.GraphicProperties|undefined}
*/
this.graphic;
/**
* @return {?odf.MasterPage}
*/
this.masterPage = function () {
var masterPageName = element.getAttributeNS(odf.Namespaces.stylens,
"master-page-name"),
masterPage = null;
if (masterPageName) {
masterPage = masterPageCache.getMasterPage(masterPageName);
}
return masterPage;
};
/**
* @param {!odf.StylePileEntry} self
* @return {undefined}
*/
function init(self) {
var stylens = odf.Namespaces.stylens,
family = element.getAttributeNS(stylens, "family"),
e = null;
if (family === "graphic" || family === "chart") {
self.graphic = parent === undefined ? undefined : parent.graphic;
e = styleParseUtils.getPropertiesElement("graphic-properties", element, e);
if (e !== null) {
self.graphic = new odf.GraphicProperties(e, styleParseUtils,
self.graphic);
}
}
if (family === "paragraph" || family === "table-cell"
|| family === "graphic" || family === "presentation"
|| family === "chart") {
self.paragraph = parent === undefined ? undefined : parent.paragraph;
e = styleParseUtils.getPropertiesElement("paragraph-properties", element, e);
if (e !== null) {
self.paragraph = new odf.ParagraphProperties(e, styleParseUtils,
self.paragraph);
}
}
if (family === "text" || family === "paragraph"
|| family === "table-cell" || family === "graphic"
|| family === "presentation" || family === "chart") {
self.text = parent === undefined ? undefined : parent.text;
e = styleParseUtils.getPropertiesElement("text-properties", element, e);
if (e !== null) {
self.text = new odf.TextProperties(e, styleParseUtils, self.text);
}
}
}
init(this);
};
/**
* Collection of all the styles in the document for one style family.
* There are separate style piles for family 'text', 'paragraph', 'graphic' etc.
* @constructor
* @param {!odf.StyleParseUtils} styleParseUtils
* @param {!odf.MasterPageCache} masterPageCache
*/
odf.StylePile = function (styleParseUtils, masterPageCache) {
"use strict";
var stylens = odf.Namespaces.stylens,
/**@type{!Object.<!string,!Element>}*/
commonStyles = {},
/**@type{!Object.<!string,!Element>}*/
automaticStyles = {},
/**@type{!odf.StylePileEntry|undefined}*/
defaultStyle,
/**@type{!Object.<!string,!odf.StylePileEntry>}*/
parsedCommonStyles = {},
/**@type{!Object.<!string,!odf.StylePileEntry>}*/
parsedAutomaticStyles = {},
/**@type{!function(!string,!Array.<!string>):(!odf.StylePileEntry|undefined)}*/
getCommonStyle;
/**
* @param {!Element} element
* @param {!Array.<!string>} visitedStyles track visited styles to avoid loops
* @return {!odf.StylePileEntry}
*/
function parseStyle(element, visitedStyles) {
var parent,
parentName,
style;
if (element.hasAttributeNS(stylens, "parent-style-name")) {
parentName = element.getAttributeNS(stylens, "parent-style-name");
if (visitedStyles.indexOf(parentName) === -1) {
parent = getCommonStyle(parentName, visitedStyles);
}
}
style = new odf.StylePileEntry(element, styleParseUtils, masterPageCache, parent);
return style;
}
/**
* @param {!string} styleName
* @param {!Array.<!string>} visitedStyles track visited styles to avoid loops
* @return {!odf.StylePileEntry|undefined}
*/
getCommonStyle = function (styleName, visitedStyles) {
var style = parsedCommonStyles[styleName],
element;
if (!style) {
element = commonStyles[styleName];
if (element) {
visitedStyles.push(styleName);
style = parseStyle(element, visitedStyles);
parsedCommonStyles[styleName] = style;
}
}
return style;
};
/**
* @param {!string} styleName
* @return {!odf.StylePileEntry|undefined}
*/
function getStyle(styleName) {
var style = parsedAutomaticStyles[styleName]
|| parsedCommonStyles[styleName],
element,
visitedStyles = [];
if (!style) {
element = automaticStyles[styleName];
if (!element) {
element = commonStyles[styleName];
if (element) {
visitedStyles.push(styleName);
}
}
if (element) {
style = parseStyle(element, visitedStyles);
}
}
return style;
}
this.getStyle = getStyle;
/**
* @param {!Element} style
* @return {undefined}
*/
this.addCommonStyle = function (style) {
var name;
if (style.hasAttributeNS(stylens, "name")) {
name = style.getAttributeNS(stylens, "name");
if (!commonStyles.hasOwnProperty(name)) {
commonStyles[name] = style;
}
}
};
/**
* @param {!Element} style
* @return {undefined}
*/
this.addAutomaticStyle = function (style) {
var name;
if (style.hasAttributeNS(stylens, "name")) {
name = style.getAttributeNS(stylens, "name");
if (!automaticStyles.hasOwnProperty(name)) {
automaticStyles[name] = style;
}
}
};
/**
* @param {!Element} style
* @return {undefined}
*/
this.setDefaultStyle = function (style) {
if (defaultStyle === undefined) {
defaultStyle = parseStyle(style, []);
}
};
/**
* @return {!odf.StylePileEntry|undefined}
*/
this.getDefaultStyle = function () {
return defaultStyle;
};
};
/**
* @constructor
*/
odf.ComputedGraphicStyle = function () {
"use strict";
/**
* @type {!odf.ComputedTextProperties}
*/
this.text = new odf.ComputedTextProperties();
/**
* @type {!odf.ComputedParagraphProperties}
*/
this.paragraph = new odf.ComputedParagraphProperties();
/**
* @type {!odf.ComputedGraphicProperties}
*/
this.graphic = new odf.ComputedGraphicProperties();
};
/**
* @constructor
*/
odf.ComputedParagraphStyle = function () {
"use strict";
/**
* @type {!odf.ComputedTextProperties}
*/
this.text = new odf.ComputedTextProperties();
/**
* @type {!odf.ComputedParagraphProperties}
*/
this.paragraph = new odf.ComputedParagraphProperties();
};
/**
* @constructor
*/
odf.ComputedTextStyle = function () {
"use strict";
/**
* @type {!odf.ComputedTextProperties}
*/
this.text = new odf.ComputedTextProperties();
};
/**
* Fast and type-safe access to styling properties of an ODF document.
* When the document changes, update() has to be called to update the
* information.
*
* This class gives access to computed styles. The term 'computed' is used
* similarly to its use in the DOM function window.getComputedStyle().
* In ODF, as in CSS but differently, the evaluation of styles is influenced by
* the position of an element in a document. Specifically, the types and styles
* of the ancestor elements determine properties of a style. This is explained
* in chapter 16 of the ODF 1.2 specification.
* http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1416272_253892949
*
* Here is an example. Consider the following style and content:
<s:styles>
<s:default-style s:family="text">...</s:default-style>
<s:default-style s:family="paragraph">...</s:default-style>
<s:style s:name="Standard" s:family="text">...</s:style>
<s:style s:name="Bold" s:parent-style-name="Standard" s:family="text">...</s:style>
<s:style s:name="Standard" s:family="paragraph">...</s:style>
</s:styles>
<s:automatic-styles>
<s:style s:name="T1" s:parent-style-name="Standard" s:family="text">...</s:style>
<s:style s:name="T2" s:parent-style-name="Bold" s:family="text">...</s:style>
<s:style s:name="C1" s:parent-style-name="Standard" s:family="text">...</s:style>
<s:style s:name="C2" s:parent-style-name="Standard" s:family="text">...</s:style>
<s:style s:name="P1" s:parent-style-name="Standard" s:family="paragraph">...</s:style>
</s:automatic-styles>
...
<text:p text:style-name="P1">
<text:span text:style-name="T1" text:class-names="C1 C2">
<text:span text:style-name="T2">
hello
</text:span>
</text:span>
</text:p>
* The style properties for the word 'hello' are looked for, in order, in this
* list of styles:
* text:T2
* text:Bold
* text:Standard
* text:T1
* text:Standard
* text:C1
* text:Standard
* text:C2
* text:Standard
* paragraph:P1
* paragraph:Standard
* paragraph-document-default
* paragraph-implementation-default
*
* The style names can be concatenated into a key. The parent styles are not
* needed in the key. For the above example, the key is:
* text/T2/text/T1/text/C1/text/C2/paragraph/P1
* StyleCache creates computed style objects on demand and caches them.
*
* StyleCache also provides convenient access to page layout and master page
* information.
* Each property of the style objects has a valid value even if the underlying
* XML element or attribute is missing or invalid.
*
* @constructor
* @implements {odf.MasterPageCache}
* @implements {odf.PageLayoutCache}
* @param {!odf.ODFDocumentElement} odfroot
*/
odf.StyleCache = function (odfroot) {
"use strict";
var self = this,
/**@type{!{text:!odf.StylePile,paragraph:!odf.StylePile}}*/
stylePiles,
/**@type{!Object.<!string,!odf.ComputedTextStyle>}*/
textStyleCache,
/**@type{!Object.<!string,!odf.ComputedParagraphStyle>}*/
paragraphStyleCache,
/**@type{!Object.<!string,!odf.ComputedGraphicStyle>}*/
graphicStyleCache,
/**@type{!odf.StylePile}*/
textStylePile,
/**@type{!odf.StylePile}*/
paragraphStylePile,
/**@type{!odf.StylePile}*/
graphicStylePile,
textns = odf.Namespaces.textns,
stylens = odf.Namespaces.stylens,
styleInfo = new odf.StyleInfo(),
styleParseUtils = new odf.StyleParseUtils(),
/**@type{!Object.<!string,!Element>}*/
masterPages,
/**@type{!Object.<!string,!odf.MasterPage>}*/
parsedMasterPages,
/**@type{!odf.MasterPage}*/
defaultMasterPage,
/**@type{!odf.PageLayout}*/
defaultPageLayout,
/**@type{!Object.<!string,!Element>}*/
pageLayouts,
/**@type{!Object.<!string,!odf.PageLayout>}*/
parsedPageLayouts;
/**
* @param {!string} family
* @param {!string} ns
* @param {!Element} element
* @param {!Array.<!string>} chain
* @return {undefined}
*/
function appendClassNames(family, ns, element, chain) {
var names = element.getAttributeNS(ns, "class-names"),
stylename,
i;
if (names) {
names = names.split(" ");
for (i = 0; i < names.length; i += 1) {
stylename = names[i];
if (stylename) {
chain.push(family);
chain.push(stylename);
}
}
}
}
/**
* @param {!Element} element
* @param {!Array.<!string>} chain
* @return {!Array.<!string>}
*/
function getGraphicStyleChain(element, chain) {
var stylename = styleInfo.getStyleName("graphic", element);
if (stylename !== undefined) {
chain.push("graphic");
chain.push(stylename);
}
return chain;
}
/**
* @param {!Element} element
* @param {!Array.<!string>} chain
* @return {!Array.<!string>}
*/
function getParagraphStyleChain(element, chain) {
var stylename = styleInfo.getStyleName("paragraph", element);
if (stylename !== undefined) {
chain.push("paragraph");
chain.push(stylename);
}
// text:p and text:h can have text:class-names
if (element.namespaceURI === textns &&
(element.localName === "h" || element.localName === "p")) {
appendClassNames("paragraph", textns, element, chain);
}
return chain;
}
/**
* @param {!Array.<!string>} styleChain
* @param {!string} propertiesName
* @param {!string} defaultFamily
* @return {!Array.<!Object>}
*/
function createPropertiesChain(styleChain, propertiesName, defaultFamily) {
var chain = [], i, lastProperties, family, styleName, pile, style,
properties;
for (i = 0; i < styleChain.length; i += 2) {
family = styleChain[i];
styleName = styleChain[i + 1];
pile = /**@type{!odf.StylePile}*/(stylePiles[family]);
style = pile.getStyle(styleName);
if (style !== undefined) {
properties = /**@type{!Object|undefined}*/(style[propertiesName]);
if (properties !== undefined && properties !== lastProperties) {
chain.push(properties);
lastProperties = properties;
}
}
}
pile = /**@type{!odf.StylePile}*/(stylePiles[defaultFamily]);
style = pile.getDefaultStyle();
if (style) {
properties = /**@type{!Object|undefined}*/(style[propertiesName]);
if (properties !== undefined && properties !== lastProperties) {
chain.push(properties);
}
}
return chain;
}
/**
* Return the paragraph style for the given content element.
* @param {!Element} element
* @return {!odf.ComputedGraphicStyle}
*/
this.getComputedGraphicStyle = function (element) {
var styleChain = getGraphicStyleChain(element, []),
key = styleChain.join("/"),
computedStyle = graphicStyleCache[key];
runtime.assert(styleChain.length % 2 === 0, "Invalid style chain.");
if (computedStyle === undefined) {
computedStyle = new odf.ComputedGraphicStyle();
computedStyle.graphic.setGraphicProperties(/**@type{!odf.GraphicProperties|undefined}*/(
createPropertiesChain(styleChain, "graphic", "graphic")[0]
));
computedStyle.text.setStyleChain(/**@type{!Array.<!odf.TextProperties>}*/(
createPropertiesChain(styleChain, "text", "graphic")
));
computedStyle.paragraph.setStyleChain(/**@type{!Array.<!odf.ParagraphProperties>}*/(
createPropertiesChain(styleChain, "paragraph", "graphic")
));
graphicStyleCache[key] = computedStyle;
}
return computedStyle;
};
/**
* Return the paragraph style for the given content element.
* @param {!Element} element
* @return {!odf.ComputedParagraphStyle}
*/
this.getComputedParagraphStyle = function (element) {
var styleChain = getParagraphStyleChain(element, []),
key = styleChain.join("/"),
computedStyle = paragraphStyleCache[key];
runtime.assert(styleChain.length % 2 === 0, "Invalid style chain.");
if (computedStyle === undefined) {
computedStyle = new odf.ComputedParagraphStyle();
computedStyle.text.setStyleChain(/**@type{!Array.<!odf.TextProperties>}*/(
createPropertiesChain(styleChain, "text", "paragraph")
));
computedStyle.paragraph.setStyleChain(/**@type{!Array.<!odf.ParagraphProperties>}*/(
createPropertiesChain(styleChain, "paragraph", "paragraph")
));
paragraphStyleCache[key] = computedStyle;
}
return computedStyle;
};
/**
* @param {!Element} element
* @param {!Array.<!string>} chain
* @return {!Array.<!string>}
*/
function getTextStyleChain(element, chain) {
var stylename = styleInfo.getStyleName("text", element),
parent = /**@type{!Element}*/(element.parentNode);
if (stylename !== undefined) {
chain.push("text");
chain.push(stylename);
}
// a text:span can have text:class-names
if (element.localName === "span" && element.namespaceURI === textns) {
appendClassNames("text", textns, element, chain);
}
if (!parent || parent === odfroot) {
return chain;
}
if (parent.namespaceURI === textns &&
(parent.localName === "p" || parent.localName === "h")) {
getParagraphStyleChain(parent, chain);
} else {
getTextStyleChain(parent, chain);
}
return chain;
}
/**
* Return the text style for the given content element.
* @param {!Element} element
* @return {!odf.ComputedTextStyle}
*/
this.getComputedTextStyle = function (element) {
var styleChain = getTextStyleChain(element, []),
key = styleChain.join("/"),
computedStyle = textStyleCache[key];
runtime.assert(styleChain.length % 2 === 0, "Invalid style chain.");
if (computedStyle === undefined) {
computedStyle = new odf.ComputedTextStyle();
computedStyle.text.setStyleChain(/**@type{!Array.<!odf.TextProperties>}*/(
createPropertiesChain(styleChain, "text", "text")
));
textStyleCache[key] = computedStyle;
}
return computedStyle;
};
/**
* @param {!Element} element
* @return {!odf.StylePile|undefined}
*/
function getPileFromElement(element) {
var family = element.getAttributeNS(stylens, "family");
return stylePiles[family];
}
/**
* @param {!Element} element
* @return {undefined}
*/
function addMasterPage(element) {
var name = element.getAttributeNS(stylens, "name");
if (name.length > 0 && !masterPages.hasOwnProperty(name)) {
masterPages[name] = element;
}
}
/**
* @param {!string} name
* @return {!odf.PageLayout}
*/
function getPageLayout(name) {
var pageLayout = parsedPageLayouts[name], e;
if (!pageLayout) {
e = pageLayouts[name];
if (e) {
pageLayout = new odf.PageLayout(e, styleParseUtils, defaultPageLayout);
parsedPageLayouts[name] = pageLayout;
} else {
pageLayout = defaultPageLayout;
}
}
return pageLayout;
}
this.getPageLayout = getPageLayout;
/**
* @return {!odf.PageLayout}
*/
this.getDefaultPageLayout = function () {
return defaultPageLayout;
};
/**
* @param {!string} name
* @return {?odf.MasterPage}
*/
function getMasterPage(name) {
var masterPage = parsedMasterPages[name],
element;
if (masterPage === undefined) {
element = masterPages[name];
if (element) {
masterPage = new odf.MasterPage(element, self);
parsedMasterPages[name] = masterPage;
} else {
masterPage = null;
}
}
return masterPage;
}
this.getMasterPage = getMasterPage;
/**
* @return {!odf.MasterPage}
*/
this.getDefaultMasterPage = function () {
return defaultMasterPage;
};
/**
* @return {undefined}
*/
function update() {
var e,
pile,
defaultPageLayoutElement = null,
defaultMasterPageElement = null;
textStyleCache = {};
paragraphStyleCache = {};
graphicStyleCache = {};
masterPages = {};
parsedMasterPages = {};
parsedPageLayouts = {};
pageLayouts = {};
textStylePile = new odf.StylePile(styleParseUtils, self);
paragraphStylePile = new odf.StylePile(styleParseUtils, self);
graphicStylePile = new odf.StylePile(styleParseUtils, self);
stylePiles = {
text: textStylePile,
paragraph: paragraphStylePile,
graphic: graphicStylePile
};
// go through <office:styles/>
e = odfroot.styles.firstElementChild;
while (e) {
if (e.namespaceURI === stylens) {
pile = getPileFromElement(e);
if (pile) {
if (e.localName === "style") {
pile.addCommonStyle(e);
} else if (e.localName === "default-style") {
pile.setDefaultStyle(e);
}
} else if (e.localName === "default-page-layout") {
defaultPageLayoutElement = e;
}
}
e = e.nextElementSibling;
}
defaultPageLayout = new odf.PageLayout(defaultPageLayoutElement,
styleParseUtils);
// go through <office:automatic-styles/>
e = odfroot.automaticStyles.firstElementChild;
while (e) {
if (e.namespaceURI === stylens) {
pile = getPileFromElement(e);
if (pile && e.localName === "style") {
pile.addAutomaticStyle(e);
} else if (e.localName === "page-layout") {
pageLayouts[e.getAttributeNS(stylens, "name")] = e;
}
}
e = e.nextElementSibling;
}
// go through <office:master-styles/>
e = odfroot.masterStyles.firstElementChild;
while (e) {
if (e.namespaceURI === stylens && e.localName === "master-page") {
defaultMasterPageElement = defaultMasterPageElement || e;
addMasterPage(e);
}
e = e.nextElementSibling;
}
defaultMasterPage = new odf.MasterPage(defaultMasterPageElement, self);
}
this.update = update;
};