1 /** 2 * Copyright (C) 2012 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, runtime, xmldom*/ 26 27 /** 28 * RelaxNG can check a DOM tree against a Relax NG schema 29 * The RelaxNG implementation is currently not complete. Relax NG should not 30 * report errors on valid DOM trees, but it will not check all constraints that 31 * a Relax NG file can define. The current implementation does not load external 32 * parts of a Relax NG file. 33 * The main purpose of this Relax NG engine is to validate runtime ODF 34 * documents. The DOM tree is traversed via a TreeWalker. A custom TreeWalker 35 * implementation can hide parts of a DOM tree. This is useful in WebODF, where 36 * special elements and attributes in the runtime DOM tree. 37 */ 38 /** 39 * @constructor 40 */ 41 xmldom.RelaxNG2 = function RelaxNG2() { 42 "use strict"; 43 var start, 44 validateNonEmptyPattern, 45 nsmap; 46 47 /** 48 * @constructor 49 * @param {!string} error 50 * @param {Node=} context 51 */ 52 function RelaxNGParseError(error, context) { 53 this.message = function () { 54 if (context) { 55 error += (context.nodeType === Node.ELEMENT_NODE) ? " Element " : " Node "; 56 error += context.nodeName; 57 if (context.nodeValue) { 58 error += " with value '" + context.nodeValue + "'"; 59 } 60 error += "."; 61 } 62 return error; 63 }; 64 // runtime.log("[" + p.slice(0, depth) + this.message() + "]"); 65 } 66 /** 67 * @param elementdef 68 * @param walker 69 * @param {Element} element 70 * @return {Array.<RelaxNGParseError>} 71 */ 72 function validateOneOrMore(elementdef, walker, element) { 73 // The list of definitions in the elements list should be completely 74 // traversed at least once. If a second or later round fails, the walker 75 // should go back to the start of the last successful traversal 76 var node, i = 0, err; 77 do { 78 node = walker.currentNode; 79 err = validateNonEmptyPattern(elementdef.e[0], walker, element); 80 i += 1; 81 } while (!err && node !== walker.currentNode); 82 if (i > 1) { // at least one round was without error 83 // set position back to position of before last failed round 84 walker.currentNode = node; 85 return null; 86 } 87 return err; 88 } 89 /** 90 * @param {!Node} node 91 * @return {!string} 92 */ 93 function qName(node) { 94 return nsmap[node.namespaceURI] + ":" + node.localName; 95 } 96 /** 97 * @param {!Node} node 98 * @return {!boolean} 99 */ 100 function isWhitespace(node) { 101 return node && node.nodeType === Node.TEXT_NODE && /^\s+$/.test(node.nodeValue); 102 } 103 /** 104 * @param elementdef 105 * @param walker 106 * @param {Element} element 107 * @param {string=} data 108 * @return {Array.<RelaxNGParseError>} 109 */ 110 function validatePattern(elementdef, walker, element, data) { 111 if (elementdef.name === "empty") { 112 return null; 113 } 114 return validateNonEmptyPattern(elementdef, walker, element, data); 115 } 116 /** 117 * @param elementdef 118 * @param walker 119 * @param {Element} element 120 * @return {Array.<RelaxNGParseError>} 121 */ 122 function validateAttribute(elementdef, walker, element) { 123 if (elementdef.e.length !== 2) { 124 throw "Attribute with wrong # of elements: " + elementdef.e.length; 125 } 126 var att, a, l = elementdef.localnames.length, i; 127 for (i = 0; i < l; i += 1) { 128 // with older browsers getAttributeNS for a non-existing attribute 129 // can return an empty string still, so explicitly check before 130 // if the attribute is set 131 if (element.hasAttributeNS(elementdef.namespaces[i], elementdef.localnames[i])) { 132 a = element.getAttributeNS(elementdef.namespaces[i], elementdef.localnames[i]); 133 } else { 134 a = undefined; 135 } 136 137 if (att !== undefined && a !== undefined) { 138 return [new RelaxNGParseError("Attribute defined too often.", 139 element)]; 140 } 141 att = a; 142 } 143 if (att === undefined) { 144 return [new RelaxNGParseError("Attribute not found: " + 145 elementdef.names, element)]; 146 } 147 return validatePattern(elementdef.e[1], walker, element, att); 148 } 149 /** 150 * @param elementdef 151 * @param walker 152 * @param {Element} element 153 * @return {Array.<RelaxNGParseError>} 154 */ 155 function validateTop(elementdef, walker, element) { 156 // notAllowed not implemented atm 157 return validatePattern(elementdef, walker, element); 158 } 159 /** 160 * Validate an element. 161 * Function forwards the walker until an element is met. 162 * If element if of the right type, it is entered and the validation 163 * continues inside the element. After validation, regardless of whether an 164 * error occurred, the walker is at the same depth in the dom tree. 165 * @param elementdef 166 * @param walker 167 * @return {Array.<RelaxNGParseError>} 168 */ 169 function validateElement(elementdef, walker) { 170 if (elementdef.e.length !== 2) { 171 throw "Element with wrong # of elements: " + elementdef.e.length; 172 } 173 // forward until an element is seen, then check the name 174 var /**@type{Node}*/ node = walker.currentNode, 175 /**@type{number}*/ type = node ? node.nodeType : 0, 176 error = null; 177 // find the next element, skip text nodes with only whitespace 178 while (type > Node.ELEMENT_NODE) { 179 if (type !== Node.COMMENT_NODE && 180 (type !== Node.TEXT_NODE || 181 !/^\s+$/.test(walker.currentNode.nodeValue))) { 182 return [new RelaxNGParseError("Not allowed node of type " + 183 type + ".")]; 184 } 185 node = walker.nextSibling(); 186 type = node ? node.nodeType : 0; 187 } 188 if (!node) { 189 return [new RelaxNGParseError("Missing element " + 190 elementdef.names)]; 191 } 192 if (elementdef.names && elementdef.names.indexOf(qName(node)) === -1) { 193 return [new RelaxNGParseError("Found " + node.nodeName + 194 " instead of " + elementdef.names + ".", node)]; 195 } 196 // the right element was found, now parse the contents 197 if (walker.firstChild()) { 198 // currentNode now points to the first child node of this element 199 error = validateTop(elementdef.e[1], walker, node); 200 // there should be no content left 201 while (walker.nextSibling()) { 202 type = walker.currentNode.nodeType; 203 if (!isWhitespace(walker.currentNode) && type !== Node.COMMENT_NODE) { 204 return [new RelaxNGParseError("Spurious content.", 205 walker.currentNode)]; 206 } 207 } 208 if (walker.parentNode() !== node) { 209 return [new RelaxNGParseError("Implementation error.")]; 210 } 211 } else { 212 error = validateTop(elementdef.e[1], walker, node); 213 } 214 // move to the next node 215 node = walker.nextSibling(); 216 return error; 217 } 218 /** 219 * @param elementdef 220 * @param walker 221 * @param {Element} element 222 * @param {string=} data 223 * @return {Array.<RelaxNGParseError>} 224 */ 225 function validateChoice(elementdef, walker, element, data) { 226 // loop through child definitions and return if a match is found 227 if (elementdef.e.length !== 2) { 228 throw "Choice with wrong # of options: " + elementdef.e.length; 229 } 230 var node = walker.currentNode, err; 231 // if the first option is empty, just check the second one for debugging 232 // but the total choice is alwasy ok 233 if (elementdef.e[0].name === "empty") { 234 err = validateNonEmptyPattern(elementdef.e[1], walker, element, 235 data); 236 if (err) { 237 walker.currentNode = node; 238 } 239 return null; 240 } 241 err = validatePattern(elementdef.e[0], walker, element, data); 242 if (err) { 243 walker.currentNode = node; 244 err = validateNonEmptyPattern(elementdef.e[1], walker, element, 245 data); 246 } 247 return err; 248 } 249 /** 250 * @param elementdef 251 * @param walker 252 * @param {Element} element 253 * @return {Array.<RelaxNGParseError>} 254 */ 255 function validateInterleave(elementdef, walker, element) { 256 var l = elementdef.e.length, n = [l], err, i, todo = l, 257 donethisround, node, subnode, e; 258 // the interleave is done when all items are 'true' and no 259 while (todo > 0) { 260 donethisround = 0; 261 node = walker.currentNode; 262 for (i = 0; i < l; i += 1) { 263 subnode = walker.currentNode; 264 if (n[i] !== true && n[i] !== subnode) { 265 e = elementdef.e[i]; 266 err = validateNonEmptyPattern(e, walker, element); 267 if (err) { 268 walker.currentNode = subnode; 269 if (n[i] === undefined) { 270 n[i] = false; 271 } 272 } else if (subnode === walker.currentNode || 273 // this is a bit dodgy, there should be a rule to 274 // see if multiple elements are allowed 275 e.name === "oneOrMore" || 276 (e.name === "choice" && 277 (e.e[0].name === "oneOrMore" || 278 e.e[1].name === "oneOrMore"))) { 279 donethisround += 1; 280 n[i] = subnode; // no error and try this one again later 281 } else { 282 donethisround += 1; 283 n[i] = true; // no error and progress 284 } 285 } 286 } 287 if (node === walker.currentNode && donethisround === todo) { 288 return null; 289 } 290 if (donethisround === 0) { 291 for (i = 0; i < l; i += 1) { 292 if (n[i] === false) { 293 return [new RelaxNGParseError( 294 "Interleave does not match.", element 295 )]; 296 } 297 } 298 return null; 299 } 300 todo = 0; 301 for (i = 0; i < l; i += 1) { 302 if (n[i] !== true) { 303 todo += 1; 304 } 305 } 306 } 307 return null; 308 } 309 /** 310 * @param elementdef 311 * @param walker 312 * @param {Element} element 313 * @return {Array.<RelaxNGParseError>} 314 */ 315 function validateGroup(elementdef, walker, element) { 316 if (elementdef.e.length !== 2) { 317 throw "Group with wrong # of members: " + elementdef.e.length; 318 } 319 //runtime.log(elementdef.e[0].name + " " + elementdef.e[1].name); 320 return validateNonEmptyPattern(elementdef.e[0], walker, element) || 321 validateNonEmptyPattern(elementdef.e[1], walker, element); 322 } 323 /*jslint unparam: true*/ 324 /** 325 * @param elementdef 326 * @param walker 327 * @param {Element} element 328 * @return {Array.<RelaxNGParseError>} 329 */ 330 function validateText(elementdef, walker, element) { 331 var /**@type{Node}*/ node = walker.currentNode, 332 /**@type{number}*/ type = node ? node.nodeType : 0; 333 // find the next element, skip text nodes with only whitespace 334 while (node !== element && type !== 3) { 335 if (type === 1) { 336 return [new RelaxNGParseError( 337 "Element not allowed here.", node 338 )]; 339 } 340 node = walker.nextSibling(); 341 type = node ? node.nodeType : 0; 342 } 343 walker.nextSibling(); 344 return null; 345 } 346 /*jslint unparam: false*/ 347 /** 348 * @param elementdef 349 * @param walker 350 * @param {Element} element 351 * @param {string=} data 352 * @return {Array.<RelaxNGParseError>} 353 */ 354 validateNonEmptyPattern = function validateNonEmptyPattern(elementdef, 355 walker, element, data) { 356 var name = elementdef.name, err = null; 357 if (name === "text") { 358 err = validateText(elementdef, walker, element); 359 } else if (name === "data") { 360 err = null; // data not implemented 361 } else if (name === "value") { 362 if (data !== elementdef.text) { 363 err = [new RelaxNGParseError("Wrong value, should be '" + 364 elementdef.text + "', not '" + data + "'", element)]; 365 } 366 } else if (name === "list") { 367 err = null; // list not implemented 368 } else if (name === "attribute") { 369 err = validateAttribute(elementdef, walker, element); 370 } else if (name === "element") { 371 err = validateElement(elementdef, walker); 372 } else if (name === "oneOrMore") { 373 err = validateOneOrMore(elementdef, walker, element); 374 } else if (name === "choice") { 375 err = validateChoice(elementdef, walker, element, data); 376 } else if (name === "group") { 377 err = validateGroup(elementdef, walker, element); 378 } else if (name === "interleave") { 379 err = validateInterleave(elementdef, walker, element); 380 } else { 381 throw name + " not allowed in nonEmptyPattern."; 382 } 383 return err; 384 }; 385 /** 386 * Validate the elements pointed to by the TreeWalker 387 * @param {!TreeWalker} walker 388 * @param {!function(Array.<RelaxNGParseError>):undefined} callback 389 * @return {undefined} 390 */ 391 this.validate = function validate(walker, callback) { 392 walker.currentNode = walker.root; 393 var errors = validatePattern(start.e[0], walker, 394 /**@type{?Element}*/(walker.root)); 395 callback(errors); 396 }; 397 this.init = function init(start1, nsmap1) { 398 start = start1; 399 nsmap = nsmap1; 400 }; 401 }; 402