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,840 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { LibraryQaReportBuilder } from './LibraryQaReportBuilder.mjs'
|
|
6
|
+
import { LibraryRenderManifestBuilder } from './LibraryRenderManifestBuilder.mjs'
|
|
7
|
+
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
8
|
+
import { ParserUtils } from './ParserUtils.mjs'
|
|
9
|
+
import { SchematicImplementationParser } from './SchematicImplementationParser.mjs'
|
|
10
|
+
|
|
11
|
+
const { getField, parseBoolean, parseNumericField, stripExtension } =
|
|
12
|
+
ParserUtils
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Normalizes extracted SchLib stream data into a read-only library model.
|
|
16
|
+
*/
|
|
17
|
+
export class SchLibModelParser {
|
|
18
|
+
/**
|
|
19
|
+
* Parses one extracted SchLib into a normalized read-only model.
|
|
20
|
+
* @param {string} fileName Source file name.
|
|
21
|
+
* @param {{ libraryRecords?: object[], fileHeaderRecords?: object[], sectionKeys?: object[], symbols?: object[], embeddedFiles?: object, nativeStreams?: object, streamNames?: string[], opaqueRecords?: object[], diagnostics?: object } | null} extraction Extracted streams.
|
|
22
|
+
* @returns {object}
|
|
23
|
+
*/
|
|
24
|
+
static parse(fileName, extraction = null) {
|
|
25
|
+
const safeExtraction = extraction || {}
|
|
26
|
+
const embeddedFiles = safeExtraction.embeddedFiles || {
|
|
27
|
+
schema: 'altium-toolkit.embedded-files.a1',
|
|
28
|
+
files: [],
|
|
29
|
+
diagnostics: []
|
|
30
|
+
}
|
|
31
|
+
const symbols = (safeExtraction.symbols || []).map((entry, index) =>
|
|
32
|
+
SchLibModelParser.#symbol(entry, index, embeddedFiles.files || [])
|
|
33
|
+
)
|
|
34
|
+
const schematicLibrary = {
|
|
35
|
+
libraryHeader: SchLibModelParser.#libraryHeader(
|
|
36
|
+
safeExtraction.libraryRecords || [],
|
|
37
|
+
safeExtraction.fileHeaderRecords || []
|
|
38
|
+
),
|
|
39
|
+
sectionKeys: safeExtraction.sectionKeys || [],
|
|
40
|
+
streamNames: safeExtraction.streamNames || [],
|
|
41
|
+
...(safeExtraction.nativeStreams
|
|
42
|
+
? { nativeStreams: safeExtraction.nativeStreams }
|
|
43
|
+
: {}),
|
|
44
|
+
symbols,
|
|
45
|
+
indexes: SchLibModelParser.#indexes(symbols),
|
|
46
|
+
embeddedFiles,
|
|
47
|
+
opaqueRecords: safeExtraction.opaqueRecords || []
|
|
48
|
+
}
|
|
49
|
+
schematicLibrary.renderManifest =
|
|
50
|
+
LibraryRenderManifestBuilder.buildSchematicLibraryManifest(
|
|
51
|
+
schematicLibrary
|
|
52
|
+
)
|
|
53
|
+
const model = NormalizedModelSchema.attach({
|
|
54
|
+
kind: 'schematic-library',
|
|
55
|
+
fileType: 'SchLib',
|
|
56
|
+
fileName,
|
|
57
|
+
summary: SchLibModelParser.#summary(
|
|
58
|
+
fileName,
|
|
59
|
+
symbols,
|
|
60
|
+
schematicLibrary.opaqueRecords,
|
|
61
|
+
safeExtraction.nativeStreams
|
|
62
|
+
),
|
|
63
|
+
diagnostics: SchLibModelParser.#diagnostics(
|
|
64
|
+
symbols,
|
|
65
|
+
safeExtraction
|
|
66
|
+
),
|
|
67
|
+
schematicLibrary,
|
|
68
|
+
bom: []
|
|
69
|
+
})
|
|
70
|
+
model.schematicLibrary.qa = LibraryQaReportBuilder.build({
|
|
71
|
+
schematicLibraries: [model]
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return model
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Builds one symbol read-model row.
|
|
79
|
+
* @param {object} entry Extracted symbol entry.
|
|
80
|
+
* @param {number} index Symbol index.
|
|
81
|
+
* @param {object[]} embeddedFiles Embedded file inventory rows.
|
|
82
|
+
* @returns {object}
|
|
83
|
+
*/
|
|
84
|
+
static #symbol(entry, index, embeddedFiles) {
|
|
85
|
+
const records = Array.isArray(entry.records) ? entry.records : []
|
|
86
|
+
const implementationRows =
|
|
87
|
+
SchLibModelParser.#implementationRows(records)
|
|
88
|
+
const componentRecord =
|
|
89
|
+
records.find((record) =>
|
|
90
|
+
['component', 'symbol', '1'].includes(
|
|
91
|
+
SchLibModelParser.#recordType(record).toLowerCase()
|
|
92
|
+
)
|
|
93
|
+
) || records[0]
|
|
94
|
+
const sourceStorage = String(entry.sourceStorage || '')
|
|
95
|
+
const name =
|
|
96
|
+
getField(componentRecord?.fields, 'Name') ||
|
|
97
|
+
getField(componentRecord?.fields, 'LibReference') ||
|
|
98
|
+
getField(componentRecord?.fields, 'DesignItemId') ||
|
|
99
|
+
SchLibModelParser.#basename(sourceStorage) ||
|
|
100
|
+
'Symbol' + (index + 1)
|
|
101
|
+
const pins = records
|
|
102
|
+
.filter((record) =>
|
|
103
|
+
['pin', '2'].includes(
|
|
104
|
+
SchLibModelParser.#recordType(record).toLowerCase()
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
.map((record, pinIndex) =>
|
|
108
|
+
SchLibModelParser.#pin(record, pinIndex, entry.pinSidecars)
|
|
109
|
+
)
|
|
110
|
+
const parameters = SchLibModelParser.#parameters(records)
|
|
111
|
+
const implementations = records
|
|
112
|
+
.filter((record) =>
|
|
113
|
+
['implementation', '45'].includes(
|
|
114
|
+
SchLibModelParser.#recordType(record).toLowerCase()
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
.map((record) =>
|
|
118
|
+
SchLibModelParser.#implementation(record, implementationRows)
|
|
119
|
+
)
|
|
120
|
+
const primitives = records
|
|
121
|
+
.filter((record) => !SchLibModelParser.#isSemanticRecord(record))
|
|
122
|
+
.map((record) => SchLibModelParser.#primitive(record))
|
|
123
|
+
const parts = SchLibModelParser.#parts(pins, primitives)
|
|
124
|
+
const symbolEmbeddedAssets = [
|
|
125
|
+
...(entry.embeddedAssets || []),
|
|
126
|
+
...embeddedFiles.filter((file) =>
|
|
127
|
+
String(file.sourceStream || '').startsWith(sourceStorage + '/')
|
|
128
|
+
)
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
return SchLibModelParser.#stripUndefined({
|
|
132
|
+
name,
|
|
133
|
+
displayName: getField(componentRecord?.fields, 'DisplayName'),
|
|
134
|
+
sourceId: getField(componentRecord?.fields, 'SourceId'),
|
|
135
|
+
sourceStorage,
|
|
136
|
+
sourceStream: entry.sourceStream,
|
|
137
|
+
declaredPinCount: SchLibModelParser.#optionalNumber(
|
|
138
|
+
getField(componentRecord?.fields, 'PinCount')
|
|
139
|
+
),
|
|
140
|
+
declaredPrimitiveCount: SchLibModelParser.#optionalNumber(
|
|
141
|
+
getField(componentRecord?.fields, 'PrimitiveCount')
|
|
142
|
+
),
|
|
143
|
+
pins,
|
|
144
|
+
parts,
|
|
145
|
+
parameters,
|
|
146
|
+
implementations,
|
|
147
|
+
primitives,
|
|
148
|
+
embeddedAssets: symbolEmbeddedAssets,
|
|
149
|
+
opaqueRecords: entry.opaqueRecords || []
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Builds one pin row.
|
|
155
|
+
* @param {object} record Pin source record.
|
|
156
|
+
* @param {number} pinIndex Zero-based pin index.
|
|
157
|
+
* @param {Record<string, object[]>} pinSidecars Parsed pin side streams.
|
|
158
|
+
* @returns {object}
|
|
159
|
+
*/
|
|
160
|
+
static #pin(record, pinIndex, pinSidecars = {}) {
|
|
161
|
+
const fracFields = SchLibModelParser.#sidecarFields(
|
|
162
|
+
pinSidecars,
|
|
163
|
+
'PinFrac',
|
|
164
|
+
pinIndex
|
|
165
|
+
)
|
|
166
|
+
const descFields = SchLibModelParser.#sidecarFields(
|
|
167
|
+
pinSidecars,
|
|
168
|
+
'PinDesc',
|
|
169
|
+
pinIndex
|
|
170
|
+
)
|
|
171
|
+
const textFields = SchLibModelParser.#sidecarFields(
|
|
172
|
+
pinSidecars,
|
|
173
|
+
'PinTextData',
|
|
174
|
+
pinIndex
|
|
175
|
+
)
|
|
176
|
+
const packageFields = SchLibModelParser.#sidecarFields(
|
|
177
|
+
pinSidecars,
|
|
178
|
+
'PinPackageLength',
|
|
179
|
+
pinIndex
|
|
180
|
+
)
|
|
181
|
+
const functionFields = SchLibModelParser.#sidecarFields(
|
|
182
|
+
pinSidecars,
|
|
183
|
+
'PinFunctionData',
|
|
184
|
+
pinIndex
|
|
185
|
+
)
|
|
186
|
+
const symbolLineFields = SchLibModelParser.#sidecarFields(
|
|
187
|
+
pinSidecars,
|
|
188
|
+
'PinSymbolLineWidth',
|
|
189
|
+
pinIndex
|
|
190
|
+
)
|
|
191
|
+
const textStyle = SchLibModelParser.#pinTextStyle(textFields)
|
|
192
|
+
|
|
193
|
+
return SchLibModelParser.#stripUndefined({
|
|
194
|
+
designator:
|
|
195
|
+
getField(record.fields, 'Designator') ||
|
|
196
|
+
getField(record.fields, 'PinNumber'),
|
|
197
|
+
name: getField(record.fields, 'Name'),
|
|
198
|
+
partId: SchLibModelParser.#partId(record.fields),
|
|
199
|
+
electricalType: SchLibModelParser.#lowerField(
|
|
200
|
+
record.fields,
|
|
201
|
+
'ElectricalType'
|
|
202
|
+
),
|
|
203
|
+
formalType: SchLibModelParser.#lowerField(
|
|
204
|
+
record.fields,
|
|
205
|
+
'FormalType'
|
|
206
|
+
),
|
|
207
|
+
description:
|
|
208
|
+
SchLibModelParser.#firstField(descFields, [
|
|
209
|
+
'Description',
|
|
210
|
+
'PinDescription',
|
|
211
|
+
'Text'
|
|
212
|
+
]) || getField(record.fields, 'Description'),
|
|
213
|
+
pinFunction: SchLibModelParser.#firstField(functionFields, [
|
|
214
|
+
'PinFunction',
|
|
215
|
+
'Function',
|
|
216
|
+
'Text'
|
|
217
|
+
]),
|
|
218
|
+
x:
|
|
219
|
+
SchLibModelParser.#optionalNumber(
|
|
220
|
+
SchLibModelParser.#firstField(fracFields, [
|
|
221
|
+
'Location.X',
|
|
222
|
+
'X'
|
|
223
|
+
])
|
|
224
|
+
) ?? parseNumericField(record.fields, 'Location.X'),
|
|
225
|
+
y:
|
|
226
|
+
SchLibModelParser.#optionalNumber(
|
|
227
|
+
SchLibModelParser.#firstField(fracFields, [
|
|
228
|
+
'Location.Y',
|
|
229
|
+
'Y'
|
|
230
|
+
])
|
|
231
|
+
) ?? parseNumericField(record.fields, 'Location.Y'),
|
|
232
|
+
length:
|
|
233
|
+
SchLibModelParser.#optionalNumber(
|
|
234
|
+
SchLibModelParser.#firstField(fracFields, [
|
|
235
|
+
'PinLength',
|
|
236
|
+
'Length'
|
|
237
|
+
])
|
|
238
|
+
) ??
|
|
239
|
+
parseNumericField(record.fields, 'PinLength') ??
|
|
240
|
+
parseNumericField(record.fields, 'Length'),
|
|
241
|
+
pinPackageLength:
|
|
242
|
+
SchLibModelParser.#optionalNumber(
|
|
243
|
+
SchLibModelParser.#firstField(packageFields, [
|
|
244
|
+
'PinPackageLength',
|
|
245
|
+
'PackageLength',
|
|
246
|
+
'Length'
|
|
247
|
+
])
|
|
248
|
+
) ??
|
|
249
|
+
parseNumericField(record.fields, 'PinPackageLength') ??
|
|
250
|
+
parseNumericField(record.fields, 'PackageLength'),
|
|
251
|
+
symbolInner: SchLibModelParser.#lowerFirstField(record.fields, [
|
|
252
|
+
'Symbol_Inner',
|
|
253
|
+
'SymbolInner',
|
|
254
|
+
'SymbolInnerEdge'
|
|
255
|
+
]),
|
|
256
|
+
symbolOuter: SchLibModelParser.#lowerFirstField(record.fields, [
|
|
257
|
+
'Symbol_Outer',
|
|
258
|
+
'SymbolOuter',
|
|
259
|
+
'SymbolOuterEdge'
|
|
260
|
+
]),
|
|
261
|
+
symbolInside: SchLibModelParser.#lowerFirstField(record.fields, [
|
|
262
|
+
'Symbol_Inside',
|
|
263
|
+
'SymbolInside'
|
|
264
|
+
]),
|
|
265
|
+
symbolOutside: SchLibModelParser.#lowerFirstField(record.fields, [
|
|
266
|
+
'Symbol_Outside',
|
|
267
|
+
'SymbolOutside'
|
|
268
|
+
]),
|
|
269
|
+
symbolLineWidth:
|
|
270
|
+
SchLibModelParser.#optionalNumber(
|
|
271
|
+
SchLibModelParser.#firstField(symbolLineFields, [
|
|
272
|
+
'SymbolLineWidth',
|
|
273
|
+
'LineWidth',
|
|
274
|
+
'Width'
|
|
275
|
+
])
|
|
276
|
+
) ?? parseNumericField(record.fields, 'SymbolLineWidth'),
|
|
277
|
+
swapIdPart: parseNumericField(record.fields, 'SwapIdPart'),
|
|
278
|
+
swapIdPair: parseNumericField(record.fields, 'SwapIdPair'),
|
|
279
|
+
swapIdPartPin: parseNumericField(record.fields, 'SwapIdPartPin'),
|
|
280
|
+
swapIdPin: parseNumericField(record.fields, 'SwapIdPin'),
|
|
281
|
+
defaultValue: getField(record.fields, 'DefaultValue'),
|
|
282
|
+
hidden: SchLibModelParser.#optionalBoolean(record.fields, [
|
|
283
|
+
'IsHidden',
|
|
284
|
+
'Hidden'
|
|
285
|
+
]),
|
|
286
|
+
...(Object.keys(textStyle).length ? { textStyle } : {})
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Builds one implementation row.
|
|
292
|
+
* @param {object} record Implementation source record.
|
|
293
|
+
* @param {Map<number, object>} implementationRows Parsed implementation rows.
|
|
294
|
+
* @returns {object}
|
|
295
|
+
*/
|
|
296
|
+
static #implementation(record, implementationRows) {
|
|
297
|
+
const implementationRow = implementationRows.get(record.recordIndex)
|
|
298
|
+
const detailedTargetLibraries = (
|
|
299
|
+
implementationRow?.targetLibraries || []
|
|
300
|
+
)
|
|
301
|
+
.map((target) => target.fileName || target.entity)
|
|
302
|
+
.filter(Boolean)
|
|
303
|
+
const targetLibraries = [
|
|
304
|
+
...new Set([
|
|
305
|
+
...SchLibModelParser.#targetLibraries(record.fields),
|
|
306
|
+
...detailedTargetLibraries
|
|
307
|
+
])
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
return SchLibModelParser.#stripUndefined({
|
|
311
|
+
modelName: getField(record.fields, 'ModelName'),
|
|
312
|
+
modelType: SchLibModelParser.#lowerField(
|
|
313
|
+
record.fields,
|
|
314
|
+
'ModelType'
|
|
315
|
+
),
|
|
316
|
+
targetLibraries,
|
|
317
|
+
searchPaths: implementationRow?.searchPaths,
|
|
318
|
+
mapDefiners: implementationRow?.mapDefiners,
|
|
319
|
+
parameters: implementationRow?.parameters
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Builds one primitive summary row.
|
|
325
|
+
* @param {object} record Primitive source record.
|
|
326
|
+
* @returns {object}
|
|
327
|
+
*/
|
|
328
|
+
static #primitive(record) {
|
|
329
|
+
return SchLibModelParser.#stripUndefined({
|
|
330
|
+
recordType: SchLibModelParser.#recordType(record),
|
|
331
|
+
recordIndex: record.recordIndex,
|
|
332
|
+
partId: SchLibModelParser.#partId(record.fields)
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Builds symbol parameters from parameter records.
|
|
338
|
+
* @param {object[]} records Source records.
|
|
339
|
+
* @returns {Record<string, string>}
|
|
340
|
+
*/
|
|
341
|
+
static #parameters(records) {
|
|
342
|
+
const parameters = {}
|
|
343
|
+
|
|
344
|
+
for (const record of records) {
|
|
345
|
+
if (
|
|
346
|
+
!['parameter', '41'].includes(
|
|
347
|
+
SchLibModelParser.#recordType(record).toLowerCase()
|
|
348
|
+
)
|
|
349
|
+
) {
|
|
350
|
+
continue
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const name = getField(record.fields, 'Name')
|
|
354
|
+
const value =
|
|
355
|
+
getField(record.fields, 'Text') ||
|
|
356
|
+
getField(record.fields, 'Value')
|
|
357
|
+
if (name) {
|
|
358
|
+
parameters[name] = value
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return parameters
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Builds part summaries from pins and primitive rows.
|
|
367
|
+
* @param {object[]} pins Pin rows.
|
|
368
|
+
* @param {object[]} primitives Primitive rows.
|
|
369
|
+
* @returns {object[]}
|
|
370
|
+
*/
|
|
371
|
+
static #parts(pins, primitives) {
|
|
372
|
+
const partIds = [
|
|
373
|
+
...new Set(
|
|
374
|
+
[...pins, ...primitives]
|
|
375
|
+
.map((row) => String(row.partId || '').trim())
|
|
376
|
+
.filter(Boolean)
|
|
377
|
+
)
|
|
378
|
+
].sort((left, right) => left.localeCompare(right))
|
|
379
|
+
|
|
380
|
+
return partIds.map((partId) => ({
|
|
381
|
+
partId,
|
|
382
|
+
pinCount: pins.filter((pin) => pin.partId === partId).length,
|
|
383
|
+
primitiveCount: primitives.filter(
|
|
384
|
+
(primitive) => primitive.partId === partId
|
|
385
|
+
).length
|
|
386
|
+
}))
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Builds library indexes.
|
|
391
|
+
* @param {object[]} symbols Symbol rows.
|
|
392
|
+
* @returns {object}
|
|
393
|
+
*/
|
|
394
|
+
static #indexes(symbols) {
|
|
395
|
+
return {
|
|
396
|
+
symbolsByName: Object.fromEntries(
|
|
397
|
+
symbols.map((symbol, index) => [
|
|
398
|
+
symbol.name,
|
|
399
|
+
{
|
|
400
|
+
index,
|
|
401
|
+
name: symbol.name,
|
|
402
|
+
sourceStorage: symbol.sourceStorage,
|
|
403
|
+
pinCount: symbol.pins.length,
|
|
404
|
+
partCount: symbol.parts.length,
|
|
405
|
+
keywordCount: SchLibModelParser.#keywords(symbol).length
|
|
406
|
+
}
|
|
407
|
+
])
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Builds a library header from Library/Data records.
|
|
414
|
+
* @param {object[]} records Library records.
|
|
415
|
+
* @param {object[]} fileHeaderRecords FileHeader records.
|
|
416
|
+
* @returns {Record<string, string>}
|
|
417
|
+
*/
|
|
418
|
+
static #libraryHeader(records, fileHeaderRecords) {
|
|
419
|
+
const headerRecord = records.find((record) =>
|
|
420
|
+
getField(record.fields, 'HEADER')
|
|
421
|
+
)
|
|
422
|
+
const header = headerRecord
|
|
423
|
+
? {
|
|
424
|
+
header: getField(headerRecord.fields, 'HEADER')
|
|
425
|
+
}
|
|
426
|
+
: {}
|
|
427
|
+
const fileHeader = SchLibModelParser.#fileHeader(fileHeaderRecords)
|
|
428
|
+
|
|
429
|
+
return SchLibModelParser.#stripUndefined({
|
|
430
|
+
...header,
|
|
431
|
+
...(fileHeader ? { fileHeader } : {})
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Builds the parser summary.
|
|
437
|
+
* @param {string} fileName Source file name.
|
|
438
|
+
* @param {object[]} symbols Symbol rows.
|
|
439
|
+
* @param {object[]} opaqueRecords Opaque records.
|
|
440
|
+
* @param {object | undefined} nativeStreams Native stream inventory.
|
|
441
|
+
* @returns {object}
|
|
442
|
+
*/
|
|
443
|
+
static #summary(fileName, symbols, opaqueRecords, nativeStreams) {
|
|
444
|
+
return {
|
|
445
|
+
title: stripExtension(fileName),
|
|
446
|
+
symbolCount: symbols.length,
|
|
447
|
+
pinCount: symbols.reduce(
|
|
448
|
+
(count, symbol) => count + symbol.pins.length,
|
|
449
|
+
0
|
|
450
|
+
),
|
|
451
|
+
partCount: symbols.reduce(
|
|
452
|
+
(count, symbol) => count + symbol.parts.length,
|
|
453
|
+
0
|
|
454
|
+
),
|
|
455
|
+
embeddedAssetCount: symbols.reduce(
|
|
456
|
+
(count, symbol) => count + (symbol.embeddedAssets || []).length,
|
|
457
|
+
0
|
|
458
|
+
),
|
|
459
|
+
...(nativeStreams?.summary?.streamCount
|
|
460
|
+
? {
|
|
461
|
+
nativeStreamCount: nativeStreams.summary.streamCount
|
|
462
|
+
}
|
|
463
|
+
: {}),
|
|
464
|
+
opaqueRecordCount: opaqueRecords.length
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Builds parser diagnostics.
|
|
470
|
+
* @param {object[]} symbols Symbol rows.
|
|
471
|
+
* @param {object} extraction Extraction metadata.
|
|
472
|
+
* @returns {object[]}
|
|
473
|
+
*/
|
|
474
|
+
static #diagnostics(symbols, extraction) {
|
|
475
|
+
const diagnostics = [
|
|
476
|
+
{
|
|
477
|
+
severity: 'info',
|
|
478
|
+
message:
|
|
479
|
+
'Recovered ' +
|
|
480
|
+
symbols.length +
|
|
481
|
+
' schematic library symbol definitions.'
|
|
482
|
+
}
|
|
483
|
+
]
|
|
484
|
+
|
|
485
|
+
if (Array.isArray(extraction.streamNames)) {
|
|
486
|
+
diagnostics.push({
|
|
487
|
+
severity: 'info',
|
|
488
|
+
message:
|
|
489
|
+
'Recovered ' +
|
|
490
|
+
extraction.streamNames.length +
|
|
491
|
+
' SchLib data streams from the compound document.'
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return diagnostics
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Returns true when the record has a dedicated semantic model row.
|
|
500
|
+
* @param {object} record Source record.
|
|
501
|
+
* @returns {boolean}
|
|
502
|
+
*/
|
|
503
|
+
static #isSemanticRecord(record) {
|
|
504
|
+
return [
|
|
505
|
+
'component',
|
|
506
|
+
'symbol',
|
|
507
|
+
'1',
|
|
508
|
+
'pin',
|
|
509
|
+
'2',
|
|
510
|
+
'parameter',
|
|
511
|
+
'41',
|
|
512
|
+
'implementation',
|
|
513
|
+
'45'
|
|
514
|
+
].includes(SchLibModelParser.#recordType(record).toLowerCase())
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Resolves one record type string.
|
|
519
|
+
* @param {object} record Source record.
|
|
520
|
+
* @returns {string}
|
|
521
|
+
*/
|
|
522
|
+
static #recordType(record) {
|
|
523
|
+
return getField(record?.fields, 'RECORD') || 'unknown'
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Resolves one part id from common source fields.
|
|
528
|
+
* @param {object} fields Source fields.
|
|
529
|
+
* @returns {string | undefined}
|
|
530
|
+
*/
|
|
531
|
+
static #partId(fields) {
|
|
532
|
+
return (
|
|
533
|
+
getField(fields, 'PartId') ||
|
|
534
|
+
getField(fields, 'OwnerPartId') ||
|
|
535
|
+
getField(fields, 'OwnerPartID') ||
|
|
536
|
+
undefined
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Resolves target library names from implementation fields.
|
|
542
|
+
* @param {object} fields Source fields.
|
|
543
|
+
* @returns {string[]}
|
|
544
|
+
*/
|
|
545
|
+
static #targetLibraries(fields) {
|
|
546
|
+
const directTargets = (
|
|
547
|
+
getField(fields, 'TargetLibraries') ||
|
|
548
|
+
getField(fields, 'TargetLibrary') ||
|
|
549
|
+
''
|
|
550
|
+
)
|
|
551
|
+
.split(/[;,]/u)
|
|
552
|
+
.map((value) => value.trim())
|
|
553
|
+
.filter(Boolean)
|
|
554
|
+
const indexedTargets = []
|
|
555
|
+
const count =
|
|
556
|
+
parseNumericField(fields, 'DatafileCount') ??
|
|
557
|
+
SchLibModelParser.#countIndexedFields(fields, 'ModelDatafileEntity')
|
|
558
|
+
|
|
559
|
+
for (let index = 0; index < count; index += 1) {
|
|
560
|
+
const entity = getField(fields, 'ModelDatafileEntity' + index)
|
|
561
|
+
const kind = getField(fields, 'ModelDatafileKind' + index)
|
|
562
|
+
const fileName = SchLibModelParser.#libraryFileName(entity, kind)
|
|
563
|
+
if (fileName) {
|
|
564
|
+
indexedTargets.push(fileName)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return [...new Set([...directTargets, ...indexedTargets])]
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Builds parsed implementation rows keyed by source record index.
|
|
573
|
+
* @param {object[]} records Symbol records.
|
|
574
|
+
* @returns {Map<number, object>}
|
|
575
|
+
*/
|
|
576
|
+
static #implementationRows(records) {
|
|
577
|
+
const parsed = SchematicImplementationParser.parse(records)
|
|
578
|
+
|
|
579
|
+
return new Map(
|
|
580
|
+
(parsed?.implementations || []).map((implementation) => [
|
|
581
|
+
Number(
|
|
582
|
+
String(implementation.recordKey || '').replace(
|
|
583
|
+
/^schematic-record-/u,
|
|
584
|
+
''
|
|
585
|
+
)
|
|
586
|
+
),
|
|
587
|
+
implementation
|
|
588
|
+
])
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Builds one file-header summary.
|
|
594
|
+
* @param {object[]} records FileHeader records.
|
|
595
|
+
* @returns {object | null}
|
|
596
|
+
*/
|
|
597
|
+
static #fileHeader(records) {
|
|
598
|
+
const record = (records || []).find((entry) =>
|
|
599
|
+
getField(entry.fields, 'HEADER')
|
|
600
|
+
)
|
|
601
|
+
if (!record) {
|
|
602
|
+
return null
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const fonts = SchLibModelParser.#fonts(record.fields)
|
|
606
|
+
|
|
607
|
+
return SchLibModelParser.#stripUndefined({
|
|
608
|
+
header: getField(record.fields, 'HEADER'),
|
|
609
|
+
fonts: fonts.length ? fonts : undefined
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Parses indexed font entries from FileHeader fields.
|
|
615
|
+
* @param {object} fields FileHeader fields.
|
|
616
|
+
* @returns {object[]}
|
|
617
|
+
*/
|
|
618
|
+
static #fonts(fields) {
|
|
619
|
+
return Object.keys(fields || {})
|
|
620
|
+
.map((key) => /^FontName(\d+)$/iu.exec(key))
|
|
621
|
+
.filter(Boolean)
|
|
622
|
+
.map((match) => {
|
|
623
|
+
const index = Number.parseInt(match[1], 10)
|
|
624
|
+
return SchLibModelParser.#stripUndefined({
|
|
625
|
+
index,
|
|
626
|
+
name: getField(fields, 'FontName' + index),
|
|
627
|
+
size: SchLibModelParser.#optionalNumber(
|
|
628
|
+
getField(fields, 'FontSize' + index)
|
|
629
|
+
),
|
|
630
|
+
id: SchLibModelParser.#optionalNumber(
|
|
631
|
+
getField(fields, 'FontId' + index)
|
|
632
|
+
)
|
|
633
|
+
})
|
|
634
|
+
})
|
|
635
|
+
.sort((left, right) => left.index - right.index)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Returns the sidecar fields for one pin index.
|
|
640
|
+
* @param {Record<string, object[]>} pinSidecars Parsed sidecar records.
|
|
641
|
+
* @param {string} sidecarName Sidecar stream basename.
|
|
642
|
+
* @param {number} pinIndex Zero-based pin index.
|
|
643
|
+
* @returns {object}
|
|
644
|
+
*/
|
|
645
|
+
static #sidecarFields(pinSidecars, sidecarName, pinIndex) {
|
|
646
|
+
const records = pinSidecars?.[sidecarName] || []
|
|
647
|
+
|
|
648
|
+
return (
|
|
649
|
+
records.find((record, recordIndex) => {
|
|
650
|
+
const explicitIndex =
|
|
651
|
+
parseNumericField(record.fields, 'PinIndex') ??
|
|
652
|
+
parseNumericField(record.fields, 'Index') ??
|
|
653
|
+
parseNumericField(record.fields, 'RecordIndex')
|
|
654
|
+
|
|
655
|
+
return explicitIndex === null
|
|
656
|
+
? recordIndex === pinIndex
|
|
657
|
+
: explicitIndex === pinIndex
|
|
658
|
+
})?.fields || {}
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Builds text-style metadata from PinTextData fields.
|
|
664
|
+
* @param {object} fields Pin text fields.
|
|
665
|
+
* @returns {object}
|
|
666
|
+
*/
|
|
667
|
+
static #pinTextStyle(fields) {
|
|
668
|
+
return SchLibModelParser.#stripUndefined({
|
|
669
|
+
nameFontId: parseNumericField(fields, 'NameFontId'),
|
|
670
|
+
designatorFontId: parseNumericField(fields, 'DesignatorFontId'),
|
|
671
|
+
namePosition: SchLibModelParser.#lowerField(fields, 'NamePosition'),
|
|
672
|
+
designatorPosition: SchLibModelParser.#lowerField(
|
|
673
|
+
fields,
|
|
674
|
+
'DesignatorPosition'
|
|
675
|
+
)
|
|
676
|
+
})
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Returns the first non-empty field from a field set.
|
|
681
|
+
* @param {object} fields Source fields.
|
|
682
|
+
* @param {string[]} keys Candidate keys.
|
|
683
|
+
* @returns {string}
|
|
684
|
+
*/
|
|
685
|
+
static #firstField(fields, keys) {
|
|
686
|
+
for (const key of keys || []) {
|
|
687
|
+
const value = getField(fields, key)
|
|
688
|
+
if (value) {
|
|
689
|
+
return value
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
return ''
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Reads and lower-cases the first available field.
|
|
698
|
+
* @param {object} fields Source fields.
|
|
699
|
+
* @param {string[]} keys Candidate keys.
|
|
700
|
+
* @returns {string | undefined}
|
|
701
|
+
*/
|
|
702
|
+
static #lowerFirstField(fields, keys) {
|
|
703
|
+
const value = SchLibModelParser.#firstField(fields, keys)
|
|
704
|
+
|
|
705
|
+
return value ? value.toLowerCase() : undefined
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Reads an optional boolean field.
|
|
710
|
+
* @param {object} fields Source fields.
|
|
711
|
+
* @param {string[]} keys Candidate keys.
|
|
712
|
+
* @returns {boolean | undefined}
|
|
713
|
+
*/
|
|
714
|
+
static #optionalBoolean(fields, keys) {
|
|
715
|
+
for (const key of keys || []) {
|
|
716
|
+
const value = getField(fields, key)
|
|
717
|
+
if (value) {
|
|
718
|
+
return parseBoolean(value)
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return undefined
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Counts fields with an indexed prefix.
|
|
727
|
+
* @param {object} fields Source fields.
|
|
728
|
+
* @param {string} prefix Indexed field prefix.
|
|
729
|
+
* @returns {number}
|
|
730
|
+
*/
|
|
731
|
+
static #countIndexedFields(fields, prefix) {
|
|
732
|
+
const normalizedPrefix = prefix.toLowerCase()
|
|
733
|
+
|
|
734
|
+
return Object.keys(fields || {}).filter((key) =>
|
|
735
|
+
key.toLowerCase().startsWith(normalizedPrefix)
|
|
736
|
+
).length
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Infers a native library file name from entity and kind fields.
|
|
741
|
+
* @param {string} entity Library entity.
|
|
742
|
+
* @param {string} kind Library kind.
|
|
743
|
+
* @returns {string}
|
|
744
|
+
*/
|
|
745
|
+
static #libraryFileName(entity, kind) {
|
|
746
|
+
if (!entity) {
|
|
747
|
+
return ''
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (/\.[^.]+$/u.test(entity)) {
|
|
751
|
+
return entity
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const extension =
|
|
755
|
+
{
|
|
756
|
+
pcblib: 'PcbLib',
|
|
757
|
+
schlib: 'SchLib',
|
|
758
|
+
intlib: 'IntLib',
|
|
759
|
+
sim: 'SimModel'
|
|
760
|
+
}[
|
|
761
|
+
String(kind || '')
|
|
762
|
+
.trim()
|
|
763
|
+
.toLowerCase()
|
|
764
|
+
] || kind
|
|
765
|
+
|
|
766
|
+
return extension ? entity + '.' + extension : entity
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Builds symbol search keywords.
|
|
771
|
+
* @param {object} symbol Symbol row.
|
|
772
|
+
* @returns {string[]}
|
|
773
|
+
*/
|
|
774
|
+
static #keywords(symbol) {
|
|
775
|
+
return [
|
|
776
|
+
symbol.name,
|
|
777
|
+
symbol.displayName,
|
|
778
|
+
symbol.sourceId,
|
|
779
|
+
...Object.values(symbol.parameters || {})
|
|
780
|
+
]
|
|
781
|
+
.map((value) => String(value || '').trim())
|
|
782
|
+
.filter(Boolean)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Reads a lower-case field value.
|
|
787
|
+
* @param {object} fields Source fields.
|
|
788
|
+
* @param {string} key Field key.
|
|
789
|
+
* @returns {string | undefined}
|
|
790
|
+
*/
|
|
791
|
+
static #lowerField(fields, key) {
|
|
792
|
+
const value = getField(fields, key)
|
|
793
|
+
return value ? value.toLowerCase() : undefined
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Normalizes an optional numeric value.
|
|
798
|
+
* @param {unknown} value Source value.
|
|
799
|
+
* @returns {number | undefined}
|
|
800
|
+
*/
|
|
801
|
+
static #optionalNumber(value) {
|
|
802
|
+
if (value === undefined || value === null) {
|
|
803
|
+
return undefined
|
|
804
|
+
}
|
|
805
|
+
if (String(value).trim() === '') {
|
|
806
|
+
return undefined
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const numeric = Number(value)
|
|
810
|
+
return Number.isFinite(numeric) ? numeric : undefined
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Resolves the final path component.
|
|
815
|
+
* @param {string} value Path.
|
|
816
|
+
* @returns {string}
|
|
817
|
+
*/
|
|
818
|
+
static #basename(value) {
|
|
819
|
+
return (
|
|
820
|
+
String(value || '')
|
|
821
|
+
.split('/')
|
|
822
|
+
.filter(Boolean)
|
|
823
|
+
.pop() || ''
|
|
824
|
+
)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Removes undefined values from an object.
|
|
829
|
+
* @param {object} row Row to strip.
|
|
830
|
+
* @returns {object}
|
|
831
|
+
*/
|
|
832
|
+
static #stripUndefined(row) {
|
|
833
|
+
return Object.fromEntries(
|
|
834
|
+
Object.entries(row).filter(
|
|
835
|
+
([, value]) =>
|
|
836
|
+
value !== undefined && value !== null && value !== ''
|
|
837
|
+
)
|
|
838
|
+
)
|
|
839
|
+
}
|
|
840
|
+
}
|