@vessel-dsp/core 0.5.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/LICENSE.md +21 -0
- package/README.md +6 -0
- package/package.json +56 -0
- package/src/editor/commands.ts +344 -0
- package/src/editor/factory.ts +148 -0
- package/src/editor/history.ts +142 -0
- package/src/editor/index.ts +11 -0
- package/src/editor/layout.ts +207 -0
- package/src/formats/circuit-json/serializer.ts +1410 -0
- package/src/formats/document.ts +274 -0
- package/src/formats/interchange/parser.ts +1165 -0
- package/src/formats/interchange/serializer.ts +594 -0
- package/src/formats/ltspice/catalog.ts +181 -0
- package/src/formats/ltspice/encoding.ts +151 -0
- package/src/formats/ltspice/parser.ts +432 -0
- package/src/formats/ltspice/serializer.ts +169 -0
- package/src/formats/schx/catalog.ts +439 -0
- package/src/formats/schx/parser.ts +261 -0
- package/src/formats/schx/runtime-descriptors.ts +502 -0
- package/src/formats/schx/serializer.ts +211 -0
- package/src/formats/schx/transforms.ts +38 -0
- package/src/formats/spice/parser.ts +373 -0
- package/src/formats/spice/serializer.ts +43 -0
- package/src/index.ts +205 -0
- package/src/model/connectivity.ts +239 -0
- package/src/model/netlist.ts +375 -0
- package/src/model/properties.ts +101 -0
- package/src/model/quantity.ts +173 -0
- package/src/model/types.ts +309 -0
- package/src/model/validation.ts +985 -0
- package/src/model/wires.ts +86 -0
- package/src/panel/extract.ts +878 -0
- package/src/panel/index.ts +39 -0
- package/src/panel/knobs.ts +70 -0
- package/src/panel/protocol.ts +117 -0
- package/src/panel/types.ts +180 -0
- package/src/preview/bounds.ts +85 -0
- package/src/preview/box-layout.ts +24 -0
- package/src/preview/colors.ts +43 -0
- package/src/preview/hanging.ts +94 -0
- package/src/preview/junctions.ts +94 -0
- package/src/preview/label-layout.ts +90 -0
- package/src/preview/ports.ts +101 -0
- package/src/preview/renderable-wires.ts +113 -0
- package/src/preview/routing.ts +15 -0
- package/src/preview/snap.ts +104 -0
- package/src/preview/symbols/analog-switch.svg +17 -0
- package/src/preview/symbols/battery.svg +16 -0
- package/src/preview/symbols/bbd.svg +21 -0
- package/src/preview/symbols/bjt-npn.svg +16 -0
- package/src/preview/symbols/bjt-pnp.svg +17 -0
- package/src/preview/symbols/capacitor-electrolytic.svg +13 -0
- package/src/preview/symbols/capacitor.svg +12 -0
- package/src/preview/symbols/current-source.svg +14 -0
- package/src/preview/symbols/delay-ic.svg +22 -0
- package/src/preview/symbols/diode-schottky.svg +12 -0
- package/src/preview/symbols/diode-zener.svg +12 -0
- package/src/preview/symbols/diode.svg +13 -0
- package/src/preview/symbols/flipflop.svg +20 -0
- package/src/preview/symbols/ground.svg +12 -0
- package/src/preview/symbols/ic-block.svg +20 -0
- package/src/preview/symbols/ic.svg +19 -0
- package/src/preview/symbols/inductor.svg +11 -0
- package/src/preview/symbols/jack-input.svg +16 -0
- package/src/preview/symbols/jack-output.svg +16 -0
- package/src/preview/symbols/jfet-junction-n.svg +17 -0
- package/src/preview/symbols/jfet-n.svg +17 -0
- package/src/preview/symbols/jfet-p.svg +17 -0
- package/src/preview/symbols/label.svg +8 -0
- package/src/preview/symbols/led.svg +18 -0
- package/src/preview/symbols/mosfet-n.svg +21 -0
- package/src/preview/symbols/mosfet-p.svg +21 -0
- package/src/preview/symbols/named-wire.svg +11 -0
- package/src/preview/symbols/opamp.svg +21 -0
- package/src/preview/symbols/optocoupler.svg +30 -0
- package/src/preview/symbols/ota.svg +20 -0
- package/src/preview/symbols/pentode.svg +25 -0
- package/src/preview/symbols/photoresistor.svg +19 -0
- package/src/preview/symbols/port.svg +8 -0
- package/src/preview/symbols/potentiometer.svg +15 -0
- package/src/preview/symbols/power-amp.svg +20 -0
- package/src/preview/symbols/rail.svg +11 -0
- package/src/preview/symbols/regulator.svg +13 -0
- package/src/preview/symbols/relay.svg +20 -0
- package/src/preview/symbols/resistor.svg +11 -0
- package/src/preview/symbols/svg-content.ts +59 -0
- package/src/preview/symbols/switch-3pdt.svg +32 -0
- package/src/preview/symbols/switch-rotary.svg +23 -0
- package/src/preview/symbols/switch-spdt.svg +16 -0
- package/src/preview/symbols/switch-spst.svg +14 -0
- package/src/preview/symbols/switch-toggle.svg +14 -0
- package/src/preview/symbols/transformer.svg +17 -0
- package/src/preview/symbols/triode.svg +17 -0
- package/src/preview/symbols/tube-diode.svg +13 -0
- package/src/preview/symbols/unsupported.svg +8 -0
- package/src/preview/symbols/variable-resistor.svg +13 -0
- package/src/preview/symbols/voltage-source.svg +15 -0
- package/src/preview/symbols.ts +207 -0
- package/src/preview/wire-chains.ts +153 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { ComponentKind, Point } from '../../model/types';
|
|
2
|
+
|
|
3
|
+
// LTspice's native grid uses pin spacings of ~32–64 units per component
|
|
4
|
+
// (resistor pins 64 apart, BJT pins 64 apart). The schematic renderer assumes
|
|
5
|
+
// components fit within a ±20 box centered on origin, so we scale every LTspice
|
|
6
|
+
// coordinate uniformly at parse time so terminal positions land near the box
|
|
7
|
+
// edges instead of floating outside. 0.5 puts a resistor's pin-to-pin span at
|
|
8
|
+
// 32 — comfortably inside the 40-unit box. Applied symmetrically to terminal
|
|
9
|
+
// local offsets here and to all parsed Points in the parser; connectivity is
|
|
10
|
+
// preserved because both sides are scaled identically.
|
|
11
|
+
export const LTSPICE_COORD_SCALE = 0.5;
|
|
12
|
+
|
|
13
|
+
export type LtspiceTerminalDef = Readonly<{ name: string; local: Point }>;
|
|
14
|
+
|
|
15
|
+
export type LtspiceSymbolDef = Readonly<{
|
|
16
|
+
symbolName: string;
|
|
17
|
+
kind: ComponentKind;
|
|
18
|
+
terminals: readonly LtspiceTerminalDef[];
|
|
19
|
+
valueProperty: string | null;
|
|
20
|
+
modelFromValue: boolean;
|
|
21
|
+
// When true, derive `properties.model` from the basename of the symbol path
|
|
22
|
+
// (e.g. SYMBOL Opamps\LM308 → model = "LM308") instead of from SYMATTR Value.
|
|
23
|
+
// LTspice manufacturer-specific opamp symbols encode the model in the path,
|
|
24
|
+
// not in a Value attribute, so this is how we capture the part number.
|
|
25
|
+
modelFromSymbolPath?: boolean;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
// Per-symbol pin layouts derived empirically from the cushychicken corpus
|
|
29
|
+
// (and confirmed against LTspice's bundled .asy files).
|
|
30
|
+
// Each layout matches the actual PIN positions in res.asy / cap.asy / etc.,
|
|
31
|
+
// so wires drawn by LTspice land exactly on our computed terminal positions.
|
|
32
|
+
const RES_PINS: readonly LtspiceTerminalDef[] = [
|
|
33
|
+
{ name: 'a', local: { x: 16, y: 16 } },
|
|
34
|
+
{ name: 'b', local: { x: 16, y: 96 } },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const CAP_PINS: readonly LtspiceTerminalDef[] = [
|
|
38
|
+
{ name: 'a', local: { x: 16, y: 0 } },
|
|
39
|
+
{ name: 'b', local: { x: 16, y: 64 } },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const IND_PINS: readonly LtspiceTerminalDef[] = [
|
|
43
|
+
{ name: 'a', local: { x: 16, y: 0 } },
|
|
44
|
+
{ name: 'b', local: { x: 16, y: 80 } },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const DIODE_PINS: readonly LtspiceTerminalDef[] = [
|
|
48
|
+
{ name: 'anode', local: { x: 16, y: 0 } },
|
|
49
|
+
{ name: 'cathode', local: { x: 16, y: 64 } },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const VOLTAGE_SOURCE_PINS: readonly LtspiceTerminalDef[] = [
|
|
53
|
+
{ name: '+', local: { x: 0, y: 16 } },
|
|
54
|
+
{ name: '-', local: { x: 0, y: 96 } },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const BJT_PINS: readonly LtspiceTerminalDef[] = [
|
|
58
|
+
{ name: 'collector', local: { x: 64, y: 0 } },
|
|
59
|
+
{ name: 'base', local: { x: 0, y: 48 } },
|
|
60
|
+
{ name: 'emitter', local: { x: 64, y: 96 } },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const JFET_PINS: readonly LtspiceTerminalDef[] = [
|
|
64
|
+
{ name: 'drain', local: { x: 64, y: 0 } },
|
|
65
|
+
{ name: 'gate', local: { x: 0, y: 32 } },
|
|
66
|
+
{ name: 'source', local: { x: 64, y: 64 } },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const MOSFET_PINS: readonly LtspiceTerminalDef[] = [
|
|
70
|
+
{ name: 'drain', local: { x: 64, y: 0 } },
|
|
71
|
+
{ name: 'gate', local: { x: 0, y: 32 } },
|
|
72
|
+
{ name: 'source', local: { x: 64, y: 64 } },
|
|
73
|
+
{ name: 'body', local: { x: 32, y: 32 } },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const DEFS: readonly LtspiceSymbolDef[] = [
|
|
77
|
+
{ symbolName: 'res', kind: 'resistor', terminals: RES_PINS, valueProperty: 'R', modelFromValue: false },
|
|
78
|
+
{ symbolName: 'res2', kind: 'resistor', terminals: RES_PINS, valueProperty: 'R', modelFromValue: false },
|
|
79
|
+
{ symbolName: 'cap', kind: 'capacitor', terminals: CAP_PINS, valueProperty: 'C', modelFromValue: false },
|
|
80
|
+
{ symbolName: 'cap2', kind: 'capacitor', terminals: CAP_PINS, valueProperty: 'C', modelFromValue: false },
|
|
81
|
+
{ symbolName: 'ind', kind: 'inductor', terminals: IND_PINS, valueProperty: 'L', modelFromValue: false },
|
|
82
|
+
{ symbolName: 'diode', kind: 'diode', terminals: DIODE_PINS, valueProperty: null, modelFromValue: true },
|
|
83
|
+
{ symbolName: 'led', kind: 'led', terminals: DIODE_PINS, valueProperty: null, modelFromValue: true },
|
|
84
|
+
{ symbolName: 'zener', kind: 'diode', terminals: DIODE_PINS, valueProperty: null, modelFromValue: true },
|
|
85
|
+
{ symbolName: 'schottky', kind: 'diode', terminals: DIODE_PINS, valueProperty: null, modelFromValue: true },
|
|
86
|
+
{ symbolName: 'voltage', kind: 'voltage-source', terminals: VOLTAGE_SOURCE_PINS, valueProperty: 'V', modelFromValue: false },
|
|
87
|
+
{ symbolName: 'current', kind: 'current-source', terminals: VOLTAGE_SOURCE_PINS, valueProperty: 'I', modelFromValue: false },
|
|
88
|
+
{ symbolName: 'npn', kind: 'bjt', terminals: BJT_PINS, valueProperty: null, modelFromValue: true },
|
|
89
|
+
{ symbolName: 'pnp', kind: 'bjt', terminals: BJT_PINS, valueProperty: null, modelFromValue: true },
|
|
90
|
+
{ symbolName: 'njf', kind: 'jfet', terminals: JFET_PINS, valueProperty: null, modelFromValue: true },
|
|
91
|
+
{ symbolName: 'pjf', kind: 'jfet', terminals: JFET_PINS, valueProperty: null, modelFromValue: true },
|
|
92
|
+
{ symbolName: 'nmos', kind: 'mosfet', terminals: MOSFET_PINS, valueProperty: null, modelFromValue: true },
|
|
93
|
+
{ symbolName: 'pmos', kind: 'mosfet', terminals: MOSFET_PINS, valueProperty: null, modelFromValue: true },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const BY_SYMBOL = new Map<string, LtspiceSymbolDef>(DEFS.map((def) => [def.symbolName, def]));
|
|
97
|
+
|
|
98
|
+
// LTspice manufacturer opamps live under `Opamps\<model>` or `OpAmps/<model>` paths
|
|
99
|
+
// (mixed case, both separators). Without the corresponding .asy files we don't know
|
|
100
|
+
// per-model pin geometry, so the generic fallback uses an empty terminal list and
|
|
101
|
+
// records the model name via modelFromSymbolPath. Wire-snapping for opamp pins is
|
|
102
|
+
// best-effort until a future change parses .asy pin positions.
|
|
103
|
+
const GENERIC_OPAMP_DEF: LtspiceSymbolDef = {
|
|
104
|
+
symbolName: '_generic-opamp',
|
|
105
|
+
kind: 'opamp',
|
|
106
|
+
terminals: [],
|
|
107
|
+
valueProperty: null,
|
|
108
|
+
modelFromValue: false,
|
|
109
|
+
modelFromSymbolPath: true,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export function normalizeLtspiceSymbolName(symbolName: string): string {
|
|
113
|
+
const pathParts = symbolName.replaceAll('\\', '/').split('/');
|
|
114
|
+
const base = pathParts[pathParts.length - 1] ?? symbolName;
|
|
115
|
+
return base.replace(/\.asy$/i, '').toLowerCase();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Returns the model name from a path-style symbol identifier, preserving the
|
|
119
|
+
// original case. e.g. "Opamps\\LM308" -> "LM308", "OpAmps/AD820.asy" -> "AD820".
|
|
120
|
+
export function extractModelFromSymbolPath(symbolName: string): string {
|
|
121
|
+
const parts = symbolName.replaceAll('\\', '/').split('/');
|
|
122
|
+
const last = parts[parts.length - 1] ?? symbolName;
|
|
123
|
+
return last.replace(/\.asy$/i, '');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isOpampPath(symbolName: string): boolean {
|
|
127
|
+
const normalized = symbolName.replaceAll('\\', '/').toLowerCase();
|
|
128
|
+
return /(^|\/)opamps\//.test(normalized);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function lookupLtspiceSymbolDef(symbolName: string): LtspiceSymbolDef | undefined {
|
|
132
|
+
const direct = BY_SYMBOL.get(normalizeLtspiceSymbolName(symbolName));
|
|
133
|
+
if (direct !== undefined) {
|
|
134
|
+
return direct;
|
|
135
|
+
}
|
|
136
|
+
if (isOpampPath(symbolName)) {
|
|
137
|
+
return GENERIC_OPAMP_DEF;
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function mapLtspiceTerminal(local: Point, placement: Point, orientation: string): Point {
|
|
143
|
+
// Scale the per-symbol local offset; placement is scaled upstream by the parser.
|
|
144
|
+
const sx = local.x * LTSPICE_COORD_SCALE;
|
|
145
|
+
const sy = local.y * LTSPICE_COORD_SCALE;
|
|
146
|
+
const mirrored = orientation.toUpperCase().startsWith('M');
|
|
147
|
+
const degrees = parseOrientationDegrees(orientation);
|
|
148
|
+
const x = mirrored ? -sx : sx;
|
|
149
|
+
const y = sy;
|
|
150
|
+
|
|
151
|
+
// LTspice's R90 rotates the symbol 90° clockwise on screen. Because the
|
|
152
|
+
// screen y axis points down in LTspice, on-screen-CW corresponds to the
|
|
153
|
+
// math transform (x, y) → (-y, x), and R270 (CCW on screen) is (y, -x).
|
|
154
|
+
// The previous case 90 / case 270 had these swapped — every R90 component
|
|
155
|
+
// ended up with its pins on the wrong side, so wires "floated" away from
|
|
156
|
+
// terminals. Verified against the cushychicken corpus.
|
|
157
|
+
switch (degrees) {
|
|
158
|
+
case 0:
|
|
159
|
+
return { x: placement.x + x, y: placement.y + y };
|
|
160
|
+
case 90:
|
|
161
|
+
return { x: placement.x - y, y: placement.y + x };
|
|
162
|
+
case 180:
|
|
163
|
+
return { x: placement.x - x, y: placement.y - y };
|
|
164
|
+
case 270:
|
|
165
|
+
return { x: placement.x + y, y: placement.y - x };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseOrientationDegrees(orientation: string): 0 | 90 | 180 | 270 {
|
|
170
|
+
const match = orientation.toUpperCase().match(/[MR](0|90|180|270)$/);
|
|
171
|
+
if (match?.[1] === '90') {
|
|
172
|
+
return 90;
|
|
173
|
+
}
|
|
174
|
+
if (match?.[1] === '180') {
|
|
175
|
+
return 180;
|
|
176
|
+
}
|
|
177
|
+
if (match?.[1] === '270') {
|
|
178
|
+
return 270;
|
|
179
|
+
}
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// LTspice writes .asc files in Windows-1252 on Windows builds, so lone bytes like
|
|
2
|
+
// 0xB5 (µ) appear that aren't valid UTF-8. Strict UTF-8 decoding throws on those;
|
|
3
|
+
// fall back to Windows-1252 (a superset of Latin-1 within the printable range we
|
|
4
|
+
// care about). This is the same heuristic many cross-platform parsers use.
|
|
5
|
+
export function decodeLtspiceBytes(bytes: Uint8Array): string {
|
|
6
|
+
try {
|
|
7
|
+
return decodeUtf8Strict(bytes);
|
|
8
|
+
} catch {
|
|
9
|
+
return decodeWindows1252(bytes);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const WINDOWS_1252_CONTROL_CODEPOINTS: readonly number[] = [
|
|
14
|
+
0x20ac,
|
|
15
|
+
0x0081,
|
|
16
|
+
0x201a,
|
|
17
|
+
0x0192,
|
|
18
|
+
0x201e,
|
|
19
|
+
0x2026,
|
|
20
|
+
0x2020,
|
|
21
|
+
0x2021,
|
|
22
|
+
0x02c6,
|
|
23
|
+
0x2030,
|
|
24
|
+
0x0160,
|
|
25
|
+
0x2039,
|
|
26
|
+
0x0152,
|
|
27
|
+
0x008d,
|
|
28
|
+
0x017d,
|
|
29
|
+
0x008f,
|
|
30
|
+
0x0090,
|
|
31
|
+
0x2018,
|
|
32
|
+
0x2019,
|
|
33
|
+
0x201c,
|
|
34
|
+
0x201d,
|
|
35
|
+
0x2022,
|
|
36
|
+
0x2013,
|
|
37
|
+
0x2014,
|
|
38
|
+
0x02dc,
|
|
39
|
+
0x2122,
|
|
40
|
+
0x0161,
|
|
41
|
+
0x203a,
|
|
42
|
+
0x0153,
|
|
43
|
+
0x009d,
|
|
44
|
+
0x017e,
|
|
45
|
+
0x0178,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function decodeWindows1252(bytes: Uint8Array): string {
|
|
49
|
+
const codePoints: number[] = [];
|
|
50
|
+
|
|
51
|
+
for (const byte of bytes) {
|
|
52
|
+
if (byte >= 0x80 && byte <= 0x9f) {
|
|
53
|
+
codePoints.push(WINDOWS_1252_CONTROL_CODEPOINTS[byte - 0x80] ?? byte);
|
|
54
|
+
} else {
|
|
55
|
+
codePoints.push(byte);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return codePointsToString(codePoints);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function decodeUtf8Strict(bytes: Uint8Array): string {
|
|
63
|
+
const codePoints: number[] = [];
|
|
64
|
+
|
|
65
|
+
for (let index = 0; index < bytes.length; ) {
|
|
66
|
+
const first = requireByte(bytes, index);
|
|
67
|
+
|
|
68
|
+
if (first <= 0x7f) {
|
|
69
|
+
codePoints.push(first);
|
|
70
|
+
index += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (first >= 0xc2 && first <= 0xdf) {
|
|
75
|
+
const second = continuationBits(requireByte(bytes, index + 1));
|
|
76
|
+
codePoints.push(((first & 0x1f) << 6) | second);
|
|
77
|
+
index += 2;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (first >= 0xe0 && first <= 0xef) {
|
|
82
|
+
const secondByte = requireByte(bytes, index + 1);
|
|
83
|
+
const second = continuationBits(secondByte);
|
|
84
|
+
const third = continuationBits(requireByte(bytes, index + 2));
|
|
85
|
+
|
|
86
|
+
if (first === 0xe0 && secondByte < 0xa0) {
|
|
87
|
+
throw new Error('Overlong UTF-8 sequence');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (first === 0xed && secondByte >= 0xa0) {
|
|
91
|
+
throw new Error('UTF-8 surrogate sequence');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
codePoints.push(((first & 0x0f) << 12) | (second << 6) | third);
|
|
95
|
+
index += 3;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (first >= 0xf0 && first <= 0xf4) {
|
|
100
|
+
const secondByte = requireByte(bytes, index + 1);
|
|
101
|
+
const second = continuationBits(secondByte);
|
|
102
|
+
const third = continuationBits(requireByte(bytes, index + 2));
|
|
103
|
+
const fourth = continuationBits(requireByte(bytes, index + 3));
|
|
104
|
+
|
|
105
|
+
if (first === 0xf0 && secondByte < 0x90) {
|
|
106
|
+
throw new Error('Overlong UTF-8 sequence');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (first === 0xf4 && secondByte > 0x8f) {
|
|
110
|
+
throw new Error('UTF-8 code point out of range');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
codePoints.push(
|
|
114
|
+
((first & 0x07) << 18) | (second << 12) | (third << 6) | fourth,
|
|
115
|
+
);
|
|
116
|
+
index += 4;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new Error('Invalid UTF-8 sequence');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return codePointsToString(codePoints);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function requireByte(bytes: Uint8Array, index: number): number {
|
|
127
|
+
const byte = bytes[index];
|
|
128
|
+
if (byte === undefined) {
|
|
129
|
+
throw new Error('Truncated byte sequence');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return byte;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function continuationBits(byte: number): number {
|
|
136
|
+
if ((byte & 0xc0) !== 0x80) {
|
|
137
|
+
throw new Error('Invalid UTF-8 continuation byte');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return byte & 0x3f;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function codePointsToString(codePoints: readonly number[]): string {
|
|
144
|
+
const chunks: string[] = [];
|
|
145
|
+
|
|
146
|
+
for (let index = 0; index < codePoints.length; index += 8192) {
|
|
147
|
+
chunks.push(String.fromCodePoint(...codePoints.slice(index, index + 8192)));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return chunks.join('');
|
|
151
|
+
}
|