- PromptStore/Frontmatter : un prompt = un fichier Markdown, en-tête au sous-ensemble strict du document d'architecture (§7), écriture UTF-8 - VariableEngine : variables typées + automatiques (selection, clipboard, date, app, cursor), résolution et profils de destination - Injector/ClipboardGuard : focus capturé/rendu, presse-papiers sauvegardé puis restauré conditionnellement, aucun envoi automatique par défaut - Search : classement flou insensible aux accents, pondéré par l'usage - UsageStats (usage.json), HotkeyManager (conflits signalés, jamais muets) - Settings : config.ini + profils par IA (claude, chatgpt, qwen, terminal…) - PromptKey.ahk : tray, instance unique, capture du contexte, surveillance de la bibliothèque, installation du pack au premier lancement
209 lines
7.8 KiB
AutoHotkey
209 lines
7.8 KiB
AutoHotkey
; 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
|
|
}
|