@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/README.md +166 -0
- package/bin/stamp +21 -0
- package/directives/brew.rip +48 -0
- package/directives/container.rip +106 -0
- package/directives/dataset.rip +40 -0
- package/directives/firewall.rip +78 -0
- package/directives/group.rip +34 -0
- package/directives/incus.rip +111 -0
- package/directives/multipass.rip +47 -0
- package/directives/packages.rip +40 -0
- package/directives/pool.rip +53 -0
- package/directives/profile.rip +29 -0
- package/directives/service.rip +52 -0
- package/directives/ssh.rip +64 -0
- package/directives/user.rip +61 -0
- package/package.json +45 -0
- package/src/cli.rip +125 -0
- package/src/engine.rip +146 -0
- package/src/helpers.rip +83 -0
- package/src/parser.rip +170 -0
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 }
|