altium-toolkit 1.0.8 → 1.0.9
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 +18 -6
- package/docs/api.md +78 -16
- package/docs/model-format.md +229 -8
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/testing.md +9 -3
- package/package.json +1 -1
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumLayoutParser.mjs +104 -8
- package/src/core/altium/AltiumParser.mjs +191 -45
- package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
- package/src/core/altium/IntLibModelParser.mjs +240 -0
- package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
- package/src/core/altium/LibrarySearchIndex.mjs +215 -0
- package/src/core/altium/NormalizedModelSchema.mjs +36 -0
- package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
- package/src/core/altium/PcbDefaultsParser.mjs +171 -0
- package/src/core/altium/PcbDimensionParser.mjs +229 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
- package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
- package/src/core/altium/PcbLibModelParser.mjs +235 -14
- package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
- package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
- package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
- package/src/core/altium/PcbModelParser.mjs +466 -28
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +58 -0
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
- package/src/core/altium/PcbRuleParser.mjs +354 -33
- package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
- package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
- package/src/core/altium/PcbStreamExtractor.mjs +111 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
- package/src/core/altium/PcbUnionParser.mjs +307 -0
- package/src/core/altium/PcbViaStackParser.mjs +98 -10
- package/src/core/altium/PcbViaStructureParser.mjs +335 -0
- package/src/core/altium/PrintableTextDecoder.mjs +53 -3
- package/src/core/altium/PrjPcbModelParser.mjs +257 -5
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
- package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
- package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
- package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
- package/src/core/altium/SchematicHarnessParser.mjs +302 -0
- package/src/core/altium/SchematicImageParser.mjs +474 -3
- package/src/core/altium/SchematicImplementationParser.mjs +518 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
- package/src/core/altium/SchematicPinParser.mjs +84 -1
- package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
- package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
- package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
- package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
- package/src/core/altium/SchematicTemplateParser.mjs +256 -0
- package/src/core/altium/SchematicTextParser.mjs +123 -0
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +29 -0
- package/src/styles/altium-renderers.css +19 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionIndex.mjs +9 -4
- package/src/ui/PcbScene3dBuilder.mjs +137 -3
- package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
- package/src/ui/PcbSvgRenderer.mjs +1187 -34
- package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
- package/src/ui/SchematicNoteRenderer.mjs +9 -2
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
- package/src/ui/SchematicShapeRenderer.mjs +362 -0
- package/src/ui/SchematicSvgRenderer.mjs +1442 -92
- package/src/ui/SchematicTypography.mjs +48 -5
- package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalizes integrated-library extraction into the public parser model.
|
|
9
|
+
*/
|
|
10
|
+
export class IntLibModelParser {
|
|
11
|
+
/**
|
|
12
|
+
* Builds one integrated-library model.
|
|
13
|
+
* @param {string} fileName
|
|
14
|
+
* @param {{ version?: string, crossReferences?: object[], parameters?: Record<string, string>, parameterRecords?: object[], sources?: object[], streamNames?: string[], diagnostics?: Record<string, number> } | null} extraction
|
|
15
|
+
* @returns {{ schema: string, kind: 'integrated-library', fileType: 'IntLib', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], integratedLibrary: object, bom: [] }}
|
|
16
|
+
*/
|
|
17
|
+
static parse(fileName, extraction = null) {
|
|
18
|
+
const normalizedExtraction = extraction || {}
|
|
19
|
+
const sources = Array.isArray(normalizedExtraction.sources)
|
|
20
|
+
? normalizedExtraction.sources
|
|
21
|
+
: []
|
|
22
|
+
const crossReferences = Array.isArray(
|
|
23
|
+
normalizedExtraction.crossReferences
|
|
24
|
+
)
|
|
25
|
+
? normalizedExtraction.crossReferences
|
|
26
|
+
: []
|
|
27
|
+
const parameters = normalizedExtraction.parameters || {}
|
|
28
|
+
const streamNames = Array.isArray(normalizedExtraction.streamNames)
|
|
29
|
+
? normalizedExtraction.streamNames
|
|
30
|
+
: []
|
|
31
|
+
|
|
32
|
+
return NormalizedModelSchema.attach({
|
|
33
|
+
kind: 'integrated-library',
|
|
34
|
+
fileType: 'IntLib',
|
|
35
|
+
fileName,
|
|
36
|
+
summary: {
|
|
37
|
+
title: IntLibModelParser.#stripExtension(fileName),
|
|
38
|
+
version: normalizedExtraction.version || '',
|
|
39
|
+
sourceCount: sources.length,
|
|
40
|
+
crossReferenceCount: crossReferences.length,
|
|
41
|
+
parameterCount: Object.keys(parameters).length
|
|
42
|
+
},
|
|
43
|
+
diagnostics: IntLibModelParser.#buildDiagnostics(
|
|
44
|
+
streamNames,
|
|
45
|
+
sources,
|
|
46
|
+
crossReferences,
|
|
47
|
+
parameters,
|
|
48
|
+
normalizedExtraction.diagnostics?.issues || []
|
|
49
|
+
),
|
|
50
|
+
integratedLibrary: {
|
|
51
|
+
version: normalizedExtraction.version || '',
|
|
52
|
+
streamNames,
|
|
53
|
+
crossReferences,
|
|
54
|
+
parameters,
|
|
55
|
+
parameterRecords: normalizedExtraction.parameterRecords || [],
|
|
56
|
+
diagnostics: {
|
|
57
|
+
...(normalizedExtraction.diagnostics || {}),
|
|
58
|
+
issues: normalizedExtraction.diagnostics?.issues || []
|
|
59
|
+
},
|
|
60
|
+
indexes: IntLibModelParser.#buildIndexes(
|
|
61
|
+
sources,
|
|
62
|
+
crossReferences
|
|
63
|
+
),
|
|
64
|
+
sources
|
|
65
|
+
},
|
|
66
|
+
bom: []
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Builds source and model lookup indexes for bundled library consumers.
|
|
72
|
+
* @param {object[]} sources Bundled source entries.
|
|
73
|
+
* @param {object[]} crossReferences Cross-reference rows.
|
|
74
|
+
* @returns {object}
|
|
75
|
+
*/
|
|
76
|
+
static #buildIndexes(sources, crossReferences) {
|
|
77
|
+
return {
|
|
78
|
+
sourcesByFileName:
|
|
79
|
+
IntLibModelParser.#buildSourcesByFileName(sources),
|
|
80
|
+
sourcesByKind: IntLibModelParser.#buildSourcesByKind(sources),
|
|
81
|
+
modelsByComponent:
|
|
82
|
+
IntLibModelParser.#buildModelsByComponent(crossReferences),
|
|
83
|
+
symbolsByComponent: IntLibModelParser.#buildModelNamesByKind(
|
|
84
|
+
crossReferences,
|
|
85
|
+
'SCH'
|
|
86
|
+
),
|
|
87
|
+
footprintsByComponent: IntLibModelParser.#buildModelNamesByKind(
|
|
88
|
+
crossReferences,
|
|
89
|
+
'PCB'
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Indexes source entries by file name.
|
|
96
|
+
* @param {object[]} sources Source entries.
|
|
97
|
+
* @returns {Record<string, object>}
|
|
98
|
+
*/
|
|
99
|
+
static #buildSourcesByFileName(sources) {
|
|
100
|
+
const index = {}
|
|
101
|
+
for (const [sourceIndex, source] of [...sources]
|
|
102
|
+
.map((source, index) => ({ ...source, sourceIndex: index }))
|
|
103
|
+
.sort((left, right) =>
|
|
104
|
+
String(left.fileName || '').localeCompare(
|
|
105
|
+
String(right.fileName || '')
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
.entries()) {
|
|
109
|
+
index[source.fileName] = {
|
|
110
|
+
index: source.sourceIndex ?? sourceIndex,
|
|
111
|
+
path: source.path,
|
|
112
|
+
fileType: source.fileType,
|
|
113
|
+
libraryKind: source.libraryKind
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return index
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Indexes source file names by source kind.
|
|
121
|
+
* @param {object[]} sources Source entries.
|
|
122
|
+
* @returns {Record<string, string[]>}
|
|
123
|
+
*/
|
|
124
|
+
static #buildSourcesByKind(sources) {
|
|
125
|
+
const index = {}
|
|
126
|
+
for (const source of [...sources].sort((left, right) =>
|
|
127
|
+
String(left.libraryKind || '').localeCompare(
|
|
128
|
+
String(right.libraryKind || '')
|
|
129
|
+
)
|
|
130
|
+
)) {
|
|
131
|
+
const kind = source.libraryKind || 'other'
|
|
132
|
+
index[kind] ||= []
|
|
133
|
+
index[kind].push(source.fileName)
|
|
134
|
+
}
|
|
135
|
+
return index
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Indexes cross-reference model rows by component name.
|
|
140
|
+
* @param {object[]} crossReferences Cross-reference rows.
|
|
141
|
+
* @returns {Record<string, object[]>}
|
|
142
|
+
*/
|
|
143
|
+
static #buildModelsByComponent(crossReferences) {
|
|
144
|
+
const index = {}
|
|
145
|
+
for (const row of crossReferences || []) {
|
|
146
|
+
if (!row.component || !row.model) continue
|
|
147
|
+
index[row.component] ||= []
|
|
148
|
+
index[row.component].push({
|
|
149
|
+
model: row.model,
|
|
150
|
+
kind: row.kind || ''
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
return index
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Indexes cross-reference model names of one kind by component.
|
|
158
|
+
* @param {object[]} crossReferences Cross-reference rows.
|
|
159
|
+
* @param {string} kind Model kind.
|
|
160
|
+
* @returns {Record<string, string[]>}
|
|
161
|
+
*/
|
|
162
|
+
static #buildModelNamesByKind(crossReferences, kind) {
|
|
163
|
+
const index = {}
|
|
164
|
+
const lookup = kind.toUpperCase()
|
|
165
|
+
for (const row of crossReferences || []) {
|
|
166
|
+
if (
|
|
167
|
+
!row.component ||
|
|
168
|
+
!row.model ||
|
|
169
|
+
String(row.kind || '').toUpperCase() !== lookup
|
|
170
|
+
) {
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
index[row.component] ||= []
|
|
174
|
+
index[row.component].push(row.model)
|
|
175
|
+
}
|
|
176
|
+
return index
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Builds parser diagnostics for one integrated-library model.
|
|
181
|
+
* @param {string[]} streamNames
|
|
182
|
+
* @param {object[]} sources
|
|
183
|
+
* @param {object[]} crossReferences
|
|
184
|
+
* @param {Record<string, string>} parameters
|
|
185
|
+
* @param {object[]} issues Structured parser issues.
|
|
186
|
+
* @returns {{ severity: 'info' | 'warning', message: string }[]}
|
|
187
|
+
*/
|
|
188
|
+
static #buildDiagnostics(
|
|
189
|
+
streamNames,
|
|
190
|
+
sources,
|
|
191
|
+
crossReferences,
|
|
192
|
+
parameters,
|
|
193
|
+
issues
|
|
194
|
+
) {
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
severity: 'info',
|
|
198
|
+
message:
|
|
199
|
+
'Recovered ' +
|
|
200
|
+
streamNames.length +
|
|
201
|
+
' integrated-library data streams.'
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
severity: 'info',
|
|
205
|
+
message:
|
|
206
|
+
'Recovered ' +
|
|
207
|
+
sources.length +
|
|
208
|
+
' bundled library source entries.'
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
severity: 'info',
|
|
212
|
+
message:
|
|
213
|
+
'Recovered ' +
|
|
214
|
+
crossReferences.length +
|
|
215
|
+
' integrated-library cross references.'
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
severity: 'info',
|
|
219
|
+
message:
|
|
220
|
+
'Recovered ' +
|
|
221
|
+
Object.keys(parameters).length +
|
|
222
|
+
' integrated-library parameters.'
|
|
223
|
+
},
|
|
224
|
+
...(issues || []).map((issue) => ({
|
|
225
|
+
severity: issue.severity || 'warning',
|
|
226
|
+
code: issue.code,
|
|
227
|
+
message: issue.message
|
|
228
|
+
}))
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Returns a file name without its last extension.
|
|
234
|
+
* @param {string} fileName
|
|
235
|
+
* @returns {string}
|
|
236
|
+
*/
|
|
237
|
+
static #stripExtension(fileName) {
|
|
238
|
+
return String(fileName || '').replace(/\.[^.]+$/u, '')
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { unzlibSync } from 'fflate'
|
|
6
|
+
import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
|
|
7
|
+
import { PcbSidecarRecordParser } from './PcbSidecarRecordParser.mjs'
|
|
8
|
+
import { PrintableTextDecoder } from './PrintableTextDecoder.mjs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extracts read-only metadata and bundled source entries from integrated
|
|
12
|
+
* library compound documents.
|
|
13
|
+
*/
|
|
14
|
+
export class IntLibStreamExtractor {
|
|
15
|
+
/**
|
|
16
|
+
* Extracts integrated-library data directly from one OLE buffer.
|
|
17
|
+
* @param {ArrayBuffer} arrayBuffer
|
|
18
|
+
* @returns {ReturnType<typeof IntLibStreamExtractor.extractFromStreams> | null}
|
|
19
|
+
*/
|
|
20
|
+
static extractFromArrayBuffer(arrayBuffer) {
|
|
21
|
+
const compoundDocument =
|
|
22
|
+
OleCompoundDocument.fromArrayBuffer(arrayBuffer)
|
|
23
|
+
const streams = new Map()
|
|
24
|
+
|
|
25
|
+
for (const name of compoundDocument.listStreams()) {
|
|
26
|
+
streams.set(name, compoundDocument.getStream(name))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return IntLibStreamExtractor.extractFromStreams(streams)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extracts integrated-library metadata and source payloads from streams.
|
|
34
|
+
* @param {Map<string, Uint8Array>} streams
|
|
35
|
+
* @returns {{ version: string, crossReferences: object[], parameters: Record<string, string>, parameterRecords: object[], sources: object[], streamNames: string[], diagnostics: Record<string, number> }}
|
|
36
|
+
*/
|
|
37
|
+
static extractFromStreams(streams) {
|
|
38
|
+
const version = IntLibStreamExtractor.#decodeText(
|
|
39
|
+
streams.get('Version.Txt')
|
|
40
|
+
).trim()
|
|
41
|
+
const crossReferenceParse = IntLibStreamExtractor.#parseCrossReferences(
|
|
42
|
+
streams.get('LibCrossRef.Txt')
|
|
43
|
+
)
|
|
44
|
+
const crossReferences = crossReferenceParse.records
|
|
45
|
+
const parameterRecords = IntLibStreamExtractor.#parseParameterRecords(
|
|
46
|
+
streams.get('Parameters .bin')
|
|
47
|
+
)
|
|
48
|
+
const parameters =
|
|
49
|
+
IntLibStreamExtractor.#buildParameterMap(parameterRecords)
|
|
50
|
+
const sources = IntLibStreamExtractor.#extractSources(streams)
|
|
51
|
+
const streamNames = IntLibStreamExtractor.#collectUsedStreamNames(
|
|
52
|
+
streams,
|
|
53
|
+
sources
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
version,
|
|
58
|
+
crossReferences,
|
|
59
|
+
parameters,
|
|
60
|
+
parameterRecords,
|
|
61
|
+
sources,
|
|
62
|
+
streamNames,
|
|
63
|
+
diagnostics: {
|
|
64
|
+
crossReferenceCount: crossReferences.length,
|
|
65
|
+
parameterCount: Object.keys(parameters).length,
|
|
66
|
+
sourceCount: sources.length,
|
|
67
|
+
issues: crossReferenceParse.issues
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parses LibCrossRef.Txt into field records.
|
|
74
|
+
* @param {Uint8Array | undefined} bytes
|
|
75
|
+
* @returns {{ records: object[], issues: object[] }}
|
|
76
|
+
*/
|
|
77
|
+
static #parseCrossReferences(bytes) {
|
|
78
|
+
const records = []
|
|
79
|
+
const issues = []
|
|
80
|
+
|
|
81
|
+
IntLibStreamExtractor.#decodeText(bytes)
|
|
82
|
+
.split(/\r?\n/u)
|
|
83
|
+
.map((line) => line.trim())
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.forEach((line, index) => {
|
|
86
|
+
const fields = PcbSidecarRecordParser.parseRecordFields(
|
|
87
|
+
new TextEncoder().encode(line)
|
|
88
|
+
)
|
|
89
|
+
const component = fields.COMPONENT || fields.COMPONENTNAME || ''
|
|
90
|
+
const model = fields.MODEL || fields.MODELNAME || ''
|
|
91
|
+
|
|
92
|
+
if (!Object.keys(fields).length || !component || !model) {
|
|
93
|
+
issues.push(
|
|
94
|
+
IntLibStreamExtractor.#issue(
|
|
95
|
+
'intlib.crossref.malformed-row',
|
|
96
|
+
'LibCrossRef.Txt',
|
|
97
|
+
index + 1,
|
|
98
|
+
'Skipped malformed integrated-library cross-reference row.'
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
records.push({
|
|
105
|
+
component,
|
|
106
|
+
model,
|
|
107
|
+
kind: fields.KIND || fields.TYPE || '',
|
|
108
|
+
fields
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return { records, issues }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Builds a structured parser issue.
|
|
117
|
+
* @param {string} code Stable diagnostic code.
|
|
118
|
+
* @param {string} stream Source stream name.
|
|
119
|
+
* @param {number} line One-based line number.
|
|
120
|
+
* @param {string} message User-facing summary.
|
|
121
|
+
* @returns {object}
|
|
122
|
+
*/
|
|
123
|
+
static #issue(code, stream, line, message) {
|
|
124
|
+
return {
|
|
125
|
+
code,
|
|
126
|
+
severity: 'warning',
|
|
127
|
+
stream,
|
|
128
|
+
line,
|
|
129
|
+
message
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parses integrated-library parameter records.
|
|
135
|
+
* @param {Uint8Array | undefined} bytes
|
|
136
|
+
* @returns {object[]}
|
|
137
|
+
*/
|
|
138
|
+
static #parseParameterRecords(bytes) {
|
|
139
|
+
const lengthPrefixed =
|
|
140
|
+
PcbSidecarRecordParser.parseLengthPrefixedRecords(
|
|
141
|
+
bytes,
|
|
142
|
+
'Parameters .bin'
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if (lengthPrefixed.length) {
|
|
146
|
+
return lengthPrefixed.map((record) => record.fields)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return IntLibStreamExtractor.#decodeText(bytes)
|
|
150
|
+
.split(/\r?\n/u)
|
|
151
|
+
.map((line) => line.trim())
|
|
152
|
+
.filter(Boolean)
|
|
153
|
+
.map((line) =>
|
|
154
|
+
PcbSidecarRecordParser.parseRecordFields(
|
|
155
|
+
new TextEncoder().encode(line)
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
.filter((fields) => Object.keys(fields).length)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Builds a parameter lookup from parameter records.
|
|
163
|
+
* @param {Record<string, string>[]} records
|
|
164
|
+
* @returns {Record<string, string>}
|
|
165
|
+
*/
|
|
166
|
+
static #buildParameterMap(records) {
|
|
167
|
+
const parameters = {}
|
|
168
|
+
|
|
169
|
+
for (const record of records) {
|
|
170
|
+
const name = record.NAME || record.PARAMETER || ''
|
|
171
|
+
if (!name) {
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
parameters[name] = record.VALUE || ''
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return parameters
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Extracts bundled source entries from recognized source-library folders.
|
|
183
|
+
* @param {Map<string, Uint8Array>} streams
|
|
184
|
+
* @returns {object[]}
|
|
185
|
+
*/
|
|
186
|
+
static #extractSources(streams) {
|
|
187
|
+
const sources = []
|
|
188
|
+
|
|
189
|
+
for (const [path, bytes] of streams.entries()) {
|
|
190
|
+
const sourceKind = IntLibStreamExtractor.#sourceKind(path)
|
|
191
|
+
if (!sourceKind) {
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const payload = IntLibStreamExtractor.#decodePayload(bytes)
|
|
196
|
+
sources.push({
|
|
197
|
+
path,
|
|
198
|
+
fileName: IntLibStreamExtractor.#basename(path),
|
|
199
|
+
fileType: IntLibStreamExtractor.#sourceFileType(path),
|
|
200
|
+
libraryKind: sourceKind,
|
|
201
|
+
compression: payload.compression,
|
|
202
|
+
byteLength: payload.bytes.byteLength,
|
|
203
|
+
payloadBase64: IntLibStreamExtractor.#bytesToBase64(
|
|
204
|
+
payload.bytes
|
|
205
|
+
),
|
|
206
|
+
payloadText: IntLibStreamExtractor.#decodePrintablePayload(
|
|
207
|
+
payload.bytes
|
|
208
|
+
)
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return sources.sort((left, right) =>
|
|
213
|
+
left.path.localeCompare(right.path)
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Decodes a possibly wrapped zlib source payload.
|
|
219
|
+
* @param {Uint8Array} bytes
|
|
220
|
+
* @returns {{ bytes: Uint8Array, compression: string }}
|
|
221
|
+
*/
|
|
222
|
+
static #decodePayload(bytes) {
|
|
223
|
+
const normalized = PcbSidecarRecordParser.toUint8Array(bytes)
|
|
224
|
+
|
|
225
|
+
if (normalized[0] === 0x02 && normalized[1] === 0x78) {
|
|
226
|
+
return {
|
|
227
|
+
bytes: Uint8Array.from(unzlibSync(normalized.subarray(1))),
|
|
228
|
+
compression: 'zlib-wrapper'
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (normalized[0] === 0x78) {
|
|
233
|
+
return {
|
|
234
|
+
bytes: Uint8Array.from(unzlibSync(normalized)),
|
|
235
|
+
compression: 'zlib'
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
bytes: normalized,
|
|
241
|
+
compression: 'none'
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Decodes text bytes with toolkit printable-text fallbacks.
|
|
247
|
+
* @param {Uint8Array | undefined} bytes
|
|
248
|
+
* @returns {string}
|
|
249
|
+
*/
|
|
250
|
+
static #decodeText(bytes) {
|
|
251
|
+
if (!bytes) {
|
|
252
|
+
return ''
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return PrintableTextDecoder.decodeBytes(
|
|
256
|
+
PcbSidecarRecordParser.toUint8Array(bytes)
|
|
257
|
+
).replace(/^\uFEFF/u, '')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Decodes source payload text only when it looks printable.
|
|
262
|
+
* @param {Uint8Array} bytes
|
|
263
|
+
* @returns {string}
|
|
264
|
+
*/
|
|
265
|
+
static #decodePrintablePayload(bytes) {
|
|
266
|
+
const text = IntLibStreamExtractor.#decodeText(bytes)
|
|
267
|
+
.replace(/\u0000/gu, '')
|
|
268
|
+
.trim()
|
|
269
|
+
if (!text || /[\u0001-\u0008\u000b\u000c\u000e-\u001f]/u.test(text)) {
|
|
270
|
+
return ''
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return text
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Resolves the source-library kind for one stream path.
|
|
278
|
+
* @param {string} path
|
|
279
|
+
* @returns {string}
|
|
280
|
+
*/
|
|
281
|
+
static #sourceKind(path) {
|
|
282
|
+
const normalized = String(path || '').replace(/\\/gu, '/')
|
|
283
|
+
|
|
284
|
+
if (/^SchLib\//iu.test(normalized)) {
|
|
285
|
+
return 'schematic-symbols'
|
|
286
|
+
}
|
|
287
|
+
if (/^PCBLib\//iu.test(normalized)) {
|
|
288
|
+
return 'pcb-footprints'
|
|
289
|
+
}
|
|
290
|
+
if (/^PCB3DLib\//iu.test(normalized)) {
|
|
291
|
+
return 'pcb-3d-models'
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return ''
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Resolves the source file type from one stream path.
|
|
299
|
+
* @param {string} path
|
|
300
|
+
* @returns {string}
|
|
301
|
+
*/
|
|
302
|
+
static #sourceFileType(path) {
|
|
303
|
+
const extension = IntLibStreamExtractor.#basename(path).split('.').pop()
|
|
304
|
+
|
|
305
|
+
if (/^SchLib$/iu.test(extension)) return 'SchLib'
|
|
306
|
+
if (/^PcbLib$/iu.test(extension)) return 'PcbLib'
|
|
307
|
+
if (/^PCB3DLib$/iu.test(extension)) return 'PCB3DLib'
|
|
308
|
+
|
|
309
|
+
return extension || 'unknown'
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Returns the final path segment.
|
|
314
|
+
* @param {string} path
|
|
315
|
+
* @returns {string}
|
|
316
|
+
*/
|
|
317
|
+
static #basename(path) {
|
|
318
|
+
return String(path || '')
|
|
319
|
+
.replace(/\\/gu, '/')
|
|
320
|
+
.split('/')
|
|
321
|
+
.pop()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Collects streams that contributed to extraction.
|
|
326
|
+
* @param {Map<string, Uint8Array>} streams
|
|
327
|
+
* @param {object[]} sources
|
|
328
|
+
* @returns {string[]}
|
|
329
|
+
*/
|
|
330
|
+
static #collectUsedStreamNames(streams, sources) {
|
|
331
|
+
return [
|
|
332
|
+
'Version.Txt',
|
|
333
|
+
'LibCrossRef.Txt',
|
|
334
|
+
'Parameters .bin',
|
|
335
|
+
...sources.map((source) => source.path)
|
|
336
|
+
]
|
|
337
|
+
.filter((streamName) => streams.has(streamName))
|
|
338
|
+
.sort((left, right) => left.localeCompare(right))
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Encodes bytes as base64 in browser and Node runtimes.
|
|
343
|
+
* @param {Uint8Array} bytes
|
|
344
|
+
* @returns {string}
|
|
345
|
+
*/
|
|
346
|
+
static #bytesToBase64(bytes) {
|
|
347
|
+
if (typeof btoa === 'function') {
|
|
348
|
+
let binary = ''
|
|
349
|
+
const chunkSize = 0x8000
|
|
350
|
+
|
|
351
|
+
for (
|
|
352
|
+
let offset = 0;
|
|
353
|
+
offset < bytes.byteLength;
|
|
354
|
+
offset += chunkSize
|
|
355
|
+
) {
|
|
356
|
+
binary += String.fromCharCode(
|
|
357
|
+
...bytes.subarray(offset, offset + chunkSize)
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return btoa(binary)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return Buffer.from(bytes).toString('base64')
|
|
365
|
+
}
|
|
366
|
+
}
|