/******************************************************************************/ /* */ /* YAMLFrontMatter.cls -- Parse YAML front matter from a Markdown source */ /* ==================================================================== */ /* */ /* This file is part of the Rexx Parser package */ /* [See https://rexx.epbcn.com/rexx-parser/] */ /* */ /* Copyright (c) 2024-2026 Josep Maria Blasco */ /* */ /* License: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0) */ /* */ /* Version history: */ /* */ /* Date Version Details */ /* -------- ------- --------------------------------------------------------- */ /* 20260311 0.5 First public release */ /* */ /******************************************************************************/ /******************************************************************************/ /* */ /* YAMLFrontMatter(source) parses the YAML front matter of a Markdown */ /* document. */ /* */ /* 'source' is an array of source lines. */ /* */ /* Returns a StringTable with the parsed key-value pairs. When a key */ /* maps to a group of subkeys, the value is itself a StringTable. */ /* Returns .nil when no YAML front matter is found. */ /* */ /* Only a subset of YAML is supported: block mappings with scalar values, */ /* up to 3 levels of nesting. Lists, anchors, tags, flow style, and */ /* multiline scalars are not supported. */ /* */ /******************************************************************************/ ::Routine YAMLFrontMatter Public Use Strict Arg source -- The YAML front matter must start at line 1 with "---" If source~items == 0 Then Return .Nil If Strip(source[1]) \== "---" Then Return .Nil -- Collect YAML lines (between opening "---" and closing "---" or "...") Loop i = 2 To source~items s = Strip(source[i]) If s \== "---", s \== "..." Then Iterate yamlLines = source~section(2,i-1) Signal YAMLFrontMatterFound End -- If we ran off the end without finding a closing delimiter, no valid YAML Return .nil YAMLFrontMatterFound: Return ParseYAMLBlock(yamlLines, 1, yamlLines~items, 0) /******************************************************************************/ /* ParseYAMLBlock -- Parse a range of YAML lines at a given indent level */ /* */ /* Returns a StringTable with the parsed key-value pairs. */ /******************************************************************************/ ::Routine ParseYAMLBlock Use Strict Arg lines, first, last, expectedIndent yaml = .StringTable~new Loop i = first To last line = lines[i] stripped1 = Strip(line) If stripped1 == "" Then Iterate -- Skip blank lines If stripped1[1] == "#" Then Iterate -- Skip comment lines -- Determine indentation stripped2 = Strip(line, "L") indent = Length(line) - Length(stripped2) -- If less indented than expected, we've left this block If indent < expectedIndent Then Leave -- Parse key: value Parse Var stripped2 key":"value key = Strip(key) value = Strip(value) -- Remove optional quotes from value If Length(value) >= 2 Then Do If Left(value,1) == '"', Right(value,1) == '"' Then value = SubStr(value, 2, Length(value) - 2) Else If Left(value,1) == "'", Right(value,1) == "'" Then value = SubStr(value, 2, Length(value) - 2) End -- Simple key: value pair If value \== "" Then Do yaml[key] = value Iterate End -- No value: this is a group. Find the sub-block. subFirst = i + 1 If subFirst > last Then Iterate -- Determine the indent of the sub-block from its first non-blank line Do subFirst = subFirst To last If Strip(lines[subFirst]) \== "", - Strip(lines[subFirst])[1] \== "#" Then Leave End If subFirst > last Then Iterate subStripped = Strip(lines[subFirst], "L") subIndent = Length(lines[subFirst]) - Length(subStripped) -- If the sub-block is not more indented, there is no sub-block If subIndent <= indent Then Do yaml[key] = .StringTable~new Iterate End -- Find the end of the sub-block: all lines with indent >= subIndent subLast = subFirst Do j = subFirst + 1 To last If Strip(lines[j]) == "" | Strip(lines[j])[1] == "#" Then Do subLast = j Iterate End jStripped = Strip(lines[j], "L") jIndent = Length(lines[j]) - Length(jStripped) If jIndent < subIndent Then Leave subLast = j End -- Recurse yaml[key] = ParseYAMLBlock(lines, subFirst, subLast, subIndent) i = subLast End Return yaml