Public
Edited
Apr 14
Insert cell
Insert cell
tape = await import("https://esm.sh/gh/Kreijstal/tap-js").then((_) => _.default)
Insert cell
{
var test = tape.createHarness();
var str = "";
test.createStream({ objectMode: false }).on("data", function (row) {
str += row + "\n"; // Append raw TAP output
// console.log(row); // Optional: Log raw TAP output
});

// Keep your other tests if needed, just add the new suite
var mainTestPromise = test("Macro and Parser tests", (t) => {
// --- Keep your existing Lang.WORD tests if you have them ---
// t.test("WORD Parser Tests", (t) => { ... });

// --- Add the new test suite for simpleTexMacroExpand ---
t.test("simpleTexMacroExpand Tests", (t) => {
t.test("Basic definition and usage", (t) => {
const input = "\\def\\greet{Hello}\n\\greet World";
const expected = "\nHello World";
const actual = simpleTexMacroExpand(input);
t.equal(actual, expected, "Should replace defined macro");
t.end();
});

t.test("Multiple definitions and usage", (t) => {
const input =
"\\def\\adj{Wonderful}\\def\\noun{World}\nSuch a \\adj \\noun!";
const expected = "\nSuch a Wonderful World!";
const actual = simpleTexMacroExpand(input);
t.equal(actual, expected, "Should handle multiple definitions");
t.end();
});

t.test("No definitions", (t) => {
const input = "Just plain text.";
const expected = "Just plain text.";
const actual = simpleTexMacroExpand(input);
t.equal(actual, expected, "Should return input if no defs");
t.end();
});

t.test("Definitions but no usage", (t) => {
const input = "Text before.\\def\\unused{Secret}Text after.";
const expected = "Text before.Text after."; // Definition removed
const actual = simpleTexMacroExpand(input);
t.equal(actual, expected, "Should remove unused definitions");
t.end();
});

t.test("Multi-line definition", (t) => {
const input = "\\def\\multiline{Line 1\nLine 2}\nUsage: \\multiline";
const expected = "\nUsage: Line 1\nLine 2";
const actual = simpleTexMacroExpand(input);
t.equal(actual, expected, "Should handle multi-line def body");
t.end();
});

t.test("Nested/Iterative expansion", (t) => {
const input =
"\\def\\base{A}\\def\\level1{\\base B}\\def\\level2{\\level1 C}\nExpand: \\level2";
const expected = "\nExpand: A B C";
const actual = simpleTexMacroExpand(input);
t.equal(actual, expected, "Should expand macros within macros");
t.end();
});

t.test("Undefined macro usage", (t) => {
const input = "\\def\\defined{Yes}\nUsing \\defined and \\undefined.";
const expected = "\nUsing Yes and \\undefined."; // \undefined remains untouched
const actual = simpleTexMacroExpand(input);
t.equal(actual, expected, "Should leave undefined macros as is");
t.end();
});

t.test("Word boundary check", (t) => {
const input = "\\def\\short{One}\nUse \\short and \\shortAndLong.";
const expected = "\nUse One and \\shortAndLong."; // Only \short replaced
const actual = simpleTexMacroExpand(input);
t.equal(actual, expected, "Should respect word boundaries");
t.end();
});

t.test("Empty definition", (t) => {
const input = "Start\\def\\empty{}End\nUse it here: >\\empty<";
const expected = "StartEnd\nUse it here: ><";
const actual = simpleTexMacroExpand(input);
t.equal(
actual,
expected,
"Should replace with empty string for empty def"
);
t.end();
});

t.test("Single argument macro", (t) => {
const input = "\\def\\echo#1{Input: #1}\nCall: \\echo{Test}";
const expected = "\nCall: Input: Test";
const actual = simpleTexMacroExpand(input);
t.equal(actual, expected, "Should handle single argument");
t.end();
});

t.test("Multiple argument macro", (t) => {
const input = "\\def\\pair#1#2{(#1, #2)}\nCoords: \\pair{X}{Y}";
const expected = "\nCoords: (X, Y)";
const actual = simpleTexMacroExpand(input);
t.equal(actual, expected, "Should handle multiple arguments");
t.end();
});

t.test("Arguments containing macros", (t) => {
const input =
"\\def\\bold#1{{\\bf #1}}\\def\\bf{BOLD}\\Usage: \\bold{Text}";
const expected = "\nUsage: {BOLD Text}"; // Note: Braces from def remain
const actual = simpleTexMacroExpand(input);
t.equal(
actual,
expected,
"Should expand macros within arguments during subsequent passes"
);
t.end();
});

t.test("Argument contains hash character", (t) => {
const input = "\\def\\show#1{See: #1}\n\\show{Value # is important}";
const expected = "\nSee: Value # is important";
const actual = simpleTexMacroExpand(input);
t.equal(
actual,
expected,
"Should not replace '#' within argument text"
);
t.end();
});

t.test("Macro with args used without args", (t) => {
const input = "\\def\\needsone#1{Got #1}\nCall: \\needsone maybe?";
const expected = "\nCall: \\needsone maybe?"; // Does not match regex, so no expansion
const actual = simpleTexMacroExpand(input);
t.equal(
actual,
expected,
"Should not expand arg macro if called without args"
);
t.end();
});

t.test("Macro without args used with braces (incorrectly)", (t) => {
const input = "\\def\\noargs{NA}\nCall: \\noargs{stuff}";
// Expands \noargs, leaves {stuff} as it wasn't part of the match
const expected = "\nCall: NA{stuff}";
const actual = simpleTexMacroExpand(input);
t.equal(
actual,
expected,
"Should expand no-arg macro and leave subsequent braces"
);
t.end();
});

t.test("Nested arguments and expansion", (t) => {
const input =
"\\def\\outer#1{Outer<#1>}\\def\\inner#1{Inner(#1)}\nExpand: \\outer{\\inner{Value}}";
const expected = "\nExpand: Outer<Inner(Value)>"; // \inner expanded first within the arg in a later pass typically, though order depends on loop structure. Here \outer gets expanded first, then \inner inside it.
const actual = simpleTexMacroExpand(input);
t.equal(
actual,
expected,
"Should handle nested macro calls within arguments"
);
t.end();
});

t.test("Argument order", (t) => {
const input = "\\def\\reorder#1#2{#2 before #1}\nUse: \\reorder{A}{B}";
const expected = "\nUse: B before A";
const actual = simpleTexMacroExpand(input);
t.equal(actual, expected, "Should respect argument numbering (#1, #2)");
t.end();
});

// Signal end of this specific test suite
t.end();
});

// Signal end of the main test group (important if you add more top-level groups)
// t.end(); // Usually not needed here if this is the only top-level group run by the harness
});

// --- Keep your Promise resolution logic ---
var m = new Promise((resolve, reject) => {
let finished = false; // Prevent double resolution/rejection
let timeoutId = setTimeout(() => {
// Add a timeout for safety
if (!finished) {
console.error("Test harness timed out!");
reject("Timeout\n" + str); // Reject with collected output on timeout
}
}, 5000); // 5 second timeout

test.onFinish(() => {
if (!finished) {
finished = true;
clearTimeout(timeoutId);
console.log("--- TAP Output ---");
console.log(str.trim()); // Log the final TAP output
console.log("--- Test Finished ---");
// Check TAP output for failures before resolving
if (str.includes("not ok")) {
reject("Test failures detected.\n" + str);
} else {
resolve(str);
}
}
});
});

return m;
}
Insert cell
/**
* Performs a simplified, iterative expansion of TeX-like macros.
* Handles basic \def\name#1#2{body} definitions and \name{arg1}{arg2} usage.
* Does NOT support optional arguments, scope, or complex TeX features.
* Limited to 9 arguments (#1 through #9).
*
* @param {string} inputText The text containing macro definitions and usage.
* @param {number} [maxPasses=100] Maximum iterations to prevent infinite loops.
* @returns {string} The text with macros expanded.
*/
function simpleTexMacroExpand(inputText, maxPasses = 100) {
const macros = {}; // Store { name: { body: string, args: number } }

// Regex to find definitions: \def \name (#1#2...) { body }
// - \\def\\ : Literal "\def\"
// - (\w+) : Capture group 1: the macro name (word characters)
// - ((?:#\d)*) : Capture group 2: Argument specifiers like #1#2 (optional, non-capturing group repeated)
// - \{ : Literal "{"
// - (.*?) : Capture group 3: the macro body (non-greedy)
// - \} : Literal "}"
// - g : Global search
// - s : Dot matches newline
const defRegex = /\\def\\(\w+)((?:#\d)*)\{(.*?)\}/gs;

let textToExpand = "";
let lastIndex = 0;
let match;

// --- Pass 1: Extract definitions and build the text for expansion ---
while ((match = defRegex.exec(inputText)) !== null) {
const macroName = match[1];
const argSpecifiers = match[2]; // String like "#1#2" or ""
const expansion = match[3];

// Calculate number of arguments (simple count of '#')
// This assumes they are sequential #1, #2... which is typical
const argCount = (argSpecifiers.match(/#\d/g) || []).length;

macros[macroName] = {
body: expansion,
args: argCount
};

// Append the text chunk before this definition found
textToExpand += inputText.substring(lastIndex, match.index);
// Update index to skip over the definition itself
lastIndex = defRegex.lastIndex;
}
// Append any remaining text after the last definition
textToExpand += inputText.substring(lastIndex);

// --- DEBUG ---
// console.log("Macros defined:", macros);
// console.log("Text prepared for expansion:", textToExpand);
// ---

// --- Pass 2: Iterative Expansion ---
let expandedText = textToExpand;
let changedInPass = true;
let currentPass = 0;

while (changedInPass && currentPass < maxPasses) {
changedInPass = false;
currentPass++;

// console.log(`--- Expansion Pass ${currentPass} ---`); // Debug
// console.log("Before:", expandedText); // Debug

for (const name in macros) {
if (!macros.hasOwnProperty(name)) continue;

const { body, args } = macros[name];
const escapedName = escapeRegExp(name);
let usageRegex;
let replacementHandler;

if (args === 0) {
// --- No arguments ---
// Regex: \name followed by a word boundary
usageRegex = new RegExp(`\\\\${escapedName}\\b`, "g");
replacementHandler = () => body; // Simple replacement
} else {
// --- With arguments ---
// Build regex: \name {arg1} {arg2} ...
// - We need 'args' number of capture groups for the arguments.
// - {(.*?)} captures anything inside braces non-greedily.
// - We assume no significant whitespace between macro name and args, or between args.
let argPatterns = "";
for (let i = 0; i < args; i++) {
// Match whitespace? No, keep it simple: arguments must follow directly.
argPatterns += `\\{(.*?)\\}`; // Capture group for each argument
}
// Regex: \name{arg1}{arg2}...
// Need 's' flag if arguments can contain newlines.
usageRegex = new RegExp(`\\\\${escapedName}${argPatterns}`, "gs");

replacementHandler = (match, ...capturedArgs) => {
// capturedArgs will contain the content of each {.*?} group
if (capturedArgs.length !== args) {
// Should not happen with this regex, but safety check
console.warn(`Argument mismatch for \\${name}`);
return match; // Return original match if something is wrong
}

let substitutedBody = body;
// Replace #1, #2, ... in the body with the captured arguments
for (let i = 1; i <= args; i++) {
// Use a regex to replace exactly #i, avoiding issues with #10 etc.
// Need to escape '#' for the regex? No, # is not special here.
const argPlaceholderRegex = new RegExp(`#${i}`, "g");
// Use capturedArgs[i-1] because array is 0-indexed
substitutedBody = substitutedBody.replace(
argPlaceholderRegex,
capturedArgs[i - 1]
);
}
return substitutedBody;
};
}

// --- Perform replacement ---
const textBeforeReplacement = expandedText;
expandedText = expandedText.replace(usageRegex, replacementHandler);

// If replace made a change, mark that we need potentially another pass
if (expandedText !== textBeforeReplacement) {
changedInPass = true;
}
}
// console.log("After:", expandedText); // Debug
}

if (currentPass >= maxPasses) {
console.warn(
`Macro expansion stopped after ${maxPasses} passes. Potential infinite loop or complex expansion.`
);
}

return expandedText;
}
Insert cell
// Helper function to escape strings for use in RegExp constructor
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more