feat(core): moteur PromptKey — bibliothèque .md, variables, injection, raccourcis
- 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
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
; 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
|
||||
}
|
||||
Reference in New Issue
Block a user