#Requires AutoHotkey v2.0 ; MOML 2.0 reference port for AutoHotkey v2. ; Contract matches the Python MOML 2.0 reference implementation. class MOMLError extends Error { } class MOMLDict extends Map { HasActive := false Active := "" } class MOMLList extends Array { HasActive := false Active := "" } class MOML2 { static Header := "# MOML 2.0" static Loads(text) { lines := StrSplit(StrReplace(text, "`r`n", "`n"), "`n") if lines.Length = 0 throw MOMLError("First line must be exactly: " this.Header) first := lines[1] if SubStr(first, 1, 1) = Chr(0xFEFF) first := SubStr(first, 2) if first != this.Header throw MOMLError("First line must be exactly: " this.Header) body := "" Loop lines.Length - 1 { if A_Index > 1 body .= "`n" body .= lines[A_Index + 1] } stmts := this._Preprocess(body) result := this._ParseBlock(stmts, 1, true) root := result[1] if !(root is MOMLDict) throw MOMLError("Root of a MOML 2.0 document must be a pairs block, not bare items") return root } static Load(path) { if !RegExMatch(path, "i)\.m2$") throw MOMLError("MOML 2.0 files must use the .m2 extension: " path) return this.Loads(FileRead(path, "UTF-8")) } static Dumps(config) { if !(config is MOMLDict) throw MOMLError("Dumps() requires a MOMLDict root") lines := [this.Header, ""] for line in this._DumpBlock(config, 0) lines.Push(line) return this._Join(lines, "`n") "`n" } static Dump(config, path) { if !RegExMatch(path, "i)\.m2$") throw MOMLError("MOML 2.0 files must use the .m2 extension: " path) f := FileOpen(path, "w", "UTF-8") if !IsObject(f) throw MOMLError("Could not write " path) f.Write(this.Dumps(config)) f.Close() } static Get(config, keys*) { default := "" node := config for key in keys { if !(node is MOMLDict) || !node.Has(key) return default node := node[key] } return node } static GetActive(block) { if block is MOMLDict { if block.HasActive return [block.Active, block[block.Active]] return ["", ""] } if block is MOMLList { if block.HasActive return [block.Active, block.Active] return ["", ""] } return ["", ""] } static _Preprocess(text) { out := [] lines := StrSplit(StrReplace(text, "`r`n", "`n"), "`n") for raw in lines { if SubStr(LTrim(raw), 1, 1) = "#" continue line := Trim(raw) if line != "" out.Push(line) } return out } static _ParseBlock(lines, pos, topLevel) { pairs := MOMLDict() items := MOMLList() hasPairs := false hasItems := false loop { if pos > lines.Length { if topLevel break throw MOMLError("Missing closing '}' - unexpected end of input") } line := lines[pos] lineNo := pos if line = "}" { if topLevel throw MOMLError("Unexpected '}' at top level (line " lineNo ")") return [hasItems ? items : pairs, pos + 1] } blockInfo := this._BlockOpen(line) if IsObject(blockInfo) { if hasItems throw MOMLError("Cannot mix a named block with bare items (line " lineNo ")") star := blockInfo[1] name := blockInfo[2] childResult := this._ParseBlock(lines, pos + 1, false) pairs[name] := childResult[1] if star { if pairs.HasActive throw MOMLError("Duplicate active marker '*' in block") pairs.HasActive := true pairs.Active := name } hasPairs := true pos := childResult[2] continue } pairInfo := this._SplitPair(line) if IsObject(pairInfo) { if hasItems throw MOMLError("Cannot mix key=value pairs with bare items (line " lineNo ")") star := pairInfo[1] key := pairInfo[2] rawValue := pairInfo[3] pairs[key] := this._ParseAssignmentValue(rawValue, lineNo) if star { if pairs.HasActive throw MOMLError("Duplicate active marker '*' in block") pairs.HasActive := true pairs.Active := key } hasPairs := true pos += 1 continue } if hasPairs throw MOMLError("Cannot mix bare items with key=value pairs / blocks (line " lineNo ")") text := line star := SubStr(text, 1, 1) = "*" if star text := Trim(SubStr(text, 2)) value := this._ParseScalar(Trim(text)) items.Push(value) if star { if items.HasActive throw MOMLError("Duplicate active marker '*' in list block") items.HasActive := true items.Active := value } hasItems := true pos += 1 } return [hasItems ? items : pairs, pos] } static _BlockOpen(line) { if RegExMatch(line, "^(\*)?(\S+)\s*\{$", &m) return [m[1] = "*", m[2]] return 0 } static _FindUnquotedEquals(line) { q := Chr(34) inQuotes := false i := 1 while i <= StrLen(line) { c := SubStr(line, i, 1) if inQuotes { if c = "\" { if i + 1 > StrLen(line) throw MOMLError("Unterminated escape in line") i += 2 continue } if c = q inQuotes := false } else { if c = q inQuotes := true else if c = "=" return i } i += 1 } return 0 } static _SplitPair(line) { idx := this._FindUnquotedEquals(line) if idx = 0 return 0 key := Trim(SubStr(line, 1, idx - 1)) star := SubStr(key, 1, 1) = "*" if star key := Trim(SubStr(key, 2)) if key = "" || RegExMatch(key, "\s") throw MOMLError("Invalid key before '='") return [star, key, SubStr(line, idx + 1)] } static _ScanValue(s) { q := Chr(34) tokens := [] start := 1 inQuotes := false i := 1 while i <= StrLen(s) { c := SubStr(s, i, 1) if inQuotes { if c = "\" { if i + 1 > StrLen(s) throw MOMLError("Unterminated escape in value") i += 2 continue } if c = q inQuotes := false } else { if c = q inQuotes := true else if c = "," { tokens.Push(SubStr(s, start, i - start)) start := i + 1 } } i += 1 } if inQuotes throw MOMLError("Unterminated quoted string in value") tokens.Push(SubStr(s, start)) return tokens } static _ParseAssignmentValue(rawValue, lineNo) { tokens := this._ScanValue(rawValue) if tokens.Length = 1 { t := Trim(tokens[1]) return t = "" ? "" : this._ParseScalar(t) } out := [] for tok in tokens { t := Trim(tok) if t = "" throw MOMLError("Empty list element must be written as an explicit " Chr(34) Chr(34) " (line " lineNo ")") out.Push(this._ParseScalar(t)) } return out } static _ParseScalar(s) { q := Chr(34) if !InStr(s, q) return s if this._FullyQuoted(s) return this._Unescape(SubStr(s, 2, StrLen(s) - 2)) throw MOMLError("Partial or malformed quoting is not supported") } static _FullyQuoted(s) { q := Chr(34) n := StrLen(s) if n < 2 || SubStr(s, 1, 1) != q return false i := 2 while i <= n { c := SubStr(s, i, 1) if c = "\" { if i + 1 > n throw MOMLError("Unterminated escape in quoted string") i += 2 continue } if c = q return i = n i += 1 } throw MOMLError("Unterminated quoted string") } static _Unescape(inner) { q := Chr(34) out := "" i := 1 while i <= StrLen(inner) { c := SubStr(inner, i, 1) if c != "\" { out .= c i += 1 continue } if i + 1 > StrLen(inner) throw MOMLError("Trailing backslash in quoted string") n := SubStr(inner, i + 1, 1) if n = "\" out .= "\" else if n = q out .= q else if n = "n" out .= "`n" else if n = "t" out .= "`t" else throw MOMLError("Unknown escape sequence in quoted string") i += 2 } return out } static _NeedsQuoting(s) { q := Chr(34) return InStr(s, q) || InStr(s, ",") || s != Trim(s) || InStr(s, "`n") || InStr(s, "`t") } static _BareItemNeedsQuoting(s) { if s = "" || this._NeedsQuoting(s) return true if SubStr(s, 1, 1) = "#" || SubStr(s, 1, 1) = "*" || InStr(s, "=") return true if s = "}" || s = "{" || IsObject(this._BlockOpen(s)) return true return false } static _Escape(s) { q := Chr(34) s := StrReplace(s, "\", "\\") s := StrReplace(s, q, "\" q) s := StrReplace(s, "`n", "\n") s := StrReplace(s, "`t", "\t") return s } static _RequireStr(value, context) { if Type(value) != "String" throw MOMLError("MOML 2.0 scalar must be String for " context ". Rule 1: type conversion belongs to the gatekeeper.") } static _ValidateName(name) { if Type(name) != "String" throw MOMLError("MOML 2.0 key/block name must be a String") if name = "" || RegExMatch(name, "\s") || SubStr(name, 1, 1) = "#" || SubStr(name, 1, 1) = "*" throw MOMLError("Invalid MOML 2.0 key/block name: " name) if InStr(name, "=") || InStr(name, "{") || InStr(name, "}") || InStr(name, Chr(34)) throw MOMLError("Invalid MOML 2.0 key/block name: " name) } static _WriteValue(s) { this._RequireStr(s, "an inline-list element or scalar value") if s = "" return Chr(34) Chr(34) return this._NeedsQuoting(s) ? Chr(34) this._Escape(s) Chr(34) : s } static _WriteBareItem(s) { this._RequireStr(s, "a bare-item block element") if s = "" return Chr(34) Chr(34) return this._BareItemNeedsQuoting(s) ? Chr(34) this._Escape(s) Chr(34) : s } static _DumpBlock(block, indent) { pad := "" Loop indent pad .= " " lines := [] if block is MOMLList { activeWritten := false for item in block { star := "" if block.HasActive && !activeWritten && item = block.Active { star := "*" activeWritten := true } lines.Push(pad star this._WriteBareItem(item)) } return lines } if !(block is MOMLDict) throw MOMLError("Dumps() root/blocks must be MOMLDict or MOMLList") for key, value in block { this._ValidateName(key) star := block.HasActive && key = block.Active ? "*" : "" if value is MOMLDict || value is MOMLList { lines.Push(pad star key " {") for childLine in this._DumpBlock(value, indent + 1) lines.Push(childLine) lines.Push(pad "}") } else if value is Array { elements := [] for item in value elements.Push(item) if elements.Length = 1 elements.Push("") if elements.Length { encoded := [] for item in elements encoded.Push(this._WriteValue(item)) lines.Push(pad star key " = " this._Join(encoded, ", ")) } else { lines.Push(pad star key " =") } } else { this._RequireStr(value, "value for key " key) if value = "" lines.Push(pad star key " =") else lines.Push(pad star key " = " this._WriteValue(value)) } } return lines } static _Join(arr, sep) { out := "" for idx, item in arr { if idx > 1 out .= sep out .= item } return out } }