// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Read a skill document.
///
/// The document opens with a `---` fence, then YAML, then a closing `---`, then
/// the Markdown body. The YAML is read as StrictYAML, the dialect the
/// specification was written against: every scalar is text, so `name: 42` is
/// the name forty-two rather than a number, and a frontmatter that means
/// something other than what it looks like is refused instead of guessed.
///
/// `root` is the directory the document came from. [`validate`] compares the
/// directory name with the skill name, and [`Skill::location`] needs it to
/// name the document; leave it out when the text has no directory yet.
///
/// Reading is lenient about the fields themselves: a document with no `name`
/// still parses, and [`validate`] says what is wrong with it. Only a document
/// that is not a skill document at all is refused.
pub fn parse(text : StringView, root? : String) -> Skill raise Refused {
let root = root.unwrap_or("")
guard text.length() >= 3 && text[0] == '-' && text[1] == '-' && text[2] == '-' else {
raise Refused::MissingFrontmatter
}
let close = match find_fence(text, 3) {
Some(index) => index
None => raise Refused::UnclosedFrontmatter
}
let frontmatter = text[3:close]
let body = text[close + 3:].trim().to_owned()
let document = @strict.loads(frontmatter) catch {
error => raise Refused::BadYaml(error.to_string())
}
let fields = match document {
Object(fields) => fields
_ => raise Refused::NotAMapping
}
{
name: field_text(fields, "name"),
description: field_text(fields, "description"),
body,
meta: fields,
root,
}
}
///|
/// The offset of the next `---`, searching from `from`.
///
/// The fence is three hyphens wherever they fall: the closing fence does not
/// have to start a line, which is what the reference reader does too, and a
/// body that mentions `---` further down cannot become the closing fence
/// because only the first one counts.
fn find_fence(text : StringView, from : Int32) -> Int32? {
let last = text.length() - 3
let mut index = from
while index <= last {
if text[index] == '-' && text[index + 1] == '-' && text[index + 2] == '-' {
return Some(index)
}
index = index + 1
}
None
}
///|
/// A frontmatter field as trimmed text. Anything that is not text reads as
/// empty, which [`validate`] then reports.
fn field_text(fields : Map[String, Json], key : String) -> String {
match fields.get(key) {
Some(String(text)) => text.trim().to_owned()
_ => ""
}
}