@tabnas/proto 0.6.11 → 0.6.12

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.
@@ -1 +1 @@
1
- {"version":3,"file":"translate.js","sourceRoot":"","sources":["../src/translate.ts"],"names":[],"mappings":";AAAA,2EAA2E;;QA4pCzE,SAAS;AA3oCX,MAAM,WAAW,GAAqB,MAAM,CAAC,MAAM,CAAC;IAClD,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgDX;IACC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAilCxD,EAAE,CAAC;CACH,CAAC,CAAA;AAEF,SAAS,SAAS;IAChB,OAAO,WAAW,CAAA;AACpB,CAAC"}
1
+ {"version":3,"file":"translate.js","sourceRoot":"","sources":["../src/translate.ts"],"names":[],"mappings":";AAAA,2EAA2E;;QAghCzE,SAAS;AA9/BX,MAAM,WAAW,GAAqB,MAAM,CAAC,MAAM,CAAC;IAClD,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CX;IACC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkcxD,EAAE,CAAC;IACF,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAugB9D,EAAE,CAAC;CACH,CAAC,CAAA;AAEF,SAAS,SAAS;IAChB,OAAO,WAAW,CAAA;AACpB,CAAC"}
@@ -1 +1 @@
1
- {"version":"7.0.2","root":["../src/aggregate.ts","../src/build-descriptor.ts","../src/descriptor.ts","../src/detect-version.ts","../src/grammar.ts","../src/preflight.ts","../src/proto.ts","../src/translate.ts"],"packageJsons":["../node_modules/@tabnas/parser/package.json","../package.json"],"missingPackageJsons":["../node_modules/@tabnas/parser/dist/package.json"]}
1
+ {"version":"7.0.2","root":["../src/aggregate.ts","../src/build-descriptor.ts","../src/build-tree.ts","../src/descriptor.ts","../src/detect-version.ts","../src/grammar.ts","../src/preflight.ts","../src/proto.ts","../src/translate.ts"],"packageJsons":["../node_modules/@tabnas/parser/package.json","../package.json"],"missingPackageJsons":["../node_modules/@tabnas/parser/dist/package.json"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tabnas/proto",
3
- "version": "0.6.11",
3
+ "version": "0.6.12",
4
4
  "description": "Parse Protocol Buffers .proto IDL (proto2, proto3, edition 2023/2024) into FileDescriptorProto-shaped JSON, using the Tabnas parser and an ABNF grammar.",
5
5
  "main": "dist/proto.js",
6
6
  "type": "commonjs",
@@ -55,7 +55,7 @@
55
55
  "LICENSE"
56
56
  ],
57
57
  "peerDependencies": {
58
- "@tabnas/parser": ">=0.12.11"
58
+ "@tabnas/parser": ">=0.12.16"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@tabnas/abnf": "*",
@@ -214,6 +214,18 @@ function typeNodeOf(n: Node): Node | undefined {
214
214
  return child(n, 'fieldType') ?? child(n, 'messageType') ?? child(n, 'fullIdent')
215
215
  }
216
216
 
217
+ // The field type's text. Where the type surfaces as a bare `fullIdent`,
218
+ // the optional leading dot of `messageType` was inlined with it, into the
219
+ // text ahead of the node, and a fully qualified type keeps it there:
220
+ // `oneof o { .a.B t = 5; }` names `.a.B`, not `a.B`.
221
+ function typeTextOf(n: Node): string {
222
+ const t = typeNodeOf(n)
223
+ if (null == t) return ''
224
+ if ('fullIdent' !== t.rule) return t.src
225
+ const gap = gapsBefore(n, 'fullIdent')[0] ?? ''
226
+ return (gap.endsWith('.') ? '.' : '') + t.src
227
+ }
228
+
217
229
  function applyFieldOptions(f: FieldDescriptorProto, opts: Node | undefined): void {
218
230
  const po = readFieldOptions(opts)
219
231
  if (undefined !== po.jsonName) f.jsonName = po.jsonName
@@ -223,7 +235,6 @@ function applyFieldOptions(f: FieldDescriptorProto, opts: Node | undefined): voi
223
235
 
224
236
  function buildField(n: Node, version: ProtoVersion): FieldDescriptorProto {
225
237
  const label = child(n, 'label')
226
- const type = typeNodeOf(n)
227
238
  const name = child(n, 'ident')
228
239
  const number = child(n, 'fieldNumber')
229
240
 
@@ -231,7 +242,7 @@ function buildField(n: Node, version: ProtoVersion): FieldDescriptorProto {
231
242
  name: name ? name.src : '',
232
243
  number: number ? Number(number.src) : 0,
233
244
  ...fieldLabel(label, version),
234
- ...fieldTypeName(type ? type.src : ''),
245
+ ...fieldTypeName(typeTextOf(n)),
235
246
  }
236
247
  applyFieldOptions(f, child(n, 'fieldOptions'))
237
248
  return f
@@ -659,3 +670,10 @@ export function buildFile(proto: Node, version: ProtoVersion): FileDescriptorPro
659
670
  }
660
671
  return file
661
672
  }
673
+
674
+ // The CST readers the source-ordered tree's walk shares (./build-tree.ts).
675
+ export type { Node }
676
+ export {
677
+ R, kw, child, gaps, gapsBefore, optionNameOf, typeTextOf, isGroup,
678
+ isOptionStmt, parenthesised,
679
+ }
@@ -0,0 +1,313 @@
1
+ /* Copyright (c) 2026 Richard Rodger and other contributors, MIT License */
2
+
3
+ // Walk the `{rule, src, kids}` CST into the source-ordered tree, the
4
+ // schema `proto-ast` (alchemy's docs/streaming-plan.md, section 13.1):
5
+ // the file's declarations in the order the file writes them, each node an
6
+ // object whose first member, `type`, names it and whose other members
7
+ // follow in the order the statement spells them. A scalar is the text
8
+ // the source spells, its tokens run together as a CST node's `src` keeps
9
+ // them: a name, a type, a number with its sign, an option's name, and a
10
+ // string literal with its quotes and escapes as written. An aggregate
11
+ // option value is its fields, `[{name, value}]`, each value text or an
12
+ // aggregate again. An empty statement declares nothing and is left out.
13
+ //
14
+ // The transducer part (alchemy/transduce.alc) makes the same tree's
15
+ // events from the parse's rule passes; this walk is the tree a host reads
16
+ // whole, and the one the part is held to. It finds each statement the way
17
+ // ./build-descriptor.ts does, by the keyword ahead of the node's first
18
+ // child (`kw`), since the ABNF compiler inlines a leading rule into the
19
+ // dispatch node that holds it.
20
+
21
+ import {
22
+ Node, R, kw, child, gaps, gapsBefore, optionNameOf, typeTextOf, isGroup,
23
+ isOptionStmt, parenthesised,
24
+ } from './build-descriptor'
25
+
26
+ // A node of the tree: an object whose members are in the schema's order.
27
+ export type TreeNode = { [key: string]: TreeValue }
28
+ export type TreeValue = string | boolean | TreeValue[] | TreeNode
29
+
30
+ // A constant's value: its text, or an aggregate's fields.
31
+ function constantTree(n: Node | undefined): TreeValue {
32
+ if (null == n) return ''
33
+ if (!n.src.startsWith('{')) return n.src
34
+ return R(n).filter((k) => 'messageValueEntry' === k.rule).map(aggregateField)
35
+ }
36
+
37
+ // `messageValueEntry = ident [":"] (strLit 1*strLit / constant) [","/";"]`.
38
+ // The leading `ident` is inlined, so the name is the text ahead of the
39
+ // first child, less the colon; adjacent string literals are their texts
40
+ // run together, as the entry's `src` keeps them.
41
+ function aggregateField(e: Node): TreeNode {
42
+ const kids = R(e)
43
+ const name = (gaps(e)[0] ?? '').replace(/:$/, '')
44
+ const value: TreeValue =
45
+ 1 === kids.length && 'constant' === kids[0].rule
46
+ ? constantTree(kids[0])
47
+ : kids.map((k) => k.src).join('')
48
+ return { name, value }
49
+ }
50
+
51
+ // `option name = constant;`, as a statement.
52
+ function optionTree(el: Node): TreeNode {
53
+ const cst = child(el, 'constant')
54
+ return { type: 'option', name: optionNameOf(el, cst), value: constantTree(cst) }
55
+ }
56
+
57
+ // `[name = constant, ...]`: a field's, an enum value's or an extension
58
+ // range's options.
59
+ function optionsTree(opts: Node | undefined): TreeNode[] {
60
+ return R(opts).filter((k) => 'fieldOption' === k.rule).map((fo) => {
61
+ const cst = child(fo, 'constant')
62
+ return { name: optionNameOf(fo, cst), value: constantTree(cst) }
63
+ })
64
+ }
65
+
66
+ // The members of a statement that carry `[options]`, appended where the
67
+ // statement writes them.
68
+ function withOptions(node: TreeNode, el: Node): TreeNode {
69
+ const opts = child(el, 'fieldOptions')
70
+ if (opts) node.options = optionsTree(opts)
71
+ return node
72
+ }
73
+
74
+ // `[label] fieldType ident "=" fieldNumber [fieldOptions] ";"`.
75
+ function fieldTree(n: Node): TreeNode {
76
+ const node: TreeNode = { type: 'field' }
77
+ const label = child(n, 'label')
78
+ if (label) node.label = label.src
79
+ node.fieldType = typeTextOf(n)
80
+ node.name = child(n, 'ident')?.src ?? ''
81
+ node.number = child(n, 'fieldNumber')?.src ?? ''
82
+ return withOptions(node, n)
83
+ }
84
+
85
+ // `[label] "group" ident "=" fieldNumber messageBody`.
86
+ function groupTree(n: Node): TreeNode {
87
+ const node: TreeNode = { type: 'group' }
88
+ const label = child(n, 'label')
89
+ if (label) node.label = label.src
90
+ node.name = child(n, 'ident')?.src ?? ''
91
+ node.number = child(n, 'fieldNumber')?.src ?? ''
92
+ node.body = messageBodyTree(child(n, 'messageBody'))
93
+ return node
94
+ }
95
+
96
+ // `"map" "<" fieldType "," fieldType ">" ident "=" fieldNumber [fieldOptions] ";"`.
97
+ function mapTree(n: Node): TreeNode {
98
+ const types = R(n).filter((k) => 'fieldType' === k.rule)
99
+ return withOptions({
100
+ type: 'map',
101
+ keyType: types[0]?.src ?? '',
102
+ valueType: types[1]?.src ?? '',
103
+ name: child(n, 'ident')?.src ?? '',
104
+ number: child(n, 'fieldNumber')?.src ?? '',
105
+ }, n)
106
+ }
107
+
108
+ // The ranges of a `reserved` or `extensions` statement, each `from` and,
109
+ // where the range writes one, `to`. The leading `range` is inlined into
110
+ // `ranges`, so they are read from its text, which no number or `max`
111
+ // spells with a `t`.
112
+ function rangesTree(n: Node | undefined): TreeNode[] {
113
+ if (null == n) return []
114
+ return n.src.split(',').map((part): TreeNode => {
115
+ const at = part.indexOf('to')
116
+ return at < 0 ? { from: part } : { from: part.slice(0, at), to: part.slice(at + 2) }
117
+ })
118
+ }
119
+
120
+ // The names of a `reserved` statement, each as written: a string literal
121
+ // with its quotes, or an identifier. They are read from the statement's
122
+ // text, cut at the commas outside a literal.
123
+ function reservedNamesTree(n: Node): string[] {
124
+ const names = child(n, 'fieldNames')
125
+ const src = names ? names.src : ''
126
+ const out: string[] = []
127
+ let from = 0
128
+ for (let i = 0; i < src.length; i++) {
129
+ const c = src[i]
130
+ if ('"' === c || "'" === c || '`' === c) {
131
+ for (i++; i < src.length && c !== src[i]; i++) if ('\\' === src[i]) i++
132
+ } else if (',' === c) {
133
+ out.push(src.slice(from, i))
134
+ from = i + 1
135
+ }
136
+ }
137
+ out.push(src.slice(from))
138
+ return out
139
+ }
140
+
141
+ function reservedTree(n: Node): TreeNode {
142
+ const ranges = child(n, 'ranges')
143
+ return ranges
144
+ ? { type: 'reserved', ranges: rangesTree(ranges) }
145
+ : { type: 'reserved', names: reservedNamesTree(n) }
146
+ }
147
+
148
+ // The edition-2024 `export` / `local` modifier keeps the message or enum
149
+ // as a child node, which the walk reads with the modifier ahead of it.
150
+ function visibleTree(el: Node, k: string): TreeNode | undefined {
151
+ const visibility = k.startsWith('export') ? 'export' : 'local'
152
+ const m = child(el, 'message')
153
+ if (m) return messageTree(m, visibility)
154
+ const e = child(el, 'enumDef')
155
+ if (e) return enumTree(e, visibility)
156
+ return undefined
157
+ }
158
+
159
+ function messageTree(n: Node, visibility?: string): TreeNode {
160
+ const node: TreeNode = { type: 'message' }
161
+ if (visibility) node.visibility = visibility
162
+ node.name = child(n, 'ident')?.src ?? ''
163
+ node.body = messageBodyTree(child(n, 'messageBody'))
164
+ return node
165
+ }
166
+
167
+ function messageBodyTree(body: Node | undefined): TreeNode[] {
168
+ const out: TreeNode[] = []
169
+ for (const el of R(body)) {
170
+ if ('messageElement' !== el.rule) continue
171
+ const node = messageElementTree(el)
172
+ if (node) out.push(node)
173
+ }
174
+ return out
175
+ }
176
+
177
+ function messageElementTree(el: Node): TreeNode | undefined {
178
+ const k = kw(el)
179
+ if (isOptionStmt(el)) return optionTree(el)
180
+ if (k.startsWith('map<')) return mapTree(el)
181
+ if (k.startsWith('oneof')) return oneofTree(el)
182
+ if (k.startsWith('export') || k.startsWith('local')) return visibleTree(el, k)
183
+ if (isGroup(el)) return groupTree(el)
184
+ if (k.startsWith('message')) return messageTree(el)
185
+ if (k.startsWith('enum')) return enumTree(el)
186
+ if (k.startsWith('reserved')) return reservedTree(el)
187
+ if (k.startsWith('extensions')) {
188
+ return withOptions({ type: 'extensions', ranges: rangesTree(child(el, 'ranges')) }, el)
189
+ }
190
+ if (k.startsWith('extend')) return extendTree(el)
191
+ if (';' === el.src) return undefined
192
+ const first = R(el)[0]
193
+ if (first && ('fieldType' === first.rule || 'label' === first.rule)) return fieldTree(el)
194
+ return undefined
195
+ }
196
+
197
+ function oneofTree(el: Node): TreeNode {
198
+ const body: TreeNode[] = []
199
+ for (const of of R(el)) {
200
+ if ('oneofElement' !== of.rule) continue
201
+ if (isOptionStmt(of)) body.push(optionTree(of))
202
+ else if (';' === of.src) continue
203
+ else body.push(isGroup(of) ? groupTree(of) : fieldTree(of))
204
+ }
205
+ return { type: 'oneof', name: child(el, 'ident')?.src ?? '', body }
206
+ }
207
+
208
+ function enumTree(n: Node, visibility?: string): TreeNode {
209
+ const node: TreeNode = { type: 'enum' }
210
+ if (visibility) node.visibility = visibility
211
+ node.name = child(n, 'ident')?.src ?? ''
212
+ const body: TreeNode[] = []
213
+ for (const el of R(n)) {
214
+ if ('enumElement' !== el.rule) continue
215
+ // The first child says what the statement is (./build-descriptor.ts
216
+ // has why): `optionName` an option, `ranges` or `fieldNames` a
217
+ // `reserved`, `fieldNumber` a value.
218
+ const first = R(el)[0]
219
+ const rule = first?.rule
220
+ if ('ranges' === rule || 'fieldNames' === rule) body.push(reservedTree(el))
221
+ else if ('optionName' === rule) body.push(optionTree(el))
222
+ else if ('fieldNumber' === rule) {
223
+ // `ident "=" ["-"] fieldNumber`: the name and the sign are in the
224
+ // text ahead of the number node.
225
+ const gap = gapsBefore(el, 'fieldNumber')[0] ?? ''
226
+ const eq = gap.indexOf('=')
227
+ const name = eq < 0 ? gap : gap.slice(0, eq)
228
+ const number = (gap.endsWith('-') ? '-' : '') + first.src
229
+ body.push(withOptions({ type: 'enumValue', name, number }, el))
230
+ }
231
+ }
232
+ node.body = body
233
+ return node
234
+ }
235
+
236
+ // `extend messageType "{" *( field / emptyStmt ) "}"`.
237
+ function extendTree(n: Node): TreeNode {
238
+ const body: TreeNode[] = []
239
+ for (const f of R(n)) {
240
+ if ('field' === f.rule || 'messageElement' === f.rule) body.push(fieldTree(f))
241
+ }
242
+ return { type: 'extend', extendee: child(n, 'messageType')?.src ?? '', body }
243
+ }
244
+
245
+ function serviceTree(n: Node): TreeNode {
246
+ const body: TreeNode[] = []
247
+ for (const el of R(n)) {
248
+ if ('serviceElement' !== el.rule) continue
249
+ if (kw(el).startsWith('rpc')) body.push(rpcTree(el))
250
+ else if (isOptionStmt(el)) body.push(optionTree(el))
251
+ }
252
+ return { type: 'service', name: child(n, 'ident')?.src ?? '', body }
253
+ }
254
+
255
+ // `rpc ident "(" ["stream"] messageType ")" "returns" "(" ["stream"]
256
+ // messageType ")" ( ";" / "{" *( optionStmt / emptyStmt ) "}" )`. Whether
257
+ // each side streams is read from its parenthesised span, as the
258
+ // descriptor's walk reads it; the block is there where the statement
259
+ // ends in a brace.
260
+ function rpcTree(el: Node): TreeNode {
261
+ const types = R(el).filter((k) => 'messageType' === k.rule)
262
+ const [input, output] = parenthesised(el.src)
263
+ const inputType = types[0]?.src ?? ''
264
+ const outputType = types[1]?.src ?? ''
265
+ const node: TreeNode = { type: 'rpc', name: child(el, 'ident')?.src ?? '' }
266
+ if ('stream' + inputType === input) node.inputStream = true
267
+ node.inputType = inputType
268
+ if ('stream' + outputType === output) node.outputStream = true
269
+ node.outputType = outputType
270
+ if (el.src.endsWith('}')) {
271
+ node.body = R(el).filter((k) => 'optionStmt' === k.rule).map(optionTree)
272
+ }
273
+ return node
274
+ }
275
+
276
+ function topLevelTree(def: Node): TreeNode | undefined {
277
+ const k = kw(def)
278
+ if (k.startsWith('import')) {
279
+ const node: TreeNode = { type: 'import' }
280
+ const modifier = k.slice('import'.length)
281
+ if ('' !== modifier) node.modifier = modifier
282
+ node.path = child(def, 'strLit')?.src ?? ''
283
+ return node
284
+ }
285
+ if (k.startsWith('package')) {
286
+ return { type: 'package', name: child(def, 'fullIdent')?.src ?? '' }
287
+ }
288
+ if (isOptionStmt(def)) return optionTree(def)
289
+ if (k.startsWith('export') || k.startsWith('local')) return visibleTree(def, k)
290
+ if (k.startsWith('message')) return messageTree(def)
291
+ if (k.startsWith('enum')) return enumTree(def)
292
+ if (k.startsWith('service')) return serviceTree(def)
293
+ if (k.startsWith('extend')) return extendTree(def)
294
+ return undefined
295
+ }
296
+
297
+ // The file's tree: its declarations in source order. An empty file, which
298
+ // parses to no node at all, is a file with an empty body.
299
+ export function buildTree(proto: Node | undefined | null): TreeNode {
300
+ const body: TreeNode[] = []
301
+ for (const k of R(proto)) {
302
+ if ('syntaxOrEdition' === k.rule) {
303
+ body.push({
304
+ type: kw(k).startsWith('edition') ? 'edition' : 'syntax',
305
+ value: child(k, 'strLit')?.src ?? '',
306
+ })
307
+ } else if ('topLevelDef' === k.rule) {
308
+ const node = topLevelTree(k)
309
+ if (node) body.push(node)
310
+ }
311
+ }
312
+ return { type: 'file', body }
313
+ }
package/src/proto.ts CHANGED
@@ -12,6 +12,8 @@ import type { GrammarSpec } from '@tabnas/parser'
12
12
  // run gen-grammar` (gen-grammar.js). Generated: never edit it.
13
13
  import compiledGrammar from './proto-grammar.json'
14
14
  import { buildFile } from './build-descriptor'
15
+ import { buildTree } from './build-tree'
16
+ import type { TreeNode, TreeValue } from './build-tree'
15
17
  import { recordAggregate } from './aggregate'
16
18
  import { MAX_NESTING_DEPTH, preflight } from './preflight'
17
19
  import {
@@ -86,13 +88,42 @@ function parse(src: string, options?: Partial<ProtoOptions>): FileDescriptorProt
86
88
  return toDescriptor(tn.parse(src), options)
87
89
  }
88
90
 
91
+ // The parser a host reads a `.proto` document with: a fresh engine with
92
+ // the grammar installed, as `parse` builds one. Its rule passes are what
93
+ // the transducer part (alchemy/transduce.alc) reads, and its value is the
94
+ // `{rule, src, kids}` CST that `toTree` and `toDescriptor` walk. A host
95
+ // runs `preflight(src)` on a source before it hands it to this parser.
96
+ function make(): Tabnas {
97
+ const tn = new Tabnas({ rewind: { history: 8192 } })
98
+ Proto(tn)
99
+ return tn
100
+ }
101
+
102
+ // The source-ordered tree of a parsed proto CST: the file's declarations
103
+ // in the order the file writes them (the schema `proto-ast`; see
104
+ // ./build-tree.ts). It is the tree the translation parts read and write.
105
+ function toTree(cst: any): TreeNode {
106
+ return buildTree(cst)
107
+ }
108
+
109
+ // Convenience: parse a `.proto` source string to its source-ordered tree
110
+ // in one call, refusing a document that nests past MAX_NESTING_DEPTH
111
+ // first, as `parse` does.
112
+ function parseTree(src: string): TreeNode {
113
+ preflight(src)
114
+ return toTree(make().parse(src))
115
+ }
116
+
89
117
  // VERSION is this package's version. It MUST equal package.json "version":
90
118
  // the release orchestrator rewrites both, and test/version.test.ts fails the
91
119
  // build if they drift. Mirrors `const VERSION` in go/proto.go.
92
- const VERSION = '0.6.11'
120
+ const VERSION = '0.6.12'
93
121
 
94
- export { Proto, parse, toDescriptor, preflight, MAX_NESTING_DEPTH, VERSION }
95
- export type { ProtoVersion }
122
+ export {
123
+ Proto, parse, toDescriptor, parseTree, toTree, make, preflight,
124
+ MAX_NESTING_DEPTH, VERSION,
125
+ }
126
+ export type { ProtoVersion, TreeNode, TreeValue }
96
127
  export * from './descriptor'
97
128
  export { translate } from './translate'
98
129
  export type { TranslationPart, TranslationParts } from './translate'