]> git.gir.st - LegacyFox.git/blob - legacy/RDFDataSource.jsm
add backwards compatible code for Services.jsm removal
[LegacyFox.git] / legacy / RDFDataSource.jsm
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 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
138
139 XPCOMUtils.defineLazyGlobalGetters(this, ["DOMParser", "Element", "XMLSerializer", "fetch"]);
140
141 ChromeUtils.defineModuleGetter(this, "OS",
142 "resource://gre/modules/osfile.jsm");
143 const Services = globalThis.Services || ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
144
145 function isAttr(obj) {
146 return obj && typeof obj == "object" && ChromeUtils.getClassName(obj) == "Attr";
147 }
148 function isDocument(obj) {
149 return obj && typeof obj == "object" && obj.nodeType == Element.DOCUMENT_NODE;
150 }
151 function isElement(obj) {
152 return Element.isInstance(obj);
153 }
154 function isText(obj) {
155 return obj && typeof obj == "object" && ChromeUtils.getClassName(obj) == "Text";
156 }
157
158 /**
159 * Logs an error message to the error console
160 */
161 function ERROR(str) {
162 Cu.reportError(str);
163 }
164
165 function RDF_R(name) {
166 return NS_RDF + name;
167 }
168
169 function renameNode(domnode, namespaceURI, qname) {
170 if (isElement(domnode)) {
171 var newdomnode = domnode.ownerDocument.createElementNS(namespaceURI, qname);
172 if ("listCounter" in domnode)
173 newdomnode.listCounter = domnode.listCounter;
174 domnode.replaceWith(newdomnode);
175 while (domnode.firstChild)
176 newdomnode.appendChild(domnode.firstChild);
177 for (let attr of domnode.attributes) {
178 domnode.removeAttributeNode(attr);
179 newdomnode.setAttributeNode(attr);
180 }
181 return newdomnode;
182 } else if (isAttr(domnode)) {
183 if (domnode.ownerElement.hasAttribute(namespaceURI, qname))
184 throw new Error("attribute already exists");
185 var attr = domnode.ownerDocument.createAttributeNS(namespaceURI, qname);
186 attr.value = domnode.value;
187 domnode.ownerElement.setAttributeNode(attr);
188 domnode.ownerElement.removeAttributeNode(domnode);
189 return attr;
190 }
191 throw new Error("cannot rename node of this type");
192 }
193
194 function predicateOrder(a, b) {
195 return a.getPredicate().localeCompare(b.getPredicate());
196 }
197
198 /**
199 * Returns either an rdf namespaced attribute or an un-namespaced attribute
200 * value. Returns null if neither exists,
201 */
202 function getRDFAttribute(element, name) {
203 if (element.hasAttributeNS(NS_RDF, name))
204 return element.getAttributeNS(NS_RDF, name);
205 if (element.hasAttribute(name))
206 return element.getAttribute(name);
207 return undefined;
208 }
209
210 /**
211 * Represents an assertion in the datasource
212 */
213 class RDFAssertion {
214 constructor(subject, predicate, object) {
215 if (!(subject instanceof RDFSubject))
216 throw new Error("subject must be an RDFSubject");
217
218 if (typeof(predicate) != "string")
219 throw new Error("predicate must be a string URI");
220
221 if (!(object instanceof RDFLiteral) && !(object instanceof RDFSubject))
222 throw new Error("object must be a concrete RDFNode");
223
224 if (object instanceof RDFSubject && object._ds != subject._ds)
225 throw new Error("object must be from the same datasource as subject");
226
227 // The subject on this assertion, an RDFSubject
228 this._subject = subject;
229 // The predicate, a string
230 this._predicate = predicate;
231 // The object, an RDFNode
232 this._object = object;
233 // The datasource this assertion exists in
234 this._ds = this._subject._ds;
235 // Marks that _DOMnode is the subject's element
236 this._isSubjectElement = false;
237 // The DOM node that represents this assertion. Could be a property element,
238 // a property attribute or the subject's element for rdf:type
239 this._DOMNode = null;
240 }
241
242 /**
243 * Adds content to _DOMnode to store this assertion in the DOM document.
244 */
245 _applyToDOMNode() {
246 if (this._object instanceof RDFLiteral)
247 this._object._applyToDOMNode(this._ds, this._DOMnode);
248 else
249 this._object._addReferenceToElement(this._DOMnode);
250 }
251
252 /**
253 * Returns the DOM Element linked to the subject that this assertion is
254 * attached to.
255 */
256 _getSubjectElement() {
257 if (isAttr(this._DOMnode))
258 return this._DOMnode.ownerElement;
259 if (this._isSubjectElement)
260 return this._DOMnode;
261 return this._DOMnode.parentNode;
262 }
263
264 getSubject() {
265 return this._subject;
266 }
267
268 getPredicate() {
269 return this._predicate;
270 }
271
272 getObject() {
273 return this._object;
274 }
275 }
276
277 class RDFNode {
278 equals(rdfnode) {
279 return (rdfnode.constructor === this.constructor &&
280 rdfnode._value == this._value);
281 }
282 }
283
284 /**
285 * A simple literal value
286 */
287 class RDFLiteral extends RDFNode {
288 constructor(value) {
289 super();
290 this._value = value;
291 }
292
293 /**
294 * This stores the value of the literal in the given DOM node
295 */
296 _applyToDOMNode(ds, domnode) {
297 if (isElement(domnode))
298 domnode.textContent = this._value;
299 else if (isAttr(domnode))
300 domnode.value = this._value;
301 else
302 throw new Error("cannot use this node for a literal");
303 }
304
305 getValue() {
306 return this._value;
307 }
308 }
309
310 /**
311 * A literal that is integer typed.
312 */
313 class RDFIntLiteral extends RDFLiteral {
314 constructor(value) {
315 super(parseInt(value));
316 }
317
318 /**
319 * This stores the value of the literal in the given DOM node
320 */
321 _applyToDOMNode(ds, domnode) {
322 if (!isElement(domnode))
323 throw new Error("cannot use this node for a literal");
324
325 RDFLiteral.prototype._applyToDOMNode.call(this, ds, domnode);
326 var prefix = ds._resolvePrefix(domnode, `${NS_NC}parseType`);
327 domnode.setAttributeNS(prefix.namespaceURI, prefix.qname, "Integer");
328 }
329 }
330
331 /**
332 * A literal that represents a date.
333 */
334 class RDFDateLiteral extends RDFLiteral {
335 constructor(value) {
336 if (!(value instanceof Date))
337 throw new Error("RDFDateLiteral must be constructed with a Date object");
338
339 super(value);
340 }
341
342 /**
343 * This stores the value of the literal in the given DOM node
344 */
345 _applyToDOMNode(ds, domnode) {
346 if (!isElement(domnode))
347 throw new Error("cannot use this node for a literal");
348
349 domnode.textContent = this._value.getTime();
350 var prefix = ds._resolvePrefix(domnode, `${NS_NC}parseType`);
351 domnode.setAttributeNS(prefix.namespaceURI, prefix.qname, "Date");
352 }
353 }
354
355 /**
356 * This is an RDF node that can be a subject so a resource or a blank node
357 */
358 class RDFSubject extends RDFNode {
359 constructor(ds) {
360 super();
361 // A lookup of the assertions with this as the subject. Keyed on predicate
362 this._assertions = {};
363 // A lookup of the assertions with this as the object. Keyed on predicate
364 this._backwards = {};
365 // The datasource this subject belongs to
366 this._ds = ds;
367 // The DOM elements in the document that represent this subject. Array of Element
368 this._elements = [];
369 }
370
371 /**
372 * Creates a new Element in the document for holding assertions about this
373 * subject. The URI controls what tagname to use.
374 */
375 _createElement(uri) {
376 // Seek an appropriate reference to this node to add this node under
377 var parent = null;
378 for (var p in this._backwards) {
379 for (let back of this._backwards[p]) {
380 // Don't add under an rdf:type
381 if (back.getPredicate() == RDF_R("type"))
382 continue;
383 // The assertion already has a child node, probably one of ours
384 if (back._DOMnode.firstChild)
385 continue;
386 parent = back._DOMnode;
387 var element = this._ds._addElement(parent, uri);
388 this._removeReferenceFromElement(parent);
389 break;
390 }
391 if (parent)
392 break;
393 }
394
395 // No back assertions that are sensible to use
396 if (!parent)
397 element = this._ds._addElement(this._ds._document.documentElement, uri);
398
399 element.listCounter = 1;
400 this._applyToElement(element);
401 this._elements.push(element);
402 return element;
403 }
404
405 /**
406 * When a DOM node representing this subject is removed from the document
407 * we must remove the node and recreate any child assertions elsewhere.
408 */
409 _removeElement(element) {
410 var pos = this._elements.indexOf(element);
411 if (pos < 0)
412 throw new Error("invalid element");
413 this._elements.splice(pos, 1);
414 if (element.parentNode != element.ownerDocument.documentElement)
415 this._addReferenceToElement(element.parentNode);
416 this._ds._removeElement(element);
417
418 // Find all the assertions that are represented here and create new
419 // nodes for them.
420 for (var predicate in this._assertions) {
421 for (let assertion of this._assertions[predicate]) {
422 if (assertion._getSubjectElement() == element)
423 this._createDOMNodeForAssertion(assertion);
424 }
425 }
426 }
427
428 /**
429 * Creates a DOM node to represent the assertion in the document. If the
430 * assertion has rdf:type as the predicate then an attempt will be made to
431 * create a typed subject Element, otherwise a new property Element is
432 * created. For list items an attempt is made to find an appropriate container
433 * that an rdf:li element can be added to.
434 */
435 _createDOMNodeForAssertion(assertion) {
436 let elements;
437 if (RDF_LISTITEM.test(assertion.getPredicate())) {
438 // Find all the containers
439 elements = this._elements.filter(function(element) {
440 return (element.namespaceURI == NS_RDF && (element.localName == "Seq" ||
441 element.localName == "Bag" ||
442 element.localName == "Alt"));
443 });
444 if (elements.length > 0) {
445 // Look for one whose listCounter matches the item we want to add
446 var item = parseInt(assertion.getPredicate().substring(NS_RDF.length + 1));
447 for (let element of elements) {
448 if (element.listCounter == item) {
449 assertion._DOMnode = this._ds._addElement(element, RDF_R("li"));
450 assertion._applyToDOMNode();
451 element.listCounter++;
452 return;
453 }
454 }
455 // No good container to add to, shove in the first real container
456 assertion._DOMnode = this._ds._addElement(elements[0], assertion.getPredicate());
457 assertion._applyToDOMNode();
458 return;
459 }
460 // TODO No containers, this will end up in a non-container for now
461 } else if (assertion.getPredicate() == RDF_R("type")) {
462 // Try renaming an existing rdf:Description
463 for (let element of this.elements) {
464 if (element.namespaceURI == NS_RDF &&
465 element.localName == "Description") {
466 try {
467 var prefix = this._ds._resolvePrefix(element.parentNode, assertion.getObject().getURI());
468 element = renameNode(element, prefix.namespaceURI, prefix.qname);
469 assertion._DOMnode = element;
470 assertion._isSubjectElement = true;
471 return;
472 } catch (e) {
473 // If the type cannot be sensibly turned into a prefix then just set
474 // as a regular property
475 }
476 }
477 }
478 }
479
480 // Filter out all the containers
481 elements = this._elements.filter(function(element) {
482 return (element.namespaceURI != NS_RDF || (element.localName != "Seq" &&
483 element.localName != "Bag" &&
484 element.localName != "Alt"));
485 });
486 if (elements.length == 0) {
487 // Create a new node of the right type
488 if (assertion.getPredicate() == RDF_R("type")) {
489 try {
490 assertion._DOMnode = this._createElement(assertion.getObject().getURI());
491 assertion._isSubjectElement = true;
492 return;
493 } catch (e) {
494 // If the type cannot be sensibly turned into a prefix then just set
495 // as a regular property
496 }
497 }
498 elements[0] = this._createElement(RDF_R("Description"));
499 }
500 assertion._DOMnode = this._ds._addElement(elements[0], assertion.getPredicate());
501 assertion._applyToDOMNode();
502 }
503
504 /**
505 * Removes the DOM node representing the assertion.
506 */
507 _removeDOMNodeForAssertion(assertion) {
508 if (isAttr(assertion._DOMnode)) {
509 var parent = assertion._DOMnode.ownerElement;
510 parent.removeAttributeNode(assertion._DOMnode);
511 } else if (assertion._isSubjectElement) {
512 var domnode = renameNode(assertion._DOMnode, NS_RDF, "Description");
513 if (domnode != assertion._DOMnode) {
514 var pos = this._elements.indexOf(assertion._DOMnode);
515 this._elements.splice(pos, 1, domnode);
516 }
517 parent = domnode;
518 } else {
519 var object = assertion.getObject();
520 if (object instanceof RDFSubject && assertion._DOMnode.firstChild) {
521 // Object is a subject that has an Element inside this assertion's node.
522 for (let element of object._elements) {
523 if (element.parentNode == assertion._DOMnode) {
524 object._removeElement(element);
525 break;
526 }
527 }
528 }
529 parent = assertion._DOMnode.parentNode;
530 if (assertion._DOMnode.namespaceURI == NS_RDF &&
531 assertion._DOMnode.localName == "li")
532 parent.listCounter--;
533 this._ds._removeElement(assertion._DOMnode);
534 }
535
536 // If there are no assertions left using the assertion's containing dom node
537 // then remove it from the document.
538 // TODO could do with a quick lookup list for assertions attached to a node
539 for (var p in this._assertions) {
540 for (let assertion of this._assertions[p]) {
541 if (assertion._getSubjectElement() == parent)
542 return;
543 }
544 }
545 // No assertions left in this element.
546 this._removeElement(parent);
547 }
548
549 /**
550 * Parses the given Element from the DOM document
551 */
552 /* eslint-disable complexity */
553 _parseElement(element) {
554 this._elements.push(element);
555
556 // There might be an inferred rdf:type assertion in the element name
557 if (element.namespaceURI != NS_RDF ||
558 element.localName != "Description") {
559 if (element.namespaceURI == NS_RDF && element.localName == "li")
560 throw new Error("rdf:li is not a valid type for a subject node");
561 var assertion = new RDFAssertion(this, RDF_R("type"),
562 this._ds.getResource(element.namespaceURI + element.localName));
563 assertion._DOMnode = element;
564 assertion._isSubjectElement = true;
565 this._addAssertion(assertion);
566 }
567
568 // Certain attributes can be literal properties
569 for (let attr of element.attributes) {
570 if (attr.namespaceURI == NS_XML || attr.namespaceURI == NS_XMLNS ||
571 attr.nodeName == "xmlns")
572 continue;
573 if ((attr.namespaceURI == NS_RDF || !attr.namespaceURI) &&
574 (["nodeID", "about", "resource", "ID", "parseType"].includes(attr.localName)))
575 continue;
576 var object = null;
577 if (attr.namespaceURI == NS_RDF) {
578 if (attr.localName == "type")
579 object = this._ds.getResource(attr.nodeValue);
580 else if (attr.localName == "li")
581 throw new Error("rdf:li is not allowed as a property attribute");
582 else if (attr.localName == "aboutEach")
583 throw new Error("rdf:aboutEach is deprecated");
584 else if (attr.localName == "aboutEachPrefix")
585 throw new Error("rdf:aboutEachPrefix is deprecated");
586 else if (attr.localName == "aboutEach")
587 throw new Error("rdf:aboutEach is deprecated");
588 else if (attr.localName == "bagID")
589 throw new Error("rdf:bagID is deprecated");
590 }
591 if (!object)
592 object = new RDFLiteral(attr.nodeValue);
593 assertion = new RDFAssertion(this, attr.namespaceURI + attr.localName, object);
594 assertion._DOMnode = attr;
595 this._addAssertion(assertion);
596 }
597
598 var child = element.firstChild;
599 element.listCounter = 1;
600 while (child) {
601 if (isText(child) && /\S/.test(child.nodeValue)) {
602 ERROR(`Text ${child.nodeValue} is not allowed in a subject node`);
603 throw new Error("subject nodes cannot contain text content");
604 } else if (isElement(child)) {
605 object = null;
606 var predicate = child.namespaceURI + child.localName;
607 if (child.namespaceURI == NS_RDF) {
608 if (RDF_PROPERTY_INVALID_TYPES.includes(child.localName) &&
609 !child.localName.match(/^_\d+$/))
610 throw new Error(`${child.nodeName} is an invalid property`);
611 if (child.localName == "li") {
612 predicate = RDF_R(`_${element.listCounter}`);
613 element.listCounter++;
614 }
615 }
616
617 // Check for and bail out on unknown attributes on the property element
618 for (let attr of child.attributes) {
619 // Ignore XML namespaced attributes
620 if (attr.namespaceURI == NS_XML)
621 continue;
622 // These are reserved by XML for future use
623 if (attr.localName.substring(0, 3).toLowerCase() == "xml")
624 continue;
625 // We can handle these RDF attributes
626 if ((!attr.namespaceURI || attr.namespaceURI == NS_RDF) &&
627 ["resource", "nodeID"].includes(attr.localName))
628 continue;
629 // This is a special attribute we handle for compatibility with Mozilla RDF
630 if (attr.namespaceURI == NS_NC &&
631 attr.localName == "parseType")
632 continue;
633 throw new Error(`Attribute ${attr.nodeName} is not supported`);
634 }
635
636 var parseType = child.getAttributeNS(NS_NC, "parseType");
637 if (parseType && parseType != "Date" && parseType != "Integer") {
638 ERROR(`parseType ${parseType} is not supported`);
639 throw new Error("unsupported parseType");
640 }
641
642 var resource = getRDFAttribute(child, "resource");
643 var nodeID = getRDFAttribute(child, "nodeID");
644 if ((resource && (nodeID || parseType)) ||
645 (nodeID && (resource || parseType))) {
646 ERROR("Cannot use more than one of parseType, resource and nodeID on a single node");
647 throw new Error("Invalid rdf assertion");
648 }
649
650 if (resource !== undefined) {
651 var base = Services.io.newURI(element.baseURI);
652 object = this._ds.getResource(base.resolve(resource));
653 } else if (nodeID !== undefined) {
654 if (!nodeID.match(XML_NCNAME))
655 throw new Error("rdf:nodeID must be a valid XML name");
656 object = this._ds.getBlankNode(nodeID);
657 } else {
658 var hasText = false;
659 var childElement = null;
660 var subchild = child.firstChild;
661 while (subchild) {
662 if (isText(subchild) && /\S/.test(subchild.nodeValue)) {
663 hasText = true;
664 } else if (isElement(subchild)) {
665 if (childElement) {
666 new Error(`Multiple object elements found in ${child.nodeName}`);
667 }
668 childElement = subchild;
669 }
670 subchild = subchild.nextSibling;
671 }
672
673 if ((resource || nodeID) && (hasText || childElement)) {
674 ERROR("Assertion references a resource so should not contain additional contents");
675 throw new Error("assertion cannot contain multiple objects");
676 }
677
678 if (hasText && childElement) {
679 ERROR(`Both literal and resource objects found in ${child.nodeName}`);
680 throw new Error("assertion cannot contain multiple objects");
681 }
682
683 if (childElement) {
684 if (parseType) {
685 ERROR("Cannot specify a parseType for an assertion with resource object");
686 throw new Error("parseType is not valid in this context");
687 }
688 object = this._ds._getSubjectForElement(childElement);
689 object._parseElement(childElement);
690 } else if (parseType == "Integer") {
691 object = new RDFIntLiteral(child.textContent);
692 } else if (parseType == "Date") {
693 object = new RDFDateLiteral(new Date(child.textContent));
694 } else {
695 object = new RDFLiteral(child.textContent);
696 }
697 }
698
699 assertion = new RDFAssertion(this, predicate, object);
700 this._addAssertion(assertion);
701 assertion._DOMnode = child;
702 }
703 child = child.nextSibling;
704 }
705 }
706 /* eslint-enable complexity */
707
708 /**
709 * Adds a new assertion to the internal hashes. Should be called for every
710 * new assertion parsed or created programmatically.
711 */
712 _addAssertion(assertion) {
713 var predicate = assertion.getPredicate();
714 if (predicate in this._assertions)
715 this._assertions[predicate].push(assertion);
716 else
717 this._assertions[predicate] = [ assertion ];
718
719 var object = assertion.getObject();
720 if (object instanceof RDFSubject) {
721 // Create reverse assertion
722 if (predicate in object._backwards)
723 object._backwards[predicate].push(assertion);
724 else
725 object._backwards[predicate] = [ assertion ];
726 }
727 }
728
729 /**
730 * Removes an assertion from the internal hashes. Should be called for all
731 * assertions that are programmatically deleted.
732 */
733 _removeAssertion(assertion) {
734 var predicate = assertion.getPredicate();
735 if (predicate in this._assertions) {
736 var pos = this._assertions[predicate].indexOf(assertion);
737 if (pos >= 0)
738 this._assertions[predicate].splice(pos, 1);
739 if (this._assertions[predicate].length == 0)
740 delete this._assertions[predicate];
741 }
742
743 var object = assertion.getObject();
744 if (object instanceof RDFSubject) {
745 // Delete reverse assertion
746 if (predicate in object._backwards) {
747 pos = object._backwards[predicate].indexOf(assertion);
748 if (pos >= 0)
749 object._backwards[predicate].splice(pos, 1);
750 if (object._backwards[predicate].length == 0)
751 delete object._backwards[predicate];
752 }
753 }
754 }
755
756 /**
757 * Returns the ordinal assertions from this subject in order.
758 */
759 _getChildAssertions() {
760 var assertions = [];
761 for (var i in this._assertions) {
762 if (RDF_LISTITEM.test(i))
763 assertions.push(...this._assertions[i]);
764 }
765 assertions.sort(predicateOrder);
766 return assertions;
767 }
768
769 /**
770 * Compares this to another rdf node
771 */
772 equals(rdfnode) {
773 // subjects are created by the datasource so no two objects ever correspond
774 // to the same one.
775 return this === rdfnode;
776 }
777
778 /**
779 * Adds a new assertion with this as the subject
780 */
781 assert(predicate, object) {
782 if (predicate == RDF_R("type") && !(object instanceof RDFResource))
783 throw new Error("rdf:type must be an RDFResource");
784
785 var assertion = new RDFAssertion(this, predicate, object);
786 this._createDOMNodeForAssertion(assertion);
787 this._addAssertion(assertion);
788 }
789
790 /**
791 * Removes an assertion matching the predicate and node given, if such an
792 * assertion exists.
793 */
794 unassert(predicate, object) {
795 if (!(predicate in this._assertions))
796 return;
797
798 for (let assertion of this._assertions[predicate]) {
799 if (assertion.getObject().equals(object)) {
800 this._removeAssertion(assertion);
801 this._removeDOMNodeForAssertion(assertion);
802 return;
803 }
804 }
805 }
806
807 /**
808 * Returns an array of all the predicates that exist in assertions from this
809 * subject.
810 */
811 getPredicates() {
812 return Object.keys(this._assertions);
813 }
814
815 /**
816 * Returns all objects in assertions with this subject and the given predicate.
817 */
818 getObjects(predicate) {
819 if (predicate in this._assertions)
820 return Array.from(this._assertions[predicate],
821 i => i.getObject());
822
823 return [];
824 }
825
826 /**
827 * Returns all of the ordinal children of this subject in order.
828 */
829 getChildren() {
830 return Array.from(this._getChildAssertions(),
831 i => i.getObject());
832 }
833
834 /**
835 * Removes the child at the given index. This is the index based on the
836 * children returned from getChildren. Forces a reordering of the later
837 * children.
838 */
839 removeChildAt(pos) {
840 if (pos < 0)
841 throw new Error("no such child");
842 var assertions = this._getChildAssertions();
843 if (pos >= assertions.length)
844 throw new Error("no such child");
845 for (var i = pos; i < assertions.length; i++) {
846 this._removeAssertion(assertions[i]);
847 this._removeDOMNodeForAssertion(assertions[i]);
848 }
849 var index = 1;
850 if (pos > 0)
851 index = parseInt(assertions[pos - 1].getPredicate().substring(NS_RDF.length + 1)) + 1;
852 for (let i = pos + 1; i < assertions.length; i++) {
853 assertions[i]._predicate = RDF_R(`_${index}`);
854 this._addAssertion(assertions[i]);
855 this._createDOMNodeForAssertion(assertions[i]);
856 index++;
857 }
858 }
859
860 /**
861 * Removes the child with the given object. It is unspecified which child is
862 * removed if the object features more than once.
863 */
864 removeChild(object) {
865 var assertions = this._getChildAssertions();
866 for (var pos = 0; pos < assertions.length; pos++) {
867 if (assertions[pos].getObject().equals(object)) {
868 for (var i = pos; i < assertions.length; i++) {
869 this._removeAssertion(assertions[i]);
870 this._removeDOMNodeForAssertion(assertions[i]);
871 }
872 var index = 1;
873 if (pos > 0)
874 index = parseInt(assertions[pos - 1].getPredicate().substring(NS_RDF.length + 1)) + 1;
875 for (let i = pos + 1; i < assertions.length; i++) {
876 assertions[i]._predicate = RDF_R(`_${index}`);
877 this._addAssertion(assertions[i]);
878 this._createDOMNodeForAssertion(assertions[i]);
879 index++;
880 }
881 return;
882 }
883 }
884 throw new Error("no such child");
885 }
886
887 /**
888 * Adds a new ordinal child to this subject.
889 */
890 addChild(object) {
891 var max = 0;
892 for (var i in this._assertions) {
893 if (RDF_LISTITEM.test(i))
894 max = Math.max(max, parseInt(i.substring(NS_RDF.length + 1)));
895 }
896 max++;
897 this.assert(RDF_R(`_${max}`), object);
898 }
899
900 /**
901 * This reorders the child assertions to remove duplicates and gaps in the
902 * sequence. Generally this will move all children to be under the same
903 * container element and all represented as an rdf:li
904 */
905 reorderChildren() {
906 var assertions = this._getChildAssertions();
907 for (let assertion of assertions) {
908 this._removeAssertion(assertion);
909 this._removeDOMNodeForAssertion(assertion);
910 }
911 var index = 1;
912 for (let assertion of assertions) {
913 assertion._predicate = RDF_R(`_${index}`);
914 this._addAssertion(assertion);
915 this._createDOMNodeForAssertion(assertion);
916 index++;
917 }
918 }
919
920 /**
921 * Returns the type of this subject or null if there is no specified type.
922 */
923 getType() {
924 var type = this.getProperty(RDF_R("type"));
925 if (type && type instanceof RDFResource)
926 return type.getURI();
927 return null;
928 }
929
930 /**
931 * Tests if a property exists for the given predicate.
932 */
933 hasProperty(predicate) {
934 return (predicate in this._assertions);
935 }
936
937 /**
938 * Retrieves the first property value for the given predicate.
939 */
940 getProperty(predicate) {
941 if (predicate in this._assertions)
942 return this._assertions[predicate][0].getObject();
943 return null;
944 }
945
946 /**
947 * Sets the property value for the given predicate, clearing any existing
948 * values.
949 */
950 setProperty(predicate, object) {
951 // TODO optimise by replacing the first assertion and clearing the rest
952 this.clearProperty(predicate);
953 this.assert(predicate, object);
954 }
955
956 /**
957 * Clears any existing properties for the given predicate.
958 */
959 clearProperty(predicate) {
960 if (!(predicate in this._assertions))
961 return;
962
963 var assertions = this._assertions[predicate];
964 while (assertions.length > 0) {
965 var assertion = assertions[0];
966 this._removeAssertion(assertion);
967 this._removeDOMNodeForAssertion(assertion);
968 }
969 }
970 }
971
972 /**
973 * Creates a new RDFResource for the datasource. Private.
974 */
975 class RDFResource extends RDFSubject {
976 constructor(ds, uri) {
977 if (!(ds instanceof RDFDataSource))
978 throw new Error("datasource must be an RDFDataSource");
979
980 if (!uri)
981 throw new Error("An RDFResource requires a non-null uri");
982
983 super(ds);
984 // This is the uri that the resource represents.
985 this._uri = uri;
986 }
987
988 /**
989 * Sets attributes on the DOM element to mark it as representing this resource
990 */
991 _applyToElement(element) {
992 if (USE_RDFNS_ATTR) {
993 var prefix = this._ds._resolvePrefix(element, RDF_R("about"));
994 element.setAttributeNS(prefix.namespaceURI, prefix.qname, this._uri);
995 } else {
996 element.setAttribute("about", this._uri);
997 }
998 }
999
1000 /**
1001 * Adds a reference to this resource to the given property Element.
1002 */
1003 _addReferenceToElement(element) {
1004 if (USE_RDFNS_ATTR) {
1005 var prefix = this._ds._resolvePrefix(element, RDF_R("resource"));
1006 element.setAttributeNS(prefix.namespaceURI, prefix.qname, this._uri);
1007 } else {
1008 element.setAttribute("resource", this._uri);
1009 }
1010 }
1011
1012 /**
1013 * Removes any reference to this resource from the given property Element.
1014 */
1015 _removeReferenceFromElement(element) {
1016 if (element.hasAttributeNS(NS_RDF, "resource"))
1017 element.removeAttributeNS(NS_RDF, "resource");
1018 if (element.hasAttribute("resource"))
1019 element.removeAttribute("resource");
1020 }
1021
1022 getURI() {
1023 return this._uri;
1024 }
1025 }
1026
1027 /**
1028 * Creates a new blank node. Private.
1029 */
1030 class RDFBlankNode extends RDFSubject {
1031 constructor(ds, nodeID) {
1032 if (!(ds instanceof RDFDataSource))
1033 throw new Error("datasource must be an RDFDataSource");
1034
1035 super(ds);
1036 // The nodeID of this node. May be null if there is no ID.
1037 this._nodeID = nodeID;
1038 }
1039
1040 /**
1041 * Sets attributes on the DOM element to mark it as representing this node
1042 */
1043 _applyToElement(element) {
1044 if (!this._nodeID)
1045 return;
1046 if (USE_RDFNS_ATTR) {
1047 var prefix = this._ds._resolvePrefix(element, RDF_R("nodeID"));
1048 element.setAttributeNS(prefix.namespaceURI, prefix.qname, this._nodeID);
1049 } else {
1050 element.setAttribute("nodeID", this._nodeID);
1051 }
1052 }
1053
1054 /**
1055 * Creates a new Element in the document for holding assertions about this
1056 * subject. The URI controls what tagname to use.
1057 */
1058 _createNewElement(uri) {
1059 // If there are already nodes representing this in the document then we need
1060 // a nodeID to match them
1061 if (!this._nodeID && this._elements.length > 0) {
1062 this._ds._createNodeID(this);
1063 for (let element of this._elements)
1064 this._applyToElement(element);
1065 }
1066
1067 return super._createNewElement.call(uri);
1068 }
1069
1070 /**
1071 * Adds a reference to this node to the given property Element.
1072 */
1073 _addReferenceToElement(element) {
1074 if (this._elements.length > 0 && !this._nodeID) {
1075 // In document elsewhere already
1076 // Create a node ID and update the other nodes referencing
1077 this._ds._createNodeID(this);
1078 for (let element of this._elements)
1079 this._applyToElement(element);
1080 }
1081
1082 if (this._nodeID) {
1083 if (USE_RDFNS_ATTR) {
1084 let prefix = this._ds._resolvePrefix(element, RDF_R("nodeID"));
1085 element.setAttributeNS(prefix.namespaceURI, prefix.qname, this._nodeID);
1086 } else {
1087 element.setAttribute("nodeID", this._nodeID);
1088 }
1089 } else {
1090 // Add the empty blank node, this is generally right since further
1091 // assertions will be added to fill this out
1092 var newelement = this._ds._addElement(element, RDF_R("Description"));
1093 newelement.listCounter = 1;
1094 this._elements.push(newelement);
1095 }
1096 }
1097
1098 /**
1099 * Removes any reference to this node from the given property Element.
1100 */
1101 _removeReferenceFromElement(element) {
1102 if (element.hasAttributeNS(NS_RDF, "nodeID"))
1103 element.removeAttributeNS(NS_RDF, "nodeID");
1104 if (element.hasAttribute("nodeID"))
1105 element.removeAttribute("nodeID");
1106 }
1107
1108 getNodeID() {
1109 return this._nodeID;
1110 }
1111 }
1112
1113 /**
1114 * Creates a new RDFDataSource from the given document. The document will be
1115 * changed as assertions are added and removed to the RDF. Pass a null document
1116 * to start with an empty graph.
1117 */
1118 class RDFDataSource {
1119 constructor(document) {
1120 // All known resources, indexed on URI
1121 this._resources = {};
1122 // All blank nodes
1123 this._allBlankNodes = [];
1124 // All blank nodes with IDs, indexed on ID
1125 this._blankNodes = {};
1126 // Suggested prefixes to use for namespaces, index is prefix, value is namespaceURI.
1127 this._prefixes = {
1128 rdf: NS_RDF,
1129 NC: NS_NC,
1130 };
1131
1132 if (!document) {
1133 // Creating a document through xpcom leaves out the xml prolog so just parse
1134 // something small
1135 var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
1136 createInstance(Ci.nsIDOMParser);
1137 var doctext = `<?xml version="1.0"?>\n<rdf:RDF xmlns:rdf="${NS_RDF}"/>\n`;
1138 document = parser.parseFromString(doctext, "text/xml");
1139 }
1140 // The underlying DOM document for this datasource
1141 this._document = document;
1142 this._parseDocument();
1143 }
1144
1145 static loadFromString(text) {
1146 let parser = new DOMParser();
1147 let document = parser.parseFromString(text, "application/xml");
1148
1149 return new this(document);
1150 }
1151
1152 static loadFromBuffer(buffer) {
1153 let parser = new DOMParser();
1154 let document = parser.parseFromBuffer(new Uint8Array(buffer), "application/xml");
1155
1156 return new this(document);
1157 }
1158
1159 static async loadFromFile(uri) {
1160 if (uri instanceof Ci.nsIFile)
1161 uri = Services.io.newFileURI(uri);
1162 else if (typeof(uri) == "string")
1163 uri = Services.io.newURI(uri);
1164
1165 let resp = await fetch(uri.spec);
1166 return this.loadFromBuffer(await resp.arrayBuffer());
1167 }
1168
1169 get uri() {
1170 return this._document.documentURI;
1171 }
1172
1173 /**
1174 * Creates a new nodeID for an unnamed blank node. Just node<number>.
1175 */
1176 _createNodeID(blanknode) {
1177 var i = 1;
1178 while (`node${i}` in this._blankNodes)
1179 i++;
1180 blanknode._nodeID = `node${i}`;
1181 this._blankNodes[blanknode._nodeID] = blanknode;
1182 }
1183
1184 /**
1185 * Returns an rdf subject for the given DOM Element. If the subject has not
1186 * been seen before a new one is created.
1187 */
1188 _getSubjectForElement(element) {
1189 if (element.namespaceURI == NS_RDF &&
1190 RDF_NODE_INVALID_TYPES.includes(element.localName))
1191 throw new Error(`${element.nodeName} is not a valid class for a subject node`);
1192
1193 var about = getRDFAttribute(element, "about");
1194 var id = getRDFAttribute(element, "ID");
1195 var nodeID = getRDFAttribute(element, "nodeID");
1196
1197 if ((about && (id || nodeID)) ||
1198 (nodeID && (id || about))) {
1199 ERROR("More than one of about, ID and nodeID present on the same subject");
1200 throw new Error("invalid subject in rdf");
1201 }
1202
1203 if (about !== undefined) {
1204 let base = Services.io.newURI(element.baseURI);
1205 return this.getResource(base.resolve(about));
1206 }
1207 if (id !== undefined) {
1208 if (!id.match(XML_NCNAME))
1209 throw new Error("rdf:ID must be a valid XML name");
1210 let base = Services.io.newURI(element.baseURI);
1211 return this.getResource(base.resolve(`#${id}`));
1212 }
1213 if (nodeID !== undefined)
1214 return this.getBlankNode(nodeID);
1215 return this.getBlankNode(null);
1216 }
1217
1218 /**
1219 * Parses the document for subjects at the top level.
1220 */
1221 _parseDocument() {
1222 if (!this._document.documentElement) {
1223 ERROR("No document element in document");
1224 throw new Error("document contains no root element");
1225 }
1226
1227 if (this._document.documentElement.namespaceURI != NS_RDF ||
1228 this._document.documentElement.localName != "RDF") {
1229 ERROR(`${this._document.documentElement.nodeName} is not rdf:RDF`);
1230 throw new Error("document does not appear to be RDF");
1231 }
1232
1233 var domnode = this._document.documentElement.firstChild;
1234 while (domnode) {
1235 if (isText(domnode) && /\S/.test(domnode.nodeValue)) {
1236 ERROR("RDF does not allow for text in the root of the document");
1237 throw new Error("invalid markup in document");
1238 } else if (isElement(domnode)) {
1239 var subject = this._getSubjectForElement(domnode);
1240 subject._parseElement(domnode);
1241 }
1242 domnode = domnode.nextSibling;
1243 }
1244 }
1245
1246 /**
1247 * Works out a sensible namespace prefix to use for the given uri. node should
1248 * be the parent of where the element is to be inserted, or the node that an
1249 * attribute is to be added to. This will recursively walk to the top of the
1250 * document finding an already registered prefix that matches for the uri.
1251 * If none is found a new prefix is registered.
1252 * This returns an object with keys namespaceURI, prefix, localName and qname.
1253 * Pass null or undefined for badPrefixes for the first call.
1254 */
1255 _resolvePrefix(domnode, uri, badPrefixes) {
1256 if (!badPrefixes)
1257 badPrefixes = [];
1258
1259 // No known prefix, try to create one from the lookup list
1260 if (!domnode || isDocument(domnode)) {
1261 for (let i in this._prefixes) {
1262 if (badPrefixes.includes(i))
1263 continue;
1264 if (this._prefixes[i] == uri.substring(0, this._prefixes[i].length)) {
1265 var local = uri.substring(this._prefixes[i].length);
1266 var test = URI_SUFFIX.exec(local);
1267 // Remaining part of uri is a good XML Name
1268 if (test && test[0] == local) {
1269 this._document.documentElement.setAttributeNS(NS_XMLNS, `xmlns:${i}`, this._prefixes[i]);
1270 return {
1271 namespaceURI: this._prefixes[i],
1272 prefix: i,
1273 localName: local,
1274 qname: i ? `${i}:${local}` : local,
1275 };
1276 }
1277 }
1278 }
1279
1280 // No match, make something up
1281 test = URI_SUFFIX.exec(uri);
1282 if (test) {
1283 var namespaceURI = uri.substring(0, uri.length - test[0].length);
1284 local = test[0];
1285 let i = 1;
1286 while (badPrefixes.includes(`NS${i}`))
1287 i++;
1288 this._document.documentElement.setAttributeNS(NS_XMLNS, `xmlns:NS${i}`, namespaceURI);
1289 return {
1290 namespaceURI,
1291 prefix: `NS${i}`,
1292 localName: local,
1293 qname: `NS${i}:${local}`,
1294 };
1295 }
1296 // There is no end part of this URI that is an XML Name
1297 throw new Error(`invalid node name: ${uri}`);
1298 }
1299
1300 for (let attr of domnode.attributes) {
1301 // Not a namespace declaration, ignore this attribute
1302 if (attr.namespaceURI != NS_XMLNS && attr.nodeName != "xmlns")
1303 continue;
1304
1305 var prefix = attr.prefix ? attr.localName : "";
1306 // Seen this prefix before, cannot use it
1307 if (badPrefixes.includes(prefix))
1308 continue;
1309
1310 // Namespace matches the start of the uri
1311 if (attr.value == uri.substring(0, attr.value.length)) {
1312 local = uri.substring(attr.value.length);
1313 test = URI_SUFFIX.exec(local);
1314 // Remaining part of uri is a good XML Name
1315 if (test && test[0] == local) {
1316 return {
1317 namespaceURI: attr.value,
1318 prefix,
1319 localName: local,
1320 qname: prefix ? `${prefix}:${local}` : local,
1321 };
1322 }
1323 }
1324
1325 badPrefixes.push(prefix);
1326 }
1327
1328 // No prefix found here, move up the document
1329 return this._resolvePrefix(domnode.parentNode, uri, badPrefixes);
1330 }
1331
1332 /**
1333 * Guess the indent level within the given Element. The method looks for
1334 * elements that are preceded by whitespace including a newline. The
1335 * whitespace following the newline is presumed to be the indentation for the
1336 * element.
1337 * If the indentation cannot be guessed then it recurses up the document
1338 * hierarchy until it can guess the indent or until the Document is reached.
1339 */
1340 _guessIndent(element) {
1341 // The indent at document level is 0
1342 if (!element || isDocument(element))
1343 return "";
1344
1345 // Check the text immediately preceding each child node. One could be
1346 // a valid indent
1347 var pretext = "";
1348 var child = element.firstChild;
1349 while (child) {
1350 if (isText(child)) {
1351 pretext += child.nodeValue;
1352 } else if (isElement(child)) {
1353 var result = INDENT.exec(pretext);
1354 if (result)
1355 return result[1];
1356 pretext = "";
1357 }
1358 child = child.nextSibling;
1359 }
1360
1361 // pretext now contains any trailing text in the element. This can be
1362 // the indent of the end tag. If so add a little to it.
1363 result = INDENT.exec(pretext);
1364 if (result)
1365 return `${result[1]} `;
1366
1367 // Check the text immediately before this node
1368 pretext = "";
1369 var sibling = element.previousSibling;
1370 while (sibling && isText(sibling)) {
1371 pretext += sibling.nodeValue;
1372 sibling = sibling.previousSibling;
1373 }
1374
1375 // If there is a sensible indent then just add to it.
1376 result = INDENT.exec(pretext);
1377 if (result)
1378 return `${result[1]} `;
1379
1380 // Last chance, get the indent level for the tag above and add to it
1381 return `${this._guessIndent(element.parentNode)} `;
1382 }
1383
1384 _addElement(parent, uri) {
1385 var prefix = this._resolvePrefix(parent, uri);
1386 var element = this._document.createElementNS(prefix.namespaceURI, prefix.qname);
1387
1388 if (parent.lastChild) {
1389 // We want to insert immediately after the last child element
1390 var last = parent.lastChild;
1391 while (last && isText(last))
1392 last = last.previousSibling;
1393 // No child elements so insert at the start
1394 if (!last)
1395 last = parent.firstChild;
1396 else
1397 last = last.nextSibling;
1398
1399 let indent = this._guessIndent(parent);
1400 parent.insertBefore(this._document.createTextNode(`\n${indent}`), last);
1401 parent.insertBefore(element, last);
1402 } else {
1403 // No children, must indent our element and the end tag
1404 let indent = this._guessIndent(parent.parentNode);
1405 parent.append(`\n${indent} `, element, `\n${indent}`);
1406 }
1407 return element;
1408 }
1409
1410 /**
1411 * Removes the element from its parent. Should also remove surrounding
1412 * white space as appropriate.
1413 */
1414 _removeElement(element) {
1415 var parent = element.parentNode;
1416 var sibling = element.previousSibling;
1417 // Drop any text nodes immediately preceding the element
1418 while (sibling && isText(sibling)) {
1419 var temp = sibling;
1420 sibling = sibling.previousSibling;
1421 parent.removeChild(temp);
1422 }
1423
1424 sibling = element.nextSibling;
1425 // Drop the element
1426 parent.removeChild(element);
1427
1428 // If the next node after element is now the first child then element was
1429 // the first child. If there are no other child elements then remove the
1430 // remaining child nodes.
1431 if (parent.firstChild == sibling) {
1432 while (sibling && isText(sibling))
1433 sibling = sibling.nextSibling;
1434 if (!sibling) {
1435 // No other child elements
1436 while (parent.lastChild)
1437 parent.removeChild(parent.lastChild);
1438 }
1439 }
1440 }
1441
1442 /**
1443 * Requests that a given prefix be used for the namespace where possible.
1444 * This must be called before any assertions are made using the namespace
1445 * and the registration will not override any existing prefix used in the
1446 * document.
1447 */
1448 registerPrefix(prefix, namespaceURI) {
1449 this._prefixes[prefix] = namespaceURI;
1450 }
1451
1452 /**
1453 * Gets a blank node. nodeID may be null and if so a new blank node is created.
1454 * If a nodeID is given then the blank node with that ID is returned or created.
1455 */
1456 getBlankNode(nodeID) {
1457 if (nodeID && nodeID in this._blankNodes)
1458 return this._blankNodes[nodeID];
1459
1460 if (nodeID && !nodeID.match(XML_NCNAME))
1461 throw new Error("rdf:nodeID must be a valid XML name");
1462
1463 var rdfnode = new RDFBlankNode(this, nodeID);
1464 this._allBlankNodes.push(rdfnode);
1465 if (nodeID)
1466 this._blankNodes[nodeID] = rdfnode;
1467 return rdfnode;
1468 }
1469
1470 /**
1471 * Gets all blank nodes
1472 */
1473 getAllBlankNodes() {
1474 return this._allBlankNodes.slice();
1475 }
1476
1477 /**
1478 * Gets the resource for the URI. The resource is created if it has not been
1479 * used already.
1480 */
1481 getResource(uri) {
1482 if (uri in this._resources)
1483 return this._resources[uri];
1484
1485 var resource = new RDFResource(this, uri);
1486 this._resources[uri] = resource;
1487 return resource;
1488 }
1489
1490 /**
1491 * Gets all resources that have been used.
1492 */
1493 getAllResources() {
1494 return Object.values(this._resources);
1495 }
1496
1497 /**
1498 * Returns all blank nodes and resources
1499 */
1500 getAllSubjects() {
1501 return [...Object.values(this._resources),
1502 ...this._allBlankNodes];
1503 }
1504
1505 /**
1506 * Saves the RDF/XML to a string.
1507 */
1508 serializeToString() {
1509 var serializer = new XMLSerializer();
1510 return serializer.serializeToString(this._document);
1511 }
1512
1513 /**
1514 * Saves the RDF/XML to a file.
1515 */
1516 async saveToFile(file) {
1517 return OS.File.writeAtomic(file, new TextEncoder().encode(this.serializeToString()));
1518 }
1519 }
Imprint / Impressum