compactAllIris method
Implementation
IriCompactionResult compactAllIris(
RdfGraph graph,
Map<String, String> customPrefixes, {
String? baseUri,
}) {
final prefixCandidates = {
..._namespaceMappings.asMap(),
};
prefixCandidates
.removeWhere((key, value) => customPrefixes.values.contains(value));
prefixCandidates.addAll(customPrefixes);
final usedPrefixes = <String, String>{};
// Indexed by IriRole.index (0..IriRole.values.length-1) to avoid record-tuple
// allocations — each map lookup used to create a (IriTerm, IriRole) heap object.
final compactIris = <IriTerm, List<CompactIri?>>{};
// Create an inverted index for quick lookup
final iriToPrefixMap = {
for (final e in prefixCandidates.entries) e.value: e.key
};
if (iriToPrefixMap.length != prefixCandidates.length) {
throw ArgumentError(
'Duplicate namespace URIs found in prefix candidates: $prefixCandidates',
);
}
// Snapshot the original prefix keys before the main loop so the
// post-processing step can distinguish user- / stdlib-provided prefixes
// from generated ones without inspecting the mappings object again.
final originalCandidateKeys = Set<String>.from(prefixCandidates.keys);
// Iterate over triples directly — no intermediate list, no record-tuple
// allocations. Each unique (IriTerm, IriRole) pair is processed exactly once;
// duplicates are skipped via the per-term slot list.
// Tracks the next free index per generated base-prefix (e.g. 'mo' → 3 means
// 'mo1' and 'mo2' are taken). Passed to getOrGeneratePrefix so the
// numbered-suffix search is O(1) amortized instead of O(N²).
final prefixCounters = <String, int>{};
void processIri(IriTerm iri, IriRole role) {
final slots = compactIris[iri];
if (slots != null && slots[role.index] != null) {
return; // already resolved
}
final compacted = compactIri(
iri, role, baseUri, iriToPrefixMap, prefixCandidates, customPrefixes,
prefixCounters: prefixCounters);
final dest =
slots ?? List<CompactIri?>.filled(IriRole.values.length, null);
if (slots == null) compactIris[iri] = dest;
dest[role.index] = compacted;
if (compacted
case PrefixedIri(
prefix: var prefix,
namespace: var namespace,
)) {
// Add the prefix to all relevant maps
usedPrefixes[prefix] = namespace;
final oldNamespace = prefixCandidates[prefix];
final oldPrefix = iriToPrefixMap[namespace];
if (oldNamespace != null && oldNamespace != namespace) {
throw ArgumentError(
'Namespace conflict for prefix "$prefix": '
'already mapped to "$oldNamespace", cannot map to "$namespace".',
);
}
if (oldPrefix != null && oldPrefix != prefix) {
throw ArgumentError(
'Prefix conflict for namespace "$namespace": '
'already mapped to "$oldPrefix", cannot map to "$prefix".',
);
}
// Update candidates with new prefix
prefixCandidates[prefix] = namespace;
iriToPrefixMap[namespace] = prefix; // Update inverse mapping
}
}
for (final triple in graph.triples) {
if (triple.subject is IriTerm) {
processIri(triple.subject as IriTerm, IriRole.subject);
}
if (triple.predicate is IriTerm) {
processIri(triple.predicate as IriTerm, IriRole.predicate);
}
if (triple.object is IriTerm) {
processIri(
triple.object as IriTerm,
triple.predicate == Rdf.type ? IriRole.type : IriRole.object,
);
} else if (triple.object is LiteralTerm) {
processIri((triple.object as LiteralTerm).datatype, IriRole.datatype);
}
}
// Post-processing: deterministic generated-prefix numbering.
//
// Generated prefix numbers (ns1, ns2, …) would otherwise depend on the
// order in which namespaces are first encountered during triple iteration.
// Because triple order can vary across decode→encode round-trips, we
// re-number every generated prefix by sorting its namespace URI
// alphabetically within its base-prefix group (e.g. all "ns*" entries),
// producing stable, order-independent output.
{
final generated = usedPrefixes.entries
.where((e) => !originalCandidateKeys.contains(e.key))
.toList();
if (generated.isNotEmpty) {
// Group by base prefix (strip trailing digits).
// e.g. "ns42" → "ns", "mo1" → "mo".
final digitSuffix = RegExp(r'\d+$');
final groups = <String, List<(String prefix, String namespace)>>{};
for (final e in generated) {
final base = e.key.replaceFirst(digitSuffix, '');
(groups[base] ??= []).add((e.key, e.value));
}
final renaming = <String, String>{}; // oldPrefix → newPrefix
for (final MapEntry(key: base, value: pairs) in groups.entries) {
// Single-member groups are already stable: only one namespace competed
// for this base prefix, so its assigned number (or lack thereof) is
// fully determined by _tryGeneratePrefixFromUrl and is independent of
// triple encounter order.
if (pairs.length <= 1) continue;
// Sort by namespace URI — makes numbering input-order-independent.
pairs.sort((a, b) => a.$2.compareTo(b.$2));
var num = 1;
for (final (oldPrefix, _) in pairs) {
// Skip indices that collide with a pre-existing non-generated
// candidate so those prefixes are never shadowed.
while (originalCandidateKeys.contains('$base$num')) {
num++;
}
final newPrefix = '$base$num';
num++;
if (newPrefix != oldPrefix) renaming[oldPrefix] = newPrefix;
}
}
if (renaming.isNotEmpty) {
// Rebuild usedPrefixes, prefixCandidates, and iriToPrefixMap.
// Renaming may form cycles (e.g. ns2↔ns8), so collect all the
// namespace values BEFORE modifying the maps to avoid overwriting
// an entry that another renaming still needs to read.
final toAdd = <String, String>{}; // newPrefix → namespace
for (final MapEntry(key: old, value: neo) in renaming.entries) {
final ns = usedPrefixes.remove(old)!;
prefixCandidates.remove(old);
iriToPrefixMap[ns] = neo;
toAdd[neo] = ns;
}
usedPrefixes.addAll(toAdd);
prefixCandidates.addAll(toAdd);
// Update every compactIris slot that references a renamed prefix.
for (final slots in compactIris.values) {
for (int i = 0; i < slots.length; i++) {
final slot = slots[i];
if (slot is PrefixedIri) {
final neo = renaming[slot.prefix];
if (neo != null) {
slots[i] = PrefixedIri(neo, slot.namespace, slot.localPart);
}
}
}
}
}
}
}
return IriCompactionResult(
prefixes: usedPrefixes, compactIris: compactIris);
}