UNPKG

node-webodf

Version:

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

1,062 lines (999 loc) 38.1 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, runtime, odf, NodeFilter, core*/ /** * @constructor */ odf.OdfUtilsImpl = function OdfUtilsImpl() { "use strict"; var /**@const @type{!string}*/ textns = odf.Namespaces.textns, /**@const @type{!string}*/ drawns = odf.Namespaces.drawns, /**@const @type{!string}*/ xlinkns = odf.Namespaces.xlinkns, domUtils = core.DomUtils, // only add odf element namespaces here. // Namespaces solely used for attributes are excluded. eg. fo, xlink & xml odfNodeNamespaceMap = [ odf.Namespaces.dbns, odf.Namespaces.dcns, odf.Namespaces.dr3dns, odf.Namespaces.drawns, odf.Namespaces.chartns, odf.Namespaces.formns, odf.Namespaces.numberns, odf.Namespaces.officens, odf.Namespaces.presentationns, odf.Namespaces.stylens, odf.Namespaces.svgns, odf.Namespaces.tablens, odf.Namespaces.textns ], odfSchema = odf.OdfSchema; /** * Determine if the node is a draw:image element. * @param {?Node} e * @return {!boolean} */ function isImage(e) { var name = e && e.localName; return name === "image" && e.namespaceURI === drawns; } this.isImage = isImage; /** * Determine if the node is a draw:frame element and has its text:anchor-type attribute set to 'as-char'. * @param {?Node} e * @return {!boolean} */ function isCharacterFrame(e) { // TODO the anchor-type can be defined on any style associated with the frame return e !== null && e.nodeType === Node.ELEMENT_NODE && e.localName === "frame" && e.namespaceURI === drawns && /**@type{!Element}*/(e).getAttributeNS(textns, "anchor-type") === "as-char"; } this.isCharacterFrame = isCharacterFrame; /** * Determine if the node is an office:annotation element. * @param {?Node} e * @return {!boolean} */ function isAnnotation(e) { var name = e && e.localName; return name === "annotation" && e.namespaceURI === odf.Namespaces.officens; } /** * Determine if the node is an annotation wrapper element. * @param {?Node} e * @return {!boolean} */ function isAnnotationWrapper(e) { var name = e && e.localName; return name === "div" && /**@type{!HTMLDivElement}*/(e).className === "annotationWrapper"; } /** * Determine if the node is an inline 'root' type, * i.e. an office:annotation or a wrapper for an annotaiton. * @param {?Node} e * @return {!boolean} */ function isInlineRoot(e) { return isAnnotation(e) || isAnnotationWrapper(e); } this.isInlineRoot = isInlineRoot; /** * Determine if the node is a text:span element. * @param {?Node} e * @return {!boolean} */ this.isTextSpan = function (e) { var name = e && e.localName; return name === "span" && e.namespaceURI === textns; }; /** * Determine if the node is a text:a element. * @param {?Node} node * @return {!boolean} */ function isHyperlink(node) { var name = node && node.localName; return name === "a" && node.namespaceURI === textns; } this.isHyperlink = isHyperlink; /** * Gets the href attribute of text:a element * @param {!Element} element * @return {!string} */ this.getHyperlinkTarget = function (element) { return element.getAttributeNS(xlinkns, 'href') || ""; }; /** * Determine if the node is a text:p or a text:h element. * @param {?Node} e * @return {!boolean} */ function isParagraph(e) { var name = e && e.localName; return (name === "p" || name === "h") && e.namespaceURI === textns; } this.isParagraph = isParagraph; /** * Find the paragraph containing the specified node. If an offset is provided and * the node has a child at the specified offset, this will be included in the search * as well if the supplied node is not a paragraph itself. * @param {?Node} node * @param {!number=} offset * @return {?Element} */ function getParagraphElement(node, offset) { if (node && offset !== undefined && !isParagraph(node) && node.childNodes.item(offset)) { node = node.childNodes.item(offset); } while (node && !isParagraph(node)) { node = node.parentNode; } return /**@type{?Element}*/(node); } this.getParagraphElement = getParagraphElement; /** * @param {?Node} node Node to start searching with * @param {!Element} container Root container to stop searching at. * @return {?Element} */ function getParentAnnotation(node, container) { while (node && node !== container) { if (node.namespaceURI === odf.Namespaces.officens && node.localName === 'annotation') { return /**@type{!Element}*/(node); } node = node.parentNode; } return null; } this.getParentAnnotation = getParentAnnotation; /** * @param {?Node} node Node to start searching with * @param {!Element} container Root container to stop searching at. * @return {!boolean} */ this.isWithinAnnotation = function (node, container) { return Boolean(getParentAnnotation(node, container)); }; /** * Gets the creator of an annotation. * @param {!Element} annotationElement * @return {!string} */ this.getAnnotationCreator = function (annotationElement) { var creatorElement = /**@type{!Element}*/(annotationElement.getElementsByTagNameNS(odf.Namespaces.dcns, "creator")[0]); return creatorElement.textContent; }; /** * Determine if the node is a text:list-item element. * @param {?Node} e * @return {!boolean} */ this.isListItem = function (e) { var name = e && e.localName; return name === "list-item" && e.namespaceURI === textns; }; /** * Determine if the node is a text:line-break element. * @param {?Node} e * @return {!boolean} */ this.isLineBreak = function (e) { var name = e && e.localName; return name === "line-break" && e.namespaceURI === textns; }; /** * Determine if the text consists entirely of whitespace characters. * At least one whitespace is required. * @param {!string} text * @return {!boolean} */ function isODFWhitespace(text) { return (/^[ \t\r\n]+$/).test(text); } this.isODFWhitespace = isODFWhitespace; /** * Determine if the node is a grouping element. * @param {?Node} n * @return {!boolean} */ function isGroupingElement(n) { if (n === null || n.nodeType !== Node.ELEMENT_NODE) { return false; } var e = /**@type{!Element}*/(n), localName = e.localName; return odfSchema.isTextContainer(e.namespaceURI, localName) || (localName === "span" && e.className === "webodf-annotationHighlight"); } this.isGroupingElement = isGroupingElement; /** * @param {?Node} n * @return {!boolean} */ function isFieldElement(n) { if (n === null || n.nodeType !== Node.ELEMENT_NODE) { return false; } var e = /**@type{!Element}*/(n), localName = e.localName; return odfSchema.isField(e.namespaceURI, localName); } this.isFieldElement = isFieldElement; /** * Determine if the node is a character element, * namely "s", "tab", or "line-break". * @param {?Node} e * @return {!boolean} */ function isCharacterElement(e) { var n = e && e.localName, ns, r = false; if (n) { ns = e.namespaceURI; if (ns === textns) { r = n === "s" || n === "tab" || n === "line-break"; } } return r; } this.isCharacterElement = isCharacterElement; /** * Determine if the node is an 'as char' type of element, * i.e. any element which behaves like a character with * respect to it's surrounding positions, such as the * space/tab/line-break elements, draw:frames with * anchor type being 'as-char', or inline root elements * such as annotations. * @param {?Node} e * @return {!boolean} */ function isAnchoredAsCharacterElement(e) { return isCharacterElement(e) || isFieldElement(e) || isCharacterFrame(e) || isInlineRoot(e); } this.isAnchoredAsCharacterElement = isAnchoredAsCharacterElement; /** * Determine if the node is a <text:s/> character element. * @param {?Node} e * @return {!boolean} */ function isSpaceElement(e) { var n = e && e.localName, ns, r = false; if (n) { ns = e.namespaceURI; if (ns === textns) { r = n === "s"; } } return r; } this.isSpaceElement = isSpaceElement; /** * Returns true if the given node is an odf node * @param {!Node} node * @return {!boolean} */ function isODFNode(node) { return odfNodeNamespaceMap.indexOf(node.namespaceURI) !== -1; } this.isODFNode = isODFNode; /** * Returns true if the supplied node contains no text-in-ODF, or ODF elements * @param {!Node} node * @return {!boolean} */ function hasNoODFContent(node) { var childNode; if (isCharacterElement(node) || isFieldElement(node)) { return false; } if (isGroupingElement(/**@type{!Node}*/(node.parentNode)) && node.nodeType === Node.TEXT_NODE) { return node.textContent.length === 0; } childNode = node.firstChild; while (childNode) { if (isODFNode(childNode) || !hasNoODFContent(childNode)) { return false; } childNode = childNode.nextSibling; } return true; } this.hasNoODFContent= hasNoODFContent; /** * @param {!Node} node * @return {!Node} */ function firstChild(node) { while (node.firstChild !== null && isGroupingElement(node)) { node = node.firstChild; } return node; } this.firstChild = firstChild; /** * @param {!Node} node * @return {!Node} */ function lastChild(node) { while (node.lastChild !== null && isGroupingElement(node)) { node = node.lastChild; } return node; } this.lastChild = lastChild; /** * @param {!Node} node * @return {?Node} */ function previousNode(node) { while (!isParagraph(node) && node.previousSibling === null) { node = /**@type{!Node}*/(node.parentNode); } return isParagraph(node) ? null : lastChild(/**@type{!Node}*/(node.previousSibling)); } this.previousNode = previousNode; /** * @param {!Node} node * @return {?Node} */ function nextNode(node) { while (!isParagraph(node) && node.nextSibling === null) { node = /**@type{!Node}*/(node.parentNode); } return isParagraph(node) ? null : firstChild(/**@type{!Node}*/(node.nextSibling)); } this.nextNode = nextNode; /** * Walk to the left along the DOM and return true if the first thing * encountered is either a non-whitespace text character or a non-space * character element (i.e., any character element other than <text:s/>). * Walking goes through grouping elements. * @param {?Node} node the first node to scan * @return {!boolean} */ function scanLeftForNonSpace(node) { var r = false, text; while (node) { if (node.nodeType === Node.TEXT_NODE) { text = /**@type{!Text}*/(node); if (text.length === 0) { node = previousNode(text); } else { return !isODFWhitespace( text.data.substr(text.length - 1, 1) ); } } else if (isAnchoredAsCharacterElement(node)) { r = isSpaceElement(node) === false; node = null; } else { node = previousNode(node); } } return r; } this.scanLeftForNonSpace = scanLeftForNonSpace; /** * Walk to the left along the DOM and return the type of the first * thing encountered. * 0 none of the below * 1 non-whitespace character or a character element * 2 whitespace character that is preceded by a non-whitespace character * or a character element * * @param {!Node} node the first node to scan * @return {!number} */ function lookLeftForCharacter(node) { var text, r = 0, tl = 0; if (node.nodeType === Node.TEXT_NODE) { tl = /**@type{!Text}*/(node).length; } if (tl > 0) { text = /**@type{!Text}*/(node).data; if (!isODFWhitespace(text.substr(tl - 1, 1))) { r = 1; // character found } else if (tl === 1) { r = scanLeftForNonSpace(previousNode(node)) ? 2 : 0; } else { r = isODFWhitespace(text.substr(tl - 2, 1)) ? 0 : 2; } } else if (isAnchoredAsCharacterElement(node)) { r = 1; } return r; } this.lookLeftForCharacter = lookLeftForCharacter; /** * Look to the right along the DOM and return true if the first thing * encountered is either a non-whitespace character or a character * element. * * @param {?Node} node the first node to scan * @return {!boolean} */ function lookRightForCharacter(node) { var r = false, l = 0; if (node && node.nodeType === Node.TEXT_NODE) { l = /**@type{!Text}*/(node).length; } if (l > 0) { r = !isODFWhitespace(/**@type{!Text}*/(node).data.substr(0, 1)); } else if (isAnchoredAsCharacterElement(node)) { r = true; } return r; } this.lookRightForCharacter = lookRightForCharacter; /** * Walk to the left along the DOM and return true if either a * non-whitespace character or a character element is encountered. * * @param {?Node} node the first node to scan * @return {!boolean} */ function scanLeftForAnyCharacter(node) { var r = false, l; node = node && lastChild(node); while (node) { if (node.nodeType === Node.TEXT_NODE) { l = /**@type{!Text}*/(node).length; } else { l = 0; } if (l > 0 && !isODFWhitespace(/**@type{!Text}*/(node).data)) { r = true; break; } if (isAnchoredAsCharacterElement(node)) { r = true; break; } node = previousNode(node); } return r; } this.scanLeftForAnyCharacter = scanLeftForAnyCharacter; /** * Walk to the right along the DOM and return true if either a * non-whitespace character or a character element is encountered. * * @param {?Node} node the first node to scan * @return {!boolean} */ function scanRightForAnyCharacter(node) { var r = false, l; node = node && firstChild(node); while (node) { if (node.nodeType === Node.TEXT_NODE) { l = /**@type{!Text}*/(node).length; } else { l = 0; } if (l > 0 && !isODFWhitespace(/**@type{!Text}*/(node).data)) { r = true; break; } if (isAnchoredAsCharacterElement(node)) { r = true; break; } node = nextNode(node); } return r; } this.scanRightForAnyCharacter = scanRightForAnyCharacter; /** * check if the node is part of the trailing whitespace * @param {!Text} textnode * @param {!number} offset * @return {!boolean} */ function isTrailingWhitespace(textnode, offset) { if (!isODFWhitespace(textnode.data.substr(offset))) { return false; } return !scanRightForAnyCharacter(nextNode(textnode)); } this.isTrailingWhitespace = isTrailingWhitespace; /** * Takes a textNode and an offset, and returns true if the character * at that offset is a significant whitespace. * * Significant whitespace is defined as: * - Not part of the leading whitespace block in a paragraph * - Not part of the trailing whitespace block in a paragraph * - The first whitespace character after a text node or character * * All other whitespace elements are considered insignificant * @param {!Text} textNode * @param {!number} offset * @return {!boolean} */ function isSignificantWhitespace(textNode, offset) { var text = textNode.data, result; if (!isODFWhitespace(text[offset])) { // Character is not whitespace return false; } if (isAnchoredAsCharacterElement(textNode.parentNode)) { // Parent is a character element, and therefore does not actually contain text // This prevents a space element from being upgraded again return false; } if (offset > 0) { if (!isODFWhitespace(text[offset - 1])) { // First whitespace after a character is significant result = true; } } else if (scanLeftForNonSpace(previousNode(textNode))) { // If the first character found scanning to the left is non-whitespace, this might still be significant result = true; } if (result === true) { return isTrailingWhitespace(textNode, offset) ? false : true; } return false; } this.isSignificantWhitespace = isSignificantWhitespace; /** * Returns true if the supplied node is a downgradeable space element. * As per http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#element-text_s * a downgradeable whitespace element is a space element that is immediately preceded by something other than a space * and has at least one non-space character after it * @param {!Node} node * @return {!boolean} */ this.isDowngradableSpaceElement = function(node) { if (isSpaceElement(node)) { return scanLeftForNonSpace(previousNode(node)) && scanRightForAnyCharacter(nextNode(node)); } return false; }; /** * Returns the length split as value and unit, from an ODF attribute * @param {?string|undefined} length * @return {?{value:!number,unit:!string}} */ function parseLength(length) { var re = /(-?[0-9]*[0-9][0-9]*(\.[0-9]*)?|0+\.[0-9]*[1-9][0-9]*|\.[0-9]*[1-9][0-9]*)((cm)|(mm)|(in)|(pt)|(pc)|(px)|(%))/, m = re.exec(length); if (!m) { return null; } return {value: parseFloat(m[1]), unit: m[3]}; } this.parseLength = parseLength; /** * Returns the value and unit of the length, if it is positive ( > 0) * @param {?string|undefined} length * @return {?{value:!number,unit:!string}} */ function parsePositiveLength(length) { var result = parseLength(length); if (result && (result.value <= 0 || result.unit === '%')) { return null; } return result; } /** * Returns the value and unit of the length, if it is non-negative ( >= 0) * @param {?string|undefined} length * @return {?{value:!number,unit:!string}} */ function parseNonNegativeLength(length) { var result = parseLength(length); if (result && (result.value < 0 || result.unit === '%')) { return null; } return result; } this.parseNonNegativeLength = parseNonNegativeLength; /** * Returns the value and unit(%) of the length, if it is specified in %age * @param {?string|undefined} length * @return {?{value:!number,unit:!string}} */ function parsePercentage(length) { var result = parseLength(length); if (result && (result.unit !== '%')) { return null; } return result; } /** * Returns the value and unit of the font size, in conformance with fo:font-size * constraints * @param {?string|undefined} fontSize * @return {?{value:!number,unit:!string}} */ function parseFoFontSize(fontSize) { return parsePositiveLength(fontSize) || parsePercentage(fontSize); } this.parseFoFontSize = parseFoFontSize; /** * Returns the value and unit of the line height, in conformance with fo:line-height * constraints * @param {?string|undefined} lineHeight * @return {?{value:!number,unit:!string}} */ function parseFoLineHeight(lineHeight) { return parseNonNegativeLength(lineHeight) || parsePercentage(lineHeight); } this.parseFoLineHeight = parseFoLineHeight; /** * Adapted from instructions on how to generate plain text from an ODT document. * See algorithm at http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1415196_253892949 * @param {!Node} node * @return {boolean} */ function isTextContentContainingNode(node) { switch (node.namespaceURI) { // Namespace skips case odf.Namespaces.drawns: case odf.Namespaces.svgns: case odf.Namespaces.dr3dns: return false; case odf.Namespaces.textns: // Specific node type skips //noinspection FallthroughInSwitchStatementJS switch (node.localName) { case 'note-body': case 'ruby-text': return false; } break; case odf.Namespaces.officens: // Specific node type skips //noinspection FallthroughInSwitchStatementJS switch (node.localName) { case 'annotation': case 'binary-data': case 'event-listeners': return false; } break; default: // Skip webodf edit markers or cursor information switch (node.localName) { case 'cursor': case 'editinfo': return false; } break; } return true; } this.isTextContentContainingNode = isTextContentContainingNode; /** * Returns true if the text node is within a paragraph and contains either non-whitespace characters, or * significant whitespace characters (as defined by the ODF standards). * WARNING this method is likely to be quite slow, so should be used as little as possible * @param {!Text} textNode * @return {!boolean} */ function isSignificantTextContent(textNode) { return Boolean(getParagraphElement(textNode) && (!isODFWhitespace(textNode.textContent) || isSignificantWhitespace(textNode, 0))); } /** * Remove any nodes that aren't fully contained within the supplied range. This function assumes * the nodes appear in document order. * @param {!Range} range * @param {!Array.<!Node>} nodes * @return {undefined} */ function removePartiallyContainedNodes(range, nodes) { while (nodes.length > 0 && !domUtils.rangeContainsNode(range, /**@type{!Node}*/(nodes[0]))) { nodes.shift(); } while (nodes.length > 0 && !domUtils.rangeContainsNode(range, /**@type{!Node}*/(nodes[nodes.length - 1]))) { nodes.pop(); } } /** * Returns a array of text nodes considered to be part of the supplied range. * This will exclude elements that are not part of the ODT main text body, * as well as insignificant whitespace text nodes. * @param {!Range} range Range to search for nodes within * @param {boolean} includePartial Include partially intersecting text nodes * in the result. * @return {!Array.<!Node>} */ function getTextNodes(range, includePartial) { var textNodes; /** * @param {!Node} node * @return {number} */ function nodeFilter(node) { var result = NodeFilter.FILTER_REJECT; if (node.nodeType === Node.TEXT_NODE) { if (isSignificantTextContent(/**@type{!Text}*/(node))) { result = NodeFilter.FILTER_ACCEPT; } } else if (isTextContentContainingNode(node)) { result = NodeFilter.FILTER_SKIP; } return result; } /*jslint bitwise:true*/ textNodes = domUtils.getNodesInRange(range, nodeFilter, NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT); /*jslint bitwise:false*/ if (!includePartial) { removePartiallyContainedNodes(range, textNodes); } return textNodes; } this.getTextNodes = getTextNodes; /** * Get all character elements and text nodes fully contained within the * supplied range in document order. * * For example, given the following fragment, with the range starting at b, * and ending at c: * <text:p>ab<text:s/>cd</text:p> * this function would return the following array: * ["b", text:s, "c"] * @param {!Range} range * @param {!boolean} includePartial Include partially intersecting text & * character nodes in the result. * @param {!boolean} includeInsignificantWhitespace Include whitespace only * nodes that are not considered significant text * content. This includes whitespace only elements * used in pretty-formatted xml as LibreOffice * produces in flat ODT files. * @return {!Array.<!Element|!Text>} */ function getTextElements(range, includePartial, includeInsignificantWhitespace) { var elements; /** * @param {!Node} node * @return {number} */ function nodeFilter(node) { var result = NodeFilter.FILTER_REJECT; // do not return anything inside an character element or an inline root such as an annotation if (isCharacterElement(node.parentNode) || isFieldElement(node.parentNode) || isInlineRoot(node)) { result = NodeFilter.FILTER_REJECT; } else if (node.nodeType === Node.TEXT_NODE) { if (includeInsignificantWhitespace || isSignificantTextContent(/**@type{!Text}*/(node))) { // Text nodes should only be returned if they are // fully contained within the range. result = NodeFilter.FILTER_ACCEPT; } } else if (isAnchoredAsCharacterElement(node)) { // Character elements should only be returned if they are // fully contained within the range. result = NodeFilter.FILTER_ACCEPT; } else if (isTextContentContainingNode(node) || isGroupingElement(node)) { result = NodeFilter.FILTER_SKIP; } return result; } /*jslint bitwise:true*/ elements = domUtils.getNodesInRange(range, nodeFilter, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); /*jslint bitwise:false*/ if (!includePartial) { removePartiallyContainedNodes(range, elements); } return elements; } this.getTextElements = getTextElements; /** * Crawl parent nodes starting at the startContainer until a matching node is found, * or the first inline root is met. If a node is accepted by the supplied filter, and is * not already the first element in the elements array, this node is prepended to the start * of the elements array. * * @param {!Node} startContainer Container to start search from (inclusive) * @param {!Array.<!Node>} elements Existing elements already discovered * @param {!function(!Node):!boolean} filter */ function prependParentContainers(startContainer, elements, filter) { var container = startContainer; while (container) { if (filter(container)) { if (elements[0] !== container) { elements.unshift(container); } break; } if (isInlineRoot(container)) { break; } container = container.parentNode; } } /** * Get all paragraph elements that intersect the supplied range in document * order. * * For example, given the following fragment, with the range starting at b, * and ending at c: * <text:p id="A">ab</text:p><text:p id="B"><text:s/>cd</text:p> * this function would return the following array: * [text:p{id="A"}, text:p{id="B"}] * @param {!Range} range * @return {!Array.<!Element>} */ this.getParagraphElements = function (range) { var elements; /** * @param {!Node} node * @return {number} */ function nodeFilter(node) { var result = NodeFilter.FILTER_REJECT; if (isParagraph(node)) { result = NodeFilter.FILTER_ACCEPT; } else if (isTextContentContainingNode(node) || isGroupingElement(node)) { result = NodeFilter.FILTER_SKIP; } return result; } elements = domUtils.getNodesInRange(range, nodeFilter, NodeFilter.SHOW_ELEMENT); // getNodesInRange will only return nodes it enters during the iteration. // However, we desire all paragraph nodes either contained OR containing this range, // so we crawl the parentNodes of the start container until a root is found. // Note, this isn't necessary for the end container because iteration crosses the // node boundary when entering towards the end container, meaning all paragraphs in // the end container's parentNodes will be reported by getNodesInRange. prependParentContainers(/**@type{!Node}*/(range.startContainer), elements, isParagraph); return elements; }; /** * Get all image elements that fully contained within the supplied range in * document order. * @param {!Range} range * @return {!Array.<Node>} */ this.getImageElements = function (range) { var elements; /** * @param {!Node} node * @return {number} */ function nodeFilter(node) { var result = NodeFilter.FILTER_SKIP; if (isImage(node)) { result = NodeFilter.FILTER_ACCEPT; } return result; } elements = domUtils.getNodesInRange(range, nodeFilter, NodeFilter.SHOW_ELEMENT); // See description in getParagraphElements as to why this is necessary // Short summary: want to include images that completely contain this range prependParentContainers(/**@type{!Node}*/(range.startContainer), elements, isImage); return elements; }; /** * Returns the node right after the given point. * @param {!Node} container * @param {!number} offset * @return {!Node} */ function getRightNode(container, offset) { var node = container; if (offset < node.childNodes.length - 1) { node = /** @type {!Node} */(node.childNodes[offset + 1]); } else { while (!node.nextSibling) { node = node.parentNode; } node = node.nextSibling; } while (node.firstChild) { node = node.firstChild; } return node; } /** * Get all hyperlink elements that intersect the supplied range in document order * * For example, given the following fragment, with the range starting at b, and ending at c: * <text:a xlink:href="google">ab</text:a><text:a xlink:href="apple">cd</text:a> * this function would return the following array: * [text:a{xlink:href="google"}, text:a{xlink:href="apple"}] * @param {!Range} range * @return {!Array.<Node>} */ this.getHyperlinkElements = function (range) { var links = [], newRange = /** @type {!Range}*/(range.cloneRange()), node, textNodes; if (range.collapsed && range.endContainer.nodeType === Node.ELEMENT_NODE) { node = getRightNode(range.endContainer, range.endOffset); if (node.nodeType === Node.TEXT_NODE) { newRange.setEnd(node, 1); } } textNodes = getTextElements(newRange, true, false); textNodes.forEach(function (node) { var parent = node.parentNode; while (!isParagraph(parent)) { if (isHyperlink(parent) && links.indexOf(parent) === -1) { links.push(parent); break; } parent = parent.parentNode; } }); newRange.detach(); return links; }; /** * Normalize the font-family name as defined in * http://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#propdef-font-family * (see there text behind: "There are two types of font family names: <family-name>") * @param {!string} fontFamilyName * @return {!string} */ /*jslint regexp: true*/ this.getNormalizedFontFamilyName = function(fontFamilyName) { // not quoted with either single- or double-quotes? // (\n & \r are syntactically okay as whitespaces, so need to be accepted as well) // ^(["']) -> match either " or ' at begin (and store match) // (?:.|[\n\r])*? -> match non-greedy any number of any char or \r and \n // \1$ -> match content of first match at end if (!(/^(["'])(?:.|[\n\r])*?\1$/).test(fontFamilyName)) { // remove any whitespaces at begin and end of full name (ignore internal yet) // ^[ \t\r\n\f]* -> match whitespace at begin // ((?:.|[\n\r])*?) -> match non-greedy any number of any char or \r and \n (and store match) // [ \t\r\n\f]*$ -> match whitespace at end fontFamilyName = fontFamilyName.replace(/^[ \t\r\n\f]*((?:.|[\n\r])*?)[ \t\r\n\f]*$/, "$1"); // if there is any internal whitespace, reduce it to just one normal whitespace per group // and add quotes around the full name // (quotes should be only added if there is whitespace inside, as the passed fontFamilyName could // be a generic-family one, which must not be quoted) if ((/[ \t\r\n\f]/).test(fontFamilyName)) { fontFamilyName = "'" + fontFamilyName.replace(/[ \t\r\n\f]+/g, " ") + "'"; } } return fontFamilyName; }; /*jslint regexp: false*/ }; /** * @type {!odf.OdfUtilsImpl} */ odf.OdfUtils = new odf.OdfUtilsImpl();