@schemd/core 0.1.0 → 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/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) {
@@ -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 use a
44
- * 70% font size and an empty inverse-`dy` tspan so consecutive shifts cannot
45
- * accumulate vertical drift.
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.
@@ -43,85 +43,120 @@ function escapeXml(value) {
43
43
  .replaceAll("'", '&#39;');
44
44
  }
45
45
  function commandAt(value, index) {
46
- const match = value.slice(index + 1).match(/^[A-Za-z]+/);
47
- if (!match)
48
- return { text: value[index], end: index + 1 };
49
- const command = match[0];
50
- const replacement = MATH_SYMBOLS[command];
51
- return {
52
- text: replacement ?? `\\${command}`,
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
- return output;
70
- }
71
- function shiftedValue(value, index) {
72
- const first = value[index + 1];
73
- if (first !== '{') {
74
- const character = Array.from(value.slice(index + 1))[0];
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
- const closing = value.indexOf('}', index + 2);
78
- if (closing < 0)
79
- return undefined;
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
- value: translateMathSymbols(value.slice(index + 2, closing)),
82
- end: closing + 1
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
- if (previous?.kind === kind) {
90
- segments[segments.length - 1] = { kind, value: previous.value + value };
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
- segments.push({ kind, value });
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
- let text = '';
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 shifted = shiftedValue(value, index);
107
- if (shifted !== undefined && shifted.value !== '') {
108
- flush();
109
- appendSegment(segments, character === '_' ? 'subscript' : 'superscript', shifted.value);
110
- index = shifted.end;
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
- text += command.text;
151
+ appendSegment(segments, context.kind, command.text, context.fontScale, context.baselineShiftEm);
117
152
  index = command.end;
118
153
  continue;
119
154
  }
120
- if (character !== '{' && character !== '}')
121
- text += character;
122
- index += 1;
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
- return Array.from(mathLabelText(value)).length;
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);