convert method

  1. @override
String convert(
  1. RdfDataset dataset, {
  2. String? baseUri,
})
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:

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();
}