@lofcz/tikz 0.3.0 → 0.3.1
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 +5 -0
- package/dist/browser-worker.js +86 -83
- package/dist/worker.js +361 -49
- package/package.json +1 -1
- package/src/recover.js +255 -0
- package/src/source.js +60 -5
- package/src/tables.js +1 -1
package/src/recover.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// Conservative repairs of generated TikZ syntax, using the LaTeX lexer before
|
|
2
|
+
// argument attachment. Never invent coordinates, lengths, or missing geometry.
|
|
3
|
+
import { parseMinimal } from '@unified-latex/unified-latex-util-parse';
|
|
4
|
+
import { printRaw } from '@unified-latex/unified-latex-util-print-raw';
|
|
5
|
+
import { parsePgfkeys } from '@unified-latex/unified-latex-util-pgfkeys';
|
|
6
|
+
|
|
7
|
+
const string = content => ({ type: 'string', content });
|
|
8
|
+
const group = content => ({ type: 'group', content });
|
|
9
|
+
const is = (node, text) => node?.type === 'string' && node.content === text;
|
|
10
|
+
const space = node => node?.type === 'whitespace' || node?.type === 'parbreak';
|
|
11
|
+
const skip = (nodes, i) => { while (space(nodes[i])) i++; return i; };
|
|
12
|
+
function walk(nodes, visit) {
|
|
13
|
+
visit(nodes);
|
|
14
|
+
for (const node of nodes) if (Array.isArray(node.content)) walk(node.content, visit);
|
|
15
|
+
}
|
|
16
|
+
function closeAt(nodes, start, open, close) {
|
|
17
|
+
let depth = 0;
|
|
18
|
+
for (let i = start; i < nodes.length; i++) {
|
|
19
|
+
if (is(nodes[i], open)) depth++;
|
|
20
|
+
else if (is(nodes[i], close) && --depth === 0) return i;
|
|
21
|
+
}
|
|
22
|
+
return -1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEFINITIONS = new Set(['def', 'gdef', 'edef', 'xdef', 'let', 'newcommand', 'renewcommand', 'providecommand', 'pgfmathsetmacro', 'pgfmathtruncatemacro', 'pgfmathsetlengthmacro']);
|
|
26
|
+
function extendedIdentifiers(root, source) {
|
|
27
|
+
const bindings = new Map();
|
|
28
|
+
const names = new Set();
|
|
29
|
+
walk(root, nodes => { for (const node of nodes) if (node.type === 'macro') names.add(node.content); });
|
|
30
|
+
walk(root, nodes => {
|
|
31
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
32
|
+
if (nodes[i].type !== 'macro' || !DEFINITIONS.has(nodes[i].content)) continue;
|
|
33
|
+
const at = skip(nodes, i + 1);
|
|
34
|
+
const target = nodes[at]?.type === 'group' ? nodes[at].content : nodes.slice(at, at + 3);
|
|
35
|
+
const [macro, tail, end] = target;
|
|
36
|
+
if (macro?.type !== 'macro' || tail?.type !== 'string' || !/^(?:_[A-Za-z0-9_]+|[0-9][A-Za-z0-9_]*)$/.test(tail.content)) continue;
|
|
37
|
+
if (end && end.type !== 'group' && !(nodes[i].content === 'let' && end.type === 'macro')) continue; // keep real delimited/parameter macros
|
|
38
|
+
if (macro.position?.end.offset !== tail.position?.start.offset) continue;
|
|
39
|
+
const key = macro.content + tail.content;
|
|
40
|
+
if (bindings.has(key)) continue;
|
|
41
|
+
let name = 'tikzRecoveredVariable';
|
|
42
|
+
let n = bindings.size;
|
|
43
|
+
do { name += String.fromCharCode(65 + n % 26); n = Math.floor(n / 26); } while (n);
|
|
44
|
+
while (names.has(name)) name += 'X';
|
|
45
|
+
names.add(name);
|
|
46
|
+
bindings.set(key, { base: macro.content, suffix: tail.content, name });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
if (!bindings.size) return;
|
|
50
|
+
walk(root, nodes => {
|
|
51
|
+
for (let i = 0; i + 1 < nodes.length; i++) {
|
|
52
|
+
const node = nodes[i];
|
|
53
|
+
if (node.type !== 'macro') continue;
|
|
54
|
+
for (const binding of bindings.values()) {
|
|
55
|
+
const offset = node.position?.end.offset;
|
|
56
|
+
if (node.content !== binding.base || !source.startsWith(binding.suffix, offset)) continue;
|
|
57
|
+
const end = offset + binding.suffix.length;
|
|
58
|
+
if (/[A-Za-z0-9_]/.test(source[end] ?? '')) continue;
|
|
59
|
+
let j = i + 1;
|
|
60
|
+
while (j < nodes.length && nodes[j].position?.end.offset <= end) j++;
|
|
61
|
+
if (nodes[j - 1]?.position?.end.offset !== end) continue;
|
|
62
|
+
node.content = binding.name; nodes.splice(i + 1, j - i - 1);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// >=Latex[length=3mm,width=2mm] needs braces around the complete arrow value;
|
|
70
|
+
// TeX's optional argument scanner otherwise stops at the inner closing bracket.
|
|
71
|
+
function nestedOptions(nodes) {
|
|
72
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
73
|
+
if (!is(nodes[i], '[')) continue;
|
|
74
|
+
let close = closeAt(nodes, i, '[', ']');
|
|
75
|
+
if (close < 0) continue;
|
|
76
|
+
for (let j = i + 1; j < close; j++) {
|
|
77
|
+
if (!is(nodes[j], '[')) continue;
|
|
78
|
+
const end = closeAt(nodes, j, '[', ']');
|
|
79
|
+
let start = j - 1;
|
|
80
|
+
while (start > i && !is(nodes[start], '=') && !is(nodes[start], ',')) start--;
|
|
81
|
+
if (end < 0 || !is(nodes[start], '=')) continue;
|
|
82
|
+
const value = nodes.slice(start + 1, end + 1);
|
|
83
|
+
nodes.splice(start + 1, end - start, group(value));
|
|
84
|
+
close -= end - start - 1;
|
|
85
|
+
j = start + 1;
|
|
86
|
+
}
|
|
87
|
+
i = close;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function coordinateComponent(nodes, alreadyGrouped = false) {
|
|
92
|
+
const significant = nodes.filter(node => !space(node));
|
|
93
|
+
if (significant.length === 1 && significant[0].type === 'group') {
|
|
94
|
+
significant[0].content = coordinateComponent(significant[0].content, true);
|
|
95
|
+
return nodes;
|
|
96
|
+
}
|
|
97
|
+
let unitStart = significant.length - 1;
|
|
98
|
+
let unit = significant[unitStart]?.type === 'string' ? significant[unitStart].content : '';
|
|
99
|
+
if (unit.length === 1 && significant[unitStart - 1]?.type === 'string') { unitStart--; unit = significant[unitStart].content + unit; }
|
|
100
|
+
if (!/^(?:cm|mm|pt|bp|in|em|ex)$/.test(unit)) unit = null;
|
|
101
|
+
if (unit && significant.length > 1 && (significant[0].type === 'group' || significant.some(node => is(node, '(')))) {
|
|
102
|
+
const expr = significant.slice(0, unitStart).flatMap(node => node.type === 'group' ? [string('('), ...node.content, string(')')] : [node]);
|
|
103
|
+
const repaired = [string('('), ...expr, string(')*1' + unit)];
|
|
104
|
+
return alreadyGrouped ? repaired : [group(repaired)];
|
|
105
|
+
}
|
|
106
|
+
if (!alreadyGrouped && nodes.some(node => is(node, '('))) return [group(nodes)];
|
|
107
|
+
return nodes;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function coordinates(nodes) {
|
|
111
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
112
|
+
if (nodes[i].type === 'inlinemath') { coordinates(nodes[i].content); continue; }
|
|
113
|
+
if (!is(nodes[i], '(')) continue;
|
|
114
|
+
const end = closeAt(nodes, i, '(', ')');
|
|
115
|
+
if (end < 0) continue;
|
|
116
|
+
const inside = nodes.slice(i + 1, end);
|
|
117
|
+
// Calc expressions have their own coordinate parentheses inside $...$.
|
|
118
|
+
if (inside.some(node => node.type === 'inlinemath')) { for (const node of inside) if (node.type === 'inlinemath') coordinates(node.content); i = end; continue; }
|
|
119
|
+
let depth = 0, start = 0;
|
|
120
|
+
const segments = [], delimiters = [];
|
|
121
|
+
for (let j = 0; j < inside.length; j++) {
|
|
122
|
+
if (is(inside[j], '(')) depth++;
|
|
123
|
+
else if (is(inside[j], ')')) depth--;
|
|
124
|
+
else if (!depth && (is(inside[j], ',') || is(inside[j], ':') || is(inside[j], 'and'))) {
|
|
125
|
+
segments.push(inside.slice(start, j)); delimiters.push(is(inside[j], 'and') ? string(' and ') : inside[j]); start = j + 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
segments.push(inside.slice(start));
|
|
129
|
+
// Do not reinterpret named nodes, explicit coordinate systems or projections.
|
|
130
|
+
if (inside.some(node => is(node, '=') || is(node, '|') || is(node, 'cs'))) { i = end; continue; }
|
|
131
|
+
const rebuilt = segments.flatMap((part, j) => [...coordinateComponent(part), ...(delimiters[j] ? [delimiters[j]] : [])]);
|
|
132
|
+
nodes.splice(i + 1, end - i - 1, ...rebuilt);
|
|
133
|
+
i += rebuilt.length + 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const PATHS = new Set(['draw', 'fill', 'filldraw', 'shade', 'shadedraw', 'path', 'coordinate', 'node', 'clip', 'pic']);
|
|
138
|
+
|
|
139
|
+
function delimited(nodes, at, open, close) {
|
|
140
|
+
at = skip(nodes, at);
|
|
141
|
+
if (!is(nodes[at], open)) return null;
|
|
142
|
+
const end = closeAt(nodes, at, open, close);
|
|
143
|
+
return end < 0 ? null : { start: at, end: end + 1, content: nodes.slice(at + 1, end) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Adapt old/shorthand tkz calls to the bundled package's explicit results. */
|
|
147
|
+
function tkzCommands(nodes) {
|
|
148
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
149
|
+
const node = nodes[i];
|
|
150
|
+
if (node.type !== 'macro' || !node.content.startsWith('tkz')) continue;
|
|
151
|
+
const options = delimited(nodes, i + 1, '[', ']');
|
|
152
|
+
const point = delimited(nodes, options?.end ?? i + 1, '(', ')');
|
|
153
|
+
if (!point) continue;
|
|
154
|
+
if (node.content === 'tkzDefLine' && options) {
|
|
155
|
+
const first = skip(options.content, 0);
|
|
156
|
+
const through = skip(options.content, first + 1);
|
|
157
|
+
if (['orthogonal', 'perpendicular', 'parallel'].some(key => is(options.content[first], key)) && is(options.content[through], 'through')) {
|
|
158
|
+
nodes.splice(options.start + 1 + first + 1, 0, string('='));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (node.content === 'tkzMidPoint') node.content = 'tkzDefMidPoint';
|
|
162
|
+
if (['tkzDefMidPoint', 'tkzDefPointBy', 'tkzDefPointWith'].includes(node.content)) {
|
|
163
|
+
const out = skip(nodes, point.end);
|
|
164
|
+
if (nodes[out]?.type === 'group') nodes.splice(out, 0, { type: 'macro', content: 'tkzGetPoint' });
|
|
165
|
+
if (node.content === 'tkzDefPointBy' && options) {
|
|
166
|
+
const onto = options.content.findIndex(token => is(token, 'onto'));
|
|
167
|
+
const line = skip(options.content, onto + 1);
|
|
168
|
+
if (onto >= 0 && is(options.content[line], 'line')) {
|
|
169
|
+
options.content.splice(line, 1);
|
|
170
|
+
nodes.splice(options.start + 1, options.end - options.start - 2, ...options.content);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (node.content === 'tkzInter' && options && printRaw(options.content).trim() === 'R' && delimited(nodes, point.end, '(', ')')) node.content = 'tkzInterCC';
|
|
175
|
+
if (node.content === 'tkzDrawCircle' && options) {
|
|
176
|
+
const keys = parsePgfkeys(options.content).map(item => (item.itemParts ?? []).map(part => printRaw(part).trim()).join('='));
|
|
177
|
+
const mode = keys.find(key => ['circum', 'in', 'diameter', 'R'].includes(key));
|
|
178
|
+
const count = point.content.filter(token => is(token, ',')).length + 1;
|
|
179
|
+
if (!mode || count !== (['circum', 'in'].includes(mode) ? 3 : 2)) continue;
|
|
180
|
+
const rest = keys.filter(key => key !== mode).join(',');
|
|
181
|
+
if (mode === 'R') {
|
|
182
|
+
const comma = point.content.findIndex(token => is(token, ','));
|
|
183
|
+
nodes.splice(i, point.end - i, string(String.raw`\draw[circle style,${rest}] (${printRaw(point.content.slice(0, comma))}) circle (${printRaw(point.content.slice(comma + 1))});`));
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// tkzDefCircle supplies both center and a point on the circle. Drawing
|
|
187
|
+
// from those results preserves the construction rather than guessing a radius.
|
|
188
|
+
const replacement = String.raw`{\tkzDefCircle[${mode}](${printRaw(point.content)})\tkzGetPoints{tikzRecoveredCircleCenter}{tikzRecoveredCirclePoint}\tkzDrawCircle[${rest}](tikzRecoveredCircleCenter,tikzRecoveredCirclePoint)}`;
|
|
189
|
+
nodes.splice(i, point.end - i, string(replacement));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** PGF prints numbers, not expressions; evaluate explicit arithmetic first. */
|
|
195
|
+
function numberExpressions(nodes) {
|
|
196
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
197
|
+
const node = nodes[i];
|
|
198
|
+
if (node.type !== 'macro') continue;
|
|
199
|
+
if (['pgfmathparse', 'pgfmathsetmacro', 'pgfmathtruncatemacro'].includes(node.content)) {
|
|
200
|
+
let at = skip(nodes, i + 1);
|
|
201
|
+
if (node.content !== 'pgfmathparse') at = skip(nodes, at + 1);
|
|
202
|
+
const expr = nodes[at];
|
|
203
|
+
if (expr?.type !== 'group') continue;
|
|
204
|
+
const mod = expr.content.findIndex(token => is(token, 'mod'));
|
|
205
|
+
const atom = tokens => { const value = tokens.filter(token => !space(token)); return value.length === 1 && (value[0].type === 'macro' || value[0].type === 'group' || (value[0].type === 'string' && /^-?\d+(?:\.\d+)?$/.test(value[0].content))); };
|
|
206
|
+
if (mod > 0 && atom(expr.content.slice(0, mod)) && atom(expr.content.slice(mod + 1))) expr.content = [string('mod('), ...expr.content.slice(0, mod), string(','), ...expr.content.slice(mod + 1), string(')')];
|
|
207
|
+
}
|
|
208
|
+
if (node.content !== 'pgfmathprintnumber') continue;
|
|
209
|
+
const options = delimited(nodes, i + 1, '[', ']');
|
|
210
|
+
const at = skip(nodes, options?.end ?? i + 1);
|
|
211
|
+
const expr = nodes[at];
|
|
212
|
+
if (expr?.type !== 'group' || !expr.content.some(token => token.type === 'string' && ['*', '/', '+', '('].includes(token.content))) continue;
|
|
213
|
+
nodes.splice(i, at - i + 1, string(String.raw`{\pgfmathparse{${printRaw(expr.content)}}\pgfmathprintnumber${options ? `[${printRaw(options.content)}]` : ''}{\pgfmathresult}}`));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** A node position is one coordinate, not a path with a trailing ++ step. */
|
|
218
|
+
function relativeNodePosition(nodes) {
|
|
219
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
220
|
+
if (nodes[i].type !== 'macro' || nodes[i].content !== 'node') continue;
|
|
221
|
+
let at = i + 1;
|
|
222
|
+
while (at < nodes.length && !is(nodes[at], 'at') && !is(nodes[at], ';')) at++;
|
|
223
|
+
if (!is(nodes[at], 'at')) continue;
|
|
224
|
+
const base = delimited(nodes, at + 1, '(', ')');
|
|
225
|
+
if (!base) continue;
|
|
226
|
+
let plus = skip(nodes, base.end);
|
|
227
|
+
if (!is(nodes[plus], '+')) continue;
|
|
228
|
+
plus++;
|
|
229
|
+
if (is(nodes[plus], '+')) plus++;
|
|
230
|
+
const offset = delimited(nodes, plus, '(', ')');
|
|
231
|
+
if (!offset) continue;
|
|
232
|
+
nodes.splice(base.start, offset.end - base.start, string(`($(${printRaw(base.content)})+(${printRaw(offset.content)})$)`));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function recoverGeneratedSyntax(source) {
|
|
237
|
+
const root = parseMinimal(source).content;
|
|
238
|
+
extendedIdentifiers(root, source);
|
|
239
|
+
walk(root, nestedOptions);
|
|
240
|
+
walk(root, tkzCommands);
|
|
241
|
+
walk(root, numberExpressions);
|
|
242
|
+
walk(root, relativeNodePosition);
|
|
243
|
+
walk(root, nodes => {
|
|
244
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
245
|
+
if (nodes[i].type !== 'macro' || !PATHS.has(nodes[i].content)) continue;
|
|
246
|
+
let end = i + 1;
|
|
247
|
+
while (end < nodes.length && !is(nodes[end], ';')) end++;
|
|
248
|
+
const tokens = nodes.slice(i + 1, end);
|
|
249
|
+
coordinates(tokens);
|
|
250
|
+
nodes.splice(i + 1, end - i - 1, ...tokens);
|
|
251
|
+
i += tokens.length;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
return printRaw(root);
|
|
255
|
+
}
|
package/src/source.js
CHANGED
|
@@ -17,6 +17,7 @@ import { printRaw } from '@unified-latex/unified-latex-util-print-raw';
|
|
|
17
17
|
import { parsePgfkeys } from '@unified-latex/unified-latex-util-pgfkeys';
|
|
18
18
|
import { BUNDLED_PACKAGES, BUNDLED_PGFPLOTS_LIBRARIES, BUNDLED_TIKZ_LIBRARIES } from './bundled.js';
|
|
19
19
|
import * as T from './tables.js';
|
|
20
|
+
import { recoverGeneratedSyntax } from './recover.js';
|
|
20
21
|
|
|
21
22
|
export const DEFAULT_LIBRARIES = ['calc', 'intersections', 'angles', 'quotes', 'arrows.meta', 'positioning'];
|
|
22
23
|
|
|
@@ -492,11 +493,33 @@ function processKeyList(nodes, context) {
|
|
|
492
493
|
for (const item of items) {
|
|
493
494
|
const parts = item.itemParts ?? [];
|
|
494
495
|
if (!parts.length) continue;
|
|
495
|
-
|
|
496
|
+
let key = printRaw(parts[0]).trim();
|
|
496
497
|
const value = parts[1];
|
|
498
|
+
// In TikZ, a named collection of keys needs /.style. PGFPlots has real
|
|
499
|
+
// setters named "tick style" etc., so leave its options alone.
|
|
500
|
+
if (context.tikz && !key.includes('/') && key.endsWith(' style') && value?.some(isGroup)) {
|
|
501
|
+
key += '/.style';
|
|
502
|
+
parts[0] = [str(key)];
|
|
503
|
+
}
|
|
497
504
|
if (value && TEXT_KEYS.has(key)) repairTextNodes(value, context);
|
|
498
505
|
if (value && /^(?:xtick|ytick|ztick)labels$|^legend entries$/.test(key)) for (const node of value) if (isGroup(node)) repairTextNodes(node.content, context);
|
|
499
506
|
const styleName = /^(.+?)\/\.(?:style|append style)$/.exec(key)?.[1];
|
|
507
|
+
// A user style called name/label must not replace TikZ's node naming or
|
|
508
|
+
// labeling machinery. Keep explicit property assignments on the native key.
|
|
509
|
+
if (styleName && ['name', 'label'].includes(styleName)) {
|
|
510
|
+
const alias = `tikz recovered ${styleName} style`;
|
|
511
|
+
context.styleAliases?.set(styleName, alias);
|
|
512
|
+
parts[0] = [str(key.replace(styleName, alias))];
|
|
513
|
+
} else if (!value && context.styleAliases?.has(key)) parts[0] = [str(context.styleAliases.get(key))];
|
|
514
|
+
if (value && /^(above|below|left|right)( left| right)?$/.test(key)) {
|
|
515
|
+
const target = value.findIndex(node => isString(node, 'of'));
|
|
516
|
+
const point = target >= 0 ? skipSpace(value, target + 1) : -1;
|
|
517
|
+
if (point >= 0 && value[point]?.type === 'inlinemath') {
|
|
518
|
+
// positioning adds the outer parentheses itself; only protect commas
|
|
519
|
+
// from the PGF key parser, which does not recognize math delimiters.
|
|
520
|
+
parts[1] = [{ type: 'group', content: value }];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
500
523
|
if (styleName && value) for (const node of value) if (isGroup(node)) unloopStyle(node.content, styleName);
|
|
501
524
|
if (key === 'font' && value) {
|
|
502
525
|
const color = pullColors(value);
|
|
@@ -535,6 +558,16 @@ function mergeParenthesisedValues(items) {
|
|
|
535
558
|
for (let i = 0; i < items.length; i++) {
|
|
536
559
|
const item = items[i];
|
|
537
560
|
const parts = item.itemParts ?? [];
|
|
561
|
+
if (parts.length === 2 && /^(above|below|left|right)( left| right)?$/.test(printRaw(parts[0]).trim())) {
|
|
562
|
+
const of = parts[1].findIndex(node => isString(node, 'of'));
|
|
563
|
+
const number = tokens => /^-?\d+(?:\.\d+)?(?:cm|mm|pt)?$/.test(printRaw(tokens).trim());
|
|
564
|
+
const next = items[i + 1]?.itemParts;
|
|
565
|
+
if (of >= 0 && number(parts[1].slice(of + 1)) && next?.length === 1 && number(next[0])) {
|
|
566
|
+
out.push({ ...item, itemParts: [parts[0], [{ type: 'group', content: [...parts[1], str(','), ...next[0]] }]] });
|
|
567
|
+
i++;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
538
571
|
if (parts.length !== 2 || balance(parts[1]) <= 0) { out.push(item); continue; }
|
|
539
572
|
const value = [...parts[1]];
|
|
540
573
|
let depth = balance(value);
|
|
@@ -620,7 +653,7 @@ function processKeyLists(root, context) {
|
|
|
620
653
|
continue;
|
|
621
654
|
}
|
|
622
655
|
if (node?.type === 'environment' && Array.isArray(node.args)) {
|
|
623
|
-
for (const argument of node.args) if (argument.openMark === '[') processKeyList(argument.content, context);
|
|
656
|
+
for (const argument of node.args) if (argument.openMark === '[') processKeyList(argument.content, { ...context, tikz: ['tikzpicture', 'scope', 'circuitikz'].includes(node.env) });
|
|
624
657
|
}
|
|
625
658
|
if (isMacro(node) && KEY_LIST_MACROS.has(node.content) && hasArgs(node)) {
|
|
626
659
|
for (const argument of node.args) if (argument.openMark === '{') processKeyList(argument.content, context);
|
|
@@ -882,6 +915,20 @@ function repairForeachLists(root) {
|
|
|
882
915
|
}
|
|
883
916
|
});
|
|
884
917
|
}
|
|
918
|
+
|
|
919
|
+
// \boldmath expands unsafely while TikZ parses quoted labels. Move its effect
|
|
920
|
+
// into the actual math expression, keeping group scope and the displayed value.
|
|
921
|
+
function repairBoldMath(root) {
|
|
922
|
+
walkArrays(root, array => {
|
|
923
|
+
if (!array.some(node => node.type === 'inlinemath')) return;
|
|
924
|
+
let bold = false;
|
|
925
|
+
for (let i = 0; i < array.length; i++) {
|
|
926
|
+
if (isMacro(array[i], 'boldmath')) { bold = true; array.splice(i--, 1); continue; }
|
|
927
|
+
if (isMacro(array[i], 'unboldmath')) { bold = false; array.splice(i--, 1); continue; }
|
|
928
|
+
if (bold && array[i].type === 'inlinemath') array[i].content = [macro('boldsymbol', [{ type: 'argument', openMark: '{', closeMark: '}', content: array[i].content }])];
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
}
|
|
885
932
|
function mathOnly(nodes) {
|
|
886
933
|
if (nodes.some(node => node.type === 'inlinemath' || node.type === 'displaymath')) return false;
|
|
887
934
|
return needsMath(nodes.filter(node => !isSpace(node)));
|
|
@@ -925,7 +972,7 @@ function formatNumbers(root) {
|
|
|
925
972
|
* @returns {{ source: string, unavailable: string[], notes: string[] }}
|
|
926
973
|
*/
|
|
927
974
|
export function prepareTikzDocument(code) {
|
|
928
|
-
const cleaned = cleanInput(code);
|
|
975
|
+
const cleaned = recoverGeneratedSyntax(cleanInput(code));
|
|
929
976
|
if (!cleaned) throw new TikzSourceError('TikZ source is empty.');
|
|
930
977
|
const state = {
|
|
931
978
|
packages: new Map(), libraries: new Set(DEFAULT_LIBRARIES), pgfplotsLibraries: new Set(), gdLibraries: new Set(),
|
|
@@ -944,15 +991,17 @@ export function prepareTikzDocument(code) {
|
|
|
944
991
|
const unicodeSeen = new Set();
|
|
945
992
|
const colorsDefined = new Set();
|
|
946
993
|
const colorsSeen = new Set();
|
|
947
|
-
|
|
994
|
+
const keyContext = { styleAliases: new Map() };
|
|
995
|
+
for (const root of [statementsRoot, body]) {
|
|
948
996
|
rewriteHexColors(root);
|
|
949
997
|
replaceUnavailableMacros(root, state);
|
|
950
|
-
processKeyLists(root,
|
|
998
|
+
processKeyLists(root, keyContext);
|
|
951
999
|
}
|
|
952
1000
|
stripOverlayKeys(body);
|
|
953
1001
|
protectAndDelimiter(body);
|
|
954
1002
|
repairForeachLists(body);
|
|
955
1003
|
replaceTypos(body);
|
|
1004
|
+
repairBoldMath(body);
|
|
956
1005
|
repairNodeTexts(body);
|
|
957
1006
|
// siunitx parses quantities itself; preserve its input grammar.
|
|
958
1007
|
unicodeSupport(body, unicodeSeen, unicode);
|
|
@@ -988,6 +1037,12 @@ export function prepareTikzDocument(code) {
|
|
|
988
1037
|
if (state.gdLibraries.size) lines.push(`\\usegdlibrary{${[...state.gdLibraries].join(',')}}`);
|
|
989
1038
|
if (state.packages.has('pgfplots') && !state.compat) lines.push('\\pgfplotsset{compat=1.18}');
|
|
990
1039
|
lines.push(...unicode, ...colors, T.COMPAT_SHIM);
|
|
1040
|
+
if (!excluded.has('pgfmathdistance') && /\\pgfmathdistance\b/.test(everything)) lines.push(String.raw`\makeatletter
|
|
1041
|
+
\providecommand{\pgfmathdistance}[2]{\pgfpointdiff{#1}{#2}\pgfmathveclen{\the\pgf@x}{\the\pgf@y}}
|
|
1042
|
+
\makeatother`);
|
|
1043
|
+
if (/\bparallelogram\b/.test(everything)) lines.push(String.raw`\pgfkeysifdefined{/tikz/parallelogram/.@cmd}{}{\tikzset{parallelogram/.style={shape=trapezium,trapezium left angle=70,trapezium right angle=110}}}`);
|
|
1044
|
+
lines.push(String.raw`\pgfkeysifdefined{/tikz/rounded cap/.@cmd}{}{\tikzset{rounded cap/.style={line cap=round}}}
|
|
1045
|
+
\pgfkeysifdefined{/tikz/final/.@cmd}{}{\tikzset{final/.style={accepting}}}`);
|
|
991
1046
|
if (state.shims.has('siunitx')) lines.push(T.siunitxShim());
|
|
992
1047
|
if (state.shims.has('physics')) lines.push(T.PHYSICS_SHIM);
|
|
993
1048
|
if (state.shims.has('upgreek')) lines.push(...T.GREEK_NAMES.map(name => `\\providecommand{\\up${name}}{\\ensuremath{\\${name}}}`));
|
package/src/tables.js
CHANGED
|
@@ -90,7 +90,7 @@ export const AUTO_PACKAGES = [
|
|
|
90
90
|
];
|
|
91
91
|
export const AUTO_LIBRARIES = [
|
|
92
92
|
[/\bmatrix of (?:math )?nodes\b|\\matrix\b/, 'matrix'],
|
|
93
|
-
[/\b(?:regular polygon|trapezium|semicircle|isosceles triangle|kite|dart|circular sector|cylinder|diamond|star|ellipse)\s*[,\]=]/, 'shapes.geometric'],
|
|
93
|
+
[/\b(?:regular polygon|trapezium|parallelogram|semicircle|isosceles triangle|kite|dart|circular sector|cylinder|diamond|star|ellipse)\s*[,\]=]/, 'shapes.geometric'],
|
|
94
94
|
[/\b(?:cross out|strike out|rounded rectangle|chamfered rectangle)\b/, 'shapes.misc'],
|
|
95
95
|
[/\b(?:cloud|forbidden sign|magnifying glass|starburst|signal|tape|magnetic tape|correct forbidden sign)\s*[,\]=]/, 'shapes.symbols'],
|
|
96
96
|
[/\b(?:rectangle split|circle split|ellipse split)\b/, 'shapes.multipart'],
|