; JSON.ahk — parser/serializador JSON mínimo para AutoHotkey v2. ; Cubre lo que necesita el puente PressF8: objetos (Map), arrays (Array), ; strings con escapes (\uXXXX incluido), números, true/false/null. ; null se convierte en "" (string vacío). class JSON { static Parse(text) { pos := 1 value := JSON._ParseValue(text, &pos) JSON._SkipWs(text, &pos) if (pos <= StrLen(text)) throw Error("JSON inválido: contenido adicional en posición " pos) return value } static Stringify(value) { if value is Map { parts := [] for k, v in value parts.Push(JSON._Quote(k) ":" JSON.Stringify(v)) out := "{" for i, p in parts out .= (i > 1 ? "," : "") p return out "}" } if value is Array { out := "[" for i, v in value out .= (i > 1 ? "," : "") JSON.Stringify(v) return out "]" } if value is Integer || value is Float return String(value) return JSON._Quote(String(value)) } static _Quote(str) { out := '"' Loop Parse str { ch := A_LoopField code := Ord(ch) if (ch = '"') out .= '\"' else if (ch = "\") out .= "\\" else if (ch = "`n") out .= "\n" else if (ch = "`r") out .= "\r" else if (ch = "`t") out .= "\t" else if (code < 0x20) out .= Format("\u{:04x}", code) else out .= ch } return out '"' } static _SkipWs(text, &pos) { while pos <= StrLen(text) { ch := SubStr(text, pos, 1) if (ch = " " || ch = "`t" || ch = "`n" || ch = "`r" || Ord(ch) = 0xFEFF) pos++ else break } } static _ParseValue(text, &pos) { JSON._SkipWs(text, &pos) ch := SubStr(text, pos, 1) if (ch = "{") return JSON._ParseObject(text, &pos) if (ch = "[") return JSON._ParseArray(text, &pos) if (ch = '"') return JSON._ParseString(text, &pos) if (SubStr(text, pos, 4) = "true") { pos += 4 return true } if (SubStr(text, pos, 5) = "false") { pos += 5 return false } if (SubStr(text, pos, 4) = "null") { pos += 4 return "" } return JSON._ParseNumber(text, &pos) } static _ParseObject(text, &pos) { obj := Map() pos++ ; { JSON._SkipWs(text, &pos) if (SubStr(text, pos, 1) = "}") { pos++ return obj } loop { JSON._SkipWs(text, &pos) key := JSON._ParseString(text, &pos) JSON._SkipWs(text, &pos) if (SubStr(text, pos, 1) != ":") throw Error("JSON inválido: se esperaba ':' en posición " pos) pos++ obj[key] := JSON._ParseValue(text, &pos) JSON._SkipWs(text, &pos) ch := SubStr(text, pos, 1) pos++ if (ch = "}") return obj if (ch != ",") throw Error("JSON inválido: se esperaba ',' o '}' en posición " pos) } } static _ParseArray(text, &pos) { arr := [] pos++ ; [ JSON._SkipWs(text, &pos) if (SubStr(text, pos, 1) = "]") { pos++ return arr } loop { arr.Push(JSON._ParseValue(text, &pos)) JSON._SkipWs(text, &pos) ch := SubStr(text, pos, 1) pos++ if (ch = "]") return arr if (ch != ",") throw Error("JSON inválido: se esperaba ',' o ']' en posición " pos) } } static _ParseString(text, &pos) { if (SubStr(text, pos, 1) != '"') throw Error("JSON inválido: se esperaba string en posición " pos) pos++ out := "" while pos <= StrLen(text) { ch := SubStr(text, pos, 1) if (ch = '"') { pos++ return out } if (ch = "\") { esc := SubStr(text, pos + 1, 1) switch esc { case '"': out .= '"' case "\": out .= "\" case "/": out .= "/" case "b": out .= Chr(8) case "f": out .= Chr(12) case "n": out .= "`n" case "r": out .= "`r" case "t": out .= "`t" case "u": out .= Chr("0x" SubStr(text, pos + 2, 4)) pos += 4 default: throw Error("JSON inválido: escape desconocido \" esc) } pos += 2 } else { out .= ch pos++ } } throw Error("JSON inválido: string sin cerrar") } static _ParseNumber(text, &pos) { ; El ancla ^ se aplica al string completo aunque RegExMatch reciba una ; posición inicial. Analizar el segmento restante permite números dentro ; de objetos/arrays, no solo en la posición 1 del documento. remaining := SubStr(text, pos) if !RegExMatch(remaining, "^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?", &m) throw Error("JSON inválido: valor no reconocido en posición " pos) pos += m.Len return Number(m[0]) } }