altium-toolkit 1.0.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/api.md +6 -2
- package/docs/model-format.md +29 -4
- package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +80 -0
- package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
- package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +166 -0
- package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
- package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +856 -7
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
- package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
- package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +27 -0
- package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
- package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
- package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
- package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
- package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
- package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +12 -2
- package/src/core/altium/CiArtifactBundleBuilder.mjs +213 -0
- package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
- package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +928 -0
- package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
- package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
- package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
- package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
- package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
- package/src/core/altium/PcbLibModelParser.mjs +2 -0
- package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
- package/src/core/altium/PcbModelParser.mjs +211 -22
- package/src/core/altium/PcbPadStackParser.mjs +171 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +11 -1
- package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
- package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +71 -2
- package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
- package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
- package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
- package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
- package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +9 -0
- package/src/core/altium/PrintableTextDecoder.mjs +70 -6
- package/src/core/altium/PrjPcbModelParser.mjs +69 -2
- package/src/core/altium/PrjScrModelParser.mjs +386 -0
- package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +76 -2
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +5 -1
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +435 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +300 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/parser.mjs +21 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
- package/src/ui/PcbScene3dBuilder.mjs +26 -4
- package/src/ui/PcbSvgRenderer.mjs +65 -0
- package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
- package/src/ui/SchematicSvgRenderer.mjs +48 -2
|
@@ -14,6 +14,15 @@ export class PcbStatisticsBuilder {
|
|
|
14
14
|
static build(pcb) {
|
|
15
15
|
return {
|
|
16
16
|
schema: 'altium-toolkit.pcb.statistics.a1',
|
|
17
|
+
units: {
|
|
18
|
+
coordinate: 'mil',
|
|
19
|
+
length: 'mil',
|
|
20
|
+
board: 'mil',
|
|
21
|
+
drill: 'mil',
|
|
22
|
+
thickness: 'mil',
|
|
23
|
+
copperWeight: 'oz',
|
|
24
|
+
angle: 'deg'
|
|
25
|
+
},
|
|
17
26
|
board: PcbStatisticsBuilder.#boardStats(pcb?.boardOutline || {}),
|
|
18
27
|
drills: PcbStatisticsBuilder.#drillStats(
|
|
19
28
|
pcb?.pads || [],
|
|
@@ -12,6 +12,36 @@ export class PrintableTextDecoder {
|
|
|
12
12
|
0x9c, 0x9e, 0x9f
|
|
13
13
|
])
|
|
14
14
|
|
|
15
|
+
static #WINDOWS_1252_CONTROL_CODE_POINTS = new Map([
|
|
16
|
+
[0x80, 0x20ac],
|
|
17
|
+
[0x82, 0x201a],
|
|
18
|
+
[0x83, 0x0192],
|
|
19
|
+
[0x84, 0x201e],
|
|
20
|
+
[0x85, 0x2026],
|
|
21
|
+
[0x86, 0x2020],
|
|
22
|
+
[0x87, 0x2021],
|
|
23
|
+
[0x88, 0x02c6],
|
|
24
|
+
[0x89, 0x2030],
|
|
25
|
+
[0x8a, 0x0160],
|
|
26
|
+
[0x8b, 0x2039],
|
|
27
|
+
[0x8c, 0x0152],
|
|
28
|
+
[0x8e, 0x017d],
|
|
29
|
+
[0x91, 0x2018],
|
|
30
|
+
[0x92, 0x2019],
|
|
31
|
+
[0x93, 0x201c],
|
|
32
|
+
[0x94, 0x201d],
|
|
33
|
+
[0x95, 0x2022],
|
|
34
|
+
[0x96, 0x2013],
|
|
35
|
+
[0x97, 0x2014],
|
|
36
|
+
[0x98, 0x02dc],
|
|
37
|
+
[0x99, 0x2122],
|
|
38
|
+
[0x9a, 0x0161],
|
|
39
|
+
[0x9b, 0x203a],
|
|
40
|
+
[0x9c, 0x0153],
|
|
41
|
+
[0x9e, 0x017e],
|
|
42
|
+
[0x9f, 0x0178]
|
|
43
|
+
])
|
|
44
|
+
|
|
15
45
|
/**
|
|
16
46
|
* Returns printable ASCII-like runs from a binary buffer.
|
|
17
47
|
* @param {ArrayBuffer} arrayBuffer
|
|
@@ -93,7 +123,7 @@ export class PrintableTextDecoder {
|
|
|
93
123
|
preferredEncoding === 'cp1252'
|
|
94
124
|
) {
|
|
95
125
|
return (
|
|
96
|
-
PrintableTextDecoder.#
|
|
126
|
+
PrintableTextDecoder.#tryDecodeWindows1252(bytes) ||
|
|
97
127
|
new TextDecoder('utf-8').decode(bytes)
|
|
98
128
|
)
|
|
99
129
|
}
|
|
@@ -104,10 +134,8 @@ export class PrintableTextDecoder {
|
|
|
104
134
|
}
|
|
105
135
|
|
|
106
136
|
if (PrintableTextDecoder.#hasWindows1252PreferredBytes(bytes)) {
|
|
107
|
-
const windows1252 =
|
|
108
|
-
bytes
|
|
109
|
-
'windows-1252'
|
|
110
|
-
)
|
|
137
|
+
const windows1252 =
|
|
138
|
+
PrintableTextDecoder.#tryDecodeWindows1252(bytes)
|
|
111
139
|
if (windows1252 !== null) {
|
|
112
140
|
return windows1252
|
|
113
141
|
}
|
|
@@ -115,7 +143,7 @@ export class PrintableTextDecoder {
|
|
|
115
143
|
|
|
116
144
|
return (
|
|
117
145
|
PrintableTextDecoder.#tryDecode(bytes, 'gb18030') ||
|
|
118
|
-
PrintableTextDecoder.#
|
|
146
|
+
PrintableTextDecoder.#tryDecodeWindows1252(bytes) ||
|
|
119
147
|
new TextDecoder('utf-8').decode(bytes)
|
|
120
148
|
)
|
|
121
149
|
}
|
|
@@ -203,4 +231,40 @@ export class PrintableTextDecoder {
|
|
|
203
231
|
return null
|
|
204
232
|
}
|
|
205
233
|
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Tries a Windows-1252 decode and normalizes runtimes that expose C1 bytes
|
|
237
|
+
* as control characters instead of punctuation.
|
|
238
|
+
* @param {Uint8Array} bytes
|
|
239
|
+
* @returns {string | null}
|
|
240
|
+
*/
|
|
241
|
+
static #tryDecodeWindows1252(bytes) {
|
|
242
|
+
const decoded = PrintableTextDecoder.#tryDecode(bytes, 'windows-1252')
|
|
243
|
+
if (decoded === null) return null
|
|
244
|
+
|
|
245
|
+
return PrintableTextDecoder.#normalizeWindows1252Controls(decoded)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Maps Windows-1252 control-range punctuation to stable Unicode code
|
|
250
|
+
* points across Node/ICU builds.
|
|
251
|
+
* @param {string} text
|
|
252
|
+
* @returns {string}
|
|
253
|
+
*/
|
|
254
|
+
static #normalizeWindows1252Controls(text) {
|
|
255
|
+
let normalized = ''
|
|
256
|
+
|
|
257
|
+
for (const character of text) {
|
|
258
|
+
const codePoint = character.codePointAt(0)
|
|
259
|
+
const windows1252CodePoint =
|
|
260
|
+
PrintableTextDecoder.#WINDOWS_1252_CONTROL_CODE_POINTS.get(
|
|
261
|
+
codePoint
|
|
262
|
+
)
|
|
263
|
+
normalized += String.fromCodePoint(
|
|
264
|
+
windows1252CodePoint || codePoint
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return normalized
|
|
269
|
+
}
|
|
206
270
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
6
6
|
import { ProjectOutJobDigestBuilder } from './ProjectOutJobDigestBuilder.mjs'
|
|
7
|
+
import { ProjectDocumentGraphBuilder } from './ProjectDocumentGraphBuilder.mjs'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Parses Altium PrjPcb INI-style project files into a normalized project
|
|
@@ -58,6 +59,11 @@ export class PrjPcbModelParser {
|
|
|
58
59
|
documents,
|
|
59
60
|
outputGroups
|
|
60
61
|
})
|
|
62
|
+
const documentGraph = ProjectDocumentGraphBuilder.build({
|
|
63
|
+
documents,
|
|
64
|
+
documentGroups,
|
|
65
|
+
outputGroups
|
|
66
|
+
})
|
|
61
67
|
const summary = PrjPcbModelParser.#buildSummary(
|
|
62
68
|
fileName,
|
|
63
69
|
documents,
|
|
@@ -87,6 +93,7 @@ export class PrjPcbModelParser {
|
|
|
87
93
|
configurations,
|
|
88
94
|
outputGroups,
|
|
89
95
|
outJobDigest,
|
|
96
|
+
documentGraph,
|
|
90
97
|
classGeneration,
|
|
91
98
|
sections: PrjPcbModelParser.#serializeSections(sections)
|
|
92
99
|
},
|
|
@@ -229,6 +236,9 @@ export class PrjPcbModelParser {
|
|
|
229
236
|
integratedLibraries: documents.filter(
|
|
230
237
|
(document) => document.kind === 'integrated-library'
|
|
231
238
|
),
|
|
239
|
+
harnessFiles: documents.filter(
|
|
240
|
+
(document) => document.kind === 'harness'
|
|
241
|
+
),
|
|
232
242
|
outJobs: documents.filter(
|
|
233
243
|
(document) => document.kind === 'output-job'
|
|
234
244
|
),
|
|
@@ -699,7 +709,18 @@ export class PrjPcbModelParser {
|
|
|
699
709
|
PrjPcbModelParser.#stringField(fields, 'OutputType' + index) ||
|
|
700
710
|
''
|
|
701
711
|
if (!type) continue
|
|
702
|
-
|
|
712
|
+
const targetPath =
|
|
713
|
+
PrjPcbModelParser.#stringField(
|
|
714
|
+
fields,
|
|
715
|
+
'OutputTargetPath' + index
|
|
716
|
+
) ||
|
|
717
|
+
PrjPcbModelParser.#stringField(fields, 'OutputPath' + index) ||
|
|
718
|
+
''
|
|
719
|
+
const configRows = PrjPcbModelParser.#extractOutputConfigRows(
|
|
720
|
+
fields,
|
|
721
|
+
index
|
|
722
|
+
)
|
|
723
|
+
const row = {
|
|
703
724
|
index,
|
|
704
725
|
type,
|
|
705
726
|
name:
|
|
@@ -721,7 +742,50 @@ export class PrjPcbModelParser {
|
|
|
721
742
|
fields,
|
|
722
743
|
'OutputDefault' + index
|
|
723
744
|
)
|
|
724
|
-
}
|
|
745
|
+
}
|
|
746
|
+
if (targetPath) row.targetPath = targetPath
|
|
747
|
+
if (configRows.length) row.configRows = configRows
|
|
748
|
+
rows.push(row)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return rows
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Extracts output configuration rows associated with one output index.
|
|
756
|
+
* @param {Record<string, string | string[]>} fields Output group fields.
|
|
757
|
+
* @param {number} outputIndex Output row index.
|
|
758
|
+
* @returns {{ key: string, record: string, fields: Record<string, string> }[]}
|
|
759
|
+
*/
|
|
760
|
+
static #extractOutputConfigRows(fields, outputIndex) {
|
|
761
|
+
const rows = []
|
|
762
|
+
const patterns = [
|
|
763
|
+
new RegExp('^Configuration' + outputIndex + '_Item\\d+$', 'i'),
|
|
764
|
+
new RegExp(
|
|
765
|
+
'^OutputConfiguration(?:Parameter)?' +
|
|
766
|
+
outputIndex +
|
|
767
|
+
'(?:_Item\\d+)?$',
|
|
768
|
+
'i'
|
|
769
|
+
)
|
|
770
|
+
]
|
|
771
|
+
|
|
772
|
+
for (const key of Object.keys(fields || {}).sort((left, right) =>
|
|
773
|
+
left.localeCompare(right, undefined, { numeric: true })
|
|
774
|
+
)) {
|
|
775
|
+
if (!patterns.some((pattern) => pattern.test(key))) continue
|
|
776
|
+
const values = Array.isArray(fields[key])
|
|
777
|
+
? fields[key]
|
|
778
|
+
: [fields[key]]
|
|
779
|
+
for (const value of values) {
|
|
780
|
+
const parsed = PrjPcbModelParser.#parsePipeFields(value)
|
|
781
|
+
const record = parsed.Record || ''
|
|
782
|
+
delete parsed.Record
|
|
783
|
+
rows.push({
|
|
784
|
+
key,
|
|
785
|
+
record,
|
|
786
|
+
fields: parsed
|
|
787
|
+
})
|
|
788
|
+
}
|
|
725
789
|
}
|
|
726
790
|
|
|
727
791
|
return rows
|
|
@@ -986,6 +1050,9 @@ export class PrjPcbModelParser {
|
|
|
986
1050
|
return 'pcb-library'
|
|
987
1051
|
case '.intlib':
|
|
988
1052
|
return 'integrated-library'
|
|
1053
|
+
case '.harness':
|
|
1054
|
+
case '.harnessdoc':
|
|
1055
|
+
return 'harness'
|
|
989
1056
|
case '.outjob':
|
|
990
1057
|
return 'output-job'
|
|
991
1058
|
default:
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
6
|
+
import { ParserUtils } from './ParserUtils.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parses Altium script-project files into a read-only project-script digest.
|
|
10
|
+
*/
|
|
11
|
+
export class PrjScrModelParser {
|
|
12
|
+
/**
|
|
13
|
+
* Parses one script-project ArrayBuffer.
|
|
14
|
+
* @param {string} fileName Source file name.
|
|
15
|
+
* @param {ArrayBuffer} arrayBuffer Source bytes.
|
|
16
|
+
* @param {{ existingPaths?: string[] }} options Parser options.
|
|
17
|
+
* @returns {object}
|
|
18
|
+
*/
|
|
19
|
+
static parse(fileName, arrayBuffer, options = {}) {
|
|
20
|
+
return PrjScrModelParser.parseText(
|
|
21
|
+
fileName,
|
|
22
|
+
PrjScrModelParser.#decodeText(arrayBuffer),
|
|
23
|
+
options
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses one script-project text payload.
|
|
29
|
+
* @param {string} fileName Source file name.
|
|
30
|
+
* @param {string} text Source text.
|
|
31
|
+
* @param {{ existingPaths?: string[] }} options Parser options.
|
|
32
|
+
* @returns {object}
|
|
33
|
+
*/
|
|
34
|
+
static parseText(fileName, text, options = {}) {
|
|
35
|
+
const sections = PrjScrModelParser.#parseIniSections(text)
|
|
36
|
+
const design = PrjScrModelParser.#sectionFields(
|
|
37
|
+
PrjScrModelParser.#findSection(sections, 'Design')
|
|
38
|
+
)
|
|
39
|
+
const existingPaths = new Set(
|
|
40
|
+
(options.existingPaths || []).map((path) =>
|
|
41
|
+
PrjScrModelParser.#normalizePath(path)
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
const documents = PrjScrModelParser.#extractDocuments(
|
|
45
|
+
sections,
|
|
46
|
+
existingPaths,
|
|
47
|
+
options
|
|
48
|
+
)
|
|
49
|
+
const scripts = documents
|
|
50
|
+
.filter((document) => document.kind === 'script')
|
|
51
|
+
.map((document) => PrjScrModelParser.#publicScript(document))
|
|
52
|
+
const diagnostics = PrjScrModelParser.#diagnostics(documents)
|
|
53
|
+
|
|
54
|
+
return NormalizedModelSchema.attach({
|
|
55
|
+
kind: 'project-script',
|
|
56
|
+
fileType: 'PrjScr',
|
|
57
|
+
fileName,
|
|
58
|
+
summary: {
|
|
59
|
+
title: ParserUtils.stripExtension(fileName),
|
|
60
|
+
documentCount: documents.length,
|
|
61
|
+
scriptCount: scripts.length,
|
|
62
|
+
missingPathCount: scripts.filter(
|
|
63
|
+
(script) => script.exists === false
|
|
64
|
+
).length,
|
|
65
|
+
diagnosticCount: diagnostics.length
|
|
66
|
+
},
|
|
67
|
+
diagnostics,
|
|
68
|
+
projectScript: {
|
|
69
|
+
name: ParserUtils.stripExtension(fileName),
|
|
70
|
+
design,
|
|
71
|
+
documents,
|
|
72
|
+
scripts,
|
|
73
|
+
sections: PrjScrModelParser.#serializeSections(sections)
|
|
74
|
+
},
|
|
75
|
+
bom: []
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Decodes text with common project-file encodings.
|
|
81
|
+
* @param {ArrayBuffer} arrayBuffer Source bytes.
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
static #decodeText(arrayBuffer) {
|
|
85
|
+
const bytes = new Uint8Array(arrayBuffer || new ArrayBuffer(0))
|
|
86
|
+
for (const encoding of ['utf-8', 'windows-1252']) {
|
|
87
|
+
try {
|
|
88
|
+
return new TextDecoder(encoding, { fatal: true })
|
|
89
|
+
.decode(bytes)
|
|
90
|
+
.replace(/^\uFEFF/u, '')
|
|
91
|
+
} catch {
|
|
92
|
+
// Try the next legacy-compatible project encoding.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return new TextDecoder('windows-1252')
|
|
97
|
+
.decode(bytes)
|
|
98
|
+
.replace(/^\uFEFF/u, '')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parses INI sections while preserving option order.
|
|
103
|
+
* @param {string} text Source text.
|
|
104
|
+
* @returns {{ name: string, index: number, entries: object[] }[]}
|
|
105
|
+
*/
|
|
106
|
+
static #parseIniSections(text) {
|
|
107
|
+
const sections = []
|
|
108
|
+
let current = null
|
|
109
|
+
const lines = String(text || '')
|
|
110
|
+
.replace(/\r\n?/gu, '\n')
|
|
111
|
+
.split('\n')
|
|
112
|
+
|
|
113
|
+
for (const [lineIndex, rawLine] of lines.entries()) {
|
|
114
|
+
const trimmed = rawLine.trim()
|
|
115
|
+
if (
|
|
116
|
+
!trimmed ||
|
|
117
|
+
trimmed.startsWith(';') ||
|
|
118
|
+
trimmed.startsWith('#')
|
|
119
|
+
) {
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const sectionMatch = /^\[([^\]]+)\]$/u.exec(trimmed)
|
|
124
|
+
if (sectionMatch) {
|
|
125
|
+
current = {
|
|
126
|
+
name: sectionMatch[1].trim(),
|
|
127
|
+
index: sections.length,
|
|
128
|
+
entries: []
|
|
129
|
+
}
|
|
130
|
+
sections.push(current)
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!current) continue
|
|
135
|
+
const separatorIndex = rawLine.indexOf('=')
|
|
136
|
+
if (separatorIndex < 0) continue
|
|
137
|
+
|
|
138
|
+
current.entries.push({
|
|
139
|
+
key: rawLine.slice(0, separatorIndex).trim(),
|
|
140
|
+
value: rawLine.slice(separatorIndex + 1).trim(),
|
|
141
|
+
line: lineIndex + 1
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return sections
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Extracts numbered document entries.
|
|
150
|
+
* @param {object[]} sections Parsed sections.
|
|
151
|
+
* @param {Set<string>} existingPaths Normalized existing paths.
|
|
152
|
+
* @param {{ existingPaths?: string[] }} options Parser options.
|
|
153
|
+
* @returns {object[]}
|
|
154
|
+
*/
|
|
155
|
+
static #extractDocuments(sections, existingPaths, options) {
|
|
156
|
+
return PrjScrModelParser.#numberedSections(sections, 'Document').map(
|
|
157
|
+
({ section, number }) => {
|
|
158
|
+
const fields = PrjScrModelParser.#sectionFields(section)
|
|
159
|
+
const path = String(fields.DocumentPath || '')
|
|
160
|
+
const normalizedPath = PrjScrModelParser.#normalizePath(path)
|
|
161
|
+
const base = {
|
|
162
|
+
index: number,
|
|
163
|
+
section: section.name,
|
|
164
|
+
path,
|
|
165
|
+
normalizedPath,
|
|
166
|
+
fileName: PrjScrModelParser.#basename(path),
|
|
167
|
+
extension: PrjScrModelParser.#extension(path),
|
|
168
|
+
kind:
|
|
169
|
+
PrjScrModelParser.#extension(path).toLowerCase() ===
|
|
170
|
+
'.pas'
|
|
171
|
+
? 'script'
|
|
172
|
+
: 'unsupported',
|
|
173
|
+
...(options.existingPaths
|
|
174
|
+
? { exists: existingPaths.has(normalizedPath) }
|
|
175
|
+
: {}),
|
|
176
|
+
annotationEnabled: PrjScrModelParser.#optionalBoolean(
|
|
177
|
+
fields.AnnotationEnabled
|
|
178
|
+
),
|
|
179
|
+
classGeneration: PrjScrModelParser.#classGeneration(fields),
|
|
180
|
+
updatePolicies: PrjScrModelParser.#updatePolicies(fields),
|
|
181
|
+
options: fields
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return PrjScrModelParser.#stripEmpty(base)
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Builds class-generation option metadata.
|
|
191
|
+
* @param {Record<string, string>} fields Document fields.
|
|
192
|
+
* @returns {object | undefined}
|
|
193
|
+
*/
|
|
194
|
+
static #classGeneration(fields) {
|
|
195
|
+
return PrjScrModelParser.#stripEmpty({
|
|
196
|
+
classGenCcAutoEnabled: PrjScrModelParser.#optionalBoolean(
|
|
197
|
+
fields.ClassGenCCAutoEnabled
|
|
198
|
+
),
|
|
199
|
+
classGenCcAutoRoomEnabled: PrjScrModelParser.#optionalBoolean(
|
|
200
|
+
fields.ClassGenCCAutoRoomEnabled
|
|
201
|
+
),
|
|
202
|
+
classGenNcAutoScope: fields.ClassGenNCAutoScope
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Builds update-policy metadata.
|
|
208
|
+
* @param {Record<string, string>} fields Document fields.
|
|
209
|
+
* @returns {object | undefined}
|
|
210
|
+
*/
|
|
211
|
+
static #updatePolicies(fields) {
|
|
212
|
+
return PrjScrModelParser.#stripEmpty({
|
|
213
|
+
doLibraryUpdate: PrjScrModelParser.#optionalBoolean(
|
|
214
|
+
fields.DoLibraryUpdate
|
|
215
|
+
),
|
|
216
|
+
doDatabaseUpdate: PrjScrModelParser.#optionalBoolean(
|
|
217
|
+
fields.DoDatabaseUpdate
|
|
218
|
+
)
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Builds structured parser diagnostics.
|
|
224
|
+
* @param {object[]} documents Parsed documents.
|
|
225
|
+
* @returns {object[]}
|
|
226
|
+
*/
|
|
227
|
+
static #diagnostics(documents) {
|
|
228
|
+
return [
|
|
229
|
+
...documents
|
|
230
|
+
.filter(
|
|
231
|
+
(document) =>
|
|
232
|
+
document.kind === 'script' && document.exists === false
|
|
233
|
+
)
|
|
234
|
+
.map((document) => ({
|
|
235
|
+
code: 'project-script.missing-document-path',
|
|
236
|
+
severity: 'warning',
|
|
237
|
+
message: 'Script project document path was not found.',
|
|
238
|
+
path: document.path,
|
|
239
|
+
normalizedPath: document.normalizedPath
|
|
240
|
+
})),
|
|
241
|
+
...documents
|
|
242
|
+
.filter((document) => document.kind === 'unsupported')
|
|
243
|
+
.map((document) => ({
|
|
244
|
+
code: 'project-script.unsupported-document-kind',
|
|
245
|
+
severity: 'warning',
|
|
246
|
+
message:
|
|
247
|
+
'Script project document is not a supported script file.',
|
|
248
|
+
path: document.path,
|
|
249
|
+
normalizedPath: document.normalizedPath
|
|
250
|
+
}))
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Builds the public script convenience row.
|
|
256
|
+
* @param {object} document Parsed document row.
|
|
257
|
+
* @returns {object}
|
|
258
|
+
*/
|
|
259
|
+
static #publicScript(document) {
|
|
260
|
+
const { kind: _kind, ...script } = document
|
|
261
|
+
return script
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Finds numbered sections with a common prefix.
|
|
266
|
+
* @param {object[]} sections Parsed sections.
|
|
267
|
+
* @param {string} prefix Section prefix.
|
|
268
|
+
* @returns {{ section: object, number: number }[]}
|
|
269
|
+
*/
|
|
270
|
+
static #numberedSections(sections, prefix) {
|
|
271
|
+
const pattern = new RegExp('^' + prefix + '(\\d+)$', 'iu')
|
|
272
|
+
|
|
273
|
+
return (sections || [])
|
|
274
|
+
.map((section) => ({
|
|
275
|
+
section,
|
|
276
|
+
match: pattern.exec(section.name)
|
|
277
|
+
}))
|
|
278
|
+
.filter(({ match }) => match)
|
|
279
|
+
.map(({ section, match }) => ({
|
|
280
|
+
section,
|
|
281
|
+
number: Number.parseInt(match[1], 10)
|
|
282
|
+
}))
|
|
283
|
+
.sort((left, right) => left.number - right.number)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Finds one section by case-insensitive name.
|
|
288
|
+
* @param {object[]} sections Parsed sections.
|
|
289
|
+
* @param {string} name Section name.
|
|
290
|
+
* @returns {object | undefined}
|
|
291
|
+
*/
|
|
292
|
+
static #findSection(sections, name) {
|
|
293
|
+
const normalized = String(name || '').toLowerCase()
|
|
294
|
+
return (sections || []).find(
|
|
295
|
+
(section) => section.name.toLowerCase() === normalized
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Converts one section to a key-value map.
|
|
301
|
+
* @param {{ entries?: { key: string, value: string }[] } | undefined} section Parsed section.
|
|
302
|
+
* @returns {Record<string, string>}
|
|
303
|
+
*/
|
|
304
|
+
static #sectionFields(section) {
|
|
305
|
+
return Object.fromEntries(
|
|
306
|
+
(section?.entries || []).map((entry) => [entry.key, entry.value])
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Serializes preserved sections.
|
|
312
|
+
* @param {object[]} sections Parsed sections.
|
|
313
|
+
* @returns {object[]}
|
|
314
|
+
*/
|
|
315
|
+
static #serializeSections(sections) {
|
|
316
|
+
return (sections || []).map((section) => ({
|
|
317
|
+
name: section.name,
|
|
318
|
+
entries: (section.entries || []).map((entry) => ({
|
|
319
|
+
key: entry.key,
|
|
320
|
+
value: entry.value
|
|
321
|
+
}))
|
|
322
|
+
}))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Parses optional boolean option values.
|
|
327
|
+
* @param {string | undefined} value Raw value.
|
|
328
|
+
* @returns {boolean | undefined}
|
|
329
|
+
*/
|
|
330
|
+
static #optionalBoolean(value) {
|
|
331
|
+
const normalized = String(value ?? '')
|
|
332
|
+
.trim()
|
|
333
|
+
.toLowerCase()
|
|
334
|
+
if (!normalized) return undefined
|
|
335
|
+
return ['1', 'true', 't', 'yes'].includes(normalized)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Normalizes project-relative paths.
|
|
340
|
+
* @param {string} path Source path.
|
|
341
|
+
* @returns {string}
|
|
342
|
+
*/
|
|
343
|
+
static #normalizePath(path) {
|
|
344
|
+
return String(path || '').replace(/\\/gu, '/')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Returns a basename from a project path.
|
|
349
|
+
* @param {string} path Source path.
|
|
350
|
+
* @returns {string}
|
|
351
|
+
*/
|
|
352
|
+
static #basename(path) {
|
|
353
|
+
return PrjScrModelParser.#normalizePath(path).split('/').pop() || ''
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Returns a lower-level extension token.
|
|
358
|
+
* @param {string} path Source path.
|
|
359
|
+
* @returns {string}
|
|
360
|
+
*/
|
|
361
|
+
static #extension(path) {
|
|
362
|
+
const name = PrjScrModelParser.#basename(path)
|
|
363
|
+
const dotIndex = name.lastIndexOf('.')
|
|
364
|
+
return dotIndex >= 0 ? name.slice(dotIndex) : ''
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Removes undefined and empty object fields.
|
|
369
|
+
* @param {Record<string, unknown>} value Source object.
|
|
370
|
+
* @returns {object}
|
|
371
|
+
*/
|
|
372
|
+
static #stripEmpty(value) {
|
|
373
|
+
return Object.fromEntries(
|
|
374
|
+
Object.entries(value || {}).filter(([, entryValue]) => {
|
|
375
|
+
if (
|
|
376
|
+
entryValue &&
|
|
377
|
+
typeof entryValue === 'object' &&
|
|
378
|
+
!Array.isArray(entryValue)
|
|
379
|
+
) {
|
|
380
|
+
return Object.keys(entryValue).length > 0
|
|
381
|
+
}
|
|
382
|
+
return entryValue !== undefined && entryValue !== ''
|
|
383
|
+
})
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
}
|