@rip-lang/stamp 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/parser.rip ADDED
@@ -0,0 +1,170 @@
1
+ # ── Stampfile Parser ──────────────────────────────────────────────────────────
2
+ #
3
+ # Parses a Stampfile into an ordered list of directive objects:
4
+ # { type, name, args, properties }
5
+ #
6
+ # The grammar has three indentation levels:
7
+ # Column 0: top-level directives
8
+ # Level 1: properties (or grouped items)
9
+ # Level 2: sub-properties of expanded properties
10
+
11
+ PLURALS = { datasets: 'dataset', containers: 'container', users: 'user', groups: 'group', services: 'service' }
12
+
13
+ # ── Line utilities ───────────────────────────────────────────────────────────
14
+
15
+ indent = (line) ->
16
+ match = line.match /^( *)/
17
+ match[1].length
18
+
19
+ tokenize = (text) ->
20
+ tokens = []
21
+ rest = text.trim()
22
+ while rest.length
23
+ if rest[0] in ['"', "'"]
24
+ quote = rest[0]
25
+ end = rest.indexOf quote, 1
26
+ throw new Error "Unterminated string: #{rest}" if end == -1
27
+ tokens.push rest[1...end]
28
+ rest = rest[end + 1..].trimStart()
29
+ else
30
+ idx = rest.search /[\s]/
31
+ if idx == -1
32
+ tokens.push rest
33
+ rest = ''
34
+ else
35
+ tokens.push rest[...idx]
36
+ rest = rest[idx..].trimStart()
37
+ tokens
38
+
39
+ parsePropertyLine = (tokens) ->
40
+ pname = tokens[0]
41
+ pargs = tokens[1..]
42
+ prop = { name: pname, args: pargs }
43
+
44
+ arrowIdx = pargs.indexOf '->'
45
+ if arrowIdx >= 0
46
+ beforeArrow = pargs[...arrowIdx]
47
+ prop.source = beforeArrow[beforeArrow.length - 1]
48
+ prop.dest = pargs[arrowIdx + 1]
49
+ prop.flags = pargs[arrowIdx + 2..]
50
+ prop.args = beforeArrow[...-1]
51
+
52
+ prop
53
+
54
+ # ── Main parser ──────────────────────────────────────────────────────────────
55
+
56
+ export parse = (source, env = {}) ->
57
+ rawLines = source.split /\r?\n/
58
+
59
+ # Strip trailing whitespace
60
+ lines = rawLines.map (line) -> line.replace /\s+$/, ''
61
+
62
+ # Remove comment lines and blank lines, track original line numbers
63
+ filtered = []
64
+ for line, i in lines
65
+ trimmed = line.trimStart()
66
+ continue if trimmed == ''
67
+ continue if trimmed[0] == '#'
68
+ throw new Error "Tabs are not allowed (line #{i + 1})" if /\t/.test line
69
+ filtered.push { text: line, num: i + 1 }
70
+
71
+ # Pass 1: Process `set` lines → build variable map
72
+ vars = { ...env }
73
+ remaining = []
74
+ for item in filtered
75
+ trimmed = item.text.trimStart()
76
+ if trimmed.startsWith 'set '
77
+ tokens = tokenize trimmed
78
+ vars[tokens[1]] = tokens[2..].join ' '
79
+ else
80
+ remaining.push item
81
+
82
+ # Pass 2: Expand variables in all remaining lines
83
+ expandVars = (text) ->
84
+ text.replace /\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced, bare) ->
85
+ vname = braced ?? bare
86
+ vars[vname] ?? ''
87
+
88
+ for item in remaining
89
+ item.text = expandVars item.text
90
+
91
+ # Pass 3: Process `use` lines → collect external handler sources
92
+ uses = []
93
+ dirLines = []
94
+ for item in remaining
95
+ trimmed = item.text.trimStart()
96
+ if trimmed.startsWith 'use '
97
+ tokens = tokenize trimmed
98
+ uses.push tokens[1]
99
+ else
100
+ dirLines.push item
101
+
102
+ # Pass 4: Group lines into directives by indentation
103
+ directives = []
104
+ current = null
105
+
106
+ for item in dirLines
107
+ depth = indent item.text
108
+ tokens = tokenize item.text
109
+
110
+ if depth == 0
111
+ current = { type: tokens[0], args: tokens[1..], properties: {}, line: item.num, _propDepth: null, _lastPropName: null }
112
+ directives.push current
113
+
114
+ else if current
115
+ # Sub-property (deeper than first property level)
116
+ if current._propDepth? and depth > current._propDepth
117
+ lpname = current._lastPropName
118
+ if lpname and current.properties[lpname]
119
+ lastProp = current.properties[lpname]
120
+ last = lastProp[lastProp.length - 1]
121
+ last.sub ??= {}
122
+ parsed = parsePropertyLine tokens
123
+ last.sub[parsed.name] ??= []
124
+ last.sub[parsed.name].push { args: parsed.args }
125
+ continue
126
+
127
+ # Regular property
128
+ current._propDepth ??= depth
129
+ parsed = parsePropertyLine tokens
130
+ current._lastPropName = parsed.name
131
+ current.properties[parsed.name] ??= []
132
+ propEntry = { args: parsed.args }
133
+ propEntry.source = parsed.source if parsed.source?
134
+ propEntry.dest = parsed.dest if parsed.dest?
135
+ propEntry.flags = parsed.flags if parsed.flags?
136
+ propEntry.sub = parsed.sub if parsed.sub?
137
+ current.properties[parsed.name].push propEntry
138
+
139
+ # Clean internal tracking fields
140
+ for dir in directives
141
+ delete dir._propDepth
142
+ delete dir._lastPropName
143
+
144
+ # Pass 5: Separate name from args based on directive type
145
+ for dir in directives
146
+ dtype = dir.type
147
+
148
+ if dtype in ['packages', 'firewall', 'ssh', 'incus'] or PLURALS[dtype]
149
+ dir.name = null
150
+ else if dir.args.length > 0
151
+ dir.name = dir.args[0]
152
+ dir.args = dir.args[1..]
153
+ else
154
+ dir.name = null
155
+
156
+ # Pass 6: Expand plural forms
157
+ expanded = []
158
+ for dir in directives
159
+ singular = PLURALS[dir.type]
160
+ if singular
161
+ for own pname, props of dir.properties
162
+ sub = { type: singular, name: pname, args: [], properties: {}, line: dir.line }
163
+ for prop in props when prop.args.length or prop.source? or prop.sub?
164
+ for own k, v of (prop.sub ?? {})
165
+ sub.properties[k] = v
166
+ expanded.push sub
167
+ else
168
+ expanded.push dir
169
+
170
+ { directives: expanded, uses, vars }