altium-toolkit 1.1.22 → 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 +134 -23
- package/docs/model-format.md +174 -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 +511 -1
- 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 +2 -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/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumParser.mjs +352 -14
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +9 -0
- 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/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/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/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/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +12 -45
- package/src/core/altium/SchematicPrimitiveParser.mjs +9 -14
- 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 +64 -25
- 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/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/parser.mjs +35 -1
- package/src/renderers.mjs +1 -0
- package/src/ui/SchematicShapeRenderer.mjs +49 -6
- package/src/ui/SchematicSvgRenderer.mjs +37 -8
- package/src/ui/SchematicTypography.mjs +4 -3
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds compact reports for preserved raw and unknown parser data.
|
|
7
|
+
*/
|
|
8
|
+
export class RawDataPreservationReportBuilder {
|
|
9
|
+
static SCHEMA = 'altium-toolkit.raw-data-preservation.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a raw-data preservation report.
|
|
13
|
+
* @param {{ models?: object[], rawRecords?: object[], unknownRecords?: object[], opaqueRecords?: object[], nativeStreams?: object[] }} [input]
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(input = {}) {
|
|
17
|
+
const rawRecords = RawDataPreservationReportBuilder.#rawRecords(input)
|
|
18
|
+
const unknownRecords =
|
|
19
|
+
RawDataPreservationReportBuilder.#unknownRecords(input)
|
|
20
|
+
const opaqueRecords =
|
|
21
|
+
RawDataPreservationReportBuilder.#opaqueRecords(input)
|
|
22
|
+
const nativeStreams =
|
|
23
|
+
RawDataPreservationReportBuilder.#nativeStreams(input)
|
|
24
|
+
const records = rawRecords.map((record) =>
|
|
25
|
+
RawDataPreservationReportBuilder.#recordRow(record)
|
|
26
|
+
)
|
|
27
|
+
const unknownRows = unknownRecords.map((record) =>
|
|
28
|
+
RawDataPreservationReportBuilder.#unknownRecordRow(record)
|
|
29
|
+
)
|
|
30
|
+
const opaqueRows = opaqueRecords.map((record) =>
|
|
31
|
+
RawDataPreservationReportBuilder.#opaqueRecordRow(record)
|
|
32
|
+
)
|
|
33
|
+
const nativeStreamRows = nativeStreams.map((stream) =>
|
|
34
|
+
RawDataPreservationReportBuilder.#nativeStreamRow(stream)
|
|
35
|
+
)
|
|
36
|
+
const preservedByteCount = [
|
|
37
|
+
...records,
|
|
38
|
+
...unknownRows,
|
|
39
|
+
...opaqueRows
|
|
40
|
+
].reduce((sum, record) => sum + Number(record.byteLength || 0), 0)
|
|
41
|
+
const nativeStreamSummary = nativeStreamRows.length
|
|
42
|
+
? {
|
|
43
|
+
nativeStreamCount: nativeStreamRows.length,
|
|
44
|
+
unknownNativeStreamCount: nativeStreamRows.filter(
|
|
45
|
+
(stream) => !stream.known
|
|
46
|
+
).length,
|
|
47
|
+
unconsumedNativeStreamCount: nativeStreamRows.filter(
|
|
48
|
+
(stream) => !stream.consumed
|
|
49
|
+
).length,
|
|
50
|
+
nativeStreamByteCount: nativeStreamRows.reduce(
|
|
51
|
+
(sum, stream) => sum + Number(stream.byteLength || 0),
|
|
52
|
+
0
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
: {}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
schema: RawDataPreservationReportBuilder.SCHEMA,
|
|
59
|
+
summary: {
|
|
60
|
+
rawRecordCount: records.length,
|
|
61
|
+
unknownRecordCount: unknownRows.length,
|
|
62
|
+
opaqueRecordCount: opaqueRows.length,
|
|
63
|
+
supportedRawRecordCount: records.filter(
|
|
64
|
+
(record) => record.supported
|
|
65
|
+
).length,
|
|
66
|
+
unsupportedRawRecordCount: records.filter(
|
|
67
|
+
(record) => !record.supported
|
|
68
|
+
).length,
|
|
69
|
+
parsedRawRecordCount: records.filter((record) => record.parsed)
|
|
70
|
+
.length,
|
|
71
|
+
unparsedRawRecordCount: records.filter(
|
|
72
|
+
(record) => !record.parsed
|
|
73
|
+
).length,
|
|
74
|
+
...nativeStreamSummary,
|
|
75
|
+
preservedByteCount
|
|
76
|
+
},
|
|
77
|
+
records,
|
|
78
|
+
unknownRecords: unknownRows,
|
|
79
|
+
opaqueRecords: opaqueRows,
|
|
80
|
+
nativeStreams: nativeStreamRows,
|
|
81
|
+
families: RawDataPreservationReportBuilder.#familyRows(records)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Collects raw records from direct input and parser roots.
|
|
87
|
+
* @param {object} input Build input.
|
|
88
|
+
* @returns {object[]}
|
|
89
|
+
*/
|
|
90
|
+
static #rawRecords(input) {
|
|
91
|
+
return [
|
|
92
|
+
...(Array.isArray(input?.rawRecords) ? input.rawRecords : []),
|
|
93
|
+
...RawDataPreservationReportBuilder.#models(input).flatMap(
|
|
94
|
+
(model) => [
|
|
95
|
+
...(model?.pcb?.rawRecords || []),
|
|
96
|
+
...((model?.pcbLibrary?.footprints || []).flatMap(
|
|
97
|
+
(footprint) => footprint.rawRecords || []
|
|
98
|
+
) || [])
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Collects unknown records from direct input and parser roots.
|
|
106
|
+
* @param {object} input Build input.
|
|
107
|
+
* @returns {object[]}
|
|
108
|
+
*/
|
|
109
|
+
static #unknownRecords(input) {
|
|
110
|
+
return [
|
|
111
|
+
...(Array.isArray(input?.unknownRecords)
|
|
112
|
+
? input.unknownRecords
|
|
113
|
+
: []),
|
|
114
|
+
...RawDataPreservationReportBuilder.#models(input).flatMap(
|
|
115
|
+
(model) =>
|
|
116
|
+
(model?.pcbLibrary?.footprints || []).flatMap(
|
|
117
|
+
(footprint) => footprint.unknownRecords || []
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Collects opaque records from direct input and parser roots.
|
|
125
|
+
* @param {object} input Build input.
|
|
126
|
+
* @returns {object[]}
|
|
127
|
+
*/
|
|
128
|
+
static #opaqueRecords(input) {
|
|
129
|
+
return [
|
|
130
|
+
...(Array.isArray(input?.opaqueRecords) ? input.opaqueRecords : []),
|
|
131
|
+
...RawDataPreservationReportBuilder.#models(input).flatMap(
|
|
132
|
+
(model) => [
|
|
133
|
+
...(model?.schematic?.opaqueRecords || []),
|
|
134
|
+
...(model?.schematicLibrary?.opaqueRecords || []),
|
|
135
|
+
...((model?.schematicLibrary?.components || []).flatMap(
|
|
136
|
+
(component) => component.opaqueRecords || []
|
|
137
|
+
) || []),
|
|
138
|
+
...((model?.integratedLibrary?.sources || []).flatMap(
|
|
139
|
+
(source) => source.opaqueRecords || []
|
|
140
|
+
) || [])
|
|
141
|
+
]
|
|
142
|
+
)
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Collects native stream inventory rows from direct input and parser roots.
|
|
148
|
+
* @param {object} input Build input.
|
|
149
|
+
* @returns {object[]}
|
|
150
|
+
*/
|
|
151
|
+
static #nativeStreams(input) {
|
|
152
|
+
return [
|
|
153
|
+
...(Array.isArray(input?.nativeStreams) ? input.nativeStreams : []),
|
|
154
|
+
...RawDataPreservationReportBuilder.#models(input).flatMap(
|
|
155
|
+
(model) => [
|
|
156
|
+
...(model?.schematic?.nativeStreams?.streams || []),
|
|
157
|
+
...(model?.pcb?.nativeStreams?.streams || []),
|
|
158
|
+
...(model?.schematicLibrary?.nativeStreams?.streams || []),
|
|
159
|
+
...(model?.pcbLibrary?.nativeStreams?.streams || []),
|
|
160
|
+
...((model?.integratedLibrary?.sources || []).flatMap(
|
|
161
|
+
(source) => source.nativeStreams?.streams || []
|
|
162
|
+
) || [])
|
|
163
|
+
]
|
|
164
|
+
)
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Normalizes models input.
|
|
170
|
+
* @param {object} input Build input.
|
|
171
|
+
* @returns {object[]}
|
|
172
|
+
*/
|
|
173
|
+
static #models(input) {
|
|
174
|
+
return Array.isArray(input?.models) ? input.models : []
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Builds a public raw-record row without copying raw payload bytes.
|
|
179
|
+
* @param {object} record Raw record.
|
|
180
|
+
* @returns {object}
|
|
181
|
+
*/
|
|
182
|
+
static #recordRow(record) {
|
|
183
|
+
return RawDataPreservationReportBuilder.#stripUndefined({
|
|
184
|
+
source: record?.source,
|
|
185
|
+
sourceStorage: record?.sourceStorage,
|
|
186
|
+
sourceStream: record?.sourceStream,
|
|
187
|
+
family: record?.family || 'unknown',
|
|
188
|
+
type: record?.type || 'unknown',
|
|
189
|
+
typeId: RawDataPreservationReportBuilder.#finiteOrUndefined(
|
|
190
|
+
record?.typeId
|
|
191
|
+
),
|
|
192
|
+
recordIndex: RawDataPreservationReportBuilder.#finiteOrUndefined(
|
|
193
|
+
record?.recordIndex
|
|
194
|
+
),
|
|
195
|
+
byteLength: RawDataPreservationReportBuilder.#byteLength(record),
|
|
196
|
+
supported: Boolean(record?.supported),
|
|
197
|
+
parsed: Boolean(record?.parsed),
|
|
198
|
+
hasRawPayload: Boolean(record?.rawBase64)
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Builds a public unknown-record row.
|
|
204
|
+
* @param {object} record Unknown record.
|
|
205
|
+
* @returns {object}
|
|
206
|
+
*/
|
|
207
|
+
static #unknownRecordRow(record) {
|
|
208
|
+
return RawDataPreservationReportBuilder.#stripUndefined({
|
|
209
|
+
sourceStorage: record?.sourceStorage,
|
|
210
|
+
sourceStream: record?.sourceStream,
|
|
211
|
+
typeId: RawDataPreservationReportBuilder.#finiteOrUndefined(
|
|
212
|
+
record?.typeId
|
|
213
|
+
),
|
|
214
|
+
recordIndex: RawDataPreservationReportBuilder.#finiteOrUndefined(
|
|
215
|
+
record?.recordIndex
|
|
216
|
+
),
|
|
217
|
+
byteLength: RawDataPreservationReportBuilder.#byteLength(record)
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Builds a public opaque-record row.
|
|
223
|
+
* @param {object} record Opaque record.
|
|
224
|
+
* @returns {object}
|
|
225
|
+
*/
|
|
226
|
+
static #opaqueRecordRow(record) {
|
|
227
|
+
return RawDataPreservationReportBuilder.#stripUndefined({
|
|
228
|
+
source: record?.source,
|
|
229
|
+
sourceStorage: record?.sourceStorage,
|
|
230
|
+
sourceStream: record?.sourceStream,
|
|
231
|
+
frameType: RawDataPreservationReportBuilder.#finiteOrUndefined(
|
|
232
|
+
record?.frameType
|
|
233
|
+
),
|
|
234
|
+
typeId: RawDataPreservationReportBuilder.#finiteOrUndefined(
|
|
235
|
+
record?.typeId
|
|
236
|
+
),
|
|
237
|
+
recordIndex: RawDataPreservationReportBuilder.#finiteOrUndefined(
|
|
238
|
+
record?.recordIndex
|
|
239
|
+
),
|
|
240
|
+
byteLength: RawDataPreservationReportBuilder.#byteLength(record),
|
|
241
|
+
hasRawPayload: Boolean(record?.rawBase64)
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Builds a public native-stream inventory row.
|
|
247
|
+
* @param {object} stream Native stream row.
|
|
248
|
+
* @returns {object}
|
|
249
|
+
*/
|
|
250
|
+
static #nativeStreamRow(stream) {
|
|
251
|
+
return RawDataPreservationReportBuilder.#stripUndefined({
|
|
252
|
+
source: stream?.source,
|
|
253
|
+
sourceStorage: stream?.sourceStorage,
|
|
254
|
+
sourceStream: stream?.sourceStream,
|
|
255
|
+
leafName: stream?.leafName,
|
|
256
|
+
byteLength: RawDataPreservationReportBuilder.#finiteOrUndefined(
|
|
257
|
+
stream?.byteLength
|
|
258
|
+
),
|
|
259
|
+
known: Boolean(stream?.known),
|
|
260
|
+
consumed: Boolean(stream?.consumed),
|
|
261
|
+
classification: stream?.classification,
|
|
262
|
+
consumedBy: stream?.consumedBy,
|
|
263
|
+
checksum: stream?.checksum
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Builds per-family summary rows.
|
|
269
|
+
* @param {object[]} records Raw record rows.
|
|
270
|
+
* @returns {object[]}
|
|
271
|
+
*/
|
|
272
|
+
static #familyRows(records) {
|
|
273
|
+
const byFamily = new Map()
|
|
274
|
+
for (const record of records) {
|
|
275
|
+
const key = record.family || 'unknown'
|
|
276
|
+
const row = byFamily.get(key) || {
|
|
277
|
+
family: key,
|
|
278
|
+
rawRecordCount: 0,
|
|
279
|
+
supportedRawRecordCount: 0,
|
|
280
|
+
unsupportedRawRecordCount: 0,
|
|
281
|
+
preservedByteCount: 0
|
|
282
|
+
}
|
|
283
|
+
row.rawRecordCount += 1
|
|
284
|
+
row.preservedByteCount += Number(record.byteLength || 0)
|
|
285
|
+
if (record.supported) row.supportedRawRecordCount += 1
|
|
286
|
+
else row.unsupportedRawRecordCount += 1
|
|
287
|
+
byFamily.set(key, row)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return [...byFamily.values()].sort((left, right) =>
|
|
291
|
+
left.family.localeCompare(right.family)
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Resolves a byte length from explicit metadata or payload size.
|
|
297
|
+
* @param {object} record Raw record.
|
|
298
|
+
* @returns {number}
|
|
299
|
+
*/
|
|
300
|
+
static #byteLength(record) {
|
|
301
|
+
const explicit = Number(record?.byteLength)
|
|
302
|
+
if (Number.isFinite(explicit) && explicit >= 0) return explicit
|
|
303
|
+
if (typeof record?.rawBase64 === 'string') {
|
|
304
|
+
return RawDataPreservationReportBuilder.#base64ByteLength(
|
|
305
|
+
record.rawBase64
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
return 0
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Computes decoded byte length for a base64 payload without decoding it.
|
|
313
|
+
* @param {string} value Base64 payload.
|
|
314
|
+
* @returns {number}
|
|
315
|
+
*/
|
|
316
|
+
static #base64ByteLength(value) {
|
|
317
|
+
const normalized = String(value || '').replace(/\s/gu, '')
|
|
318
|
+
if (!normalized) return 0
|
|
319
|
+
|
|
320
|
+
const padding = normalized.endsWith('==')
|
|
321
|
+
? 2
|
|
322
|
+
: normalized.endsWith('=')
|
|
323
|
+
? 1
|
|
324
|
+
: 0
|
|
325
|
+
return Math.max(Math.floor((normalized.length * 3) / 4) - padding, 0)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Returns a finite number or undefined.
|
|
330
|
+
* @param {unknown} value Numeric value.
|
|
331
|
+
* @returns {number | undefined}
|
|
332
|
+
*/
|
|
333
|
+
static #finiteOrUndefined(value) {
|
|
334
|
+
const numeric = Number(value)
|
|
335
|
+
return Number.isFinite(numeric) ? numeric : undefined
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Removes undefined values from a row.
|
|
340
|
+
* @param {object} row Row to strip.
|
|
341
|
+
* @returns {object}
|
|
342
|
+
*/
|
|
343
|
+
static #stripUndefined(row) {
|
|
344
|
+
return Object.fromEntries(
|
|
345
|
+
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
}
|