OkfDocument.parse constructor

OkfDocument.parse(
  1. String source, {
  2. String? sourcePath,
})

Parses an OKF document.

If the first line is not a --- delimiter, all input is treated as the Markdown body and hasFrontmatter is false. This is syntactically consumable but a concept validator can still report that frontmatter and type are required.

Implementation

factory OkfDocument.parse(String source, {String? sourcePath}) {
  final normalized = _normalizeLineEndings(source);
  final lines = normalized.split('\n');
  if (lines.isEmpty || lines.first.trim() != '---') {
    return OkfDocument(
      body: normalized,
      hasFrontmatter: false,
    );
  }

  int? closingLine;
  for (var index = 1; index < lines.length; index++) {
    if (lines[index].trim() == '---') {
      closingLine = index;
      break;
    }
  }
  if (closingLine == null) {
    throw OkfDocumentException(
      'Unterminated YAML frontmatter block',
      sourcePath: sourcePath,
      line: lines.length,
      column: 1,
    );
  }

  final yamlSource = lines.sublist(1, closingLine).join('\n');
  Object? parsed;
  try {
    parsed = yamlSource.trim().isEmpty ? null : loadYaml(yamlSource);
  } on YamlException catch (error) {
    final start = error.span?.start;
    throw OkfDocumentException(
      'Invalid YAML in frontmatter: ${error.message}',
      sourcePath: sourcePath,
      // Account for the opening delimiter and convert zero-based positions.
      line: start == null ? null : start.line + 2,
      column: start == null ? null : start.column + 1,
    );
  }

  if (parsed != null && parsed is! Map) {
    throw OkfDocumentException(
      'Frontmatter must be a YAML mapping',
      sourcePath: sourcePath,
      line: 2,
      column: 1,
    );
  }

  final frontmatter = <String, Object?>{};
  if (parsed is Map) {
    late final Object? converted;
    try {
      converted = _YamlConversionState().convert(parsed);
    } on _YamlStructureException catch (error) {
      throw OkfDocumentException(
        error.message,
        sourcePath: sourcePath,
        line: 2,
        column: 1,
      );
    }
    for (final entry in (converted! as Map<Object?, Object?>).entries) {
      if (entry.key is! String) {
        throw OkfDocumentException(
          'Frontmatter keys must be strings',
          sourcePath: sourcePath,
          line: 2,
          column: 1,
        );
      }
      frontmatter[entry.key as String] = entry.value;
    }
  }

  var body = lines.sublist(closingLine + 1).join('\n');
  // A single empty line conventionally separates frontmatter from Markdown.
  if (body.startsWith('\n')) {
    body = body.substring(1);
  }
  return OkfDocument(frontmatter: frontmatter, body: body);
}