yaml-config.mjs
2.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// lib/yaml-config.mjs — minimal YAML reader for config-vars.yaml.
//
// Scope is intentionally tiny: config-vars.yaml is exactly two levels deep
// (top-level section header `key:` → 2-space-indented `key: value` scalars).
// We deliberately support NO lists, NO flow style ([], {}), NO anchors and
// NO multiline — config-vars.yaml uses none of them, so a parser that handled
// them would be untested surface. Mirrors the zero-dependency, single-purpose
// spirit of the dotenv parser this replaces.
//
// Value rules (same philosophy as the old dotenv parser):
// - a single layer of matching surrounding quotes ('…' or "…") is removed,
// and anything after the closing quote (e.g. a trailing ` # comment`) is dropped
// - an unquoted value is cut at the first ` #` (space-hash) inline comment
// - a value that is empty or starts with `#` is treated as empty
// - NO variable expansion: `$FOO`, `${FOO}`, backticks stay literal
// - numbers stay strings (callers Number() them, as with dotenv)
/** Parse one `key:`-stripped raw value into its literal string. */
export function parseScalar(raw) {
let s = String(raw).trim()
if (s === '' || s[0] === '#') return ''
const q = s[0]
if (q === '"' || q === "'") {
const end = s.indexOf(q, 1)
if (end !== -1) return s.slice(1, end) // ignore anything after the closing quote
// no closing quote → fall through and treat the (still-quoted) text literally
}
const hash = s.indexOf(' #')
if (hash !== -1) s = s.slice(0, hash).trim()
return s
}
/**
* Parse config-vars.yaml text into a nested plain object.
*
* Top-level `section:` with no value opens a mapping; subsequent indented
* `key: value` lines attach to it. A top-level `key: value` stays a root scalar.
*
* @param {string} text
* @returns {Record<string, any>}
*/
export function parseYamlConfig(text) {
const root = {}
if (typeof text !== 'string') return root
let section = null
for (const rawLine of text.split('\n')) {
const line = rawLine.replace(/\r$/, '') // tolerate CRLF
const trimmed = line.trim()
if (trimmed === '' || trimmed[0] === '#') continue // blank / full-line comment
const colon = line.indexOf(':')
if (colon === -1) continue // not a key: line; ignore
const key = line.slice(0, colon).trim()
if (key === '') continue
const indent = line.length - line.replace(/^\s+/, '').length
const value = parseScalar(line.slice(colon + 1))
if (indent === 0) {
if (value === '') {
section = {}
root[key] = section
} else {
root[key] = value
section = null
}
} else if (section) {
section[key] = value
} else {
root[key] = value // indented line with no open section — tolerate as root scalar
}
}
return root
}