convert method
Converts an RDF graph to a Turtle string representation.
This method serializes the given RDF graph to the Turtle format with advanced formatting features including:
- Automatically detecting and writing prefix declarations
- Grouping triples by subject for more compact output
- Proper indentation and formatting for readability
- Optimizing blank nodes that appear only once as objects by inlining them
- Serializing RDF collections (lists) in the compact Turtle '(item1 item2)' notation
Parameters:
graphThe RDF graph to serialize to TurtlebaseUriOptional base URI to use for resolving relative IRIs and generating shorter references. When provided and includeBaseDeclaration is true, a @base directive will be included in the output. When includeBaseDeclaration is false, the baseUri is still used for URI relativization but not declared in the output.
Returns:
- A properly formatted Turtle string representation of the input graph.
Example:
final graph = RdfGraph();
Implementation
@override
/// Converts an RDF graph to a Turtle string representation.
///
/// This method serializes the given RDF graph to the Turtle format with
/// advanced formatting features including:
/// - Automatically detecting and writing prefix declarations
/// - Grouping triples by subject for more compact output
/// - Proper indentation and formatting for readability
/// - Optimizing blank nodes that appear only once as objects by inlining them
/// - Serializing RDF collections (lists) in the compact Turtle '(item1 item2)' notation
///
/// Parameters:
/// - [graph] The RDF graph to serialize to Turtle
/// - [baseUri] Optional base URI to use for resolving relative IRIs and
/// generating shorter references. When provided and includeBaseDeclaration
/// is true, a @base directive will be included in the output. When
/// includeBaseDeclaration is false, the baseUri is still used for URI
/// relativization but not declared in the output.
///
/// Returns:
/// - A properly formatted Turtle string representation of the input graph.
///
/// Example:
/// ```dart
/// final graph = RdfGraph();
String convert(RdfDataset dataset, {String? baseUri}) {
final buffer = StringBuffer();
// Write base directive if provided and includeBaseDeclaration is true.
// Track whether the last byte written to [buffer] was a newline so we can
// emit the correct number of separator newlines before each named graph
// without ever calling buffer.toString() (which is O(N) per invocation).
var priorEndsWithNewline = false;
if (baseUri != null && _options.includeBaseDeclaration) {
buffer.writeln('@base <$baseUri> .');
priorEndsWithNewline = true;
}
// Collect all graphs for prefix generation
final allGraphs = [
dataset.defaultGraph,
...dataset.namedGraphs.map((ng) => ng.graph)
];
// Generate blank node labels with a monotonic counter — O(N) total,
// no per-graph label-map scan.
final Map<BlankNodeTerm, String> blankNodeLabels = {};
var blankNodeCounter = 0;
for (final graph in allGraphs) {
blankNodeCounter =
_generateBlankNodeLabels(graph, blankNodeLabels, blankNodeCounter);
}
// 1. Generate prefixes by combining all triples from all graphs.
// This ensures consistent prefix generation across the entire dataset
// and avoids conflicts where the same namespace gets different prefixes.
final compactedIris = compactDatasetIris(allGraphs, dataset, baseUri);
_writePrefixes(buffer, compactedIris.prefixes);
if (compactedIris.prefixes.isNotEmpty) priorEndsWithNewline = true;
// 2. Write default graph triples (if any).
if (dataset.defaultGraph.triples.isNotEmpty) {
// Same inter-section separator as for named graphs.
if (buffer.isNotEmpty) {
if (priorEndsWithNewline) {
buffer.writeln();
} else {
buffer.writeln();
buffer.writeln();
}
}
_writeTriples(
buffer, dataset.defaultGraph, compactedIris, blankNodeLabels);
// _writeSubjectGroup ends with ' .' — no trailing newline.
priorEndsWithNewline = false;
}
// 3. Write named graphs (sorted for deterministic output).
final sortedNamedGraphs = dataset.namedGraphs
.where((ng) => ng.graph.triples.isNotEmpty)
.toList()
..sort((a, b) {
final na = a.name;
final nb = b.name;
if (na is IriTerm && nb is IriTerm) return na.value.compareTo(nb.value);
if (na is IriTerm) return -1;
if (nb is IriTerm) return 1;
final la = na is BlankNodeTerm ? blankNodeLabels[na] : null;
final lb = nb is BlankNodeTerm ? blankNodeLabels[nb] : null;
if (la != null && lb != null) return la.compareTo(lb);
if (la != null) return -1;
if (lb != null) return 1;
return identityHashCode(na).compareTo(identityHashCode(nb));
});
for (final namedGraph in sortedNamedGraphs) {
// Ensure exactly one blank line before the GRAPH keyword, using the
// tracked newline state instead of buffer.toString() (which is O(N)).
if (buffer.isNotEmpty) {
if (priorEndsWithNewline) {
buffer.writeln(); // prior '\'\n' + this '\'\n' = blank line
} else {
buffer.writeln(); // terminate ' .'
buffer.writeln(); // blank line
}
}
// Write graph name using the shared compacted IRIs.
final graphNameStr = writeTerm(
namedGraph.name,
iriRole: IriRole.subject,
compactedIris: compactedIris,
blankNodeLabels: blankNodeLabels,
);
if (_options.useGraphKeyword) {
buffer.writeln('GRAPH $graphNameStr {');
} else {
buffer.writeln('$graphNameStr {');
}
// Write triples directly into an indenting sink — avoids allocating a
// separate StringBuffer and converting it to a string + split('\'\n').
final sink = _IndentedSink(buffer, ' ');
_writeTriples(
sink,
namedGraph.graph,
compactedIris,
blankNodeLabels,
);
// The last subject group ends with ' .' but no trailing '\n', so the
// line is still buffered in the sink. Flush it before writing '}'.
sink.flush();
buffer.writeln('}');
priorEndsWithNewline = true;
}
return buffer.toString();
}