altium-toolkit 1.0.7 → 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/renderers.mjs +3 -0
- package/src/styles/altium-renderers.css +25 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionGeometry.mjs +350 -0
- package/src/ui/PcbInteractionIndex.mjs +593 -0
- package/src/ui/PcbInteractionItemRegistry.mjs +66 -0
- package/src/ui/PcbInteractionLayerModel.mjs +99 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +74 -9
- package/src/ui/PcbScene3dBuilder.mjs +169 -7
- 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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
6
|
+
import { ProjectOutJobDigestBuilder } from './ProjectOutJobDigestBuilder.mjs'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Parses Altium PrjPcb INI-style project files into a normalized project
|
|
@@ -35,7 +36,15 @@ export class PrjPcbModelParser {
|
|
|
35
36
|
)
|
|
36
37
|
const currentVariant =
|
|
37
38
|
PrjPcbModelParser.#stringField(design, 'CurrentVariant') || ''
|
|
38
|
-
|
|
39
|
+
let documents = PrjPcbModelParser.#extractDocuments(sections)
|
|
40
|
+
const classGeneration = PrjPcbModelParser.#extractClassGeneration(
|
|
41
|
+
sections,
|
|
42
|
+
documents
|
|
43
|
+
)
|
|
44
|
+
documents = PrjPcbModelParser.#attachDocumentClassGeneration(
|
|
45
|
+
documents,
|
|
46
|
+
classGeneration
|
|
47
|
+
)
|
|
39
48
|
const documentGroups = PrjPcbModelParser.#buildDocumentGroups(documents)
|
|
40
49
|
const parameters = PrjPcbModelParser.#extractParameters(sections)
|
|
41
50
|
const variants = PrjPcbModelParser.#extractVariants(
|
|
@@ -45,6 +54,10 @@ export class PrjPcbModelParser {
|
|
|
45
54
|
const configurations =
|
|
46
55
|
PrjPcbModelParser.#extractConfigurations(sections)
|
|
47
56
|
const outputGroups = PrjPcbModelParser.#extractOutputGroups(sections)
|
|
57
|
+
const outJobDigest = ProjectOutJobDigestBuilder.build({
|
|
58
|
+
documents,
|
|
59
|
+
outputGroups
|
|
60
|
+
})
|
|
48
61
|
const summary = PrjPcbModelParser.#buildSummary(
|
|
49
62
|
fileName,
|
|
50
63
|
documents,
|
|
@@ -73,6 +86,8 @@ export class PrjPcbModelParser {
|
|
|
73
86
|
variants,
|
|
74
87
|
configurations,
|
|
75
88
|
outputGroups,
|
|
89
|
+
outJobDigest,
|
|
90
|
+
classGeneration,
|
|
76
91
|
sections: PrjPcbModelParser.#serializeSections(sections)
|
|
77
92
|
},
|
|
78
93
|
bom: []
|
|
@@ -251,6 +266,188 @@ export class PrjPcbModelParser {
|
|
|
251
266
|
return { list, map }
|
|
252
267
|
}
|
|
253
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Extracts project and per-document class-generation policy sections.
|
|
271
|
+
* @param {{ name: string, entries: { key: string, value: string }[] }[]} sections
|
|
272
|
+
* @param {object[]} documents
|
|
273
|
+
* @returns {{ section: string, policies: object, options: Record<string, string | string[]>, documents: object[], byDocumentPath: Record<string, object>, byDocumentIndex: Record<string, object> }}
|
|
274
|
+
*/
|
|
275
|
+
static #extractClassGeneration(sections, documents) {
|
|
276
|
+
const projectSection =
|
|
277
|
+
PrjPcbModelParser.#findSection(sections, 'PrjClassGen') || null
|
|
278
|
+
const projectFields = PrjPcbModelParser.#sectionFields(projectSection)
|
|
279
|
+
const documentPolicies = PrjPcbModelParser.#numberedSections(
|
|
280
|
+
sections,
|
|
281
|
+
'DocumentClassGen'
|
|
282
|
+
).map(({ section, number }) => {
|
|
283
|
+
const fields = PrjPcbModelParser.#sectionFields(section)
|
|
284
|
+
const documentPath =
|
|
285
|
+
PrjPcbModelParser.#stringField(fields, 'DocumentPath') || ''
|
|
286
|
+
const normalizedPath =
|
|
287
|
+
PrjPcbModelParser.#normalizeDocumentPath(documentPath)
|
|
288
|
+
const documentIndex =
|
|
289
|
+
PrjPcbModelParser.#integerField(fields, 'DocumentIndex') ||
|
|
290
|
+
PrjPcbModelParser.#documentIndexForPath(
|
|
291
|
+
documents,
|
|
292
|
+
normalizedPath
|
|
293
|
+
) ||
|
|
294
|
+
number
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
index: number,
|
|
298
|
+
section: section.name,
|
|
299
|
+
documentIndex,
|
|
300
|
+
documentPath,
|
|
301
|
+
normalizedPath,
|
|
302
|
+
policies: PrjPcbModelParser.#classGenerationPolicies(fields),
|
|
303
|
+
options: fields
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
const byDocumentPath = {}
|
|
307
|
+
const byDocumentIndex = {}
|
|
308
|
+
|
|
309
|
+
for (const policy of documentPolicies) {
|
|
310
|
+
if (policy.normalizedPath) {
|
|
311
|
+
byDocumentPath[policy.normalizedPath] = policy
|
|
312
|
+
}
|
|
313
|
+
byDocumentIndex[String(policy.documentIndex)] = policy
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
section: projectSection?.name || '',
|
|
318
|
+
policies: PrjPcbModelParser.#classGenerationPolicies(projectFields),
|
|
319
|
+
options: projectFields,
|
|
320
|
+
documents: documentPolicies,
|
|
321
|
+
byDocumentPath,
|
|
322
|
+
byDocumentIndex
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Attaches per-document class-generation options to document rows.
|
|
328
|
+
* @param {object[]} documents Project documents.
|
|
329
|
+
* @param {object} classGeneration Class-generation model.
|
|
330
|
+
* @returns {object[]}
|
|
331
|
+
*/
|
|
332
|
+
static #attachDocumentClassGeneration(documents, classGeneration) {
|
|
333
|
+
return (documents || []).map((document) => {
|
|
334
|
+
const inlinePolicies =
|
|
335
|
+
PrjPcbModelParser.#classGenerationPoliciesFromDocument(
|
|
336
|
+
document.options || {}
|
|
337
|
+
)
|
|
338
|
+
const sectionPolicy =
|
|
339
|
+
classGeneration.byDocumentPath?.[document.normalizedPath] ||
|
|
340
|
+
classGeneration.byDocumentIndex?.[String(document.index)] ||
|
|
341
|
+
null
|
|
342
|
+
const policies = {
|
|
343
|
+
...inlinePolicies,
|
|
344
|
+
...(sectionPolicy?.policies || {})
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return Object.keys(policies).length
|
|
348
|
+
? {
|
|
349
|
+
...document,
|
|
350
|
+
classGeneration: {
|
|
351
|
+
documentIndex: document.index,
|
|
352
|
+
policies,
|
|
353
|
+
section: sectionPolicy?.section || '',
|
|
354
|
+
options:
|
|
355
|
+
sectionPolicy?.options ||
|
|
356
|
+
PrjPcbModelParser.#classGenerationDocumentOptions(
|
|
357
|
+
document.options || {}
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
: document
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Extracts class-generation policies from inline document options.
|
|
367
|
+
* @param {Record<string, string | string[]>} fields Document option fields.
|
|
368
|
+
* @returns {Record<string, boolean>}
|
|
369
|
+
*/
|
|
370
|
+
static #classGenerationPoliciesFromDocument(fields) {
|
|
371
|
+
const options =
|
|
372
|
+
PrjPcbModelParser.#classGenerationDocumentOptions(fields)
|
|
373
|
+
return PrjPcbModelParser.#classGenerationPolicies(options)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Selects only inline class-generation options from one document section.
|
|
378
|
+
* @param {Record<string, string | string[]>} fields Document option fields.
|
|
379
|
+
* @returns {Record<string, string | string[]>}
|
|
380
|
+
*/
|
|
381
|
+
static #classGenerationDocumentOptions(fields) {
|
|
382
|
+
const options = {}
|
|
383
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
384
|
+
if (/^ClassGen/i.test(key)) {
|
|
385
|
+
options[key] = value
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return options
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Converts class-generation option fields into stable camelCase policy
|
|
393
|
+
* names.
|
|
394
|
+
* @param {Record<string, string | string[]>} fields Class-generation fields.
|
|
395
|
+
* @returns {Record<string, boolean>}
|
|
396
|
+
*/
|
|
397
|
+
static #classGenerationPolicies(fields) {
|
|
398
|
+
const policies = {}
|
|
399
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
400
|
+
const policyName = PrjPcbModelParser.#classGenerationPolicyName(key)
|
|
401
|
+
if (!policyName) continue
|
|
402
|
+
policies[policyName] = PrjPcbModelParser.#booleanValue(value)
|
|
403
|
+
}
|
|
404
|
+
return policies
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Resolves a public class-generation policy name.
|
|
409
|
+
* @param {string} key Raw option key.
|
|
410
|
+
* @returns {string}
|
|
411
|
+
*/
|
|
412
|
+
static #classGenerationPolicyName(key) {
|
|
413
|
+
const normalized = String(key || '')
|
|
414
|
+
.replace(/^ClassGen/i, '')
|
|
415
|
+
.replace(/[^a-z0-9]/gi, '')
|
|
416
|
+
.toLowerCase()
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
{
|
|
420
|
+
generateclasses: 'generateClasses',
|
|
421
|
+
generatenetclasses: 'generateNetClasses',
|
|
422
|
+
generatecomponentclasses: 'generateComponentClasses',
|
|
423
|
+
generatedifferentialpairclasses:
|
|
424
|
+
'generateDifferentialPairClasses',
|
|
425
|
+
generaterooms: 'generateRooms',
|
|
426
|
+
generatesheetclasses: 'generateSheetClasses',
|
|
427
|
+
generatepolygonclasses: 'generatePolygonClasses',
|
|
428
|
+
transfernetclasses: 'transferNetClasses',
|
|
429
|
+
transfercomponentclasses: 'transferComponentClasses',
|
|
430
|
+
transferdifferentialpairclasses:
|
|
431
|
+
'transferDifferentialPairClasses',
|
|
432
|
+
transferroomdirectives: 'transferRoomDirectives'
|
|
433
|
+
}[normalized] || ''
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Finds a document index for a normalized path.
|
|
439
|
+
* @param {object[]} documents Project document rows.
|
|
440
|
+
* @param {string} normalizedPath Normalized document path.
|
|
441
|
+
* @returns {number}
|
|
442
|
+
*/
|
|
443
|
+
static #documentIndexForPath(documents, normalizedPath) {
|
|
444
|
+
if (!normalizedPath) return 0
|
|
445
|
+
const match = (documents || []).find(
|
|
446
|
+
(document) => document.normalizedPath === normalizedPath
|
|
447
|
+
)
|
|
448
|
+
return Number(match?.index) || 0
|
|
449
|
+
}
|
|
450
|
+
|
|
254
451
|
/**
|
|
255
452
|
* Extracts project variants and their variation rows.
|
|
256
453
|
* @param {{ name: string, entries: { key: string, value: string }[] }[]} sections
|
|
@@ -316,6 +513,8 @@ export class PrjPcbModelParser {
|
|
|
316
513
|
PrjPcbModelParser.#buildParameterOverrideMap(
|
|
317
514
|
paramVariations
|
|
318
515
|
),
|
|
516
|
+
alternateFitted:
|
|
517
|
+
PrjPcbModelParser.#buildAlternateFittedMap(variations),
|
|
319
518
|
dnp: variations
|
|
320
519
|
.filter((variation) => variation.Kind === '1')
|
|
321
520
|
.map((variation) => variation.Designator || '')
|
|
@@ -448,6 +647,41 @@ export class PrjPcbModelParser {
|
|
|
448
647
|
return overrides
|
|
449
648
|
}
|
|
450
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Groups alternate fitted component rows by designator.
|
|
652
|
+
* @param {Record<string, string>[]} rows Variant variation rows.
|
|
653
|
+
* @returns {Record<string, object>}
|
|
654
|
+
*/
|
|
655
|
+
static #buildAlternateFittedMap(rows) {
|
|
656
|
+
const alternates = {}
|
|
657
|
+
for (const row of rows || []) {
|
|
658
|
+
const designator = String(row.Designator || '').trim()
|
|
659
|
+
if (!designator) continue
|
|
660
|
+
const alternatePart = String(row.AlternatePart || '').trim()
|
|
661
|
+
const isAlternate =
|
|
662
|
+
String(row.Kind || '').trim() === '2' || Boolean(alternatePart)
|
|
663
|
+
if (!isAlternate) continue
|
|
664
|
+
|
|
665
|
+
alternates[designator] = {
|
|
666
|
+
designator,
|
|
667
|
+
alternatePart,
|
|
668
|
+
libReference:
|
|
669
|
+
row.AlternateLibReference ||
|
|
670
|
+
row.AlternateLibraryRef ||
|
|
671
|
+
row.LibraryRef ||
|
|
672
|
+
'',
|
|
673
|
+
footprint:
|
|
674
|
+
row.AlternateFootprint ||
|
|
675
|
+
row.Footprint ||
|
|
676
|
+
row.Pattern ||
|
|
677
|
+
'',
|
|
678
|
+
comment: row.AlternateComment || row.Comment || '',
|
|
679
|
+
description: row.AlternateDescription || row.Description || ''
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return alternates
|
|
683
|
+
}
|
|
684
|
+
|
|
451
685
|
/**
|
|
452
686
|
* Extracts numbered output rows from one OutputGroup section.
|
|
453
687
|
* @param {Record<string, string | string[]>} fields
|
|
@@ -662,10 +896,28 @@ export class PrjPcbModelParser {
|
|
|
662
896
|
* @returns {boolean}
|
|
663
897
|
*/
|
|
664
898
|
static #booleanField(fields, key) {
|
|
665
|
-
|
|
666
|
-
PrjPcbModelParser.#stringField(fields, key)
|
|
667
|
-
)
|
|
668
|
-
|
|
899
|
+
return PrjPcbModelParser.#booleanValue(
|
|
900
|
+
PrjPcbModelParser.#stringField(fields, key)
|
|
901
|
+
)
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Parses one boolean-ish value.
|
|
906
|
+
* @param {unknown} value Raw field value.
|
|
907
|
+
* @returns {boolean}
|
|
908
|
+
*/
|
|
909
|
+
static #booleanValue(value) {
|
|
910
|
+
const raw = Array.isArray(value)
|
|
911
|
+
? String(value[0] || '')
|
|
912
|
+
: String(value || '')
|
|
913
|
+
const normalized = raw.toLowerCase()
|
|
914
|
+
|
|
915
|
+
return (
|
|
916
|
+
normalized === '1' ||
|
|
917
|
+
normalized === 't' ||
|
|
918
|
+
normalized === 'true' ||
|
|
919
|
+
normalized === 'yes'
|
|
920
|
+
)
|
|
669
921
|
}
|
|
670
922
|
|
|
671
923
|
/**
|
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
* Parses annotation mapping files used to relate logical and compiled
|
|
9
|
+
* designators.
|
|
10
|
+
*/
|
|
11
|
+
export class ProjectAnnotationParser {
|
|
12
|
+
/**
|
|
13
|
+
* Parses one annotation ArrayBuffer.
|
|
14
|
+
* @param {string} fileName Annotation file name.
|
|
15
|
+
* @param {ArrayBuffer} arrayBuffer Annotation bytes.
|
|
16
|
+
* @returns {object}
|
|
17
|
+
*/
|
|
18
|
+
static parse(fileName, arrayBuffer) {
|
|
19
|
+
return ProjectAnnotationParser.parseText(
|
|
20
|
+
fileName,
|
|
21
|
+
new TextDecoder('windows-1252')
|
|
22
|
+
.decode(arrayBuffer || new ArrayBuffer(0))
|
|
23
|
+
.replace(/^\uFEFF/u, '')
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses one annotation text payload.
|
|
29
|
+
* @param {string} fileName Annotation file name.
|
|
30
|
+
* @param {string} text Annotation text.
|
|
31
|
+
* @returns {object}
|
|
32
|
+
*/
|
|
33
|
+
static parseText(fileName, text) {
|
|
34
|
+
const sections = ProjectAnnotationParser.#parseIniSections(text)
|
|
35
|
+
const mappings = ProjectAnnotationParser.#extractMappings(
|
|
36
|
+
fileName,
|
|
37
|
+
sections
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return NormalizedModelSchema.attach({
|
|
41
|
+
kind: 'project-annotation',
|
|
42
|
+
fileType: 'Annotation',
|
|
43
|
+
fileName,
|
|
44
|
+
summary: {
|
|
45
|
+
title: fileName,
|
|
46
|
+
mappingCount: mappings.length
|
|
47
|
+
},
|
|
48
|
+
diagnostics: [
|
|
49
|
+
{
|
|
50
|
+
severity: 'info',
|
|
51
|
+
message:
|
|
52
|
+
'Recovered ' +
|
|
53
|
+
mappings.length +
|
|
54
|
+
' annotation designator mappings.'
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
annotations: {
|
|
58
|
+
mappings,
|
|
59
|
+
bySourceDesignator: ProjectAnnotationParser.#indexBy(
|
|
60
|
+
mappings,
|
|
61
|
+
'sourceDesignator'
|
|
62
|
+
),
|
|
63
|
+
byCompiledDesignator: ProjectAnnotationParser.#indexBy(
|
|
64
|
+
mappings,
|
|
65
|
+
'compiledDesignator'
|
|
66
|
+
)
|
|
67
|
+
},
|
|
68
|
+
bom: []
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parses INI-like sections.
|
|
74
|
+
* @param {string} text Text payload.
|
|
75
|
+
* @returns {{ name: string, entries: { key: string, value: string }[] }[]}
|
|
76
|
+
*/
|
|
77
|
+
static #parseIniSections(text) {
|
|
78
|
+
const sections = []
|
|
79
|
+
let current = null
|
|
80
|
+
|
|
81
|
+
for (const rawLine of String(text || '')
|
|
82
|
+
.replace(/\r\n?/gu, '\n')
|
|
83
|
+
.split('\n')) {
|
|
84
|
+
const trimmed = rawLine.trim()
|
|
85
|
+
if (
|
|
86
|
+
!trimmed ||
|
|
87
|
+
trimmed.startsWith(';') ||
|
|
88
|
+
trimmed.startsWith('#')
|
|
89
|
+
) {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/u)
|
|
94
|
+
if (sectionMatch) {
|
|
95
|
+
current = {
|
|
96
|
+
name: sectionMatch[1].trim(),
|
|
97
|
+
entries: []
|
|
98
|
+
}
|
|
99
|
+
sections.push(current)
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!current) continue
|
|
104
|
+
const separatorIndex = rawLine.indexOf('=')
|
|
105
|
+
if (separatorIndex === -1) continue
|
|
106
|
+
current.entries.push({
|
|
107
|
+
key: rawLine.slice(0, separatorIndex).trim(),
|
|
108
|
+
value: rawLine.slice(separatorIndex + 1).trim()
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return sections
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extracts designator mapping rows.
|
|
117
|
+
* @param {string} fileName Annotation file name.
|
|
118
|
+
* @param {{ name: string, entries: { key: string, value: string }[] }[]} sections Parsed sections.
|
|
119
|
+
* @returns {object[]}
|
|
120
|
+
*/
|
|
121
|
+
static #extractMappings(fileName, sections) {
|
|
122
|
+
return (sections || [])
|
|
123
|
+
.map((section, sectionIndex) => {
|
|
124
|
+
const match = String(section.name || '').match(
|
|
125
|
+
/^Annotation(\d*)$/iu
|
|
126
|
+
)
|
|
127
|
+
if (!match) return null
|
|
128
|
+
const fields = ProjectAnnotationParser.#sectionFields(section)
|
|
129
|
+
const sourceDesignator =
|
|
130
|
+
ProjectAnnotationParser.#field(fields, [
|
|
131
|
+
'SourceDesignator',
|
|
132
|
+
'LogicalDesignator',
|
|
133
|
+
'OriginalDesignator'
|
|
134
|
+
]) || ''
|
|
135
|
+
const compiledDesignator =
|
|
136
|
+
ProjectAnnotationParser.#field(fields, [
|
|
137
|
+
'CompiledDesignator',
|
|
138
|
+
'PhysicalDesignator',
|
|
139
|
+
'NewDesignator'
|
|
140
|
+
]) || ''
|
|
141
|
+
if (!sourceDesignator || !compiledDesignator) return null
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
index: Number.parseInt(match[1] || '', 10) || sectionIndex,
|
|
145
|
+
sourceDesignator,
|
|
146
|
+
compiledDesignator,
|
|
147
|
+
uniqueId:
|
|
148
|
+
ProjectAnnotationParser.#field(fields, [
|
|
149
|
+
'UniqueId',
|
|
150
|
+
'UniqueID'
|
|
151
|
+
]) || '',
|
|
152
|
+
sourceFileName: fileName,
|
|
153
|
+
options: fields
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.sort((left, right) => left.index - right.index)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Builds a field map from one section.
|
|
162
|
+
* @param {{ entries: { key: string, value: string }[] }} section Section row.
|
|
163
|
+
* @returns {Record<string, string>}
|
|
164
|
+
*/
|
|
165
|
+
static #sectionFields(section) {
|
|
166
|
+
const fields = {}
|
|
167
|
+
for (const entry of section?.entries || []) {
|
|
168
|
+
fields[entry.key] = entry.value
|
|
169
|
+
}
|
|
170
|
+
return fields
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Reads the first matching field.
|
|
175
|
+
* @param {Record<string, string>} fields Field map.
|
|
176
|
+
* @param {string[]} names Candidate names.
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
static #field(fields, names) {
|
|
180
|
+
for (const name of names) {
|
|
181
|
+
const lower = name.toLowerCase()
|
|
182
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
183
|
+
if (key.toLowerCase() === lower) {
|
|
184
|
+
return String(value || '').trim()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return ''
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Builds a compact index by field.
|
|
193
|
+
* @param {object[]} rows Rows.
|
|
194
|
+
* @param {string} key Key.
|
|
195
|
+
* @returns {Record<string, object>}
|
|
196
|
+
*/
|
|
197
|
+
static #indexBy(rows, key) {
|
|
198
|
+
const index = {}
|
|
199
|
+
for (const row of rows || []) {
|
|
200
|
+
const value = String(row?.[key] || '').trim()
|
|
201
|
+
if (value) index[value] = row
|
|
202
|
+
}
|
|
203
|
+
return index
|
|
204
|
+
}
|
|
205
|
+
}
|