altium-toolkit 1.1.3 → 1.1.23
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 +34 -5
- package/docs/api.md +171 -23
- package/docs/model-format.md +192 -20
- package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
- package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
- package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
- package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +513 -3
- package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
- package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
- package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
- package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
- package/docs/testing.md +7 -0
- package/examples/README.md +21 -0
- package/examples/cli-utils.mjs +148 -0
- package/examples/corpus-smoke.mjs +523 -0
- package/examples/extract-bom.mjs +47 -0
- package/examples/generate-pnp.mjs +59 -0
- package/examples/inspect-board.mjs +70 -0
- package/examples/inspect-schematic.mjs +406 -0
- package/examples/library-catalog.mjs +115 -0
- package/examples/net-report.mjs +61 -0
- package/examples/validate-library.mjs +59 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +357 -16
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +52 -11
- package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
- package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
- package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
- package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
- package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
- package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
- package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
- package/src/core/altium/NormalizedModelSchema.mjs +3 -31
- package/src/core/altium/ParameterCollection.mjs +431 -0
- package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
- package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
- package/src/core/altium/ParserErrors.mjs +90 -0
- package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
- package/src/core/altium/ParserUtils.mjs +24 -0
- package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
- package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
- package/src/core/altium/PcbLayerGroups.mjs +308 -0
- package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
- package/src/core/altium/PcbLibModelParser.mjs +20 -4
- package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
- package/src/core/altium/PcbModelParser.mjs +223 -4
- package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PcbStreamExtractor.mjs +130 -6
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
- package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
- package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
- package/src/core/altium/SchLibModelParser.mjs +840 -0
- package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
- package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
- package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
- package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
- package/src/core/altium/SchematicHarnessParser.mjs +9 -3
- package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +99 -65
- package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
- package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
- package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
- package/src/core/altium/SchematicSheetParser.mjs +8 -2
- package/src/core/altium/SchematicStreamExtractor.mjs +107 -21
- package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
- package/src/core/altium/SchematicTextParser.mjs +28 -12
- package/src/core/altium/SchematicTextRunParser.mjs +81 -0
- package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +43 -1
- package/src/renderers.mjs +1 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +185 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNoteRenderer.mjs +5 -1
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
- package/src/ui/SchematicShapeRenderer.mjs +86 -17
- package/src/ui/SchematicSvgRenderer.mjs +980 -58
- package/src/ui/SchematicTypography.mjs +4 -3
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds metadata-only inventories for native OLE streams.
|
|
7
|
+
*/
|
|
8
|
+
export class NativeStreamInventoryBuilder {
|
|
9
|
+
static SCHEMA_ID = 'altium-toolkit.native-stream-inventory.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a native stream inventory from a compound-document stream map.
|
|
13
|
+
* @param {Map<string, Uint8Array>} streams Native stream map.
|
|
14
|
+
* @param {{ source?: string, consumedStreamNames?: Iterable<string>, knownStreamNames?: Iterable<string>, consumersByStreamName?: Map<string, string> | Record<string, string> }} [options] Inventory options.
|
|
15
|
+
* @returns {{ schema: string, summary: object, streams: object[] }}
|
|
16
|
+
*/
|
|
17
|
+
static buildFromStreams(streams, options = {}) {
|
|
18
|
+
const consumedStreamNames = new Set(options.consumedStreamNames || [])
|
|
19
|
+
const knownStreamNames = new Set(options.knownStreamNames || [])
|
|
20
|
+
const consumersByStreamName = NativeStreamInventoryBuilder.#consumerMap(
|
|
21
|
+
options.consumersByStreamName
|
|
22
|
+
)
|
|
23
|
+
const rows = [...(streams || new Map()).entries()]
|
|
24
|
+
.map(([sourceStream, bytes]) =>
|
|
25
|
+
NativeStreamInventoryBuilder.#streamRow(sourceStream, bytes, {
|
|
26
|
+
source: options.source || '',
|
|
27
|
+
consumedStreamNames,
|
|
28
|
+
knownStreamNames,
|
|
29
|
+
consumersByStreamName
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
.sort((left, right) =>
|
|
33
|
+
left.sourceStream.localeCompare(right.sourceStream)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
schema: NativeStreamInventoryBuilder.SCHEMA_ID,
|
|
38
|
+
summary: NativeStreamInventoryBuilder.#summary(rows),
|
|
39
|
+
streams: rows
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builds one metadata-only stream row.
|
|
45
|
+
* @param {string} sourceStream Stream path.
|
|
46
|
+
* @param {Uint8Array} bytes Stream payload.
|
|
47
|
+
* @param {object} options Row options.
|
|
48
|
+
* @returns {object}
|
|
49
|
+
*/
|
|
50
|
+
static #streamRow(sourceStream, bytes, options) {
|
|
51
|
+
const streamBytes =
|
|
52
|
+
bytes instanceof Uint8Array ? bytes : new Uint8Array()
|
|
53
|
+
const known = options.knownStreamNames.has(sourceStream)
|
|
54
|
+
const consumed = options.consumedStreamNames.has(sourceStream)
|
|
55
|
+
const { sourceStorage, leafName } =
|
|
56
|
+
NativeStreamInventoryBuilder.#pathParts(sourceStream)
|
|
57
|
+
|
|
58
|
+
return NativeStreamInventoryBuilder.#stripUndefined({
|
|
59
|
+
source: options.source || undefined,
|
|
60
|
+
sourceStream,
|
|
61
|
+
sourceStorage,
|
|
62
|
+
leafName,
|
|
63
|
+
byteLength: streamBytes.byteLength,
|
|
64
|
+
known,
|
|
65
|
+
consumed,
|
|
66
|
+
classification: NativeStreamInventoryBuilder.#classification(
|
|
67
|
+
known,
|
|
68
|
+
consumed,
|
|
69
|
+
streamBytes.byteLength
|
|
70
|
+
),
|
|
71
|
+
consumedBy:
|
|
72
|
+
options.consumersByStreamName.get(sourceStream) || undefined,
|
|
73
|
+
checksum: {
|
|
74
|
+
algorithm: 'fnv1a32',
|
|
75
|
+
value: NativeStreamInventoryBuilder.#fnv1a32(streamBytes)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Builds aggregate counters for inventory rows.
|
|
82
|
+
* @param {object[]} rows Stream rows.
|
|
83
|
+
* @returns {object}
|
|
84
|
+
*/
|
|
85
|
+
static #summary(rows) {
|
|
86
|
+
return {
|
|
87
|
+
streamCount: rows.length,
|
|
88
|
+
knownStreamCount: rows.filter((row) => row.known).length,
|
|
89
|
+
unknownStreamCount: rows.filter((row) => !row.known).length,
|
|
90
|
+
consumedStreamCount: rows.filter((row) => row.consumed).length,
|
|
91
|
+
unconsumedStreamCount: rows.filter((row) => !row.consumed).length,
|
|
92
|
+
emptyStreamCount: rows.filter((row) => row.byteLength === 0).length,
|
|
93
|
+
byteCount: rows.reduce(
|
|
94
|
+
(total, row) => total + Number(row.byteLength || 0),
|
|
95
|
+
0
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Classifies one stream by parser knowledge and consumption status.
|
|
102
|
+
* @param {boolean} known Whether the stream name is recognized.
|
|
103
|
+
* @param {boolean} consumed Whether a parser or inventory consumed it.
|
|
104
|
+
* @param {number} byteLength Stream byte length.
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
static #classification(known, consumed, byteLength) {
|
|
108
|
+
if (byteLength === 0) {
|
|
109
|
+
return known ? 'known-empty' : 'unknown-empty'
|
|
110
|
+
}
|
|
111
|
+
if (known && consumed) return 'known-consumed'
|
|
112
|
+
if (known) return 'known-unconsumed'
|
|
113
|
+
if (consumed) return 'unknown-consumed'
|
|
114
|
+
return 'unknown-opaque'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Splits a native stream path into storage and leaf names.
|
|
119
|
+
* @param {string} sourceStream Stream path.
|
|
120
|
+
* @returns {{ sourceStorage: string, leafName: string }}
|
|
121
|
+
*/
|
|
122
|
+
static #pathParts(sourceStream) {
|
|
123
|
+
const parts = String(sourceStream || '')
|
|
124
|
+
.split('/')
|
|
125
|
+
.filter((part) => part !== '')
|
|
126
|
+
|
|
127
|
+
if (parts.length <= 1) {
|
|
128
|
+
return {
|
|
129
|
+
sourceStorage: '',
|
|
130
|
+
leafName: parts[0] || ''
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
sourceStorage: parts.slice(0, -1).join('/'),
|
|
136
|
+
leafName: parts.at(-1) || ''
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Normalizes optional consumer metadata.
|
|
142
|
+
* @param {Map<string, string> | Record<string, string> | undefined} value Consumer map.
|
|
143
|
+
* @returns {Map<string, string>}
|
|
144
|
+
*/
|
|
145
|
+
static #consumerMap(value) {
|
|
146
|
+
if (value instanceof Map) return value
|
|
147
|
+
if (!value || typeof value !== 'object') return new Map()
|
|
148
|
+
return new Map(Object.entries(value))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Computes a stable FNV-1a checksum.
|
|
153
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
154
|
+
* @returns {string}
|
|
155
|
+
*/
|
|
156
|
+
static #fnv1a32(bytes) {
|
|
157
|
+
let hash = 0x811c9dc5
|
|
158
|
+
|
|
159
|
+
for (const value of bytes) {
|
|
160
|
+
hash ^= value
|
|
161
|
+
hash = Math.imul(hash, 0x01000193) >>> 0
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return hash.toString(16).padStart(8, '0')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Removes undefined fields from a row.
|
|
169
|
+
* @param {object} row Source row.
|
|
170
|
+
* @returns {object}
|
|
171
|
+
*/
|
|
172
|
+
static #stripUndefined(row) {
|
|
173
|
+
return Object.fromEntries(
|
|
174
|
+
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
|
+
import { ParserDiagnosticNormalizer } from './ParserDiagnosticNormalizer.mjs'
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Defines the current normalized model contract emitted by parser roots.
|
|
7
9
|
*/
|
|
@@ -24,41 +26,11 @@ export class NormalizedModelSchema {
|
|
|
24
26
|
normalizedModel.schema = NormalizedModelSchema.CURRENT_SCHEMA_ID
|
|
25
27
|
if (Array.isArray(normalizedModel.diagnostics)) {
|
|
26
28
|
normalizedModel.diagnostics =
|
|
27
|
-
|
|
29
|
+
ParserDiagnosticNormalizer.normalizeMany(
|
|
28
30
|
normalizedModel.diagnostics
|
|
29
31
|
)
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
return normalizedModel
|
|
33
35
|
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Adds machine-readable codes to parser diagnostics.
|
|
37
|
-
* @param {object[]} diagnostics Parser diagnostics.
|
|
38
|
-
* @returns {object[]}
|
|
39
|
-
*/
|
|
40
|
-
static #normalizeDiagnostics(diagnostics) {
|
|
41
|
-
return diagnostics.map((diagnostic) => ({
|
|
42
|
-
code:
|
|
43
|
-
typeof diagnostic?.code === 'string' && diagnostic.code
|
|
44
|
-
? diagnostic.code
|
|
45
|
-
: NormalizedModelSchema.#deriveDiagnosticCode(diagnostic),
|
|
46
|
-
...diagnostic
|
|
47
|
-
}))
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Derives a stable fallback code from one diagnostic message.
|
|
52
|
-
* @param {object} diagnostic Parser diagnostic.
|
|
53
|
-
* @returns {string}
|
|
54
|
-
*/
|
|
55
|
-
static #deriveDiagnosticCode(diagnostic) {
|
|
56
|
-
const slug = String(diagnostic?.message || 'diagnostic')
|
|
57
|
-
.toLowerCase()
|
|
58
|
-
.replace(/[^a-z0-9]+/gu, '.')
|
|
59
|
-
.replace(/^\.+|\.+$/gu, '')
|
|
60
|
-
.slice(0, 80)
|
|
61
|
-
|
|
62
|
-
return 'parser.' + (slug || 'diagnostic')
|
|
63
|
-
}
|
|
64
36
|
}
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents one parsed parameter value with typed read helpers.
|
|
7
|
+
*/
|
|
8
|
+
export class ParameterValue {
|
|
9
|
+
#entry
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a typed parameter value wrapper.
|
|
13
|
+
* @param {{ key?: string, rawKey?: string, value?: string, isUtf8?: boolean } | null} entry Parsed entry.
|
|
14
|
+
*/
|
|
15
|
+
constructor(entry = null) {
|
|
16
|
+
this.#entry = entry ? { ...entry } : null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns true when this value is backed by a parsed entry.
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
get exists() {
|
|
24
|
+
return this.#entry !== null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns the normalized parameter key.
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
get key() {
|
|
32
|
+
return this.#entry?.key || ''
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns the original source key.
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
get rawKey() {
|
|
40
|
+
return this.#entry?.rawKey || ''
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the raw source value.
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
get value() {
|
|
48
|
+
return this.#entry?.value || ''
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns true when the source key carried a UTF-8 marker.
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
get isUtf8() {
|
|
56
|
+
return Boolean(this.#entry?.isUtf8)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Reads the value as a string.
|
|
61
|
+
* @param {string} [defaultValue] Value returned when missing.
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
asString(defaultValue = '') {
|
|
65
|
+
return this.exists ? this.value : defaultValue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reads the value as an integer.
|
|
70
|
+
* @param {number} [defaultValue] Value returned when missing or malformed.
|
|
71
|
+
* @returns {number}
|
|
72
|
+
*/
|
|
73
|
+
asInt(defaultValue = 0) {
|
|
74
|
+
const parsed = ParameterValue.#numericMatch(this.value)
|
|
75
|
+
if (!this.exists || parsed === null) return defaultValue
|
|
76
|
+
|
|
77
|
+
const integer = Number.parseInt(parsed, 10)
|
|
78
|
+
return Number.isFinite(integer) ? integer : defaultValue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reads the value as a finite number.
|
|
83
|
+
* @param {number} [defaultValue] Value returned when missing or malformed.
|
|
84
|
+
* @returns {number}
|
|
85
|
+
*/
|
|
86
|
+
asNumber(defaultValue = 0) {
|
|
87
|
+
const parsed = ParameterValue.#numericMatch(this.value)
|
|
88
|
+
if (!this.exists || parsed === null) return defaultValue
|
|
89
|
+
|
|
90
|
+
const number = Number(parsed)
|
|
91
|
+
return Number.isFinite(number) ? number : defaultValue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Reads the value as an Altium-style boolean.
|
|
96
|
+
* @param {boolean} [defaultValue] Value returned when missing or malformed.
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
asBool(defaultValue = false) {
|
|
100
|
+
if (!this.exists) return defaultValue
|
|
101
|
+
|
|
102
|
+
const normalized = this.value.trim().toLowerCase()
|
|
103
|
+
if (['t', 'true', '1', 'y', 'yes'].includes(normalized)) return true
|
|
104
|
+
if (['f', 'false', '0', 'n', 'no'].includes(normalized)) return false
|
|
105
|
+
return defaultValue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Reads the value as an integer code.
|
|
110
|
+
* @param {number | null} [defaultValue] Value returned when missing or malformed.
|
|
111
|
+
* @returns {number | null}
|
|
112
|
+
*/
|
|
113
|
+
asCode(defaultValue = null) {
|
|
114
|
+
return this.asInt(defaultValue)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Reads the value as a numeric coordinate with an optional unit suffix.
|
|
119
|
+
* @param {object | null} [defaultValue] Value returned when missing or malformed.
|
|
120
|
+
* @returns {{ value: number, unit?: string } | object | null}
|
|
121
|
+
*/
|
|
122
|
+
asCoordinate(defaultValue = null) {
|
|
123
|
+
if (!this.exists) return ParameterValue.#cloneDefault(defaultValue)
|
|
124
|
+
|
|
125
|
+
const match = this.value
|
|
126
|
+
.trim()
|
|
127
|
+
.match(/^(-?\d+(?:\.\d+)?(?:e[+-]?\d+)?)(.*)$/iu)
|
|
128
|
+
if (!match) return ParameterValue.#cloneDefault(defaultValue)
|
|
129
|
+
|
|
130
|
+
const value = Number(match[1])
|
|
131
|
+
if (!Number.isFinite(value)) {
|
|
132
|
+
return ParameterValue.#cloneDefault(defaultValue)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const unit = match[2].trim()
|
|
136
|
+
return unit ? { value, unit } : { value }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Converts the value to a JSON-friendly entry.
|
|
141
|
+
* @returns {object | null}
|
|
142
|
+
*/
|
|
143
|
+
toJSON() {
|
|
144
|
+
return this.#entry ? { ...this.#entry } : null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Returns the first numeric token in a source value.
|
|
149
|
+
* @param {string} value Source value.
|
|
150
|
+
* @returns {string | null}
|
|
151
|
+
*/
|
|
152
|
+
static #numericMatch(value) {
|
|
153
|
+
const match = String(value || '').match(
|
|
154
|
+
/-?\d+(?:\.\d+)?(?:e[+-]?\d+)?/iu
|
|
155
|
+
)
|
|
156
|
+
return match ? match[0] : null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Clones object defaults to avoid exposing shared mutable state.
|
|
161
|
+
* @param {unknown} value Default value.
|
|
162
|
+
* @returns {unknown}
|
|
163
|
+
*/
|
|
164
|
+
static #cloneDefault(value) {
|
|
165
|
+
if (!value || typeof value !== 'object') return value
|
|
166
|
+
return Array.isArray(value) ? [...value] : { ...value }
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Provides duplicate-preserving, case-insensitive access to parameter records.
|
|
172
|
+
*/
|
|
173
|
+
export class ParameterCollection {
|
|
174
|
+
#entries
|
|
175
|
+
#entriesByKey
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Creates a collection from parsed entries.
|
|
179
|
+
* @param {{ key?: string, rawKey?: string, value?: string, isUtf8?: boolean }[]} [entries] Parsed entries.
|
|
180
|
+
*/
|
|
181
|
+
constructor(entries = []) {
|
|
182
|
+
this.#entries = entries
|
|
183
|
+
.map((entry, index) =>
|
|
184
|
+
ParameterCollection.#normalizeEntry(entry, index)
|
|
185
|
+
)
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
this.#entriesByKey = ParameterCollection.#buildIndex(this.#entries)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Parses a raw pipe-delimited string or parsed field object.
|
|
192
|
+
* @param {string | Record<string, string | string[]> | { fields?: Record<string, string | string[]> }} input Source value.
|
|
193
|
+
* @returns {ParameterCollection}
|
|
194
|
+
*/
|
|
195
|
+
static parse(input) {
|
|
196
|
+
if (typeof input === 'string') {
|
|
197
|
+
return new ParameterCollection(
|
|
198
|
+
ParameterCollection.#entriesFromRaw(input)
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (input && typeof input === 'object' && 'fields' in input) {
|
|
203
|
+
return ParameterCollection.fromFields(input.fields)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return ParameterCollection.fromFields(input)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Builds a collection from an already parsed field object.
|
|
211
|
+
* @param {Record<string, string | string[]> | undefined} fields Field object.
|
|
212
|
+
* @returns {ParameterCollection}
|
|
213
|
+
*/
|
|
214
|
+
static fromFields(fields) {
|
|
215
|
+
return new ParameterCollection(
|
|
216
|
+
ParameterCollection.#entriesFromFields(fields)
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Returns the number of parsed entries including duplicates.
|
|
222
|
+
* @returns {number}
|
|
223
|
+
*/
|
|
224
|
+
get count() {
|
|
225
|
+
return this.#entries.length
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Returns parsed entries in source order.
|
|
230
|
+
* @returns {object[]}
|
|
231
|
+
*/
|
|
232
|
+
get entries() {
|
|
233
|
+
return this.#entries.map((entry) => ({ ...entry }))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Returns true when a key is present.
|
|
238
|
+
* @param {string} key Parameter key.
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
has(key) {
|
|
242
|
+
return this.#entriesByKey.has(ParameterCollection.#lookupKey(key))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Returns the first value for a key.
|
|
247
|
+
* @param {string} key Parameter key.
|
|
248
|
+
* @returns {ParameterValue}
|
|
249
|
+
*/
|
|
250
|
+
get(key) {
|
|
251
|
+
return new ParameterValue(
|
|
252
|
+
this.#entriesByKey.get(ParameterCollection.#lookupKey(key))?.[0] ||
|
|
253
|
+
null
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Returns the last value for a key.
|
|
259
|
+
* @param {string} key Parameter key.
|
|
260
|
+
* @returns {ParameterValue}
|
|
261
|
+
*/
|
|
262
|
+
last(key) {
|
|
263
|
+
const entries = this.#entriesByKey.get(
|
|
264
|
+
ParameterCollection.#lookupKey(key)
|
|
265
|
+
)
|
|
266
|
+
return new ParameterValue(entries?.[entries.length - 1] || null)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Returns every value for a key in source order.
|
|
271
|
+
* @param {string} key Parameter key.
|
|
272
|
+
* @returns {ParameterValue[]}
|
|
273
|
+
*/
|
|
274
|
+
getAll(key) {
|
|
275
|
+
return (
|
|
276
|
+
this.#entriesByKey.get(ParameterCollection.#lookupKey(key)) || []
|
|
277
|
+
).map((entry) => new ParameterValue(entry))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Converts the collection into a JSON-friendly object.
|
|
282
|
+
* @param {{ duplicates?: 'first' | 'last' | 'array' }} [options] Conversion options.
|
|
283
|
+
* @returns {Record<string, string | string[]>}
|
|
284
|
+
*/
|
|
285
|
+
toObject(options = {}) {
|
|
286
|
+
const duplicates = options.duplicates || 'last'
|
|
287
|
+
const output = {}
|
|
288
|
+
|
|
289
|
+
for (const entry of this.#entries) {
|
|
290
|
+
if (duplicates === 'array') {
|
|
291
|
+
output[entry.key] = [
|
|
292
|
+
...(Array.isArray(output[entry.key])
|
|
293
|
+
? output[entry.key]
|
|
294
|
+
: output[entry.key] === undefined
|
|
295
|
+
? []
|
|
296
|
+
: [output[entry.key]]),
|
|
297
|
+
entry.value
|
|
298
|
+
]
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (duplicates === 'first' && output[entry.key] !== undefined) {
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
output[entry.key] = entry.value
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return output
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Iterates parsed entries in source order.
|
|
314
|
+
* @returns {IterableIterator<object>}
|
|
315
|
+
*/
|
|
316
|
+
*[Symbol.iterator]() {
|
|
317
|
+
for (const entry of this.entries) {
|
|
318
|
+
yield entry
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Parses entries from a raw pipe-delimited record string.
|
|
324
|
+
* @param {string} raw Raw record string.
|
|
325
|
+
* @returns {object[]}
|
|
326
|
+
*/
|
|
327
|
+
static #entriesFromRaw(raw) {
|
|
328
|
+
return String(raw || '')
|
|
329
|
+
.replace(/[\r\n]/gu, '')
|
|
330
|
+
.split('|')
|
|
331
|
+
.map((segment) => segment.trim())
|
|
332
|
+
.filter(Boolean)
|
|
333
|
+
.map((segment) => ParameterCollection.#entryFromSegment(segment))
|
|
334
|
+
.filter(Boolean)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Parses one raw segment into an entry.
|
|
339
|
+
* @param {string} segment Segment text.
|
|
340
|
+
* @returns {object | null}
|
|
341
|
+
*/
|
|
342
|
+
static #entryFromSegment(segment) {
|
|
343
|
+
const separatorIndex = segment.indexOf('=')
|
|
344
|
+
if (separatorIndex <= 0) return null
|
|
345
|
+
|
|
346
|
+
const rawKey = segment.slice(0, separatorIndex).trim()
|
|
347
|
+
const isUtf8 = rawKey.startsWith('%UTF8%')
|
|
348
|
+
const key = rawKey.replace(/^%UTF8%/u, '')
|
|
349
|
+
if (!key) return null
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
key,
|
|
353
|
+
rawKey,
|
|
354
|
+
value: segment.slice(separatorIndex + 1).trim(),
|
|
355
|
+
isUtf8
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Converts parsed fields into ordered collection entries.
|
|
361
|
+
* @param {Record<string, string | string[]> | undefined} fields Parsed fields.
|
|
362
|
+
* @returns {object[]}
|
|
363
|
+
*/
|
|
364
|
+
static #entriesFromFields(fields) {
|
|
365
|
+
if (!fields || typeof fields !== 'object') return []
|
|
366
|
+
|
|
367
|
+
const entries = []
|
|
368
|
+
for (const key of Object.keys(fields)) {
|
|
369
|
+
if (key.startsWith('UTF8:')) continue
|
|
370
|
+
|
|
371
|
+
const utf8Key = 'UTF8:' + key
|
|
372
|
+
const rawValues = fields[utf8Key] || fields[key]
|
|
373
|
+
const values = Array.isArray(rawValues) ? rawValues : [rawValues]
|
|
374
|
+
for (const value of values) {
|
|
375
|
+
entries.push({
|
|
376
|
+
key,
|
|
377
|
+
rawKey: fields[utf8Key] ? '%UTF8%' + key : key,
|
|
378
|
+
value: String(value ?? ''),
|
|
379
|
+
isUtf8: Boolean(fields[utf8Key])
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return entries
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Normalizes one entry for internal storage.
|
|
389
|
+
* @param {object} entry Entry candidate.
|
|
390
|
+
* @param {number} index Source index.
|
|
391
|
+
* @returns {object | null}
|
|
392
|
+
*/
|
|
393
|
+
static #normalizeEntry(entry, index) {
|
|
394
|
+
const key = String(entry?.key || '').trim()
|
|
395
|
+
if (!key) return null
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
key,
|
|
399
|
+
rawKey: String(entry.rawKey || key),
|
|
400
|
+
value: String(entry.value ?? ''),
|
|
401
|
+
isUtf8: Boolean(entry.isUtf8),
|
|
402
|
+
index
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Builds a case-insensitive entry lookup.
|
|
408
|
+
* @param {object[]} entries Parsed entries.
|
|
409
|
+
* @returns {Map<string, object[]>}
|
|
410
|
+
*/
|
|
411
|
+
static #buildIndex(entries) {
|
|
412
|
+
const index = new Map()
|
|
413
|
+
|
|
414
|
+
for (const entry of entries) {
|
|
415
|
+
const lookupKey = ParameterCollection.#lookupKey(entry.key)
|
|
416
|
+
if (!index.has(lookupKey)) index.set(lookupKey, [])
|
|
417
|
+
index.get(lookupKey).push(entry)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return index
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Normalizes a lookup key.
|
|
425
|
+
* @param {string} key Parameter key.
|
|
426
|
+
* @returns {string}
|
|
427
|
+
*/
|
|
428
|
+
static #lookupKey(key) {
|
|
429
|
+
return String(key || '').toLowerCase()
|
|
430
|
+
}
|
|
431
|
+
}
|