]> git.gir.st - LegacyFox.git/blob - legacy/RDFDataSource.sys.mjs
provide easy way to find modifications to BootstrapLoader
[LegacyFox.git] / legacy / RDFDataSource.sys.mjs
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 /**
6 * This module creates a new API for accessing and modifying RDF graphs. The
7 * goal is to be able to serialise the graph in a human readable form. Also
8 * if the graph was originally loaded from an RDF/XML the serialisation should
9 * closely match the original with any new data closely following the existing
10 * layout. The output should always be compatible with Mozilla's RDF parser.
11 *
12 * This is all achieved by using a DOM Document to hold the current state of the
13 * graph in XML form. This can be initially loaded and parsed from disk or
14 * a blank document used for an empty graph. As assertions are added to the
15 * graph, appropriate DOM nodes are added to the document to represent them
16 * along with any necessary whitespace to properly layout the XML.
17 *
18 * In general the order of adding assertions to the graph will impact the form
19 * the serialisation takes. If a resource is first added as the object of an
20 * assertion then it will eventually be serialised inside the assertion's
21 * property element. If a resource is first added as the subject of an assertion
22 * then it will be serialised at the top level of the XML.
23 */
24
25 const NS_XML = "http://www.w3.org/XML/1998/namespace";
26 const NS_XMLNS = "http://www.w3.org/2000/xmlns/";
27 const NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
28 const NS_NC = "http://home.netscape.com/NC-rdf#";
29
30 /* eslint prefer-template: 1 */
31
32 function raw(strings) {
33 return strings.raw[0].replace(/\s+/, "");
34 }
35
36 // Copied from http://www.w3.org/TR/2000/REC-xml-20001006#CharClasses
37 const XML_LETTER = raw`
38 \u0041-\u005A\u0061-\u007A\u00C0-\u00D6\u00D8-\u00F6
39 \u00F8-\u00FF\u0100-\u0131\u0134-\u013E\u0141-\u0148
40 \u014A-\u017E\u0180-\u01C3\u01CD-\u01F0\u01F4-\u01F5
41 \u01FA-\u0217\u0250-\u02A8\u02BB-\u02C1\u0386\u0388-\u038A
42 \u038C\u038E-\u03A1\u03A3-\u03CE\u03D0-\u03D6\u03DA\u03DC
43 \u03DE\u03E0\u03E2-\u03F3\u0401-\u040C\u040E-\u044F
44 \u0451-\u045C\u045E-\u0481\u0490-\u04C4\u04C7-\u04C8
45 \u04CB-\u04CC\u04D0-\u04EB\u04EE-\u04F5\u04F8-\u04F9
46 \u0531-\u0556\u0559\u0561-\u0586\u05D0-\u05EA\u05F0-\u05F2
47 \u0621-\u063A\u0641-\u064A\u0671-\u06B7\u06BA-\u06BE
48 \u06C0-\u06CE\u06D0-\u06D3\u06D5\u06E5-\u06E6\u0905-\u0939
49 \u093D\u0958-\u0961\u0985-\u098C\u098F-\u0990\u0993-\u09A8
50 \u09AA-\u09B0\u09B2\u09B6-\u09B9\u09DC-\u09DD\u09DF-\u09E1
51 \u09F0-\u09F1\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28
52 \u0A2A-\u0A30\u0A32-\u0A33\u0A35-\u0A36\u0A38-\u0A39
53 \u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8B\u0A8D
54 \u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3
55 \u0AB5-\u0AB9\u0ABD\u0AE0\u0B05-\u0B0C\u0B0F-\u0B10
56 \u0B13-\u0B28\u0B2A-\u0B30\u0B32-\u0B33\u0B36-\u0B39
57 \u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B85-\u0B8A\u0B8E-\u0B90
58 \u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F\u0BA3-\u0BA4
59 \u0BA8-\u0BAA\u0BAE-\u0BB5\u0BB7-\u0BB9\u0C05-\u0C0C
60 \u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39
61 \u0C60-\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8
62 \u0CAA-\u0CB3\u0CB5-\u0CB9\u0CDE\u0CE0-\u0CE1\u0D05-\u0D0C
63 \u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39\u0D60-\u0D61
64 \u0E01-\u0E2E\u0E30\u0E32-\u0E33\u0E40-\u0E45\u0E81-\u0E82
65 \u0E84\u0E87-\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F
66 \u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA-\u0EAB\u0EAD-\u0EAE\u0EB0
67 \u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0F40-\u0F47\u0F49-\u0F69
68 \u10A0-\u10C5\u10D0-\u10F6\u1100\u1102-\u1103\u1105-\u1107
69 \u1109\u110B-\u110C\u110E-\u1112\u113C\u113E\u1140\u114C
70 \u114E\u1150\u1154-\u1155\u1159\u115F-\u1161\u1163\u1165
71 \u1167\u1169\u116D-\u116E\u1172-\u1173\u1175\u119E\u11A8
72 \u11AB\u11AE-\u11AF\u11B7-\u11B8\u11BA\u11BC-\u11C2\u11EB
73 \u11F0\u11F9\u1E00-\u1E9B\u1EA0-\u1EF9\u1F00-\u1F15
74 \u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57
75 \u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC
76 \u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB
77 \u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2126\u212A-\u212B
78 \u212E\u2180-\u2182\u3041-\u3094\u30A1-\u30FA\u3105-\u312C
79 \uAC00-\uD7A3\u4E00-\u9FA5\u3007\u3021-\u3029
80 `;
81 const XML_DIGIT = raw`
82 \u0030-\u0039\u0660-\u0669\u06F0-\u06F9\u0966-\u096F
83 \u09E6-\u09EF\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F
84 \u0BE7-\u0BEF\u0C66-\u0C6F\u0CE6-\u0CEF\u0D66-\u0D6F
85 \u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F29
86 `;
87 const XML_COMBINING = raw`
88 \u0300-\u0345\u0360-\u0361\u0483-\u0486\u0591-\u05A1
89 \u05A3-\u05B9\u05BB-\u05BD\u05BF\u05C1-\u05C2\u05C4
90 \u064B-\u0652\u0670\u06D6-\u06DC\u06DD-\u06DF\u06E0-\u06E4
91 \u06E7-\u06E8\u06EA-\u06ED\u0901-\u0903\u093C\u093E-\u094C
92 \u094D\u0951-\u0954\u0962-\u0963\u0981-\u0983\u09BC\u09BE
93 \u09BF\u09C0-\u09C4\u09C7-\u09C8\u09CB-\u09CD\u09D7
94 \u09E2-\u09E3\u0A02\u0A3C\u0A3E\u0A3F\u0A40-\u0A42
95 \u0A47-\u0A48\u0A4B-\u0A4D\u0A70-\u0A71\u0A81-\u0A83
96 \u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0B01-\u0B03
97 \u0B3C\u0B3E-\u0B43\u0B47-\u0B48\u0B4B-\u0B4D\u0B56-\u0B57
98 \u0B82-\u0B83\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7
99 \u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D
100 \u0C55-\u0C56\u0C82-\u0C83\u0CBE-\u0CC4\u0CC6-\u0CC8
101 \u0CCA-\u0CCD\u0CD5-\u0CD6\u0D02-\u0D03\u0D3E-\u0D43
102 \u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0E31\u0E34-\u0E3A
103 \u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB-\u0EBC\u0EC8-\u0ECD
104 \u0F18-\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84
105 \u0F86-\u0F8B\u0F90-\u0F95\u0F97\u0F99-\u0FAD\u0FB1-\u0FB7
106 \u0FB9\u20D0-\u20DC\u20E1\u302A-\u302F\u3099\u309A
107 `;
108 const XML_EXTENDER = raw`
109 \u00B7\u02D0\u02D1\u0387\u0640\u0E46\u0EC6\u3005
110 \u3031-\u3035\u309D-\u309E\u30FC-\u30FE
111 `;
112 const XML_NCNAMECHAR = String.raw`${XML_LETTER}${XML_DIGIT}\.\-_${XML_COMBINING}${XML_EXTENDER}`;
113 const XML_NCNAME = new RegExp(`^[${XML_LETTER}_][${XML_NCNAMECHAR}]*$`);
114
115 const URI_SUFFIX = /[A-Za-z_][0-9A-Za-z\.\-_]*$/;
116 const INDENT = /\n([ \t]*)$/;
117 const RDF_LISTITEM = /^http:\/\/www.w3.org\/1999\/02\/22-rdf-syntax-ns#_\d+$/;
118
119 const RDF_NODE_INVALID_TYPES =
120 ["RDF", "ID", "about", "bagID", "parseType", "resource", "nodeID",
121 "li", "aboutEach", "aboutEachPrefix"];
122 const RDF_PROPERTY_INVALID_TYPES =
123 ["Description", "RDF", "ID", "about", "bagID", "parseType", "resource",
124 "nodeID", "aboutEach", "aboutEachPrefix"];
125
126 /**
127 * Whether to use properly namespaces attributes for rdf:about etc...
128 * When on this produces poor output in the event that the rdf namespace is the
129 * default namespace, and the parser recognises unnamespaced attributes and
130 * most of our rdf examples are unnamespaced so leaving off for the time being.
131 */
132 const USE_RDFNS_ATTR = false;
133
134 var EXPORTED_SYMBOLS = ["RDFLiteral", "RDFIntLiteral", "RDFDateLiteral",
135 "RDFBlankNode", "RDFResource", "RDFDataSource"];
136
137 function isAttr(obj) {
138 return obj && typeof obj == "object" && ChromeUtils.getClassName(obj) == "Attr";
139 }
140 function isDocument(obj) {
141 return obj && typeof obj == "object" && obj.nodeType == Element.DOCUMENT_NODE;
142 }
143 function isElement(obj) {
144 return Element.isInstance(obj);
145 }
146 function isText(obj) {
147 return obj && typeof obj == "object" && ChromeUtils.getClassName(obj) == "Text";
148 }
149
150 /**
151 * Logs an error message to the error console
152 */
153 function ERROR(str) {
154 Cu.reportError(str);
155 }
156
157 function RDF_R(name) {
158 return NS_RDF + name;
159 }
160
161 function renameNode(domnode, namespaceURI, qname) {
162 if (isElement(domnode)) {
163 var newdomnode = domnode.ownerDocument.createElementNS(namespaceURI, qname);
164 if ("listCounter" in domnode)
165 newdomnode.listCounter = domnode.listCounter;
166 domnode.replaceWith(newdomnode);
167 while (domnode.firstChild)
168 newdomnode.appendChild(domnode.firstChild);
169 for (let attr of domnode.attributes) {
170 domnode.removeAttributeNode(attr);
171 newdomnode.setAttributeNode(attr);
172 }
173 return newdomnode;
174 } else if (isAttr(domnode)) {
175 if (domnode.ownerElement.hasAttribute(namespaceURI, qname))
176 throw new Error("attribute already exists");
177 var attr = domnode.ownerDocument.createAttributeNS(namespaceURI, qname);
178 attr.value = domnode.value;
179 domnode.ownerElement.setAttributeNode(attr);
180 domnode.ownerElement.removeAttributeNode(domnode);
181 return attr;
182 }
183 throw new Error("cannot rename node of this type");
184 }
185
186 function predicateOrder(a, b) {
187 return a.getPredicate().localeCompare(b.getPredicate());
188 }
189
190 /**
191 * Returns either an rdf namespaced attribute or an un-namespaced attribute
192 * value. Returns null if neither exists,
193 */
194 function getRDFAttribute(element, name) {
195 if (element.hasAttributeNS(NS_RDF, name))
196 return element.getAttributeNS(NS_RDF, name);
197 if (element.hasAttribute(name))
198 return element.getAttribute(name);
199 return undefined;
200 }
201
202 /**
203 * Represents an assertion in the datasource
204 */
205 class RDFAssertion {
206 constructor(subject, predicate, object) {
207 if (!(subject instanceof RDFSubject))
208 throw new Error("subject must be an RDFSubject");
209
210 if (typeof(predicate) != "string")
211 throw new Error("predicate must be a string URI");
212
213 if (!(object instanceof RDFLiteral) && !(object instanceof RDFSubject))
214 throw new Error("object must be a concrete RDFNode");
215
216 if (object instanceof RDFSubject && object._ds != subject._ds)
217 throw new Error("object must be from the same datasource as subject");
218
219 // The subject on this assertion, an RDFSubject
220 this._subject = subject;
221 // The predicate, a string
222 this._predicate = predicate;
223 // The object, an RDFNode
224 this._object = object;
225 // The datasource this assertion exists in
226 this._ds = this._subject._ds;
227 // Marks that _DOMnode is the subject's element
228 this._isSubjectElement = false;
229 // The DOM node that represents this assertion. Could be a property element,
230 // a property attribute or the subject's element for rdf:type
231 this._DOMNode = null;
232 }
233
234 /**
235 * Adds content to _DOMnode to store this assertion in the DOM document.
236 */
237 _applyToDOMNode() {
238 if (this._object instanceof RDFLiteral)
239 this._object._applyToDOMNode(this._ds, this._DOMnode);
240 else
241 this._object._addReferenceToElement(this._DOMnode);
242 }
243
244 /**
245 * Returns the DOM Element linked to the subject that this assertion is
246 * attached to.
247 */
248 _getSubjectElement() {
249 if (isAttr(this._DOMnode))
250 return this._DOMnode.ownerElement;
251 if (this._isSubjectElement)
252 return this._DOMnode;
253 return this._DOMnode.parentNode;
254 }
255
256 getSubject() {
257 return this._subject;
258 }
259
260 getPredicate() {
261 return this._predicate;
262 }
263
264 getObject() {
265 return this._object;
266 }
267 }
268
269 class RDFNode {
270 equals(rdfnode) {
271 return (rdfnode.constructor === this.constructor &&
272 rdfnode._value == this._value);
273 }
274 }
275
276 /**
277 * A simple literal value
278 */
279 class RDFLiteral extends RDFNode {
280 constructor(value) {
281 super();
282 this._value = value;
283 }
284
285 /**
286 * This stores the value of the literal in the given DOM node
287 */
288 _applyToDOMNode(ds, domnode) {
289 if (isElement(domnode))
290 domnode.textContent = this._value;
291 else if (isAttr(domnode))
292 domnode.value = this._value;
293 else
294 throw new Error("cannot use this node for a literal");
295 }
296
297 getValue() {
298 return this._value;
299 }
300 }
301
302 /**
303 * A literal that is integer typed.
304 */
305 class RDFIntLiteral extends RDFLiteral {
306 constructor(value) {
307 super(parseInt(value));
308 }
309
310 /**
311 * This stores the value of the literal in the given DOM node
312 */
313 _applyToDOMNode(ds, domnode) {
314 if (!isElement(domnode))
315 throw new Error("cannot use this node for a literal");
316
317 RDFLiteral.prototype._applyToDOMNode.call(this, ds, domnode);
318 var prefix = ds._resolvePrefix(domnode, `${NS_NC}parseType`);
319 domnode.setAttributeNS(prefix.namespaceURI, prefix.qname, "Integer");
320 }
321 }
322
323 /**
324 * A literal that represents a date.
325 */
326 class RDFDateLiteral extends RDFLiteral {
327 constructor(value) {
328 if (!(value instanceof Date))
329 throw new Error("RDFDateLiteral must be constructed with a Date object");
330
331 super(value);
332 }
333
334 /**
335 * This stores the value of the literal in the given DOM node
336 */
337 _applyToDOMNode(ds, domnode) {
338 if (!isElement(domnode))
339 throw new Error("cannot use this node for a literal");
340
341 domnode.textContent = this._value.getTime();
342 var prefix = ds._resolvePrefix(domnode, `${NS_NC}parseType`);
343 domnode.setAttributeNS(prefix.namespaceURI, prefix.qname, "Date");
344 }
345 }
346
347 /**
348 * This is an RDF node that can be a subject so a resource or a blank node
349 */
350 class RDFSubject extends RDFNode {
351 constructor(ds) {
352 super();
353 // A lookup of the assertions with this as the subject. Keyed on predicate
354 this._assertions = {};
355 // A lookup of the assertions with this as the object. Keyed on predicate
356 this._backwards = {};
357 // The datasource this subject belongs to
358 this._ds = ds;
359 // The DOM elements in the document that represent this subject. Array of Element
360 this._elements = [];
361 }
362
363 /**
364 * Creates a new Element in the document for holding assertions about this
365 * subject. The URI controls what tagname to use.
366 */
367 _createElement(uri) {
368 // Seek an appropriate reference to this node to add this node under
369 var parent = null;
370 for (var p in this._backwards) {
371 for (let back of this._backwards[p]) {
372 // Don't add under an rdf:type
373 if (back.getPredicate() == RDF_R("type"))
374 continue;
375 // The assertion already has a child node, probably one of ours
376 if (back._DOMnode.firstChild)
377 continue;
378 parent = back._DOMnode;
379 var element = this._ds._addElement(parent, uri);
380 this._removeReferenceFromElement(parent);
381 break;
382 }
383 if (parent)
384 break;
385 }
386
387 // No back assertions that are sensible to use
388 if (!parent)
389 element = this._ds._addElement(this._ds._document.documentElement, uri);
390
391 element.listCounter = 1;
392 this._applyToElement(element);
393 this._elements.push(element);
394 return element;
395 }
396
397 /**
398 * When a DOM node representing this subject is removed from the document
399 * we must remove the node and recreate any child assertions elsewhere.
400 */
401 _removeElement(element) {
402 var pos = this._elements.indexOf(element);
403 if (pos < 0)
404 throw new Error("invalid element");
405 this._elements.splice(pos, 1);
406 if (element.parentNode != element.ownerDocument.documentElement)
407 this._addReferenceToElement(element.parentNode);
408 this._ds._removeElement(element);
409
410 // Find all the assertions that are represented here and create new
411 // nodes for them.
412 for (var predicate in this._assertions) {
413 for (let assertion of this._assertions[predicate]) {
414 if (assertion._getSubjectElement() == element)
415 this._createDOMNodeForAssertion(assertion);
416 }
417 }
418 }
419
420 /**
421 * Creates a DOM node to represent the assertion in the document. If the
422 * assertion has rdf:type as the predicate then an attempt will be made to
423 * create a typed subject Element, otherwise a new property Element is
424 * created. For list items an attempt is made to find an appropriate container
425 * that an rdf:li element can be added to.
426 */
427 _createDOMNodeForAssertion(assertion) {
428 let elements;
429 if (RDF_LISTITEM.test(assertion.getPredicate())) {
430 // Find all the containers
431 elements = this._elements.filter(function(element) {
432 return (element.namespaceURI == NS_RDF && (element.localName == "Seq" ||
433 element.localName == "Bag" ||
434 element.localName == "Alt"));
435 });
436 if (elements.length > 0) {
437 // Look for one whose listCounter matches the item we want to add
438 var item = parseInt(assertion.getPredicate().substring(NS_RDF.length + 1));
439 for (let element of elements) {
440 if (element.listCounter == item) {
441 assertion._DOMnode = this._ds._addElement(element, RDF_R("li"));
442 assertion._applyToDOMNode();
443 element.listCounter++;
444 return;
445 }
446 }
447 // No good container to add to, shove in the first real container
448 assertion._DOMnode = this._ds._addElement(elements[0], assertion.getPredicate());
449 assertion._applyToDOMNode();
450 return;
451 }
452 // TODO No containers, this will end up in a non-container for now
453 } else if (assertion.getPredicate() == RDF_R("type")) {
454 // Try renaming an existing rdf:Description
455 for (let element of this.elements) {
456 if (element.namespaceURI == NS_RDF &&
457 element.localName == "Description") {
458 try {
459 var prefix = this._ds._resolvePrefix(element.parentNode, assertion.getObject().getURI());
460 element = renameNode(element, prefix.namespaceURI, prefix.qname);
461 assertion._DOMnode = element;
462 assertion._isSubjectElement = true;
463 return;
464 } catch (e) {
465 // If the type cannot be sensibly turned into a prefix then just set
466 // as a regular property
467 }
468 }
469 }
470 }
471
472 // Filter out all the containers
473 elements = this._elements.filter(function(element) {
474 return (element.namespaceURI != NS_RDF || (element.localName != "Seq" &&
475 element.localName != "Bag" &&
476 element.localName != "Alt"));
477 });
478 if (elements.length == 0) {
479 // Create a new node of the right type
480 if (assertion.getPredicate() == RDF_R("type")) {
481 try {
482 assertion._DOMnode = this._createElement(assertion.getObject().getURI());
483 assertion._isSubjectElement = true;
484 return;
485 } catch (e) {
486 // If the type cannot be sensibly turned into a prefix then just set
487 // as a regular property
488 }
489 }
490 elements[0] = this._createElement(RDF_R("Description"));
491 }
492 assertion._DOMnode = this._ds._addElement(elements[0], assertion.getPredicate());
493 assertion._applyToDOMNode();
494 }
495
496 /**
497 * Removes the DOM node representing the assertion.
498 */
499 _removeDOMNodeForAssertion(assertion) {
500 if (isAttr(assertion._DOMnode)) {
501 var parent = assertion._DOMnode.ownerElement;
502 parent.removeAttributeNode(assertion._DOMnode);
503 } else if (assertion._isSubjectElement) {
504 var domnode = renameNode(assertion._DOMnode, NS_RDF, "Description");
505 if (domnode != assertion._DOMnode) {
506 var pos = this._elements.indexOf(assertion._DOMnode);
507 this._elements.splice(pos, 1, domnode);
508 }
509 parent = domnode;
510 } else {
511 var object = assertion.getObject();
512 if (object instanceof RDFSubject && assertion._DOMnode.firstChild) {
513 // Object is a subject that has an Element inside this assertion's node.
514 for (let element of object._elements) {
515 if (element.parentNode == assertion._DOMnode) {
516 object._removeElement(element);
517 break;
518 }
519 }
520 }
521 parent = assertion._DOMnode.parentNode;
522 if (assertion._DOMnode.namespaceURI == NS_RDF &&
523 assertion._DOMnode.localName == "li")
524 parent.listCounter--;
525 this._ds._removeElement(assertion._DOMnode);
526 }
527
528 // If there are no assertions left using the assertion's containing dom node
529 // then remove it from the document.
530 // TODO could do with a quick lookup list for assertions attached to a node
531 for (var p in this._assertions) {
532 for (let assertion of this._assertions[p]) {
533 if (assertion._getSubjectElement() == parent)
534 return;
535 }
536 }
537 // No assertions left in this element.
538 this._removeElement(parent);
539 }
540
541 /**
542 * Parses the given Element from the DOM document
543 */
544 /* eslint-disable complexity */
545 _parseElement(element) {
546 this._elements.push(element);
547
548 // There might be an inferred rdf:type assertion in the element name
549 if (element.namespaceURI != NS_RDF ||
550 element.localName != "Description") {
551 if (element.namespaceURI == NS_RDF && element.localName == "li")
552 throw new Error("rdf:li is not a valid type for a subject node");
553 var assertion = new RDFAssertion(this, RDF_R("type"),
554 this._ds.getResource(element.namespaceURI + element.localName));
555 assertion._DOMnode = element;
556 assertion._isSubjectElement = true;
557 this._addAssertion(assertion);
558 }
559
560 // Certain attributes can be literal properties
561 for (let attr of element.attributes) {
562 if (attr.namespaceURI == NS_XML || attr.namespaceURI == NS_XMLNS ||
563 attr.nodeName == "xmlns")
564 continue;
565 if ((attr.namespaceURI == NS_RDF || !attr.namespaceURI) &&
566 (["nodeID", "about", "resource", "ID", "parseType"].includes(attr.localName)))
567 continue;
568 var object = null;
569 if (attr.namespaceURI == NS_RDF) {
570 if (attr.localName == "type")
571 object = this._ds.getResource(attr.nodeValue);
572 else if (attr.localName == "li")
573 throw new Error("rdf:li is not allowed as a property attribute");
574 else if (attr.localName == "aboutEach")
575 throw new Error("rdf:aboutEach is deprecated");
576 else if (attr.localName == "aboutEachPrefix")
577 throw new Error("rdf:aboutEachPrefix is deprecated");
578 else if (attr.localName == "aboutEach")
579 throw new Error("rdf:aboutEach is deprecated");
580 else if (attr.localName == "bagID")
581 throw new Error("rdf:bagID is deprecated");
582 }
583 if (!object)
584 object = new RDFLiteral(attr.nodeValue);
585 assertion = new RDFAssertion(this, attr.namespaceURI + attr.localName, object);
586 assertion._DOMnode = attr;
587 this._addAssertion(assertion);
588 }
589
590 var child = element.firstChild;
591 element.listCounter = 1;
592 while (child) {
593 if (isText(child) && /\S/.test(child.nodeValue)) {
594 ERROR(`Text ${child.nodeValue} is not allowed in a subject node`);
595 throw new Error("subject nodes cannot contain text content");
596 } else if (isElement(child)) {
597 object = null;
598 var predicate = child.namespaceURI + child.localName;
599 if (child.namespaceURI == NS_RDF) {
600 if (RDF_PROPERTY_INVALID_TYPES.includes(child.localName) &&
601 !child.localName.match(/^_\d+$/))
602 throw new Error(`${child.nodeName} is an invalid property`);
603 if (child.localName == "li") {
604 predicate = RDF_R(`_${element.listCounter}`);
605 element.listCounter++;
606 }
607 }
608
609 // Check for and bail out on unknown attributes on the property element
610 for (let attr of child.attributes) {
611 // Ignore XML namespaced attributes
612 if (attr.namespaceURI == NS_XML)
613 continue;
614 // These are reserved by XML for future use
615 if (attr.localName.substring(0, 3).toLowerCase() == "xml")
616 continue;
617 // We can handle these RDF attributes
618 if ((!attr.namespaceURI || attr.namespaceURI == NS_RDF) &&
619 ["resource", "nodeID"].includes(attr.localName))
620 continue;
621 // This is a special attribute we handle for compatibility with Mozilla RDF
622 if (attr.namespaceURI == NS_NC &&
623 attr.localName == "parseType")
624 continue;
625 throw new Error(`Attribute ${attr.nodeName} is not supported`);
626 }
627
628 var parseType = child.getAttributeNS(NS_NC, "parseType");
629 if (parseType && parseType != "Date" && parseType != "Integer") {
630 ERROR(`parseType ${parseType} is not supported`);
631 throw new Error("unsupported parseType");
632 }
633
634 var resource = getRDFAttribute(child, "resource");
635 var nodeID = getRDFAttribute(child, "nodeID");
636 if ((resource && (nodeID || parseType)) ||
637 (nodeID && (resource || parseType))) {
638 ERROR("Cannot use more than one of parseType, resource and nodeID on a single node");
639 throw new Error("Invalid rdf assertion");
640 }
641
642 if (resource !== undefined) {
643 var base = Services.io.newURI(element.baseURI);
644 object = this._ds.getResource(base.resolve(resource));
645 } else if (nodeID !== undefined) {
646 if (!nodeID.match(XML_NCNAME))
647 throw new Error("rdf:nodeID must be a valid XML name");
648 object = this._ds.getBlankNode(nodeID);
649 } else {
650 var hasText = false;
651 var childElement = null;
652 var subchild = child.firstChild;
653 while (subchild) {
654 if (isText(subchild) && /\S/.test(subchild.nodeValue)) {
655 hasText = true;
656 } else if (isElement(subchild)) {
657 if (childElement) {
658 new Error(`Multiple object elements found in ${child.nodeName}`);
659 }
660 childElement = subchild;
661 }
662 subchild = subchild.nextSibling;
663 }
664
665 if ((resource || nodeID) && (hasText || childElement)) {
666 ERROR("Assertion references a resource so should not contain additional contents");
667 throw new Error("assertion cannot contain multiple objects");
668 }
669
670 if (hasText && childElement) {
671 ERROR(`Both literal and resource objects found in ${child.nodeName}`);
672 throw new Error("assertion cannot contain multiple objects");
673 }
674
675 if (childElement) {
676 if (parseType) {
677 ERROR("Cannot specify a parseType for an assertion with resource object");
678 throw new Error("parseType is not valid in this context");
679 }
680 object = this._ds._getSubjectForElement(childElement);
681 object._parseElement(childElement);
682 } else if (parseType == "Integer") {
683 object = new RDFIntLiteral(child.textContent);
684 } else if (parseType == "Date") {
685 object = new RDFDateLiteral(new Date(child.textContent));
686 } else {
687 object = new RDFLiteral(child.textContent);
688 }
689 }
690
691 assertion = new RDFAssertion(this, predicate, object);
692 this._addAssertion(assertion);
693 assertion._DOMnode = child;
694 }
695 child = child.nextSibling;
696 }
697 }
698 /* eslint-enable complexity */
699
700 /**
701 * Adds a new assertion to the internal hashes. Should be called for every
702 * new assertion parsed or created programmatically.
703 */
704 _addAssertion(assertion) {
705 var predicate = assertion.getPredicate();
706 if (predicate in this._assertions)
707 this._assertions[predicate].push(assertion);
708 else
709 this._assertions[predicate] = [ assertion ];
710
711 var object = assertion.getObject();
712 if (object instanceof RDFSubject) {
713 // Create reverse assertion
714 if (predicate in object._backwards)
715 object._backwards[predicate].push(assertion);
716 else
717 object._backwards[predicate] = [ assertion ];
718 }
719 }
720
721 /**
722 * Removes an assertion from the internal hashes. Should be called for all
723 * assertions that are programmatically deleted.
724 */
725 _removeAssertion(assertion) {
726 var predicate = assertion.getPredicate();
727 if (predicate in this._assertions) {
728 var pos = this._assertions[predicate].indexOf(assertion);
729 if (pos >= 0)
730 this._assertions[predicate].splice(pos, 1);
731 if (this._assertions[predicate].length == 0)
732 delete this._assertions[predicate];
733 }
734
735 var object = assertion.getObject();
736 if (object instanceof RDFSubject) {
737 // Delete reverse assertion
738 if (predicate in object._backwards) {
739 pos = object._backwards[predicate].indexOf(assertion);
740 if (pos >= 0)
741 object._backwards[predicate].splice(pos, 1);
742 if (object._backwards[predicate].length == 0)
743 delete object._backwards[predicate];
744 }
745 }
746 }
747
748 /**
749 * Returns the ordinal assertions from this subject in order.
750 */
751 _getChildAssertions() {
752 var assertions = [];
753 for (var i in this._assertions) {
754 if (RDF_LISTITEM.test(i))
755 assertions.push(...this._assertions[i]);
756 }
757 assertions.sort(predicateOrder);
758 return assertions;
759 }
760
761 /**
762 * Compares this to another rdf node
763 */
764 equals(rdfnode) {
765 // subjects are created by the datasource so no two objects ever correspond
766 // to the same one.
767 return this === rdfnode;
768 }
769
770 /**
771 * Adds a new assertion with this as the subject
772 */
773 assert(predicate, object) {
774 if (predicate == RDF_R("type") && !(object instanceof RDFResource))
775 throw new Error("rdf:type must be an RDFResource");
776
777 var assertion = new RDFAssertion(this, predicate, object);
778 this._createDOMNodeForAssertion(assertion);
779 this._addAssertion(assertion);
780 }
781
782 /**
783 * Removes an assertion matching the predicate and node given, if such an
784 * assertion exists.
785 */
786 unassert(predicate, object) {
787 if (!(predicate in this._assertions))
788 return;
789
790 for (let assertion of this._assertions[predicate]) {
791 if (assertion.getObject().equals(object)) {
792 this._removeAssertion(assertion);
793 this._removeDOMNodeForAssertion(assertion);
794 return;
795 }
796 }
797 }
798
799 /**
800 * Returns an array of all the predicates that exist in assertions from this
801 * subject.
802 */
803 getPredicates() {
804 return Object.keys(this._assertions);
805 }
806
807 /**
808 * Returns all objects in assertions with this subject and the given predicate.
809 */
810 getObjects(predicate) {
811 if (predicate in this._assertions)
812 return Array.from(this._assertions[predicate],
813 i => i.getObject());
814
815 return [];
816 }
817
818 /**
819 * Returns all of the ordinal children of this subject in order.
820 */
821 getChildren() {
822 return Array.from(this._getChildAssertions(),
823 i => i.getObject());
824 }
825
826 /**
827 * Removes the child at the given index. This is the index based on the
828 * children returned from getChildren. Forces a reordering of the later
829 * children.
830 */
831 removeChildAt(pos) {
832 if (pos < 0)
833 throw new Error("no such child");
834 var assertions = this._getChildAssertions();
835 if (pos >= assertions.length)
836 throw new Error("no such child");
837 for (var i = pos; i < assertions.length; i++) {
838 this._removeAssertion(assertions[i]);
839 this._removeDOMNodeForAssertion(assertions[i]);
840 }
841 var index = 1;
842 if (pos > 0)
843 index = parseInt(assertions[pos - 1].getPredicate().substring(NS_RDF.length + 1)) + 1;
844 for (let i = pos + 1; i < assertions.length; i++) {
845 assertions[i]._predicate = RDF_R(`_${index}`);
846 this._addAssertion(assertions[i]);
847 this._createDOMNodeForAssertion(assertions[i]);
848 index++;
849 }
850 }
851
852 /**
853 * Removes the child with the given object. It is unspecified which child is
854 * removed if the object features more than once.
855 */
856 removeChild(object) {
857 var assertions = this._getChildAssertions();
858 for (var pos = 0; pos < assertions.length; pos++) {
859 if (assertions[pos].getObject().equals(object)) {
860 for (var i = pos; i < assertions.length; i++) {
861 this._removeAssertion(assertions[i]);
862 this._removeDOMNodeForAssertion(assertions[i]);
863 }
864 var index = 1;
865 if (pos > 0)
866 index = parseInt(assertions[pos - 1].getPredicate().substring(NS_RDF.length + 1)) + 1;
867 for (let i = pos + 1; i < assertions.length; i++) {
868 assertions[i]._predicate = RDF_R(`_${index}`);
869 this._addAssertion(assertions[i]);
870 this._createDOMNodeForAssertion(assertions[i]);
871 index++;
872 }
873 return;
874 }
875 }
876 throw new Error("no such child");
877 }
878
879 /**
880 * Adds a new ordinal child to this subject.
881 */
882 addChild(object) {
883 var max = 0;
884 for (var i in this._assertions) {
885 if (RDF_LISTITEM.test(i))
886 max = Math.max(max, parseInt(i.substring(NS_RDF.length + 1)));
887 }
888 max++;
889 this.assert(RDF_R(`_${max}`), object);
890 }
891
892 /**
893 * This reorders the child assertions to remove duplicates and gaps in the
894 * sequence. Generally this will move all children to be under the same
895 * container element and all represented as an rdf:li
896 */
897 reorderChildren() {
898 var assertions = this._getChildAssertions();
899 for (let assertion of assertions) {
900 this._removeAssertion(assertion);
901 this._removeDOMNodeForAssertion(assertion);
902 }
903 var index = 1;
904 for (let assertion of assertions) {
905 assertion._predicate = RDF_R(`_${index}`);
906 this._addAssertion(assertion);
907 this._createDOMNodeForAssertion(assertion);
908 index++;
909 }
910 }
911
912 /**
913 * Returns the type of this subject or null if there is no specified type.
914 */
915 getType() {
916 var type = this.getProperty(RDF_R("type"));
917 if (type && type instanceof RDFResource)
918 return type.getURI();
919 return null;
920 }
921
922 /**
923 * Tests if a property exists for the given predicate.
924 */
925 hasProperty(predicate) {
926 return (predicate in this._assertions);
927 }
928
929 /**
930 * Retrieves the first property value for the given predicate.
931 */
932 getProperty(predicate) {
933 if (predicate in this._assertions)
934 return this._assertions[predicate][0].getObject();
935 return null;
936 }
937
938 /**
939 * Sets the property value for the given predicate, clearing any existing
940 * values.
941 */
942 setProperty(predicate, object) {
943 // TODO optimise by replacing the first assertion and clearing the rest
944 this.clearProperty(predicate);
945 this.assert(predicate, object);
946 }
947
948 /**
949 * Clears any existing properties for the given predicate.
950 */
951 clearProperty(predicate) {
952 if (!(predicate in this._assertions))
953 return;
954
955 var assertions = this._assertions[predicate];
956 while (assertions.length > 0) {
957 var assertion = assertions[0];
958 this._removeAssertion(assertion);
959 this._removeDOMNodeForAssertion(assertion);
960 }
961 }
962 }
963
964 /**
965 * Creates a new RDFResource for the datasource. Private.
966 */
967 class RDFResource extends RDFSubject {
968 constructor(ds, uri) {
969 if (!(ds instanceof RDFDataSource))
970 throw new Error("datasource must be an RDFDataSource");
971
972 if (!uri)
973 throw new Error("An RDFResource requires a non-null uri");
974
975 super(ds);
976 // This is the uri that the resource represents.
977 this._uri = uri;
978 }
979
980 /**
981 * Sets attributes on the DOM element to mark it as representing this resource
982 */
983 _applyToElement(element) {
984 if (USE_RDFNS_ATTR) {
985 var prefix = this._ds._resolvePrefix(element, RDF_R("about"));
986 element.setAttributeNS(prefix.namespaceURI, prefix.qname, this._uri);
987 } else {
988 element.setAttribute("about", this._uri);
989 }
990 }
991
992 /**
993 * Adds a reference to this resource to the given property Element.
994 */
995 _addReferenceToElement(element) {
996 if (USE_RDFNS_ATTR) {
997 var prefix = this._ds._resolvePrefix(element, RDF_R("resource"));
998 element.setAttributeNS(prefix.namespaceURI, prefix.qname, this._uri);
999 } else {
1000 element.setAttribute("resource", this._uri);
1001 }
1002 }
1003
1004 /**
1005 * Removes any reference to this resource from the given property Element.
1006 */
1007 _removeReferenceFromElement(element) {
1008 if (element.hasAttributeNS(NS_RDF, "resource"))
1009 element.removeAttributeNS(NS_RDF, "resource");
1010 if (element.hasAttribute("resource"))
1011 element.removeAttribute("resource");
1012 }
1013
1014 getURI() {
1015 return this._uri;
1016 }
1017 }
1018
1019 /**
1020 * Creates a new blank node. Private.
1021 */
1022 class RDFBlankNode extends RDFSubject {
1023 constructor(ds, nodeID) {
1024 if (!(ds instanceof RDFDataSource))
1025 throw new Error("datasource must be an RDFDataSource");
1026
1027 super(ds);
1028 // The nodeID of this node. May be null if there is no ID.
1029 this._nodeID = nodeID;
1030 }
1031
1032 /**
1033 * Sets attributes on the DOM element to mark it as representing this node
1034 */
1035 _applyToElement(element) {
1036 if (!this._nodeID)
1037 return;
1038 if (USE_RDFNS_ATTR) {
1039 var prefix = this._ds._resolvePrefix(element, RDF_R("nodeID"));
1040 element.setAttributeNS(prefix.namespaceURI, prefix.qname, this._nodeID);
1041 } else {
1042 element.setAttribute("nodeID", this._nodeID);
1043 }
1044 }
1045
1046 /**
1047 * Creates a new Element in the document for holding assertions about this
1048 * subject. The URI controls what tagname to use.
1049 */
1050 _createNewElement(uri) {
1051 // If there are already nodes representing this in the document then we need
1052 // a nodeID to match them
1053 if (!this._nodeID && this._elements.length > 0) {
1054 this._ds._createNodeID(this);
1055 for (let element of this._elements)
1056 this._applyToElement(element);
1057 }
1058
1059 return super._createNewElement.call(uri);
1060 }
1061
1062 /**
1063 * Adds a reference to this node to the given property Element.
1064 */
1065 _addReferenceToElement(element) {
1066 if (this._elements.length > 0 && !this._nodeID) {
1067 // In document elsewhere already
1068 // Create a node ID and update the other nodes referencing
1069 this._ds._createNodeID(this);
1070 for (let element of this._elements)
1071 this._applyToElement(element);
1072 }
1073
1074 if (this._nodeID) {
1075 if (USE_RDFNS_ATTR) {
1076 let prefix = this._ds._resolvePrefix(element, RDF_R("nodeID"));
1077 element.setAttributeNS(prefix.namespaceURI, prefix.qname, this._nodeID);
1078 } else {
1079 element.setAttribute("nodeID", this._nodeID);
1080 }
1081 } else {
1082 // Add the empty blank node, this is generally right since further
1083 // assertions will be added to fill this out
1084 var newelement = this._ds._addElement(element, RDF_R("Description"));
1085 newelement.listCounter = 1;
1086 this._elements.push(newelement);
1087 }
1088 }
1089
1090 /**
1091 * Removes any reference to this node from the given property Element.
1092 */
1093 _removeReferenceFromElement(element) {
1094 if (element.hasAttributeNS(NS_RDF, "nodeID"))
1095 element.removeAttributeNS(NS_RDF, "nodeID");
1096 if (element.hasAttribute("nodeID"))
1097 element.removeAttribute("nodeID");
1098 }
1099
1100 getNodeID() {
1101 return this._nodeID;
1102 }
1103 }
1104
1105 /**
1106 * Creates a new RDFDataSource from the given document. The document will be
1107 * changed as assertions are added and removed to the RDF. Pass a null document
1108 * to start with an empty graph.
1109 */
1110 export class RDFDataSource {
1111 constructor(document) {
1112 // All known resources, indexed on URI
1113 this._resources = {};
1114 // All blank nodes
1115 this._allBlankNodes = [];
1116 // All blank nodes with IDs, indexed on ID
1117 this._blankNodes = {};
1118 // Suggested prefixes to use for namespaces, index is prefix, value is namespaceURI.
1119 this._prefixes = {
1120 rdf: NS_RDF,
1121 NC: NS_NC,
1122 };
1123
1124 if (!document) {
1125 // Creating a document through xpcom leaves out the xml prolog so just parse
1126 // something small
1127 var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
1128 createInstance(Ci.nsIDOMParser);
1129 var doctext = `<?xml version="1.0"?>\n<rdf:RDF xmlns:rdf="${NS_RDF}"/>\n`;
1130 document = parser.parseFromString(doctext, "text/xml");
1131 }
1132 // The underlying DOM document for this datasource
1133 this._document = document;
1134 this._parseDocument();
1135 }
1136
1137 static loadFromString(text) {
1138 let parser = new DOMParser();
1139 let document = parser.parseFromString(text, "application/xml");
1140
1141 return new this(document);
1142 }
1143
1144 static loadFromBuffer(buffer) {
1145 let parser = new DOMParser();
1146 let document = parser.parseFromBuffer(new Uint8Array(buffer), "application/xml");
1147
1148 return new this(document);
1149 }
1150
1151 static async loadFromFile(uri) {
1152 if (uri instanceof Ci.nsIFile)
1153 uri = Services.io.newFileURI(uri);
1154 else if (typeof(uri) == "string")
1155 uri = Services.io.newURI(uri);
1156
1157 let resp = await fetch(uri.spec);
1158 return this.loadFromBuffer(await resp.arrayBuffer());
1159 }
1160
1161 get uri() {
1162 return this._document.documentURI;
1163 }
1164
1165 /**
1166 * Creates a new nodeID for an unnamed blank node. Just node<number>.
1167 */
1168 _createNodeID(blanknode) {
1169 var i = 1;
1170 while (`node${i}` in this._blankNodes)
1171 i++;
1172 blanknode._nodeID = `node${i}`;
1173 this._blankNodes[blanknode._nodeID] = blanknode;
1174 }
1175
1176 /**
1177 * Returns an rdf subject for the given DOM Element. If the subject has not
1178 * been seen before a new one is created.
1179 */
1180 _getSubjectForElement(element) {
1181 if (element.namespaceURI == NS_RDF &&
1182 RDF_NODE_INVALID_TYPES.includes(element.localName))
1183 throw new Error(`${element.nodeName} is not a valid class for a subject node`);
1184
1185 var about = getRDFAttribute(element, "about");
1186 var id = getRDFAttribute(element, "ID");
1187 var nodeID = getRDFAttribute(element, "nodeID");
1188
1189 if ((about && (id || nodeID)) ||
1190 (nodeID && (id || about))) {
1191 ERROR("More than one of about, ID and nodeID present on the same subject");
1192 throw new Error("invalid subject in rdf");
1193 }
1194
1195 if (about !== undefined) {
1196 let base = Services.io.newURI(element.baseURI);
1197 return this.getResource(base.resolve(about));
1198 }
1199 if (id !== undefined) {
1200 if (!id.match(XML_NCNAME))
1201 throw new Error("rdf:ID must be a valid XML name");
1202 let base = Services.io.newURI(element.baseURI);
1203 return this.getResource(base.resolve(`#${id}`));
1204 }
1205 if (nodeID !== undefined)
1206 return this.getBlankNode(nodeID);
1207 return this.getBlankNode(null);
1208 }
1209
1210 /**
1211 * Parses the document for subjects at the top level.
1212 */
1213 _parseDocument() {
1214 if (!this._document.documentElement) {
1215 ERROR("No document element in document");
1216 throw new Error("document contains no root element");
1217 }
1218
1219 if (this._document.documentElement.namespaceURI != NS_RDF ||
1220 this._document.documentElement.localName != "RDF") {
1221 ERROR(`${this._document.documentElement.nodeName} is not rdf:RDF`);
1222 throw new Error("document does not appear to be RDF");
1223 }
1224
1225 var domnode = this._document.documentElement.firstChild;
1226 while (domnode) {
1227 if (isText(domnode) && /\S/.test(domnode.nodeValue)) {
1228 ERROR("RDF does not allow for text in the root of the document");
1229 throw new Error("invalid markup in document");
1230 } else if (isElement(domnode)) {
1231 var subject = this._getSubjectForElement(domnode);
1232 subject._parseElement(domnode);
1233 }
1234 domnode = domnode.nextSibling;
1235 }
1236 }
1237
1238 /**
1239 * Works out a sensible namespace prefix to use for the given uri. node should
1240 * be the parent of where the element is to be inserted, or the node that an
1241 * attribute is to be added to. This will recursively walk to the top of the
1242 * document finding an already registered prefix that matches for the uri.
1243 * If none is found a new prefix is registered.
1244 * This returns an object with keys namespaceURI, prefix, localName and qname.
1245 * Pass null or undefined for badPrefixes for the first call.
1246 */
1247 _resolvePrefix(domnode, uri, badPrefixes) {
1248 if (!badPrefixes)
1249 badPrefixes = [];
1250
1251 // No known prefix, try to create one from the lookup list
1252 if (!domnode || isDocument(domnode)) {
1253 for (let i in this._prefixes) {
1254 if (badPrefixes.includes(i))
1255 continue;
1256 if (this._prefixes[i] == uri.substring(0, this._prefixes[i].length)) {
1257 var local = uri.substring(this._prefixes[i].length);
1258 var test = URI_SUFFIX.exec(local);
1259 // Remaining part of uri is a good XML Name
1260 if (test && test[0] == local) {
1261 this._document.documentElement.setAttributeNS(NS_XMLNS, `xmlns:${i}`, this._prefixes[i]);
1262 return {
1263 namespaceURI: this._prefixes[i],
1264 prefix: i,
1265 localName: local,
1266 qname: i ? `${i}:${local}` : local,
1267 };
1268 }
1269 }
1270 }
1271
1272 // No match, make something up
1273 test = URI_SUFFIX.exec(uri);
1274 if (test) {
1275 var namespaceURI = uri.substring(0, uri.length - test[0].length);
1276 local = test[0];
1277 let i = 1;
1278 while (badPrefixes.includes(`NS${i}`))
1279 i++;
1280 this._document.documentElement.setAttributeNS(NS_XMLNS, `xmlns:NS${i}`, namespaceURI);
1281 return {
1282 namespaceURI,
1283 prefix: `NS${i}`,
1284 localName: local,
1285 qname: `NS${i}:${local}`,
1286 };
1287 }
1288 // There is no end part of this URI that is an XML Name
1289 throw new Error(`invalid node name: ${uri}`);
1290 }
1291
1292 for (let attr of domnode.attributes) {
1293 // Not a namespace declaration, ignore this attribute
1294 if (attr.namespaceURI != NS_XMLNS && attr.nodeName != "xmlns")
1295 continue;
1296
1297 var prefix = attr.prefix ? attr.localName : "";
1298 // Seen this prefix before, cannot use it
1299 if (badPrefixes.includes(prefix))
1300 continue;
1301
1302 // Namespace matches the start of the uri
1303 if (attr.value == uri.substring(0, attr.value.length)) {
1304 local = uri.substring(attr.value.length);
1305 test = URI_SUFFIX.exec(local);
1306 // Remaining part of uri is a good XML Name
1307 if (test && test[0] == local) {
1308 return {
1309 namespaceURI: attr.value,
1310 prefix,
1311 localName: local,
1312 qname: prefix ? `${prefix}:${local}` : local,
1313 };
1314 }
1315 }
1316
1317 badPrefixes.push(prefix);
1318 }
1319
1320 // No prefix found here, move up the document
1321 return this._resolvePrefix(domnode.parentNode, uri, badPrefixes);
1322 }
1323
1324 /**
1325 * Guess the indent level within the given Element. The method looks for
1326 * elements that are preceded by whitespace including a newline. The
1327 * whitespace following the newline is presumed to be the indentation for the
1328 * element.
1329 * If the indentation cannot be guessed then it recurses up the document
1330 * hierarchy until it can guess the indent or until the Document is reached.
1331 */
1332 _guessIndent(element) {
1333 // The indent at document level is 0
1334 if (!element || isDocument(element))
1335 return "";
1336
1337 // Check the text immediately preceding each child node. One could be
1338 // a valid indent
1339 var pretext = "";
1340 var child = element.firstChild;
1341 while (child) {
1342 if (isText(child)) {
1343 pretext += child.nodeValue;
1344 } else if (isElement(child)) {
1345 var result = INDENT.exec(pretext);
1346 if (result)
1347 return result[1];
1348 pretext = "";
1349 }
1350 child = child.nextSibling;
1351 }
1352
1353 // pretext now contains any trailing text in the element. This can be
1354 // the indent of the end tag. If so add a little to it.
1355 result = INDENT.exec(pretext);
1356 if (result)
1357 return `${result[1]} `;
1358
1359 // Check the text immediately before this node
1360 pretext = "";
1361 var sibling = element.previousSibling;
1362 while (sibling && isText(sibling)) {
1363 pretext += sibling.nodeValue;
1364 sibling = sibling.previousSibling;
1365 }
1366
1367 // If there is a sensible indent then just add to it.
1368 result = INDENT.exec(pretext);
1369 if (result)
1370 return `${result[1]} `;
1371
1372 // Last chance, get the indent level for the tag above and add to it
1373 return `${this._guessIndent(element.parentNode)} `;
1374 }
1375
1376 _addElement(parent, uri) {
1377 var prefix = this._resolvePrefix(parent, uri);
1378 var element = this._document.createElementNS(prefix.namespaceURI, prefix.qname);
1379
1380 if (parent.lastChild) {
1381 // We want to insert immediately after the last child element
1382 var last = parent.lastChild;
1383 while (last && isText(last))
1384 last = last.previousSibling;
1385 // No child elements so insert at the start
1386 if (!last)
1387 last = parent.firstChild;
1388 else
1389 last = last.nextSibling;
1390
1391 let indent = this._guessIndent(parent);
1392 parent.insertBefore(this._document.createTextNode(`\n${indent}`), last);
1393 parent.insertBefore(element, last);
1394 } else {
1395 // No children, must indent our element and the end tag
1396 let indent = this._guessIndent(parent.parentNode);
1397 parent.append(`\n${indent} `, element, `\n${indent}`);
1398 }
1399 return element;
1400 }
1401
1402 /**
1403 * Removes the element from its parent. Should also remove surrounding
1404 * white space as appropriate.
1405 */
1406 _removeElement(element) {
1407 var parent = element.parentNode;
1408 var sibling = element.previousSibling;
1409 // Drop any text nodes immediately preceding the element
1410 while (sibling && isText(sibling)) {
1411 var temp = sibling;
1412 sibling = sibling.previousSibling;
1413 parent.removeChild(temp);
1414 }
1415
1416 sibling = element.nextSibling;
1417 // Drop the element
1418 parent.removeChild(element);
1419
1420 // If the next node after element is now the first child then element was
1421 // the first child. If there are no other child elements then remove the
1422 // remaining child nodes.
1423 if (parent.firstChild == sibling) {
1424 while (sibling && isText(sibling))
1425 sibling = sibling.nextSibling;
1426 if (!sibling) {
1427 // No other child elements
1428 while (parent.lastChild)
1429 parent.removeChild(parent.lastChild);
1430 }
1431 }
1432 }
1433
1434 /**
1435 * Requests that a given prefix be used for the namespace where possible.
1436 * This must be called before any assertions are made using the namespace
1437 * and the registration will not override any existing prefix used in the
1438 * document.
1439 */
1440 registerPrefix(prefix, namespaceURI) {
1441 this._prefixes[prefix] = namespaceURI;
1442 }
1443
1444 /**
1445 * Gets a blank node. nodeID may be null and if so a new blank node is created.
1446 * If a nodeID is given then the blank node with that ID is returned or created.
1447 */
1448 getBlankNode(nodeID) {
1449 if (nodeID && nodeID in this._blankNodes)
1450 return this._blankNodes[nodeID];
1451
1452 if (nodeID && !nodeID.match(XML_NCNAME))
1453 throw new Error("rdf:nodeID must be a valid XML name");
1454
1455 var rdfnode = new RDFBlankNode(this, nodeID);
1456 this._allBlankNodes.push(rdfnode);
1457 if (nodeID)
1458 this._blankNodes[nodeID] = rdfnode;
1459 return rdfnode;
1460 }
1461
1462 /**
1463 * Gets all blank nodes
1464 */
1465 getAllBlankNodes() {
1466 return this._allBlankNodes.slice();
1467 }
1468
1469 /**
1470 * Gets the resource for the URI. The resource is created if it has not been
1471 * used already.
1472 */
1473 getResource(uri) {
1474 if (uri in this._resources)
1475 return this._resources[uri];
1476
1477 var resource = new RDFResource(this, uri);
1478 this._resources[uri] = resource;
1479 return resource;
1480 }
1481
1482 /**
1483 * Gets all resources that have been used.
1484 */
1485 getAllResources() {
1486 return Object.values(this._resources);
1487 }
1488
1489 /**
1490 * Returns all blank nodes and resources
1491 */
1492 getAllSubjects() {
1493 return [...Object.values(this._resources),
1494 ...this._allBlankNodes];
1495 }
1496
1497 /**
1498 * Saves the RDF/XML to a string.
1499 */
1500 serializeToString() {
1501 var serializer = new XMLSerializer();
1502 return serializer.serializeToString(this._document);
1503 }
1504
1505 /**
1506 * Saves the RDF/XML to a file.
1507 */
1508 async saveToFile(file) {
1509 return IOUtils.writeUTF8(file, this.serializeToString());
1510 }
1511 }
Imprint / Impressum