@schemd/core 0.1.1 → 0.1.2
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 +23 -82
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/layout.d.ts +14 -4
- package/dist/layout.js +510 -138
- package/dist/limits.d.ts +3 -0
- package/dist/limits.js +2 -0
- package/dist/math-label.d.ts +20 -3
- package/dist/math-label.js +149 -56
- package/dist/parser.js +248 -27
- package/dist/renderer.js +147 -27
- package/dist/types.d.ts +58 -3
- package/dist/types.js +33 -1
- package/package.json +33 -3
package/dist/limits.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare const MAX_SCHEMATIC_SOURCE_CHARACTERS = 131072;
|
|
|
13
13
|
export declare const MAX_SCHEMATIC_COMPONENTS = 512;
|
|
14
14
|
/** Maximum directed connections accepted in one document. */
|
|
15
15
|
export declare const MAX_SCHEMATIC_CONNECTIONS = 2048;
|
|
16
|
+
/** Maximum rendered orthogonal intersections before the crossing pass aborts. */
|
|
17
|
+
export declare const MAX_SCHEMATIC_WIRE_CROSSINGS = 32768;
|
|
16
18
|
/** Maximum UTF-8 bytes the bounded SVG writer may emit. */
|
|
17
19
|
export declare const MAX_SCHEMATIC_SVG_OUTPUT_BYTES = 2097152;
|
|
18
20
|
/** Frozen runtime-readable form of every compiler allocation ceiling. */
|
|
@@ -20,6 +22,7 @@ export declare const SCHEMATIC_LIMITS: Readonly<{
|
|
|
20
22
|
sourceCharacters: 131072;
|
|
21
23
|
components: 512;
|
|
22
24
|
connections: 2048;
|
|
25
|
+
wireCrossings: 32768;
|
|
23
26
|
svgOutputBytes: 2097152;
|
|
24
27
|
}>;
|
|
25
28
|
/**
|
package/dist/limits.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export const MAX_SCHEMATIC_SOURCE_CHARACTERS = 131_072;
|
|
2
2
|
export const MAX_SCHEMATIC_COMPONENTS = 512;
|
|
3
3
|
export const MAX_SCHEMATIC_CONNECTIONS = 2_048;
|
|
4
|
+
export const MAX_SCHEMATIC_WIRE_CROSSINGS = 32_768;
|
|
4
5
|
export const MAX_SCHEMATIC_SVG_OUTPUT_BYTES = 2_097_152;
|
|
5
6
|
export const SCHEMATIC_LIMITS = Object.freeze({
|
|
6
7
|
sourceCharacters: MAX_SCHEMATIC_SOURCE_CHARACTERS,
|
|
7
8
|
components: MAX_SCHEMATIC_COMPONENTS,
|
|
8
9
|
connections: MAX_SCHEMATIC_CONNECTIONS,
|
|
10
|
+
wireCrossings: MAX_SCHEMATIC_WIRE_CROSSINGS,
|
|
9
11
|
svgOutputBytes: MAX_SCHEMATIC_SVG_OUTPUT_BYTES
|
|
10
12
|
});
|
|
11
13
|
export function utf8ByteLength(value) {
|
package/dist/math-label.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface MathLabelSegment {
|
|
|
15
15
|
readonly kind: MathLabelSegmentKind;
|
|
16
16
|
/** Plain Unicode content with grouping delimiters removed. */
|
|
17
17
|
readonly value: string;
|
|
18
|
+
/** Absolute font scale for nested scripts; omitted for normal and first-level runs. */
|
|
19
|
+
readonly fontScale?: number;
|
|
20
|
+
/** Absolute baseline shift in parent em units for nested scripts. */
|
|
21
|
+
readonly baselineShiftEm?: number;
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
20
24
|
* Parse a deliberately small, linear-time math-label subset.
|
|
@@ -37,12 +41,25 @@ export declare function mathLabelText(value: string): string;
|
|
|
37
41
|
* @returns Unicode code-point count after command translation.
|
|
38
42
|
*/
|
|
39
43
|
export declare function mathLabelGlyphLength(value: string): number;
|
|
44
|
+
/**
|
|
45
|
+
* Estimate deterministic text advance without browser font measurement.
|
|
46
|
+
*
|
|
47
|
+
* Combining marks consume no additional width, East Asian and emoji code
|
|
48
|
+
* points consume two cells, and nested scripts retain their parsed font scale.
|
|
49
|
+
* The result deliberately overestimates ambiguous mathematical glyphs slightly
|
|
50
|
+
* so labels are fitted before they can collide with neighboring geometry.
|
|
51
|
+
*
|
|
52
|
+
* @param value - Raw micro-math label.
|
|
53
|
+
* @param cellAdvance - Width of one ordinary glyph in viewBox units.
|
|
54
|
+
* @returns Estimated rendered width in viewBox units.
|
|
55
|
+
*/
|
|
56
|
+
export declare function mathLabelTextWidth(value: string, cellAdvance?: number): number;
|
|
40
57
|
/**
|
|
41
58
|
* Emit escaped inline SVG text with explicit baseline restoration.
|
|
42
59
|
*
|
|
43
|
-
* Plain labels avoid `<tspan>` allocation entirely. Shifted segments
|
|
44
|
-
*
|
|
45
|
-
* accumulate
|
|
60
|
+
* Plain labels avoid `<tspan>` allocation entirely. Shifted segments carry
|
|
61
|
+
* absolute scale and baseline metrics; each emitted `dy` is the delta from the
|
|
62
|
+
* preceding run, so nested and consecutive scripts cannot accumulate drift.
|
|
46
63
|
*
|
|
47
64
|
* @param value - Raw micro-math label.
|
|
48
65
|
* @returns Trusted compiler-owned XML text suitable inside an SVG `<text>` node.
|
package/dist/math-label.js
CHANGED
|
@@ -43,85 +43,120 @@ function escapeXml(value) {
|
|
|
43
43
|
.replaceAll("'", ''');
|
|
44
44
|
}
|
|
45
45
|
function commandAt(value, index) {
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
text:
|
|
53
|
-
end: index + command.length + 1
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
function translateMathSymbols(value) {
|
|
57
|
-
let output = '';
|
|
58
|
-
let index = 0;
|
|
59
|
-
while (index < value.length) {
|
|
60
|
-
if (value[index] !== '\\') {
|
|
61
|
-
output += value[index];
|
|
62
|
-
index += 1;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const command = commandAt(value, index);
|
|
66
|
-
output += command.text;
|
|
67
|
-
index = command.end;
|
|
46
|
+
const escaped = value[index + 1];
|
|
47
|
+
if (escaped === '\\' ||
|
|
48
|
+
escaped === '{' ||
|
|
49
|
+
escaped === '}' ||
|
|
50
|
+
escaped === '_' ||
|
|
51
|
+
escaped === '^') {
|
|
52
|
+
return { text: escaped, end: index + 2 };
|
|
68
53
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return { value: translateMathSymbols(character), end: index + 1 + character.length };
|
|
54
|
+
let end = index + 1;
|
|
55
|
+
while (end < value.length) {
|
|
56
|
+
const code = value.charCodeAt(end);
|
|
57
|
+
if (!((code >= 65 && code <= 90) || (code >= 97 && code <= 122)))
|
|
58
|
+
break;
|
|
59
|
+
end += 1;
|
|
76
60
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
61
|
+
if (end === index + 1)
|
|
62
|
+
return { text: '\\', end: index + 1 };
|
|
63
|
+
const command = value.slice(index + 1, end);
|
|
64
|
+
const replacement = MATH_SYMBOLS[command];
|
|
80
65
|
return {
|
|
81
|
-
|
|
82
|
-
end
|
|
66
|
+
text: replacement ?? `\\${command}`,
|
|
67
|
+
end
|
|
83
68
|
};
|
|
84
69
|
}
|
|
85
|
-
function appendSegment(segments, kind, value) {
|
|
86
|
-
if (value === '')
|
|
87
|
-
return;
|
|
70
|
+
function appendSegment(segments, kind, value, fontScale, baselineShiftEm) {
|
|
88
71
|
const previous = segments.at(-1);
|
|
89
|
-
|
|
90
|
-
|
|
72
|
+
const previousScale = previous?.fontScale ?? (previous?.kind === 'text' ? 1 : 0.7);
|
|
73
|
+
const previousShift = previous?.baselineShiftEm ??
|
|
74
|
+
(previous?.kind === 'subscript' ? 0.35 : previous?.kind === 'superscript' ? -0.55 : 0);
|
|
75
|
+
if (previous?.kind === kind &&
|
|
76
|
+
previousScale === fontScale &&
|
|
77
|
+
previousShift === baselineShiftEm) {
|
|
78
|
+
previous.value += value;
|
|
91
79
|
return;
|
|
92
80
|
}
|
|
93
|
-
|
|
81
|
+
const nested = fontScale !== (kind === 'text' ? 1 : 0.7) ||
|
|
82
|
+
baselineShiftEm !== (kind === 'subscript' ? 0.35 : kind === 'superscript' ? -0.55 : 0);
|
|
83
|
+
segments.push(nested ? { kind, value, fontScale, baselineShiftEm } : { kind, value });
|
|
94
84
|
}
|
|
95
85
|
export function parseMathLabel(value) {
|
|
96
86
|
const segments = [];
|
|
97
|
-
|
|
87
|
+
const braceStack = [];
|
|
88
|
+
const matchingBraces = new Map();
|
|
89
|
+
const matchedClosings = new Set();
|
|
90
|
+
for (let cursor = 0; cursor < value.length; cursor += 1) {
|
|
91
|
+
if (value[cursor] === '\\') {
|
|
92
|
+
cursor += 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (value[cursor] === '{')
|
|
96
|
+
braceStack.push(cursor);
|
|
97
|
+
else if (value[cursor] === '}') {
|
|
98
|
+
const opening = braceStack.pop();
|
|
99
|
+
if (opening !== undefined) {
|
|
100
|
+
matchingBraces.set(opening, cursor);
|
|
101
|
+
matchedClosings.add(cursor);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const contexts = [
|
|
106
|
+
{ end: value.length, kind: 'text', fontScale: 1, baselineShiftEm: 0 }
|
|
107
|
+
];
|
|
98
108
|
let index = 0;
|
|
99
|
-
const flush = () => {
|
|
100
|
-
appendSegment(segments, 'text', text);
|
|
101
|
-
text = '';
|
|
102
|
-
};
|
|
103
109
|
while (index < value.length) {
|
|
110
|
+
const context = contexts[contexts.length - 1];
|
|
111
|
+
if (index === context.end) {
|
|
112
|
+
contexts.pop();
|
|
113
|
+
index += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
104
116
|
const character = value[index];
|
|
117
|
+
if (matchingBraces.has(index) || matchedClosings.has(index)) {
|
|
118
|
+
index += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
105
121
|
if ((character === '_' || character === '^') && index + 1 < value.length) {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
const kind = character === '_' ? 'subscript' : 'superscript';
|
|
123
|
+
const fontScale = context.fontScale * 0.7;
|
|
124
|
+
const baselineShiftEm = context.baselineShiftEm +
|
|
125
|
+
(character === '_' ? 0.35 * context.fontScale : -0.55 * context.fontScale);
|
|
126
|
+
if (value[index + 1] === '{') {
|
|
127
|
+
const closing = matchingBraces.get(index + 1);
|
|
128
|
+
if (closing !== undefined) {
|
|
129
|
+
contexts.push({ end: closing, kind, fontScale, baselineShiftEm });
|
|
130
|
+
index += 2;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
appendSegment(segments, context.kind, character, context.fontScale, context.baselineShiftEm);
|
|
134
|
+
index += 1;
|
|
111
135
|
continue;
|
|
112
136
|
}
|
|
137
|
+
let token;
|
|
138
|
+
if (value[index + 1] === '\\')
|
|
139
|
+
token = commandAt(value, index + 1);
|
|
140
|
+
else {
|
|
141
|
+
const codePoint = value.codePointAt(index + 1);
|
|
142
|
+
const text = String.fromCodePoint(codePoint);
|
|
143
|
+
token = { text, end: index + 1 + text.length };
|
|
144
|
+
}
|
|
145
|
+
appendSegment(segments, kind, token.text, fontScale, baselineShiftEm);
|
|
146
|
+
index = token.end;
|
|
147
|
+
continue;
|
|
113
148
|
}
|
|
114
149
|
if (character === '\\') {
|
|
115
150
|
const command = commandAt(value, index);
|
|
116
|
-
|
|
151
|
+
appendSegment(segments, context.kind, command.text, context.fontScale, context.baselineShiftEm);
|
|
117
152
|
index = command.end;
|
|
118
153
|
continue;
|
|
119
154
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
155
|
+
const codePoint = value.codePointAt(index);
|
|
156
|
+
const text = String.fromCodePoint(codePoint);
|
|
157
|
+
appendSegment(segments, context.kind, text, context.fontScale, context.baselineShiftEm);
|
|
158
|
+
index += text.length;
|
|
123
159
|
}
|
|
124
|
-
flush();
|
|
125
160
|
return segments;
|
|
126
161
|
}
|
|
127
162
|
export function mathLabelText(value) {
|
|
@@ -130,13 +165,71 @@ export function mathLabelText(value) {
|
|
|
130
165
|
.join('');
|
|
131
166
|
}
|
|
132
167
|
export function mathLabelGlyphLength(value) {
|
|
133
|
-
|
|
168
|
+
let length = 0;
|
|
169
|
+
for (const _character of mathLabelText(value))
|
|
170
|
+
length += 1;
|
|
171
|
+
return length;
|
|
172
|
+
}
|
|
173
|
+
export function mathLabelTextWidth(value, cellAdvance = 7) {
|
|
174
|
+
let cells = 0;
|
|
175
|
+
for (const segment of parseMathLabel(value)) {
|
|
176
|
+
const scale = segment.fontScale ?? (segment.kind === 'text' ? 1 : 0.7);
|
|
177
|
+
for (const character of segment.value) {
|
|
178
|
+
const codePoint = character.codePointAt(0);
|
|
179
|
+
if ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
|
|
180
|
+
(codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
|
|
181
|
+
(codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
|
|
182
|
+
(codePoint >= 0xfe00 && codePoint <= 0xfe0f) ||
|
|
183
|
+
(codePoint >= 0xfe20 && codePoint <= 0xfe2f) ||
|
|
184
|
+
codePoint === 0x200d) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const doubleWidth = codePoint >= 0x1100 &&
|
|
188
|
+
(codePoint <= 0x115f ||
|
|
189
|
+
(codePoint >= 0x2329 && codePoint <= 0x232a) ||
|
|
190
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf) ||
|
|
191
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
192
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
193
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe6f) ||
|
|
194
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
195
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1faff));
|
|
196
|
+
let widthFactor = 1;
|
|
197
|
+
if (doubleWidth)
|
|
198
|
+
widthFactor = 2;
|
|
199
|
+
else if ('MWmw@%&'.includes(character))
|
|
200
|
+
widthFactor = 1.55;
|
|
201
|
+
else if ('ilI.,:;!|\'` '.includes(character))
|
|
202
|
+
widthFactor = 0.55;
|
|
203
|
+
else if (codePoint === 0x03a9 || codePoint === 0x221e)
|
|
204
|
+
widthFactor = 1.35;
|
|
205
|
+
cells += widthFactor * scale;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return cells * cellAdvance;
|
|
134
209
|
}
|
|
135
210
|
export function renderMathLabelTspans(value) {
|
|
136
211
|
const segments = parseMathLabel(value);
|
|
137
212
|
if (segments.length === 1 && segments[0]?.kind === 'text' && segments[0].value === value) {
|
|
138
213
|
return escapeXml(value);
|
|
139
214
|
}
|
|
215
|
+
const nested = segments.some((segment) => segment.fontScale !== undefined);
|
|
216
|
+
if (nested) {
|
|
217
|
+
let previousShift = 0;
|
|
218
|
+
let markup = '';
|
|
219
|
+
for (const segment of segments) {
|
|
220
|
+
const shift = segment.baselineShiftEm ??
|
|
221
|
+
(segment.kind === 'subscript' ? 0.35 : segment.kind === 'superscript' ? -0.55 : 0);
|
|
222
|
+
const scale = segment.fontScale ?? (segment.kind === 'text' ? 1 : 0.7);
|
|
223
|
+
const delta = Number((shift - previousShift).toFixed(4));
|
|
224
|
+
const fontPercent = Number((scale * 100).toFixed(2));
|
|
225
|
+
markup += `<tspan dy="${delta}em" font-size="${fontPercent}%">${escapeXml(segment.value)}</tspan>`;
|
|
226
|
+
previousShift = shift;
|
|
227
|
+
}
|
|
228
|
+
if (previousShift !== 0) {
|
|
229
|
+
markup += `<tspan dy="${Number((-previousShift).toFixed(4))}em" font-size="100%"></tspan>`;
|
|
230
|
+
}
|
|
231
|
+
return markup;
|
|
232
|
+
}
|
|
140
233
|
return segments
|
|
141
234
|
.map((segment) => {
|
|
142
235
|
const content = escapeXml(segment.value);
|