@schemd/core 0.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/LICENSE +21 -0
- package/README.md +726 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +7 -0
- package/dist/layout.d.ts +151 -0
- package/dist/layout.js +592 -0
- package/dist/limits.d.ts +31 -0
- package/dist/limits.js +25 -0
- package/dist/marked-extension.d.ts +23 -0
- package/dist/marked-extension.js +75 -0
- package/dist/math-label.d.ts +50 -0
- package/dist/math-label.js +151 -0
- package/dist/parser.d.ts +56 -0
- package/dist/parser.js +598 -0
- package/dist/renderer.d.ts +44 -0
- package/dist/renderer.js +485 -0
- package/dist/types.d.ts +248 -0
- package/dist/types.js +25 -0
- package/package.json +56 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import { ANALOG_KINDS, CLASSICAL_GATE_KINDS, COMPONENT_KINDS, DIODE_TYPES, GROUND_STYLES, PASSIVE_KINDS, SEMANTIC_COLORS, SCHEMATIC_SIGNAL_MARKERS, TRANSISTOR_TYPES, SchematicSyntaxError } from './types.js';
|
|
2
|
+
import { validateDocumentGeometry } from './layout.js';
|
|
3
|
+
import { MAX_SCHEMATIC_COMPONENTS, MAX_SCHEMATIC_CONNECTIONS, MAX_SCHEMATIC_SOURCE_CHARACTERS } from './limits.js';
|
|
4
|
+
const COMPONENT_PATTERN = /^([A-Za-z][A-Za-z0-9_-]*):([A-Za-z][A-Za-z0-9_-]*)\s+"([^"]+)"\s+at\s+\((-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)\)\s+(.+)$/;
|
|
5
|
+
const CONNECTION_PATTERN = /^([A-Za-z][A-Za-z0-9_-]*)\.([A-Za-z][A-Za-z0-9_-]*)\s*->\s*([A-Za-z][A-Za-z0-9_-]*)\.([A-Za-z][A-Za-z0-9_-]*)\s+(.+)$/;
|
|
6
|
+
const SCHEMD_FENCE_PATTERN = /^schemd\s+bounds="(\d+)x(\d+)"(?:\s+title="([^"]+)")?\s*$/i;
|
|
7
|
+
const NUMBER_PATTERN = /^[+-]?(?:\d+\.?\d*|\.\d+)$/;
|
|
8
|
+
const ANGLE_PATTERN = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:deg|grad|rad|turn)?$/i;
|
|
9
|
+
const ALIAS_PATTERN = /^[a-z][a-z0-9-]{0,63}$/i;
|
|
10
|
+
const PIN_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
11
|
+
const RESERVED_IC_PIN_NAMES = new Set(['in', 'out']);
|
|
12
|
+
const MAX_IC_PINS_PER_SIDE = 64;
|
|
13
|
+
const IC_MINIMUM_WIDTH = 88;
|
|
14
|
+
const IC_MINIMUM_HEIGHT = 64;
|
|
15
|
+
const IC_HORIZONTAL_PIN_SPACING = 22;
|
|
16
|
+
const IC_VERTICAL_PIN_SPACING = 18;
|
|
17
|
+
const IC_BODY_PADDING = 24;
|
|
18
|
+
const MAX_FENCE_TITLE_LENGTH = 512;
|
|
19
|
+
const parsedDocuments = new WeakSet();
|
|
20
|
+
function freezeParsedDocument(document) {
|
|
21
|
+
for (const component of document.components) {
|
|
22
|
+
Object.freeze(component.color);
|
|
23
|
+
if (component.kind === 'ic') {
|
|
24
|
+
Object.freeze(component.pins.left);
|
|
25
|
+
Object.freeze(component.pins.right);
|
|
26
|
+
Object.freeze(component.pins.top);
|
|
27
|
+
Object.freeze(component.pins.bottom);
|
|
28
|
+
Object.freeze(component.pins);
|
|
29
|
+
}
|
|
30
|
+
Object.freeze(component);
|
|
31
|
+
}
|
|
32
|
+
for (const connection of document.connections) {
|
|
33
|
+
Object.freeze(connection.from);
|
|
34
|
+
Object.freeze(connection.to);
|
|
35
|
+
Object.freeze(connection.color);
|
|
36
|
+
Object.freeze(connection);
|
|
37
|
+
}
|
|
38
|
+
Object.freeze(document.components);
|
|
39
|
+
Object.freeze(document.connections);
|
|
40
|
+
Object.freeze(document);
|
|
41
|
+
parsedDocuments.add(document);
|
|
42
|
+
return document;
|
|
43
|
+
}
|
|
44
|
+
export function assertParsedSchematicDocument(document) {
|
|
45
|
+
if (!parsedDocuments.has(document)) {
|
|
46
|
+
throw new TypeError('renderSchematic requires an immutable document returned by parseSchematic.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function includesValue(values, value) {
|
|
50
|
+
return values.includes(value);
|
|
51
|
+
}
|
|
52
|
+
function parseRangedNumber(value, minimum, maximum, allowPercent) {
|
|
53
|
+
const percent = allowPercent && value.endsWith('%');
|
|
54
|
+
const numeric = percent ? value.slice(0, -1) : value;
|
|
55
|
+
if (!NUMBER_PATTERN.test(numeric))
|
|
56
|
+
return false;
|
|
57
|
+
const parsed = Number(numeric);
|
|
58
|
+
return parsed >= minimum && parsed <= (percent ? 100 : maximum);
|
|
59
|
+
}
|
|
60
|
+
function parseAlpha(value) {
|
|
61
|
+
return parseRangedNumber(value, 0, 1, true);
|
|
62
|
+
}
|
|
63
|
+
function parseRgb(body) {
|
|
64
|
+
const commaSyntax = body.includes(',');
|
|
65
|
+
const slashParts = body.split('/').map((part) => part.trim());
|
|
66
|
+
if (slashParts.length > 2 || (commaSyntax && slashParts.length > 1))
|
|
67
|
+
return undefined;
|
|
68
|
+
const parsedChannels = commaSyntax
|
|
69
|
+
? slashParts[0].split(',').map((part) => part.trim())
|
|
70
|
+
: slashParts[0].split(/\s+/);
|
|
71
|
+
const legacyAlpha = commaSyntax && parsedChannels.length === 4 ? parsedChannels[3] : undefined;
|
|
72
|
+
const rawChannels = legacyAlpha === undefined ? parsedChannels : parsedChannels.slice(0, 3);
|
|
73
|
+
const alpha = slashParts.at(1) ?? legacyAlpha;
|
|
74
|
+
if (rawChannels.length !== 3 ||
|
|
75
|
+
!rawChannels.every((channel) => parseRangedNumber(channel, 0, 255, true)) ||
|
|
76
|
+
(alpha !== undefined && !parseAlpha(alpha))) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return `rgb(${rawChannels.join(' ')}${alpha === undefined ? '' : ` / ${alpha}`})`;
|
|
80
|
+
}
|
|
81
|
+
function parseHsl(body) {
|
|
82
|
+
const commaSyntax = body.includes(',');
|
|
83
|
+
const slashParts = body.split('/').map((part) => part.trim());
|
|
84
|
+
if (slashParts.length > 2 || (commaSyntax && slashParts.length > 1))
|
|
85
|
+
return undefined;
|
|
86
|
+
const parsedChannels = commaSyntax
|
|
87
|
+
? slashParts[0].split(',').map((part) => part.trim())
|
|
88
|
+
: slashParts[0].split(/\s+/);
|
|
89
|
+
const legacyAlpha = commaSyntax && parsedChannels.length === 4 ? parsedChannels[3] : undefined;
|
|
90
|
+
const rawChannels = legacyAlpha === undefined ? parsedChannels : parsedChannels.slice(0, 3);
|
|
91
|
+
const alpha = slashParts.at(1) ?? legacyAlpha;
|
|
92
|
+
const [hue, saturation, lightness] = rawChannels;
|
|
93
|
+
if (rawChannels.length !== 3 ||
|
|
94
|
+
hue === undefined ||
|
|
95
|
+
!ANGLE_PATTERN.test(hue) ||
|
|
96
|
+
saturation === undefined ||
|
|
97
|
+
!saturation.endsWith('%') ||
|
|
98
|
+
!parseRangedNumber(saturation, 0, 100, true) ||
|
|
99
|
+
lightness === undefined ||
|
|
100
|
+
!lightness.endsWith('%') ||
|
|
101
|
+
!parseRangedNumber(lightness, 0, 100, true) ||
|
|
102
|
+
(alpha !== undefined && !parseAlpha(alpha))) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return `hsl(${rawChannels.join(' ')}${alpha === undefined ? '' : ` / ${alpha}`})`;
|
|
106
|
+
}
|
|
107
|
+
export function parseSchematicColor(input, line = 1) {
|
|
108
|
+
const value = input.trim();
|
|
109
|
+
const tokenCandidate = (value.startsWith('#') ? value.slice(1) : value).toLowerCase();
|
|
110
|
+
if (includesValue(SEMANTIC_COLORS, tokenCandidate)) {
|
|
111
|
+
return { kind: 'token', value: tokenCandidate };
|
|
112
|
+
}
|
|
113
|
+
if (/^#[A-Fa-f0-9]+$/.test(value)) {
|
|
114
|
+
if (![4, 5, 7, 9].includes(value.length)) {
|
|
115
|
+
throw new SchematicSyntaxError('Hex colors require 3, 4, 6, or 8 digits.', line);
|
|
116
|
+
}
|
|
117
|
+
return { kind: 'css', value: value.toLowerCase() };
|
|
118
|
+
}
|
|
119
|
+
const functionMatch = value.match(/^(rgb|rgba|hsl|hsla)\((.*)\)$/i);
|
|
120
|
+
if (functionMatch) {
|
|
121
|
+
const functionName = functionMatch[1].toLowerCase();
|
|
122
|
+
const body = functionMatch[2];
|
|
123
|
+
const normalized = functionName.startsWith('rgb') ? parseRgb(body) : parseHsl(body);
|
|
124
|
+
if (!normalized) {
|
|
125
|
+
throw new SchematicSyntaxError(`Invalid ${functionName} color.`, line);
|
|
126
|
+
}
|
|
127
|
+
return { kind: 'css', value: normalized };
|
|
128
|
+
}
|
|
129
|
+
const alias = value.startsWith('#') ? value.slice(1) : value;
|
|
130
|
+
if (ALIAS_PATTERN.test(alias))
|
|
131
|
+
return { kind: 'alias', value: alias.toLowerCase() };
|
|
132
|
+
throw new SchematicSyntaxError('Unsafe or unsupported color expression.', line);
|
|
133
|
+
}
|
|
134
|
+
function parseAttributes(raw, line) {
|
|
135
|
+
const attributes = new Map();
|
|
136
|
+
if (raw === undefined)
|
|
137
|
+
return attributes;
|
|
138
|
+
if (raw.trim() === '')
|
|
139
|
+
throw new SchematicSyntaxError('Malformed component options.', line);
|
|
140
|
+
const source = raw.trim();
|
|
141
|
+
let cursor = 0;
|
|
142
|
+
while (cursor < source.length) {
|
|
143
|
+
if (cursor > 0) {
|
|
144
|
+
const separator = source.slice(cursor).match(/^\s+/);
|
|
145
|
+
if (!separator)
|
|
146
|
+
throw new SchematicSyntaxError('Malformed component options.', line);
|
|
147
|
+
cursor += separator[0].length;
|
|
148
|
+
}
|
|
149
|
+
const keyMatch = source.slice(cursor).match(/^([a-z][a-z0-9-]*)=/);
|
|
150
|
+
if (!keyMatch) {
|
|
151
|
+
throw new SchematicSyntaxError('Malformed component options.', line);
|
|
152
|
+
}
|
|
153
|
+
const key = keyMatch[1];
|
|
154
|
+
cursor += keyMatch[0].length;
|
|
155
|
+
let value;
|
|
156
|
+
if (source[cursor] === '"') {
|
|
157
|
+
const closingQuote = source.indexOf('"', cursor + 1);
|
|
158
|
+
value = source.slice(cursor + 1, closingQuote);
|
|
159
|
+
cursor = closingQuote + 1;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const valueMatch = source.slice(cursor).match(/^[^\s]+/);
|
|
163
|
+
if (!valueMatch)
|
|
164
|
+
throw new SchematicSyntaxError('Malformed component options.', line);
|
|
165
|
+
value = valueMatch[0];
|
|
166
|
+
cursor += value.length;
|
|
167
|
+
}
|
|
168
|
+
if (attributes.has(key))
|
|
169
|
+
throw new SchematicSyntaxError(`Duplicate option ${key}.`, line);
|
|
170
|
+
attributes.set(key, value);
|
|
171
|
+
}
|
|
172
|
+
return attributes;
|
|
173
|
+
}
|
|
174
|
+
function assertOnlyAttributes(attributes, allowed, line) {
|
|
175
|
+
for (const key of attributes.keys()) {
|
|
176
|
+
if (!allowed.includes(key))
|
|
177
|
+
throw new SchematicSyntaxError(`Option ${key} is not supported.`, line);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function parseCount(value, fallback, name, line) {
|
|
181
|
+
if (value === undefined)
|
|
182
|
+
return fallback;
|
|
183
|
+
if (!/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 32) {
|
|
184
|
+
throw new SchematicSyntaxError(`${name} must be an integer from 1 through 32.`, line);
|
|
185
|
+
}
|
|
186
|
+
return Number(value);
|
|
187
|
+
}
|
|
188
|
+
function splitDeclarationTail(raw, line, allowMissingColor = false) {
|
|
189
|
+
const source = raw.trim();
|
|
190
|
+
let quoteOpen = false;
|
|
191
|
+
let parentheses = 0;
|
|
192
|
+
let optionsStart = -1;
|
|
193
|
+
let optionsEnd = -1;
|
|
194
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
195
|
+
const character = source[index];
|
|
196
|
+
if (character === '"') {
|
|
197
|
+
quoteOpen = !quoteOpen;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (quoteOpen)
|
|
201
|
+
continue;
|
|
202
|
+
if (character === '(') {
|
|
203
|
+
parentheses += 1;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (character === ')') {
|
|
207
|
+
parentheses -= 1;
|
|
208
|
+
if (parentheses < 0)
|
|
209
|
+
throw new SchematicSyntaxError('Malformed declaration tail.', line);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (character === '[' && parentheses === 0) {
|
|
213
|
+
if (optionsStart >= 0 || (index > 0 && !/\s/.test(source[index - 1]))) {
|
|
214
|
+
throw new SchematicSyntaxError('Malformed declaration options.', line);
|
|
215
|
+
}
|
|
216
|
+
optionsStart = index;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (character === ']' && parentheses === 0) {
|
|
220
|
+
if (optionsStart < 0 || optionsEnd >= 0) {
|
|
221
|
+
throw new SchematicSyntaxError('Malformed declaration options.', line);
|
|
222
|
+
}
|
|
223
|
+
optionsEnd = index;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (quoteOpen || parentheses !== 0) {
|
|
227
|
+
throw new SchematicSyntaxError('Malformed declaration tail.', line);
|
|
228
|
+
}
|
|
229
|
+
if (optionsStart < 0) {
|
|
230
|
+
return { color: source, options: undefined };
|
|
231
|
+
}
|
|
232
|
+
if (optionsEnd < optionsStart || source.slice(optionsEnd + 1).trim() !== '') {
|
|
233
|
+
throw new SchematicSyntaxError('Malformed declaration options.', line);
|
|
234
|
+
}
|
|
235
|
+
const color = source.slice(0, optionsStart).trim();
|
|
236
|
+
if (color === '' && !allowMissingColor) {
|
|
237
|
+
throw new SchematicSyntaxError('A declaration color is required.', line);
|
|
238
|
+
}
|
|
239
|
+
return { color, options: source.slice(optionsStart + 1, optionsEnd) };
|
|
240
|
+
}
|
|
241
|
+
function parseEnumOption(value, fallback, values, name, line) {
|
|
242
|
+
if (value === undefined)
|
|
243
|
+
return fallback;
|
|
244
|
+
if (!includesValue(values, value)) {
|
|
245
|
+
throw new SchematicSyntaxError(`${name} must be one of: ${values.join(', ')}.`, line);
|
|
246
|
+
}
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
function parseIcPins(attributes, line) {
|
|
250
|
+
const registered = new Set();
|
|
251
|
+
const parseSide = (side) => {
|
|
252
|
+
const source = attributes.get(side);
|
|
253
|
+
if (source === undefined || source.trim() === '')
|
|
254
|
+
return [];
|
|
255
|
+
const pins = source.split(',').map((pin) => pin.trim());
|
|
256
|
+
if (pins.length > MAX_IC_PINS_PER_SIDE) {
|
|
257
|
+
throw new SchematicSyntaxError(`${side} supports at most ${MAX_IC_PINS_PER_SIDE} IC pins.`, line);
|
|
258
|
+
}
|
|
259
|
+
for (const pin of pins) {
|
|
260
|
+
if (!PIN_NAME_PATTERN.test(pin)) {
|
|
261
|
+
throw new SchematicSyntaxError(`Invalid IC pin name ${pin || '(empty)'}.`, line);
|
|
262
|
+
}
|
|
263
|
+
if (RESERVED_IC_PIN_NAMES.has(pin)) {
|
|
264
|
+
throw new SchematicSyntaxError(`IC pin ${pin} collides with the reserved ${pin} alias. Use ${pin}1 or another pin name.`, line);
|
|
265
|
+
}
|
|
266
|
+
if (registered.has(pin)) {
|
|
267
|
+
throw new SchematicSyntaxError(`Duplicate IC pin ${pin}.`, line);
|
|
268
|
+
}
|
|
269
|
+
registered.add(pin);
|
|
270
|
+
}
|
|
271
|
+
return pins;
|
|
272
|
+
};
|
|
273
|
+
const pins = {
|
|
274
|
+
left: parseSide('left'),
|
|
275
|
+
right: parseSide('right'),
|
|
276
|
+
top: parseSide('top'),
|
|
277
|
+
bottom: parseSide('bottom')
|
|
278
|
+
};
|
|
279
|
+
if (registered.size === 0) {
|
|
280
|
+
throw new SchematicSyntaxError('An IC must declare at least one pin.', line);
|
|
281
|
+
}
|
|
282
|
+
return pins;
|
|
283
|
+
}
|
|
284
|
+
function integratedCircuitDimensions(pins) {
|
|
285
|
+
return {
|
|
286
|
+
bodyWidth: Math.max(IC_MINIMUM_WIDTH, Math.max(pins.top.length, pins.bottom.length) * IC_HORIZONTAL_PIN_SPACING + IC_BODY_PADDING),
|
|
287
|
+
bodyHeight: Math.max(IC_MINIMUM_HEIGHT, Math.max(pins.left.length, pins.right.length) * IC_VERTICAL_PIN_SPACING + IC_BODY_PADDING)
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function commonComponent(match, color, line) {
|
|
291
|
+
return {
|
|
292
|
+
id: match[2],
|
|
293
|
+
label: match[3],
|
|
294
|
+
x: Number(match[4]),
|
|
295
|
+
y: Number(match[5]),
|
|
296
|
+
color: parseSchematicColor(color, line),
|
|
297
|
+
line
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function parseComponent(match, line) {
|
|
301
|
+
const rawKind = match[1].toLowerCase();
|
|
302
|
+
if (!includesValue(COMPONENT_KINDS, rawKind)) {
|
|
303
|
+
throw new SchematicSyntaxError(`Unsupported component kind ${match[1]}.`, line);
|
|
304
|
+
}
|
|
305
|
+
const kind = rawKind;
|
|
306
|
+
const tail = splitDeclarationTail(match[6], line, kind === 'ic');
|
|
307
|
+
const attributes = parseAttributes(tail.options, line);
|
|
308
|
+
const common = commonComponent(match, tail.color === '' ? 'slate' : tail.color, line);
|
|
309
|
+
if (includesValue(PASSIVE_KINDS, kind)) {
|
|
310
|
+
assertOnlyAttributes(attributes, [], line);
|
|
311
|
+
return { kind, ...common };
|
|
312
|
+
}
|
|
313
|
+
if (includesValue(ANALOG_KINDS, kind)) {
|
|
314
|
+
switch (kind) {
|
|
315
|
+
case 'diode':
|
|
316
|
+
assertOnlyAttributes(attributes, ['type'], line);
|
|
317
|
+
return {
|
|
318
|
+
kind,
|
|
319
|
+
...common,
|
|
320
|
+
diodeType: parseEnumOption(attributes.get('type'), 'standard', DIODE_TYPES, 'type', line)
|
|
321
|
+
};
|
|
322
|
+
case 'transistor':
|
|
323
|
+
assertOnlyAttributes(attributes, ['type'], line);
|
|
324
|
+
return {
|
|
325
|
+
kind,
|
|
326
|
+
...common,
|
|
327
|
+
transistorType: parseEnumOption(attributes.get('type'), 'npn', TRANSISTOR_TYPES, 'type', line)
|
|
328
|
+
};
|
|
329
|
+
case 'port':
|
|
330
|
+
assertOnlyAttributes(attributes, [], line);
|
|
331
|
+
return { kind, ...common };
|
|
332
|
+
case 'ground':
|
|
333
|
+
assertOnlyAttributes(attributes, ['style'], line);
|
|
334
|
+
return {
|
|
335
|
+
kind,
|
|
336
|
+
...common,
|
|
337
|
+
groundStyle: parseEnumOption(attributes.get('style'), 'signal', GROUND_STYLES, 'style', line)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (includesValue(CLASSICAL_GATE_KINDS, kind)) {
|
|
342
|
+
assertOnlyAttributes(attributes, ['inputs', 'outputs', 'standard'], line);
|
|
343
|
+
const standard = attributes.get('standard') ?? 'ieee';
|
|
344
|
+
if (standard !== 'ieee' && standard !== 'iec') {
|
|
345
|
+
throw new SchematicSyntaxError('standard must be ieee or iec.', line);
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
kind,
|
|
349
|
+
...common,
|
|
350
|
+
inputs: parseCount(attributes.get('inputs'), kind === 'not' ? 1 : 2, 'inputs', line),
|
|
351
|
+
outputs: parseCount(attributes.get('outputs'), 1, 'outputs', line),
|
|
352
|
+
standard
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (kind === 'ic') {
|
|
356
|
+
assertOnlyAttributes(attributes, ['left', 'right', 'top', 'bottom'], line);
|
|
357
|
+
const pins = parseIcPins(attributes, line);
|
|
358
|
+
return {
|
|
359
|
+
kind,
|
|
360
|
+
...common,
|
|
361
|
+
pins,
|
|
362
|
+
...integratedCircuitDimensions(pins)
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const quantumKind = kind;
|
|
366
|
+
assertOnlyAttributes(attributes, quantumKind === 'qgate' ? ['parameter', 'matrix', 'phase'] : [], line);
|
|
367
|
+
const component = { kind: quantumKind, ...common };
|
|
368
|
+
const parameter = attributes.get('parameter');
|
|
369
|
+
const matrix = attributes.get('matrix');
|
|
370
|
+
const phase = attributes.get('phase');
|
|
371
|
+
if (parameter !== undefined)
|
|
372
|
+
component.parameter = parameter;
|
|
373
|
+
if (matrix !== undefined)
|
|
374
|
+
component.matrix = matrix;
|
|
375
|
+
if (phase !== undefined)
|
|
376
|
+
component.phase = phase;
|
|
377
|
+
return component;
|
|
378
|
+
}
|
|
379
|
+
function parseEndpoint(componentId, port) {
|
|
380
|
+
return { componentId, port };
|
|
381
|
+
}
|
|
382
|
+
function parseSignalMarker(value, option, line) {
|
|
383
|
+
if (!includesValue(SCHEMATIC_SIGNAL_MARKERS, value)) {
|
|
384
|
+
throw new SchematicSyntaxError(`${option} must be one of: ${SCHEMATIC_SIGNAL_MARKERS.join(', ')}.`, line);
|
|
385
|
+
}
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
function parseConnectionOptions(raw, line) {
|
|
389
|
+
let curve = 'line';
|
|
390
|
+
let markerStart = 'none';
|
|
391
|
+
let markerEnd = 'none';
|
|
392
|
+
if (raw === undefined || raw.trim() === '')
|
|
393
|
+
return { curve, markerStart, markerEnd };
|
|
394
|
+
const seen = new Set();
|
|
395
|
+
for (const token of raw.trim().split(/\s+/)) {
|
|
396
|
+
if (token === 'line' || token === 'bezier' || token === 'ortho') {
|
|
397
|
+
if (seen.has('curve')) {
|
|
398
|
+
throw new SchematicSyntaxError('Connection routing can only be declared once.', line);
|
|
399
|
+
}
|
|
400
|
+
curve = token;
|
|
401
|
+
seen.add('curve');
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (token === 'arrow' || token === 'dot') {
|
|
405
|
+
if (seen.has('marker-end')) {
|
|
406
|
+
throw new SchematicSyntaxError('Connection marker-end can only be declared once.', line);
|
|
407
|
+
}
|
|
408
|
+
markerEnd = token;
|
|
409
|
+
seen.add('marker-end');
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const match = token.match(/^(marker-start|marker-end)=(\S+)$/);
|
|
413
|
+
if (!match) {
|
|
414
|
+
throw new SchematicSyntaxError('Connection routing options support line, bezier, ortho, arrow, dot, marker-start, and marker-end.', line);
|
|
415
|
+
}
|
|
416
|
+
const option = match[1];
|
|
417
|
+
if (seen.has(option)) {
|
|
418
|
+
throw new SchematicSyntaxError(`Connection ${option} can only be declared once.`, line);
|
|
419
|
+
}
|
|
420
|
+
const marker = parseSignalMarker(match[2], option, line);
|
|
421
|
+
if (option === 'marker-start')
|
|
422
|
+
markerStart = marker;
|
|
423
|
+
else
|
|
424
|
+
markerEnd = marker;
|
|
425
|
+
seen.add(option);
|
|
426
|
+
}
|
|
427
|
+
return { curve, markerStart, markerEnd };
|
|
428
|
+
}
|
|
429
|
+
function parseConnection(match, line) {
|
|
430
|
+
const tail = splitDeclarationTail(match[5], line);
|
|
431
|
+
const options = parseConnectionOptions(tail.options, line);
|
|
432
|
+
return {
|
|
433
|
+
from: parseEndpoint(match[1], match[2]),
|
|
434
|
+
to: parseEndpoint(match[3], match[4]),
|
|
435
|
+
color: parseSchematicColor(tail.color, line),
|
|
436
|
+
curve: options.curve,
|
|
437
|
+
markerStart: options.markerStart,
|
|
438
|
+
markerEnd: options.markerEnd,
|
|
439
|
+
line
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function validateComponent(component, fence) {
|
|
443
|
+
if (component.x < 0 ||
|
|
444
|
+
component.x > fence.bounds.width ||
|
|
445
|
+
component.y < 0 ||
|
|
446
|
+
component.y > fence.bounds.height) {
|
|
447
|
+
throw new SchematicSyntaxError(`${component.id} is outside the declared ${fence.bounds.width}x${fence.bounds.height} bounds.`, component.line);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function validGatePort(component, port) {
|
|
451
|
+
if (port === 'in' || port === 'out')
|
|
452
|
+
return true;
|
|
453
|
+
const match = port.match(/^(in|out)([1-9]\d*)$/);
|
|
454
|
+
if (!match)
|
|
455
|
+
return false;
|
|
456
|
+
const index = Number(match[2]);
|
|
457
|
+
return match[1] === 'in'
|
|
458
|
+
? index <= component.inputs && index >= 1
|
|
459
|
+
: index <= component.outputs && index >= 1;
|
|
460
|
+
}
|
|
461
|
+
function isClassicalGate(component) {
|
|
462
|
+
return CLASSICAL_GATE_KINDS.includes(component.kind);
|
|
463
|
+
}
|
|
464
|
+
function validIcPort(component, port) {
|
|
465
|
+
const sides = Object.values(component.pins);
|
|
466
|
+
if (port === 'in') {
|
|
467
|
+
return (sides.some((pins) => pins.includes('in1')) ||
|
|
468
|
+
component.pins.left.length > 0 ||
|
|
469
|
+
component.pins.top.length > 0);
|
|
470
|
+
}
|
|
471
|
+
if (port === 'out') {
|
|
472
|
+
return (sides.some((pins) => pins.includes('out1')) ||
|
|
473
|
+
component.pins.right.length > 0 ||
|
|
474
|
+
component.pins.bottom.length > 0);
|
|
475
|
+
}
|
|
476
|
+
return sides.some((pins) => pins.includes(port));
|
|
477
|
+
}
|
|
478
|
+
function validateEndpoint(endpoint, components, line) {
|
|
479
|
+
const component = components.get(endpoint.componentId);
|
|
480
|
+
if (!component)
|
|
481
|
+
throw new SchematicSyntaxError(`Unknown component ${endpoint.componentId}.`, line);
|
|
482
|
+
let valid;
|
|
483
|
+
if (isClassicalGate(component)) {
|
|
484
|
+
valid = validGatePort(component, endpoint.port);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
switch (component.kind) {
|
|
488
|
+
case 'resistor':
|
|
489
|
+
case 'capacitor':
|
|
490
|
+
case 'inductor':
|
|
491
|
+
valid = ['in', 'out', 'left', 'right', 'l', 'r'].includes(endpoint.port);
|
|
492
|
+
break;
|
|
493
|
+
case 'diode':
|
|
494
|
+
valid = ['anode', 'a', 'cathode', 'k', 'c'].includes(endpoint.port);
|
|
495
|
+
break;
|
|
496
|
+
case 'transistor':
|
|
497
|
+
valid = [
|
|
498
|
+
'base',
|
|
499
|
+
'gate',
|
|
500
|
+
'b',
|
|
501
|
+
'g',
|
|
502
|
+
'collector',
|
|
503
|
+
'drain',
|
|
504
|
+
'c',
|
|
505
|
+
'd',
|
|
506
|
+
'emitter',
|
|
507
|
+
'source',
|
|
508
|
+
'e',
|
|
509
|
+
's'
|
|
510
|
+
].includes(endpoint.port);
|
|
511
|
+
break;
|
|
512
|
+
case 'port':
|
|
513
|
+
case 'hadamard':
|
|
514
|
+
case 'qgate':
|
|
515
|
+
valid = endpoint.port === 'in' || endpoint.port === 'out';
|
|
516
|
+
break;
|
|
517
|
+
case 'ground':
|
|
518
|
+
valid = endpoint.port === 'in';
|
|
519
|
+
break;
|
|
520
|
+
case 'cnot':
|
|
521
|
+
valid = ['in', 'out', 'control', 'target'].includes(endpoint.port);
|
|
522
|
+
break;
|
|
523
|
+
case 'ic':
|
|
524
|
+
valid = validIcPort(component, endpoint.port);
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (!valid) {
|
|
529
|
+
throw new SchematicSyntaxError(`Port ${endpoint.componentId}.${endpoint.port} is invalid for ${component.kind}.`, line);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
export function parseSchematicFence(info, defaultTitle = 'Engineering schematic') {
|
|
533
|
+
if (info === undefined || !/^schemd(?:\s|$)/i.test(info.trim())) {
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
const match = info.trim().match(SCHEMD_FENCE_PATTERN);
|
|
537
|
+
if (!match) {
|
|
538
|
+
throw new SchematicSyntaxError('schemd fences require: schemd bounds="WIDTHxHEIGHT" title="Optional title".');
|
|
539
|
+
}
|
|
540
|
+
const width = Number(match[1]);
|
|
541
|
+
const height = Number(match[2]);
|
|
542
|
+
if (width < 64 || height < 64 || width > 4096 || height > 4096) {
|
|
543
|
+
throw new SchematicSyntaxError('Schematic bounds must be integers from 64 through 4096.');
|
|
544
|
+
}
|
|
545
|
+
const title = match[3] ?? defaultTitle;
|
|
546
|
+
if (title.length > MAX_FENCE_TITLE_LENGTH) {
|
|
547
|
+
throw new SchematicSyntaxError('Schematic titles cannot exceed 512 characters.');
|
|
548
|
+
}
|
|
549
|
+
return { bounds: { width, height }, title };
|
|
550
|
+
}
|
|
551
|
+
export function parseSchematic(source, fence) {
|
|
552
|
+
if (source.length > MAX_SCHEMATIC_SOURCE_CHARACTERS) {
|
|
553
|
+
throw new SchematicSyntaxError('Schematic source exceeds the 131,072 character limit.');
|
|
554
|
+
}
|
|
555
|
+
const components = [];
|
|
556
|
+
const connections = [];
|
|
557
|
+
const componentIds = new Set();
|
|
558
|
+
for (const [lineIndex, rawLine] of source.replace(/\r\n?/g, '\n').split('\n').entries()) {
|
|
559
|
+
const line = rawLine.trim();
|
|
560
|
+
if (line === '' || line.startsWith('//'))
|
|
561
|
+
continue;
|
|
562
|
+
const lineNumber = lineIndex + 1;
|
|
563
|
+
const componentMatch = line.match(COMPONENT_PATTERN);
|
|
564
|
+
if (componentMatch) {
|
|
565
|
+
if (components.length >= MAX_SCHEMATIC_COMPONENTS) {
|
|
566
|
+
throw new SchematicSyntaxError('Schematic exceeds the 512 component limit.', lineNumber);
|
|
567
|
+
}
|
|
568
|
+
const component = parseComponent(componentMatch, lineNumber);
|
|
569
|
+
if (componentIds.has(component.id)) {
|
|
570
|
+
throw new SchematicSyntaxError(`Duplicate component ID ${component.id}.`, lineNumber);
|
|
571
|
+
}
|
|
572
|
+
validateComponent(component, fence);
|
|
573
|
+
componentIds.add(component.id);
|
|
574
|
+
components.push(component);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const connectionMatch = line.match(CONNECTION_PATTERN);
|
|
578
|
+
if (connectionMatch) {
|
|
579
|
+
if (connections.length >= MAX_SCHEMATIC_CONNECTIONS) {
|
|
580
|
+
throw new SchematicSyntaxError('Schematic exceeds the 2,048 connection limit.', lineNumber);
|
|
581
|
+
}
|
|
582
|
+
connections.push(parseConnection(connectionMatch, lineNumber));
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
throw new SchematicSyntaxError('Unrecognized schematic declaration.', lineNumber);
|
|
586
|
+
}
|
|
587
|
+
if (components.length === 0) {
|
|
588
|
+
throw new SchematicSyntaxError('A schematic must declare at least one component.');
|
|
589
|
+
}
|
|
590
|
+
const componentsById = new Map(components.map((component) => [component.id, component]));
|
|
591
|
+
for (const connection of connections) {
|
|
592
|
+
validateEndpoint(connection.from, componentsById, connection.line);
|
|
593
|
+
validateEndpoint(connection.to, componentsById, connection.line);
|
|
594
|
+
}
|
|
595
|
+
const document = { components, connections };
|
|
596
|
+
validateDocumentGeometry(document, fence);
|
|
597
|
+
return freezeParsedDocument(document);
|
|
598
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type CompileSchematicOptions, type SchematicDocument } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Legacy alias for the hard SVG allocation ceiling.
|
|
4
|
+
*
|
|
5
|
+
* @deprecated Prefer the root-exported `MAX_SCHEMATIC_SVG_OUTPUT_BYTES`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MAX_SVG_OUTPUT_BYTES = 2097152;
|
|
8
|
+
/**
|
|
9
|
+
* Append-only SVG sink that enforces the compiler's UTF-8 output ceiling.
|
|
10
|
+
*
|
|
11
|
+
* The writer tracks encoded byte cost incrementally without allocating an
|
|
12
|
+
* intermediate `TextEncoder` buffer. Chunks are joined only after successful
|
|
13
|
+
* completion, so partial oversized output is never returned.
|
|
14
|
+
*/
|
|
15
|
+
export declare class BoundedSvgWriter {
|
|
16
|
+
#private;
|
|
17
|
+
/**
|
|
18
|
+
* Add one trusted SVG fragment within the hard output budget.
|
|
19
|
+
*
|
|
20
|
+
* @param chunk - Compiler-generated markup fragment.
|
|
21
|
+
* @throws {SchematicSyntaxError} When the aggregate UTF-8 size exceeds the limit.
|
|
22
|
+
*/
|
|
23
|
+
append(chunk: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Join all accepted fragments into the final SVG figure.
|
|
26
|
+
*
|
|
27
|
+
* @returns Complete trusted markup in append order.
|
|
28
|
+
*/
|
|
29
|
+
finish(): string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Render a validated schematic AST as a bounded, accessible inline SVG figure.
|
|
33
|
+
*
|
|
34
|
+
* Geometry is revalidated against the supplied bounds before serialization.
|
|
35
|
+
* `default` compounds paths with no embedded styles, `embedded-css` adds theme
|
|
36
|
+
* and hover visuals, and `full` emits per-node event delegation attributes.
|
|
37
|
+
*
|
|
38
|
+
* @param document - Immutable AST returned by `parseSchematic` in this module instance.
|
|
39
|
+
* @param options - Intrinsic bounds, title, optional ID namespace, and output mode.
|
|
40
|
+
* @returns Complete trusted `<figure>` markup containing an inline SVG.
|
|
41
|
+
* @throws {TypeError} When `document` lacks parser provenance.
|
|
42
|
+
* @throws {SchematicSyntaxError} For invalid options, geometry, or output-budget overflow.
|
|
43
|
+
*/
|
|
44
|
+
export declare function renderSchematic(document: SchematicDocument, options: CompileSchematicOptions): string;
|