; Frontmatter.ahk — Parseur / sérialiseur de l'en-tête des fichiers prompts. ; Contrat strict défini dans le document d'architecture, section 7.3 (sous-ensemble ligne par ligne, ; PAS un YAML complet). Toute déviation produit une erreur avec numéro de ligne, jamais une devinette. class Frontmatter { ; Retourne un Map : { meta: Map, body: String, errors: [String], hasHeader: Bool } static Parse(content) { result := Map("meta", Map(), "body", content, "errors", [], "hasHeader", false) text := StrReplace(content, "`r`n", "`n") lines := StrSplit(text, "`n") if (lines.Length = 0 || Trim(lines[1]) != "---") return result ; pas d'en-tête : titre = nom de fichier, corps = tout (architecture §7.3) ; Trouver le second délimiteur. endIdx := 0 loop lines.Length - 1 { if (Trim(lines[A_Index + 1]) = "---") { endIdx := A_Index + 1 break } } if (endIdx = 0) { result["errors"].Push("Ligne 1 : en-tête ouvert par --- mais jamais fermé.") return result } result["hasHeader"] := true meta := result["meta"] vars := [] meta["variables"] := vars inVars := false currentVar := "" loop endIdx - 2 { idx := A_Index + 1 raw := lines[idx] lineNo := idx line := RTrim(raw) if (Trim(line) = "" || SubStr(Trim(line), 1, 1) = "#") continue ; Commentaire de fin de ligne hors guillemets : on coupe à " #". line := Frontmatter.StripComment(line) if (inVars) { if RegExMatch(line, "^ - (.*)$", &m) { currentVar := Map() vars.Push(currentVar) inner := Trim(m[1]) if (inner != "") Frontmatter.SetPair(currentVar, inner, lineNo, result["errors"]) continue } if RegExMatch(line, "^ (.+)$", &m) { if (currentVar = "") { result["errors"].Push("Ligne " lineNo " : entrée de variable sans élément de liste.") continue } Frontmatter.SetPair(currentVar, Trim(m[1]), lineNo, result["errors"]) continue } ; Fin du bloc variables : on retombe sur le traitement normal ci-dessous. inVars := false } if RegExMatch(line, "^([A-Za-z_][A-Za-z0-9_]*) *: *(.*)$", &m) { key := StrLower(m[1]) val := Trim(m[2]) if (key = "variables") { if (val != "") { result["errors"].Push("Ligne " lineNo " : 'variables:' doit être seul sur sa ligne (liste indentée en dessous).") } inVars := true continue } ; Interdits du sous-ensemble (§7.3). if (val = "|" || val = ">" || InStr(val, "&") = 1 || InStr(val, "*") = 1) { result["errors"].Push("Ligne " lineNo " : valeur multi-ligne ou ancre YAML interdite dans ce format.") continue } meta[key] := Frontmatter.ParseValue(val) } else { result["errors"].Push("Ligne " lineNo " : ligne non reconnue dans l'en-tête : « " Util.Truncate(line, 40) " »") } } if (vars.Length = 0) meta.Delete("variables") ; Corps : tout ce qui suit le second délimiteur, en sautant une ligne vide initiale. bodyLines := [] loop lines.Length - endIdx { bodyLines.Push(lines[endIdx + A_Index]) } body := Join(bodyLines, "`n") body := RegExReplace(body, "^\n+", "") result["body"] := body return result } static StripComment(line) { inQ := false loop parse line { if (A_LoopField = '"') inQ := !inQ else if (A_LoopField = "#" && !inQ && A_Index > 1 && SubStr(line, A_Index - 1, 1) = " ") return RTrim(SubStr(line, 1, A_Index - 2)) } return line } static SetPair(map, pairText, lineNo, errors) { if !RegExMatch(pairText, "^([A-Za-z_][A-Za-z0-9_]*) *: *(.*)$", &m) { errors.Push("Ligne " lineNo " : paire clé: valeur attendue, reçu « " Util.Truncate(pairText, 40) " »") return } map[StrLower(m[1])] := Frontmatter.ParseValue(Trim(m[2])) } ; Valeur : liste [a, b, "c d"] | booléen | nombre (gardé en texte, typé par le consommateur) | chaîne. static ParseValue(val) { if (val = "") return "" if (SubStr(val, 1, 1) = "[" && SubStr(val, -1) = "]") { inner := SubStr(val, 2, StrLen(val) - 2) items := [] if (Trim(inner) = "") return items cur := "", inQ := false loop parse inner { ch := A_LoopField if (ch = '"') inQ := !inQ if (ch = "," && !inQ) { items.Push(Util.Unquote(Trim(cur))) cur := "" } else { cur .= ch } } items.Push(Util.Unquote(Trim(cur))) return items } low := StrLower(val) if (low = "true") return 1 if (low = "false") return 0 return Util.Unquote(val) } ; Sérialisation : ordre stable des clés connues, puis les autres, puis le bloc variables. static Serialize(meta, body) { out := "---`n" order := ["id", "title", "tags", "targets", "favorite", "pinned", "hotkey", "hotstring", "profile", "send_after", "sensitive", "created", "updated"] done := Map() for key in order { if meta.Has(key) { out .= key ": " Frontmatter.FormatValue(key, meta[key]) "`n" done[key] := true } } for key, val in meta { if (key = "variables" || done.Has(key)) continue out .= key ": " Frontmatter.FormatValue(key, val) "`n" } if meta.Has("variables") && meta["variables"].Length > 0 { out .= "variables:`n" for v in meta["variables"] { first := true vOrder := ["name", "label", "type", "choices", "default", "required", "multiline"] for key in vOrder { if v.Has(key) { prefix := first ? " - " : " " out .= prefix key ": " Frontmatter.FormatValue(key, v[key]) "`n" first := false } } for key, val in v { if !HasInArray(vOrder, key) { prefix := first ? " - " : " " out .= prefix key ": " Frontmatter.FormatValue(key, val) "`n" first := false } } } } out .= "---`n`n" body return out } static FormatValue(key, val) { if (val is Array) { parts := [] for item in val parts.Push(Util.QuoteIfNeeded(String(item))) return "[" Join(parts, ", ") "]" } if (key = "favorite" || key = "send_after" || key = "sensitive" || key = "required" || key = "multiline") return (val = 1 || val = "1" || val = "true") ? "true" : "false" return Util.QuoteIfNeeded(String(val)) } } HasInArray(arr, needle) { for v in arr { if (v = needle) return true } return false }