altium-toolkit 1.1.41 → 1.2.0

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.
Files changed (58) hide show
  1. package/README.md +113 -19
  2. package/docs/api.md +224 -18
  3. package/docs/capabilities.md +65 -0
  4. package/docs/migration/legacy-001.md +309 -0
  5. package/docs/migration/legacy-002.md +309 -0
  6. package/docs/migration/legacy-003.md +309 -0
  7. package/docs/migration/legacy-004.md +309 -0
  8. package/docs/migration/legacy-005.md +111 -0
  9. package/docs/migration.md +20 -0
  10. package/docs/model-format.md +28 -3
  11. package/docs/release-notes-v1.2.0.md +147 -0
  12. package/docs/testing.md +43 -1
  13. package/examples/arduino-uno/PcbThreeSceneRenderer.mjs +1 -1
  14. package/examples/arduino-uno/example.mjs +1 -1
  15. package/examples/cli-utils.mjs +1 -1
  16. package/examples/corpus-smoke.mjs +1 -1
  17. package/examples/inspect-board.mjs +1 -1
  18. package/examples/library-catalog.mjs +1 -1
  19. package/examples/validate-library.mjs +1 -1
  20. package/package.json +20 -5
  21. package/spec/api-baseline-v1.1.41.json +1 -0
  22. package/spec/asset-baseline-v1.1.41.json +1 -0
  23. package/spec/feature-preservation.json +1 -0
  24. package/spec/library-scope.md +14 -4
  25. package/spec/native-source-manifest-v1.1.41.json +1 -0
  26. package/src/capabilities.mjs +4 -0
  27. package/src/convergence/AltiumCircuitJsonProjection.mjs +230 -0
  28. package/src/convergence/AltiumDocumentBuilder.mjs +167 -0
  29. package/src/convergence/AltiumExtensionResolver.mjs +98 -0
  30. package/src/convergence/AltiumProjectDocumentResolver.mjs +324 -0
  31. package/src/convergence/AltiumSchematicCoordinateProjection.mjs +130 -0
  32. package/src/convergence/AltiumWorkerClient.mjs +95 -0
  33. package/src/convergence/Parser.mjs +285 -0
  34. package/src/convergence/ParserInput.mjs +282 -0
  35. package/src/convergence/ProjectLoader.mjs +916 -0
  36. package/src/convergence/SchematicSvgRenderer.mjs +126 -0
  37. package/src/convergence/ToolkitCapabilities.mjs +49 -0
  38. package/src/core/circuit-json/CircuitJsonSchematicDocumentGraphicBuilder.mjs +785 -0
  39. package/src/core/circuit-json/CircuitJsonSchematicGraphicBuilder.mjs +909 -0
  40. package/src/core/circuit-json/CircuitJsonSchematicImageProjection.mjs +277 -0
  41. package/src/core/circuit-json/CircuitJsonSchematicStrokeStyle.mjs +47 -0
  42. package/src/extensions.mjs +11 -0
  43. package/src/index.mjs +20 -4
  44. package/src/interaction.mjs +7 -0
  45. package/src/legacy-netlist-query.mjs +11 -0
  46. package/src/legacy-parser.mjs +141 -0
  47. package/src/legacy-renderers.mjs +25 -0
  48. package/src/legacy-scene3d.mjs +10 -0
  49. package/src/manufacturing.mjs +4 -0
  50. package/src/parser.mjs +12 -138
  51. package/src/project.mjs +11 -0
  52. package/src/query.mjs +4 -0
  53. package/src/renderers.mjs +4 -22
  54. package/src/scene3d.mjs +5 -8
  55. package/src/simulation.mjs +4 -0
  56. package/src/styles/renderers.css +27 -0
  57. package/src/testing.mjs +8 -0
  58. package/src/workers/parser.worker.mjs +37 -0
