diff --git a/assets/css/latex2js.css b/assets/css/latex2js.css
index dc0d4f1..90b9a8b 100644
--- a/assets/css/latex2js.css
+++ b/assets/css/latex2js.css
@@ -7,9 +7,19 @@ svg {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
+/*
+ * The gap TeX calls \parskip: the space between paragraphs, and between a
+ * paragraph and a figure. One value, set once, because that is what it is in
+ * TeX — a document-level style, not something an author adjusts by pressing
+ * return more times.
+ */
+:root {
+ --latex2js-parskip: 1.6em;
+}
+
.pspicture {
position: relative;
- margin: auto;
+ margin: var(--latex2js-parskip) auto;
}
.enumerate {
@@ -27,6 +37,63 @@ p.quotation {
font-size: 10pt;
}
+/* End-of-proof tombstone, set flush right as amsthm places it. */
+span.qed {
+ display: block;
+ text-align: right;
+}
+
+/*
+ * Theorem-like environments, set the way LaTeX sets them: the label runs into
+ * the statement as part of the sentence rather than sitting above it as a
+ * heading, and the body of a theorem is italic while a remark or a proof is
+ * not. The markup keeps a real heading element, so the structure survives for
+ * anything reading the document rather than looking at it.
+ */
+.theorem-env {
+ margin: 1em 0;
+}
+
+h4.theorem-head {
+ display: inline;
+ margin: 0;
+ font-size: inherit;
+ font-style: normal;
+ font-weight: bold;
+}
+
+h4.theorem-head::after {
+ content: '.';
+}
+
+.theorem-env--theorem,
+.theorem-env--lemma,
+.theorem-env--corollary,
+.theorem-env--proposition,
+.theorem-env--definition,
+.theorem-env--axiom,
+.theorem-env--claim {
+ font-style: italic;
+}
+
+/* amsthm leaves these upright even though they are numbered alongside. */
+.theorem-env--remark,
+.theorem-env--note,
+.theorem-env--example,
+.theorem-env--exercise,
+.theorem-env--question,
+.theorem-env--problem,
+.theorem-env--solution,
+.theorem-env--proof {
+ font-style: normal;
+}
+
+/* The proof label is italic where a theorem label is bold. */
+.theorem-env--proof > h4.theorem-head {
+ font-weight: normal;
+ font-style: italic;
+}
+
.nicebox {
margin-top: 20px;
min-height: 20px;
@@ -59,6 +126,7 @@ p.quotation {
padding-right: 20px;
padding-left: 20px;
}
+
.latex-container {
width: auto;
}
@@ -73,3 +141,19 @@ p.quotation {
pre {
overflow: auto;
}
+/*
+ * Paragraph spacing. A run of blank lines is one \par however long it is, so
+ * the gap belongs to the document rather than to how many times the author hit
+ * return. Setting it here is what makes it adjustable at all: it used to be
+ * one
per blank line, which no stylesheet could reach.
+ *
+ * Bottom margin only, so the value is the gap. Adjacent margins would collapse
+ * to the larger of the two and the arithmetic would stop being obvious.
+ */
+.math > p.para {
+ margin: 0 0 var(--latex2js-parskip);
+}
+
+.math > p.para:last-child {
+ margin-bottom: 0;
+}
diff --git a/assets/js/latex2html5.bundle.js b/assets/js/latex2html5.bundle.js
index 8e1791b..6475470 100644
--- a/assets/js/latex2html5.bundle.js
+++ b/assets/js/latex2html5.bundle.js
@@ -22,6 +22,51 @@ function render(that) {
},{}],2:[function(require,module,exports){
"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.default = render;
+function itemizeLine(line) {
+ var m = line.match(/\\item (.*)/);
+ if (m)
+ return '
' + m[1] + '';
+ return line;
+}
+function descriptionLine(line) {
+ var m = line.match(/\\item\[([^\]]*)\]\s*(.*)/);
+ if (m)
+ return '' + m[1] + '' + m[2] + '';
+ return itemizeLine(line);
+}
+/**
+ * Renders enumerate / itemize / description lists from \item lines.
+ */
+function render(that) {
+ const type = that.type || 'enumerate';
+ const convert = type === 'description' ? descriptionLine : itemizeLine;
+ const lines = that.lines.map(convert).join('\n');
+ let el;
+ if (type === 'enumerate') {
+ const ol = document.createElement('ol');
+ ol.className = 'math enumerate';
+ ol.innerHTML = lines;
+ el = ol;
+ }
+ else if (type === 'description') {
+ const dl = document.createElement('dl');
+ dl.className = 'math description';
+ dl.innerHTML = lines;
+ el = dl;
+ }
+ else {
+ const ul = document.createElement('ul');
+ ul.className = 'math itemize';
+ ul.innerHTML = lines;
+ el = ul;
+ }
+ return el;
+}
+
+},{}],3:[function(require,module,exports){
+"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
@@ -37,18 +82,23 @@ function render(_that) {
return div;
}
-},{"@latex2js/macros":14}],3:[function(require,module,exports){
+},{"@latex2js/macros":18}],4:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = render;
+/**
+ * A block, not an inline span: the parser now emits real paragraphs, and a
+ * `` inside a `` is invalid nesting that a browser silently hoists
+ * out, taking the text with it.
+ */
function render(that) {
- const span = document.createElement('span');
- span.className = 'math';
- span.innerHTML = that.lines.join('\n');
- return span;
+ const div = document.createElement('div');
+ div.className = 'math';
+ div.innerHTML = that.lines.join('\n');
+ return div;
}
-},{}],4:[function(require,module,exports){
+},{}],5:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = render;
@@ -59,7 +109,7 @@ function render(that) {
return span;
}
-},{}],5:[function(require,module,exports){
+},{}],6:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = render;
@@ -91,6 +141,16 @@ function render(that) {
if (!env.variables)
env.variables = {};
env.variables[variable] = val;
+ // Re-render through the incremental path so plots stay in document
+ // order; removing the .psplot nodes and re-appending them put them at
+ // the end of the SVG, on top of every later shape. The graph keys the
+ // re-render on the variable that moved.
+ const redraw = that.redraw;
+ if (typeof redraw === 'function') {
+ redraw({ changed: [variable] });
+ return;
+ }
+ // Fallback for a psgraph that predates the incremental renderer.
svgEl.selectAll('.psplot').remove();
Object.entries(plot).forEach(([k, plotData]) => {
if (k.match(/psplot/)) {
@@ -121,7 +181,7 @@ function render(that) {
return div;
}
-},{"@latex2js/pstricks":16,"@latex2js/utils":20}],6:[function(require,module,exports){
+},{"@latex2js/pstricks":20,"@latex2js/utils":25}],7:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = render;
@@ -132,30 +192,33 @@ function render(that) {
return pre;
}
-},{}],7:[function(require,module,exports){
+},{}],8:[function(require,module,exports){
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
-exports.init = exports.macros = exports.math = exports.verbatim = exports.enumerate = exports.nicebox = exports.pspicture = void 0;
+exports.init = exports.DEFAULT_CONFIG = exports.macros = exports.math = exports.verbatim = exports.list = exports.enumerate = exports.nicebox = exports.pspicture = void 0;
exports.default = render;
const latex2js_1 = __importDefault(require("latex2js"));
const mathjaxjs_1 = require("mathjaxjs");
+Object.defineProperty(exports, "DEFAULT_CONFIG", { enumerable: true, get: function () { return mathjaxjs_1.DEFAULT_CONFIG; } });
const pspicture_js_1 = __importDefault(require("./components/pspicture.js"));
exports.pspicture = pspicture_js_1.default;
const nicebox_js_1 = __importDefault(require("./components/nicebox.js"));
exports.nicebox = nicebox_js_1.default;
const enumerate_js_1 = __importDefault(require("./components/enumerate.js"));
exports.enumerate = enumerate_js_1.default;
+const list_js_1 = __importDefault(require("./components/list.js"));
+exports.list = list_js_1.default;
const verbatim_js_1 = __importDefault(require("./components/verbatim.js"));
exports.verbatim = verbatim_js_1.default;
const math_js_1 = __importDefault(require("./components/math.js"));
exports.math = math_js_1.default;
const macros_1 = __importDefault(require("./components/macros"));
exports.macros = macros_1.default;
-const ELEMENTS = { pspicture: pspicture_js_1.default, nicebox: nicebox_js_1.default, enumerate: enumerate_js_1.default, verbatim: verbatim_js_1.default, math: math_js_1.default, macros: macros_1.default };
-function render(tex, resolve) {
+const ELEMENTS = { pspicture: pspicture_js_1.default, nicebox: nicebox_js_1.default, enumerate: enumerate_js_1.default, itemize: list_js_1.default, description: list_js_1.default, verbatim: verbatim_js_1.default, math: math_js_1.default, macros: macros_1.default };
+function render(tex, resolve, config) {
const done = () => {
const latex = new latex2js_1.default();
const parsed = latex.parse(tex);
@@ -174,21 +237,1469 @@ function render(tex, resolve) {
if ((0, mathjaxjs_1.getMathJax)()) {
return done();
}
- (0, mathjaxjs_1.loadMathJax)(done);
+ (0, mathjaxjs_1.loadMathJax)(done, config);
}
-const init = () => {
- (0, mathjaxjs_1.loadMathJax)();
+const init = (config) => {
+ (0, mathjaxjs_1.loadMathJax)(undefined, config);
document.querySelectorAll('script[type="text/latex"]').forEach((el) => {
render(el.innerHTML, (div) => {
if (el.parentNode) {
el.parentNode.insertBefore(div, el.nextSibling);
}
- });
+ }, config);
});
};
exports.init = init;
-},{"./components/enumerate.js":1,"./components/macros":2,"./components/math.js":3,"./components/nicebox.js":4,"./components/pspicture.js":5,"./components/verbatim.js":6,"latex2js":8,"mathjaxjs":15}],8:[function(require,module,exports){
+},{"./components/enumerate.js":1,"./components/list.js":2,"./components/macros":3,"./components/math.js":4,"./components/nicebox.js":5,"./components/pspicture.js":6,"./components/verbatim.js":7,"latex2js":10,"mathjaxjs":19}],9:[function(require,module,exports){
+// @generated by Peggy 5.1.0.
+//
+// https://peggyjs.org/
+
+"use strict";
+
+class peg$SyntaxError extends SyntaxError {
+ constructor(message, expected, found, location) {
+ super(message);
+ this.expected = expected;
+ this.found = found;
+ this.location = location;
+ this.name = "SyntaxError";
+ }
+
+ format(sources) {
+ let str = "Error: " + this.message;
+ if (this.location) {
+ let src = null;
+ const st = sources.find(s => s.source === this.location.source);
+ if (st) {
+ src = st.text.split(/\r\n|\n|\r/g);
+ }
+ const s = this.location.start;
+ const offset_s = (this.location.source && (typeof this.location.source.offset === "function"))
+ ? this.location.source.offset(s)
+ : s;
+ const loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
+ if (src) {
+ const e = this.location.end;
+ const filler = "".padEnd(offset_s.line.toString().length, " ");
+ const line = src[s.line - 1];
+ const last = s.line === e.line ? e.column : line.length + 1;
+ const hatLen = (last - s.column) || 1;
+ str += "\n --> " + loc + "\n"
+ + filler + " |\n"
+ + offset_s.line + " | " + line + "\n"
+ + filler + " | " + "".padEnd(s.column - 1, " ")
+ + "".padEnd(hatLen, "^");
+ } else {
+ str += "\n at " + loc;
+ }
+ }
+ return str;
+ }
+
+ static buildMessage(expected, found) {
+ function hex(ch) {
+ return ch.codePointAt(0).toString(16).toUpperCase();
+ }
+
+ const nonPrintable = Object.prototype.hasOwnProperty.call(RegExp.prototype, "unicode")
+ ? new RegExp("[\\p{C}\\p{Mn}\\p{Mc}]", "gu")
+ : null;
+ function unicodeEscape(s) {
+ if (nonPrintable) {
+ return s.replace(nonPrintable, ch => "\\u{" + hex(ch) + "}");
+ }
+ return s;
+ }
+
+ function literalEscape(s) {
+ return unicodeEscape(s
+ .replace(/\\/g, "\\\\")
+ .replace(/"/g, "\\\"")
+ .replace(/\0/g, "\\0")
+ .replace(/\t/g, "\\t")
+ .replace(/\n/g, "\\n")
+ .replace(/\r/g, "\\r")
+ .replace(/[\x00-\x0F]/g, ch => "\\x0" + hex(ch))
+ .replace(/[\x10-\x1F\x7F-\x9F]/g, ch => "\\x" + hex(ch)));
+ }
+
+ function classEscape(s) {
+ return unicodeEscape(s
+ .replace(/\\/g, "\\\\")
+ .replace(/\]/g, "\\]")
+ .replace(/\^/g, "\\^")
+ .replace(/-/g, "\\-")
+ .replace(/\0/g, "\\0")
+ .replace(/\t/g, "\\t")
+ .replace(/\n/g, "\\n")
+ .replace(/\r/g, "\\r")
+ .replace(/[\x00-\x0F]/g, ch => "\\x0" + hex(ch))
+ .replace(/[\x10-\x1F\x7F-\x9F]/g, ch => "\\x" + hex(ch)));
+ }
+
+ const DESCRIBE_EXPECTATION_FNS = {
+ literal(expectation) {
+ return "\"" + literalEscape(expectation.text) + "\"";
+ },
+
+ class(expectation) {
+ const escapedParts = expectation.parts.map(
+ part => (Array.isArray(part)
+ ? classEscape(part[0]) + "-" + classEscape(part[1])
+ : classEscape(part))
+ );
+
+ return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]" + (expectation.unicode ? "u" : "");
+ },
+
+ any() {
+ return "any character";
+ },
+
+ end() {
+ return "end of input";
+ },
+
+ other(expectation) {
+ return expectation.description;
+ },
+ };
+
+ function describeExpectation(expectation) {
+ return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);
+ }
+
+ function describeExpected(expected) {
+ const descriptions = expected.map(describeExpectation);
+ descriptions.sort();
+
+ if (descriptions.length > 0) {
+ let j = 1;
+ for (let i = 1; i < descriptions.length; i++) {
+ if (descriptions[i - 1] !== descriptions[i]) {
+ descriptions[j] = descriptions[i];
+ j++;
+ }
+ }
+ descriptions.length = j;
+ }
+
+ switch (descriptions.length) {
+ case 1:
+ return descriptions[0];
+
+ case 2:
+ return descriptions[0] + " or " + descriptions[1];
+
+ default:
+ return descriptions.slice(0, -1).join(", ")
+ + ", or "
+ + descriptions[descriptions.length - 1];
+ }
+ }
+
+ function describeFound(found) {
+ return found ? "\"" + literalEscape(found) + "\"" : "end of input";
+ }
+
+ return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found.";
+ }
+}
+
+function peg$parse(input, options) {
+ options = options !== undefined ? options : {};
+
+ const peg$FAILED = {};
+ const peg$source = options.grammarSource;
+
+ const peg$startRuleFunctions = {
+ Document: peg$parseDocument,
+ };
+ let peg$startRuleFunction = peg$parseDocument;
+
+ const peg$c0 = "\\begin{";
+ const peg$c1 = "verbatim";
+ const peg$c2 = "print";
+ const peg$c3 = "}";
+ const peg$c4 = "\\end{";
+ const peg$c5 = "\\";
+ const peg$c6 = "begin{";
+ const peg$c7 = "end{";
+ const peg$c8 = "%";
+ const peg$c9 = "\r\n";
+
+ const peg$r0 = /^[a-zA-Z*]/;
+ const peg$r1 = /^[a-zA-Z@]/;
+ const peg$r2 = /^[([{]/;
+ const peg$r3 = /^[)\]}]/;
+ const peg$r4 = /^[\n\r]/;
+ const peg$r5 = /^[ \t]/;
+
+ const peg$e0 = peg$anyExpectation();
+ const peg$e1 = peg$literalExpectation("\\begin{", false);
+ const peg$e2 = peg$literalExpectation("verbatim", false);
+ const peg$e3 = peg$literalExpectation("print", false);
+ const peg$e4 = peg$literalExpectation("}", false);
+ const peg$e5 = peg$literalExpectation("\\end{", false);
+ const peg$e6 = peg$classExpectation([["a", "z"], ["A", "Z"], "*"], false, false, false);
+ const peg$e7 = peg$literalExpectation("\\", false);
+ const peg$e8 = peg$literalExpectation("begin{", false);
+ const peg$e9 = peg$literalExpectation("end{", false);
+ const peg$e10 = peg$classExpectation([["a", "z"], ["A", "Z"], "@"], false, false, false);
+ const peg$e11 = peg$literalExpectation("%", false);
+ const peg$e12 = peg$classExpectation(["(", "[", "{"], false, false, false);
+ const peg$e13 = peg$classExpectation([")", "]", "}"], false, false, false);
+ const peg$e14 = peg$literalExpectation("\r\n", false);
+ const peg$e15 = peg$classExpectation(["\n", "\r"], false, false, false);
+ const peg$e16 = peg$classExpectation([" ", "\t"], false, false, false);
+
+ function peg$f0(segs) { return segs; }
+ function peg$f1(e) { return { kind: 'strayEnd', name: e.name, raw: e.raw, loc: loc() }; }
+ function peg$f2(start, content, end) {
+ return {
+ kind: 'env',
+ name: start.name,
+ verbatim: true,
+ begin: start,
+ end: { name: start.name, raw: '\\end{' + end + '}', loc: loc() },
+ content: [{
+ kind: 'verbatim',
+ text: content.map((pair) => pair[1]).join('').replace(/\n$/, '')
+ }],
+ loc: loc()
+ };
+ }
+ function peg$f3(n) { return { name: n, raw: '\\begin{' + n + '}', loc: loc() }; }
+ function peg$f4(n) { return n; }
+ function peg$f5(b, content, e) {
+ return { kind: 'env', name: b.name, verbatim: false, begin: b, end: e || null, content: content, loc: loc() };
+ }
+ function peg$f6(name, tail) {
+ return { name: name, raw: '\\begin{' + name + '}' + tail, loc: loc() };
+ }
+ function peg$f7(name) {
+ return { name: name, raw: '\\end{' + name + '}', loc: loc() };
+ }
+ function peg$f8(chars) { return chars.join(''); }
+ function peg$f9(start, tail) {
+ depth = 0;
+ return { kind: 'command', name: start.name, raw: start.raw + tail, loc: loc() };
+ }
+ function peg$f10(chars) {
+ return { name: chars.join(''), raw: '\\' + chars.join('') };
+ }
+ function peg$f11(parts) { return parts.join(''); }
+ function peg$f12() { depth++; return text(); }
+ function peg$f13() { depth = Math.max(0, depth - 1); return text(); }
+ function peg$f14() { return depth === 0; }
+ function peg$f15(c) { return c; }
+ function peg$f16() { return depth > 0; }
+ function peg$f17(c) { return c; }
+ function peg$f18() { return ''; }
+ function peg$f19(parts, eol) { return { kind: 'line', parts: parts, hasEol: !!eol, loc: loc() }; }
+ function peg$f20(eol) { return { kind: 'line', parts: [], hasEol: true, loc: loc() }; }
+ function peg$f21(c) { return { kind: 'char', c: c, loc: loc() }; }
+ let peg$currPos = options.peg$currPos | 0;
+ let peg$savedPos = peg$currPos;
+ const peg$posDetailsCache = [{ line: 1, column: 1 }];
+ let peg$maxFailPos = peg$currPos;
+ let peg$maxFailExpected = options.peg$maxFailExpected || [];
+ let peg$silentFails = options.peg$silentFails | 0;
+
+ let peg$result;
+
+ if (options.startRule) {
+ if (!(options.startRule in peg$startRuleFunctions)) {
+ throw new Error("Can't start parsing from rule \"" + options.startRule + "\".");
+ }
+
+ peg$startRuleFunction = peg$startRuleFunctions[options.startRule];
+ }
+
+ function text() {
+ return input.substring(peg$savedPos, peg$currPos);
+ }
+
+ function offset() {
+ return peg$savedPos;
+ }
+
+ function range() {
+ return {
+ source: peg$source,
+ start: peg$savedPos,
+ end: peg$currPos,
+ };
+ }
+
+ function location() {
+ return peg$computeLocation(peg$savedPos, peg$currPos);
+ }
+
+ function expected(description, location) {
+ location = location !== undefined
+ ? location
+ : peg$computeLocation(peg$savedPos, peg$currPos);
+
+ throw peg$buildStructuredError(
+ [peg$otherExpectation(description)],
+ input.substring(peg$savedPos, peg$currPos),
+ location
+ );
+ }
+
+ function error(message, location) {
+ location = location !== undefined
+ ? location
+ : peg$computeLocation(peg$savedPos, peg$currPos);
+
+ throw peg$buildSimpleError(message, location);
+ }
+
+ function peg$getUnicode(pos = peg$currPos) {
+ const cp = input.codePointAt(pos);
+ if (cp === undefined) {
+ return "";
+ }
+ return String.fromCodePoint(cp);
+ }
+
+ function peg$literalExpectation(text, ignoreCase) {
+ return { type: "literal", text, ignoreCase };
+ }
+
+ function peg$classExpectation(parts, inverted, ignoreCase, unicode) {
+ return { type: "class", parts, inverted, ignoreCase, unicode };
+ }
+
+ function peg$anyExpectation() {
+ return { type: "any" };
+ }
+
+ function peg$endExpectation() {
+ return { type: "end" };
+ }
+
+ function peg$otherExpectation(description) {
+ return { type: "other", description };
+ }
+
+ function peg$computePosDetails(pos) {
+ let details = peg$posDetailsCache[pos];
+ let p;
+
+ if (details) {
+ return details;
+ } else {
+ if (pos >= peg$posDetailsCache.length) {
+ p = peg$posDetailsCache.length - 1;
+ } else {
+ p = pos;
+ while (!peg$posDetailsCache[--p]) {}
+ }
+
+ details = peg$posDetailsCache[p];
+ details = {
+ line: details.line,
+ column: details.column,
+ };
+
+ while (p < pos) {
+ if (input.charCodeAt(p) === 10) {
+ details.line++;
+ details.column = 1;
+ } else {
+ details.column++;
+ }
+
+ p++;
+ }
+
+ peg$posDetailsCache[pos] = details;
+
+ return details;
+ }
+ }
+
+ function peg$computeLocation(startPos, endPos, offset) {
+ const startPosDetails = peg$computePosDetails(startPos);
+ const endPosDetails = peg$computePosDetails(endPos);
+
+ const res = {
+ source: peg$source,
+ start: {
+ offset: startPos,
+ line: startPosDetails.line,
+ column: startPosDetails.column,
+ },
+ end: {
+ offset: endPos,
+ line: endPosDetails.line,
+ column: endPosDetails.column,
+ },
+ };
+ if (offset && peg$source && (typeof peg$source.offset === "function")) {
+ res.start = peg$source.offset(res.start);
+ res.end = peg$source.offset(res.end);
+ }
+ return res;
+ }
+
+ function peg$fail(expected) {
+ if (peg$currPos < peg$maxFailPos) { return; }
+
+ if (peg$currPos > peg$maxFailPos) {
+ peg$maxFailPos = peg$currPos;
+ peg$maxFailExpected = [];
+ }
+
+ peg$maxFailExpected.push(expected);
+ }
+
+ function peg$buildSimpleError(message, location) {
+ return new peg$SyntaxError(message, null, null, location);
+ }
+
+ function peg$buildStructuredError(expected, found, location) {
+ return new peg$SyntaxError(
+ peg$SyntaxError.buildMessage(expected, found),
+ expected,
+ found,
+ location
+ );
+ }
+
+ function peg$parseDocument() {
+ let s0, s1, s2;
+
+ s0 = peg$currPos;
+ s1 = [];
+ s2 = peg$parseSegment();
+ while (s2 !== peg$FAILED) {
+ s1.push(s2);
+ s2 = peg$parseSegment();
+ }
+ peg$savedPos = s0;
+ s1 = peg$f0(s1);
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parseSegment() {
+ let s0;
+
+ s0 = peg$parseEnv();
+ if (s0 === peg$FAILED) {
+ s0 = peg$parseStrayEnd();
+ if (s0 === peg$FAILED) {
+ s0 = peg$parseLine();
+ }
+ }
+
+ return s0;
+ }
+
+ function peg$parseStrayEnd() {
+ let s0, s1;
+
+ s0 = peg$currPos;
+ s1 = peg$parseEndTag();
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f1(s1);
+ }
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parseEnv() {
+ let s0;
+
+ s0 = peg$parseVerbatimEnv();
+ if (s0 === peg$FAILED) {
+ s0 = peg$parseRegularEnv();
+ }
+
+ return s0;
+ }
+
+ function peg$parseVerbatimEnv() {
+ let s0, s1, s2, s3, s4, s5;
+
+ s0 = peg$currPos;
+ s1 = peg$parseBeginVerb();
+ if (s1 !== peg$FAILED) {
+ s2 = [];
+ s3 = peg$currPos;
+ s4 = peg$currPos;
+ peg$silentFails++;
+ s5 = peg$parseEndVerb();
+ peg$silentFails--;
+ if (s5 === peg$FAILED) {
+ s4 = undefined;
+ } else {
+ peg$currPos = s4;
+ s4 = peg$FAILED;
+ }
+ if (s4 !== peg$FAILED) {
+ if (input.length > peg$currPos) {
+ s5 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e0); }
+ }
+ if (s5 !== peg$FAILED) {
+ s4 = [s4, s5];
+ s3 = s4;
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ while (s3 !== peg$FAILED) {
+ s2.push(s3);
+ s3 = peg$currPos;
+ s4 = peg$currPos;
+ peg$silentFails++;
+ s5 = peg$parseEndVerb();
+ peg$silentFails--;
+ if (s5 === peg$FAILED) {
+ s4 = undefined;
+ } else {
+ peg$currPos = s4;
+ s4 = peg$FAILED;
+ }
+ if (s4 !== peg$FAILED) {
+ if (input.length > peg$currPos) {
+ s5 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e0); }
+ }
+ if (s5 !== peg$FAILED) {
+ s4 = [s4, s5];
+ s3 = s4;
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ }
+ s3 = peg$parseEndVerb();
+ if (s3 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f2(s1, s2, s3);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseBeginVerb() {
+ let s0, s1, s2, s3;
+
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 7) === peg$c0) {
+ s1 = peg$c0;
+ peg$currPos += 7;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e1); }
+ }
+ if (s1 !== peg$FAILED) {
+ if (input.substr(peg$currPos, 8) === peg$c1) {
+ s2 = peg$c1;
+ peg$currPos += 8;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e2); }
+ }
+ if (s2 === peg$FAILED) {
+ if (input.substr(peg$currPos, 5) === peg$c2) {
+ s2 = peg$c2;
+ peg$currPos += 5;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e3); }
+ }
+ }
+ if (s2 !== peg$FAILED) {
+ if (input.charCodeAt(peg$currPos) === 125) {
+ s3 = peg$c3;
+ peg$currPos++;
+ } else {
+ s3 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e4); }
+ }
+ if (s3 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f3(s2);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseEndVerb() {
+ let s0, s1, s2, s3;
+
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 5) === peg$c4) {
+ s1 = peg$c4;
+ peg$currPos += 5;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e5); }
+ }
+ if (s1 !== peg$FAILED) {
+ if (input.substr(peg$currPos, 8) === peg$c1) {
+ s2 = peg$c1;
+ peg$currPos += 8;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e2); }
+ }
+ if (s2 === peg$FAILED) {
+ if (input.substr(peg$currPos, 5) === peg$c2) {
+ s2 = peg$c2;
+ peg$currPos += 5;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e3); }
+ }
+ }
+ if (s2 !== peg$FAILED) {
+ if (input.charCodeAt(peg$currPos) === 125) {
+ s3 = peg$c3;
+ peg$currPos++;
+ } else {
+ s3 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e4); }
+ }
+ if (s3 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f4(s2);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseRegularEnv() {
+ let s0, s1, s2, s3, s4, s5;
+
+ s0 = peg$currPos;
+ s1 = peg$parseBeginTag();
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ s3 = [];
+ s4 = peg$parseEnvContent();
+ while (s4 !== peg$FAILED) {
+ s3.push(s4);
+ s4 = peg$parseEnvContent();
+ }
+ s4 = peg$parse_();
+ s5 = peg$parseEndTag();
+ if (s5 === peg$FAILED) {
+ s5 = null;
+ }
+ peg$savedPos = s0;
+ s0 = peg$f5(s1, s3, s5);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseBeginTag() {
+ let s0, s1, s2, s3, s4;
+
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 7) === peg$c0) {
+ s1 = peg$c0;
+ peg$currPos += 7;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e1); }
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parseEnvName();
+ if (s2 !== peg$FAILED) {
+ if (input.charCodeAt(peg$currPos) === 125) {
+ s3 = peg$c3;
+ peg$currPos++;
+ } else {
+ s3 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e4); }
+ }
+ if (s3 !== peg$FAILED) {
+ s4 = peg$parseTail();
+ peg$savedPos = s0;
+ s0 = peg$f6(s2, s4);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseEndTag() {
+ let s0, s1, s2, s3;
+
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 5) === peg$c4) {
+ s1 = peg$c4;
+ peg$currPos += 5;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e5); }
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parseEnvName();
+ if (s2 !== peg$FAILED) {
+ if (input.charCodeAt(peg$currPos) === 125) {
+ s3 = peg$c3;
+ peg$currPos++;
+ } else {
+ s3 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e4); }
+ }
+ if (s3 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f7(s2);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseEnvName() {
+ let s0, s1, s2;
+
+ s0 = peg$currPos;
+ s1 = [];
+ s2 = input.charAt(peg$currPos);
+ if (peg$r0.test(s2)) {
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e6); }
+ }
+ if (s2 !== peg$FAILED) {
+ while (s2 !== peg$FAILED) {
+ s1.push(s2);
+ s2 = input.charAt(peg$currPos);
+ if (peg$r0.test(s2)) {
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e6); }
+ }
+ }
+ } else {
+ s1 = peg$FAILED;
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f8(s1);
+ }
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parseEnvContent() {
+ let s0;
+
+ s0 = peg$parseEnv();
+ if (s0 === peg$FAILED) {
+ s0 = peg$parseCommand();
+ if (s0 === peg$FAILED) {
+ s0 = peg$parseLine();
+ }
+ }
+
+ return s0;
+ }
+
+ function peg$parseCommand() {
+ let s0, s1, s2;
+
+ s0 = peg$currPos;
+ s1 = peg$parseCommandStart();
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parseTail();
+ peg$savedPos = s0;
+ s0 = peg$f9(s1, s2);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseCommandStart() {
+ let s0, s1, s2, s3, s4, s5;
+
+ s0 = peg$currPos;
+ if (input.charCodeAt(peg$currPos) === 92) {
+ s1 = peg$c5;
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e7); }
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = peg$currPos;
+ peg$silentFails++;
+ if (input.substr(peg$currPos, 6) === peg$c6) {
+ s3 = peg$c6;
+ peg$currPos += 6;
+ } else {
+ s3 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e8); }
+ }
+ peg$silentFails--;
+ if (s3 === peg$FAILED) {
+ s2 = undefined;
+ } else {
+ peg$currPos = s2;
+ s2 = peg$FAILED;
+ }
+ if (s2 !== peg$FAILED) {
+ s3 = peg$currPos;
+ peg$silentFails++;
+ if (input.substr(peg$currPos, 4) === peg$c7) {
+ s4 = peg$c7;
+ peg$currPos += 4;
+ } else {
+ s4 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e9); }
+ }
+ peg$silentFails--;
+ if (s4 === peg$FAILED) {
+ s3 = undefined;
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ if (s3 !== peg$FAILED) {
+ s4 = [];
+ s5 = input.charAt(peg$currPos);
+ if (peg$r1.test(s5)) {
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e10); }
+ }
+ if (s5 !== peg$FAILED) {
+ while (s5 !== peg$FAILED) {
+ s4.push(s5);
+ s5 = input.charAt(peg$currPos);
+ if (peg$r1.test(s5)) {
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e10); }
+ }
+ }
+ } else {
+ s4 = peg$FAILED;
+ }
+ if (s4 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f10(s4);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseTail() {
+ let s0, s1, s2;
+
+ s0 = peg$currPos;
+ s1 = [];
+ s2 = peg$parseTailPart();
+ while (s2 !== peg$FAILED) {
+ s1.push(s2);
+ s2 = peg$parseTailPart();
+ }
+ peg$savedPos = s0;
+ s1 = peg$f11(s1);
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parseTailPart() {
+ let s0, s1, s2, s3, s4, s5, s6;
+
+ s0 = peg$parseComment();
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ s1 = peg$parseOpen();
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f12();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ s1 = peg$parseClose();
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f13();
+ }
+ s0 = s1;
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ peg$savedPos = peg$currPos;
+ s1 = peg$f14();
+ if (s1) {
+ s1 = undefined;
+ } else {
+ s1 = peg$FAILED;
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = peg$currPos;
+ peg$silentFails++;
+ s3 = peg$parseEOL();
+ peg$silentFails--;
+ if (s3 === peg$FAILED) {
+ s2 = undefined;
+ } else {
+ peg$currPos = s2;
+ s2 = peg$FAILED;
+ }
+ if (s2 !== peg$FAILED) {
+ s3 = peg$currPos;
+ peg$silentFails++;
+ s4 = peg$parseCommandStart();
+ peg$silentFails--;
+ if (s4 === peg$FAILED) {
+ s3 = undefined;
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ if (s3 !== peg$FAILED) {
+ s4 = peg$currPos;
+ peg$silentFails++;
+ s5 = peg$parseBeginStart();
+ peg$silentFails--;
+ if (s5 === peg$FAILED) {
+ s4 = undefined;
+ } else {
+ peg$currPos = s4;
+ s4 = peg$FAILED;
+ }
+ if (s4 !== peg$FAILED) {
+ s5 = peg$currPos;
+ peg$silentFails++;
+ s6 = peg$parseEndStart();
+ peg$silentFails--;
+ if (s6 === peg$FAILED) {
+ s5 = undefined;
+ } else {
+ peg$currPos = s5;
+ s5 = peg$FAILED;
+ }
+ if (s5 !== peg$FAILED) {
+ if (input.length > peg$currPos) {
+ s6 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s6 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e0); }
+ }
+ if (s6 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f15(s6);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ peg$savedPos = peg$currPos;
+ s1 = peg$f16();
+ if (s1) {
+ s1 = undefined;
+ } else {
+ s1 = peg$FAILED;
+ }
+ if (s1 !== peg$FAILED) {
+ if (input.length > peg$currPos) {
+ s2 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e0); }
+ }
+ if (s2 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f17(s2);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ }
+ }
+ }
+ }
+
+ return s0;
+ }
+
+ function peg$parseComment() {
+ let s0, s1, s2, s3, s4, s5;
+
+ s0 = peg$currPos;
+ if (input.charCodeAt(peg$currPos) === 37) {
+ s1 = peg$c8;
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e11); }
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = [];
+ s3 = peg$currPos;
+ s4 = peg$currPos;
+ peg$silentFails++;
+ s5 = peg$parseEOL();
+ peg$silentFails--;
+ if (s5 === peg$FAILED) {
+ s4 = undefined;
+ } else {
+ peg$currPos = s4;
+ s4 = peg$FAILED;
+ }
+ if (s4 !== peg$FAILED) {
+ if (input.length > peg$currPos) {
+ s5 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e0); }
+ }
+ if (s5 !== peg$FAILED) {
+ s4 = [s4, s5];
+ s3 = s4;
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ while (s3 !== peg$FAILED) {
+ s2.push(s3);
+ s3 = peg$currPos;
+ s4 = peg$currPos;
+ peg$silentFails++;
+ s5 = peg$parseEOL();
+ peg$silentFails--;
+ if (s5 === peg$FAILED) {
+ s4 = undefined;
+ } else {
+ peg$currPos = s4;
+ s4 = peg$FAILED;
+ }
+ if (s4 !== peg$FAILED) {
+ if (input.length > peg$currPos) {
+ s5 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e0); }
+ }
+ if (s5 !== peg$FAILED) {
+ s4 = [s4, s5];
+ s3 = s4;
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ }
+ peg$savedPos = s0;
+ s0 = peg$f18();
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseOpen() {
+ let s0;
+
+ s0 = input.charAt(peg$currPos);
+ if (peg$r2.test(s0)) {
+ peg$currPos++;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e12); }
+ }
+
+ return s0;
+ }
+
+ function peg$parseClose() {
+ let s0;
+
+ s0 = input.charAt(peg$currPos);
+ if (peg$r3.test(s0)) {
+ peg$currPos++;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e13); }
+ }
+
+ return s0;
+ }
+
+ function peg$parseBeginStart() {
+ let s0;
+
+ if (input.substr(peg$currPos, 7) === peg$c0) {
+ s0 = peg$c0;
+ peg$currPos += 7;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e1); }
+ }
+
+ return s0;
+ }
+
+ function peg$parseEndStart() {
+ let s0;
+
+ if (input.substr(peg$currPos, 5) === peg$c4) {
+ s0 = peg$c4;
+ peg$currPos += 5;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e5); }
+ }
+
+ return s0;
+ }
+
+ function peg$parseLine() {
+ let s0, s1, s2;
+
+ s0 = peg$currPos;
+ s1 = [];
+ s2 = peg$parseLinePart();
+ if (s2 !== peg$FAILED) {
+ while (s2 !== peg$FAILED) {
+ s1.push(s2);
+ s2 = peg$parseLinePart();
+ }
+ } else {
+ s1 = peg$FAILED;
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parseEOL();
+ if (s2 === peg$FAILED) {
+ s2 = null;
+ }
+ peg$savedPos = s0;
+ s0 = peg$f19(s1, s2);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ s1 = peg$parseEOL();
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f20(s1);
+ }
+ s0 = s1;
+ }
+
+ return s0;
+ }
+
+ function peg$parseLinePart() {
+ let s0, s1, s2, s3, s4;
+
+ s0 = peg$parseComment();
+ if (s0 === peg$FAILED) {
+ s0 = peg$parseCommand();
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ s1 = peg$currPos;
+ peg$silentFails++;
+ s2 = peg$parseBeginStart();
+ peg$silentFails--;
+ if (s2 === peg$FAILED) {
+ s1 = undefined;
+ } else {
+ peg$currPos = s1;
+ s1 = peg$FAILED;
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = peg$currPos;
+ peg$silentFails++;
+ s3 = peg$parseEndStart();
+ peg$silentFails--;
+ if (s3 === peg$FAILED) {
+ s2 = undefined;
+ } else {
+ peg$currPos = s2;
+ s2 = peg$FAILED;
+ }
+ if (s2 !== peg$FAILED) {
+ s3 = peg$currPos;
+ peg$silentFails++;
+ s4 = peg$parseEOL();
+ peg$silentFails--;
+ if (s4 === peg$FAILED) {
+ s3 = undefined;
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ if (s3 !== peg$FAILED) {
+ if (input.length > peg$currPos) {
+ s4 = input.charAt(peg$currPos);
+ peg$currPos++;
+ } else {
+ s4 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e0); }
+ }
+ if (s4 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f21(s4);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ }
+ }
+
+ return s0;
+ }
+
+ function peg$parseEOL() {
+ let s0;
+
+ if (input.substr(peg$currPos, 2) === peg$c9) {
+ s0 = peg$c9;
+ peg$currPos += 2;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e14); }
+ }
+ if (s0 === peg$FAILED) {
+ s0 = input.charAt(peg$currPos);
+ if (peg$r4.test(s0)) {
+ peg$currPos++;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e15); }
+ }
+ }
+
+ return s0;
+ }
+
+ function peg$parse_() {
+ let s0, s1;
+
+ s0 = [];
+ s1 = input.charAt(peg$currPos);
+ if (peg$r5.test(s1)) {
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e16); }
+ }
+ while (s1 !== peg$FAILED) {
+ s0.push(s1);
+ s1 = input.charAt(peg$currPos);
+ if (peg$r5.test(s1)) {
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e16); }
+ }
+ }
+
+ return s0;
+ }
+
+
+ let depth = 0;
+
+ function loc() {
+ const l = location();
+ return { line: l.start.line, column: l.start.column };
+ }
+
+ peg$result = peg$startRuleFunction();
+
+ const peg$success = (peg$result !== peg$FAILED && peg$currPos === input.length);
+ function peg$throw() {
+ if (peg$result !== peg$FAILED && peg$currPos < input.length) {
+ peg$fail(peg$endExpectation());
+ }
+
+ throw peg$buildStructuredError(
+ peg$maxFailExpected,
+ peg$maxFailPos < input.length ? peg$getUnicode(peg$maxFailPos) : null,
+ peg$maxFailPos < input.length
+ ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)
+ : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
+ );
+ }
+ if (options.peg$library) {
+ return /** @type {any} */ ({
+ peg$result,
+ peg$currPos,
+ peg$FAILED,
+ peg$maxFailExpected,
+ peg$maxFailPos,
+ peg$success,
+ peg$throw: peg$success ? undefined : peg$throw,
+ });
+ }
+ if (peg$success) {
+ return peg$result;
+ } else {
+ peg$throw();
+ }
+}
+
+module.exports = {
+ StartRules: ["Document"],
+ SyntaxError: peg$SyntaxError,
+ parse: peg$parse,
+};
+
+},{}],10:[function(require,module,exports){
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -202,6 +1713,16 @@ const ignore_1 = __importDefault(require("./lib/ignore"));
const parser_1 = __importDefault(require("./lib/parser"));
class LaTeX2HTML5 {
constructor(Text = text_1.default, Headers = headers_1.default, Environments = environments_1.default, Ignore = ignore_1.default, PSTricks = pstricks_1.pstricks, Views = {}) {
+ this.lastDiagnostics = [];
+ /**
+ * Which language documents are read as, when they do not declare one.
+ *
+ * An application that only ever renders LaTeX2JS content sets this once
+ * rather than editing every document; a document's own
+ * `\psset{dialect=...}` still overrides it. Defaults to `pstricks`, so an
+ * undeclared extension is reported rather than passing unnoticed.
+ */
+ this.dialect = 'pstricks';
this.Text = Text;
this.Headers = Headers;
this.Environments = Environments;
@@ -251,6 +1772,7 @@ class LaTeX2HTML5 {
parse(text) {
const parser = new parser_1.default(this);
const parsed = parser.parse(text);
+ this.lastDiagnostics = parser.diagnostics;
parsed.forEach((element) => {
if (!element.hasOwnProperty('type')) {
throw new Error('no type!');
@@ -262,62 +1784,325 @@ class LaTeX2HTML5 {
}
exports.default = LaTeX2HTML5;
-},{"./lib/environments":9,"./lib/headers":10,"./lib/ignore":11,"./lib/parser":12,"./lib/text":13,"@latex2js/pstricks":16}],9:[function(require,module,exports){
+},{"./lib/environments":13,"./lib/headers":14,"./lib/ignore":15,"./lib/parser":16,"./lib/text":17,"@latex2js/pstricks":20}],11:[function(require,module,exports){
+"use strict";
+/**
+ * Document counters for sectioning and theorem-like environments.
+ *
+ * Numbering is what makes a document cross-referenceable — "see Theorem 3" only
+ * works if theorems are numbered — and LaTeX2JS emitted none, so a rendered
+ * document could not be cited from or navigated the way its printed form can.
+ *
+ * Equations are deliberately absent: those stay with MathJax, which already
+ * numbers AMS environments and resolves `\label`/`\ref` against them. Splitting
+ * that between two systems would give a document two disagreeing sets of
+ * numbers.
+ */
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.NUMBERED_ENVIRONMENTS = exports.Counters = exports.SECTION_LEVELS = void 0;
+/** The sectioning levels, outermost first. */
+exports.SECTION_LEVELS = ['section', 'subsection', 'subsubsection'];
+/**
+ * Counters for one document.
+ *
+ * A parser instance is reused across documents, so these are reset at the start
+ * of every parse; otherwise a second document would continue the first one's
+ * numbering.
+ */
+class Counters {
+ constructor() {
+ this.sections = exports.SECTION_LEVELS.map(() => 0);
+ this.environments = {};
+ }
+ /** Starts a fresh document. */
+ reset() {
+ this.sections = exports.SECTION_LEVELS.map(() => 0);
+ this.environments = {};
+ }
+ /**
+ * Advances a sectioning level and returns its number.
+ *
+ * Deeper levels reset, so a new section restarts subsection numbering. A
+ * subsection appearing before any section reports `0.1`, which is what LaTeX
+ * does rather than an error — the document is odd, not broken.
+ *
+ * @param level - which sectioning level is starting
+ * @returns the dotted number, such as `2.3`
+ */
+ section(level) {
+ const depth = exports.SECTION_LEVELS.indexOf(level);
+ this.sections[depth] += 1;
+ for (let i = depth + 1; i < this.sections.length; i++)
+ this.sections[i] = 0;
+ return this.sections.slice(0, depth + 1).join('.');
+ }
+ /**
+ * Advances the counter for a theorem-like environment and returns its number.
+ *
+ * Each kind counts independently — Theorem 1 and Lemma 1 can both exist —
+ * which is what a plain `\newtheorem{name}{Name}` declaration gives.
+ *
+ * @param name - the environment name, such as `theorem`
+ * @returns the next number for that kind
+ */
+ environment(name) {
+ this.environments[name] = (this.environments[name] ?? 0) + 1;
+ return this.environments[name];
+ }
+}
+exports.Counters = Counters;
+/** Environments that carry a number. `proof` and quotations deliberately do not. */
+exports.NUMBERED_ENVIRONMENTS = [
+ 'theorem',
+ 'lemma',
+ 'corollary',
+ 'proposition',
+ 'definition',
+ 'axiom',
+ 'claim',
+ 'example',
+ 'remark',
+ 'note',
+ 'exercise',
+ 'question',
+ 'problem',
+ 'solution',
+];
+
+},{}],12:[function(require,module,exports){
"use strict";
+/**
+ * Which constructs belong to the LaTeX2JS dialect rather than to PSTricks.
+ *
+ * LaTeX2JS accepts a superset of PSTricks: some of it is the deliberate reason
+ * this project exists (the interactive macros), some of it is convenience
+ * (infix plot bodies, CSS colour names), and some is a genuine semantic fork
+ * (`log` is natural log here, base 10 there). Until a document could declare
+ * which language it was written in, none of that was distinguishable from a
+ * defect — by a reader, a test, or the author.
+ *
+ * A document declares `\psset{dialect=latex2js}`, or the embedding application
+ * sets it once. Anything left undeclared is read as PSTricks and reported.
+ *
+ * Reporting is all this does. The dialect changes a small number of documented
+ * semantics (see `Dialect` in @latex2js/settings); it does not switch the
+ * renderer, and nothing here refuses to draw.
+ */
Object.defineProperty(exports, "__esModule", { value: true });
-const environments = ['pspicture', 'verbatim', 'enumerate', 'print', 'nicebox'];
+exports.dialectUses = dialectUses;
+/** Colour names the browser knows and xcolor does not. */
+const CSS_ONLY_COLORS = ['lightblue', 'lightgray', 'lightgrey', 'lightgreen', 'darkblue', 'darkgreen', 'pink', 'gold', 'silver', 'navy', 'teal', 'lime', 'aqua', 'fuchsia'];
+/** Commands with no PSTricks counterpart at all. */
+const LATEX2JS_ONLY_COMMANDS = {
+ userline: 'draws a line the reader can drag; PSTricks has no interactive graphics',
+ uservariable: 'binds a value to the pointer position; PSTricks has no such binding',
+ slider: 'renders a control the reader can move; PSTricks has no such control',
+};
+/**
+ * Reports the dialect constructs one parsed command uses.
+ *
+ * @param name - the command name, without its backslash
+ * @param raw - the command's source text
+ * @param options - its parsed options, or null when it has none
+ * @returns every construct the command relies on, empty when it is plain PSTricks
+ */
+function dialectUses(name, raw, options) {
+ const uses = [];
+ const only = LATEX2JS_ONLY_COMMANDS[name];
+ if (only)
+ uses.push({ construct: `\\${name}`, detail: only });
+ // A bare key is a syntax error to PSTricks, which expects `key=value`.
+ const bare = /\[([^\]]*)\]/.exec(raw);
+ if (bare) {
+ for (const part of bare[1].split(',')) {
+ const token = part.trim();
+ if (token && !token.includes('=')) {
+ uses.push({
+ construct: 'bare option flag',
+ detail: `\`${token}\` has no value; PSTricks requires \`${token}=true\``,
+ });
+ }
+ }
+ }
+ if (options) {
+ for (const key of ['linecolor', 'fillcolor', 'hatchcolor', 'gridcolor']) {
+ const value = String(options[key] ?? '').trim().toLowerCase();
+ if (CSS_ONLY_COLORS.indexOf(value) !== -1) {
+ uses.push({
+ construct: 'CSS colour name',
+ detail: `\`${value}\` is a browser colour; xcolor does not define it`,
+ });
+ }
+ }
+ if (options.plotpoints !== undefined && Number(options.plotpoints) === 1) {
+ uses.push({
+ construct: 'plotpoints=1',
+ detail: 'PSTricks requires at least 2 samples',
+ });
+ }
+ }
+ if (name === 'psplot')
+ uses.push(...plotBodyUses(raw, options));
+ return uses;
+}
+/**
+ * Reports what a `\psplot` body relies on.
+ *
+ * The dialects have opposite defaults here: LaTeX2JS always reads the body as
+ * an infix expression, while PSTricks reads RPN PostScript unless told
+ * `algebraic=true`. A body is therefore worth reporting whenever the document
+ * has not asked for algebraic mode, whatever it contains.
+ */
+function plotBodyUses(raw, options) {
+ const uses = [];
+ const groups = raw.match(/\{([^{}]*)\}/g) || [];
+ const bodies = groups.map((g) => g.slice(1, -1));
+ const declaredAlgebraic = String(options?.algebraic ?? '').toLowerCase() === 'true';
+ if (!declaredAlgebraic) {
+ uses.push({
+ construct: 'infix plot body',
+ detail: 'read as an infix expression; PSTricks reads RPN PostScript unless algebraic=true',
+ });
+ }
+ const body = bodies[bodies.length - 1] ?? '';
+ if (/\bpow\s*\(/.test(body)) {
+ uses.push({ construct: 'pow()', detail: 'PSTricks has no pow function, only the ^ operator' });
+ }
+ if (/\blog\s*\(/.test(body)) {
+ uses.push({ construct: 'log()', detail: 'natural log here, base 10 in PSTricks' });
+ }
+ // The two bounds precede the body; a non-numeric one is an expression over
+ // variables, which PSTricks cannot evaluate.
+ for (const bound of bodies.slice(0, -1)) {
+ if (bound.trim() && !/^-?[\d.]+$/.test(bound.trim())) {
+ uses.push({
+ construct: 'variable plot bound',
+ detail: `\`${bound.trim()}\` is an expression; PSTricks needs a literal number`,
+ });
+ }
+ }
+ return uses;
+}
+
+},{}],13:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const environments = ['pspicture', 'verbatim', 'enumerate', 'print', 'nicebox', 'itemize', 'description'];
exports.default = environments;
-},{}],10:[function(require,module,exports){
+},{}],14:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Functions = exports.Expressions = void 0;
exports.Expressions = {
bq: /\\begin\{quotation\}/,
- claim: /\\begin\{claim\}/,
- corollary: /\\begin\{corollary\}/,
- definition: /\\begin\{definition\}/,
- endclaim: /\\end\{claim\}/,
- endcorallary: /\\end\{corallary\}/,
- enddefinition: /\\end\{definition\}/,
- endexample: /\\end\{example\}/,
- endproblem: /\\end\{problem\}/,
- endsolution: /\\end\{solution\}/,
- endtheorem: /\\end\{theorem\}/,
+ claim: /\\begin\{claim\*?\}/,
+ corollary: /\\begin\{corollary\*?\}/,
+ definition: /\\begin\{definition\*?\}/,
+ lemma: /\\begin\{lemma\*?\}/,
+ proposition: /\\begin\{proposition\*?\}/,
+ axiom: /\\begin\{axiom\*?\}/,
+ remark: /\\begin\{remark\*?\}/,
+ note: /\\begin\{note\*?\}/,
+ exercise: /\\begin\{exercise\*?\}/,
+ question: /\\begin\{question\*?\}/,
+ endclaim: /\\end\{claim\*?\}/,
+ endcorollary: /\\end\{corollary\*?\}/,
+ enddefinition: /\\end\{definition\*?\}/,
+ endexample: /\\end\{example\*?\}/,
+ endlemma: /\\end\{lemma\*?\}/,
+ endproposition: /\\end\{proposition\*?\}/,
+ endaxiom: /\\end\{axiom\*?\}/,
+ endremark: /\\end\{remark\*?\}/,
+ endnote: /\\end\{note\*?\}/,
+ endexercise: /\\end\{exercise\*?\}/,
+ endquestion: /\\end\{question\*?\}/,
+ endproblem: /\\end\{problem\*?\}/,
+ endsolution: /\\end\{solution\*?\}/,
+ endtheorem: /\\end\{theorem\*?\}/,
eq: /\\end\{quotation\}/,
- example: /\\begin\{example\}/,
- problem: /\\begin\{problem\}/,
+ example: /\\begin\{example\*?\}/,
+ problem: /\\begin\{problem\*?\}/,
proof: /\\begin\{proof\}/,
qed: /\\end\{proof\}/,
- solution: /\\begin\{solution\}/,
- theorem: /\\begin\{theorem\}/
+ solution: /\\begin\{solution\*?\}/,
+ theorem: /\\begin\{theorem\*?\}/
};
+/**
+ * Opens a theorem-like environment with a run-in heading.
+ *
+ * LaTeX sets these as `**Theorem 1.** statement` on one line, not as a heading
+ * with the statement beneath it — the label is part of the sentence. The
+ * wrapper lets CSS carry that, and lets the body take the italic that amsthm
+ * gives a theorem and withholds from a remark.
+ *
+ * The number comes from the parser through the receiver, so this registry
+ * carries no parser internals; a host that supplies none still renders the
+ * heading, just unnumbered. Each kind counts independently — Theorem 1 and
+ * Lemma 1 can both exist — which is what a plain \newtheorem gives.
+ *
+ * @param title - the label a reader sees, such as `Theorem`
+ * @param name - the environment name, for the counter and the body style
+ * @param parser - the receiver, when the caller supplied one
+ * @param match - the matched \begin, so a starred form can opt out
+ * @returns the opening markup, closed by the matching `end` entry
+ */
+function headed(title, name, parser, match) {
+ const raw = Array.isArray(match) ? String(match[0] ?? '') : String(match ?? '');
+ const number = parser && typeof parser.environmentNumber === 'function'
+ ? parser.environmentNumber(name, raw)
+ : null;
+ const label = number === null || number === undefined ? '' : ' ' + number;
+ return ('' +
+ '
' + title + label + '
');
+}
+/** Closes an environment opened by {@link headed}. */
+function closed() {
+ return '
';
+}
exports.Functions = {
bq: () => '',
- claim: () => '
Claim
',
- corollary: () => 'Corollary
',
- definition: () => 'Definition
',
- endclaim: () => '',
- endcorollary: () => '',
- enddefinition: () => '',
- endexample: () => '',
- endproblem: () => '',
- endsolution: () => '',
- endtheorem: () => '',
+ claim(m) { return headed('Claim', 'claim', this, m); },
+ corollary(m) { return headed('Corollary', 'corollary', this, m); },
+ definition(m) { return headed('Definition', 'definition', this, m); },
+ lemma(m) { return headed('Lemma', 'lemma', this, m); },
+ proposition(m) { return headed('Proposition', 'proposition', this, m); },
+ axiom(m) { return headed('Axiom', 'axiom', this, m); },
+ remark(m) { return headed('Remark', 'remark', this, m); },
+ note(m) { return headed('Note', 'note', this, m); },
+ exercise(m) { return headed('Exercise', 'exercise', this, m); },
+ question(m) { return headed('Question', 'question', this, m); },
+ endclaim: () => closed(),
+ endcorollary: () => closed(),
+ enddefinition: () => closed(),
+ endexample: () => closed(),
+ endlemma: () => closed(),
+ endproposition: () => closed(),
+ endaxiom: () => closed(),
+ endremark: () => closed(),
+ endnote: () => closed(),
+ endexercise: () => closed(),
+ endquestion: () => closed(),
+ endproblem: () => closed(),
+ endsolution: () => closed(),
+ endtheorem: () => closed(),
eq: () => '',
- example: () => 'Example
',
- problem: () => 'Problem
',
- proof: () => 'Proof
',
- qed: () => '$\\qed$',
- solution: () => 'Solution
',
- theorem: () => 'Theorem
'
+ example(m) { return headed('Example', 'example', this, m); },
+ problem(m) { return headed('Problem', 'problem', this, m); },
+ proof: () => 'Proof
',
+ // amsthm closes a proof with an open square. Emitted as a character rather
+ // than as math: MathJax defines no \qed, so the previous `$\qed$` surfaced
+ // an "Undefined control sequence" box at the end of every proof.
+ qed: () => '□',
+ solution(m) { return headed('Solution', 'solution', this, m); },
+ theorem(m) { return headed('Theorem', 'theorem', this, m); }
};
exports.default = {
Expressions: exports.Expressions,
Functions: exports.Functions
};
-},{}],11:[function(require,module,exports){
+},{}],15:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ignore = [
@@ -343,9 +2128,226 @@ const ignore = [
];
exports.default = ignore;
-},{}],12:[function(require,module,exports){
+},{}],16:[function(require,module,exports){
"use strict";
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || (function () {
+ var ownKeys = function(o) {
+ ownKeys = Object.getOwnPropertyNames || function (o) {
+ var ar = [];
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
+ return ar;
+ };
+ return ownKeys(o);
+ };
+ return function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
+ __setModuleDefault(result, mod);
+ return result;
+ };
+})();
Object.defineProperty(exports, "__esModule", { value: true });
+const pegParser = __importStar(require("../grammar/parser.js"));
+const dialect_1 = require("./dialect");
+const settings_1 = require("@latex2js/settings");
+const utils_1 = require("@latex2js/utils");
+const counters_1 = require("./counters");
+/**
+ * Keys `\psset` may declare that are not style defaults for a shape.
+ *
+ * Units are consumed when the picture is set up and are already folded into
+ * every coordinate by the time a command is parsed; copying them onto the
+ * shape as well would apply them a second time. The dialect is a document
+ * property, not a drawing option.
+ */
+const PSSET_NON_STYLE = new Set(['unit', 'runit', 'xunit', 'yunit', 'dialect']);
+/**
+ * `\definecolor{name}{model}{spec}`.
+ *
+ * A preamble declaration rather than content, so it is intercepted where
+ * \psset is: the grammar delivers a command inside the line that holds it,
+ * not as a node of its own, and anything not intercepted is rendered as text.
+ */
+const DEFINECOLOR_RE = /\\definecolor\s*\{([^}]*)\}\s*\{([^}]*)\}\s*\{([^}]*)\}/;
+/**
+ * The style defaults out of a parsed `\psset`.
+ *
+ * Kept apart from `settings`, which is not purely psset state: the pspicture
+ * parse function is invoked with `settings` as its receiver and assigns the
+ * picture bounds onto it, so `settings` also carries x0, y0, x1, y1, w and h.
+ * Copying those onto a shape overwrites its computed geometry with the corner
+ * of the picture.
+ *
+ * @param declared - the result of parsing one \psset
+ * @returns only the keys that are defaults for a later command
+ */
+function pssetStyle(declared) {
+ const style = {};
+ for (const [key, value] of Object.entries(declared || {})) {
+ if (PSSET_NON_STYLE.has(key))
+ continue;
+ if (value === undefined)
+ continue;
+ style[key] = value;
+ }
+ return style;
+}
+/**
+ * Applies the `\psset` defaults in force where a command was written.
+ *
+ * A command's own brackets win, its psset defaults come next, and the
+ * hardcoded default in its parse function is only the last resort. That order
+ * cannot be expressed by assigning before or after `parseOptions`, because by
+ * the time the parse function returns there is no way to tell a hardcoded
+ * `linecolor: 'black'` from one the author wrote — so the inline options are
+ * re-read here, and a psset key is applied to anything the author left out.
+ *
+ * Without this, psset parsed and then discarded every style key: a picture
+ * opening with `\psset{linewidth=2pt,linestyle=dashed,fillstyle=solid}` drew
+ * plain thin outlines.
+ *
+ * @param data - the parsed command, mutated in place
+ * @param settings - the psset state at this command's position in the source
+ * @param raw - the command's source, read for its own bracket group
+ */
+function applyPsset(data, style, raw) {
+ if (!data || !style)
+ return;
+ const bracket = typeof raw === 'string' ? raw.match(/\[([^\]]*)\]/) : null;
+ const inline = bracket ? bracket[1].split(',').map((p) => p.split('=')[0].trim()) : [];
+ for (const [key, value] of Object.entries(style)) {
+ if (inline.indexOf(key) !== -1)
+ continue;
+ data[key] = value;
+ }
+}
+/**
+ * The environment a command's coordinates should be computed against, once the
+ * `\psset` above it has changed the units.
+ *
+ * A picture fixes its units when `\begin{pspicture}` is read, so a later
+ * `\psset{xunit=2}` inside it had no effect at all — `\psellipse(0,0)(1,1.5)`
+ * came out taller than wide where PSTricks draws it wider than tall.
+ *
+ * The picture's origin does not move when the units change: the box was laid
+ * out with the units in force at `\begin`, and only the coordinates written
+ * after the declaration are rescaled. `X(v)` is `(w - x1) * xunit + v * xunit`,
+ * so holding the first term at its original value while the second takes the
+ * new unit means solving for the `w` that keeps the offset — which is what the
+ * adjusted bounds below do.
+ *
+ * @param env - the picture environment, carrying the units from \begin
+ * @param units - the units in force where the command was written
+ * @returns `env` itself when nothing changed, else a rescaled copy
+ */
+function envForUnits(env, units) {
+ if (!env || !units)
+ return env;
+ const xunit = Number(units.xunit);
+ const yunit = Number(units.yunit);
+ const sameX = !isFinite(xunit) || xunit === env.xunit;
+ const sameY = !isFinite(yunit) || yunit === env.yunit;
+ const sameR = !isFinite(Number(units.runit)) || Number(units.runit) === env.runit;
+ if (sameX && sameY && sameR)
+ return env;
+ const scaled = { ...env };
+ // A radius follows runit, and needs no origin correction: it is a length,
+ // not a position.
+ const runit = Number(units.runit);
+ if (isFinite(runit) && runit > 0)
+ scaled.runit = runit;
+ if (!sameX && xunit > 0) {
+ const originX = (env.w - env.x1) * env.xunit;
+ scaled.xunit = xunit;
+ scaled.w = originX / xunit + env.x1;
+ }
+ if (!sameY && yunit > 0) {
+ const originY = env.y1 * env.yunit;
+ scaled.yunit = yunit;
+ scaled.y1 = originY / yunit;
+ }
+ return scaled;
+}
+/**
+ * The brace group of an `\rput`, matched by depth rather than by regex.
+ *
+ * The rput expression ends in `\{(.*)\}`, which is greedy and brace-blind: on
+ * `\rput(0,0){\pscircle(0,0){0.8}}` it captures through the inner group and
+ * leaves the tail behind, which is how `0.8}` ended up rendered as a label.
+ *
+ * @param raw - the command's source
+ * @returns the contents of the outermost brace group, empty when unbalanced
+ */
+function braceGroup(raw) {
+ if (typeof raw !== 'string')
+ return '';
+ const start = raw.indexOf('{');
+ if (start === -1)
+ return '';
+ let depth = 0;
+ for (let i = start; i < raw.length; i++) {
+ if (raw[i] === '{')
+ depth++;
+ else if (raw[i] === '}' && --depth === 0)
+ return raw.slice(start + 1, i);
+ }
+ return '';
+}
+/**
+ * Names the numeric fields of a parsed command that came out non-finite.
+ *
+ * `X` and `Y` return NaN for a coordinate they cannot compute rather than
+ * inventing one at the origin, so this is where that shows up: a command whose
+ * geometry is unusable is reported against its own source location instead of
+ * drawing a plausible shape in the wrong place.
+ *
+ * @param value - a parsed command's data
+ * @returns the paths of the offending fields, empty when everything is usable
+ */
+function nonFiniteFields(value, path = '', depth = 0) {
+ if (depth > 4 || value === null || value === undefined)
+ return [];
+ if (typeof value === 'number')
+ return isFinite(value) ? [] : [path || 'value'];
+ if (Array.isArray(value)) {
+ return value.flatMap((v, i) => typeof v === 'number' && !isFinite(v) ? [`${path}[${i}]`] : nonFiniteFields(v, `${path}[${i}]`, depth + 1));
+ }
+ if (typeof value !== 'object')
+ return [];
+ // `global` is the shared environment, not this command's own geometry.
+ return Object.entries(value)
+ .filter(([k]) => k !== 'global' && k !== 'env')
+ .flatMap(([k, v]) => nonFiniteFields(v, path ? `${path}.${k}` : k, depth + 1));
+}
+/**
+ * Parser: turns a LaTeX-ish document into the flat environment objects the
+ * components consume ({type, lines, env, plot}) — but driven by the Peggy
+ * grammar in src/grammar instead of per-line regular expressions.
+ *
+ * The grammar tokenizes structure (balanced environments, commands with args,
+ * comments, verbatim). This class interprets that tree using the registries
+ * (Text / Headers / Ignore / PSTricks / Delimiters), so the runtime extension
+ * API (addEnvironment / addText / addHeaders) keeps working. It also collects
+ * diagnostics (unclosed environments, unknown commands, syntax errors) that
+ * were previously silent.
+ */
class Parser {
constructor(LaTeX2JS) {
this.Ignore = LaTeX2JS.Ignore;
@@ -359,30 +2361,308 @@ class Parser {
'',
'units=1cm,linecolor=black,linestyle=solid,fillstyle=none'
]);
+ this.style = pssetStyle(this.settings);
+ this.diagnostics = [];
+ // The embedding application can declare the dialect once for every document
+ // it renders; a document's own \psset overrides it.
+ this.dialect = (0, settings_1.normalizeDialect)(LaTeX2JS.dialect) ?? 'pstricks';
+ this.counters = new counters_1.Counters();
}
+ /**
+ * The number a sectioning command should carry, or null when it is starred.
+ *
+ * Text transforms call this through their receiver, so the registry stays
+ * free of parser internals and a third-party transform that does not care
+ * about numbering keeps working.
+ *
+ * @param level - which sectioning level is starting
+ * @param raw - the command's source, so a starred form can opt out
+ * @returns the dotted number, or null for an unnumbered heading
+ */
+ sectionNumber(level, raw) {
+ if (/\\[a-z]+\*/.test(raw))
+ return null;
+ return this.counters.section(level);
+ }
+ /**
+ * The number a theorem-like environment should carry, or null when starred.
+ *
+ * @param name - the environment name, such as `theorem`
+ * @param raw - the `\begin` source, so a starred form can opt out
+ * @returns the next number for that kind, or null when unnumbered
+ */
+ environmentNumber(name, raw) {
+ if (/\{[a-z]+\*\}/.test(raw))
+ return null;
+ return this.counters.environment(name);
+ }
+ // -------------------------------------------------------------------------
+ // Public API
+ // -------------------------------------------------------------------------
parse(text) {
+ this.diagnostics = [];
+ // A parser instance is reused across documents; without this the second
+ // would continue the first one's numbering, and inherit any colour the
+ // first defined for itself.
+ this.counters.reset();
+ (0, utils_1.resetDefinedColors)();
if (!text)
return [];
- var lines = text.split('\n');
- this.parseEnvText(lines);
- this.parseEnv(lines);
+ const tree = this.parseTree(text);
+ this.walk(tree);
this.objects.forEach((obj) => {
if (obj.type.match(/pspicture/)) {
- obj.plot = this.parsePSTricks(obj.lines, obj.env);
+ obj.plot = this.parsePSTricks(obj.commands || [], obj.env);
+ delete obj.commands;
}
});
return this.objects;
}
- newEnvironment(type) {
- if (this.environment && this.environment.lines.length) {
- this.environment.settings = { ...this.settings };
- this.objects.push(this.environment);
+ // -------------------------------------------------------------------------
+ // Grammar integration
+ // -------------------------------------------------------------------------
+ parseTree(text) {
+ try {
+ return pegParser.parse(text);
}
- this.environment = {
- type: type,
- lines: []
+ catch (err) {
+ const loc = err.location || { start: { line: 1, column: 1 } };
+ this.diagnostics.push({
+ severity: 'error',
+ message: `parse error: ${err.message || String(err)}`,
+ line: loc.start.line,
+ column: loc.start.column
+ });
+ // Degraded fallback: treat the whole input as a math text block.
+ return [{ kind: 'raw', text: text }];
+ }
+ }
+ // -------------------------------------------------------------------------
+ // Tree walk
+ // -------------------------------------------------------------------------
+ walk(segments) {
+ this.objects = [];
+ this.environment = { type: 'math', lines: [] };
+ segments.forEach((seg) => this.walkSegment(seg));
+ this.newEnvironment('math');
+ }
+ walkSegment(seg) {
+ if (seg.kind === 'raw') {
+ seg.text.split('\n').forEach((line) => this.pushMathLine(line));
+ return;
+ }
+ switch (seg.kind) {
+ case 'line':
+ this.walkContent(seg);
+ break;
+ case 'env':
+ this.walkEnv(seg);
+ break;
+ case 'strayEnd':
+ if (this.isIgnored(seg.raw))
+ return;
+ this.diagnose('warning', `unexpected \\end{${seg.name}}`, seg.loc);
+ break;
+ }
+ }
+ walkEnv(env) {
+ const name = env.name;
+ // Ignored wrapper environments (center, document, interactive…) are
+ // dropped, but their content is still walked in the current context.
+ if (this.isIgnoredEnv(name)) {
+ env.content.forEach((c) => this.walkContent(c));
+ return;
+ }
+ const structural = env.verbatim || !!this.Delimiters[name];
+ if (!structural) {
+ // Non-structural environments (theorem, proof, quotation…) flatten into
+ // the current environment as header text (handled by the Headers pass).
+ const inPspicture = this.inPspicture();
+ if (inPspicture)
+ this.pushLine(env.begin.raw);
+ else
+ this.pushMathLine(env.begin.raw);
+ env.content.forEach((c) => this.walkContent(c));
+ if (env.end) {
+ if (inPspicture)
+ this.pushLine(env.end.raw);
+ else
+ this.pushMathLine(env.end.raw);
+ }
+ else {
+ this.diagnose('warning', `unclosed \\begin{${name}}`, env.begin.loc);
+ }
+ return;
+ }
+ // Structural environment: close the current one and open a new one.
+ this.newEnvironment(name);
+ if (!env.verbatim)
+ this.metaData(name, env);
+ if (env.verbatim) {
+ const v = env.content[0];
+ this.environment.lines = v && v.kind === 'verbatim' ? v.text.split('\n') : [];
+ }
+ else if (name.match(/pspicture/)) {
+ this.environment.commands = [];
+ env.content.forEach((c) => this.walkContent(c));
+ }
+ else {
+ // enumerate / itemize / nicebox: content is text lines (with transforms).
+ this.walkTextContent(env.content);
+ }
+ if (env.end && env.end.name !== name) {
+ this.diagnose('warning', `\\end{${env.end.name}} does not match \\begin{${name}}`, env.end.loc);
+ }
+ else if (!env.end) {
+ this.diagnose('warning', `unclosed environment '${name}'`, env.begin.loc);
+ }
+ this.newEnvironment('math');
+ }
+ /**
+ * Walk a text environment's content, rejoining the nodes that came from one
+ * source line.
+ *
+ * `EnvContent` matches `Command` before `Line`, and a command's tail stops at
+ * the next command, so `\item First with \textbf{bold} text` arrives as two
+ * command nodes. Walking them individually renders each as its own line,
+ * which broke every list item at its first macro. pspicture keeps the
+ * per-node walk, because it depends on receiving commands separately.
+ */
+ walkTextContent(content) {
+ let pending = [];
+ const flush = () => {
+ if (!pending.length)
+ return;
+ const text = pending
+ .map((n) => (n.kind === 'line' ? this.lineToString(n) : n.raw))
+ .join('');
+ pending = [];
+ this.pushMathLine(text);
};
+ content.forEach((node) => {
+ if (node.kind === 'env') {
+ flush();
+ this.walkEnv(node);
+ return;
+ }
+ // An empty Line is the newline itself: it closes the line being built,
+ // or is a genuine paragraph break when there is nothing to close.
+ if (node.kind === 'line' && node.parts.length === 0) {
+ if (pending.length)
+ flush();
+ else
+ this.pushBlankLine(false);
+ return;
+ }
+ const at = node.loc && node.loc.line;
+ const open = pending.length ? pending[0].loc && pending[0].loc.line : at;
+ if (pending.length && at !== open)
+ flush();
+ pending.push(node);
+ // A Line node consumed its own EOL, so nothing more belongs to it.
+ if (node.kind === 'line')
+ flush();
+ });
+ flush();
+ }
+ /**
+ * Walk one node of environment content. Behavior depends on the current
+ * environment: inside pspicture we collect commands (and raw lines) for plot
+ * extraction; elsewhere lines go through the text/header passes.
+ */
+ walkContent(node) {
+ const inPspicture = this.inPspicture();
+ switch (node.kind) {
+ case 'line': {
+ // Comment-only lines are dropped (mirrors the old /^%/ ignore rule).
+ const allComments = node.parts.length > 0 && node.parts.every((p) => p.kind === 'comment');
+ if (allComments)
+ return;
+ if (node.parts.length === 0) {
+ this.pushBlankLine(inPspicture);
+ return;
+ }
+ const text = this.lineToString(node);
+ if (inPspicture)
+ this.pushLine(text);
+ else
+ this.pushMathLine(text);
+ break;
+ }
+ case 'command': {
+ if (node.name === 'psset') {
+ this.parseUnits(node.raw);
+ return;
+ }
+ // The settings in force at THIS point in the source, not at the end of
+ // it. Commands are collected during the walk and parsed afterwards, so
+ // reading `this.settings` when they are parsed would give every shape
+ // in the picture the last \psset rather than the one above it.
+ if (inPspicture) {
+ node.settings = { ...this.style };
+ // Units are snapshotted separately: they are not style defaults to
+ // copy onto a shape, they change how its coordinates are computed.
+ node.units = {
+ xunit: this.settings.xunit,
+ yunit: this.settings.yunit,
+ runit: this.settings.runit
+ };
+ this.environment.commands.push(node);
+ }
+ else
+ this.pushMathLine(node.raw);
+ break;
+ }
+ case 'env':
+ this.walkEnv(node);
+ break;
+ default:
+ break;
+ }
+ }
+ /**
+ * Convert a Line node's parts back to a string, dropping comment fragments.
+ */
+ lineToString(line) {
+ return line.parts
+ .filter((p) => p.kind !== 'comment')
+ .map((p) => (p.kind === 'char' ? p.c : p.raw))
+ .join('');
+ }
+ // -------------------------------------------------------------------------
+ // Line handling
+ // -------------------------------------------------------------------------
+ inPspicture() {
+ return !!(this.environment && this.environment.type.match(/pspicture/));
+ }
+ pushBlankLine(inPspicture) {
+ if (inPspicture)
+ return;
+ if (this.inPspicture())
+ return;
+ this.environment.lines.push('
');
}
+ pushMathLine(text) {
+ if (this.isIgnored(text))
+ return;
+ if (!text.trim().length) {
+ this.environment.lines.push('
');
+ return;
+ }
+ if (this.PSTricks.Expressions.psset.test(text)) {
+ this.parseUnits(text);
+ return;
+ }
+ if (DEFINECOLOR_RE.test(text)) {
+ this.parseDefineColor(text);
+ return;
+ }
+ const processed = this.parseText(text);
+ if (processed.trim().length)
+ this.environment.lines.push(processed);
+ }
+ /** Raw line inside a pspicture: no text/header transforms (they corrupt
+ * PSTricks content). */
pushLine(line) {
var add = true;
this.Ignore.forEach((exp) => {
@@ -390,24 +2670,126 @@ class Parser {
add = false;
}
});
- if (add) {
- if (typeof line === 'string' && line.trim().length) {
- if (this.PSTricks.Expressions.psset.test(line)) {
- this.parseUnits(line);
- }
- else {
- this.environment.lines.push(line);
- }
+ if (add && typeof line === 'string' && line.trim().length) {
+ if (this.PSTricks.Expressions.psset.test(line)) {
+ this.parseUnits(line);
+ }
+ else if (DEFINECOLOR_RE.test(line)) {
+ this.parseDefineColor(line);
+ }
+ else {
+ this.environment.lines.push(line);
+ }
+ }
+ }
+ isIgnored(line) {
+ return this.Ignore.some((exp) => exp.test(line));
+ }
+ isIgnoredEnv(name) {
+ return this.isIgnored('\\begin{' + name + '}');
+ }
+ /**
+ * Groups lines into paragraphs, the way TeX does.
+ *
+ * TeX has no concept of a blank line as vertical space: a run of them, of
+ * any length, is a single `\par`, and the gap between paragraphs comes from
+ * `\parskip` — a style, set once for the document, not something an author
+ * dials in by pressing return more times. Two blank lines and one are the
+ * same input.
+ *
+ * This used to emit one `
` per blank line, so the gap was however many
+ * times the author happened to hit return, and no stylesheet could adjust
+ * it. Paragraphs are real elements now and the spacing is theirs, which is
+ * both what TeX means and the only version a theme can restyle.
+ *
+ * Block elements are passed through untouched: a heading, list or picture is
+ * not part of a paragraph and brings its own margins.
+ *
+ * @param lines - the environment's rendered lines
+ * @returns the lines with runs of text wrapped in paragraphs
+ */
+ paragraphize(lines) {
+ const isBlock = (l) => /^\s*<(h[1-6]|ul|ol|li|p|div|table|blockquote|pre|figure)\b/i.test(l);
+ const out = [];
+ let para = [];
+ const flush = () => {
+ if (!para.length)
+ return;
+ out.push('' + para.join('\n') + '
');
+ para = [];
+ };
+ for (const line of lines) {
+ // Any run of these ends the paragraph, and a run is one break however
+ // long it is — consecutive flushes after the first do nothing.
+ if (line === '
') {
+ flush();
+ continue;
+ }
+ if (isBlock(line)) {
+ flush();
+ out.push(line);
+ continue;
+ }
+ para.push(line);
+ }
+ flush();
+ return out;
+ }
+ newEnvironment(type) {
+ if (this.environment &&
+ (this.environment.lines.length || this.environment.type !== 'math')) {
+ this.environment.settings = { ...this.settings };
+ // Only the plain text environment. A list keeps its \item lines for its
+ // own component to turn into , verbatim is literal, a picture is
+ // commands, and a nicebox is a single inline run — wrapping any of those
+ // in paragraphs breaks the element that consumes them.
+ if (this.environment.type === 'math') {
+ this.environment.lines = this.paragraphize(this.environment.lines);
}
+ this.objects.push(this.environment);
+ }
+ this.environment = {
+ type: type,
+ lines: []
+ };
+ }
+ /**
+ * Records a `\definecolor{name}{model}{spec}`.
+ *
+ * xcolor lets a document define its own colours, and a document that wants a
+ * shade xcolor does not name — a browser colour such as `lightblue`, say —
+ * can define it rather than rely on the renderer guessing. That is what makes
+ * such a page valid LaTeX instead of only valid here.
+ */
+ parseDefineColor(text, loc) {
+ const m = String(text || '').match(DEFINECOLOR_RE);
+ if (!m)
+ return;
+ if (!(0, utils_1.defineColor)(m[1], m[2], m[3])) {
+ this.diagnose('warning', `\\definecolor{${m[1]}}: the ${JSON.stringify(m[2])} model with ` +
+ `${JSON.stringify(m[3])} is not one this understands; the colour is left undefined`, loc);
}
}
parseUnits(line) {
- var m = line.match(this.PSTricks.Expressions.psset);
- Object.assign(this.settings, this.PSTricks.Functions.psset.call(this, m));
+ var m = line.replace(/\n/g, ' ').match(this.PSTricks.Expressions.psset);
+ const declared = this.PSTricks.Functions.psset.call(this, m);
+ if (declared.dialect)
+ this.dialect = declared.dialect;
+ Object.assign(this.settings, declared);
+ Object.assign(this.style, pssetStyle(declared));
}
- metaData(environment, line) {
+ metaData(environment, envNode) {
if (this.PSTricks.Expressions.hasOwnProperty(environment)) {
- this.environment.match = line.match(this.PSTricks.Expressions[environment]);
+ this.environment.match = envNode.begin.raw
+ .replace(/\n/g, ' ')
+ .match(this.PSTricks.Expressions[environment]);
+ if (!this.environment.match) {
+ this.diagnose('error', `could not parse \\begin{${environment}} arguments`, envNode.begin.loc);
+ this.environment.env = {};
+ this.environment.env.xunit = this.settings.xunit;
+ this.environment.env.yunit = this.settings.yunit;
+ return;
+ }
this.environment.env = this.PSTricks.Functions[environment].call(this.settings, this.environment.match);
if (environment.match(/pspicture/)) {
if (typeof this.environment.env.xunit === 'undefined') {
@@ -416,154 +2798,212 @@ class Parser {
if (typeof this.environment.env.yunit === 'undefined') {
this.environment.env.yunit = this.settings.yunit;
}
+ // A radius is scaled by runit, not by xunit, so it has to reach the
+ // env too or every circle falls back to the coordinate unit.
+ if (typeof this.environment.env.runit === 'undefined') {
+ this.environment.env.runit = this.settings.runit;
+ }
+ // Renderers read the dialect for the handful of semantics it changes.
+ this.environment.env.dialect = this.dialect;
}
}
}
- parseEnv(lines) {
- this.objects = [];
- this.environment = {
- type: 'math',
- lines: []
- };
- const Delimiters = this.Delimiters;
- lines.forEach((line) => {
- var isDelim = false;
- Object.entries(Delimiters).forEach(([env, type]) => {
- Object.entries(type).forEach(([k, delim]) => {
- if (line.match(delim)) {
- isDelim = true;
- if (k.match(/begin/)) {
- if (this.environment.type.match(/verbatim/)) {
- isDelim = false;
- }
- else if (this.environment.type.match(/print/)) {
- isDelim = false;
- }
- else {
- this.newEnvironment(env);
- this.metaData(env, line);
- }
- }
- else if (k.match(/end/)) {
- if (this.environment.type.match(/verbatim/)) {
- if (env.match(/verbatim/)) {
- this.newEnvironment('math');
- }
- else {
- isDelim = false;
- }
- }
- else if (this.environment.type.match(/print/)) {
- if (env.match(/print/)) {
- this.newEnvironment('math');
- }
- else {
- isDelim = false;
- }
- }
- else {
- this.newEnvironment('math');
- }
- }
- }
- });
- });
- if (!isDelim)
- this.pushLine(line); // }
+ // -------------------------------------------------------------------------
+ // PSTricks command extraction (ordered)
+ // -------------------------------------------------------------------------
+ /**
+ * Extract plot data from the ordered command nodes of a pspicture.
+ * Returns the grouped `plot` map (keyed by command type, used by the
+ * interactive re-render paths) and records the ordered `elements` list on
+ * the env for source-order initial rendering.
+ */
+ parsePSTricks(commands, env) {
+ var plot = {};
+ const entries = Object.entries(this.PSTricks.Expressions);
+ entries.forEach(([k, _exp]) => {
+ plot[k] = [];
});
- // push last!
- this.newEnvironment('math');
+ const elements = [];
+ this.extractCommands(commands, env, plot, elements);
+ env.elements = elements;
+ return plot;
}
- parseEnvText(lines) {
- var _env = 'math';
- const Delimiters = this.Delimiters;
- lines.forEach((line, i) => {
- var isDelim = false;
- Object.entries(Delimiters).forEach(([env, type]) => {
- Object.entries(type).forEach(([k, delim]) => {
- if (line.match(delim)) {
- isDelim = true;
- if (k.match(/begin/)) {
- if (!_env.match(/verbatim/)) {
- _env = env;
- }
- else {
- isDelim = false;
- }
- }
- else if (k.match(/end/)) {
- if (!_env.match(/verbatim/)) {
- _env = 'math';
- }
- else {
- if (!env.match(/verbatim/)) {
- isDelim = false;
- }
- else {
- _env = 'math';
- }
- }
- }
- }
- });
- });
- if (!isDelim) {
- if (!_env.match(/verbatim/)) {
- lines[i] = this.parseText(line);
+ /**
+ * Extract one command node into `plot` (grouped) and `elements` (ordered).
+ * Recurses into `\multido` bodies (expanded, counter substituted) and
+ * `\pscustom` bodies (the renderer re-parses those itself — the command is
+ * kept as a single element with its raw body).
+ */
+ extractCommands(commands, env, plot, elements) {
+ commands.forEach((node) => {
+ const k = node.name;
+ const exp = this.PSTricks.Expressions[k];
+ if (!exp) {
+ this.diagnose('warning', `unknown command \\${k} in pspicture`, node.loc);
+ return;
+ }
+ // The grammar captures commands across lines; the semantic regexes are
+ // single-line, so collapse internal newlines before matching.
+ const raw = node.raw.replace(/\n/g, ' ');
+ const m = raw.match(exp);
+ if (!m) {
+ this.diagnose('warning', `could not parse \\${k}: ${JSON.stringify(node.raw)}`, node.loc);
+ return;
+ }
+ // Units declared inside the picture rescale what comes after them.
+ const cmdEnv = envForUnits(env, node.units);
+ const data = this.PSTricks.Functions[k].call(cmdEnv, m);
+ applyPsset(data, node.settings, node.raw);
+ // An `arrows` declared by \psset arrives as a string after the parse
+ // function has already turned its own into flags, so it is normalized
+ // once more here rather than in each of them.
+ (0, utils_1.normalizeArrows)(data);
+ // \multido{var=start+step}{count}{body} — expand and recurse.
+ if (k === 'multido') {
+ this.expandMultido(data, env, plot, elements, node);
+ return;
+ }
+ // \pscustom{...} — pre-extract the inner commands into pixel data so
+ // the renderer can build a single filled/stroked path.
+ if (k === 'pscustom' && data.body) {
+ data.commands = this.extractCustomBody(data.body, cmdEnv);
+ }
+ // \rput(x,y){...} places its contents at (x,y). The contents are usually
+ // a label, and were assumed to be one — so a graphics command inside an
+ // rput drew nothing at all, and the tail the greedy regex left over was
+ // set as text. Graphics are placed by translating a group, which keeps
+ // them in document order among the other shapes rather than in the
+ // separate DOM pass the labels go through.
+ if (k === 'rput') {
+ const children = this.extractCustomBody(braceGroup(node.raw), cmdEnv);
+ if (children.length) {
+ // The contents' own origin lands on (x,y), so the offset is the
+ // command's position measured from where (0,0) falls.
+ const originX = (cmdEnv.w - cmdEnv.x1) * cmdEnv.xunit;
+ const originY = cmdEnv.y1 * cmdEnv.yunit;
+ elements.push({
+ name: 'rputgroup',
+ data: { dx: data.x - originX, dy: data.y - originY, children },
+ match: m,
+ fn: this.PSTricks.Functions[k],
+ loc: node.loc
+ });
+ return;
+ }
+ }
+ plot[k].push({ data: data, env: env, match: m, fn: this.PSTricks.Functions[k] });
+ // Under the PSTricks reading, anything this project added is worth
+ // naming. A document that declares the LaTeX2JS dialect has said it means
+ // to use them, so it is not told again.
+ if (this.dialect === 'pstricks') {
+ // `data` carries the command's parsed options, which is what the
+ // detector inspects alongside the raw source.
+ for (const use of (0, dialect_1.dialectUses)(k, node.raw ?? '', data)) {
+ this.diagnose('warning', `${use.construct} is a LaTeX2JS extension: ${use.detail}. ` +
+ 'Declare \\psset{dialect=latex2js} if that is intended.', node.loc);
}
- if (!line.trim().length) {
- lines[i] = '
';
+ }
+ if (data && data.plotpointsIgnored !== undefined) {
+ this.diagnose('warning', `plotpoints=${data.plotpointsIgnored} needs at least 2 samples to mean anything; ` +
+ 'the default sampling was used instead', node.loc);
+ }
+ const bad = nonFiniteFields(data);
+ if (bad.length) {
+ this.diagnose('warning', `\\${k} produced no usable value for ${bad.join(', ')}; it will not be drawn`, node.loc);
+ }
+ elements.push({ name: k, data: data, match: m, fn: this.PSTricks.Functions[k], loc: node.loc });
+ // side effects preserved from the old parser:
+ if (k === 'psaxes' && plot[k].length > 0) {
+ const axesData = plot[k][plot[k].length - 1].data;
+ if (axesData && axesData.dx !== undefined) {
+ env.dx = axesData.dx;
+ env.dy = axesData.dy;
+ env.origin = axesData.origin;
}
}
+ if (k === 'uservariable') {
+ env.variables = env.variables || {};
+ env.variables[data.name] = data.value;
+ }
});
}
- parsePSExpression(line, exp, plot, k, env) {
- var match = line.match(exp);
- if (match) {
- plot[k].push({
- data: this.PSTricks.Functions[k].call(env, match),
- env: env,
- match: match,
- fn: this.PSTricks.Functions[k]
+ /** Expand a \multido loop into its constituent commands. */
+ expandMultido(data, env, plot, elements, node) {
+ if (!data.variable || !(data.count > 0) || !data.body)
+ return;
+ const re = new RegExp('\\\\' + data.variable + '\\b', 'g');
+ for (let i = 0; i < data.count; i++) {
+ const value = data.start + i * data.step;
+ const body = data.body.replace(re, String(value));
+ this.commandNodesFrom(this.parseTree(body)).forEach((cmd) => {
+ this.extractCommands([cmd], env, plot, elements);
});
- return true;
}
- return false;
}
- parsePSVariables(line, exp, _plot, k, env) {
- var match = line.match(exp);
- if (match) {
- if (k.match(/uservariable/)) {
- var dd = this.PSTricks.Functions[k].call(env, match);
- env.variables = env.variables || {};
- env.variables[dd.name] = dd.value;
+ /**
+ * Extract the inner commands of a \pscustom body into pixel data for the
+ * renderer. Commands that need DOM/runtime handling (rput, slider, psset,
+ * nested pscustom, multido) are skipped.
+ */
+ extractCustomBody(body, env) {
+ const out = [];
+ const skip = ['rput', 'slider', 'psset', 'pspicture', 'pscustom', 'multido', 'uservariable'];
+ this.commandNodesFrom(this.parseTree(body)).forEach((node) => {
+ const k = node.name;
+ if (skip.indexOf(k) !== -1)
+ return;
+ const exp = this.PSTricks.Expressions[k];
+ if (!exp)
+ return;
+ const m = node.raw.replace(/\n/g, ' ').match(exp);
+ if (!m)
+ return;
+ try {
+ const data = this.PSTricks.Functions[k].call(env, m);
+ if (data)
+ out.push({ key: k, data: data });
+ }
+ catch (err) {
+ /* ignore malformed inner commands */
}
- }
- }
- parsePSTricks(lines, env) {
- var plot = {};
- const entries = Object.entries(this.PSTricks.Expressions);
- entries.forEach(([k, _exp]) => {
- plot[k] = [];
- });
- lines.forEach((line) => {
- entries.forEach(([k, exp]) => {
- this.parsePSVariables(line, exp, plot, k, env);
- const result = this.parsePSExpression(line, exp, plot, k, env);
- if (result && k === 'psaxes' && plot[k].length > 0) {
- const axesData = plot[k][plot[k].length - 1].data;
- if (axesData && axesData.dx !== undefined) {
- env.dx = axesData.dx;
- env.dy = axesData.dy;
- env.origin = axesData.origin;
- }
- }
- });
});
- return plot;
+ return out;
}
- parseTextExpression(line, exp, k, contents) {
- var match = line.match(exp);
+ /**
+ * Flatten parsed segments into an ordered list of command nodes, walking
+ * into line parts and nested environments.
+ */
+ commandNodesFrom(segs) {
+ const out = [];
+ const walk = (seg) => {
+ if (seg.kind === 'command')
+ out.push(seg);
+ else if (seg.kind === 'line') {
+ (seg.parts || []).forEach((p) => {
+ if (p.kind === 'command')
+ out.push(p);
+ });
+ }
+ else if (seg.kind === 'env') {
+ (seg.content || []).forEach(walk);
+ }
+ };
+ segs.forEach(walk);
+ return out;
+ }
+ // -------------------------------------------------------------------------
+ // Text / header transforms (reused from the old parser, string-based)
+ // -------------------------------------------------------------------------
+ /**
+ * Text transforms run in sequence over one line, so each must match the
+ * value the previous ones produced. Matching the pristine line instead makes
+ * `matchrepl` search `contents` for a literal that an earlier transform has
+ * already rewritten, and the replacement silently does nothing — which is
+ * why `\section{Cauchy--Schwarz}` survived as source text once `--` had
+ * become an en dash.
+ */
+ parseTextExpression(_line, exp, k, contents) {
+ var match = contents.match(exp);
if (match) {
return this.Text.Functions[k].call(this, match, contents);
}
@@ -572,7 +3012,9 @@ class Parser {
parseHeadersExpression(line, exp, k, contents) {
var match = line.match(exp);
if (match) {
- return this.Headers.Functions[k].call(this);
+ // The match is passed so a numbered environment can see whether its
+ // \begin was starred, which is how LaTeX spells "do not number this one".
+ return this.Headers.Functions[k].call(this, match);
}
return contents;
}
@@ -588,10 +3030,21 @@ class Parser {
});
return contents;
}
+ // -------------------------------------------------------------------------
+ // Diagnostics
+ // -------------------------------------------------------------------------
+ diagnose(severity, message, loc) {
+ this.diagnostics.push({
+ severity: severity,
+ message: message,
+ line: loc ? loc.line : undefined,
+ column: loc ? loc.column : undefined
+ });
+ }
}
exports.default = Parser;
-},{}],13:[function(require,module,exports){
+},{"../grammar/parser.js":9,"./counters":11,"./dialect":12,"@latex2js/settings":23,"@latex2js/utils":25}],17:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Functions = exports.Expressions = void 0;
@@ -616,7 +3069,49 @@ exports.Expressions = {
set: /\\set\{[^}]*\}/g,
youtube: /\\youtube\{[^}]*\}/g,
euler: /Euler\^/g,
+ textbf: /\\textbf\{[^}]*\}/g,
+ textit: /\\textit\{[^}]*\}/g,
+ texttt: /\\texttt\{[^}]*\}/g,
+ textrm: /\\textrm\{[^}]*\}/g,
+ textsc: /\\textsc\{[^}]*\}/g,
+ underline: /\\underline\{[^}]*\}/g,
+ overline: /\\overline\{[^}]*\}/g,
+ section: /\\section\*?\{[^}]*\}/,
+ subsection: /\\subsection\*?\{[^}]*\}/,
+ subsubsection: /\\subsubsection\*?\{[^}]*\}/,
+ paragraph: /\\paragraph\{[^}]*\}/,
+ hspace: /\\hspace\{[^}]*\}/,
+ noindent: /\\noindent/g,
+ newpage: /\\newpage/g,
+ hrule: /\\hrule/g,
+ rule: /\\rule\{[^}]*\}\{[^}]*\}/g,
+ textcolor: /\\textcolor\{[^}]*\}\{[^}]*\}/g,
+ footnote: /\\footnote\{[^}]*\}/g,
};
+/**
+ * Renders one sectioning command, numbered unless it is starred.
+ *
+ * The number comes from the parser through the receiver, so this registry
+ * carries no parser internals and a host that supplies neither still renders
+ * the heading — just without a number.
+ *
+ * @param tag - the heading element for this level
+ * @param level - the sectioning level, for the counter
+ * @param m - the match: [full, star, title]
+ * @param parser - the receiver, when the caller supplied one
+ * @returns the heading markup
+ */
+function heading(tag, level, m, parser) {
+ const starred = m[1] === '*';
+ const title = m[2];
+ const number = !starred && parser && typeof parser.sectionNumber === 'function'
+ ? parser.sectionNumber(level, m[0])
+ : null;
+ const label = number === null || number === undefined
+ ? ''
+ : '' + number + ' ';
+ return '<' + tag + '>' + label + title + '' + tag + '>';
+}
exports.Functions = {
cite: function (m, contents) {
m.forEach((match) => {
@@ -678,13 +3173,65 @@ exports.Functions = {
vspace: (0, utils_1.simplerepl)(/\\vspace/g, '
'),
TeX: (0, utils_1.simplerepl)(/\\TeX\\|\\TeX/g, '$\\TeX$'),
LaTeX: (0, utils_1.simplerepl)(/\\LaTeX\\|\\LaTeX/g, '$\\LaTeX$'),
+ textbf: (0, utils_1.matchrepl)(/\\textbf\{([^}]*)\}/, function (m) {
+ return '' + m[1] + '';
+ }),
+ textit: (0, utils_1.matchrepl)(/\\textit\{([^}]*)\}/, function (m) {
+ return '' + m[1] + '';
+ }),
+ texttt: (0, utils_1.matchrepl)(/\\texttt\{([^}]*)\}/, function (m) {
+ return '' + m[1] + '';
+ }),
+ textrm: (0, utils_1.matchrepl)(/\\textrm\{([^}]*)\}/, function (m) {
+ return '' + m[1] + '';
+ }),
+ textsc: (0, utils_1.matchrepl)(/\\textsc\{([^}]*)\}/, function (m) {
+ return '' + m[1] + '';
+ }),
+ underline: (0, utils_1.matchrepl)(/\\underline\{([^}]*)\}/, function (m) {
+ return '' + m[1] + '';
+ }),
+ overline: (0, utils_1.matchrepl)(/\\overline\{([^}]*)\}/, function (m) {
+ return '' + m[1] + '';
+ }),
+ section: (0, utils_1.matchrepl)(/\\section(\*?)\{([^}]*)\}/, function (m) {
+ return heading('h2', 'section', m, this);
+ }),
+ subsection: (0, utils_1.matchrepl)(/\\subsection(\*?)\{([^}]*)\}/, function (m) {
+ return heading('h3', 'subsection', m, this);
+ }),
+ subsubsection: (0, utils_1.matchrepl)(/\\subsubsection(\*?)\{([^}]*)\}/, function (m) {
+ return heading('h4', 'subsubsection', m, this);
+ }),
+ paragraph: (0, utils_1.matchrepl)(/\\paragraph\{([^}]*)\}/, function (m) {
+ return '' + m[1] + '
';
+ }),
+ hspace: (0, utils_1.matchrepl)(/\\hspace\{([^}]*)\}/, function (_m) {
+ return ' ';
+ }),
+ noindent: (0, utils_1.simplerepl)(/\\noindent/g, ''),
+ newpage: (0, utils_1.simplerepl)(/\\newpage/g, '
'),
+ hrule: (0, utils_1.simplerepl)(/\\hrule/g, '
'),
+ rule: (0, utils_1.matchrepl)(/\\rule\{([^}]*)\}\{([^}]*)\}/, function (m) {
+ return ('');
+ }),
+ textcolor: (0, utils_1.matchrepl)(/\\textcolor\{([^}]*)\}\{([^}]*)\}/, function (m) {
+ return '' + m[2] + '';
+ }),
+ footnote: (0, utils_1.matchrepl)(/\\footnote\{([^}]*)\}/, function (m) {
+ return '';
+ }),
};
exports.default = {
Expressions: exports.Expressions,
Functions: exports.Functions,
};
-},{"@latex2js/utils":20}],14:[function(require,module,exports){
+},{"@latex2js/utils":25}],18:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = String.raw `
@@ -827,16 +3374,31 @@ exports.default = String.raw `
$$
`;
-},{}],15:[function(require,module,exports){
+},{}],19:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
-exports.loadMathJax = exports.getMathJax = exports.DEFAULT_CONFIG = void 0;
+exports.loadMathJax = exports.getMathJax = exports.DEFAULT_CONFIG = exports.DEFAULT_SCRIPT_URL = void 0;
+/**
+ * Where MathJax is loaded from unless a caller overrides it with
+ * `config.scriptURL`.
+ *
+ * Pinned rather than floating on a major tag, so a build is reproducible and
+ * the URL can carry an integrity hash. Being a single constant is what made
+ * the move to MathJax 4 a one-line change: v4 dropped the `es5/` directory, so
+ * the path shape moved as well as the version.
+ */
+exports.DEFAULT_SCRIPT_URL = 'https://cdn.jsdelivr.net/npm/mathjax@4.1.3/tex-chtml.js';
exports.DEFAULT_CONFIG = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
processEscapes: true,
processEnvironments: true,
+ // Number AMS environments — equation, align, gather — and resolve \label
+ // and \ref against those numbers. Equation numbering stays with MathJax
+ // rather than the parser: it already owns the math, and two systems
+ // numbering the same document would disagree.
+ tags: 'ams',
packages: ['base', 'ams', 'newcommand', 'configmacros']
},
chtml: {
@@ -848,6 +3410,34 @@ exports.DEFAULT_CONFIG = {
}
}
};
+/**
+ * Merges an override into a base, recursively, without mutating either.
+ *
+ * The config is nested more than one level — `chtml.linebreaks` holds both
+ * `automatic` and `width` — so merging only the top level silently drops the
+ * siblings of whatever a caller overrides: passing
+ * `{ chtml: { linebreaks: { width: "80%" } } }` lost `automatic: true`. The
+ * `MathJaxConfig` type says any subset may be overridden, and this is what
+ * makes that true.
+ *
+ * Arrays replace rather than merge: `tex.packages` and `tex.inlineMath` are
+ * whole values, and concatenating them would silently keep a default a caller
+ * meant to remove.
+ */
+function deepMerge(base, override) {
+ if (override === undefined)
+ return base;
+ const mergeable = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && typeof v !== 'function';
+ if (!mergeable(base) || !mergeable(override))
+ return override;
+ const out = { ...base };
+ for (const key of Object.keys(override)) {
+ out[key] = mergeable(base[key]) && mergeable(override[key])
+ ? deepMerge(base[key], override[key])
+ : override[key];
+ }
+ return out;
+}
let mathJaxInstance = null;
const getMathJax = () => mathJaxInstance || globalThis.MathJax;
exports.getMathJax = getMathJax;
@@ -856,28 +3446,51 @@ const loadMathJax = async (callback = () => { }, config = exports.DEFAULT_CONFIG
callback();
return;
}
- if (globalThis.MathJax) {
- mathJaxInstance = globalThis.MathJax;
+ // Presence is not readiness. `window.MathJax` holds the configuration object
+ // long before the library that reads it has loaded, and pre-configuring the
+ // global is the documented way to set MathJax up — so treating any value
+ // here as a loaded library meant a page that configured MathJax itself never
+ // got the script injected at all, and MathJax never loaded.
+ const existing = globalThis.MathJax;
+ if (existing && typeof existing.typesetPromise === 'function') {
+ mathJaxInstance = existing;
+ callback();
+ return;
+ }
+ // Someone has already asked for the script; wait for that one rather than
+ // adding a second copy.
+ if (typeof document !== 'undefined' && document.getElementById('MathJax-script')) {
callback();
return;
}
+ // scriptURL is a loader concern, not a MathJax one: keep it out of the
+ // config object that is handed to MathJax itself.
+ const { scriptURL = exports.DEFAULT_SCRIPT_URL, ...mathjaxConfig } = config;
+ // Three sources, weakest first: our defaults, then any configuration the page
+ // had already put on the global, then what this caller passed. Without the
+ // merge a caller passing only { scriptURL } would drop the tex setup — ams,
+ // tags, equation numbering — entirely; without folding in `existing`, a page
+ // that pre-configured MathJax would have its settings thrown away by the
+ // very call that finally loads the library for it.
+ const preconfigured = existing && typeof existing === 'object' ? existing : {};
+ const merged = deepMerge(deepMerge(exports.DEFAULT_CONFIG, preconfigured), mathjaxConfig);
try {
globalThis.MathJax = {
- ...config,
+ ...merged,
startup: {
- ...config.startup,
+ ...merged.startup,
ready: () => {
globalThis.MathJax.startup.defaultReady();
mathJaxInstance = globalThis.MathJax;
- if (config.startup?.ready) {
- config.startup.ready();
+ if (merged.startup.ready) {
+ merged.startup.ready();
}
callback();
}
}
};
const script = document.createElement('script');
- script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js';
+ script.src = scriptURL;
script.async = true;
script.id = 'MathJax-script';
script.onload = () => {
@@ -896,7 +3509,7 @@ const loadMathJax = async (callback = () => { }, config = exports.DEFAULT_CONFIG
};
exports.loadMathJax = loadMathJax;
-},{}],16:[function(require,module,exports){
+},{}],20:[function(require,module,exports){
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
@@ -947,14 +3560,61 @@ exports.default = {
arrow: psgraph_1.arrow,
};
-},{"./lib/psgraph":17,"./lib/pstricks":18}],17:[function(require,module,exports){
+},{"./lib/psgraph":21,"./lib/pstricks":22}],21:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.arrow = arrow;
const utils_1 = require("@latex2js/utils");
-function arrow(x1, y1, x2, y2) {
+/**
+ * How long to keep waiting for a MathJax that is present but not yet usable.
+ * Bounded, so a page that configures MathJax and never loads it still shows
+ * its labels rather than hiding them forever.
+ */
+const MATHJAX_READY_TIMEOUT_MS = 10000;
+/**
+ * Resolves to a MathJax that can actually typeset, or null if none will be.
+ *
+ * `window.MathJax` is present long before it can typeset anything: the loader
+ * assigns the configuration object to the global and only then injects the CDN
+ * script, so between those two moments the global exists and `typesetPromise`
+ * does not. Reading that capability once, synchronously, therefore fails on a
+ * cold load and succeeds on a warm one — which is why an rput label sat
+ * off-centre on first paint and corrected itself on reload. It was centred on
+ * the width of the raw LaTeX, and never measured again once MathJax arrived
+ * and replaced it with the formula.
+ *
+ * The absence of the global is a different case from a global that is not
+ * ready: a page with no MathJax at all must show its labels immediately, so
+ * only the second waits.
+ *
+ * @returns the usable MathJax, or null when there is none to wait for
+ */
+function mathJaxWhenReady() {
+ const usable = (mj) => (mj && typeof mj.typesetPromise === 'function' ? mj : null);
+ const now = globalThis.MathJax;
+ if (!now)
+ return Promise.resolve(null);
+ if (usable(now))
+ return Promise.resolve(now);
+ return new Promise((resolve) => {
+ const started = Date.now();
+ const poll = () => {
+ const ready = usable(globalThis.MathJax);
+ if (ready)
+ return resolve(ready);
+ if (Date.now() - started >= MATHJAX_READY_TIMEOUT_MS)
+ return resolve(null);
+ setTimeout(poll, 50);
+ };
+ poll();
+ });
+}
+function arrow(x1, y1, x2, y2, arrowscale) {
var t = Math.PI / 6;
- var d = 8;
+ // arrowscale is a multiplier on the 8px default head size; anything that
+ // is not a positive number falls back to 1 (the PSTricks default).
+ var scale = Number(arrowscale);
+ var d = 8 * (scale > 0 ? scale : 1);
var dx = x2 - x1, dy = y2 - y1;
var l = Math.sqrt(dx * dx + dy * dy);
var cost = Math.cos(t);
@@ -978,6 +3638,462 @@ function arrow(x1, y1, x2, y2) {
context.push('Z');
return context.join(' ');
}
+/** PSTricks' `curvature=a b c` default. */
+const CURVATURE_DEFAULT = { a: 1, b: 0.1, c: 0 };
+/**
+ * The control-point scaling PSTricks itself uses, transcribed from the `CC`
+ * and `IC` procedures in its PostScript prologue (pstricks.pro).
+ *
+ * This is not a Catmull-Rom spline, which is what stood here. The difference
+ * is where the control offset gets its length: Catmull-Rom takes it from the
+ * chord between a point's two neighbours, so at a sharp turn — where that
+ * chord collapses — the curve pinches into a cusp. PSTricks takes it from the
+ * length of the segment being drawn, which stays large through the turn, so
+ * the curve rounds outward instead. Every psccurve and psecurve in the corpus
+ * drew with corners the reference does not have.
+ *
+ * @param prev - the point before this one
+ * @param cur - the point the tangent is taken at
+ * @param next - the point after this one
+ * @param p - effective curvature parameters, already through `IC`
+ * @returns the control points just before and just after `cur`
+ */
+function curveControls(prev, cur, next, p) {
+ const d0x = cur[0] - prev[0];
+ const d0y = cur[1] - prev[1];
+ const d1x = next[0] - cur[0];
+ const d1y = next[1] - cur[1];
+ const l0 = Math.hypot(d0x, d0y);
+ const l1 = Math.hypot(d1x, d1y);
+ // The tangent leans toward whichever neighbouring segment is longer, by
+ // `c` — which IC has already shifted by one, so the default 0 means 1.
+ const w0 = Math.pow(l1, p.c);
+ const w1 = Math.pow(l0, p.c);
+ const tx = d0x * w0 + d1x * w1;
+ const ty = d0y * w0 + d1y * w1;
+ const tlen = Math.hypot(tx, ty);
+ if (!tlen || !isFinite(tlen))
+ return { before: [...cur], after: [...cur] };
+ // Sharper turns pull the control points in. With the default b of 0.1 this
+ // is a very weak effect until the path nearly doubles back on itself.
+ const turn = Math.atan2(d0y, d0x) - Math.atan2(d1y, d1x);
+ const m = (p.a * Math.pow(Math.abs(Math.cos(turn / 2)), p.b)) / tlen / 2;
+ return {
+ before: [cur[0] - l0 * tx * m, cur[1] - l0 * ty * m],
+ after: [cur[0] + l1 * tx * m, cur[1] + l1 * ty * m],
+ };
+}
+/**
+ * Reads `curvature` and applies the rescaling PSTricks' `IC` does once before
+ * a curve is drawn: c is shifted up by one and clamped, and a is folded
+ * together with the b exponent.
+ */
+function curvatureParams(ctx) {
+ const raw = String((ctx && ctx.curvature) ?? '').trim();
+ const parts = raw ? raw.split(/[\s,]+/).map(Number) : [];
+ const a = isFinite(parts[0]) ? parts[0] : CURVATURE_DEFAULT.a;
+ const b = isFinite(parts[1]) ? parts[1] : CURVATURE_DEFAULT.b;
+ const c = isFinite(parts[2]) ? parts[2] : CURVATURE_DEFAULT.c;
+ return {
+ a: ((a * 2) / 3) / Math.pow(Math.cos(Math.PI / 4), b),
+ b,
+ c: Math.min(3, Math.max(0, c + 1)),
+ };
+}
+/**
+ * Cubic Bézier path through a flat [x0,y0,x1,y1,...] point list.
+ *
+ * The three PSTricks curve commands are three different shapes, not one:
+ * `\pscurve` runs through every point, `\psccurve` wraps back to the start, and
+ * `\psecurve` uses the first and last points **only** to set the tangents and
+ * draws just the span between the interior ones. Treating `psecurve` as closed
+ * — as this did — produced a loop where the reference draws a short open arc.
+ *
+ * @param data - flat coordinate pairs
+ * @param mode - which of the three curves to build
+ * @param ctx - the shape's parsed data, read for `curvature`
+ * @returns an SVG path, empty when there are too few points for the mode
+ */
+function buildCurvePath(data, mode, ctx) {
+ const pts = [];
+ for (let i = 0; i < data.length; i += 2)
+ pts.push([data[i], data[i + 1]]);
+ const n = pts.length;
+ const p = curvatureParams(ctx);
+ const seg = (from, c1, c2, to) => ' C ' + c1[0] + ' ' + c1[1] + ', ' + c2[0] + ' ' + c2[1] + ', ' + to[0] + ' ' + to[1];
+ if (mode === 'closed') {
+ // ClosedCurve wraps the neighbour lookup; PSTricks copies the first three
+ // points onto the end of the stack to the same effect.
+ if (n < 3)
+ return '';
+ const at = (i) => pts[((i % n) + n) % n];
+ const ctrl = pts.map((_, i) => curveControls(at(i - 1), at(i), at(i + 1), p));
+ let d = 'M ' + pts[0][0] + ' ' + pts[0][1];
+ for (let i = 0; i < n; i++) {
+ d += seg(at(i), ctrl[i].after, ctrl[(i + 1) % n].before, at(i + 1));
+ }
+ return d + ' Z';
+ }
+ if (mode === 'endpoints') {
+ // AltCurve draws P1..Pn-2; the outer points only feed the tangents.
+ if (n < 4)
+ return '';
+ let d = 'M ' + pts[1][0] + ' ' + pts[1][1];
+ for (let i = 1; i < n - 2; i++) {
+ const after = curveControls(pts[i - 1], pts[i], pts[i + 1], p).after;
+ const before = curveControls(pts[i], pts[i + 1], pts[i + 2], p).before;
+ d += seg(pts[i], after, before, pts[i + 1]);
+ }
+ return d;
+ }
+ // OpenCurve. IC starts with a zero tangent and EOC ends with one, so the
+ // outermost control point of each end sits on the endpoint itself.
+ if (n < 3)
+ return '';
+ let d = 'M ' + pts[0][0] + ' ' + pts[0][1];
+ for (let i = 0; i < n - 1; i++) {
+ const after = i === 0 ? pts[0] : curveControls(pts[i - 1], pts[i], pts[i + 1], p).after;
+ const before = i + 1 === n - 1 ? pts[n - 1] : curveControls(pts[i], pts[i + 1], pts[i + 2], p).before;
+ d += seg(pts[i], after, before, pts[i + 1]);
+ }
+ return d;
+}
+const TAU = Math.PI * 2;
+/** Points to device units, matching the linewidth conversion in pstricks.ts. */
+const PT_TO_PX = 1.333;
+/**
+ * Line directions each hatched fill style draws, as offsets from `hatchangle`.
+ * PSTricks hatches at `hatchangle` for hlines, ninety degrees off for vlines,
+ * and both for crosshatch — so the default 45 degrees makes hlines diagonal,
+ * not horizontal.
+ */
+const HATCH_DIRECTIONS = {
+ hlines: [0],
+ vlines: [90],
+ crosshatch: [0, 90],
+};
+/** PSTricks hatch parameter defaults, in points except the angle and colour. */
+const HATCH_DEFAULTS = { hatchwidth: 0.8, hatchsep: 4, hatchangle: 45, hatchcolor: 'black' };
+let patternSeq = 0;
+/** Reads a dimension that may carry a `pt` suffix, in device units. */
+function dimension(value, fallbackPt) {
+ if (typeof value === 'number' && isFinite(value))
+ return value * PT_TO_PX;
+ const m = typeof value === 'string' ? value.trim().match(/^([\d.]+)\s*(pt)?$/) : null;
+ return (m ? Number(m[1]) : fallbackPt) * PT_TO_PX;
+}
+/**
+ * Whether a shape has any fill at all. Renderers that must close a path before
+ * it can be filled ask this; the paint itself comes from {@link resolveFill}.
+ *
+ * @param ctx - the shape's parsed data
+ * @returns true when the shape should be built as a closed, fillable region
+ */
+function hasFill(ctx) {
+ return !!ctx.filled || (!!ctx.fillstyle && ctx.fillstyle !== 'none');
+}
+/**
+ * Resolves a shape's SVG fill value, defining a hatch pattern when the style
+ * calls for one.
+ *
+ * Every renderer previously spelled this decision itself, in three mutually
+ * inconsistent ways: `fillstyle=hlines` became a solid fill on pspolygon and
+ * psarc, and no fill at all on psellipse, pswedge and pscurve. Routing all of
+ * them through one resolver makes an unimplemented style behave the same
+ * everywhere, and gives the hatched styles a real rendering.
+ *
+ * @param ctx - the shape's parsed data, carrying fillstyle and hatch options
+ * @param svg - the container the pattern definition is attached to
+ * @returns an SVG paint value: a colour, a `url(#…)` pattern, or `none`
+ */
+function resolveFill(ctx, svg) {
+ const style = ctx.fillstyle ?? 'none';
+ // The starred forms set `filled`; they fill flat regardless of style, in the
+ // fillcolor the author wrote. PSTricks fills them with linecolor instead —
+ // a difference the dialect reports rather than switches, because rendering
+ // must not depend on a flag that cannot deliver PSTricks output anyway.
+ if (ctx.filled || style === 'solid')
+ return ctx.fillcolor;
+ if (style === 'none' || !style)
+ return 'none';
+ const starred = style.endsWith('*');
+ const directions = HATCH_DIRECTIONS[starred ? style.slice(0, -1) : style];
+ // An unrecognised style is not a fill; guessing solid is what made the same
+ // input render differently depending on the shape.
+ if (!directions)
+ return 'none';
+ const sep = Math.max(1, dimension(ctx.hatchsep, HATCH_DEFAULTS.hatchsep));
+ const width = Math.max(0.2, dimension(ctx.hatchwidth, HATCH_DEFAULTS.hatchwidth));
+ const angle = Number(ctx.hatchangle ?? HATCH_DEFAULTS.hatchangle) || 0;
+ const color = ctx.hatchcolor ?? HATCH_DEFAULTS.hatchcolor;
+ const id = 'l2j-hatch-' + ++patternSeq;
+ const pattern = svg
+ .append('svg:defs')
+ .append('svg:pattern')
+ .attr('id', id)
+ .attr('patternUnits', 'userSpaceOnUse')
+ .attr('width', sep)
+ .attr('height', sep)
+ // SVG's y axis runs opposite to the PSTricks angle convention.
+ .attr('patternTransform', 'rotate(' + -angle + ')');
+ // A starred hatch lays its lines over the fill colour instead of nothing.
+ if (starred) {
+ pattern.append('svg:rect').attr('width', sep).attr('height', sep).style('fill', ctx.fillcolor);
+ }
+ for (const d of directions) {
+ const line = pattern.append('svg:line').style('stroke', color).style('stroke-width', width);
+ if (d === 0)
+ line.attr('x1', 0).attr('y1', sep / 2).attr('x2', sep).attr('y2', sep / 2);
+ else
+ line.attr('x1', sep / 2).attr('y1', 0).attr('x2', sep / 2).attr('y2', sep);
+ }
+ return 'url(#' + id + ')';
+}
+/**
+ * Resolves a shape's SVG stroke.
+ *
+ * `linestyle=none` means the outline is not drawn at all. Exactly one renderer
+ * honoured that and the rest painted the outline anyway, so a shape asked to
+ * show only its fill still came out with a border — the same one-place-right
+ * pattern that fillstyle had.
+ *
+ * @param ctx - the shape's parsed data
+ * @param fallback - the colour to use when the shape has no linecolor of its own
+ * @returns an SVG paint value, or `none` when the outline is suppressed
+ */
+function resolveStroke(ctx, fallback) {
+ if (ctx && ctx.linestyle === 'none')
+ return 'none';
+ return (ctx && ctx.linecolor) || fallback || 'black';
+}
+/** PSTricks' own defaults for the two broken-line styles, in points. */
+const DASH_DEFAULT = '5pt 3pt';
+const DOTSEP_DEFAULT = 3;
+/** PSTricks' `dotsize=2pt 2` and `linewidth=0.8pt` defaults. */
+const DOTSIZE_DEFAULT = '2pt 2';
+const DEFAULT_LINEWIDTH_PX = 0.8 * PT_TO_PX;
+/**
+ * The SVG dash pattern for a shape's linestyle, or `none` when it draws solid.
+ *
+ * `linestyle=dashed` and `linestyle=dotted` were honoured by psline and
+ * pspolygon and ignored by every other shape, and where they were honoured
+ * both emitted the same `9,5` — so a dotted line rendered as a dashed one and
+ * neither followed the `dash` or `dotsep` the author set.
+ *
+ * @param ctx - the shape's parsed data, carrying linestyle, dash and dotsep
+ * @returns an SVG stroke-dasharray value
+ */
+function dashArray(ctx) {
+ const style = (ctx && ctx.linestyle) || 'solid';
+ const round = (n) => Math.round(n * 1000) / 1000;
+ if (style === 'dotted') {
+ // A zero-length dash under a round cap is how SVG draws a round dot; the
+ // gap is dotsep plus the width the cap itself occupies.
+ const sep = dimension(ctx.dotsep, DOTSEP_DEFAULT);
+ return '0,' + round(sep + (Number(ctx.linewidth) || 0));
+ }
+ if (style !== 'dashed')
+ return 'none';
+ // `dash=5pt 3pt` names the black length then the white one.
+ const parts = String(ctx.dash ?? DASH_DEFAULT).trim().split(/\s+/);
+ const on = dimension(parts[0], 5);
+ const off = dimension(parts[1] ?? parts[0], 3);
+ return round(on) + ',' + round(off);
+}
+/** Round caps are what turn a zero-length dash into a dot. */
+function dashCap(ctx) {
+ return ctx && ctx.linestyle === 'dotted' ? 'round' : 'butt';
+}
+/**
+ * The radius of a plotted dot.
+ *
+ * PSTricks reads `dotsize= ` and sets the dot's *diameter* to
+ * `dim + factor × linewidth`, so a thicker pen draws a proportionally bigger
+ * dot — its `dotsize=2pt 2` default and the halving are both from
+ * pstricks-dots.tex. This was a fixed radius that read neither part, so
+ * `\psdots[linewidth=4pt]` drew the same specks as a hairline where the
+ * reference draws discs five times the size.
+ *
+ * @param ctx - the shape's parsed data, carrying dotsize and linewidth
+ * @returns the radius in device units
+ */
+function dotRadius(ctx) {
+ const parts = String(ctx.dotsize ?? DOTSIZE_DEFAULT).trim().split(/\s+/);
+ const base = dimension(parts[0], 2);
+ const factor = Number(parts[1]);
+ const diameter = base + (isFinite(factor) ? factor : 0) * linewidthPx(ctx);
+ return Math.max(0.1, diameter / 2);
+}
+/**
+ * A shape's linewidth in device units.
+ *
+ * Only two of the parse functions convert a `pt` suffix, so the value reaching
+ * a renderer is sometimes a number of pixels and sometimes the string the
+ * author wrote. A bare number keeps its device-unit meaning, matching
+ * parseLinewidth in pstricks.ts.
+ */
+function linewidthPx(ctx) {
+ const v = ctx && ctx.linewidth;
+ if (typeof v === 'number' && isFinite(v))
+ return v;
+ const m = typeof v === 'string' ? v.trim().match(/^([\d.]+)\s*(pt)?$/) : null;
+ if (!m)
+ return DEFAULT_LINEWIDTH_PX;
+ return Number(m[1]) * (m[2] ? PT_TO_PX : 1);
+}
+/**
+ * Whether a command's geometry can be drawn.
+ *
+ * `X` and `Y` return NaN for a coordinate they cannot compute. Handing that to
+ * SVG does not fail visibly: an invalid geometry attribute is treated as absent
+ * and the element falls back to its default, so a broken shape reappears at the
+ * origin looking intentional. Skipping it is the honest result, and the parser
+ * has already reported the reason against the source line.
+ *
+ * @param ctx - a command's parsed data
+ * @returns false when any of its own numeric fields is non-finite
+ */
+function drawable(ctx, depth = 0) {
+ if (depth > 4 || ctx === null || ctx === undefined)
+ return true;
+ if (typeof ctx === 'number')
+ return isFinite(ctx);
+ if (Array.isArray(ctx))
+ return ctx.every((v) => drawable(v, depth + 1));
+ if (typeof ctx !== 'object')
+ return true;
+ return Object.entries(ctx).every(
+ // `global` is the shared environment, not this command's geometry.
+ ([k, v]) => k === 'global' || k === 'env' || drawable(v, depth + 1));
+}
+/**
+ * SVG arc flags for a PSTricks arc running from `angleA` to `angleB`.
+ *
+ * PSTricks always sweeps counter-clockwise in its own coordinates, taking the
+ * long way round when the end angle precedes the start. `Y` inverts the axis,
+ * so that counter-clockwise sweep is drawn with SVG's sweep-flag 0 — using 1
+ * traces the complementary arc, which is what bowed every `\pswedge` inward
+ * and turned a pie chart into a star.
+ *
+ * @param angleA - start angle in radians
+ * @param angleB - end angle in radians
+ * @returns the sweep span plus SVG's large-arc and sweep flags
+ */
+function arcFlags(angleA, angleB) {
+ let raw = angleB - angleA;
+ if (!isFinite(raw))
+ raw = 0;
+ // A span of a full turn or more paints the whole circle: PSTricks keeps
+ // sweeping past 360 and simply overlaps itself, so \psarc{0}{450} is a
+ // circle, not the 90 degrees the modulo below leaves behind. Reducing first
+ // and asking afterwards is what lost the extra turn.
+ const full = Math.abs(raw) >= TAU - 1e-9;
+ const delta = ((raw % TAU) + TAU) % TAU;
+ return { delta, large: delta > Math.PI ? 1 : 0, sweep: 0, full };
+}
+/**
+ * A full turn cannot be expressed as one SVG arc, because the start and end
+ * points coincide. Such a sweep is emitted as two half-turns instead.
+ *
+ * @param cx - centre x in device units
+ * @param cy - centre y in device units
+ * @param r - radius in device units
+ * @returns a closed circular path
+ */
+function fullCirclePath(cx, cy, r) {
+ return ('M ' + (cx - r) + ' ' + cy +
+ ' A ' + r + ' ' + r + ' 0 1 0 ' + (cx + r) + ' ' + cy +
+ ' A ' + r + ' ' + r + ' 0 1 0 ' + (cx - r) + ' ' + cy + ' Z');
+}
+function curveRenderer(svg) {
+ const mode = this.endpoints ? 'endpoints' : this.closed ? 'closed' : 'open';
+ const d = buildCurvePath(this.data, mode, this);
+ if (!d)
+ return;
+ svg
+ .append('svg:path')
+ .attr('d', d)
+ .style('stroke-width', this.linewidth)
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this))
+ .style('stroke-opacity', 1)
+ .style('fill', resolveFill(this, svg));
+}
+const SVG_NS = 'http://www.w3.org/2000/svg';
+/**
+ * A selection-like wrapper that patches nodes in place instead of appending.
+ *
+ * Renderers call `svg.append('svg:path').attr(...).style(...)` and never
+ * inspect what they built, so this shim satisfies the same interface while
+ * reconciling: `append` reuses the child that occupied the same slot last
+ * time when its tag still matches, and `attr`/`style`/`text` skip writes
+ * whose value is already there. Appending past the previous child count
+ * creates nodes; children left over from a bigger previous frame are dropped
+ * by {@link PatchSelection#prune}.
+ *
+ * Keying children by position inside an element is what lets a renderer that
+ * emits a variable number of nodes — psgrid at a new subdivision, psline with
+ * a different point count — still patch in place and shed its leftovers.
+ */
+class PatchSelection {
+ constructor(node) {
+ /** Number of appends this pass; public so the caller can prune by it. */
+ this.slot = 0;
+ this.node = node;
+ }
+ append(tagName) {
+ const node = this.node;
+ if (!node)
+ return new PatchSelection(null);
+ const tag = tagName.startsWith('svg:') ? tagName.slice(4) : tagName;
+ const existing = node.children[this.slot];
+ let child;
+ if (existing && existing.localName === tag) {
+ child = existing;
+ }
+ else {
+ child = document.createElementNS(SVG_NS, tag);
+ if (existing)
+ node.replaceChild(child, existing);
+ else
+ node.appendChild(child);
+ }
+ this.slot++;
+ return new PatchSelection(child);
+ }
+ attr(name, value) {
+ if (this.node) {
+ const v = String(value);
+ if (this.node.getAttribute(name) !== v)
+ this.node.setAttribute(name, v);
+ }
+ return this;
+ }
+ style(name, value) {
+ if (this.node instanceof SVGElement) {
+ const v = String(value);
+ const styles = this.node.style;
+ if (styles[name] !== v)
+ styles[name] = v;
+ }
+ return this;
+ }
+ text(content) {
+ if (this.node && this.node.textContent !== content)
+ this.node.textContent = content;
+ return this;
+ }
+ /** Removes children past the last slot written this pass. */
+ prune() {
+ const node = this.node;
+ if (!node)
+ return;
+ while (node.children.length > this.slot) {
+ node.removeChild(node.children[node.children.length - 1]);
+ }
+ }
+}
const psgraph = {
env: null,
getSize() {
@@ -996,6 +4112,17 @@ const psgraph = {
};
},
psframe(svg) {
+ const filled = hasFill(this);
+ if (filled) {
+ svg
+ .append('svg:rect')
+ .attr('x', Math.min(this.x1, this.x2))
+ .attr('y', Math.min(this.y1, this.y2))
+ .attr('width', Math.abs(this.x2 - this.x1))
+ .attr('height', Math.abs(this.y2 - this.y1))
+ .style('fill', resolveFill(this, svg))
+ .style('stroke', 'none');
+ }
svg
.append('svg:line')
.attr('x1', this.x1)
@@ -1003,7 +4130,9 @@ const psgraph = {
.attr('x2', this.x2)
.attr('y2', this.y1)
.style('stroke-width', 2)
- .style('stroke', 'rgb(0,0,0)')
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this))
.style('stroke-opacity', 1);
svg
.append('svg:line')
@@ -1012,7 +4141,9 @@ const psgraph = {
.attr('x2', this.x2)
.attr('y2', this.y2)
.style('stroke-width', 2)
- .style('stroke', 'rgb(0,0,0)')
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this))
.style('stroke-opacity', 1);
svg
.append('svg:line')
@@ -1021,7 +4152,9 @@ const psgraph = {
.attr('x2', this.x1)
.attr('y2', this.y2)
.style('stroke-width', 2)
- .style('stroke', 'rgb(0,0,0)')
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this))
.style('stroke-opacity', 1);
svg
.append('svg:line')
@@ -1030,24 +4163,46 @@ const psgraph = {
.attr('x2', this.x1)
.attr('y2', this.y1)
.style('stroke-width', 2)
- .style('stroke', 'rgb(0,0,0)')
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this))
.style('stroke-opacity', 1);
},
pscircle: function (svg) {
+ const filled = hasFill(this);
svg
.append('svg:circle')
.attr('cx', this.cx)
.attr('cy', this.cy)
.attr('r', this.r)
- .style('stroke', 'black')
- .style('fill', 'none')
- .style('stroke-width', 2)
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this))
+ .style('fill', resolveFill(this, svg))
+ .style('stroke-width', this.linewidth)
.style('stroke-opacity', 1);
},
psplot(svg) {
+ // `plotstyle=dots` marks the samples instead of joining them. It was parsed
+ // and dropped, so a plot asking for dots drew a line through them — or, at
+ // plotpoints=1, a path of one point, which is nothing at all. That is why
+ // the tangent markers were missing from every picture in graph.tex.
+ if (this.plotstyle === 'dots') {
+ for (let i = 0; i < this.data.length; i += 2) {
+ svg
+ .append('svg:circle')
+ .attr('cx', this.data[i])
+ .attr('cy', this.data[i + 1])
+ .attr('r', dotRadius(this))
+ .attr('class', 'psplot')
+ .style('fill', this.linecolor)
+ .style('stroke', 'none');
+ }
+ return;
+ }
var context = [];
context.push('M');
- if (this.fillstyle === 'solid') {
+ if (hasFill(this)) {
context.push(this.data[0]);
context.push(utils_1.Y.call(this.global, 0));
}
@@ -1059,7 +4214,7 @@ const psgraph = {
this.data.forEach((data) => {
context.push(data);
});
- if (this.fillstyle === 'solid') {
+ if (hasFill(this)) {
context.push(this.data[this.data.length - 2]);
context.push(utils_1.Y.call(this.global, 0));
context.push('Z');
@@ -1070,8 +4225,10 @@ const psgraph = {
.attr('class', 'psplot')
.style('stroke-width', this.linewidth)
.style('stroke-opacity', 1)
- .style('fill', this.fillstyle === 'none' ? 'none' : this.fillcolor)
- .style('stroke', this.linecolor);
+ .style('fill', resolveFill(this, svg))
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this));
},
pspolygon(svg) {
var context = [];
@@ -1088,206 +4245,274 @@ const psgraph = {
.attr('d', context.join(' '))
.style('stroke-width', this.linewidth)
.style('stroke-opacity', 1)
- .style('fill', this.fillstyle === 'none' ? 'none' : this.fillcolor)
- .style('stroke', 'black');
+ .style('fill', resolveFill(this, svg))
+ // Was hardcoded black, so linecolor and linestyle=none were both ignored.
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this));
},
psarc(svg) {
- var context = [];
- context.push('M');
- context.push(this.cx);
- context.push(this.cy);
- context.push('L');
- context.push(this.A.x);
- context.push(this.A.y);
- context.push('A');
- context.push(this.A.x);
- context.push(this.A.y);
- context.push(0);
- context.push(0);
- context.push(0);
- context.push(this.B.x);
- context.push(this.B.y);
+ const { delta, large, sweep, full } = arcFlags(this.angleA, this.angleB);
+ const filled = hasFill(this);
+ const arc = ' A ' + this.r + ' ' + this.r + ' 0 ' + large + ' ' + sweep +
+ ' ' + this.B.x + ' ' + this.B.y;
+ const d = full || delta === 0
+ ? fullCirclePath(this.cx, this.cy, this.r)
+ : filled
+ ? 'M ' + this.cx + ' ' + this.cy + ' L ' + this.A.x + ' ' + this.A.y + arc + ' Z'
+ : 'M ' + this.A.x + ' ' + this.A.y + arc;
svg
.append('svg:path')
- .attr('d', context.join(' '))
- .style('stroke-width', 2)
+ .attr('d', d)
+ .style('stroke-width', this.linewidth)
.style('stroke-opacity', 1)
- .style('fill', 'blue')
- .style('stroke', 'black');
+ .style('fill', resolveFill(this, svg))
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this));
},
psaxes(svg) {
var xaxis = [this.bottomLeft[0], this.topRight[0]];
var yaxis = [this.bottomLeft[1], this.topRight[1]];
var origin = this.origin;
+ // Resolved once here: the helper below is an inner function, so it cannot
+ // reach the axes' own data through `this`.
+ const axisStroke = resolveStroke(this);
function line(x1, y1, x2, y2) {
svg
.append('svg:path')
.attr('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2)
.style('stroke-width', 2)
- .style('stroke', 'rgb(0,0,0)')
+ .style('stroke', axisStroke)
.style('stroke-opacity', 1);
}
+ /**
+ * Tick positions, stepped outward from the origin rather than from the end
+ * of the axis. Starting at the end puts every mark at whatever offset the
+ * axis happens to begin on, so an axis spanning -3.5 to 3.5 was ticked and
+ * labelled at half-integers instead of on the whole numbers.
+ */
+ /**
+ * An axis end that carries an arrowhead, or null. `arrows[0]` points at the
+ * low end of each axis and `arrows[1]` at the high end, matching the order
+ * the arrowheads are drawn below.
+ */
+ const arrowedEnds = (axis) => [
+ this.arrows[0] ? axis[0] : null,
+ this.arrows[1] ? axis[1] : null,
+ ];
+ const positions = (from, to, at, step) => {
+ if (!(step > 0) || !isFinite(step))
+ return [];
+ // Y inverts the axis, so a vertical span arrives with its ends the other
+ // way round. Walking it as given produced no y ticks at all.
+ const lo = Math.min(from, to);
+ const hi = Math.max(from, to);
+ const out = [];
+ for (let v = at; v <= hi + 1e-6; v += step)
+ out.push(v);
+ for (let v = at - step; v >= lo - 1e-6; v -= step)
+ out.unshift(v);
+ // PSTricks gives an arrowhead the end of the axis to itself: where one is
+ // drawn, the tick and its number are both suppressed. A tick that merely
+ // falls short of the tip keeps them, so only a coincident one is dropped.
+ const suppressed = arrowedEnds([from, to]).filter((v) => v !== null);
+ return out.filter((v) => !suppressed.some((end) => Math.abs(v - end) < 1e-6));
+ };
var xticks = () => {
- for (var x = xaxis[0]; x <= xaxis[1]; x += this.dx) {
+ positions(xaxis[0], xaxis[1], origin[0], this.dx).forEach((x) => {
+ // showorigin=false suppresses the tick at the origin itself.
+ if (this.showorigin === false && Math.abs(x - origin[0]) < 1e-6)
+ return;
line(x, origin[1] - 5, x, origin[1] + 5);
- }
+ });
};
var yticks = () => {
- for (var y = yaxis[0]; y <= yaxis[1]; y += this.dy) {
+ positions(yaxis[0], yaxis[1], origin[1], this.dy).forEach((y) => {
+ if (this.showorigin === false && Math.abs(y - origin[1]) < 1e-6)
+ return;
line(origin[0] - 5, y, origin[0] + 5, y);
- }
+ });
+ };
+ const env = this.global || {};
+ /** Draws one tick number, positioned clear of its axis. */
+ const label = (text, x, y, anchor) => {
+ svg
+ .append('svg:text')
+ .attr('x', x)
+ .attr('y', y)
+ .attr('text-anchor', anchor)
+ .attr('font-size', 13)
+ .attr('font-family', 'serif')
+ .style('fill', 'black')
+ .text(text);
+ };
+ /** Tick values are device coordinates; labels need the value they stand for. */
+ const value = (device, axis) => {
+ const n = axis === 'x'
+ ? device / env.xunit - env.w + env.x1
+ : env.y1 - device / env.yunit;
+ return Math.abs(n) < 1e-9 ? 0 : Number(n.toFixed(4));
+ };
+ const xlabels = () => {
+ positions(xaxis[0], xaxis[1], origin[0], this.dx).forEach((x) => {
+ // The origin's number sits directly under the y axis, which would draw
+ // the axis line straight through the glyph, so it shifts clear of it
+ // and serves both axes — as it does on a hand-drawn pair of axes.
+ const atOrigin = Math.abs(x - origin[0]) < 1e-6;
+ // showorigin=false drops the number at the origin with its tick.
+ if (atOrigin && this.showorigin === false)
+ return;
+ if (atOrigin)
+ label(String(value(x, 'x')), x - 7, origin[1] + 20, 'end');
+ else
+ label(String(value(x, 'x')), x, origin[1] + 20, 'middle');
+ });
+ };
+ const ylabels = () => {
+ positions(yaxis[0], yaxis[1], origin[1], this.dy).forEach((y) => {
+ // The origin's own number belongs to the x axis; drawing it again here
+ // would stack two glyphs in the same place.
+ if (Math.abs(y - origin[1]) < 1e-6)
+ return;
+ label(String(value(y, 'y')), origin[0] - 10, y + 4, 'end');
+ });
};
line(xaxis[0], origin[1], xaxis[1], origin[1]);
line(origin[0], yaxis[0], origin[0], yaxis[1]);
- if (this.ticks.match(/all/)) {
- xticks();
- yticks();
- }
- else if (this.ticks.match(/x/)) {
+ const selects = (option, axis) => {
+ const v = String(option ?? 'all');
+ if (v.match(/none/))
+ return false;
+ return !!(v.match(/all/) || v.match(axis));
+ };
+ if (selects(this.ticks, 'x'))
xticks();
- }
- else if (this.ticks.match(/y/)) {
+ if (selects(this.ticks, 'y'))
yticks();
- }
+ if (env.xunit && selects(this.labels, 'x'))
+ xlabels();
+ if (env.yunit && selects(this.labels, 'y'))
+ ylabels();
if (this.arrows[0]) {
svg
.append('path')
- .attr('d', arrow(xaxis[1], origin[1], xaxis[0], origin[1]))
+ .attr('d', arrow(xaxis[1], origin[1], xaxis[0], origin[1], this.arrowscale))
.style('fill', 'black')
.style('stroke', 'black');
svg
.append('path')
- .attr('d', arrow(origin[0], yaxis[1], origin[0], yaxis[0]))
+ .attr('d', arrow(origin[0], yaxis[1], origin[0], yaxis[0], this.arrowscale))
.style('fill', 'black')
.style('stroke', 'black');
}
if (this.arrows[1]) {
svg
.append('path')
- .attr('d', arrow(xaxis[0], origin[1], xaxis[1], origin[1]))
+ .attr('d', arrow(xaxis[0], origin[1], xaxis[1], origin[1], this.arrowscale))
.style('fill', 'black')
.style('stroke', 'black');
svg
.append('path')
- .attr('d', arrow(origin[0], yaxis[0], origin[0], yaxis[1]))
+ .attr('d', arrow(origin[0], yaxis[0], origin[0], yaxis[1], this.arrowscale))
.style('fill', 'black')
.style('stroke', 'black');
}
},
psline(svg) {
- var linewidth = this.linewidth, linecolor = this.linecolor;
- function solid(x1, y1, x2, y2) {
- svg
- .append('svg:path')
- .attr('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2)
- .style('stroke-width', linewidth)
- .style('stroke', linecolor)
- .style('stroke-opacity', 1);
- }
- function dashed(x1, y1, x2, y2) {
- svg
- .append('svg:path')
- .attr('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2)
- .style('stroke-width', linewidth)
- .style('stroke', linecolor)
- .style('stroke-dasharray', '9,5')
- .style('stroke-opacity', 1);
- }
- function dotted(x1, y1, x2, y2) {
+ var linewidth = this.linewidth,
+ // Resolved here so `linestyle=none` suppresses the stroke: the helpers
+ // below are inner functions and cannot reach the shape through `this`.
+ linecolor = resolveStroke(this);
+ // One drawing function for all three styles. There used to be three, and
+ // `dashed` and `dotted` were byte-identical — both hardcoded `9,5` — so a
+ // dotted line rendered as a dashed one.
+ const dash = dashArray(this);
+ const cap = dashCap(this);
+ function draw(x1, y1, x2, y2) {
svg
.append('svg:path')
.attr('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2)
.style('stroke-width', linewidth)
.style('stroke', linecolor)
- .style('stroke-dasharray', '9,5')
+ .style('stroke-dasharray', dash)
+ .style('stroke-linecap', cap)
.style('stroke-opacity', 1);
}
- if (this.linestyle.match(/dotted/)) {
- dotted(this.x1, this.y1, this.x2, this.y2);
- }
- else if (this.linestyle.match(/dashed/)) {
- dashed(this.x1, this.y1, this.x2, this.y2);
- }
- else {
- solid(this.x1, this.y1, this.x2, this.y2);
- }
- if (this.dots[0]) {
- svg
- .append('svg:circle')
- .attr('cx', this.x1)
- .attr('cy', this.y1)
- .attr('r', 3)
- .style('stroke', this.linecolor)
- .style('fill', this.linecolor)
- .style('stroke-width', 1)
- .style('stroke-opacity', 1);
+ // Every segment of the polyline. A two-point line is the same drawing it
+ // always was; anything past the second point used to be dropped.
+ const pts = this.points && this.points.length >= 2
+ ? this.points
+ : [[this.x1, this.y1], [this.x2, this.y2]];
+ for (let i = 0; i < pts.length - 1; i++) {
+ draw(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1]);
}
- if (this.dots[1]) {
+ // The markers go on the ENDS of the polyline, like the arrowheads below;
+ // x1..y2 name the first segment, which on a three-point line is its
+ // middle vertex.
+ const marker = (at) => {
svg
.append('svg:circle')
- .attr('cx', this.x2)
- .attr('cy', this.y2)
- .attr('r', 3)
- .style('stroke', this.linecolor)
+ .attr('cx', at[0])
+ .attr('cy', at[1])
+ .attr('r', dotRadius(this))
+ .style('stroke', resolveStroke(this))
.style('fill', this.linecolor)
.style('stroke-width', 1)
.style('stroke-opacity', 1);
- }
- var x1 = this.x1, y1 = this.y1, x2 = this.x2, y2 = this.y2;
+ };
+ if (this.dots[0])
+ marker(pts[0]);
+ if (this.dots[1])
+ marker(pts[pts.length - 1]);
+ // An arrowhead belongs on the END of the polyline, and points along the
+ // last segment. Reading x1..y2 put it on the first segment instead, so a
+ // three-point line grew a head at its middle vertex.
+ const head = pts[pts.length - 1];
+ const beforeHead = pts[pts.length - 2];
+ const tail = pts[0];
+ const afterTail = pts[1];
if (this.arrows[0]) {
svg
.append('path')
- .attr('d', arrow(x2, y2, x1, y1))
+ .attr('d', arrow(afterTail[0], afterTail[1], tail[0], tail[1], this.arrowscale))
.style('fill', this.linecolor)
- .style('stroke', this.linecolor);
+ .style('stroke', resolveStroke(this));
}
if (this.arrows[1]) {
svg
.append('path')
- .attr('d', arrow(x1, y1, x2, y2))
+ .attr('d', arrow(beforeHead[0], beforeHead[1], head[0], head[1], this.arrowscale))
.style('fill', this.linecolor)
- .style('stroke', this.linecolor);
+ .style('stroke', resolveStroke(this));
}
},
userline(svg) {
- var linewidth = this.linewidth, linecolor = this.linecolor;
- function solid(x1, y1, x2, y2) {
- svg
- .append('svg:path')
- .attr('class', 'userline')
- .attr('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2)
- .style('stroke-width', linewidth)
- .style('stroke', linecolor)
- .style('stroke-opacity', 1);
- }
- function dashed(x1, y1, x2, y2) {
+ var linewidth = this.linewidth,
+ // Resolved here so `linestyle=none` suppresses the stroke: the helpers
+ // below are inner functions and cannot reach the shape through `this`.
+ linecolor = resolveStroke(this);
+ // One drawing function for all three styles; see the note in psline.
+ const dash = dashArray(this);
+ const cap = dashCap(this);
+ function draw(x1, y1, x2, y2) {
svg
.append('svg:path')
- .attr('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2)
.attr('class', 'userline')
- .style('stroke-width', linewidth)
- .style('stroke', linecolor)
- .style('stroke-dasharray', '9,5')
- .style('stroke-opacity', 1);
- }
- function dotted(x1, y1, x2, y2) {
- svg
- .append('svg:path')
.attr('d', 'M ' + x1 + ' ' + y1 + ' L ' + x2 + ' ' + y2)
- .attr('class', 'userline')
.style('stroke-width', linewidth)
.style('stroke', linecolor)
- .style('stroke-dasharray', '9,5')
+ .style('stroke-dasharray', dash)
+ .style('stroke-linecap', cap)
.style('stroke-opacity', 1);
}
- if (this.linestyle.match(/dotted/)) {
- dotted(this.x1, this.y1, this.x2, this.y2);
- }
- else if (this.linestyle.match(/dashed/)) {
- dashed(this.x1, this.y1, this.x2, this.y2);
- }
- else {
- solid(this.x1, this.y1, this.x2, this.y2);
+ // Every segment of the polyline. A two-point line is the same drawing it
+ // always was; anything past the second point used to be dropped.
+ const pts = this.points && this.points.length >= 2
+ ? this.points
+ : [[this.x1, this.y1], [this.x2, this.y2]];
+ for (let i = 0; i < pts.length - 1; i++) {
+ draw(pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1]);
}
if (this.dots[0]) {
svg
@@ -1296,7 +4521,7 @@ const psgraph = {
.attr('cy', this.y1)
.attr('r', 3)
.attr('class', 'userline')
- .style('stroke', this.linecolor)
+ .style('stroke', resolveStroke(this))
.style('fill', this.linecolor)
.style('stroke-width', 1)
.style('stroke-opacity', 1);
@@ -1308,7 +4533,7 @@ const psgraph = {
.attr('cy', this.y2)
.attr('r', 3)
.attr('class', 'userline')
- .style('stroke', this.linecolor)
+ .style('stroke', resolveStroke(this))
.style('fill', this.linecolor)
.style('stroke-width', 1)
.style('stroke-opacity', 1);
@@ -1317,20 +4542,42 @@ const psgraph = {
if (this.arrows[0]) {
svg
.append('path')
- .attr('d', arrow(x2, y2, x1, y1))
+ .attr('d', arrow(x2, y2, x1, y1, this.arrowscale))
.attr('class', 'userline')
.style('fill', this.linecolor)
- .style('stroke', this.linecolor);
+ .style('stroke', resolveStroke(this));
}
if (this.arrows[1]) {
svg
.append('path')
- .attr('d', arrow(x1, y1, x2, y2))
+ .attr('d', arrow(x1, y1, x2, y2, this.arrowscale))
.attr('class', 'userline')
.style('fill', this.linecolor)
- .style('stroke', this.linecolor);
+ .style('stroke', resolveStroke(this));
}
},
+ /**
+ * Graphics placed by an `\rput`, drawn into a translated group.
+ *
+ * The label form of rput is handled separately, in the DOM pass below. This
+ * is the case where the contents are shapes: they are drawn here so they
+ * keep their place in document order, which the DOM pass cannot express
+ * because it appends after the SVG is finished.
+ */
+ rputgroup(svg) {
+ const g = svg
+ .append('svg:g')
+ .attr('class', 'rput-group')
+ .attr('transform', 'translate(' + this.dx + ',' + this.dy + ')');
+ (this.children || []).forEach((child) => {
+ if (!child || !psgraph.hasOwnProperty(child.key))
+ return;
+ if (!drawable(child.data))
+ return;
+ child.data.global = this.global;
+ psgraph[child.key].call(child.data, g);
+ });
+ },
rput(el) {
// Import debug utilities
const startTime = Date.now();
@@ -1416,7 +4663,9 @@ const psgraph = {
};
// Enhanced MathJax processing with better async handling
const processContent = async () => {
- const mathJax = window.MathJax;
+ // Awaited, not read once: the global is assigned before the library it
+ // names has loaded, so a synchronous check races the CDN.
+ const mathJax = await mathJaxWhenReady();
if (mathJax && mathJax.typesetPromise) {
try {
// Set content before MathJax processing
@@ -1458,89 +4707,603 @@ const psgraph = {
pspicture(svg) {
var env = this.env;
var el = this.$el;
- Object.keys(this.plot).forEach((key) => {
- const plot = this.plot[key];
- if (key.match(/rput/))
+ const plots = this.plot;
+ // The parser records `env.elements` in document order, so fills sit under
+ // lines exactly as authored.
+ const elements = env && env.elements;
+ /**
+ * Recomputes an interactive element against the pointer position and the
+ * current variable values.
+ *
+ * Only elements whose data is derived from something that moves can change:
+ * a psplot re-runs its sampled function against the variables it names, and
+ * a userline re-evaluates its head/tail expressions at the pointer. Anything
+ * else keeps the geometry the parser produced.
+ */
+ function resolveDynamic(item, coords, variables) {
+ if (item.name === 'psplot') {
+ Object.entries(variables || {}).forEach(([name, value]) => {
+ env.variables[name] = value;
+ });
+ const d = item.fn.call(env, item.match);
+ d.global = Object.assign({}, env);
+ return d;
+ }
+ if (item.name === 'userline') {
+ // The head/tail expressions can only be evaluated against a pointer;
+ // without one the element keeps the geometry it was parsed with.
+ if (!coords)
+ return item.data;
+ const d = item.fn.call(env, item.match);
+ env.x2 = coords[0];
+ env.y2 = coords[1];
+ item.data.x2 = env.x2;
+ item.data.y2 = env.y2;
+ if (item.data.xExp2) {
+ item.data.x2 = d.userx2(coords);
+ item.data.x1 = d.userx(coords);
+ }
+ else if (item.data.xExp) {
+ item.data.x2 = d.userx(coords);
+ }
+ if (item.data.yExp2) {
+ item.data.y2 = d.usery2(coords);
+ item.data.y1 = d.usery(coords);
+ }
+ else if (item.data.yExp) {
+ item.data.y2 = d.usery(coords);
+ }
+ d.global = Object.assign({}, env);
+ Object.assign(d, item.data);
+ return d;
+ }
+ return item.data;
+ }
+ /**
+ * The entry the first frame uses: without a pointer position the parser's
+ * own data is the contract. Re-running a plot's function here would resolve
+ * variables that appear later in the document, which the parse-time data
+ * deliberately does not see.
+ */
+ function resolveData(item, coords, variables) {
+ if (!coords || !item.fn)
+ return item.data;
+ return resolveDynamic(item, coords, variables);
+ }
+ /** Evaluates every \uservariable at the pointer position, in source order. */
+ function readVariables(coords) {
+ const variables = {};
+ const source = elements && elements.length
+ ? elements.filter((i) => i && i.name === 'uservariable')
+ : ((plots && plots.uservariable) || []).map((p) => ({ ...p, name: 'uservariable' }));
+ source.forEach((item) => {
+ env.userx = coords[0];
+ env.usery = coords[1];
+ const dd = item.fn.call(env, item.match);
+ variables[item.data.name] = dd.value;
+ });
+ return variables;
+ }
+ // ---- dependency graph ------------------------------------------------
+ /**
+ * The variables an algebraic expression reads, minus the pointer/sampling
+ * names x and y that every evaluation scope supplies. Parsed with the same
+ * compiler the plot sampler uses, so function names and math constants
+ * never count as dependencies.
+ */
+ function expressionVariables(expr) {
+ const src = String(expr).replace(/^\{/, '').replace(/\}$/, '');
+ try {
+ return (0, utils_1.parseExpression)(src)
+ .variables()
+ .filter((n) => n !== 'x' && n !== 'y');
+ }
+ catch {
+ return [];
+ }
+ }
+ /**
+ * What an element's output depends on, derived from the element's own data
+ * and match rather than from a list of command names:
+ *
+ * - a data field whose value is a function — userline's userx/usery/…
+ * — is evaluated against the pointer on every move: the pointer dependency;
+ * - every expression string the element carries (uservariable's func,
+ * userline's head and tail, psplot's sampled function and its range)
+ * names the variables it reads.
+ *
+ * Anything else is static: the parser has already resolved its geometry.
+ */
+ const depsCache = new Map();
+ function depsFor(item) {
+ const cached = depsCache.get(item);
+ if (cached)
+ return cached;
+ const data = item && item.data;
+ const vars = new Set();
+ let pointer = false;
+ if (data) {
+ pointer = Object.values(data).some((v) => typeof v === 'function');
+ for (const k of ['func', 'xExp', 'yExp', 'xExp2', 'yExp2']) {
+ const expr = data[k];
+ if (typeof expr === 'string' && expr) {
+ expressionVariables(expr).forEach((n) => vars.add(n));
+ }
+ }
+ }
+ // psplot's sampled function and its sample range live in the parser's
+ // match record, not in the resolved data.
+ if (item && item.match) {
+ for (const g of [2, 3, 4]) {
+ const expr = item.match[g];
+ if (typeof expr === 'string' && expr) {
+ expressionVariables(expr).forEach((n) => vars.add(n));
+ }
+ }
+ }
+ const deps = { pointer, variables: [...vars] };
+ depsCache.set(item, deps);
+ return deps;
+ }
+ // ---- incremental reconciliation --------------------------------------
+ let layer = null; // SVGSelection over the picture layer
+ let layerNode = null;
+ // A group per renderable element, tracked by the element itself so a
+ // group follows its element when the list grows or shrinks; the group's
+ // `data-key` attribute carries the element's current index in document
+ // order, which is what keeps the reconciliation ordered.
+ const groups = new Map();
+ const drawn = new Set(); // elements whose group holds output
+ let lastCoords = null;
+ let lastVariables = {};
+ /** Drops the layer and everything reconciled into it. */
+ function clearLayer() {
+ if (layer)
+ layer.remove();
+ groups.clear();
+ drawn.clear();
+ layer = svg.append('svg:g').attr('class', 'pspicture-layer');
+ layerNode = layer.node();
+ }
+ /**
+ * Draws the picture, reconciling against what is already on screen.
+ *
+ * Every renderable element gets its own , keyed by the element and
+ * placed in document order, so reusing the node in place is what keeps
+ * painting order faithful to the source. The full-redraw alternative
+ * could not: removing just the interactive elements and appending them
+ * again put them at the end of the SVG, on top of every later shape, and
+ * re-emitted them grouped by command type rather than in document order,
+ * so a correct diagram silently reordered itself the first time the
+ * pointer crossed it.
+ *
+ * An element is only re-rendered when something it depends on actually
+ * changed: the pointer (userline), or one of the variables its expressions
+ * name — a psplot re-runs only when a referenced \uservariable or slider
+ * variable moved. Static elements keep their nodes untouched.
+ */
+ function drawLayer(coords, opts) {
+ opts = opts || {};
+ // Legacy data without an ordered element list cannot express author
+ // order at all, so it keeps the wholesale rebuild: there is no order to
+ // preserve and no per-element state worth reconciling.
+ if (!elements || !elements.length) {
+ clearLayer();
+ const variables = coords ? readVariables(coords) : {};
+ Object.keys(plots).forEach((key) => {
+ if (key === 'rput')
+ return;
+ if (!psgraph.hasOwnProperty(key))
+ return;
+ plots[key].forEach((entry) => {
+ const item = { name: key, data: entry.data, match: entry.match, fn: entry.fn };
+ const data = resolveData(item, coords, variables);
+ data.global = env;
+ psgraph[key].call(data, layer);
+ });
+ });
return;
- if (psgraph.hasOwnProperty(key)) {
- plot.forEach((data) => {
- data.data.global = env;
- psgraph[key].call(data.data, svg);
+ }
+ if (opts.force)
+ clearLayer();
+ if (!layerNode)
+ clearLayer();
+ const variables = coords ? readVariables(coords) : {};
+ const changed = new Set(opts.changed || []);
+ if (coords) {
+ // The \uservariable values that moved with the pointer — the only way
+ // a plot's data can differ between two pointer positions.
+ Object.entries(variables).forEach(([name, value]) => {
+ if (!(name in lastVariables) || lastVariables[name] !== value)
+ changed.add(name);
});
+ lastVariables = variables;
+ lastCoords = coords;
}
- });
+ const pointerMoved = !!coords && !!opts.pointer;
+ let prevGroup = null;
+ const touched = new Set();
+ for (let i = 0; i < elements.length; i++) {
+ const item = elements[i];
+ // Exact, not a pattern: `rputgroup` is the graphics form of rput and
+ // must be drawn here. Only the label form goes through the DOM pass.
+ if (!item || !item.name || item.name === 'rput')
+ continue;
+ if (!psgraph.hasOwnProperty(item.name))
+ continue;
+ touched.add(item);
+ const deps = depsFor(item);
+ const needs = opts.force ||
+ !drawn.has(item) ||
+ (deps.pointer && pointerMoved) ||
+ deps.variables.some((n) => changed.has(n));
+ // A group that is not re-rendered still needs its key brought up to
+ // date: when a neighbouring element left the picture, every later
+ // element's index shifted.
+ const key = (g) => {
+ if (g.getAttribute('data-key') !== String(i))
+ g.setAttribute('data-key', String(i));
+ };
+ if (!needs) {
+ const group = groups.get(item);
+ if (group) {
+ key(group);
+ prevGroup = group;
+ }
+ continue;
+ }
+ let data;
+ if (coords || opts.force) {
+ data = resolveData(item, coords, variables);
+ }
+ else if (item.name === 'psplot') {
+ // A slider-only re-render still has to re-run the plot's function:
+ // resolveData would hand back the parse-time data and swallow the
+ // move, because the new value lives in env.variables, not the
+ // pointer. userline geometry is pointer-driven, so in this case it
+ // keeps its last drawn state.
+ data = resolveDynamic(item, coords, variables);
+ }
+ else {
+ data = item.data;
+ }
+ // A coordinate that could not be computed arrives as NaN. Drawing it
+ // anyway is the trap: SVG treats an invalid geometry attribute as
+ // absent and falls back to its default, so the shape reappears at the
+ // origin looking deliberate. Nothing is the honest output.
+ if (!drawable(data)) {
+ const stale = groups.get(item);
+ if (stale) {
+ stale.remove();
+ groups.delete(item);
+ }
+ drawn.delete(item);
+ continue;
+ }
+ let group = groups.get(item);
+ if (!group) {
+ group = document.createElementNS(SVG_NS, 'g');
+ group.setAttribute('data-key', String(i));
+ if (prevGroup && prevGroup.nextSibling) {
+ layerNode.insertBefore(group, prevGroup.nextSibling);
+ }
+ else {
+ layerNode.appendChild(group);
+ }
+ groups.set(item, group);
+ }
+ else {
+ key(group);
+ }
+ prevGroup = group;
+ data.global = env;
+ const sel = new PatchSelection(group);
+ psgraph[item.name].call(data, sel);
+ if (sel.slot === 0) {
+ // The renderer drew nothing (a curve with too few points, say): an
+ // earlier frame's shapes must not linger under the new data.
+ while (group.firstChild)
+ group.removeChild(group.firstChild);
+ drawn.delete(item);
+ }
+ else {
+ sel.prune();
+ drawn.add(item);
+ }
+ }
+ // Elements that left the picture (or whose geometry went bad) drop
+ // their group; the rest stay exactly where they were.
+ groups.forEach((group, item) => {
+ if (!touched.has(item)) {
+ group.remove();
+ groups.delete(item);
+ drawn.delete(item);
+ }
+ });
+ }
+ // The first frame is a full redraw: nothing is on screen yet.
+ drawLayer(null, { force: true });
+ // Expose the incremental renderer so the slider component and the tests
+ // can drive re-renders the way pointer events do.
+ this.redraw = (arg) => {
+ if (Array.isArray(arg) || arg === null || arg === undefined) {
+ drawLayer(arg === undefined ? lastCoords : arg, { pointer: true });
+ }
+ else {
+ drawLayer(arg.coords !== undefined ? arg.coords : lastCoords, arg);
+ }
+ };
svg.on('touchmove', function (event) {
event.preventDefault();
var touch = event.touches ? event.touches[0] : null;
var rect = event.target.getBoundingClientRect();
var touchcoords = touch ? [touch.clientX - rect.left, touch.clientY - rect.top] : [0, 0];
- userEvent(touchcoords);
+ drawLayer(touchcoords, { pointer: true });
});
svg.on('mousemove', function (event) {
var coords = [event.offsetX || 0, event.offsetY || 0];
- userEvent(coords);
+ drawLayer(coords, { pointer: true });
});
- const plots = this.plot;
- function userEvent(coords) {
- svg.selectAll('.userline').remove();
- svg.selectAll('.psplot').remove();
- var currentEnvironment = {};
- Object.entries(plots || {})
- .forEach(([k, plot]) => {
- if (k.match(/uservariable/)) {
- plot.forEach((data) => {
- data.env.userx = coords[0];
- data.env.usery = coords[1];
- var dd = data.fn.call(data.env, data.match);
- currentEnvironment[data.data.name] = dd.value;
- });
+ // Enhanced cleanup and RPUT processing
+ psgraph.processRputElements.call(this, el);
+ },
+ psdots(svg) {
+ for (let i = 0; i < this.data.length; i += 2) {
+ svg
+ .append('svg:circle')
+ .attr('cx', this.data[i])
+ .attr('cy', this.data[i + 1])
+ .attr('r', dotRadius(this))
+ .style('fill', this.linecolor)
+ .style('stroke', 'none');
+ }
+ },
+ /**
+ * A PSTricks grid is three things, not one: fine subdivision lines, a heavier
+ * line on each unit, and the coordinate numbered along the left and bottom
+ * edges. Only the unit lines were drawn, in `linecolor` — which `gridcolor`
+ * could not override — so a grid was a flat mesh with no reading on it.
+ */
+ psgrid(svg) {
+ const x0 = this.x0, y0 = this.y0, x1 = this.x1, y1 = this.y1;
+ const gridcolor = this.gridcolor ?? this.linecolor;
+ const gridwidth = dimension(this.gridwidth, 0.8);
+ const subdiv = Math.max(0, Math.floor(Number(this.subgriddiv ?? 5)));
+ const subcolor = this.subgridcolor ?? 'gray';
+ const subwidth = dimension(this.subgridwidth, 0.4);
+ const rule = (a, b, c, d, color, width) => {
+ svg
+ .append('svg:line')
+ .attr('x1', a).attr('y1', b).attr('x2', c).attr('y2', d)
+ .style('stroke', color)
+ .style('stroke-width', width)
+ .style('stroke-opacity', 1);
+ };
+ /** Line offsets across a span, stepping by `step` from `origin`. */
+ const rungs = (lo, hi, origin, step) => {
+ if (!(step > 0) || !isFinite(step))
+ return [];
+ const out = [];
+ for (let v = origin; v <= hi + 1e-6; v += step)
+ out.push(v);
+ for (let v = origin - step; v >= lo - 1e-6; v -= step)
+ out.unshift(v);
+ return out;
+ };
+ const ox = this.originX ?? x0;
+ const oy = this.originY ?? y0;
+ // Subdivisions first, so the unit lines and labels sit over them.
+ if (subdiv > 1) {
+ for (const x of rungs(x0, x1, ox, this.xunit / subdiv))
+ rule(x, y0, x, y1, subcolor, subwidth);
+ for (const y of rungs(y0, y1, oy, this.yunit / subdiv))
+ rule(x0, y, x1, y, subcolor, subwidth);
+ }
+ const xs = rungs(x0, x1, ox, this.xunit);
+ const ys = rungs(y0, y1, oy, this.yunit);
+ for (const x of xs)
+ rule(x, y0, x, y1, gridcolor, gridwidth);
+ for (const y of ys)
+ rule(x0, y, x1, y, gridcolor, gridwidth);
+ // Grid numbers are drawn by default, as PSTricks draws them: `gridlabels`
+ // defaults to 10pt and only a zero or `none` turns them off. They were
+ // opt-in here on the grounds that PSTricks numbers an unbounded page while
+ // an SVG is clipped to the picture, so a grid flush with the edge would
+ // push them out of the viewport. That is true — the reference renders show
+ // PSTricks itself running off the page — but it is an argument for the
+ // clamping below, not for silently dropping a default the author expects.
+ const labels = this.gridlabels ?? 10;
+ if (labels === 'none' || Number(labels) === 0)
+ return;
+ const size = dimension(labels, 10);
+ const labelcolor = this.gridlabelcolor ?? 'black';
+ const text = (s, x, y, anchor) => {
+ svg
+ .append('svg:text')
+ .attr('x', x).attr('y', y)
+ .attr('text-anchor', anchor)
+ .attr('font-size', size)
+ .attr('font-family', 'serif')
+ .style('fill', labelcolor)
+ .text(s);
+ };
+ const round = (n) => (Math.abs(n) < 1e-9 ? 0 : Number(n.toFixed(4)));
+ const env = this.global || {};
+ // Clamped inside the picture so a grid flush with the edge still shows its
+ // numbers rather than pushing them out of the viewport.
+ const belowY = Math.min(y1 + size + 4, (env.h ?? 0) * (env.yunit ?? 1) - 2);
+ const leftX = Math.max(x0 - 4, size);
+ for (const x of xs)
+ text(String(round(x / env.xunit - env.w + env.x1)), x, belowY, 'middle');
+ for (const y of ys)
+ text(String(round(env.y1 - y / env.yunit)), leftX, y + size / 3, 'end');
+ },
+ psellipse(svg) {
+ svg
+ .append('svg:ellipse')
+ .attr('cx', this.cx)
+ .attr('cy', this.cy)
+ .attr('rx', this.rx)
+ .attr('ry', this.ry)
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this))
+ .style('stroke-width', this.linewidth)
+ .style('stroke-opacity', 1)
+ .style('fill', resolveFill(this, svg));
+ },
+ psbezier(svg) {
+ // The path stays open even when filled: PSTricks bounds the region with the
+ // chord back to the start but does not draw that chord, and SVG fills an
+ // open subpath as if closed while stroking only what was written. Closing
+ // it with Z would fill identically but paint a line along the chord.
+ const d = 'M ' + this.x1 + ' ' + this.y1 +
+ ' C ' + this.x2 + ' ' + this.y2 + ', ' + this.x3 + ' ' + this.y3 + ', ' + this.x4 + ' ' + this.y4;
+ svg
+ .append('svg:path')
+ .attr('d', d)
+ .style('stroke-width', this.linewidth)
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this))
+ .style('stroke-opacity', 1)
+ .style('fill', resolveFill(this, svg));
+ },
+ pscurve(svg) {
+ const d = buildCurvePath(this.data, this.endpoints ? 'endpoints' : this.closed ? 'closed' : 'open', this);
+ if (!d)
+ return;
+ svg
+ .append('svg:path')
+ .attr('d', d)
+ .style('stroke-width', this.linewidth)
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this))
+ .style('stroke-opacity', 1)
+ .style('fill', resolveFill(this, svg));
+ },
+ psecurve: curveRenderer,
+ psccurve: curveRenderer,
+ pswedge(svg) {
+ const { delta, large, sweep, full } = arcFlags(this.angleA, this.angleB);
+ const d = full || delta === 0
+ ? fullCirclePath(this.cx, this.cy, this.r)
+ : 'M ' + this.cx + ' ' + this.cy +
+ ' L ' + this.A.x + ' ' + this.A.y +
+ ' A ' + this.r + ' ' + this.r + ' 0 ' + large + ' ' + sweep +
+ ' ' + this.B.x + ' ' + this.B.y + ' Z';
+ svg
+ .append('svg:path')
+ .attr('d', d)
+ .style('stroke-width', this.linewidth)
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this))
+ .style('stroke-opacity', 1)
+ .style('fill', resolveFill(this, svg));
+ },
+ pscustom(svg) {
+ const filled = hasFill(this);
+ let d = '';
+ let started = false;
+ (this.commands || []).forEach((cmd) => {
+ const data = cmd.data;
+ if (!data)
+ return;
+ // The canonical path vocabulary. These describe the path directly rather
+ // than contributing a shape, so they come first.
+ if (cmd.key === 'moveto') {
+ d += ' M ' + data.x + ' ' + data.y;
+ started = true;
+ return;
+ }
+ if (cmd.key === 'lineto') {
+ if (!started) {
+ d += 'M ' + data.x + ' ' + data.y;
+ started = true;
+ return;
}
- });
- Object.entries(plots || {})
- .forEach(([k, plot]) => {
- if (k.match(/psplot/)) {
- plot.forEach((data) => {
- Object.entries(currentEnvironment || {})
- .forEach(([name, variable]) => {
- data.env.variables[name] = variable;
- });
- var d = data.fn.call(data.env, data.match);
- d.global = {};
- Object.assign(d.global, env);
- psgraph[k].call(d, svg);
- });
+ d += ' L ' + data.x + ' ' + data.y;
+ return;
+ }
+ if (cmd.key === 'curveto') {
+ if (!started) {
+ d += 'M ' + data.x1 + ' ' + data.y1;
+ started = true;
}
- if (k.match(/userline/)) {
- plot.forEach((data) => {
- var d = data.fn.call(data.env, data.match);
- data.env.x2 = coords[0];
- data.env.y2 = coords[1];
- data.data.x2 = data.env.x2;
- data.data.y2 = data.env.y2;
- if (data.data.xExp2) {
- data.data.x2 = d.userx2(coords);
- data.data.x1 = d.userx(coords);
- }
- else if (data.data.xExp) {
- data.data.x2 = d.userx(coords);
- }
- if (data.data.yExp2) {
- data.data.y2 = d.usery2(coords);
- data.data.y1 = d.usery(coords);
- }
- else if (data.data.yExp) {
- data.data.y2 = d.usery(coords);
- }
- d.global = {};
- Object.assign(d.global, env);
- Object.assign(d, data.data);
- psgraph[k].call(d, svg);
- });
+ d += ' C ' + data.x1 + ' ' + data.y1 + ', ' + data.x2 + ' ' + data.y2 +
+ ', ' + data.x + ' ' + data.y;
+ return;
+ }
+ if (cmd.key === 'closepath') {
+ if (started)
+ d += ' Z';
+ return;
+ }
+ if (cmd.key === 'psline' || cmd.key === 'userline' || cmd.key === 'psbezier') {
+ if (cmd.key === 'psbezier') {
+ if (!started) {
+ d += 'M ' + data.x1 + ' ' + data.y1;
+ started = true;
+ }
+ d += ' C ' + data.x2 + ' ' + data.y2 + ', ' + data.x3 + ' ' + data.y3 + ', ' + data.x4 + ' ' + data.y4;
+ return;
}
- });
- }
- // Enhanced cleanup and RPUT processing
- psgraph.processRputElements.call(this, el);
+ // Every point, not just the endpoint. A \psline inside a \pscustom
+ // continues the path through all of its coordinates; taking x2 alone
+ // dropped the intermediate ones — and, when the path was already open,
+ // dropped the line's own starting point as well, so a four-point
+ // zigzag came out as two segments through the wrong corners.
+ const pts = data.points && data.points.length >= 2
+ ? data.points
+ : [[data.x1, data.y1], [data.x2, data.y2]];
+ let from = 0;
+ if (!started) {
+ d += 'M ' + pts[0][0] + ' ' + pts[0][1];
+ started = true;
+ from = 1;
+ }
+ for (let i = from; i < pts.length; i++)
+ d += ' L ' + pts[i][0] + ' ' + pts[i][1];
+ }
+ else if (cmd.key === 'psframe') {
+ if (!started) {
+ d += 'M ' + data.x1 + ' ' + data.y1;
+ started = true;
+ }
+ d += ' L ' + data.x2 + ' ' + data.y1 +
+ ' L ' + data.x2 + ' ' + data.y2 +
+ ' L ' + data.x1 + ' ' + data.y2 + ' Z';
+ }
+ else if (cmd.key === 'pspolygon' || cmd.key === 'pscurve') {
+ const pts = data.data || [];
+ if (pts.length < 2)
+ return;
+ if (!started) {
+ d += 'M ' + pts[0] + ' ' + pts[1];
+ started = true;
+ }
+ for (let i = 2; i < pts.length; i += 2)
+ d += ' L ' + pts[i] + ' ' + pts[i + 1];
+ d += ' Z';
+ }
+ });
+ if (!started)
+ return;
+ if (filled)
+ d += ' Z';
+ svg
+ .append('svg:path')
+ .attr('d', d)
+ .style('stroke-width', this.linewidth)
+ // Spelled inline here rather than through the resolver, which is how this
+ // one site kept missing the sweeps that fixed the others.
+ .style('stroke', resolveStroke(this))
+ .style('stroke-dasharray', dashArray(this))
+ .style('stroke-linecap', dashCap(this))
+ .style('stroke-opacity', 1)
+ .style('fill', resolveFill(this, svg));
},
processRputElements(el) {
// Validate container
@@ -1632,7 +5395,7 @@ const psgraph = {
};
exports.default = psgraph;
-},{"@latex2js/utils":20}],18:[function(require,module,exports){
+},{"@latex2js/utils":25}],22:[function(require,module,exports){
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -1641,11 +5404,63 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.Functions = exports.Expressions = void 0;
const utils_1 = require("@latex2js/utils");
const settings_1 = __importDefault(require("@latex2js/settings"));
+/**
+ * Parse a PSTricks linewidth value: a bare number is used as-is (SVG px),
+ * a `pt` value is converted to px (1pt ≈ 1.333px).
+ */
+/**
+ * The unit a radius is measured in.
+ *
+ * PSTricks scales a radius by `runit` and a coordinate by `xunit`/`yunit`, so
+ * `\psset{xunit=2}` stretches where a circle sits without changing how big it
+ * is. Reading the radius through xunit made `\pscircle(0,0){1}` twice the size
+ * the reference draws it. `runit` shares the same default, so this only parts
+ * company from the old reading once a document sets the axes independently.
+ */
+function radiusUnit(ctx) {
+ const r = Number(ctx && ctx.runit);
+ if (isFinite(r) && r > 0)
+ return r;
+ return Number(ctx && ctx.xunit);
+}
+function parseLinewidth(value) {
+ const m = value.trim().match(/^([\d.]+)\s*(pt)?$/);
+ if (!m)
+ return 2;
+ return Number(m[1]) * (m[2] ? 1.333 : 1);
+}
+/**
+ * Device-space endpoints of an arc, measured from the arc's own centre.
+ *
+ * The radius is an offset from `(cx, cy)`, not from the picture origin, so the
+ * centre has to be added before the coordinate transform. Transforming
+ * `r*cos(theta)` alone places both endpoints as though every arc were centred
+ * on the origin — correct only for one that happens to be, which is why a pie
+ * at (0,0) looked right while the same wedge anywhere else collapsed to a
+ * spike reaching back to the origin.
+ *
+ * @param cx - centre x in picture units (empty or absent means 0)
+ * @param cy - centre y in picture units
+ * @param r - radius in picture units
+ * @param angleA - start angle in radians
+ * @param angleB - end angle in radians
+ * @returns the `A` and `B` endpoints in device coordinates
+ */
+function arcEndpoints(cx, cy, r, angleA, angleB) {
+ const ox = cx === undefined || cx === '' ? 0 : Number(cx);
+ const oy = cy === undefined || cy === '' ? 0 : Number(cy);
+ const radius = Number(r);
+ const at = (angle) => ({
+ x: utils_1.X.call(this, ox + radius * Math.cos(angle)),
+ y: utils_1.Y.call(this, oy + radius * Math.sin(angle))
+ });
+ return { A: at(angleA), B: at(angleB) };
+}
exports.Expressions = {
pspicture: /\\begin\{pspicture\}\(\s*(.*),(.*)\s*\)\(\s*(.*),(.*)\s*\)/,
- psframe: /\\psframe\(\s*(.*),(.*)\s*\)\(\s*(.*),(.*)\s*\)/,
- psplot: /\\psplot(\[[^\]]*\])?\{([^\}]*)\}\{([^\}]*)\}\{([^\}]*)\}/,
- psarc: new RegExp('\\\\psarc' +
+ psframe: /\\psframe\*?(\[[^\]]*\])?\(\s*(.*),(.*)\s*\)\(\s*(.*),(.*)\s*\)/,
+ psplot: /\\psplot\*?(\[[^\]]*\])?\{([^\}]*)\}\{([^\}]*)\}\{([^\}]*)\}/,
+ psarc: new RegExp('\\\\psarc\\*?' +
utils_1.RE.options +
utils_1.RE.type +
utils_1.RE.coords +
@@ -1653,8 +5468,8 @@ exports.Expressions = {
utils_1.RE.squiggle +
utils_1.RE.squiggle),
pscircle: /\\pscircle.*\(\s*(.*),(.*)\s*\)\{(.*)\}/,
- pspolygon: new RegExp('\\\\pspolygon' + utils_1.RE.options + '(.*)'),
- psaxes: new RegExp('\\\\psaxes' +
+ pspolygon: new RegExp('\\\\pspolygon\\*?' + utils_1.RE.options + '(.*)'),
+ psaxes: new RegExp('\\\\psaxes\\*?' +
utils_1.RE.options +
utils_1.RE.type +
utils_1.RE.coords +
@@ -1667,7 +5482,7 @@ exports.Expressions = {
utils_1.RE.squiggle +
utils_1.RE.squiggle +
utils_1.RE.squiggle),
- psline: new RegExp('\\\\psline' + utils_1.RE.options + utils_1.RE.type + utils_1.RE.coords + utils_1.RE.coordsOpt),
+ psline: new RegExp('\\\\psline\\*?' + utils_1.RE.options + utils_1.RE.type + utils_1.RE.coords + utils_1.RE.coordsOpt + '((?:\\s*\\([^)]*\\))*)'),
userline: new RegExp('\\\\userline' +
utils_1.RE.options +
utils_1.RE.type +
@@ -1678,8 +5493,28 @@ exports.Expressions = {
utils_1.RE.squiggleOpt +
utils_1.RE.squiggleOpt),
uservariable: new RegExp('\\\\uservariable' + utils_1.RE.options + utils_1.RE.squiggle + utils_1.RE.coords + utils_1.RE.squiggle),
- rput: /\\rput\((.*),(.*)\)\{(.*)\}/,
- psset: /\\psset\{(.*)\}/
+ // The coordinates cannot contain a paren or the separating comma. They were
+ // `(.*),(.*)`, which is greedy: on `\rput(1,-2){\pscircle(0,0){0.5}}` the x
+ // capture ran to the comma inside the nested shape, so the placement read
+ // its coordinates out of the contents.
+ rput: /\\rput\(\s*([^,()]*),([^()]*?)\s*\)\s*\{([\s\S]*)\}/,
+ psset: /\\psset\{(.*)\}/,
+ psdots: new RegExp('\\\\psdots' + utils_1.RE.options + '(.*)'),
+ psgrid: new RegExp('\\\\psgrid' + utils_1.RE.options + utils_1.RE.coordsOpt + utils_1.RE.coordsOpt + utils_1.RE.coordsOpt),
+ psellipse: /\\psellipse.*\(\s*(.*),(.*)\s*\)\(\s*(.*),(.*)\s*\)/,
+ psbezier: /\\psbezier\*?(\[[^\]]*\])?\((.*),(.*)\)\((.*),(.*)\)\((.*),(.*)\)\((.*),(.*)\)/,
+ pscurve: new RegExp('\\\\pscurve\\*?' + utils_1.RE.options + utils_1.RE.coords + '(.*)'),
+ psecurve: new RegExp('\\\\psecurve\\*?' + utils_1.RE.options + utils_1.RE.coords + '(.*)'),
+ psccurve: new RegExp('\\\\psccurve\\*?' + utils_1.RE.options + utils_1.RE.coords + '(.*)'),
+ pswedge: /\\pswedge\*?(\[[^\]]*\])?\(\s*(.*),(.*)\s*\)\{(.*)\}\{(.*)\}\{(.*)\}/,
+ pscustom: /\\pscustom\*?(\[[^\]]*\])?\{([\s\S]*)\}/,
+ // The canonical \pscustom path vocabulary. Only meaningful inside one, and
+ // inert elsewhere because psgraph has no renderer under these names.
+ moveto: /\\moveto\(\s*([^,)]*),([^)]*)\s*\)/,
+ lineto: /\\lineto\(\s*([^,)]*),([^)]*)\s*\)/,
+ closepath: /\\closepath/,
+ curveto: /\\curveto\(\s*([^,)]*),([^)]*)\s*\)\(\s*([^,)]*),([^)]*)\s*\)\(\s*([^,)]*),([^)]*)\s*\)/,
+ multido: /\\multido\{([^}]*)\}\{([^}]*)\}\{([\s\S]*)\}/
};
exports.Functions = {
slider(m) {
@@ -1716,19 +5551,38 @@ exports.Functions = {
},
psframe(m) {
var obj = {
- x1: utils_1.X.call(this, m[1]),
- y1: utils_1.Y.call(this, m[2]),
- x2: utils_1.X.call(this, m[3]),
- y2: utils_1.Y.call(this, m[4])
+ x1: utils_1.X.call(this, m[2]),
+ y1: utils_1.Y.call(this, m[3]),
+ x2: utils_1.X.call(this, m[4]),
+ y2: utils_1.Y.call(this, m[5]),
+ linecolor: 'black',
+ linestyle: 'solid',
+ fillstyle: 'none',
+ fillcolor: 'black',
+ linewidth: 2,
+ filled: /\\psframe\*/.test(m[0])
};
+ if (m[1])
+ Object.assign(obj, (0, utils_1.parseOptions)(m[1]));
return obj;
},
pscircle(m) {
var obj = {
cx: utils_1.X.call(this, m[1]),
cy: utils_1.Y.call(this, m[2]),
- r: this.xunit * m[3]
+ // A radius is a magnitude. PSTricks draws the same circle for a negative
+ // one; SVG rejects it outright, so the shape vanished with a console error.
+ r: Math.abs(radiusUnit(this) * Number(m[3])),
+ linecolor: 'black',
+ linestyle: 'solid',
+ fillstyle: 'none',
+ fillcolor: 'black',
+ linewidth: 2,
+ filled: /\\pscircle\*/.test(m[0])
};
+ var opts = m[0].match(/\[([^\]]*)\]/);
+ if (opts)
+ Object.assign(obj, (0, utils_1.parseOptions)(opts[1]));
return obj;
},
psaxes(m) {
@@ -1737,7 +5591,9 @@ exports.Functions = {
dy: 1 * this.yunit,
arrows: [0, 0],
dots: [0, 0],
- ticks: 'all'
+ ticks: 'all',
+ labels: 'all',
+ showorigin: true
};
if (m[1]) {
var options = (0, utils_1.parseOptions)(m[1]);
@@ -1747,11 +5603,37 @@ exports.Functions = {
if (options.Dy) {
obj.dy = Number(options.Dy) * this.yunit;
}
+ // `ticks` and `labels` select which axes get marks and numbers; both
+ // accept all / x / y / none. Dropping them meant ticks=none still drew
+ // ticks and labels could never be turned on.
+ if (options.ticks)
+ obj.ticks = options.ticks;
+ if (options.labels)
+ obj.labels = options.labels;
+ // arrowscale scales the axis arrowheads; it reaches the renderer as a
+ // string via parseOptions and is converted where the head is drawn.
+ if (options.arrowscale)
+ obj.arrowscale = options.arrowscale;
+ // showorigin=false suppresses the tick and number at the origin; the
+ // default is to draw them.
+ if (options.showorigin)
+ obj.showorigin = options.showorigin !== 'false';
}
// arrows?
var l = (0, utils_1.parseArrows)(m[2]);
obj.arrows = l.arrows;
obj.dots = l.dots;
+ // psaxes reads its options key by key rather than assigning them wholesale,
+ // so an `arrows=` option was dropped on the floor and an arrowed axis drew
+ // no head at all — and kept the tick and number the head should suppress.
+ if (m[1]) {
+ const opts = (0, utils_1.parseOptions)(m[1]);
+ if (opts.arrows) {
+ const fromOption = (0, utils_1.parseArrows)(opts.arrows);
+ obj.arrows = fromOption.arrows;
+ obj.dots = fromOption.dots;
+ }
+ }
// \psaxes*[par]{arrows}(x0,y0)(x1,y1)(x2,y2)
// m[1] [options]
// m[2] {<->}
@@ -1789,39 +5671,57 @@ exports.Functions = {
var endX = utils_1.evaluate.call(this, m[3]);
var data = [];
var x;
- // get env
- var expression = '';
- Object.entries(this.variables || {}).forEach(([name, val]) => {
- expression += 'var ' + name + ' = ' + val + ';';
- });
- const mathFunctions = 'var cos=Math.cos,sin=Math.sin,tan=Math.tan,atan=Math.atan,atan2=Math.atan2,exp=Math.exp,log=Math.log,sqrt=Math.sqrt,abs=Math.abs,floor=Math.floor,ceil=Math.ceil,round=Math.round,pow=Math.pow,PI=Math.PI,E=Math.E;';
- expression += mathFunctions + 'return ' + m[4] + ';';
- for (x = startX; x <= endX; x += 0.005) {
- data.push(utils_1.X.call(this, x));
- try {
- const evalFunc = new Function('x', expression);
- const yValue = evalFunc(x);
- if (yValue !== undefined && !isNaN(yValue)) {
- data.push(utils_1.Y.call(this, yValue));
- }
- else {
- data.push(utils_1.Y.call(this, 0));
- }
- }
- catch (err) {
- data.push(utils_1.Y.call(this, 0)); // fallback value
- }
- }
var obj = {
linecolor: 'black',
linestyle: 'solid',
fillstyle: 'none',
fillcolor: 'none',
- linewidth: 2
+ linewidth: 2,
+ // `plotstyle=dots` marks the samples rather than joining them; dotsize is
+ // the marker radius, matching psdots so a document using both agrees.
+ plotstyle: 'line',
+ dotsize: '2pt 2'
};
if (m[1])
Object.assign(obj, (0, utils_1.parseOptions)(m[1]));
+ // Sampling: honor `plotpoints=N` (number of samples); default to a
+ // fixed 0.005 step like the original implementation.
+ var step = 0.005;
+ var plotpoints = obj.plotpoints ? Number(obj.plotpoints) : 0;
+ if (plotpoints > 1) {
+ step = (endX - startX) / (plotpoints - 1);
+ }
+ else if (obj.plotpoints !== undefined && plotpoints < 2) {
+ // Fewer than two samples has no defined meaning — there is no interval
+ // left to step across — so the default sampling is used instead. Saying
+ // so beats accepting the option and quietly doing something else.
+ obj.plotpointsIgnored = plotpoints;
+ }
+ // Compile the plot expression once; evaluate per sample against a
+ // reused scope (compile-once / evaluate-many).
+ let compiled;
+ try {
+ compiled = (0, utils_1.parseExpression)(m[4]);
+ }
+ catch (err) {
+ console.warn('psplot: could not parse expression:', err.message);
+ obj.data = data;
+ return obj;
+ }
+ const scope = Object.assign({}, this.variables || {});
+ for (x = startX; x <= endX + step / 2; x += step) {
+ data.push(utils_1.X.call(this, x));
+ scope.x = x;
+ const yValue = compiled.evaluate(scope);
+ if (yValue !== undefined && !isNaN(yValue)) {
+ data.push(utils_1.Y.call(this, yValue));
+ }
+ else {
+ data.push(utils_1.Y.call(this, 0));
+ }
+ }
obj.data = data;
+ (0, utils_1.normalizeArrows)(obj);
return obj;
},
pspolygon(m) {
@@ -1845,6 +5745,7 @@ exports.Functions = {
fillstyle: 'none',
fillcolor: 'black',
linewidth: 2,
+ filled: /\\pspolygon\*/.test(m[0]),
data: data
};
if (m[1])
@@ -1858,11 +5759,15 @@ exports.Functions = {
var obj = {
linecolor: 'black',
linestyle: 'solid',
- fillstyle: 'solid',
+ // PSTricks leaves every shape unfilled unless a fillstyle is
+ // given or the starred form is used; an unstarred \psarc is an open
+ // curve, not a solid black wedge.
+ fillstyle: 'none',
fillcolor: 'black',
linewidth: 2,
arrows: arrows,
dots: dots,
+ filled: /\\psarc\*/.test(m[0]),
cx: utils_1.X.call(this, 0),
cy: utils_1.Y.call(this, 0)
};
@@ -1883,17 +5788,11 @@ exports.Functions = {
obj.cy = utils_1.Y.call(this, m[4]);
}
// choose x units over y, no reason...
- obj.r = Number(m[5]) * this.xunit;
+ obj.r = Math.abs(Number(m[5]) * radiusUnit(this));
obj.angleA = (Number(m[6]) * Math.PI) / 180;
obj.angleB = (Number(m[7]) * Math.PI) / 180;
- obj.A = {
- x: utils_1.X.call(this, Number(m[5]) * Math.cos(obj.angleA)),
- y: utils_1.Y.call(this, Number(m[5]) * Math.sin(obj.angleA))
- };
- obj.B = {
- x: utils_1.X.call(this, Number(m[5]) * Math.cos(obj.angleB)),
- y: utils_1.Y.call(this, Number(m[5]) * Math.sin(obj.angleB))
- };
+ Object.assign(obj, arcEndpoints.call(this, m[3], m[4], m[5], obj.angleA, obj.angleB));
+ (0, utils_1.normalizeArrows)(obj);
return obj;
},
psline(m) {
@@ -1909,7 +5808,8 @@ exports.Functions = {
fillcolor: 'black',
linewidth: 2,
arrows: arrows,
- dots: dots
+ dots: dots,
+ filled: /\\psline\*/.test(m[0])
};
if (m[5]) {
obj.x1 = utils_1.X.call(this, m[3]);
@@ -1923,13 +5823,23 @@ exports.Functions = {
obj.x2 = utils_1.X.call(this, m[3]);
obj.y2 = utils_1.Y.call(this, m[4]);
}
+ // \psline takes any number of points, not two. Everything past the second
+ // was dropped, so a polyline silently rendered as its first segment.
+ obj.points = [[obj.x1, obj.y1], [obj.x2, obj.y2]];
+ const extra = m[8];
+ if (extra) {
+ for (const pair of String(extra).matchAll(/\(\s*([^,()]*),([^,()]*)\s*\)/g)) {
+ obj.points.push([utils_1.X.call(this, pair[1]), utils_1.Y.call(this, pair[2])]);
+ }
+ }
if (options) {
Object.assign(obj, (0, utils_1.parseOptions)(options));
}
// TODO: add regex
if (typeof obj.linewidth === 'string') {
- obj.linewidth = 2;
+ obj.linewidth = parseLinewidth(obj.linewidth);
}
+ (0, utils_1.normalizeArrows)(obj);
return obj;
},
uservariable(m) {
@@ -1946,26 +5856,19 @@ exports.Functions = {
}
var nx1 = utils_1.Xinv.call(this, coords[0]);
var ny1 = utils_1.Yinv.call(this, coords[1]);
- var expx1 = 'var x = ' + nx1 + ';';
- var expy1 = 'var y = ' + ny1 + ';';
- // return X.call(this, eval(expy1 + expx1 + xExp));
var obj = {
name: m[2],
x: utils_1.X.call(this, m[3]),
y: utils_1.Y.call(this, m[4]),
func: m[5],
- value: (() => {
- try {
- const mathFunctions = 'var cos=Math.cos,sin=Math.sin,tan=Math.tan,atan=Math.atan,atan2=Math.atan2,exp=Math.exp,log=Math.log,sqrt=Math.sqrt,abs=Math.abs,floor=Math.floor,ceil=Math.ceil,round=Math.round,pow=Math.pow,PI=Math.PI,E=Math.E;';
- const evalFunc = new Function('', mathFunctions + expx1 + expy1 + 'return ' + m[5]);
- return evalFunc();
- }
- catch (err) {
- console.warn('Error evaluating uservariable expression:', err);
- return 0;
- }
- })()
+ value: 0
};
+ try {
+ obj.value = (0, utils_1.parseExpression)(m[5]).evaluate(Object.assign({ x: nx1, y: ny1 }, this.variables || {}));
+ }
+ catch (err) {
+ console.warn('Error evaluating uservariable expression:', err.message);
+ }
return obj;
},
userline(m) {
@@ -1975,41 +5878,40 @@ exports.Functions = {
var l = (0, utils_1.parseArrows)(lineType);
var arrows = l.arrows;
var dots = l.dots;
- var xExp = m[7];
- var yExp = m[8];
- const mathFunctions = 'var cos=Math.cos,sin=Math.sin,tan=Math.tan,atan=Math.atan,atan2=Math.atan2,exp=Math.exp,log=Math.log,sqrt=Math.sqrt,abs=Math.abs,floor=Math.floor,ceil=Math.ceil,round=Math.round,pow=Math.pow,PI=Math.PI,E=Math.E;';
- if (xExp)
- xExp = mathFunctions + xExp.replace(/^\{/, '').replace(/\}$/, '');
- if (yExp)
- yExp = mathFunctions + yExp.replace(/^\{/, '').replace(/\}$/, '');
- var xExp2 = m[9];
- var yExp2 = m[10];
- if (xExp2)
- xExp2 = mathFunctions + xExp2.replace(/^\{/, '').replace(/\}$/, '');
- if (yExp2)
- yExp2 = mathFunctions + yExp2.replace(/^\{/, '').replace(/\}$/, '');
- var expression = '';
- Object.entries(this.variables || {}).forEach(([name, val]) => {
- expression += 'var ' + name + ' = ' + val + ';';
- });
+ // Compile the interactive head/tail expressions once; each mousemove just
+ // re-evaluates them against a fresh {x, y} scope (compile-once).
+ const stripBraces = (s) => (s ? s.replace(/^\{/, '').replace(/\}$/, '').trim() : null);
+ const compileOpt = (src) => {
+ if (!src)
+ return null;
+ try {
+ return (0, utils_1.parseExpression)(src);
+ }
+ catch (err) {
+ console.warn('userline: could not parse expression:', err.message);
+ return null;
+ }
+ };
+ const xExp = compileOpt(stripBraces(m[7]));
+ const yExp = compileOpt(stripBraces(m[8]));
+ const xExp2 = compileOpt(stripBraces(m[9]));
+ const yExp2 = compileOpt(stripBraces(m[10]));
+ const variables = this.variables || {};
+ const evalAt = (compiled, x, y) => compiled.evaluate(Object.assign({ x: x, y: y }, variables));
var obj = {
x1: utils_1.X.call(this, m[3]),
y1: utils_1.Y.call(this, m[4]),
x2: utils_1.X.call(this, m[5]),
y2: utils_1.Y.call(this, m[6]),
- xExp: xExp,
- yExp: yExp,
- xExp2: xExp2,
- yExp2: yExp2,
+ xExp: m[7],
+ yExp: m[8],
+ xExp2: m[9],
+ yExp2: m[10],
userx: (coords) => {
var nx1 = utils_1.Xinv.call(this, coords[0]);
var ny1 = utils_1.Yinv.call(this, coords[1]);
- var expx1 = 'var x = ' + nx1 + ';';
- var expy1 = 'var y = ' + ny1 + ';';
try {
- const cleanExp = xExp ? xExp.replace(/^var cos=Math\.cos[^;]*;/, '') : '0';
- const evalFunc = new Function('', mathFunctions + expression + expy1 + expx1 + 'return (' + cleanExp + ')');
- return utils_1.X.call(this, evalFunc());
+ return utils_1.X.call(this, xExp ? evalAt(xExp, nx1, ny1) : 0);
}
catch (err) {
console.warn('Error evaluating userx expression:', err);
@@ -2019,12 +5921,8 @@ exports.Functions = {
usery: (coords) => {
var nx2 = utils_1.Xinv.call(this, coords[0]);
var ny2 = utils_1.Yinv.call(this, coords[1]);
- var expx2 = 'var x = ' + nx2 + ';';
- var expy2 = 'var y = ' + ny2 + ';';
try {
- const cleanExp = yExp ? yExp.replace(/^var cos=Math\.cos[^;]*;/, '') : '0';
- const evalFunc = new Function('', mathFunctions + expression + expy2 + expx2 + 'return (' + cleanExp + ')');
- return utils_1.Y.call(this, evalFunc());
+ return utils_1.Y.call(this, yExp ? evalAt(yExp, nx2, ny2) : 0);
}
catch (err) {
console.warn('Error evaluating usery expression:', err);
@@ -2034,12 +5932,8 @@ exports.Functions = {
userx2: (coords) => {
var nx3 = utils_1.Xinv.call(this, coords[0]);
var ny3 = utils_1.Yinv.call(this, coords[1]);
- var expx3 = 'var x = ' + nx3 + ';';
- var expy3 = 'var y = ' + ny3 + ';';
try {
- const cleanExp = xExp2 ? xExp2.replace(/^var cos=Math\.cos[^;]*;/, '') : '0';
- const evalFunc = new Function('', mathFunctions + expression + expy3 + expx3 + 'return (' + cleanExp + ')');
- return utils_1.X.call(this, evalFunc());
+ return utils_1.X.call(this, xExp2 ? evalAt(xExp2, nx3, ny3) : 0);
}
catch (err) {
console.warn('Error evaluating userx2 expression:', err);
@@ -2049,12 +5943,8 @@ exports.Functions = {
usery2: (coords) => {
var nx4 = utils_1.Xinv.call(this, coords[0]);
var ny4 = utils_1.Yinv.call(this, coords[1]);
- var expx4 = 'var x = ' + nx4 + ';';
- var expy4 = 'var y = ' + ny4 + ';';
try {
- const cleanExp = yExp2 ? yExp2.replace(/^var cos=Math\.cos[^;]*;/, '') : '0';
- const evalFunc = new Function('', mathFunctions + expression + expy4 + expx4 + 'return (' + cleanExp + ')');
- return utils_1.Y.call(this, evalFunc());
+ return utils_1.Y.call(this, yExp2 ? evalAt(yExp2, nx4, ny4) : 0);
}
catch (err) {
console.warn('Error evaluating usery2 expression:', err);
@@ -2074,8 +5964,9 @@ exports.Functions = {
}
// TODO: add regex
if (typeof obj.linewidth === 'string') {
- obj.linewidth = 2;
+ obj.linewidth = parseLinewidth(obj.linewidth);
}
+ (0, utils_1.normalizeArrows)(obj);
return obj;
},
rput(m) {
@@ -2085,33 +5976,263 @@ exports.Functions = {
text: m[3]
};
},
+ /**
+ * `\psset` declares defaults that every later command inherits.
+ *
+ * Every key is kept, not only the nine Settings knows about. Those nine need
+ * conversion — units become numbers, the dialect is canonicalized — and the
+ * rest are style defaults a shape reads in place of its own hardcoded one.
+ * Dropping them is why `\psset{linewidth=2pt,linestyle=dashed}` drew a thin
+ * solid line: the keys parsed, matched nothing, and were discarded.
+ */
psset(m) {
- const pairs = m[1].split(',').map((pair) => pair.split('='));
const obj = {};
- pairs.forEach((pair) => {
- const key = pair[0];
- const value = pair[1];
+ if (!m || !m[1])
+ return obj;
+ // parseOptions rather than a bare split, so a colour here resolves the
+ // same way it would inside a command's own brackets.
+ const declared = (0, utils_1.parseOptions)(m[1]);
+ Object.entries(declared).forEach(([key, value]) => {
+ let converted = false;
Object.keys(settings_1.default.Expressions).forEach((setting) => {
const exp = settings_1.default.Expressions[setting];
if (key.match(exp)) {
settings_1.default.Functions[setting](obj, value);
+ converted = true;
}
});
+ if (!converted)
+ obj[key] = value;
});
return obj;
+ },
+ psdots(m) {
+ var obj = {
+ linecolor: 'black',
+ dotstyle: 'dot',
+ // PSTricks reads `dotsize= `: the diameter is
+ // dim + factor x linewidth, so a thicker pen draws a bigger dot.
+ dotsize: '2pt 2',
+ linewidth: 0.8 * 1.333,
+ data: parseCoordList.call(this, m[2])
+ };
+ if (m[1])
+ Object.assign(obj, (0, utils_1.parseOptions)(m[1]));
+ return obj;
+ },
+ psgrid(m) {
+ var obj = {
+ linecolor: 'black',
+ linestyle: 'solid',
+ linewidth: 0.5,
+ // PSTricks grid defaults: a heavier line on the unit, five finer
+ // subdivisions between, and the coordinate numbered along two edges.
+ gridcolor: 'black',
+ gridwidth: '0.8pt',
+ subgriddiv: 5,
+ subgridcolor: 'gray',
+ subgridwidth: '0.4pt',
+ gridlabelcolor: 'black'
+ };
+ if (m[1])
+ Object.assign(obj, (0, utils_1.parseOptions)(m[1]));
+ // \psgrid[opts](x0,y0)(x1,y1) — defaults to the whole pspicture bounds.
+ // coordsOpt outer groups: m[2]/m[5]/m[8] = '(x,y)' strings, m[3],m[4] etc.
+ var has0 = m[3] !== undefined;
+ var has1 = m[6] !== undefined;
+ var x0 = has0 ? utils_1.X.call(this, m[3]) : utils_1.X.call(this, this.x0);
+ var y0 = has0 ? utils_1.Y.call(this, m[4]) : utils_1.Y.call(this, this.y0);
+ var x1 = has1 ? utils_1.X.call(this, m[6]) : utils_1.X.call(this, this.x1);
+ var y1 = has1 ? utils_1.Y.call(this, m[7]) : utils_1.Y.call(this, this.y1);
+ obj.x0 = Math.min(x0, x1);
+ obj.y0 = Math.min(y0, y1);
+ obj.x1 = Math.max(x0, x1);
+ obj.y1 = Math.max(y0, y1);
+ obj.xunit = this.xunit;
+ obj.yunit = this.yunit;
+ // The renderer numbers each line, which needs the picture coordinate the
+ // device position stands for.
+ obj.originX = utils_1.X.call(this, 0);
+ obj.originY = utils_1.Y.call(this, 0);
+ return obj;
+ },
+ psellipse(m) {
+ var obj = {
+ linecolor: 'black',
+ linestyle: 'solid',
+ fillstyle: 'none',
+ fillcolor: 'black',
+ linewidth: 2,
+ filled: /\\psellipse\*/.test(m[0])
+ };
+ var opts = m[0].match(/\[([^\]]*)\]/);
+ if (opts)
+ Object.assign(obj, (0, utils_1.parseOptions)(opts[1]));
+ obj.cx = utils_1.X.call(this, m[1]);
+ obj.cy = utils_1.Y.call(this, m[2]);
+ obj.rx = Math.abs(Number(m[3])) * this.xunit;
+ obj.ry = Math.abs(Number(m[4])) * this.yunit;
+ return obj;
+ },
+ psbezier(m) {
+ var obj = {
+ linecolor: 'black',
+ linestyle: 'solid',
+ fillstyle: 'none',
+ fillcolor: 'black',
+ linewidth: 2,
+ filled: /\\psbezier\*/.test(m[0])
+ };
+ if (m[1])
+ Object.assign(obj, (0, utils_1.parseOptions)(m[1]));
+ obj.x1 = utils_1.X.call(this, m[2]);
+ obj.y1 = utils_1.Y.call(this, m[3]);
+ obj.x2 = utils_1.X.call(this, m[4]);
+ obj.y2 = utils_1.Y.call(this, m[5]);
+ obj.x3 = utils_1.X.call(this, m[6]);
+ obj.y3 = utils_1.Y.call(this, m[7]);
+ obj.x4 = utils_1.X.call(this, m[8]);
+ obj.y4 = utils_1.Y.call(this, m[9]);
+ return obj;
+ },
+ pscurve(m) {
+ var obj = {
+ linecolor: 'black',
+ linestyle: 'solid',
+ fillstyle: 'none',
+ fillcolor: 'black',
+ linewidth: 2,
+ filled: /\\ps[ce]?curve\*/.test(m[0]),
+ // Only psccurve wraps. psecurve is an open curve whose first and last
+ // points are tangent controls rather than points it passes through.
+ closed: /\\psccurve/.test(m[0]),
+ endpoints: /\\psecurve/.test(m[0])
+ };
+ if (m[1])
+ Object.assign(obj, (0, utils_1.parseOptions)(m[1]));
+ // first point is captured separately (m[2], m[3]); the rest follow
+ obj.data = [utils_1.X.call(this, m[2]), utils_1.Y.call(this, m[3])].concat(parseCoordList.call(this, m[4] || ''));
+ return obj;
+ },
+ psecurve(m) {
+ return exports.Functions.pscurve.call(this, m);
+ },
+ psccurve(m) {
+ return exports.Functions.pscurve.call(this, m);
+ },
+ pswedge(m) {
+ var obj = {
+ linecolor: 'black',
+ linestyle: 'solid',
+ // PSTricks leaves every shape unfilled unless a fillstyle is
+ // given or the starred form is used; an unstarred \psarc is an open
+ // curve, not a solid black wedge.
+ fillstyle: 'none',
+ fillcolor: 'black',
+ linewidth: 2,
+ filled: /\\pswedge\*/.test(m[0])
+ };
+ if (m[1])
+ Object.assign(obj, (0, utils_1.parseOptions)(m[1]));
+ obj.cx = utils_1.X.call(this, m[2]);
+ obj.cy = utils_1.Y.call(this, m[3]);
+ obj.r = Math.abs(Number(m[4]) * radiusUnit(this));
+ obj.angleA = (Number(m[5]) * Math.PI) / 180;
+ obj.angleB = (Number(m[6]) * Math.PI) / 180;
+ Object.assign(obj, arcEndpoints.call(this, m[2], m[3], m[4], obj.angleA, obj.angleB));
+ return obj;
+ },
+ pscustom(m) {
+ var obj = {
+ linecolor: 'black',
+ linestyle: 'solid',
+ fillstyle: 'none',
+ fillcolor: 'black',
+ linewidth: 2,
+ filled: /\\pscustom\*/.test(m[0]),
+ body: m[2]
+ };
+ if (m[1])
+ Object.assign(obj, (0, utils_1.parseOptions)(m[1]));
+ return obj;
+ },
+ /** `\moveto(x,y)` — starts a new subpath inside \pscustom. */
+ moveto(m) {
+ return { x: utils_1.X.call(this, m[1]), y: utils_1.Y.call(this, m[2]) };
+ },
+ /** `\lineto(x,y)` — a straight segment inside \pscustom. */
+ lineto(m) {
+ return { x: utils_1.X.call(this, m[1]), y: utils_1.Y.call(this, m[2]) };
+ },
+ /** `\closepath` — closes the current subpath. */
+ closepath() {
+ return { close: true };
+ },
+ /** `\curveto(c1)(c2)(end)` — a cubic segment inside \pscustom. */
+ curveto(m) {
+ return {
+ x1: utils_1.X.call(this, m[1]), y1: utils_1.Y.call(this, m[2]),
+ x2: utils_1.X.call(this, m[3]), y2: utils_1.Y.call(this, m[4]),
+ x: utils_1.X.call(this, m[5]), y: utils_1.Y.call(this, m[6])
+ };
+ },
+ multido(m) {
+ var spec = m[1] || '';
+ var varMatch = spec.match(/\\([a-zA-Z@]+)\s*=\s*([\d.+-]+)\s*\+\s*([\d.+-]+)/);
+ return {
+ variable: varMatch ? varMatch[1] : null,
+ start: varMatch ? Number(varMatch[2]) : 0,
+ step: varMatch ? Number(varMatch[3]) : 1,
+ count: Number(m[2]),
+ body: m[3]
+ };
}
};
+/**
+ * Parse a coordinate list like `(0,0)(1,1)(2,2)` into a flat
+ * [x0,y0,x1,y1,...] pixel array.
+ */
+function parseCoordList(coords) {
+ var data = [];
+ var re = new RegExp(utils_1.RE.coords, 'g');
+ var m;
+ while ((m = re.exec(coords)) !== null) {
+ data.push(utils_1.X.call(this, m[1]));
+ data.push(utils_1.Y.call(this, m[2]));
+ }
+ return data;
+}
exports.default = {
Expressions: exports.Expressions,
Functions: exports.Functions
};
-},{"@latex2js/settings":19,"@latex2js/utils":20}],19:[function(require,module,exports){
+},{"@latex2js/settings":23,"@latex2js/utils":25}],23:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Functions = exports.Expressions = void 0;
+exports.normalizeDialect = normalizeDialect;
const utils_1 = require("@latex2js/utils");
+/**
+ * Canonicalizes a declared dialect name.
+ *
+ * `mathapedia` is accepted as an alias for `latex2js`: the dialect belongs to
+ * the renderer, while Mathapedia is a product built on it, so one canonical
+ * name keeps comparisons simple without making authors learn which to write.
+ *
+ * @param value - the declared name, in any case
+ * @returns the canonical dialect, or null when the name is not one we know
+ */
+function normalizeDialect(value) {
+ const v = String(value ?? '').trim().toLowerCase();
+ if (v === 'pstricks')
+ return 'pstricks';
+ if (v === 'latex2js' || v === 'mathapedia')
+ return 'latex2js';
+ return null;
+}
exports.Expressions = {
+ dialect: /^dialect$/,
fillcolor: /^fillcolor$/,
fillstyle: /^fillstyle$/,
linecolor: /^linecolor$/,
@@ -2122,6 +6243,17 @@ exports.Expressions = {
yunit: /^yunit/
};
exports.Functions = {
+ /**
+ * Which language the document is written in.
+ *
+ * `pstricks` is the specification; `latex2js` (alias `mathapedia`) is this
+ * project's superset — the interactive macros, infix plot bodies, natural-log
+ * `log`, starred shapes honouring `fillcolor`. Declaring it is what makes the
+ * extensions visible instead of indistinguishable from a bug.
+ */
+ dialect(o, v) {
+ o.dialect = normalizeDialect(v);
+ },
fillcolor(o, v) {
o.fillcolor = v;
},
@@ -2159,22 +6291,355 @@ exports.default = {
Functions: exports.Functions
};
-},{"@latex2js/utils":20}],20:[function(require,module,exports){
+},{"@latex2js/utils":25}],24:[function(require,module,exports){
+"use strict";
+/**
+ * Algebraic expression parser + evaluator for PSTricks-style math.
+ *
+ * PSTricks `algebraic` expressions are NOT JavaScript: they use `^` for
+ * power, allow implicit multiplication (`2x`, `2(x+1)`, `2sin(x)`), and rely
+ * on bare math function names (`cos(x)`). This module parses an expression
+ * once into an AST and compiles it to a JavaScript closure that can be
+ * evaluated cheaply many times with a variable scope — exactly the
+ * compile-once / evaluate-many pattern the interactive plot and userline
+ * paths need.
+ *
+ * Supported syntax:
+ * numbers, identifiers (variables), arithmetic + - * / ^,
+ * unary minus/plus, implicit multiplication, parentheses,
+ * function calls (cos, sin, tan, atan, atan2, pow, sqrt, abs, exp, ln,
+ * log, floor, ceil, round, min, max, ...), comparisons (< > <= >= == !=),
+ * and ternary conditionals (cond ? a : b).
+ */
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.MATH_CONSTANTS = exports.MATH_FUNCTIONS = exports.ExpressionError = void 0;
+exports.parseExpression = parseExpression;
+class ExpressionError extends Error {
+ constructor(message, position) {
+ // position is a 0-based offset; compute 1-based line/column lazily
+ super(message);
+ this.name = 'ExpressionError';
+ this.position = position;
+ this.line = 0;
+ this.column = 0;
+ }
+}
+exports.ExpressionError = ExpressionError;
+const OPS = ['<=', '>=', '==', '!=', '<', '>', '?', ':', '+', '-', '*', '/', '^', ','];
+const PARENS = new Set(['(', ')']);
+function tokenize(source) {
+ const tokens = [];
+ let i = 0;
+ const n = source.length;
+ const numberRe = /^\d*\.?\d+(?:[eE][+-]?\d+)?/;
+ const identRe = /^[a-zA-Z_][a-zA-Z0-9_]*/;
+ while (i < n) {
+ const ch = source[i];
+ if (/\s/.test(ch)) {
+ i++;
+ continue;
+ }
+ // Unicode pi
+ if (ch === 'π') {
+ tokens.push({ type: 'ident', value: 'π', pos: i });
+ i++;
+ continue;
+ }
+ if (ch === '(' || ch === ')') {
+ tokens.push({ type: 'paren', value: ch, pos: i });
+ i++;
+ continue;
+ }
+ const num = source.slice(i).match(numberRe);
+ if (num) {
+ tokens.push({ type: 'number', value: num[0], pos: i });
+ i += num[0].length;
+ continue;
+ }
+ const ident = source.slice(i).match(identRe);
+ if (ident) {
+ tokens.push({ type: 'ident', value: ident[0], pos: i });
+ i += ident[0].length;
+ continue;
+ }
+ const op = OPS.find((o) => source.startsWith(o, i));
+ if (op) {
+ tokens.push({ type: op === '(' || op === ')' ? 'paren' : 'op', value: op, pos: i });
+ i += op.length;
+ continue;
+ }
+ throw new ExpressionError(`unexpected character '${ch}'`, i);
+ }
+ tokens.push({ type: 'eof', value: '', pos: n });
+ return tokens;
+}
+class Parser {
+ constructor(source) {
+ this.source = source;
+ this.index = 0;
+ this.tokens = tokenize(source);
+ if (this.tokens.length <= 1) {
+ throw new ExpressionError('empty expression', 0);
+ }
+ }
+ peek() {
+ return this.tokens[this.index];
+ }
+ next() {
+ return this.tokens[this.index++];
+ }
+ expect(value) {
+ const t = this.peek();
+ if (t.value !== value) {
+ throw new ExpressionError(`expected '${value}' but found '${t.value || 'end of input'}'`, t.pos);
+ }
+ return this.next();
+ }
+ parse() {
+ const node = this.parseTernary();
+ const t = this.peek();
+ if (t.type !== 'eof') {
+ throw new ExpressionError(`unexpected '${t.value}'`, t.pos);
+ }
+ return node;
+ }
+ parseTernary() {
+ const cond = this.parseComparison();
+ if (this.peek().value === '?') {
+ this.next();
+ const then = this.parseTernary();
+ this.expect(':');
+ const els = this.parseTernary();
+ return { type: 'ternary', cond, then, els };
+ }
+ return cond;
+ }
+ parseComparison() {
+ let left = this.parseAdditive();
+ for (;;) {
+ const op = this.peek().value;
+ if (op === '<' || op === '>' || op === '<=' || op === '>=' || op === '==' || op === '!=') {
+ this.next();
+ const right = this.parseAdditive();
+ left = { type: 'binary', op, left, right };
+ }
+ else {
+ return left;
+ }
+ }
+ }
+ parseAdditive() {
+ let left = this.parseMultiplicative();
+ for (;;) {
+ const op = this.peek().value;
+ if (op === '+' || op === '-') {
+ this.next();
+ const right = this.parseMultiplicative();
+ left = { type: 'binary', op, left, right };
+ }
+ else {
+ return left;
+ }
+ }
+ }
+ parseMultiplicative() {
+ let left = this.parseUnary();
+ for (;;) {
+ const op = this.peek().value;
+ if (op === '*' || op === '/') {
+ this.next();
+ const right = this.parseUnary();
+ left = { type: 'binary', op, left, right };
+ }
+ else if (this.isImplicitStart(this.peek())) {
+ // implicit multiplication: 2x, 2(x+1), (x+1)(x+2), 2sin(x)
+ const right = this.parseUnary();
+ left = { type: 'binary', op: '*', left, right };
+ }
+ else {
+ return left;
+ }
+ }
+ }
+ parseUnary() {
+ const op = this.peek().value;
+ if (op === '-' || op === '+') {
+ this.next();
+ return { type: 'unary', op, operand: this.parseUnary() };
+ }
+ return this.parsePower();
+ }
+ parsePower() {
+ const left = this.parsePrimary();
+ if (this.peek().value === '^') {
+ this.next();
+ const right = this.parseUnary(); // right-associative, binds tighter on the right
+ return { type: 'binary', op: '^', left, right };
+ }
+ return left;
+ }
+ parsePrimary() {
+ const t = this.peek();
+ if (t.type === 'number') {
+ this.next();
+ return { type: 'number', value: t.value };
+ }
+ if (t.type === 'ident') {
+ this.next();
+ // a known math function followed by '(' is a function call
+ if (this.peek().value === '(' && exports.MATH_FUNCTIONS.hasOwnProperty(t.value)) {
+ this.next(); // consume '('
+ const args = [];
+ if (this.peek().value !== ')') {
+ args.push(this.parseTernary());
+ while (this.peek().value === ',') {
+ this.next();
+ args.push(this.parseTernary());
+ }
+ }
+ this.expect(')');
+ return { type: 'call', name: t.value, args };
+ }
+ return { type: 'var', name: t.value };
+ }
+ if (t.value === '(') {
+ this.next();
+ const node = this.parseTernary();
+ this.expect(')');
+ return node;
+ }
+ throw new ExpressionError(`unexpected '${t.value || 'end of input'}' in expression`, t.pos);
+ }
+ /** A token that can start an implicit multiplication operand. */
+ isImplicitStart(t) {
+ return t.type === 'number' || t.type === 'ident' || t.value === '(';
+ }
+}
+// ---------------------------------------------------------------------------
+// Compile AST → JS closure
+// ---------------------------------------------------------------------------
+exports.MATH_FUNCTIONS = {
+ cos: 'Math.cos',
+ sin: 'Math.sin',
+ tan: 'Math.tan',
+ atan: 'Math.atan',
+ atan2: 'Math.atan2',
+ asin: 'Math.asin',
+ acos: 'Math.acos',
+ exp: 'Math.exp',
+ ln: 'Math.log',
+ log: 'Math.log',
+ log10: 'Math.log10',
+ sqrt: 'Math.sqrt',
+ cbrt: 'Math.cbrt',
+ abs: 'Math.abs',
+ sign: 'Math.sign',
+ floor: 'Math.floor',
+ ceil: 'Math.ceil',
+ round: 'Math.round',
+ pow: 'Math.pow',
+ min: 'Math.min',
+ max: 'Math.max',
+ sinh: 'Math.sinh',
+ cosh: 'Math.cosh',
+ tanh: 'Math.tanh',
+};
+exports.MATH_CONSTANTS = {
+ pi: 'Math.PI',
+ π: 'Math.PI',
+ PI: 'Math.PI',
+ // pst-plot's own spelling, used by \psplot expressions.
+ Pi: 'Math.PI',
+ E: 'Math.E',
+};
+function compileNode(node, variableNames) {
+ switch (node.type) {
+ case 'number':
+ return node.value;
+ case 'var': {
+ if (exports.MATH_CONSTANTS.hasOwnProperty(node.name)) {
+ return exports.MATH_CONSTANTS[node.name];
+ }
+ variableNames.add(node.name);
+ return 'v.' + node.name;
+ }
+ case 'call': {
+ const target = exports.MATH_FUNCTIONS.hasOwnProperty(node.name)
+ ? exports.MATH_FUNCTIONS[node.name]
+ : '(v.' + node.name + ')';
+ return target + '(' + node.args.map((a) => compileNode(a, variableNames)).join(',') + ')';
+ }
+ case 'unary':
+ return '(' + node.op + compileNode(node.operand, variableNames) + ')';
+ case 'binary': {
+ const op = node.op === '^' ? '**' : node.op;
+ return '(' + compileNode(node.left, variableNames) + op + compileNode(node.right, variableNames) + ')';
+ }
+ case 'ternary':
+ return ('(' +
+ compileNode(node.cond, variableNames) +
+ '?' +
+ compileNode(node.then, variableNames) +
+ ':' +
+ compileNode(node.els, variableNames) +
+ ')');
+ default:
+ throw new Error('unknown node type ' + node.type);
+ }
+}
+/**
+ * Parse an algebraic expression and compile it to an evaluable closure.
+ * Throws ExpressionError with a character position on invalid syntax.
+ */
+function parseExpression(source) {
+ const trimmed = source.trim();
+ if (!trimmed) {
+ throw new ExpressionError('empty expression', 0);
+ }
+ const parser = new Parser(trimmed);
+ const ast = parser.parse();
+ const variableNames = new Set();
+ const js = compileNode(ast, variableNames);
+ let fn;
+ try {
+ // eslint-disable-next-line no-new-func
+ fn = new Function('v', 'return (' + js + ');');
+ }
+ catch (err) {
+ throw new ExpressionError('could not compile expression: ' + err.message, 0);
+ }
+ return {
+ source: trimmed,
+ toJS: () => js,
+ variables: () => Array.from(variableNames),
+ evaluate: (scope) => fn(scope || {}),
+ };
+}
+
+},{}],25:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
-exports.select = exports.SVGSelection = exports.dotType = exports.arrowType = exports.Yinv = exports.Y = exports.Xinv = exports.X = exports.evaluate = exports.parseArrows = exports.parseOptions = exports.RE = exports.convertUnits = exports.matchrepl = exports.simplerepl = void 0;
+exports.MATH_CONSTANTS = exports.MATH_FUNCTIONS = exports.ExpressionError = exports.parseExpression = exports.select = exports.SVGSelection = exports.dotType = exports.arrowType = exports.Yinv = exports.Y = exports.Xinv = exports.X = exports.evaluate = exports.normalizeArrows = exports.parseArrows = exports.parseOptions = exports.resolveColor = exports.defineColor = exports.resetDefinedColors = exports.RE = exports.convertUnits = exports.matchrepl = exports.simplerepl = void 0;
+const expression_1 = require("./expression");
const simplerepl = function (regex, replace) {
return function (_m, contents) {
return contents.replace(regex, replace);
};
};
exports.simplerepl = simplerepl;
+/**
+ * Builds a text transform that rewrites each match through `callback`.
+ *
+ * The callback is invoked with the same receiver the transform was called with,
+ * so a transform that needs document state — section numbering, say — can reach
+ * it. Callbacks that do not care simply ignore `this`.
+ */
const matchrepl = function (regex, callback) {
return function (m, contents) {
if (Array.isArray(m)) {
m.forEach((match) => {
var m2 = match.match(regex);
- contents = contents.replace(m2.input, callback(m2));
+ contents = contents.replace(m2.input, callback.call(this, m2));
});
}
return contents;
@@ -2209,6 +6674,142 @@ exports.RE = {
coordsOpt: '(\\(\\s*([^\\)]*),([^\\)]*)\\s*\\))?',
coords: '\\(\\s*([^\\)]*),([^\\)]*)\\s*\\)'
};
+/** Option keys whose value names a colour. */
+const COLOR_KEYS = ['linecolor', 'fillcolor', 'hatchcolor', 'gridcolor', 'bordercolor', 'shadowcolor', 'labelcolor'];
+/**
+ * xcolor's base colour set, as RGB triples, transcribed from the
+ * `\definecolorset` blocks in xcolor.sty.
+ *
+ * Nine of these are not what CSS means by the same word, and the divergence is
+ * not subtle: xcolor's `green` is pure (0,1,0) while CSS `green` is the much
+ * darker #008000, and `purple`, `violet`, `lime`, `orange` and `brown` all
+ * name different colours in the two vocabularies. Half of this table used to
+ * hold the CSS values under the xcolor names, so a tint mixed toward the wrong
+ * colour, and a plain name was handed to the browser and read as CSS.
+ */
+const BASE_COLORS = {
+ red: [255, 0, 0], green: [0, 255, 0], blue: [0, 0, 255],
+ cyan: [0, 255, 255], magenta: [255, 0, 255], yellow: [255, 255, 0],
+ black: [0, 0, 0], white: [255, 255, 255],
+ gray: [128, 128, 128], grey: [128, 128, 128],
+ darkgray: [64, 64, 64], lightgray: [191, 191, 191],
+ brown: [191, 128, 64], lime: [191, 255, 0], orange: [255, 128, 0],
+ pink: [255, 191, 191], purple: [191, 0, 64], teal: [0, 128, 128],
+ violet: [128, 0, 128], olive: [128, 128, 0],
+};
+/**
+ * Colours the document defined for itself with `\definecolor`.
+ *
+ * Kept apart from the xcolor base set so a document can shadow a built-in name
+ * — which is how a page written against browser colours can keep the exact
+ * shade it wants while staying valid LaTeX, instead of relying on a name
+ * xcolor never defined.
+ */
+const DEFINED_COLORS = {};
+/** Clears the document-defined colours. Called once per parse. */
+const resetDefinedColors = function () {
+ for (const name of Object.keys(DEFINED_COLORS))
+ delete DEFINED_COLORS[name];
+};
+exports.resetDefinedColors = resetDefinedColors;
+const clamp255 = (n) => Math.max(0, Math.min(255, Math.round(n)));
+/**
+ * Records a `\definecolor{name}{model}{spec}`.
+ *
+ * The models are xcolor's: `rgb` and `cmyk` take fractions, `RGB` takes
+ * 0-255, `gray` a single fraction, and `HTML` six hex digits.
+ *
+ * @param name - the colour's name
+ * @param model - the colour model the spec is written in
+ * @param spec - the model's components, comma separated
+ * @returns true when the definition was understood
+ */
+const defineColor = function (name, model, spec) {
+ const key = String(name ?? '').trim().toLowerCase();
+ if (!key)
+ return false;
+ const parts = String(spec ?? '').split(',').map((p) => Number(p.trim()));
+ const m = String(model ?? '').trim();
+ if (m === 'rgb' && parts.length >= 3 && parts.every(isFinite)) {
+ DEFINED_COLORS[key] = [clamp255(parts[0] * 255), clamp255(parts[1] * 255), clamp255(parts[2] * 255)];
+ return true;
+ }
+ if (m === 'RGB' && parts.length >= 3 && parts.every(isFinite)) {
+ DEFINED_COLORS[key] = [clamp255(parts[0]), clamp255(parts[1]), clamp255(parts[2])];
+ return true;
+ }
+ if (m === 'gray' && parts.length >= 1 && isFinite(parts[0])) {
+ const g = clamp255(parts[0] * 255);
+ DEFINED_COLORS[key] = [g, g, g];
+ return true;
+ }
+ if (m === 'cmyk' && parts.length >= 4 && parts.every(isFinite)) {
+ const [c, y2, y3, k] = parts;
+ DEFINED_COLORS[key] = [
+ clamp255(255 * (1 - Math.min(1, c + k))),
+ clamp255(255 * (1 - Math.min(1, y2 + k))),
+ clamp255(255 * (1 - Math.min(1, y3 + k))),
+ ];
+ return true;
+ }
+ if (m === 'HTML') {
+ const hex = String(spec ?? '').trim().replace(/^#/, '');
+ if (/^[0-9a-fA-F]{6}$/.test(hex)) {
+ DEFINED_COLORS[key] = [
+ parseInt(hex.slice(0, 2), 16),
+ parseInt(hex.slice(2, 4), 16),
+ parseInt(hex.slice(4, 6), 16),
+ ];
+ return true;
+ }
+ }
+ return false;
+};
+exports.defineColor = defineColor;
+/**
+ * Resolves an xcolor tint expression to a CSS colour.
+ *
+ * `gray!40` means forty percent gray against white, and `gray!40!red` mixes
+ * against red instead. A browser cannot read either, and an unparsable fill
+ * silently falls back to black — which is how a light grey plane rendered as
+ * a solid black one.
+ *
+ * @param value - a colour name, optionally with `!` mix terms
+ * @returns a CSS colour; names without a mix term are returned untouched
+ */
+const resolveColor = function (value) {
+ const parts = String(value).split('!').map((p) => p.trim());
+ // A document's own \definecolor wins over the built-in of the same name,
+ // as it does in xcolor.
+ const rgb = (name) => DEFINED_COLORS[name.toLowerCase()] ?? BASE_COLORS[name.toLowerCase()] ?? null;
+ // A plain name resolves too. Nine of xcolor's base colours name a different
+ // colour in CSS, so handing `green` straight to the browser drew the dark
+ // #008000 where the document asks for pure green.
+ if (parts.length < 2) {
+ const plain = rgb(parts[0]);
+ return plain ? 'rgb(' + plain[0] + ',' + plain[1] + ',' + plain[2] + ')' : value;
+ }
+ let current = rgb(parts[0]);
+ if (!current)
+ return value;
+ for (let i = 1; i < parts.length; i += 2) {
+ const pct = Number(parts[i]);
+ if (!isFinite(pct))
+ return value;
+ // An omitted second operand mixes against white, as xcolor does.
+ const against = parts[i + 1] ? rgb(parts[i + 1]) : [255, 255, 255];
+ if (!against)
+ return value;
+ const w = Math.max(0, Math.min(100, pct)) / 100;
+ current = [
+ Math.round(current[0] * w + against[0] * (1 - w)),
+ Math.round(current[1] * w + against[1] * (1 - w)),
+ Math.round(current[2] * w + against[2] * (1 - w)),
+ ];
+ }
+ return 'rgb(' + current[0] + ',' + current[1] + ',' + current[2] + ')';
+};
+exports.resolveColor = resolveColor;
// OPTIONS
// converts [showorigin=false,labels=none, Dx=3.14] to {showorigin: 'false', labels: 'none', Dx: '3.14'}
const parseOptions = function (opts) {
@@ -2218,7 +6819,9 @@ const parseOptions = function (opts) {
all.forEach((option) => {
var kv = option.split('=');
if (kv.length == 2) {
- obj[kv[0].trim()] = kv[1].trim();
+ const key = kv[0].trim();
+ const value = kv[1].trim();
+ obj[key] = COLOR_KEYS.indexOf(key) === -1 ? value : (0, exports.resolveColor)(value);
}
});
return obj;
@@ -2229,7 +6832,10 @@ const parseArrows = function (m) {
var arrows = [0, 0];
var dots = [0, 0];
if (lineType) {
- var type = lineType.match(/\{([^\-]*)?\-([^\-]*)?\}/);
+ // The braces are optional. PSTricks accepts the same specification as an
+ // option — `[arrows=->]` — and requiring `{->}` meant the option form
+ // matched nothing here, so it was left on the shape as a bare string.
+ var type = lineType.match(/\{?([^\-{}]*)\-([^\-{}]*)\}?/);
if (type) {
if (type[1]) {
// check starting point
@@ -2257,6 +6863,30 @@ const parseArrows = function (m) {
};
};
exports.parseArrows = parseArrows;
+/**
+ * Turns an `arrows` option back into the pair of flags a renderer reads.
+ *
+ * A shape's arrow specification arrives two ways: as the `{->}` group in its
+ * own syntax, which its parse function reads, and as an `arrows=->` option,
+ * which `parseOptions` hands over as a plain string. The string then
+ * overwrote the parsed pair — and since `'->'[0]` is `'-'`, which is truthy,
+ * every renderer testing `arrows[0]` drew a head at BOTH ends regardless of
+ * the direction asked for, while `*-*` drew two arrowheads where PSTricks
+ * draws two discs.
+ *
+ * @param obj - a parsed command, normalized in place
+ */
+const normalizeArrows = function (obj) {
+ if (!obj || typeof obj.arrows !== 'string')
+ return;
+ const parsed = (0, exports.parseArrows)(obj.arrows);
+ obj.arrows = parsed.arrows;
+ // Only take the dots when this specification actually names any, so an
+ // `arrows=` option cannot clear dots the `{*-*}` form already set.
+ if (parsed.dots[0] || parsed.dots[1])
+ obj.dots = parsed.dots;
+};
+exports.normalizeArrows = normalizeArrows;
// export const evaluate = function (this: any, exp: string) {
// var num = Number(exp);
// if (isNaN(num)) {
@@ -2277,53 +6907,52 @@ const evaluate = function (exp) {
if (!isNaN(num))
return num;
this.variables = this.variables || {};
- const mathKeys = Object.keys(Math);
- const varKeys = Object.keys(this.variables);
- const allKeys = [...mathKeys, ...varKeys];
- const allValues = [
- ...mathKeys.map(k => Math[k]),
- ...varKeys.map(k => this.variables[k])
- ];
try {
- // @ts-ignore
- const fn = new Function(...allKeys, `return (${exp});`);
- return fn(...allValues);
+ return getCompiled(exp).evaluate(this.variables);
}
catch (e) {
- console.warn('Evaluation error:', e);
+ console.warn('Evaluation error:', e.message);
return NaN;
}
};
exports.evaluate = evaluate;
+// Small bounded cache so repeated identical expressions (e.g. plot bounds,
+// slider-driven re-evaluation) skip re-parsing entirely.
+const expressionCache = new Map();
+const EXPRESSION_CACHE_MAX = 500;
+function getCompiled(exp) {
+ let compiled = expressionCache.get(exp);
+ if (!compiled) {
+ compiled = (0, expression_1.parseExpression)(exp);
+ if (expressionCache.size >= EXPRESSION_CACHE_MAX) {
+ expressionCache.clear();
+ }
+ expressionCache.set(exp, compiled);
+ }
+ return compiled;
+}
+/**
+ * Picture x to device x.
+ *
+ * Returns NaN for input it cannot transform. Returning 0 instead — as this did
+ * — invents a real coordinate at the origin, so a command with one bad value
+ * drew a plausible shape in the wrong place rather than failing. Callers detect
+ * the non-finite result and skip the element; see `pspicture` in dsh-psgraph.
+ *
+ * @param v - the coordinate in picture units
+ * @returns the device coordinate, or NaN when it cannot be computed
+ */
const X = function (v) {
- // Enhanced validation for coordinate transformation
const numV = typeof v === 'string' ? parseFloat(v) : v;
- if (isNaN(numV)) {
- console.warn('X function: Invalid input value', { input: v, parsed: numV });
- return 0;
- }
- if (isNaN(this.w) || isNaN(this.x1) || isNaN(this.xunit)) {
- console.warn('X function: NaN detected in context properties', { w: this.w, x1: this.x1, xunit: this.xunit });
- return 0;
- }
- // Validate context properties are reasonable
- if (this.xunit <= 0) {
- console.warn('X function: Invalid xunit value', { xunit: this.xunit });
- return 0;
- }
- // Use more precise calculation with proper parentheses
+ if (isNaN(numV))
+ return NaN;
+ if (isNaN(this.w) || isNaN(this.x1) || isNaN(this.xunit))
+ return NaN;
+ if (this.xunit <= 0)
+ return NaN;
const result = (this.w - (this.x1 - numV)) * this.xunit;
- // Validate result is finite
- if (!isFinite(result)) {
- console.warn('X function: Non-finite result', {
- input: numV,
- w: this.w,
- x1: this.x1,
- xunit: this.xunit,
- result
- });
- return 0;
- }
+ if (!isFinite(result))
+ return NaN;
return Math.round(result * 100) / 100; // Round to 2 decimal places for pixel precision
};
exports.X = X;
@@ -2331,34 +6960,25 @@ const Xinv = function (v) {
return Number(v) / this.xunit - this.w + this.x1;
};
exports.Xinv = Xinv;
+/**
+ * Picture y to device y, inverting the axis: SVG's y grows downward.
+ *
+ * Returns NaN for input it cannot transform, for the same reason as {@link X}.
+ *
+ * @param v - the coordinate in picture units
+ * @returns the device coordinate, or NaN when it cannot be computed
+ */
const Y = function (v) {
- // Enhanced validation for coordinate transformation
const numV = typeof v === 'string' ? parseFloat(v) : v;
- if (isNaN(numV)) {
- console.warn('Y function: Invalid input value', { input: v, parsed: numV });
- return 0;
- }
- if (isNaN(this.y1) || isNaN(this.yunit)) {
- console.warn('Y function: NaN detected in context properties', { y1: this.y1, yunit: this.yunit });
- return 0;
- }
- // Validate context properties are reasonable
- if (this.yunit <= 0) {
- console.warn('Y function: Invalid yunit value', { yunit: this.yunit });
- return 0;
- }
- // Use more precise calculation for Y coordinate inversion
+ if (isNaN(numV))
+ return NaN;
+ if (isNaN(this.y1) || isNaN(this.yunit))
+ return NaN;
+ if (this.yunit <= 0)
+ return NaN;
const result = (this.y1 - numV) * this.yunit;
- // Validate result is finite
- if (!isFinite(result)) {
- console.warn('Y function: Non-finite result', {
- input: numV,
- y1: this.y1,
- yunit: this.yunit,
- result
- });
- return 0;
- }
+ if (!isFinite(result))
+ return NaN;
return Math.round(result * 100) / 100; // Round to 2 decimal places for pixel precision
};
exports.Y = Y;
@@ -2371,8 +6991,13 @@ exports.dotType = exports.parseArrows;
var svg_utils_1 = require("./svg-utils");
Object.defineProperty(exports, "SVGSelection", { enumerable: true, get: function () { return svg_utils_1.SVGSelection; } });
Object.defineProperty(exports, "select", { enumerable: true, get: function () { return svg_utils_1.select; } });
+var expression_2 = require("./expression");
+Object.defineProperty(exports, "parseExpression", { enumerable: true, get: function () { return expression_2.parseExpression; } });
+Object.defineProperty(exports, "ExpressionError", { enumerable: true, get: function () { return expression_2.ExpressionError; } });
+Object.defineProperty(exports, "MATH_FUNCTIONS", { enumerable: true, get: function () { return expression_2.MATH_FUNCTIONS; } });
+Object.defineProperty(exports, "MATH_CONSTANTS", { enumerable: true, get: function () { return expression_2.MATH_CONSTANTS; } });
-},{"./svg-utils":21}],21:[function(require,module,exports){
+},{"./expression":24,"./svg-utils":26}],26:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SVGSelection = void 0;
@@ -2438,11 +7063,16 @@ class SVGSelection {
node() {
return this.elements[0] || null;
}
+ /**
+ * Sets an element's text content.
+ *
+ * `textContent` is defined on every Element, so no narrowing is needed — and
+ * testing `instanceof SVGTextElement` threw a ReferenceError outright in any
+ * DOM that does not expose that constructor as a global, jsdom included.
+ */
text(content) {
this.elements.forEach(el => {
- if (el instanceof SVGTextElement || el instanceof HTMLElement) {
- el.textContent = content;
- }
+ el.textContent = content;
});
return this;
}
@@ -2456,5 +7086,5 @@ function select(selector) {
return new SVGSelection(selector);
}
-},{}]},{},[7])(7)
+},{}]},{},[8])(8)
});
diff --git a/examples/index.html b/examples/index.html
index 45b080d..be652a2 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -131,24 +131,26 @@ LaTeX2JS Examples
+\definecolor{lightblue}{RGB}{173,216,230}
\begin{pspicture}(-2,-2)(2,2)
\psframe(-2,-2)(2,2)
\userline[linewidth=2pt,linecolor=green]{->}(0,0)(2,2){-x}{-y}
\userline[linewidth=2pt,linecolor=red]{->}(0,0)(2,2){0}{y}
-\userline[linewidth=2pt,linecolor=purple]{->}(0,0)(2,2){-x}{cos(y)}
-\userline[linewidth=2pt,linecolor=lightblue]{->}(0,0)(2,2)(sin(x)}{-y}
+\userline[linewidth=2pt,linecolor=violet]{->}(0,0)(2,2){-x}{cos(y)}
+\userline[linewidth=2pt,linecolor=lightblue]{->}(0,0)(2,2){sin(x)}{-y}
\end{pspicture}
source:
\begin{verbatim}
+\definecolor{lightblue}{RGB}{173,216,230}
\begin{pspicture}(-2,-2)(2,2)
\psframe(-2,-2)(2,2)
\userline[linewidth=2pt,linecolor=green]{->}(0,0)(2,2){-x}{-y}
\userline[linewidth=2pt,linecolor=red]{->}(0,0)(2,2){0}{y}
-\userline[linewidth=2pt,linecolor=purple]{->}(0,0)(2,2){-x}{cos(y)}
-\userline[linewidth=2pt,linecolor=lightblue]{->}(0,0)(2,2)(sin(x)}{-y}
+\userline[linewidth=2pt,linecolor=violet]{->}(0,0)(2,2){-x}{cos(y)}
+\userline[linewidth=2pt,linecolor=lightblue]{->}(0,0)(2,2){sin(x)}{-y}
\end{pspicture}
\end{verbatim}
diff --git a/index.html b/index.html
index b4b2df9..201c87f 100644
--- a/index.html
+++ b/index.html
@@ -175,23 +175,26 @@ Help spread the word!
If you specify more arguments, you can create functions for the head and and tail of the vector, which each takes the current $x$ and $y$ position of the users finger or cursor as they move and produces the following interaction:
+\definecolor{lightblue}{RGB}{173,216,230}
+
\begin{center}
\begin{pspicture}(-2,-2)(2,2)
\psframe(-2,-2)(2,2)
\userline[linewidth=2pt,linecolor=green]{->}(0,0)(2,2){-x}{-y}
\userline[linewidth=2pt,linecolor=red]{->}(0,0)(2,2){0}{y}
-\userline[linewidth=2pt,linecolor=purple]{->}(0,0)(2,2){-x}{cos(y)}
-\userline[linewidth=2pt,linecolor=lightblue]{->}(0,0)(2,2)(sin(x)}{-y}
+\userline[linewidth=2pt,linecolor=violet]{->}(0,0)(2,2){-x}{cos(y)}
+\userline[linewidth=2pt,linecolor=lightblue]{->}(0,0)(2,2){sin(x)}{-y}
\end{pspicture}
\end{center}
2 extra arguments provide functions for the head, 4 extra arguments allows you to control both and tail
\begin{verbatim}
+\definecolor{lightblue}{RGB}{173,216,230}
\userline[linewidth=2pt,linecolor=green]{->}(0,0)(2,2){-x}{-y}
\userline[linewidth=2pt,linecolor=red]{->}(0,0)(2,2){0}{y}
-\userline[linewidth=2pt,linecolor=purple]{->}(0,0)(2,2){-x}{cos(y)}
-\userline[linewidth=2pt,linecolor=lightblue]{->}(0,0)(2,2)(sin(x)}{-y}
+\userline[linewidth=2pt,linecolor=violet]{->}(0,0)(2,2){-x}{cos(y)}
+\userline[linewidth=2pt,linecolor=lightblue]{->}(0,0)(2,2){sin(x)}{-y}
\end{verbatim}
I can also draw a more complex version, and start to make more useful diagrams to describe vectors: