///|
/// Parsed SKILL.md frontmatter (flat fields only for MVP).
pub struct SkillFrontmatter {
name : Option[String]
description : Option[String]
platforms : Array[String]
/// Raw conditions extracted from metadata.hermes block
fallback_for_tools : Array[String]
fallback_for_toolsets : Array[String]
requires_tools : Array[String]
requires_toolsets : Array[String]
/// Free-form tags from metadata.hermes.tags for downstream grouping/search.
tags : Array[String]
}
///| Strip leading/trailing single or double quotes from a string.
fn strip_quotes(s : String) -> String {
let s1 = if s.has_prefix("\"") && s.has_suffix("\"") && s.length() >= 2 {
s[1:s.length() - 1].to_string()
} else {
s
}
if s1.has_prefix("'") && s1.has_suffix("'") && s1.length() >= 2 {
s1[1:s1.length() - 1].to_string()
} else {
s1
}
}
///| Extract the YAML block between --- delimiters.
/// Returns (yaml_block, body_after_delimiters) or ("", original) if no frontmatter.
fn extract_frontmatter_block(content : String) -> (String, String) {
if !content.has_prefix("---") {
return ("", content)
}
// Remove the opening --- and look for closing \n---
let rest = content[3:].to_string() // skip opening ---
// Use regexp to find the closing \n---\n pattern
let re = try {
@regexp.compile("\n---[ \t]*\n")
} catch {
_ => return ("", content)
}
match re.match_(rest) {
None => ("", content)
Some(mr) => {
// before() gives us the yaml block (text before \n---\n)
// after() gives us the body (text after \n---\n)
let yaml_block = mr.before().to_string()
let body = mr.after().to_string()
(yaml_block, body)
}
}
}
///| Parse a YAML-flow-style list: "[a, b, c]" -> ["a", "b", "c"]
fn parse_yaml_list(value : String) -> Array[String] {
let s = value.trim()
if s.has_prefix("[") && s.has_suffix("]") && s.length() >= 2 {
let inner = s[1:s.length() - 1].to_string()
let result : Array[String] = []
for item in inner.split(",") {
let trimmed = strip_quotes(item.to_string().trim().to_string())
if trimmed.length() > 0 {
result.push(trimmed)
}
}
result
} else if s.length() > 0 {
[s.to_string()]
} else {
[]
}
}
///| Extract a named list from a YAML block (single-line flow-style lists).
fn extract_list_field(yaml_block : String, key : String) -> Array[String] {
for line in yaml_block.split("\n") {
let trimmed = line.to_string().trim()
if trimmed.has_prefix(key + ":") {
let value = trimmed[key.length() + 1:].to_string().trim().to_string()
return parse_yaml_list(value)
}
}
[]
}
///| Parse SKILL.md frontmatter from the full file content.
pub fn parse_frontmatter(content : String) -> SkillFrontmatter {
let (yaml_block, _body) = extract_frontmatter_block(content)
if yaml_block.length() == 0 {
return {
name: None,
description: None,
platforms: [],
fallback_for_tools: [],
fallback_for_toolsets: [],
requires_tools: [],
requires_toolsets: [],
tags: [],
}
}
// Parse flat key: value lines
let mut name : Option[String] = None
let mut description : Option[String] = None
for line in yaml_block.split("\n") {
let trimmed = line.to_string().trim()
if trimmed.has_prefix("name:") {
let v = trimmed[5:].to_string().trim().to_string()
if v.length() > 0 {
name = Some(v)
}
} else if trimmed.has_prefix("description:") {
let v = trimmed[12:].to_string().trim().to_string()
if v.length() > 0 {
description = Some(v)
}
}
}
let platforms = extract_list_field(yaml_block, "platforms")
let fallback_for_tools = extract_list_field(yaml_block, "fallback_for_tools")
let fallback_for_toolsets = extract_list_field(
yaml_block, "fallback_for_toolsets",
)
let requires_tools = extract_list_field(yaml_block, "requires_tools")
let requires_toolsets = extract_list_field(yaml_block, "requires_toolsets")
let tags = extract_list_field(yaml_block, "tags")
{
name,
description,
platforms,
fallback_for_tools,
fallback_for_toolsets,
requires_tools,
requires_toolsets,
tags,
}
}
test "parse name from frontmatter" {
let content = "---\nname: tdd-workflow\ndescription: TDD step by step\n---\n\n# Body\n"
let fm = parse_frontmatter(content)
assert_eq(fm.name, Some("tdd-workflow"))
assert_eq(fm.description, Some("TDD step by step"))
}
test "parse platforms list" {
let content = "---\nname: foo\nplatforms: [macos, linux]\n---\nbody\n"
let fm = parse_frontmatter(content)
assert_eq(fm.platforms, ["macos", "linux"])
}
test "empty frontmatter for no --- delimiters" {
let fm = parse_frontmatter("# Just a heading\n")
assert_eq(fm.name, None)
assert_eq(fm.platforms, ([] : Array[String]))
}
test "parse requires_tools from metadata block" {
let content = "---\nname: foo\nmetadata:\n hermes:\n requires_tools: [kubectl]\n---\nbody\n"
let fm = parse_frontmatter(content)
assert_eq(fm.requires_tools, ["kubectl"])
}
test "parse tags from metadata.hermes" {
let content = "---\nname: tagged-skill\ndescription: with tags\nmetadata:\n hermes:\n tags: [alpha, beta]\n---\nbody\n"
let fm = parse_frontmatter(content)
assert_eq(fm.tags, ["alpha", "beta"])
}
test "tags default to empty when absent" {
let content = "---\nname: no-tags\n---\nbody\n"
let fm = parse_frontmatter(content)
assert_eq(fm.tags, ([] : Array[String]))
}