@@ -0,0 +1,285 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import {
5
+ ToolkitDiagnostic,
6
+ ToolkitError,
7
+ ToolkitProgress
8
+ } from 'circuitjson-toolkit/parser'
9
+
10
+ import { AltiumDocumentBuilder } from './AltiumDocumentBuilder.mjs'
11
+ import { AltiumWorkerClient } from './AltiumWorkerClient.mjs'
12
+ import { ParserInput } from './ParserInput.mjs'
13
+
14
+ const ABORTED_GETTER = Object.getOwnPropertyDescriptor(
15
+ AbortSignal.prototype,
16
+ 'aborted'
17
+ )?.get
18
+ const PROGRESS_MESSAGES = {
19
+ detect: 'Detecting Altium input.',
20
+ decode: 'Decoding native Altium data.',
21
+ validate: 'Validating canonical CircuitJSON.',
22
+ complete: 'Altium parsing complete.'
23
+ }
24
+ const SUPPORTED_EXTENSION_IDS = new Set([
25
+ 'altium.native-model',
26
+ 'altium.project-context'
27
+ ])
28
+
29
+ /**
30
+ * Parses native Altium inputs into canonical CircuitJSON document envelopes.
31
+ */
32
+ export class Parser {
33
+ /**
34
+ * Parses one input synchronously.
35
+ * @param {Record<string, any>} input Common parser input.
36
+ * @param {Record<string, any>} [options] Common parser options.
37
+ * @returns {Record<string, any>} Canonical document.
38
+ */
39
+ static parse(input, options = {}) {
40
+ try {
41
+ const normalized = ParserInput.normalize(input, options)
42
+ if (normalized.options.worker === true) {
43
+ throw Parser.#error(
44
+ 'Synchronous Altium parsing cannot use a worker.',
45
+ 'ERR_WORKER_SYNC_UNAVAILABLE',
46
+ 'unsupported',
47
+ normalized.input.fileName
48
+ )
49
+ }
50
+ Parser.#assertSupported(normalized.input)
51
+ Parser.#assertExtensions(normalized)
52
+ Parser.#assertReports(normalized)
53
+ return AltiumDocumentBuilder.build(normalized)
54
+ } catch (error) {
55
+ throw Parser.#parseError(error, input)
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Returns a discriminated parse result without throwing public failures.
61
+ * @param {Record<string, any>} input Common parser input.
62
+ * @param {Record<string, any>} [options] Common parser options.
63
+ * @returns {{ ok: true, value: Record<string, any> } | { ok: false, error: ToolkitError, diagnostics: object[] }} Parse result.
64
+ */
65
+ static tryParse(input, options = {}) {
66
+ try {
67
+ return { ok: true, value: Parser.parse(input, options) }
68
+ } catch (error) {
69
+ const normalized = Parser.#parseError(error, input)
70
+ return {
71
+ ok: false,
72
+ error: normalized,
73
+ diagnostics: [
74
+ ToolkitDiagnostic.create({
75
+ code: normalized.code,
76
+ severity: 'error',
77
+ message: normalized.message,
78
+ source: normalized.source
79
+ })
80
+ ]
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Parses one input asynchronously with progress, cancellation, and workers.
87
+ * @param {Record<string, any>} input Common parser input.
88
+ * @param {Record<string, any>} [options] Common parser options.
89
+ * @returns {Promise<Record<string, any>>} Canonical document.
90
+ */
91
+ static async parseAsync(input, options = {}) {
92
+ let normalized
93
+ try {
94
+ normalized = ParserInput.normalize(input, options)
95
+ Parser.#assertSupported(normalized.input)
96
+ Parser.#assertExtensions(normalized)
97
+ Parser.#assertReports(normalized)
98
+ Parser.#assertNotCancelled(normalized)
99
+ } catch (error) {
100
+ throw Parser.#parseError(error, input)
101
+ }
102
+ const useWorker =
103
+ normalized.options.worker === true ||
104
+ (normalized.options.worker === 'auto' &&
105
+ normalized.options.retainSource !== 'reference' &&
106
+ AltiumWorkerClient.isAvailable())
107
+ if (useWorker) {
108
+ const attempt = await AltiumWorkerClient.parseAttempt(
109
+ normalized.input,
110
+ normalized.options
111
+ )
112
+ if (attempt.ok) return attempt.value
113
+ if (normalized.options.worker !== 'auto' || !attempt.unavailable) {
114
+ throw Parser.#parseError(attempt.error, input)
115
+ }
116
+ AltiumWorkerClient.dispose()
117
+ }
118
+ let progress = Parser.#progress(normalized, 'detect')
119
+ Parser.#assertNotCancelled(normalized)
120
+ progress = Parser.#progress(normalized, 'decode', progress)
121
+ await Promise.resolve()
122
+ Parser.#assertNotCancelled(normalized)
123
+ let decoded
124
+ try {
125
+ decoded = AltiumDocumentBuilder.decode(normalized)
126
+ } catch (error) {
127
+ throw Parser.#parseError(error, input)
128
+ }
129
+ Parser.#assertNotCancelled(normalized)
130
+ progress = Parser.#progress(normalized, 'validate', progress)
131
+ Parser.#assertNotCancelled(normalized)
132
+ let document
133
+ try {
134
+ document = AltiumDocumentBuilder.build(normalized, decoded)
135
+ } catch (error) {
136
+ throw Parser.#parseError(error, input)
137
+ }
138
+ Parser.#assertNotCancelled(normalized)
139
+ Parser.#progress(normalized, 'complete', progress)
140
+ Parser.#assertNotCancelled(normalized)
141
+ return document
142
+ }
143
+
144
+ /**
145
+ * Performs bounded Altium format detection.
146
+ * @param {unknown} input Parser input candidate.
147
+ * @returns {boolean} Whether the input is supported.
148
+ */
149
+ static supports(input) {
150
+ return ParserInput.supports(input)
151
+ }
152
+
153
+ /**
154
+ * Rejects unsupported report requests explicitly.
155
+ * @param {{ input: { fileName: string }, options: { reports: string[] } }} normalized Normalized request.
156
+ * @returns {void}
157
+ */
158
+ static #assertReports(normalized) {
159
+ if (!normalized.options.reports.length) return
160
+ throw Parser.#error(
161
+ `Altium parser report is unavailable: ${normalized.options.reports[0]}.`,
162
+ 'ERR_CAPABILITY_UNAVAILABLE',
163
+ 'unsupported',
164
+ normalized.input.fileName,
165
+ { reports: normalized.options.reports }
166
+ )
167
+ }
168
+
169
+ /**
170
+ * Rejects unknown explicitly selected extension feature ids.
171
+ * @param {{ input: { fileName: string }, options: { extensions: string | string[] } }} normalized Normalized request.
172
+ * @returns {void}
173
+ */
174
+ static #assertExtensions(normalized) {
175
+ if (!Array.isArray(normalized.options.extensions)) return
176
+ const unknown = normalized.options.extensions.find(
177
+ (id) => !SUPPORTED_EXTENSION_IDS.has(id)
178
+ )
179
+ if (!unknown) return
180
+ throw Parser.#error(
181
+ `Altium parser extension is unavailable: ${unknown}.`,
182
+ 'ERR_CAPABILITY_UNAVAILABLE',
183
+ 'unsupported',
184
+ normalized.input.fileName,
185
+ { extensions: normalized.options.extensions }
186
+ )
187
+ }
188
+
189
+ /**
190
+ * Rejects unsupported source names.
191
+ * @param {Record<string, any>} input Normalized input.
192
+ * @returns {void}
193
+ */
194
+ static #assertSupported(input) {
195
+ if (ParserInput.supports(input)) return
196
+ throw Parser.#error(
197
+ `Unsupported Altium input: ${input.fileName || '(unnamed)'}.`,
198
+ 'ERR_FORMAT_UNSUPPORTED',
199
+ 'unsupported',
200
+ input.fileName
201
+ )
202
+ }
203
+
204
+ /**
205
+ * Emits one ordered direct-parser progress row.
206
+ * @param {{ options: { onProgress?: Function } }} normalized Request.
207
+ * @param {'detect' | 'decode' | 'validate' | 'complete'} stage Stage.
208
+ * @param {Record<string, any> | null} [previous] Previous row.
209
+ * @returns {Record<string, any> | null} Emitted or previous row.
210
+ */
211
+ static #progress(normalized, stage, previous = null) {
212
+ if (!normalized.options.onProgress) return previous
213
+ const row = ToolkitProgress.create(
214
+ { stage, message: PROGRESS_MESSAGES[stage] },
215
+ previous
216
+ )
217
+ normalized.options.onProgress(row)
218
+ return row
219
+ }
220
+
221
+ /**
222
+ * Rejects an aborted direct request.
223
+ * @param {{ input: { fileName: string }, options: { signal?: unknown } }} normalized Request.
224
+ * @returns {void}
225
+ */
226
+ static #assertNotCancelled(normalized) {
227
+ const { signal } = normalized.options
228
+ if (signal === undefined || signal === null) return
229
+ if (!ABORTED_GETTER) {
230
+ throw new TypeError('AbortSignal state is unavailable.')
231
+ }
232
+ let aborted = false
233
+ try {
234
+ aborted = Boolean(Reflect.apply(ABORTED_GETTER, signal, []))
235
+ } catch {
236
+ throw new TypeError('Altium signal must be an AbortSignal.')
237
+ }
238
+ if (aborted) {
239
+ throw Parser.#error(
240
+ 'Altium parsing was cancelled.',
241
+ 'ERR_CANCELLED',
242
+ 'cancelled',
243
+ normalized.input.fileName
244
+ )
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Normalizes one parser failure.
250
+ * @param {unknown} error Failure candidate.
251
+ * @param {unknown} input Original input.
252
+ * @returns {ToolkitError} Typed failure.
253
+ */
254
+ static #parseError(error, input) {
255
+ if (ToolkitError.trustedRecord(error)) return error
256
+ return ToolkitError.from(error, {
257
+ code: 'ERR_ALTIUM_PARSE',
258
+ category: 'parse',
259
+ format: 'altium',
260
+ source: ParserInput.fileName(input)
261
+ })
262
+ }
263
+
264
+ /**
265
+ * Creates one typed Altium failure.
266
+ * @param {string} message Message.
267
+ * @param {string} code Stable code.
268
+ * @param {string} category Error category.
269
+ * @param {string} source Source name.
270
+ * @param {Record<string, any>} [details] Clone-safe details.
271
+ * @returns {ToolkitError} Typed failure.
272
+ */
273
+ static #error(message, code, category, source, details = {}) {
274
+ return new ToolkitError(message, {
275
+ code,
276
+ category,
277
+ format: 'altium',
278
+ source,
279
+ details
280
+ })
281
+ }
282
+ }
283
+
284
+ Object.freeze(Parser.prototype)
285
+ Object.freeze(Parser)
@@ -0,0 +1,282 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ const ASSET_MODES = new Set(['none', 'metadata', 'full'])
5
+ const EXTENSION_MODES = new Set(['none', 'metadata', 'canonical', 'full'])
6
+ const RETAIN_SOURCE_MODES = new Set(['none', 'reference'])
7
+ const WORKER_MODES = new Set(['auto', true, false])
8
+ const SUPPORTED_SUFFIXES = new Set([
9
+ 'intlib',
10
+ 'pcbdoc',
11
+ 'pcbdwf',
12
+ 'pcblib',
13
+ 'prjpcb',
14
+ 'prjscr',
15
+ 'schdoc',
16
+ 'schlib'
17
+ ])
18
+
19
+ /**
20
+ * Normalizes source-neutral parser requests for the Altium adapter.
21
+ */
22
+ export class ParserInput {
23
+ /**
24
+ * Normalizes one parser input and common option record.
25
+ * @param {unknown} input Parser input candidate.
26
+ * @param {unknown} [options] Common options candidate.
27
+ * @returns {{ input: { fileName: string, data: string | ArrayBuffer | Uint8Array, assets: object[] }, sourceReference: object, options: Record<string, any> }} Normalized request.
28
+ */
29
+ static normalize(input, options = {}) {
30
+ const inputFields = ParserInput.#plainFields(
31
+ input,
32
+ 'Altium parser input must be a plain object.'
33
+ )
34
+ const optionFields = ParserInput.#plainFields(
35
+ options,
36
+ 'Altium parser options must be a plain object.'
37
+ )
38
+ if (!ParserInput.#isData(inputFields.data)) {
39
+ throw new TypeError(
40
+ 'Altium parser data must be a string, ArrayBuffer, or Uint8Array.'
41
+ )
42
+ }
43
+ if (
44
+ inputFields.assets !== undefined &&
45
+ !Array.isArray(inputFields.assets)
46
+ ) {
47
+ throw new TypeError('Altium parser assets must be an array.')
48
+ }
49
+ const decodeAssets = ParserInput.#enum(
50
+ optionFields.decodeAssets,
51
+ 'metadata',
52
+ ASSET_MODES,
53
+ 'asset decode mode'
54
+ )
55
+ const extensions = ParserInput.#extensions(optionFields.extensions)
56
+ const retainSource = ParserInput.#enum(
57
+ optionFields.retainSource,
58
+ 'none',
59
+ RETAIN_SOURCE_MODES,
60
+ 'source retention mode'
61
+ )
62
+ const worker =
63
+ optionFields.worker === undefined ? 'auto' : optionFields.worker
64
+ if (!WORKER_MODES.has(worker)) {
65
+ throw new TypeError('Altium worker must be auto, true, or false.')
66
+ }
67
+ if (
68
+ optionFields.onProgress !== undefined &&
69
+ typeof optionFields.onProgress !== 'function'
70
+ ) {
71
+ throw new TypeError('Altium onProgress must be a function.')
72
+ }
73
+ return {
74
+ input: {
75
+ fileName: ParserInput.fileName(inputFields.fileName),
76
+ data: inputFields.data,
77
+ assets: inputFields.assets || []
78
+ },
79
+ sourceReference: input,
80
+ options: {
81
+ preserveRaw: optionFields.preserveRaw === true,
82
+ decodeAssets,
83
+ extensions,
84
+ reports: ParserInput.#stringList(optionFields.reports),
85
+ retainSource,
86
+ worker,
87
+ transferInput: optionFields.transferInput === true,
88
+ signal: optionFields.signal,
89
+ onProgress: optionFields.onProgress
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Performs bounded format detection without parsing native contents.
96
+ * @param {unknown} input Parser input candidate.
97
+ * @returns {boolean} Whether the file name and payload are supported.
98
+ */
99
+ static supports(input) {
100
+ try {
101
+ const fields = ParserInput.#plainFields(
102
+ input,
103
+ 'Altium parser input must be a plain object.'
104
+ )
105
+ return (
106
+ ParserInput.#isData(fields.data) &&
107
+ ParserInput.supportsFileType(
108
+ ParserInput.suffix(fields.fileName)
109
+ )
110
+ )
111
+ } catch {
112
+ return false
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Checks one already normalized lowercase file type.
118
+ * @param {unknown} fileType File suffix without a period.
119
+ * @returns {boolean} Whether the Altium parser supports the file type.
120
+ */
121
+ static supportsFileType(fileType) {
122
+ return SUPPORTED_SUFFIXES.has(String(fileType))
123
+ }
124
+
125
+ /**
126
+ * Returns a normalized source file name.
127
+ * @param {unknown} input Parser input or name.
128
+ * @returns {string} Normalized name.
129
+ */
130
+ static fileName(input) {
131
+ let value = input
132
+ if (input && typeof input === 'object') {
133
+ try {
134
+ value = ParserInput.#plainFields(
135
+ input,
136
+ 'Altium parser input must be a plain object.'
137
+ ).fileName
138
+ } catch {
139
+ value = ''
140
+ }
141
+ }
142
+ return String(value || '')
143
+ .replaceAll('\\', '/')
144
+ .replace(/^\.\//u, '')
145
+ }
146
+
147
+ /**
148
+ * Returns the lowercase file suffix.
149
+ * @param {unknown} fileName File name candidate.
150
+ * @returns {string} Lowercase suffix without a period.
151
+ */
152
+ static suffix(fileName) {
153
+ const name = ParserInput.fileName(fileName)
154
+ const suffix = name.split('.').pop()
155
+ return suffix && suffix !== name ? suffix.toLowerCase() : ''
156
+ }
157
+
158
+ /**
159
+ * Copies the exact input byte range for the native parser.
160
+ * @param {string | ArrayBuffer | Uint8Array} data Parser payload.
161
+ * @returns {ArrayBuffer} Owned native parser buffer.
162
+ */
163
+ static arrayBuffer(data) {
164
+ if (typeof data === 'string')
165
+ return new TextEncoder().encode(data).buffer
166
+ if (data instanceof ArrayBuffer) return data
167
+ if (data instanceof Uint8Array) {
168
+ if (
169
+ data.byteOffset === 0 &&
170
+ data.byteLength === data.buffer.byteLength
171
+ ) {
172
+ return data.buffer
173
+ }
174
+ return data.buffer.slice(
175
+ data.byteOffset,
176
+ data.byteOffset + data.byteLength
177
+ )
178
+ }
179
+ throw new TypeError(
180
+ 'Altium parser data must be a string, ArrayBuffer, or Uint8Array.'
181
+ )
182
+ }
183
+
184
+ /**
185
+ * Normalizes one optional enum.
186
+ * @param {unknown} value Candidate value.
187
+ * @param {string} fallback Default value.
188
+ * @param {Set<string>} allowed Allowed values.
189
+ * @param {string} label Error label.
190
+ * @returns {string} Normalized value.
191
+ */
192
+ static #enum(value, fallback, allowed, label) {
193
+ const normalized = String(value === undefined ? fallback : value)
194
+ if (!allowed.has(normalized)) {
195
+ throw new TypeError(`Unsupported Altium ${label}: ${normalized}.`)
196
+ }
197
+ return normalized
198
+ }
199
+
200
+ /**
201
+ * Normalizes the extension selection contract.
202
+ * @param {unknown} value Candidate value.
203
+ * @returns {string | string[]} Normalized extension selection.
204
+ */
205
+ static #extensions(value) {
206
+ if (Array.isArray(value)) return ParserInput.#stringList(value)
207
+ return ParserInput.#enum(
208
+ value,
209
+ 'canonical',
210
+ EXTENSION_MODES,
211
+ 'extension mode'
212
+ )
213
+ }
214
+
215
+ /**
216
+ * Normalizes one unique nonempty string list.
217
+ * @param {unknown} value List candidate.
218
+ * @returns {string[]} Normalized values.
219
+ */
220
+ static #stringList(value) {
221
+ if (value === undefined) return []
222
+ if (!Array.isArray(value)) {
223
+ throw new TypeError('Altium option list must be an array.')
224
+ }
225
+ const values = []
226
+ const seen = new Set()
227
+ for (let index = 0; index < value.length; index += 1) {
228
+ const normalized = String(value[index]).trim()
229
+ if (!normalized) {
230
+ throw new TypeError('Altium option ids must not be empty.')
231
+ }
232
+ if (!seen.has(normalized)) {
233
+ seen.add(normalized)
234
+ values.push(normalized)
235
+ }
236
+ }
237
+ return values
238
+ }
239
+
240
+ /**
241
+ * Returns whether a payload uses one common binary/text input type.
242
+ * @param {unknown} value Payload candidate.
243
+ * @returns {boolean} True for supported payload values.
244
+ */
245
+ static #isData(value) {
246
+ return (
247
+ typeof value === 'string' ||
248
+ value instanceof ArrayBuffer ||
249
+ value instanceof Uint8Array
250
+ )
251
+ }
252
+
253
+ /**
254
+ * Reads one accessor-free plain record.
255
+ * @param {unknown} value Record candidate.
256
+ * @param {string} message Failure message.
257
+ * @returns {Record<string, any>} Own field values.
258
+ */
259
+ static #plainFields(value, message) {
260
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
261
+ throw new TypeError(message)
262
+ }
263
+ const prototype = Object.getPrototypeOf(value)
264
+ if (prototype !== Object.prototype && prototype !== null) {
265
+ throw new TypeError(message)
266
+ }
267
+ const descriptors = Object.getOwnPropertyDescriptors(value)
268
+ const fields = Object.create(null)
269
+ for (const [name, descriptor] of Object.entries(descriptors)) {
270
+ if (!Object.hasOwn(descriptor, 'value')) {
271
+ throw new TypeError(
272
+ 'Accessor-backed parser fields are invalid.'
273
+ )
274
+ }
275
+ fields[name] = descriptor.value
276
+ }
277
+ return fields
278
+ }
279
+ }
280
+
281
+ Object.freeze(ParserInput.prototype)
282
+ Object.freeze(ParserInput)