graphwise-reactodia 0.0.1-TR10 → 0.0.1-TR12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -776,7 +776,7 @@ var global$1 = (typeof global !== "undefined" ? global :
776
776
  typeof self !== "undefined" ? self :
777
777
  typeof window !== "undefined" ? window : {});
778
778
 
779
- var lookup = [];
779
+ var lookup$1 = [];
780
780
  var revLookup = [];
781
781
  var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array;
782
782
  var inited = false;
@@ -784,7 +784,7 @@ function init () {
784
784
  inited = true;
785
785
  var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
786
786
  for (var i = 0, len = code.length; i < len; ++i) {
787
- lookup[i] = code[i];
787
+ lookup$1[i] = code[i];
788
788
  revLookup[code.charCodeAt(i)] = i;
789
789
  }
790
790
 
@@ -838,7 +838,7 @@ function toByteArray (b64) {
838
838
  }
839
839
 
840
840
  function tripletToBase64 (num) {
841
- return lookup[num >> 18 & 0x3F] + lookup[num >> 12 & 0x3F] + lookup[num >> 6 & 0x3F] + lookup[num & 0x3F]
841
+ return lookup$1[num >> 18 & 0x3F] + lookup$1[num >> 12 & 0x3F] + lookup$1[num >> 6 & 0x3F] + lookup$1[num & 0x3F]
842
842
  }
843
843
 
844
844
  function encodeChunk (uint8, start, end) {
@@ -870,14 +870,14 @@ function fromByteArray (uint8) {
870
870
  // pad the end with zeros, but make sure to not forget the extra bytes
871
871
  if (extraBytes === 1) {
872
872
  tmp = uint8[len - 1];
873
- output += lookup[tmp >> 2];
874
- output += lookup[(tmp << 4) & 0x3F];
873
+ output += lookup$1[tmp >> 2];
874
+ output += lookup$1[(tmp << 4) & 0x3F];
875
875
  output += '==';
876
876
  } else if (extraBytes === 2) {
877
877
  tmp = (uint8[len - 2] << 8) + (uint8[len - 1]);
878
- output += lookup[tmp >> 10];
879
- output += lookup[(tmp >> 4) & 0x3F];
880
- output += lookup[(tmp << 2) & 0x3F];
878
+ output += lookup$1[tmp >> 10];
879
+ output += lookup$1[(tmp >> 4) & 0x3F];
880
+ output += lookup$1[(tmp << 2) & 0x3F];
881
881
  output += '=';
882
882
  }
883
883
 
@@ -6400,6 +6400,567 @@ function escapeRegexp(token) {
6400
6400
  return token.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
6401
6401
  }
6402
6402
  //#endregion
6403
+ //#region src/data/sparql/sparqlModels.ts
6404
+ function mapSparqlResponseIntoRdfJs(response, factory) {
6405
+ function mapSparqlBinding(binding) {
6406
+ const mapped = {};
6407
+ for (const key in binding) if (Object.prototype.hasOwnProperty.call(binding, key)) {
6408
+ const term = binding[key];
6409
+ switch (term.type) {
6410
+ case "uri":
6411
+ mapped[key] = factory.namedNode(term.value);
6412
+ break;
6413
+ case "bnode":
6414
+ mapped[key] = factory.blankNode(term.value);
6415
+ break;
6416
+ case "literal":
6417
+ case "typed-literal":
6418
+ mapped[key] = factory.literal(term.value, term["xml:lang"] ? term["xml:lang"] : term.datatype ? factory.namedNode(term.datatype) : void 0);
6419
+ break;
6420
+ default: throw new Error(`Unexpected SPARQL term type: "${term.type}"`);
6421
+ }
6422
+ }
6423
+ return mapped;
6424
+ }
6425
+ return {
6426
+ ...response,
6427
+ results: {
6428
+ ...response.results,
6429
+ bindings: response.results.bindings.map(mapSparqlBinding)
6430
+ }
6431
+ };
6432
+ }
6433
+ function isRdfIri(term) {
6434
+ return Boolean(term && term.termType === "NamedNode");
6435
+ }
6436
+ function isRdfBlank(term) {
6437
+ return Boolean(term && term.termType === "BlankNode");
6438
+ }
6439
+ function isRdfLiteral(term) {
6440
+ return Boolean(term && term.termType === "Literal");
6441
+ }
6442
+ function isBlankBinding(binding) {
6443
+ const blank = binding;
6444
+ return blank.blankTrgProp !== void 0 || blank.blankTrg !== void 0 || blank.blankSrcProp !== void 0 || blank.blankSrc !== void 0;
6445
+ }
6446
+ /**
6447
+ * IRI prefix for the encoded blank node subtype produced by this module.
6448
+ *
6449
+ * @see {@link isEncodedBlank}
6450
+ */
6451
+ var ENCODED_PREFIX = "urn:reactodia:blank:sparql:";
6452
+ /**
6453
+ * There is no way to refer to an existing blank node from a SPARQL query, and the labels
6454
+ * an endpoint returns for one are local to the response they came in. Instead of trying to
6455
+ * address a blank node, the whole statement set which describes it is encoded into the
6456
+ * element IRI, which makes the identity content-addressed: the same surrounding graph always
6457
+ * yields the same IRI, and every request about such an element can be answered locally by
6458
+ * decoding it back, without another round-trip to the endpoint.
6459
+ *
6460
+ * The trade-off is size: the IRI grows linearly with the number of statements attached to
6461
+ * the blank node, at roughly 300 characters per statement. {@link MAX_RECURSION_DEEP} bounds
6462
+ * how deep nesting can go, but nothing bounds how wide a single blank node can be, so a long
6463
+ * RDF list produces a correspondingly long IRI (measured against the W3C Organization
6464
+ * ontology: 380-1650 characters for its OWL axioms; a synthetic 100-member list: ~28 KB).
6465
+ */
6466
+ function isEncodedBlank$1(iri) {
6467
+ return iri.startsWith(ENCODED_PREFIX);
6468
+ }
6469
+ /**
6470
+ * Additional output bindings which {@link BLANK_NODE_QUERY} produces, to be added
6471
+ * to the outer projection of the lookup query.
6472
+ */
6473
+ var BLANK_NODE_QUERY_PARAMETERS = "?blankType ?blankTrgProp ?blankTrg ?blankSrc ?blankSrcProp";
6474
+ /**
6475
+ * Lookup query addition which describes every blank node in `?inst` by its outgoing
6476
+ * statements and the statement pointing at it.
6477
+ *
6478
+ * The three alternatives handle a plain blank node, an RDF list referenced directly
6479
+ * and a cell in the middle of an RDF list (which is resolved to the list head, so that
6480
+ * a list appears as a single element rather than one element per cell).
6481
+ */
6482
+ var BLANK_NODE_QUERY = `
6483
+ OPTIONAL {
6484
+ FILTER (ISBLANK(?inst)).
6485
+ {
6486
+ ?inst ?blankTrgProp ?blankTrg.
6487
+ ?blankSrc ?blankSrcProp ?inst.
6488
+ FILTER NOT EXISTS { ?inst rdf:first _:smth1 }.
6489
+ BIND("blankNode" as ?blankType)
6490
+ } UNION {
6491
+ ?inst rdf:rest*/rdf:first ?blankTrg.
6492
+ ?blankSrc ?blankSrcProp ?inst.
6493
+ _:smth2 rdf:first ?blankTrg.
6494
+ BIND(?blankSrcProp as ?blankTrgProp)
6495
+ BIND("listHead" as ?blankType)
6496
+ FILTER NOT EXISTS { _:smth3 rdf:rest ?inst }.
6497
+ } UNION {
6498
+ ?listHead rdf:rest* ?inst.
6499
+ FILTER NOT EXISTS { _:smth4 rdf:rest ?listHead }.
6500
+
6501
+ ?listHead rdf:rest*/rdf:first ?blankTrg.
6502
+ ?blankSrc ?blankSrcProp ?listHead.
6503
+ _:smth5 rdf:first ?blankTrg.
6504
+ BIND(?blankSrcProp as ?blankTrgProp)
6505
+ BIND("listHead" as ?blankType)
6506
+ }
6507
+ }
6508
+ `;
6509
+ var XSD_STRING = "http://www.w3.org/2001/XMLSchema#string";
6510
+ function encodeTerm(term) {
6511
+ switch (term.termType) {
6512
+ case "NamedNode": return ["i", term.value];
6513
+ case "BlankNode": return ["b", term.value];
6514
+ case "Literal":
6515
+ if (term.language) return [
6516
+ "l",
6517
+ term.value,
6518
+ term.language
6519
+ ];
6520
+ else if (term.datatype && term.datatype.value !== XSD_STRING) return [
6521
+ "d",
6522
+ term.value,
6523
+ term.datatype.value
6524
+ ];
6525
+ return ["l", term.value];
6526
+ }
6527
+ }
6528
+ function decodeTerm(encoded, factory) {
6529
+ switch (encoded[0]) {
6530
+ case "i": return factory.namedNode(encoded[1]);
6531
+ case "b": return factory.blankNode(encoded[1]);
6532
+ case "l": return factory.literal(encoded[1], encoded[2]);
6533
+ case "d": return factory.literal(encoded[1], factory.namedNode(encoded[2]));
6534
+ default: throw new Error(`Unexpected encoded blank node term: ${JSON.stringify(encoded)}`);
6535
+ }
6536
+ }
6537
+ /**
6538
+ * Encodes the statement set which describes a single blank node into an element IRI.
6539
+ *
6540
+ * Bindings are deduplicated and sorted, so that the same statement set always produces
6541
+ * the same IRI regardless of the order the endpoint returned it in. The blank node label
6542
+ * itself is deliberately left out: it is local to the response it came from and would make
6543
+ * the IRI change between otherwise identical requests.
6544
+ */
6545
+ function encodeId(blankBindings, statementLimit) {
6546
+ const bindingSet = /* @__PURE__ */ new Map();
6547
+ for (const binding of blankBindings) {
6548
+ const encoded = {
6549
+ t: binding.blankType.value,
6550
+ p: encodeTerm(binding.blankTrgProp),
6551
+ o: encodeTerm(binding.blankTrg),
6552
+ s: binding.blankSrc ? encodeTerm(binding.blankSrc) : void 0,
6553
+ sp: binding.blankSrcProp ? encodeTerm(binding.blankSrcProp) : void 0,
6554
+ c: binding.class ? encodeTerm(binding.class) : void 0
6555
+ };
6556
+ bindingSet.set(JSON.stringify(encoded), encoded);
6557
+ }
6558
+ const orderedKeys = Array.from(bindingSet.keys()).sort();
6559
+ const normalizedBindings = (typeof statementLimit === "number" ? orderedKeys.slice(0, Math.max(statementLimit, 0)) : orderedKeys).map((key) => bindingSet.get(key));
6560
+ return ENCODED_PREFIX + toBase64Url(JSON.stringify(normalizedBindings));
6561
+ }
6562
+ /**
6563
+ * Encodes the payload as base64url rather than percent-escaping it.
6564
+ *
6565
+ * A nested blank node is embedded into its parent by its own encoded IRI, and base64url
6566
+ * output contains only characters which need no escaping, so nesting costs a flat 4/3 per
6567
+ * level. Percent-escaping would escape the escapes, roughly doubling the payload at every
6568
+ * level instead.
6569
+ */
6570
+ function toBase64Url(text) {
6571
+ const bytes = new TextEncoder().encode(text);
6572
+ let binary = "";
6573
+ for (const byte of bytes) binary += String.fromCharCode(byte);
6574
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6575
+ }
6576
+ function fromBase64Url(encoded) {
6577
+ const binary = atob(encoded.replace(/-/g, "+").replace(/_/g, "/"));
6578
+ const bytes = new Uint8Array(binary.length);
6579
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
6580
+ return new TextDecoder().decode(bytes);
6581
+ }
6582
+ /**
6583
+ * Decodes an element IRI produced by {@link encodeId} back into the statement set it
6584
+ * was made from, or returns `undefined` if the IRI is not an encoded blank node or
6585
+ * cannot be parsed.
6586
+ */
6587
+ function decodeId(id, factory) {
6588
+ if (!isEncodedBlank$1(id)) return;
6589
+ let parsed;
6590
+ try {
6591
+ const encodedPart = id.substring(27);
6592
+ parsed = JSON.parse(fromBase64Url(encodedPart));
6593
+ if (!Array.isArray(parsed)) return;
6594
+ } catch {
6595
+ return;
6596
+ }
6597
+ try {
6598
+ const inst = factory.namedNode(id);
6599
+ return parsed.map((encoded) => {
6600
+ const binding = {
6601
+ inst,
6602
+ blankType: factory.literal(encoded.t),
6603
+ blankTrgProp: decodeTerm(encoded.p, factory),
6604
+ blankTrg: decodeTerm(encoded.o, factory),
6605
+ blankSrc: encoded.s ? decodeTerm(encoded.s, factory) : void 0,
6606
+ blankSrcProp: encoded.sp ? decodeTerm(encoded.sp, factory) : void 0,
6607
+ class: encoded.c ? decodeTerm(encoded.c, factory) : void 0
6608
+ };
6609
+ binding.label = createLabelForBlankBinding(binding, factory);
6610
+ return binding;
6611
+ });
6612
+ } catch {
6613
+ return;
6614
+ }
6615
+ }
6616
+ function createLabelForBlankBinding(binding, factory) {
6617
+ if (binding.blankType.value === "listHead") return factory.literal("RDFList");
6618
+ const { class: classTerm } = binding;
6619
+ return factory.literal(classTerm ? getLocalName(classTerm.value) ?? classTerm.value : "anonymous");
6620
+ }
6621
+ /**
6622
+ * Deduplicates identical in-flight queries, since the chains being followed
6623
+ * often overlap on their leading statements.
6624
+ */
6625
+ var QueryExecutor = class {
6626
+ queryFunction;
6627
+ running = /* @__PURE__ */ new Map();
6628
+ constructor(queryFunction) {
6629
+ this.queryFunction = queryFunction;
6630
+ }
6631
+ executeQuery(query) {
6632
+ const existing = this.running.get(query);
6633
+ if (existing) return existing;
6634
+ const execution = this.queryFunction(query).then((response) => {
6635
+ this.running.delete(query);
6636
+ return response;
6637
+ }, (error) => {
6638
+ this.running.delete(query);
6639
+ throw error;
6640
+ });
6641
+ this.running.set(query, execution);
6642
+ return execution;
6643
+ }
6644
+ };
6645
+ /**
6646
+ * Replaces the raw blank node bindings in a lookup response with bindings whose `inst`
6647
+ * is an encoded IRI, resolving nested blank nodes along the way.
6648
+ */
6649
+ async function updateLookupResults(result, queryFunction, settings, factory) {
6650
+ const completeBindings = [];
6651
+ const blankBindings = [];
6652
+ for (const binding of result.results.bindings) if (isBlankBinding(binding)) blankBindings.push(binding);
6653
+ else completeBindings.push(binding);
6654
+ if (blankBindings.length === 0) return result;
6655
+ const processedBindings = await processBlankBindings(blankBindings, queryFunction, settings, factory);
6656
+ return {
6657
+ ...result,
6658
+ results: {
6659
+ ...result.results,
6660
+ bindings: completeBindings.concat(processedBindings)
6661
+ }
6662
+ };
6663
+ }
6664
+ /**
6665
+ * Groups raw blank node bindings by the blank node they describe, resolves any nested
6666
+ * blank nodes they point at and rewrites both the instance and the nested targets into
6667
+ * encoded IRIs.
6668
+ *
6669
+ * The bindings are mutated in place and returned.
6670
+ */
6671
+ async function processBlankBindings(blankBindings, queryFunction, settings, factory) {
6672
+ const bindingGroupsById = /* @__PURE__ */ new Map();
6673
+ for (const binding of blankBindings) {
6674
+ let group = bindingGroupsById.get(binding.inst.value);
6675
+ if (!group) {
6676
+ group = [];
6677
+ bindingGroupsById.set(binding.inst.value, group);
6678
+ }
6679
+ group.push(binding);
6680
+ }
6681
+ const nestedChains = [];
6682
+ for (const binding of blankBindings) if (isRdfBlank(binding.blankTrg)) nestedChains.push([binding]);
6683
+ const loadedGroups = await loadRelatedBlankNodes(nestedChains, new QueryExecutor(queryFunction), settings, factory);
6684
+ const { blankNodeStatementLimit } = settings;
6685
+ const idsMap = getEncodedIdDictionary(loadedGroups, factory, blankNodeStatementLimit);
6686
+ for (const group of bindingGroupsById.values()) {
6687
+ for (const binding of group) {
6688
+ const encodedNestedId = idsMap.get(binding.blankTrg.value);
6689
+ if (encodedNestedId !== void 0) binding.blankTrg = factory.namedNode(encodedNestedId);
6690
+ if (!binding.label) binding.label = createLabelForBlankBinding(binding, factory);
6691
+ }
6692
+ updateGroupIds(group, encodeId(group, blankNodeStatementLimit), factory);
6693
+ }
6694
+ return blankBindings.slice();
6695
+ }
6696
+ function getEncodedIdDictionary(blankBindingGroups, factory, statementLimit) {
6697
+ const idDictionary = /* @__PURE__ */ new Map();
6698
+ for (const [key, group] of blankBindingGroups) {
6699
+ const encodedId = encodeId(group, statementLimit);
6700
+ idDictionary.set(key, encodedId);
6701
+ updateGroupIds(group, encodedId, factory);
6702
+ }
6703
+ return idDictionary;
6704
+ }
6705
+ function updateGroupIds(group, newId, factory) {
6706
+ const inst = factory.namedNode(newId);
6707
+ for (const binding of group) binding.inst = inst;
6708
+ }
6709
+ async function loadRelatedBlankNodes(blankChains, queryExecutor, settings, factory, recursionDeep = 1) {
6710
+ const loadedBlankBindings = /* @__PURE__ */ new Map();
6711
+ if (recursionDeep > 3 || blankChains.length === 0) return loadedBlankBindings;
6712
+ const results = await Promise.all(blankChains.map(async (chain) => ({
6713
+ chain,
6714
+ response: await queryExecutor.executeQuery(getQueryForChain(chain, settings))
6715
+ })));
6716
+ await Promise.all(results.map(async ({ chain, response }) => {
6717
+ const bindings = response.results.bindings;
6718
+ if (bindings.length === 0) return;
6719
+ const nestedChains = [];
6720
+ for (const binding of bindings) if (isRdfBlank(binding.blankTrg)) nestedChains.push(chain.concat([binding]));
6721
+ const idsMap = getEncodedIdDictionary(await loadRelatedBlankNodes(nestedChains, queryExecutor, settings, factory, recursionDeep + 1), factory, settings.blankNodeStatementLimit);
6722
+ for (const binding of bindings) {
6723
+ const encodedNestedId = idsMap.get(binding.blankTrg.value);
6724
+ if (encodedNestedId !== void 0) binding.blankTrg = factory.namedNode(encodedNestedId);
6725
+ binding.label = createLabelForBlankBinding(binding, factory);
6726
+ let group = loadedBlankBindings.get(binding.inst.value);
6727
+ if (!group) {
6728
+ group = [];
6729
+ loadedBlankBindings.set(binding.inst.value, group);
6730
+ }
6731
+ group.push(binding);
6732
+ }
6733
+ }));
6734
+ return loadedBlankBindings;
6735
+ }
6736
+ /**
6737
+ * Builds a query which walks the chain of statements from the named node the chain starts at
6738
+ * down to the blank node at its end, and describes that blank node.
6739
+ *
6740
+ * The chain is expressed entirely in IRIs and predicates, since blank node labels from
6741
+ * a previous response cannot be used to address anything in a new query.
6742
+ */
6743
+ function getQueryForChain(blankNodes, settings) {
6744
+ function getQueryBlock(blankNode, index, maxIndex) {
6745
+ const trustableTrgProp = index === 0 || blankNode.blankType.value !== "listHead";
6746
+ const sourceId = index > 0 ? `?inst${index - 1}` : formatChainTerm(blankNode.blankSrc, `?blankAnySrc${index}`);
6747
+ const sourcePropId = trustableTrgProp ? index > 0 ? `?blankTrgProp${index - 1}` : formatChainTerm(blankNode.blankSrcProp, `?blankAnySrcProp${index}`) : `?anyType${index}`;
6748
+ const instPostfix = index === maxIndex ? "" : index.toString();
6749
+ const targetPropId = trustableTrgProp ? formatChainTerm(blankNode.blankTrgProp, `?blankAnyTrgProp${index}`) : `?anyType0${index}`;
6750
+ return `
6751
+ # ======================
6752
+ ${sourceId} ${sourcePropId} ?blankSrc${index}.
6753
+ ${index === 0 && blankNode.blankType.value === "listHead" ? `?blankSrc${index} rdf:rest*/rdf:first ?inst${instPostfix}.` : `?blankSrc${index} ${targetPropId} ?inst${instPostfix}.`}
6754
+ BIND (${formatChainTerm(blankNode.blankTrgProp, `?blankAnyBind${index}`)} as ?blankSrcProp${index}).
6755
+ FILTER (ISBLANK(?inst${instPostfix})).
6756
+ {
6757
+ ?inst${instPostfix} ?blankTrgProp${instPostfix} ?blankTrg${instPostfix}.
6758
+ BIND("blankNode" as ?blankType${instPostfix}).
6759
+ FILTER NOT EXISTS { ?inst${instPostfix} rdf:first _:smth1${index} }.
6760
+ } UNION {
6761
+ ?inst${instPostfix} rdf:rest*/rdf:first ?blankTrg${instPostfix}.
6762
+ ?blankSrc${index} ?blankSrcProp${index} ?inst${instPostfix}.
6763
+ _:smth2${index} rdf:first ?blankTrg${instPostfix}.
6764
+ BIND(?blankSrcProp${index} as ?blankTrgProp${instPostfix})
6765
+ BIND("listHead" as ?blankType${instPostfix})
6766
+ FILTER NOT EXISTS { _:smth3${index} rdf:rest ?inst${instPostfix} }.
6767
+ }
6768
+ OPTIONAL {
6769
+ ?inst${instPostfix} rdf:type ?class${instPostfix}.
6770
+ }
6771
+ `;
6772
+ }
6773
+ const body = blankNodes.map((bn, index) => getQueryBlock(bn, index, blankNodes.length - 1)).join("\n");
6774
+ return `${settings.defaultPrefix}
6775
+ SELECT ?inst ?class ?label ?blankTrgProp ?blankTrg ?blankType
6776
+ WHERE {
6777
+ ${body}
6778
+ }
6779
+ `;
6780
+ }
6781
+ /**
6782
+ * Formats a chain term as an IRI, falling back to a fresh variable when the term is
6783
+ * missing or is itself a blank node, neither of which can be referenced from a query.
6784
+ */
6785
+ function formatChainTerm(term, fallbackVariable) {
6786
+ return isRdfIri(term) ? `<${term.value}>` : fallbackVariable;
6787
+ }
6788
+ /**
6789
+ * Answers {@link DataProvider.elements} for encoded blank nodes without querying the endpoint.
6790
+ *
6791
+ * Literal targets of the blank node are surfaced as element properties; targets which are
6792
+ * entities of their own become links instead, see {@link links}.
6793
+ */
6794
+ function elements(elementIds, factory) {
6795
+ const bindings = [];
6796
+ for (const id of elementIds) {
6797
+ const blankBindings = decodeId(id, factory);
6798
+ if (!blankBindings) continue;
6799
+ for (const binding of blankBindings) if (isRdfLiteral(binding.blankTrg)) bindings.push({
6800
+ ...binding,
6801
+ propType: binding.blankTrgProp,
6802
+ propValue: binding.blankTrg
6803
+ });
6804
+ else bindings.push(binding);
6805
+ }
6806
+ return bindings;
6807
+ }
6808
+ /**
6809
+ * Answers {@link DataProvider.links} for encoded blank nodes without querying the endpoint.
6810
+ *
6811
+ * Only links where both ends are among the requested elements are returned, matching
6812
+ * the behavior of the endpoint-backed query.
6813
+ */
6814
+ function links(elementIds, factory) {
6815
+ const requested = new Set(elementIds);
6816
+ const bindings = [];
6817
+ for (const id of elementIds) {
6818
+ const blankBindings = decodeId(id, factory);
6819
+ if (!blankBindings) continue;
6820
+ for (const binding of blankBindings) {
6821
+ const { inst, blankSrc, blankSrcProp, blankTrg, blankTrgProp } = binding;
6822
+ if (blankSrc && blankSrcProp && requested.has(blankSrc.value)) bindings.push({
6823
+ source: blankSrc,
6824
+ type: blankSrcProp,
6825
+ target: inst
6826
+ });
6827
+ if (!isRdfLiteral(blankTrg) && requested.has(blankTrg.value)) bindings.push({
6828
+ source: inst,
6829
+ type: blankTrgProp,
6830
+ target: blankTrg
6831
+ });
6832
+ }
6833
+ }
6834
+ return bindings;
6835
+ }
6836
+ /**
6837
+ * Answers {@link DataProvider.connectedLinkStats} for an encoded blank node without
6838
+ * querying the endpoint.
6839
+ */
6840
+ function connectedLinkStats(elementId, factory) {
6841
+ const blankBindings = decodeId(elementId, factory);
6842
+ if (!blankBindings) return [];
6843
+ const stats = /* @__PURE__ */ new Map();
6844
+ function getOrCreate(linkTypeId) {
6845
+ let count = stats.get(linkTypeId);
6846
+ if (!count) {
6847
+ count = {
6848
+ id: linkTypeId,
6849
+ inCount: 0,
6850
+ outCount: 0
6851
+ };
6852
+ stats.set(linkTypeId, count);
6853
+ }
6854
+ return count;
6855
+ }
6856
+ for (const binding of blankBindings) {
6857
+ const { blankSrc, blankSrcProp, blankTrg, blankTrgProp } = binding;
6858
+ if (isRdfLiteral(blankTrg)) continue;
6859
+ if (blankSrc && blankSrcProp) {
6860
+ const incoming = getOrCreate(blankSrcProp.value);
6861
+ incoming.inCount += 1;
6862
+ }
6863
+ const outgoing = getOrCreate(blankTrgProp.value);
6864
+ outgoing.outCount += 1;
6865
+ }
6866
+ return Array.from(stats.values());
6867
+ }
6868
+ /**
6869
+ * Answers {@link DataProvider.lookup} for navigation from an encoded blank node without
6870
+ * querying the endpoint, or returns `undefined` when the lookup is not about one.
6871
+ */
6872
+ function lookup(params, factory) {
6873
+ let bindings;
6874
+ if (params.elementTypeId) return;
6875
+ else if (params.refElementId && params.refElementLinkId) bindings = getAllRelatedByLinkTypeElements(params.refElementId, params.refElementLinkId, params.linkDirection, factory);
6876
+ else if (params.refElementId) bindings = getAllRelatedElements(params.refElementId, factory);
6877
+ else return;
6878
+ if (bindings.length === 0) return;
6879
+ if (params.text) {
6880
+ const text = params.text.toLowerCase();
6881
+ bindings = bindings.filter((binding) => binding.inst.value.toLowerCase().includes(text));
6882
+ }
6883
+ return {
6884
+ head: { vars: [] },
6885
+ results: { bindings }
6886
+ };
6887
+ }
6888
+ /**
6889
+ * Answers the element type query for encoded blank nodes without querying the endpoint.
6890
+ */
6891
+ function collectElementTypes$1(elementIds, factory, result) {
6892
+ for (const id of elementIds) {
6893
+ const blankBindings = decodeId(id, factory);
6894
+ if (!blankBindings) continue;
6895
+ for (const binding of blankBindings) if (binding.class) {
6896
+ let types = result.get(id);
6897
+ if (!types) {
6898
+ types = /* @__PURE__ */ new Set();
6899
+ result.set(id, types);
6900
+ }
6901
+ types.add(binding.class.value);
6902
+ }
6903
+ }
6904
+ }
6905
+ /**
6906
+ * Answers navigation from an encoded blank node over a single link type.
6907
+ *
6908
+ * The direction is relative to the blank node: `in` collects the entities which point at it,
6909
+ * `out` the entities it points at.
6910
+ */
6911
+ function getAllRelatedByLinkTypeElements(refElementId, refElementLinkId, linkDirection, factory) {
6912
+ const blankBindings = decodeId(refElementId, factory);
6913
+ if (!blankBindings) return [];
6914
+ const linkType = factory.namedNode(refElementLinkId);
6915
+ const bindings = [];
6916
+ for (const binding of blankBindings) {
6917
+ const { blankSrc, blankSrcProp, blankTrg, blankTrgProp } = binding;
6918
+ if (linkDirection !== "out" && blankSrc && blankSrcProp && blankSrcProp.value === refElementLinkId) pushRelated(bindings, blankSrc, linkType, "in", factory);
6919
+ if (linkDirection !== "in" && blankTrgProp.value === refElementLinkId && !isRdfLiteral(blankTrg)) pushRelated(bindings, blankTrg, linkType, "out", factory);
6920
+ }
6921
+ return bindings;
6922
+ }
6923
+ /**
6924
+ * Answers navigation from an encoded blank node over every link type at once.
6925
+ *
6926
+ * Literal targets are left out: they are the blank node's own properties, not connections.
6927
+ */
6928
+ function getAllRelatedElements(refElementId, factory) {
6929
+ const blankBindings = decodeId(refElementId, factory);
6930
+ if (!blankBindings) return [];
6931
+ const bindings = [];
6932
+ for (const binding of blankBindings) {
6933
+ const { blankSrc, blankSrcProp, blankTrg, blankTrgProp } = binding;
6934
+ if (blankSrc && blankSrcProp) pushRelated(bindings, blankSrc, blankSrcProp, "in", factory);
6935
+ if (!isRdfLiteral(blankTrg)) pushRelated(bindings, blankTrg, blankTrgProp, "out", factory);
6936
+ }
6937
+ return bindings;
6938
+ }
6939
+ /**
6940
+ * Adds the element at the far end of a statement to the result: a named node as-is,
6941
+ * an encoded blank node expanded back into the statements which describe it.
6942
+ *
6943
+ * The link type and the direction are carried over so that the result describes the
6944
+ * connection the same way an endpoint-backed lookup does. A named node is returned bare,
6945
+ * without a label or a type: {@link SparqlDataProvider.lookup} fills those in, since only
6946
+ * the endpoint knows them.
6947
+ */
6948
+ function pushRelated(bindings, term, linkType, direction, factory) {
6949
+ const connection = {
6950
+ link: linkType,
6951
+ direction: factory.literal(direction)
6952
+ };
6953
+ const nested = isRdfIri(term) ? decodeId(term.value, factory) : void 0;
6954
+ if (nested) for (const binding of nested) bindings.push({
6955
+ ...binding,
6956
+ ...connection
6957
+ });
6958
+ else bindings.push({
6959
+ inst: term,
6960
+ ...connection
6961
+ });
6962
+ }
6963
+ //#endregion
6403
6964
  //#region src/data/sparql/requestChunking.ts
6404
6965
  function* chunkArray(items, measure, maxChunkSize) {
6405
6966
  if (!Number.isFinite(maxChunkSize)) {
@@ -6462,46 +7023,6 @@ function* chunkDirectedCrossProduct(from, to, measure, maxChunkSize, excludedTar
6462
7023
  }
6463
7024
  }
6464
7025
  //#endregion
6465
- //#region src/data/sparql/sparqlModels.ts
6466
- function mapSparqlResponseIntoRdfJs(response, factory) {
6467
- function mapSparqlBinding(binding) {
6468
- const mapped = {};
6469
- for (const key in binding) if (Object.prototype.hasOwnProperty.call(binding, key)) {
6470
- const term = binding[key];
6471
- switch (term.type) {
6472
- case "uri":
6473
- mapped[key] = factory.namedNode(term.value);
6474
- break;
6475
- case "bnode":
6476
- mapped[key] = factory.blankNode(term.value);
6477
- break;
6478
- case "literal":
6479
- case "typed-literal":
6480
- mapped[key] = factory.literal(term.value, term["xml:lang"] ? term["xml:lang"] : term.datatype ? factory.namedNode(term.datatype) : void 0);
6481
- break;
6482
- default: throw new Error(`Unexpected SPARQL term type: "${term.type}"`);
6483
- }
6484
- }
6485
- return mapped;
6486
- }
6487
- return {
6488
- ...response,
6489
- results: {
6490
- ...response.results,
6491
- bindings: response.results.bindings.map(mapSparqlBinding)
6492
- }
6493
- };
6494
- }
6495
- function isRdfIri(term) {
6496
- return Boolean(term && term.termType === "NamedNode");
6497
- }
6498
- function isRdfBlank(term) {
6499
- return Boolean(term && term.termType === "BlankNode");
6500
- }
6501
- function isRdfLiteral(term) {
6502
- return Boolean(term && term.termType === "Literal");
6503
- }
6504
- //#endregion
6505
7026
  //#region src/data/sparql/responseHandler.ts
6506
7027
  var TYPE_PREDICATE = "urn:reactodia:sparql:type";
6507
7028
  var LABEL_PREDICATE = "urn:reactodia:sparql:label";
@@ -6780,7 +7301,7 @@ function typeMatchesDomain(config, types) {
6780
7301
  function enrichElement(element, binding, labelPredicate) {
6781
7302
  if (!element) return;
6782
7303
  if (binding.label) appendProperty(element.properties, labelPredicate, binding.label);
6783
- if (binding.class && element.types.indexOf(binding.class.value) < 0) element.types.push(binding.class.value);
7304
+ if (isRdfIri(binding.class) && element.types.indexOf(binding.class.value) < 0) element.types.push(binding.class.value);
6784
7305
  if (binding.propType && binding.propValue && binding.propType.value !== LABEL_PREDICATE) appendProperty(element.properties, binding.propType.value, binding.propValue);
6785
7306
  }
6786
7307
  function appendLabel(container, newLabel) {
@@ -7036,7 +7557,7 @@ var SparqlDataProvider = class {
7036
7557
  options;
7037
7558
  settings;
7038
7559
  queryFunction;
7039
- acceptBlankNodes = false;
7560
+ acceptBlankNodes;
7040
7561
  chunkMeasure;
7041
7562
  linkByPredicate = /* @__PURE__ */ new Map();
7042
7563
  linkById = /* @__PURE__ */ new Map();
@@ -7051,6 +7572,7 @@ var SparqlDataProvider = class {
7051
7572
  this.options = options;
7052
7573
  this.settings = settings;
7053
7574
  this.queryFunction = queryFunction;
7575
+ this.acceptBlankNodes = Boolean(settings.acceptBlankNodes);
7054
7576
  const { chunk, queryMethod = "GET", prepareLabelPredicate, prepareImagePredicate } = this.options;
7055
7577
  if (chunk) {
7056
7578
  if (!Number.isSafeInteger(chunk.maxSize)) throw new Error("SparqlDataProviderOptions.chunk.maxSize should be a non-negative number");
@@ -7174,8 +7696,9 @@ var SparqlDataProvider = class {
7174
7696
  }
7175
7697
  async elements(params) {
7176
7698
  const { elementIds, signal } = params;
7699
+ const queriedIds = this.acceptBlankNodes ? elementIds.filter((iri) => !isEncodedBlank$1(iri)) : elementIds;
7177
7700
  const triples = [];
7178
- if (elementIds.length > 0) await this.queryChunked(elementIds, async (batch) => {
7701
+ if (queriedIds.length > 0) await this.queryChunked(queriedIds, async (batch) => {
7179
7702
  const ids = batch.map(escapeIri).map((id) => ` (${id})`).join(" ");
7180
7703
  const { defaultPrefix, dataLabelProperty, filterOnlyLanguages, elementInfoQuery } = this.settings;
7181
7704
  const query = defaultPrefix + resolveTemplate(elementInfoQuery, {
@@ -7189,15 +7712,18 @@ var SparqlDataProvider = class {
7189
7712
  for (const triple of response) triples.push(triple);
7190
7713
  });
7191
7714
  const types = this.queryManyElementTypes(this.settings.propertyConfigurations.length > 0 ? elementIds : [], signal);
7192
- const elementModels = getElementsInfo(triplesToElementBinding(triples), await types, this.propertyByPredicate, this.labelPredicate, this.openWorldProperties);
7193
- if (this.options.prepareLabels) await attachProperties(Array.from(elementModels.values()), this.options.prepareLabels, this.labelPredicate, signal);
7194
- if (this.options.prepareImages) await prepareElementImages(elementModels, this.options.prepareImages, this.imagePredicate, this.factory, signal);
7715
+ const bindings = triplesToElementBinding(triples);
7716
+ if (this.acceptBlankNodes) bindings.results.bindings.push(...elements(elementIds, this.factory));
7717
+ const elementModels = getElementsInfo(bindings, await types, this.propertyByPredicate, this.labelPredicate, this.openWorldProperties);
7718
+ if (this.options.prepareLabels) await attachProperties(Array.from(elementModels.values()), this.options.prepareLabels, this.labelPredicate, this.acceptBlankNodes, signal);
7719
+ if (this.options.prepareImages) await prepareElementImages(elementModels, this.options.prepareImages, this.imagePredicate, this.factory, this.acceptBlankNodes, signal);
7195
7720
  else if (this.options.imagePropertyUris && this.options.imagePropertyUris.length) await this.attachImages(elementModels, this.options.imagePropertyUris, signal);
7196
7721
  return elementModels;
7197
7722
  }
7198
7723
  async attachImages(elements, imagePropertyIris, signal) {
7199
7724
  const imageProperties = imagePropertyIris.map(escapeIri).map((id) => ` ( ${id} )`).join(" ");
7200
- await this.queryChunked(Array.from(elements.keys()), async (batch) => {
7725
+ const imageableIds = Array.from(elements.keys()).filter((iri) => !(this.acceptBlankNodes && isEncodedBlank$1(iri)));
7726
+ await this.queryChunked(imageableIds, async (batch) => {
7201
7727
  const ids = batch.map((id) => ` ( ${escapeIri(id)} )`).join(" ");
7202
7728
  const query = this.settings.defaultPrefix + `
7203
7729
  SELECT ?inst ?linkType ?image
@@ -7219,12 +7745,14 @@ var SparqlDataProvider = class {
7219
7745
  const { filterOnlyLanguages, linksInfoQuery } = this.settings;
7220
7746
  const propLanguageFilter = formatLanguageFilter("?propValue", filterOnlyLanguages);
7221
7747
  const linkConfigurations = this.formatLinkLinks();
7748
+ const queriedPrimary = this.acceptBlankNodes ? primary.filter((iri) => !isEncodedBlank$1(iri)) : primary;
7749
+ const queriedSecondary = this.acceptBlankNodes ? secondary.filter((iri) => !isEncodedBlank$1(iri)) : secondary;
7222
7750
  let bindings;
7223
- if (primary.length > 0 && secondary.length > 0) if (linksInfoQuery.includes("${ids}")) bindings = this.queryUndirectedLinks(primary, secondary, {
7751
+ if (queriedPrimary.length > 0 && queriedSecondary.length > 0) if (linksInfoQuery.includes("${ids}")) bindings = this.queryUndirectedLinks(queriedPrimary, queriedSecondary, {
7224
7752
  propLanguageFilter,
7225
7753
  linkConfigurations
7226
7754
  }, signal);
7227
- else bindings = this.queryDirectedLinks(primary, secondary, {
7755
+ else bindings = this.queryDirectedLinks(queriedPrimary, queriedSecondary, {
7228
7756
  propLanguageFilter,
7229
7757
  linkConfigurations
7230
7758
  }, signal);
@@ -7235,7 +7763,13 @@ var SparqlDataProvider = class {
7235
7763
  for (const iri of secondary) elementsForTypes.add(iri);
7236
7764
  }
7237
7765
  const types = this.queryManyElementTypes(elementsForTypes ? Array.from(elementsForTypes) : [], signal);
7238
- let linksInfo = getLinksInfo(await bindings, await types, this.linkByPredicate, this.openWorldLinks);
7766
+ let allBindings = await bindings;
7767
+ if (this.acceptBlankNodes) {
7768
+ const connected = new Set(primary);
7769
+ for (const iri of secondary) connected.add(iri);
7770
+ allBindings = [...allBindings, ...links(Array.from(connected), this.factory)];
7771
+ }
7772
+ let linksInfo = getLinksInfo(allBindings, await types, this.linkByPredicate, this.openWorldLinks);
7239
7773
  if (linkTypeIds) {
7240
7774
  const allowedLinkTypes = new Set(linkTypeIds);
7241
7775
  linksInfo = linksInfo.filter((link) => allowedLinkTypes.has(link.linkTypeId));
@@ -7274,6 +7808,7 @@ var SparqlDataProvider = class {
7274
7808
  }
7275
7809
  async connectedLinkStats(params) {
7276
7810
  const { elementId, inexactCount, signal } = params;
7811
+ if (this.acceptBlankNodes && isEncodedBlank$1(elementId)) return connectedLinkStats(elementId, this.factory);
7277
7812
  const { defaultPrefix, linkTypesOfQuery, linkTypesStatisticsQuery, filterTypePattern } = this.settings;
7278
7813
  const bindDirection = /\?direction\b/.test(linkTypesOfQuery);
7279
7814
  const elementIri = escapeIri(elementId);
@@ -7339,11 +7874,48 @@ var SparqlDataProvider = class {
7339
7874
  limit: baseParams.limit === void 0 ? 100 : baseParams.limit
7340
7875
  };
7341
7876
  const types = this.querySingleElementTypes(params.refElementId && this.settings.linkConfigurations.length > 0 ? params.refElementId : void 0, signal);
7342
- const filterQuery = this.createFilterQuery(params);
7343
- const linkedElements = getFilteredData(await this.executeSparqlSelect(filterQuery, { signal }), await types, this.linkByPredicate, this.labelPredicate, this.openWorldLinks);
7344
- if (this.options.prepareLabels) await attachProperties(linkedElements.map((linked) => linked.element), this.options.prepareLabels, this.labelPredicate, signal);
7877
+ let bindings;
7878
+ let navigatedFromBlank = false;
7879
+ if (this.acceptBlankNodes) {
7880
+ const fromBlank = lookup(params, this.factory);
7881
+ if (fromBlank) {
7882
+ bindings = fromBlank;
7883
+ navigatedFromBlank = true;
7884
+ } else bindings = await updateLookupResults(await this.executeSparqlSelect(this.createFilterQuery(params), { signal }), (blankQuery) => this.executeSparqlSelect(blankQuery, { signal }), this.settings, this.factory);
7885
+ } else {
7886
+ const filterQuery = this.createFilterQuery(params);
7887
+ bindings = await this.executeSparqlSelect(filterQuery, { signal });
7888
+ }
7889
+ const linkedElements = getFilteredData(bindings, await types, this.linkByPredicate, this.labelPredicate, this.openWorldLinks);
7890
+ if (navigatedFromBlank) return this.attachNeighborInfo(linkedElements, signal);
7891
+ if (this.options.prepareLabels) await attachProperties(linkedElements.map((linked) => linked.element), this.options.prepareLabels, this.labelPredicate, this.acceptBlankNodes, signal);
7345
7892
  return linkedElements;
7346
7893
  }
7894
+ /**
7895
+ * Fills in the data of the elements found by navigating from a blank node.
7896
+ *
7897
+ * The navigation is answered by decoding the blank node IRI, which knows only the term
7898
+ * at the far end of each statement, so everything else about a named neighbor - its
7899
+ * label, types and properties - still has to be read from the endpoint. Without this
7900
+ * the neighbor would render as its bare IRI.
7901
+ *
7902
+ * {@link elements} applies the {@link SparqlDataProviderOptions.prepareLabels} hook
7903
+ * itself, so the caller must not apply it again.
7904
+ */
7905
+ async attachNeighborInfo(items, signal) {
7906
+ if (items.length === 0) return [];
7907
+ const elementModels = await this.elements({
7908
+ elementIds: items.map((item) => item.element.id),
7909
+ signal
7910
+ });
7911
+ return items.map((item) => {
7912
+ const element = elementModels.get(item.element.id);
7913
+ return element ? {
7914
+ ...item,
7915
+ element
7916
+ } : item;
7917
+ });
7918
+ }
7347
7919
  createFilterQuery(params) {
7348
7920
  if (!params.refElementId && params.refElementLinkId) throw new Error("Cannot execute refElementLink filter without refElement");
7349
7921
  let outerProjection = "?inst ?class ?label";
@@ -7369,10 +7941,14 @@ var SparqlDataProvider = class {
7369
7941
  filterByType = this.settings.filterTypePattern.replace(/[?$]class\b/g, elementTypeIri);
7370
7942
  }
7371
7943
  const { defaultPrefix, dataLabelProperty, lookupQuery, filterOnlyLanguages, filterElementInfoPattern, fullTextSearch } = this.settings;
7372
- const queryElementInfo = resolveTemplate(filterElementInfoPattern, {
7944
+ let queryElementInfo = resolveTemplate(filterElementInfoPattern, {
7373
7945
  dataLabelProperty,
7374
7946
  labelLanguageFilter: formatLanguageFilter("?label", filterOnlyLanguages)
7375
7947
  });
7948
+ if (this.acceptBlankNodes) {
7949
+ outerProjection += ` ${BLANK_NODE_QUERY_PARAMETERS}`;
7950
+ queryElementInfo += BLANK_NODE_QUERY;
7951
+ }
7376
7952
  let filterByText = "";
7377
7953
  let orderBy = "";
7378
7954
  if (params.text) {
@@ -7500,7 +8076,12 @@ var SparqlDataProvider = class {
7500
8076
  if (elements.length === 0) return /* @__PURE__ */ new Map();
7501
8077
  const { filterTypePattern } = this.settings;
7502
8078
  const elementTypes = /* @__PURE__ */ new Map();
7503
- await this.queryChunked(elements, async (batch) => {
8079
+ let queriedElements = elements;
8080
+ if (this.acceptBlankNodes) {
8081
+ queriedElements = elements.filter((iri) => !isEncodedBlank$1(iri));
8082
+ collectElementTypes$1(elements, this.factory, elementTypes);
8083
+ }
8084
+ await this.queryChunked(queriedElements, async (batch) => {
7504
8085
  const query = resolveTemplate("SELECT ?inst ?class { VALUES(?inst) { ${ids} } ${filterTypePattern} }", {
7505
8086
  ids: batch.map((iri) => `(${escapeIri(iri)})`).join(" "),
7506
8087
  filterTypePattern
@@ -7523,15 +8104,16 @@ async function attachLabels(items, fetchLabels, signal) {
7523
8104
  if (itemLabels) item.label = itemLabels;
7524
8105
  }
7525
8106
  }
7526
- async function attachProperties(items, fetchProperties, propertyIri, signal) {
7527
- const properties = await fetchProperties(new Set(items.map((item) => item.id)), signal);
7528
- for (const item of items) {
8107
+ async function attachProperties(items, fetchProperties, propertyIri, acceptBlankNodes, signal) {
8108
+ const resolvable = acceptBlankNodes ? items.filter((item) => !isEncodedBlank(item.id)) : items;
8109
+ const properties = await fetchProperties(new Set(resolvable.map((item) => item.id)), signal);
8110
+ for (const item of resolvable) {
7529
8111
  const itemValues = properties.get(item.id);
7530
8112
  if (itemValues) item.properties[propertyIri] = itemValues;
7531
8113
  }
7532
8114
  }
7533
- function prepareElementImages(elements, fetchImages, imagePropertyIri, factory, signal) {
7534
- return fetchImages(elements.values(), signal).then((images) => {
8115
+ function prepareElementImages(elements, fetchImages, imagePropertyIri, factory, acceptBlankNodes, signal) {
8116
+ return fetchImages(acceptBlankNodes ? Array.from(elements.values()).filter((element) => !isEncodedBlank(element.id)) : elements.values(), signal).then((images) => {
7535
8117
  for (const [iri, image] of images) {
7536
8118
  const entity = elements.get(iri);
7537
8119
  if (entity) appendProperty(entity.properties, imagePropertyIri, factory.namedNode(image));
@@ -23286,6 +23868,35 @@ function ToolbarAction(props) {
23286
23868
  });
23287
23869
  }
23288
23870
  /**
23871
+ * Toolbar action component to open a file selection dialog.
23872
+ *
23873
+ * @category Components
23874
+ */
23875
+ function ToolbarActionOpen(props) {
23876
+ const { className, fileAccept, onSelect, children, ...otherProps } = props;
23877
+ const inputRef = reactExports.useRef(null);
23878
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [/* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarAction, {
23879
+ ...otherProps,
23880
+ className: clsx(className, `${CLASS_NAME$8}__open`),
23881
+ onSelect: () => {
23882
+ inputRef.current?.click();
23883
+ },
23884
+ children
23885
+ }), /* @__PURE__ */ jsxRuntimeExports.jsx("input", {
23886
+ ref: inputRef,
23887
+ role: "none",
23888
+ type: "file",
23889
+ className: `${CLASS_NAME$8}__open-input`,
23890
+ accept: fileAccept,
23891
+ onChange: (e) => {
23892
+ if (e.currentTarget.files) {
23893
+ const file = e.currentTarget.files[0];
23894
+ onSelect(file);
23895
+ }
23896
+ }
23897
+ })] });
23898
+ }
23899
+ /**
23289
23900
  * Toolbar action component to clear diagram content.
23290
23901
  *
23291
23902
  * Clearing the diagram adds a command to the command history.
@@ -30264,6 +30875,16 @@ function getContentFittingBoxForLayout(state) {
30264
30875
  };
30265
30876
  }
30266
30877
 
30878
+ const graphwise_toolbar_action$1 = {
30879
+ "download_diagram.label": "Export to JSON",
30880
+ "download_diagram.title": "Export the diagram to a JSON file",
30881
+ "upload_diagram.label": "Import from JSON",
30882
+ "upload_diagram.title": "Replace the diagram with one from a JSON file"
30883
+ };
30884
+ var enTranslation = {
30885
+ graphwise_toolbar_action: graphwise_toolbar_action$1
30886
+ };
30887
+
30267
30888
  const authoring_state = {
30268
30889
  "entity_action_delete.label": "supprimer",
30269
30890
  "entity_action_delete.title": "Supprimer l'entité",
@@ -30518,6 +31139,12 @@ const standard_link = {
30518
31139
  const toolbar = {
30519
31140
  "menu_toggle.title": "Ouvrir le menu"
30520
31141
  };
31142
+ const graphwise_toolbar_action = {
31143
+ "download_diagram.label": "Exporter en JSON",
31144
+ "download_diagram.title": "Exporter le diagramme en fichier JSON",
31145
+ "upload_diagram.label": "Importer depuis JSON",
31146
+ "upload_diagram.title": "Remplacer le diagramme par celui d'un fichier JSON"
31147
+ };
30521
31148
  const toolbar_action = {
30522
31149
  "clear_all.command": "Tout effacer",
30523
31150
  "clear_all.title": "Supprimer tous les éléments et liens du diagramme",
@@ -30628,6 +31255,7 @@ var frTranslation = {
30628
31255
  standard_element: standard_element,
30629
31256
  standard_link: standard_link,
30630
31257
  toolbar: toolbar,
31258
+ graphwise_toolbar_action: graphwise_toolbar_action,
30631
31259
  toolbar_action: toolbar_action,
30632
31260
  unified_search: unified_search,
30633
31261
  visual_authoring: visual_authoring,
@@ -30642,8 +31270,10 @@ var LanguageKey;
30642
31270
  LanguageKey["FR"] = "fr";
30643
31271
  })(LanguageKey || (LanguageKey = {}));
30644
31272
 
30645
- // English is provided by default from reactodia so we don't need to include it here, unless we override it
31273
+ // English is provided by default from reactodia, so the English bundle holds only our own keys and overrides. If
31274
+ // A key is missing, reactodia defaults to its own translation.
30646
31275
  const TRANSLATIONS = {
31276
+ [LanguageKey.EN]: enTranslation,
30647
31277
  [LanguageKey.FR]: frTranslation
30648
31278
  };
30649
31279
 
@@ -30703,7 +31333,7 @@ function Service(_serviceClass) {
30703
31333
  // no implementation needed. We only use the type checking of the parameter
30704
31334
  }
30705
31335
 
30706
- var __decorate$3 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
31336
+ var __decorate$4 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
30707
31337
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
30708
31338
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
30709
31339
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
@@ -30714,6 +31344,20 @@ var DiagramService_1;
30714
31344
  * Service for diagram related operations and subscriptions
30715
31345
  */
30716
31346
  let DiagramService = DiagramService_1 = class DiagramService {
31347
+ /**
31348
+ * Replaces the diagram content with the given serialization. Element data is requested from the data provider and
31349
+ * the links between the imported elements are validated against it.
31350
+ *
31351
+ * @param model The diagram model to import into.
31352
+ * @param dataProvider The data provider to bind to the diagram.
31353
+ * @param diagram The serialization to import. When `undefined`, the diagram is cleared.
31354
+ * @param signal Optional signal to abort the import.
31355
+ */
31356
+ importDiagram(model, dataProvider, diagram, signal) {
31357
+ // selection is not cleared on import, so we clear it manually to avoid painting the previous selection
31358
+ model.setSelection([]);
31359
+ return model.importLayout({ dataProvider, diagram, signal, validateLinks: true });
31360
+ }
30717
31361
  /**
30718
31362
  * Subscribes to diagram changes. On every debounced change the current layout is
30719
31363
  * exported and passed to the callback.
@@ -30748,11 +31392,11 @@ let DiagramService = DiagramService_1 = class DiagramService {
30748
31392
  DiagramService.ID = 'DiagramService';
30749
31393
  DiagramService.NOTIFY_DEBOUNCE_MS = 300;
30750
31394
  DiagramService.HISTORY_CHANGED_EVENT = 'historyChanged';
30751
- DiagramService = DiagramService_1 = __decorate$3([
31395
+ DiagramService = DiagramService_1 = __decorate$4([
30752
31396
  Service
30753
31397
  ], DiagramService);
30754
31398
 
30755
- var __decorate$2 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
31399
+ var __decorate$3 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
30756
31400
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
30757
31401
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
30758
31402
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
@@ -30767,11 +31411,11 @@ let WindowService = class WindowService {
30767
31411
  }
30768
31412
  };
30769
31413
  WindowService.ID = 'WindowService';
30770
- WindowService = __decorate$2([
31414
+ WindowService = __decorate$3([
30771
31415
  Service
30772
31416
  ], WindowService);
30773
31417
 
30774
- var __decorate$1 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
31418
+ var __decorate$2 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
30775
31419
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
30776
31420
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
30777
31421
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
@@ -30815,11 +31459,11 @@ let DiagramStorageService = DiagramStorageService_1 = class DiagramStorageServic
30815
31459
  };
30816
31460
  DiagramStorageService.ID = 'DiagramStorageService';
30817
31461
  DiagramStorageService.STORAGE_KEY = 'diagram.state';
30818
- DiagramStorageService = DiagramStorageService_1 = __decorate$1([
31462
+ DiagramStorageService = DiagramStorageService_1 = __decorate$2([
30819
31463
  Service
30820
31464
  ], DiagramStorageService);
30821
31465
 
30822
- var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
31466
+ var __decorate$1 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
30823
31467
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
30824
31468
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
30825
31469
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
@@ -30842,7 +31486,7 @@ let EventService = class EventService {
30842
31486
  }
30843
31487
  };
30844
31488
  EventService.ID = 'EventService';
30845
- EventService = __decorate([
31489
+ EventService = __decorate$1([
30846
31490
  Service
30847
31491
  ], EventService);
30848
31492
 
@@ -30936,8 +31580,102 @@ const resetColors = () => {
30936
31580
  typeToIndex.clear();
30937
31581
  };
30938
31582
 
31583
+ var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
31584
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
31585
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
31586
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
31587
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
31588
+ };
31589
+ var DiagramFileService_1;
31590
+ /**
31591
+ * Service for downloading and uploading the diagram as a file. The file content is the same
31592
+ * {@link SerializedDiagram} that is persisted to the local store.
31593
+ */
31594
+ let DiagramFileService = DiagramFileService_1 = class DiagramFileService {
31595
+ /**
31596
+ * Downloads the given diagram serialization as a file with the given name.
31597
+ */
31598
+ download(diagram, fileName) {
31599
+ const window = service(WindowService).getWindow();
31600
+ const blob = new Blob([JSON.stringify(diagram)], { type: DiagramFileService_1.MIME_TYPE });
31601
+ const url = window.URL.createObjectURL(blob);
31602
+ const link = window.document.createElement('a');
31603
+ link.href = url;
31604
+ link.download = fileName;
31605
+ link.click();
31606
+ window.URL.revokeObjectURL(url);
31607
+ }
31608
+ /**
31609
+ * Reads a diagram serialization from the given file.
31610
+ *
31611
+ * @throws Error when the file is not valid JSON or is not a serialized diagram.
31612
+ */
31613
+ async read(file) {
31614
+ let diagram;
31615
+ try {
31616
+ diagram = JSON.parse(await file.text());
31617
+ }
31618
+ catch (error) {
31619
+ throw new Error(`File "${file.name}" is not valid JSON: ${error.message}`);
31620
+ }
31621
+ if (!this.isSerializedDiagram(diagram)) {
31622
+ throw new Error(`File "${file.name}" is not a serialized diagram`);
31623
+ }
31624
+ return diagram;
31625
+ }
31626
+ /**
31627
+ * Checks if the provided value is a serialized diagram
31628
+ */
31629
+ isSerializedDiagram(value) {
31630
+ var _a, _b;
31631
+ const diagram = value;
31632
+ return (diagram === null || diagram === void 0 ? void 0 : diagram['@type']) === 'Diagram'
31633
+ && Array.isArray((_a = diagram.layoutData) === null || _a === void 0 ? void 0 : _a.elements)
31634
+ && Array.isArray((_b = diagram.layoutData) === null || _b === void 0 ? void 0 : _b.links);
31635
+ }
31636
+ };
31637
+ DiagramFileService.ID = 'DiagramFileService';
31638
+ DiagramFileService.FILE_EXTENSION = '.json';
31639
+ DiagramFileService.MIME_TYPE = 'application/json';
31640
+ DiagramFileService = DiagramFileService_1 = __decorate([
31641
+ Service
31642
+ ], DiagramFileService);
31643
+
31644
+ /**
31645
+ * The workspace main menu: Reactodia's default actions, followed by the diagram download and upload.
31646
+ *
31647
+ * Passing a `menu` to the `DefaultWorkspace` replaces its default actions, so they are listed here again.
31648
+ * Rendered inside the workspace, so the actions can use the workspace hooks.
31649
+ */
31650
+ function GraphwiseMenu() {
31651
+ const { model } = useWorkspace();
31652
+ const t = useTranslation();
31653
+ const diagramFileService = service(DiagramFileService);
31654
+ const diagramService = service(DiagramService);
31655
+ const downloadDiagram = () => {
31656
+ const timestamp = new Date().toISOString().replace(/[-:]|\.\d+Z$/g, '');
31657
+ diagramFileService.download(model.exportLayout(), `diagram-${timestamp}${DiagramFileService.FILE_EXTENSION}`);
31658
+ };
31659
+ const uploadDiagram = async (file) => {
31660
+ try {
31661
+ const diagram = await diagramFileService.read(file);
31662
+ await diagramService.importDiagram(model, model.dataProvider, diagram);
31663
+ }
31664
+ catch (error) {
31665
+ console.error('Failed to upload the diagram', error);
31666
+ }
31667
+ };
31668
+ return reactExports.createElement(reactExports.Fragment, null, reactExports.createElement(ToolbarActionClearAll, {}), reactExports.createElement(ToolbarActionExport, { kind: 'exportRaster' }), reactExports.createElement(ToolbarActionExport, { kind: 'exportSvg' }), reactExports.createElement(ToolbarActionExport, { kind: 'print' }), reactExports.createElement(ToolbarAction,
31669
+ // The same icon as ToolbarActionSave. We dont use that action, since it disables saving on refresh
31670
+ { className: 'reactodia-toolbar-action__save', title: t.text('graphwise_toolbar_action.download_diagram.title'), onSelect: downloadDiagram }, t.text('graphwise_toolbar_action.download_diagram.label')), reactExports.createElement(ToolbarActionOpen, {
31671
+ title: t.text('graphwise_toolbar_action.upload_diagram.title'),
31672
+ fileAccept: DiagramFileService.FILE_EXTENSION,
31673
+ onSelect: (file) => void uploadDiagram(file)
31674
+ }, t.text('graphwise_toolbar_action.upload_diagram.label')));
31675
+ }
31676
+
30939
31677
  /**
30940
- * Reactodia ships English as its built-in default bundle, so English needs no override.
31678
+ * Reactodia ships English as its built-in default bundle, so the English bundle only adds our own keys.
30941
31679
  * Other languages are layered on top as partial bundles; unknown codes fall back to English.
30942
31680
  *
30943
31681
  * Reactodia never re-reads the translations, so the component must be remounted
@@ -31055,10 +31793,11 @@ function ReactodiaApp(props) {
31055
31793
  const dataProvider = createDataProvider(props);
31056
31794
  const isReload = props.isReload;
31057
31795
  const savedDiagram = service(DiagramStorageService).load();
31796
+ const diagramService = service(DiagramService);
31058
31797
  if (isReload) {
31059
31798
  // Simply reload without using the current state. This may happen when the user switches the language at runtime.
31060
31799
  // since there is no existing mechanism to re-translate the UI at runtime (not for the UI labels at least)
31061
- await model.importLayout({ dataProvider, diagram: currentDiagram, signal, validateLinks: true });
31800
+ await diagramService.importDiagram(model, dataProvider, currentDiagram, signal);
31062
31801
  }
31063
31802
  else if ((_a = config.seedIris) === null || _a === void 0 ? void 0 : _a.length) {
31064
31803
  await seedIrisToCanvas(context, dataProvider, config.seedIris, signal);
@@ -31069,7 +31808,7 @@ function ReactodiaApp(props) {
31069
31808
  else {
31070
31809
  // Restore the previously saved layout when present, otherwise start empty. Either way this binds the
31071
31810
  // data provider (savedDiagram is undefined -> empty diagram), so the unified search/lookup works.
31072
- await model.importLayout({ dataProvider, diagram: savedDiagram, signal, validateLinks: true });
31811
+ await diagramService.importDiagram(model, dataProvider, savedDiagram, signal);
31073
31812
  }
31074
31813
  }, [language]);
31075
31814
  /**
@@ -31088,7 +31827,7 @@ function ReactodiaApp(props) {
31088
31827
  defaultLanguage: language,
31089
31828
  translations: translationsForLanguage(language),
31090
31829
  typeStyleResolver: resolveTypeStyle
31091
- }, reactExports.createElement(DefaultWorkspace, {}));
31830
+ }, reactExports.createElement(DefaultWorkspace, { menu: reactExports.createElement(GraphwiseMenu) }));
31092
31831
  }
31093
31832
  /**
31094
31833
  * Mounts the Reactodia application into the given container and returns the React root.