OkfDocument.parse constructor
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);
}