Browse Source

all tests passing

master
silberengel 7 months ago
parent
commit
9c53a56413
  1. 275
      src/lib/utils/asciidoc_metadata.ts
  2. 8
      src/lib/utils/event_input_utils.ts
  3. 321
      src/lib/utils/markup/advancedMarkupParser.ts
  4. 22
      test_data/LaTeXtestfile.json
  5. 151
      test_data/LaTeXtestfile.md
  6. 4178
      test_output.log
  7. 83
      tests/unit/latexRendering.test.ts
  8. 186
      tests/unit/mathProcessing.test.ts
  9. 9
      tests/unit/tagExpansion.test.ts

275
src/lib/utils/asciidoc_metadata.ts

@ -115,6 +115,13 @@ function mapAttributesToMetadata(
} else if (metadataKey === "tags") { } else if (metadataKey === "tags") {
// Skip tags mapping since it's handled by extractTagsFromAttributes // Skip tags mapping since it's handled by extractTagsFromAttributes
continue; continue;
} else if (metadataKey === "summary") {
// Handle summary specially - combine with existing summary if present
if (metadata.summary) {
metadata.summary = `${metadata.summary} ${value}`;
} else {
metadata.summary = value;
}
} else { } else {
(metadata as any)[metadataKey] = value; (metadata as any)[metadataKey] = value;
} }
@ -123,84 +130,178 @@ function mapAttributesToMetadata(
} }
/** /**
* Extracts authors from header line (document or section) * Extracts authors from document header only (not sections)
*/ */
function extractAuthorsFromHeader( function extractDocumentAuthors(sourceContent: string): string[] {
sourceContent: string,
isSection: boolean = false,
): string[] {
const authors: string[] = []; const authors: string[] = [];
const lines = sourceContent.split(/\r?\n/); const lines = sourceContent.split(/\r?\n/);
const headerPattern = isSection ? /^==\s+/ : /^=\s+/;
// Find the document title line
let titleLineIndex = -1;
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
if (lines[i].match(/^=\s+/)) {
titleLineIndex = i;
break;
}
}
if (titleLineIndex === -1) {
return authors;
}
// Look for authors in the lines immediately following the title
let i = titleLineIndex + 1;
while (i < lines.length) {
const line = lines[i]; const line = lines[i];
if (line.match(headerPattern)) {
// Found title line, check subsequent lines for authors // Stop if we hit a blank line, section header, or content that's not an author
let j = i + 1; if (line.trim() === "" || line.match(/^==\s+/)) {
while (j < lines.length) { break;
const authorLine = lines[j]; }
// Stop if we hit a blank line or content that's not an author if (line.includes("<") && !line.startsWith(":")) {
if (authorLine.trim() === "") { // This is an author line like "John Doe <john@example.com>"
break; const authorName = line.split("<")[0].trim();
} if (authorName) {
authors.push(authorName);
if (authorLine.includes("<") && !authorLine.startsWith(":")) {
// This is an author line like "John Doe <john@example.com>"
const authorName = authorLine.split("<")[0].trim();
if (authorName) {
authors.push(authorName);
}
} else if (
isSection && authorLine.match(/^[A-Za-z\s]+$/) &&
authorLine.trim() !== "" && authorLine.trim().split(/\s+/).length <= 2
) {
// This is a simple author name without email (for sections)
authors.push(authorLine.trim());
} else if (authorLine.startsWith(":")) {
// This is an attribute line, skip it - attributes are handled by mapAttributesToMetadata
// Don't break here, continue to next line
} else {
// Not an author line, stop looking
break;
}
j++;
} }
} else if (line.startsWith(":")) {
// This is an attribute line, skip it
// Don't break here, continue to next line
} else {
// Not an author line, stop looking
break; break;
} }
i++;
} }
return authors;
}
/**
* Extracts authors from section header only
*/
function extractSectionAuthors(sectionContent: string): string[] {
const authors: string[] = [];
const lines = sectionContent.split(/\r?\n/);
// Find the section title line
let titleLineIndex = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].match(/^==\s+/)) {
titleLineIndex = i;
break;
}
}
if (titleLineIndex === -1) {
return authors;
}
// Look for authors in the lines immediately following the section title
let i = titleLineIndex + 1;
while (i < lines.length) {
const line = lines[i];
// Stop if we hit a blank line, another section header, or content that's not an author
if (line.trim() === "" || line.match(/^==\s+/)) {
break;
}
if (line.includes("<") && !line.startsWith(":")) {
// This is an author line like "John Doe <john@example.com>"
const authorName = line.split("<")[0].trim();
if (authorName) {
authors.push(authorName);
}
} else if (
line.match(/^[A-Za-z\s]+$/) &&
line.trim() !== "" &&
line.trim().split(/\s+/).length <= 2 &&
!line.startsWith(":")
) {
// This is a simple author name without email (for sections)
authors.push(line.trim());
} else if (line.startsWith(":")) {
// This is an attribute line, skip it
// Don't break here, continue to next line
} else {
// Not an author line, stop looking
break;
}
i++;
}
return authors; return authors;
} }
/** /**
* Strips header and attribute lines from content * Strips document header and attribute lines from content
*/ */
function stripHeaderAndAttributes( function stripDocumentHeader(content: string): string {
content: string,
isSection: boolean = false,
): string {
const lines = content.split(/\r?\n/); const lines = content.split(/\r?\n/);
let contentStart = 0; let contentStart = 0;
const headerPattern = isSection ? /^==\s+/ : /^=\s+/;
// Find where the document header ends
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i]; const line = lines[i];
// Skip title line, author line, revision line, and attribute lines // Skip title line, author line, revision line, and attribute lines
if ( if (
!line.match(headerPattern) && !line.includes("<") && !line.match(/^=\s+/) &&
!line.includes("<") &&
!line.match(/^.+,\s*.+:\s*.+$/) && !line.match(/^.+,\s*.+:\s*.+$/) &&
!line.match(/^:[^:]+:\s*.+$/) && line.trim() !== "" !line.match(/^:[^:]+:\s*.+$/) &&
line.trim() !== ""
) { ) {
contentStart = i; contentStart = i;
break; break;
} }
} }
// Filter out all attribute lines and author lines from the content // Filter out all attribute lines and author lines from the content
const contentLines = lines.slice(contentStart); const contentLines = lines.slice(contentStart);
const filteredLines = contentLines.filter((line) => {
// Skip attribute lines
if (line.match(/^:[^:]+:\s*.+$/)) {
return false;
}
return true;
});
// Remove extra blank lines and normalize newlines
return filteredLines.join("\n").replace(/\n\s*\n\s*\n/g, "\n\n").replace(
/\n\s*\n/g,
"\n",
).trim();
}
/**
* Strips section header and attribute lines from content
*/
function stripSectionHeader(sectionContent: string): string {
const lines = sectionContent.split(/\r?\n/);
let contentStart = 0;
// Find where the section header ends
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Skip section title line, author line, and attribute lines
if (
!line.match(/^==\s+/) &&
!line.includes("<") &&
!line.match(/^:[^:]+:\s*.+$/) &&
line.trim() !== "" &&
!(line.match(/^[A-Za-z\s]+$/) && line.trim() !== "" && line.trim().split(/\s+/).length <= 2)
) {
contentStart = i;
break;
}
}
// Filter out all attribute lines, author lines, and section headers from the content
const contentLines = lines.slice(contentStart);
const filteredLines = contentLines.filter((line) => { const filteredLines = contentLines.filter((line) => {
// Skip attribute lines // Skip attribute lines
if (line.match(/^:[^:]+:\s*.+$/)) { if (line.match(/^:[^:]+:\s*.+$/)) {
@ -208,14 +309,19 @@ function stripHeaderAndAttributes(
} }
// Skip author lines (simple names without email) // Skip author lines (simple names without email)
if ( if (
isSection && line.match(/^[A-Za-z\s]+$/) && line.trim() !== "" && line.match(/^[A-Za-z\s]+$/) &&
line.trim() !== "" &&
line.trim().split(/\s+/).length <= 2 line.trim().split(/\s+/).length <= 2
) { ) {
return false; return false;
} }
// Skip section headers
if (line.match(/^==\s+/)) {
return false;
}
return true; return true;
}); });
// Remove extra blank lines and normalize newlines // Remove extra blank lines and normalize newlines
return filteredLines.join("\n").replace(/\n\s*\n\s*\n/g, "\n\n").replace( return filteredLines.join("\n").replace(/\n\s*\n\s*\n/g, "\n\n").replace(
/\n\s*\n/g, /\n\s*\n/g,
@ -258,18 +364,40 @@ export function extractDocumentMetadata(inputContent: string): {
// Extract basic metadata // Extract basic metadata
const title = document.getTitle(); const title = document.getTitle();
if (title) metadata.title = title; if (title) {
// Decode HTML entities in the title
metadata.title = title
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'")
.replace(/&nbsp;/g, " ");
}
// Handle multiple authors - combine header line and attributes // Handle multiple authors - combine header line and attributes
const authors = extractAuthorsFromHeader(document.getSource()); const authors = extractDocumentAuthors(document.getSource());
// Get authors from attributes (but avoid duplicates) // Get authors from attributes in the document header only (including multiple :author: lines)
const attrAuthor = attributes["author"]; const lines = document.getSource().split(/\r?\n/);
if ( let inDocumentHeader = true;
attrAuthor && typeof attrAuthor === "string" && for (const line of lines) {
!authors.includes(attrAuthor) // Stop scanning when we hit a section header
) { if (line.match(/^==\s+/)) {
authors.push(attrAuthor); inDocumentHeader = false;
break;
}
// Process :author: attributes regardless of other content
if (inDocumentHeader) {
const match = line.match(/^:author:\s*(.+)$/);
if (match) {
const authorName = match[1].trim();
if (authorName && !authors.includes(authorName)) {
authors.push(authorName);
}
}
}
} }
if (authors.length > 0) { if (authors.length > 0) {
@ -305,7 +433,7 @@ export function extractDocumentMetadata(inputContent: string): {
metadata.tags = tags; metadata.tags = tags;
} }
const content = stripHeaderAndAttributes(document.getSource()); const content = stripDocumentHeader(document.getSource());
return { metadata, content }; return { metadata, content };
} }
@ -335,13 +463,28 @@ export function extractSectionMetadata(inputSectionContent: string): {
const attributes = parseSectionAttributes(inputSectionContent); const attributes = parseSectionAttributes(inputSectionContent);
// Extract authors from section content // Extract authors from section content
const authors = extractAuthorsFromHeader(inputSectionContent, true); const authors = extractSectionAuthors(inputSectionContent);
// Get authors from attributes (including multiple :author: lines)
const lines = inputSectionContent.split(/\r?\n/);
for (const line of lines) {
const match = line.match(/^:author:\s*(.+)$/);
if (match) {
const authorName = match[1].trim();
if (authorName && !authors.includes(authorName)) {
authors.push(authorName);
}
}
}
if (authors.length > 0) { if (authors.length > 0) {
metadata.authors = authors; metadata.authors = authors;
} }
// Map attributes to metadata (sections can have authors) // Map attributes to metadata (sections can have authors, but skip author mapping to avoid duplication)
mapAttributesToMetadata(attributes, metadata, false); const attributesWithoutAuthor = { ...attributes };
delete attributesWithoutAuthor.author;
mapAttributesToMetadata(attributesWithoutAuthor, metadata, false);
// Handle tags and keywords // Handle tags and keywords
const tags = extractTagsFromAttributes(attributes); const tags = extractTagsFromAttributes(attributes);
@ -349,7 +492,7 @@ export function extractSectionMetadata(inputSectionContent: string): {
metadata.tags = tags; metadata.tags = tags;
} }
const content = stripHeaderAndAttributes(inputSectionContent, true); const content = stripSectionHeader(inputSectionContent);
return { metadata, content, title }; return { metadata, content, title };
} }

8
src/lib/utils/event_input_utils.ts

@ -170,6 +170,14 @@ export function validate30040EventSet(content: string): {
function normalizeDTagValue(header: string): string { function normalizeDTagValue(header: string): string {
return header return header
.toLowerCase() .toLowerCase()
// Decode common HTML entities first
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'")
.replace(/&nbsp;/g, " ")
// Then normalize as before
.replace(/[^\p{L}\p{N}]+/gu, "-") .replace(/[^\p{L}\p{N}]+/gu, "-")
.replace(/^-+|-+$/g, ""); .replace(/^-+|-+$/g, "");
} }

321
src/lib/utils/markup/advancedMarkupParser.ts

@ -30,6 +30,7 @@ function escapeHtml(text: string): string {
const HEADING_REGEX = /^(#{1,6})\s+(.+)$/gm; const HEADING_REGEX = /^(#{1,6})\s+(.+)$/gm;
const ALTERNATE_HEADING_REGEX = /^([^\n]+)\n(=+|-+)\n/gm; const ALTERNATE_HEADING_REGEX = /^([^\n]+)\n(=+|-+)\n/gm;
const INLINE_CODE_REGEX = /`([^`\n]+)`/g; const INLINE_CODE_REGEX = /`([^`\n]+)`/g;
const MULTILINE_CODE_REGEX = /`([\s\S]*?)`/g;
const HORIZONTAL_RULE_REGEX = /^(?:[-*_]\s*){3,}$/gm; const HORIZONTAL_RULE_REGEX = /^(?:[-*_]\s*){3,}$/gm;
const FOOTNOTE_REFERENCE_REGEX = /\[\^([^\]]+)\]/g; const FOOTNOTE_REFERENCE_REGEX = /\[\^([^\]]+)\]/g;
const FOOTNOTE_DEFINITION_REGEX = /^\[\^([^\]]+)\]:\s*(.+)$/gm; const FOOTNOTE_DEFINITION_REGEX = /^\[\^([^\]]+)\]:\s*(.+)$/gm;
@ -390,296 +391,41 @@ function restoreCodeBlocks(text: string, blocks: Map<string, string>): string {
} }
/** /**
* Process $...$ and $$...$$ math blocks: render as LaTeX if recognized, otherwise as AsciiMath * Process math expressions inside inline code blocks
* This must run BEFORE any paragraph or inline code formatting. * Only processes math that is inside backticks and contains $...$ or $$...$$ markings
*/ */
function processDollarMath(content: string): string { function processInlineCodeMath(content: string): string {
// Display math: $$...$$ (multi-line, not empty) return content.replace(MULTILINE_CODE_REGEX, (match, codeContent) => {
content = content.replace(/\$\$([\s\S]*?\S[\s\S]*?)\$\$/g, (_match, expr) => { // Check if the code content contains math expressions
if (isLaTeXContent(expr)) { const hasInlineMath = /\$((?:[^$\\]|\\.)*?)\$/.test(codeContent);
return `<div class="math-block">$$${expr}$$</div>`; const hasDisplayMath = /\$\$[\s\S]*?\$\$/.test(codeContent);
} else {
// Strip all $ or $$ from AsciiMath if (!hasInlineMath && !hasDisplayMath) {
const clean = expr.replace(/\$+/g, "").trim(); // No math found, return the original inline code
return `<div class="math-block" data-math-type="asciimath">${clean}</div>`; return match;
} }
});
// Inline math: $...$ (not empty, not just whitespace) // Process display math ($$...$$) first to avoid conflicts with inline math
content = content.replace(/\$([^\s$][^$\n]*?)\$/g, (_match, expr) => { let processedContent = codeContent.replace(/\$\$([\s\S]*?)\$\$/g, (mathMatch: string, mathContent: string) => {
if (isLaTeXContent(expr)) { // Skip empty math expressions
return `<span class="math-inline">$${expr}$</span>`; if (!mathContent.trim()) {
} else { return mathMatch;
const clean = expr.replace(/\$+/g, "").trim();
return `<span class="math-inline" data-math-type="asciimath">${clean}</span>`;
} }
return `<span class="math-display">\\[${mathContent}\\]</span>`;
}); });
return content;
} // Process inline math ($...$) after display math
// Use a more sophisticated regex that handles escaped dollar signs
/** processedContent = processedContent.replace(/\$((?:[^$\\]|\\.)*?)\$/g, (mathMatch: string, mathContent: string) => {
* Process LaTeX math expressions only within inline code blocks // Skip empty math expressions
*/ if (!mathContent.trim()) {
function processMathExpressions(content: string): string { return mathMatch;
// Only process LaTeX within inline code blocks (backticks)
return content.replace(INLINE_CODE_REGEX, (_match, code) => {
const trimmedCode = code.trim();
// Check for unsupported LaTeX environments (like tabular) first
if (/\\begin\{tabular\}|\\\\begin\{tabular\}/.test(trimmedCode)) {
return `<div class="unrendered-latex">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">
Unrendered, as it is LaTeX typesetting, not a formula:
</p>
<pre class="bg-gray-100 dark:bg-gray-900 p-2 rounded text-xs overflow-x-auto">
<code>${escapeHtml(trimmedCode)}</code>
</pre>
</div>`;
}
// Check if the code contains LaTeX syntax
if (isLaTeXContent(trimmedCode)) {
// Detect LaTeX display math (\\[...\\])
if (/^\\\[[\s\S]*\\\]$/.test(trimmedCode)) {
// Remove the delimiters for rendering
const inner = trimmedCode.replace(/^\\\[|\\\]$/g, "");
return `<div class="math-block">$$${inner}$$</div>`;
}
// Detect display math ($$...$$)
if (/^\$\$[\s\S]*\$\$$/.test(trimmedCode)) {
// Remove the delimiters for rendering
const inner = trimmedCode.replace(/^\$\$|\$\$$/g, "");
return `<div class="math-block">$$${inner}$$</div>`;
}
// Detect inline math ($...$)
if (/^\$[\s\S]*\$$/.test(trimmedCode)) {
// Remove the delimiters for rendering
const inner = trimmedCode.replace(/^\$|\$$/g, "");
return `<span class="math-inline">$${inner}$</span>`;
}
// Default to inline math for any other LaTeX content
return `<span class="math-inline">$${trimmedCode}$</span>`;
} else {
// Check for edge cases that should remain as code, not math
// These patterns indicate code that contains dollar signs but is not math
const codePatterns = [
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=/, // Variable assignment like "const price ="
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/, // Function call like "echo("
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\{/, // Object literal like "const obj = {"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*\[/, // Array literal like "const arr = ["
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*</, // JSX or HTML like "const element = <"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*`/, // Template literal like "const str = `"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*'/, // String literal like "const str = '"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*"/, // String literal like "const str = \""
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*;/, // Statement ending like "const x = 1;"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*$/, // Just a variable name
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]/, // Operator like "const x = 1 +"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Two identifiers like "const price = amount"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]/, // Number like "const x = 1"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]/, // Complex expression like "const price = amount +"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[a-zA-Z0-9_$]*\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Three identifiers like "const price = amount + tax"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]/, // Two identifiers and number like "const price = amount + 1"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]/, // Identifier, number, operator like "const x = 1 +"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Identifier, number, identifier like "const x = 1 + y"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[0-9]/, // Identifier, number, number like "const x = 1 + 2"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Complex like "const x = 1 + y"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Complex like "const x = 1 + 2"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]/, // Very complex like "const x = 1 + y +"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Very complex like "const x = 1 + y + z"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Very complex like "const x = 1 + y + 2"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]\s*[+\-*/%=<>!&|^~]/, // Very complex like "const x = 1 + 2 +"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Very complex like "const x = 1 + 2 + y"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Very complex like "const x = 1 + 2 + 3"
// Additional patterns for JavaScript template literals and other code
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*`/, // Template literal assignment like "const str = `"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*'/, // String assignment like "const str = '"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*"/, // String assignment like "const str = \""
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[0-9]/, // Number assignment like "const x = 1"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Variable assignment like "const x = y"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[+\-*/%=<>!&|^~]/, // Assignment with operator like "const x = +"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]/, // Assignment with variable and operator like "const x = y +"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Assignment with two variables and operator like "const x = y + z"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[0-9]\s*[+\-*/%=<>!&|^~]/, // Assignment with number and operator like "const x = 1 +"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[a-zA-Z_$][a-zA-Z0-9_$]*/, // Assignment with number, operator, variable like "const x = 1 + y"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Assignment with variable, operator, number like "const x = y + 1"
/^[a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[0-9]\s*[+\-*/%=<>!&|^~]\s*[0-9]/, // Assignment with number, operator, number like "const x = 1 + 2"
];
// If it matches code patterns, treat as regular code
if (codePatterns.some((pattern) => pattern.test(trimmedCode))) {
const escapedCode = trimmedCode
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
return `<code class="px-1.5 py-0.5 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded text-sm font-mono">${escapedCode}</code>`;
}
// Return as regular inline code
const escapedCode = trimmedCode
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
return `<code class="px-1.5 py-0.5 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded text-sm font-mono">${escapedCode}</code>`;
} }
return `<span class="math-inline">\\(${mathContent}\\)</span>`;
});
return `\`${processedContent}\``;
}); });
}
/**
* Checks if content contains LaTeX syntax
*/
function isLaTeXContent(content: string): boolean {
const trimmed = content.trim();
// Check for simple math expressions first (like AsciiMath)
if (/^\$[^$]+\$$/.test(trimmed)) {
return true;
}
// Check for display math
if (/^\$\$[\s\S]*\$\$$/.test(trimmed)) {
return true;
}
// Check for LaTeX display math
if (/^\\\[[\s\S]*\\\]$/.test(trimmed)) {
return true;
}
// Check for LaTeX environments with double backslashes (like tabular)
if (/\\\\begin\{[^}]+\}/.test(trimmed) || /\\\\end\{[^}]+\}/.test(trimmed)) {
return true;
}
// Check for common LaTeX patterns
const latexPatterns = [
/\\[a-zA-Z]+/, // LaTeX commands like \frac, \sum, etc.
/\\\\[a-zA-Z]+/, // LaTeX commands with double backslashes like \\frac, \\sum, etc.
/\\[\(\)\[\]]/, // LaTeX delimiters like \(, \), \[, \]
/\\\\[\(\)\[\]]/, // LaTeX delimiters with double backslashes like \\(, \\), \\[, \\]
/\\\[[\s\S]*?\\\]/, // LaTeX display math \[ ... \]
/\\\\\[[\s\S]*?\\\\\]/, // LaTeX display math with double backslashes \\[ ... \\]
/\\begin\{/, // LaTeX environments
/\\\\begin\{/, // LaTeX environments with double backslashes
/\\end\{/, // LaTeX environments
/\\\\end\{/, // LaTeX environments with double backslashes
/\\begin\{array\}/, // LaTeX array environment
/\\\\begin\{array\}/, // LaTeX array environment with double backslashes
/\\end\{array\}/,
/\\\\end\{array\}/,
/\\begin\{matrix\}/, // LaTeX matrix environment
/\\\\begin\{matrix\}/, // LaTeX matrix environment with double backslashes
/\\end\{matrix\}/,
/\\\\end\{matrix\}/,
/\\begin\{bmatrix\}/, // LaTeX bmatrix environment
/\\\\begin\{bmatrix\}/, // LaTeX bmatrix environment with double backslashes
/\\end\{bmatrix\}/,
/\\\\end\{bmatrix\}/,
/\\begin\{pmatrix\}/, // LaTeX pmatrix environment
/\\\\begin\{pmatrix\}/, // LaTeX pmatrix environment with double backslashes
/\\end\{pmatrix\}/,
/\\\\end\{pmatrix\}/,
/\\begin\{tabular\}/, // LaTeX tabular environment
/\\\\begin\{tabular\}/, // LaTeX tabular environment with double backslashes
/\\end\{tabular\}/,
/\\\\end\{tabular\}/,
/\$\$/, // Display math delimiters
/\$[^$]+\$/, // Inline math delimiters
/\\text\{/, // LaTeX text command
/\\\\text\{/, // LaTeX text command with double backslashes
/\\mathrm\{/, // LaTeX mathrm command
/\\\\mathrm\{/, // LaTeX mathrm command with double backslashes
/\\mathbf\{/, // LaTeX bold command
/\\\\mathbf\{/, // LaTeX bold command with double backslashes
/\\mathit\{/, // LaTeX italic command
/\\\\mathit\{/, // LaTeX italic command with double backslashes
/\\sqrt/, // Square root
/\\\\sqrt/, // Square root with double backslashes
/\\frac/, // Fraction
/\\\\frac/, // Fraction with double backslashes
/\\sum/, // Sum
/\\\\sum/, // Sum with double backslashes
/\\int/, // Integral
/\\\\int/, // Integral with double backslashes
/\\lim/, // Limit
/\\\\lim/, // Limit with double backslashes
/\\infty/, // Infinity
/\\\\infty/, // Infinity with double backslashes
/\\alpha/, // Greek letters
/\\\\alpha/, // Greek letters with double backslashes
/\\beta/,
/\\\\beta/,
/\\gamma/,
/\\\\gamma/,
/\\delta/,
/\\\\delta/,
/\\theta/,
/\\\\theta/,
/\\lambda/,
/\\\\lambda/,
/\\mu/,
/\\\\mu/,
/\\pi/,
/\\\\pi/,
/\\sigma/,
/\\\\sigma/,
/\\phi/,
/\\\\phi/,
/\\omega/,
/\\\\omega/,
/\\partial/, // Partial derivative
/\\\\partial/, // Partial derivative with double backslashes
/\\nabla/, // Nabla
/\\\\nabla/, // Nabla with double backslashes
/\\cdot/, // Dot product
/\\\\cdot/, // Dot product with double backslashes
/\\times/, // Times
/\\\\times/, // Times with double backslashes
/\\div/, // Division
/\\\\div/, // Division with double backslashes
/\\pm/, // Plus-minus
/\\\\pm/, // Plus-minus with double backslashes
/\\mp/, // Minus-plus
/\\\\mp/, // Minus-plus with double backslashes
/\\leq/, // Less than or equal
/\\\\leq/, // Less than or equal with double backslashes
/\\geq/, // Greater than or equal
/\\\\geq/, // Greater than or equal with double backslashes
/\\neq/, // Not equal
/\\\\neq/, // Not equal with double backslashes
/\\approx/, // Approximately equal
/\\\\approx/, // Approximately equal with double backslashes
/\\equiv/, // Equivalent
/\\\\equiv/, // Equivalent with double backslashes
/\\propto/, // Proportional
/\\\\propto/, // Proportional with double backslashes
/\\in/, // Element of
/\\\\in/, // Element of with double backslashes
/\\notin/, // Not element of
/\\\\notin/, // Not element of with double backslashes
/\\subset/, // Subset
/\\\\subset/, // Subset with double backslashes
/\\supset/, // Superset
/\\\\supset/, // Superset with double backslashes
/\\cup/, // Union
/\\\\cup/, // Union with double backslashes
/\\cap/, // Intersection
/\\\\cap/, // Intersection with double backslashes
/\\emptyset/, // Empty set
/\\\\emptyset/, // Empty set with double backslashes
/\\mathbb\{/, // Blackboard bold
/\\\\mathbb\{/, // Blackboard bold with double backslashes
/\\mathcal\{/, // Calligraphic
/\\\\mathcal\{/, // Calligraphic with double backslashes
/\\mathfrak\{/, // Fraktur
/\\\\mathfrak\{/, // Fraktur with double backslashes
/\\mathscr\{/, // Script
/\\\\mathscr\{/, // Script with double backslashes
];
return latexPatterns.some((pattern) => pattern.test(trimmed));
} }
/** /**
@ -693,11 +439,8 @@ export async function parseAdvancedmarkup(text: string): Promise<string> {
const { text: withoutCode, blocks } = processCodeBlocks(text); const { text: withoutCode, blocks } = processCodeBlocks(text);
let processedText = withoutCode; let processedText = withoutCode;
// Step 2: Process $...$ and $$...$$ math blocks (LaTeX or AsciiMath) // Step 2: Process math inside inline code blocks
processedText = processDollarMath(processedText); processedText = processInlineCodeMath(processedText);
// Step 3: Process LaTeX math expressions ONLY within inline code blocks (legacy support)
processedText = processMathExpressions(processedText);
// Step 4: Process block-level elements (tables, headings, horizontal rules) // Step 4: Process block-level elements (tables, headings, horizontal rules)
// AI-NOTE: 2025-01-24 - Removed duplicate processBlockquotes call to fix image rendering issues // AI-NOTE: 2025-01-24 - Removed duplicate processBlockquotes call to fix image rendering issues

22
test_data/LaTeXtestfile.json

File diff suppressed because one or more lines are too long

151
test_data/LaTeXtestfile.md

@ -1,151 +0,0 @@
# This is a testfile for writing mathematic formulas in NostrMarkup
This document covers the rendering of formulas in TeX/LaTeX and AsciiMath
notation, or some combination of those within the same page. It is meant to be
rendered by clients utilizing MathJax.
If you want the entire document to be rendered as mathematics, place the entire
thing in a backtick-codeblock, but know that this makes the document slower to
load, it is harder to format the prose, and the result is less legible. It also
doesn't increase portability, as it's easy to export markup as LaTeX files, or
as PDFs, with the formulas rendered.
The general idea, is that anything placed within `single backticks` is inline
code, and inline-code will all be scanned for typical mathematics statements and
rendered with best-effort. (For more precise rendering, use Asciidoc.) We will
not render text that is not marked as inline code, as mathematical formulas, as
that is prose.
If you want the TeX to be blended into the surrounding text, wrap the text
within single `$`. Otherwise, use double `$$` symbols, for display math, and it
will appear on its own line.
## TeX Examples
Inline equation: `$\sqrt{x}$`
Same equation, in the display mode: `$$\sqrt{x}$$`
Something more complex, inline: `$\mathbb{N} = \{ a \in \mathbb{Z} : a > 0 \}$`
Something complex, in display mode:
`$$P \left( A=2 \, \middle| \, \dfrac{A^2}{B}>4 \right)$$`
Another example of `$$\prod_{i=1}^{n} x_i - 1$$` inline formulas.
Function example: `$$ f(x)= \begin{cases} 1/d_{ij} & \quad \text{when
$d_{ij} \leq 160$}\\ 0 & \quad \text{otherwise} \end{cases}
$$ `
And a matrix: ` $$
M = \begin{bmatrix} \frac{5}{6} & \frac{1}{6} & 0 \\[0.3em] \frac{5}{6} & 0 &
\frac{1}{6} \\[0.3em] 0 & \frac{5}{6} & \frac{1}{6} \end{bmatrix}
$$ `
LaTeX ypesetting won't be rendered. Use NostrMarkup delimeter tables for this
sort of thing.
`\\begin{tabular}{|c|c|c|l|r|}
\\hline
\\multicolumn{3}{|l|}{test} & A & B \\\\
\\hline
1 & 2 & 3 & 4 & 5 \\\\
\\hline
\\end{tabular}`
We also recognize common LaTeX statements:
`\[
\begin{array}{ccccc}
1 & 2 & 3 & 4 & 5 \\
\end{array}
\]`
`\[ x^n + y^n = z^n \]`
`\sqrt{x^2+1}`
Greek letters are a snap: `$\Psi$`, `$\psi$`, `$\Phi$`, `$\phi$`.
Equations within text are easy--- A well known Maxwell thermodynamic relation is
`$\left.{\partial T \over \partial P}\right|_{s} = \left.{\partial v \over \partial s}\right|_{P}$`.
You can also set aside equations like so:
`\begin{eqnarray} du &=& T\ ds -P\ dv, \qquad \mbox{first law.}\label{fl}\\ ds &\ge& {\delta q \over T}.\qquad \qquad \mbox{second law.} \label{sl} \end {eqnarray}`
## And some good ole Asciimath
Asciimath doesn't use `$` or `$$` delimiters, but we are using it to make mathy
stuff easier to find. If you want it inline, include it inline. If you want it
on a separate line, put a hard-return before and after.
Inline text example here `$E=mc^2$` and another `$1/(x+1)$`; very simple.
Displaying on a separate line:
`$$sum_(k=1)^n k = 1+2+ cdots +n=(n(n+1))/2$$`
`$$int_0^1 x^2 dx$$`
`$$x = (-6 +- sqrt((-6)^2 - 4 (1)(4)))/(2 xx 1)$$`
`$$|x|= {(x , if x ge 0 text(,)),(-x , if x <0.):}$$`
Displaying with wider spacing:
`$a=3, \ \ \ b=-3,\ \ $` and `$ \ \ c=2$`.
Thus `$(a+b)(c+b)=0$`.
Displaying with indentations:
Using the quadratic formula, the roots of `$x^2-6x+4=0$` are
`$$x = (-6 +- sqrt((-6)^2 - 4 (1)(4)))/(2 xx 1)$$`
`$$ \ \ = (-6 +- sqrt(36 - 16))/2$$`
`$$ \ \ =(-6 +- sqrt(20))/2$$`
`$$ \ \ = -0.8 or 2.2 \ \ \ $$` to 1 decimal place.
Advanced alignment and matrices looks like this:
A `$3xx3$` matrix, `$$((1,2,3),(4,5,6),(7,8,9))$$` and a `$2xx1$` matrix, or
vector, `$$((1),(0))$$`.
The outer brackets determine the delimiters e.g. `$|(a,b),(c,d)|=ad-bc$`.
A general `$m xx n$` matrix
`$$((a_(11), cdots , a_(1n)),(vdots, ddots, vdots),(a_(m1), cdots , a_(mn)))$$`
## Mixed Examples
Here are some examples mixing LaTeX and AsciiMath:
- LaTeX inline: `$\frac{1}{2}$` vs AsciiMath inline: `$1/2$`
- LaTeX display: `$$\sum_{i=1}^n x_i$$` vs AsciiMath display:
`$$sum_(i=1)^n x_i$$`
- LaTeX matrix: `$$\begin{pmatrix} a & b \\ c & d \end{pmatrix}$$` vs AsciiMath
matrix: `$$((a,b),(c,d))$$`
## Edge Cases
- Empty math: `$$`
- Just delimiters: `$ $`
- Dollar signs in text: The price is $10.50
- Currency: `$19.99`
- Shell command: `echo "Price: $100"`
- JavaScript template: `const price = \`$${amount}\``
- CSS with dollar signs: `color: $primary-color`
This document should demonstrate that:
1. LaTeX is processed within inline code blocks with proper delimiters
2. AsciiMath is processed within inline code blocks with proper delimiters
3. Regular code blocks remain unchanged
4. Mixed content is handled correctly
5. Edge cases are handled gracefully $$

4178
test_output.log

File diff suppressed because it is too large Load Diff

83
tests/unit/latexRendering.test.ts

@ -1,83 +0,0 @@
import { describe, expect, it } from "vitest";
import { parseAdvancedmarkup } from "../../src/lib/utils/markup/advancedMarkupParser";
import { readFileSync } from "fs";
import { join } from "path";
describe("LaTeX and AsciiMath Rendering in Inline Code Blocks", () => {
const jsonPath = join(__dirname, "../../test_data/LaTeXtestfile.json");
const raw = readFileSync(jsonPath, "utf-8");
// Extract the markdown content field from the JSON event
const content = JSON.parse(raw).content;
it("renders LaTeX inline and display math correctly", async () => {
const html = await parseAdvancedmarkup(content);
// Test basic LaTeX examples from the test document
expect(html).toMatch(/<span class="math-inline">\$\\sqrt\{x\}\$<\/span>/);
expect(html).toMatch(/<div class="math-block">\$\$\\sqrt\{x\}\$\$<\/div>/);
expect(html).toMatch(
/<span class="math-inline">\$\\mathbb\{N\} = \\{ a \\in \\mathbb\{Z\} : a > 0 \\}\$<\/span>/,
);
expect(html).toMatch(
/<div class="math-block">\$\$P \\left\( A=2 \\, \\middle\| \\, \\dfrac\{A\^2\}\{B\}>4 \\right\)\$\$<\/div>/,
);
});
it("renders AsciiMath inline and display math correctly", async () => {
const html = await parseAdvancedmarkup(content);
// Test AsciiMath examples
expect(html).toMatch(/<span class="math-inline">\$E=mc\^2\$<\/span>/);
expect(html).toMatch(
/<div class="math-block">\$\$sum_\(k=1\)\^n k = 1\+2\+ cdots \+n=\(n\(n\+1\)\)\/2\$\$<\/div>/,
);
expect(html).toMatch(
/<div class="math-block">\$\$int_0\^1 x\^2 dx\$\$<\/div>/,
);
});
it("renders LaTeX array and matrix environments as math", async () => {
const html = await parseAdvancedmarkup(content);
// Test array and matrix environments
expect(html).toMatch(
/<div class="math-block">\$\$[\s\S]*\\begin\{array\}\{ccccc\}[\s\S]*\\end\{array\}[\s\S]*\$\$<\/div>/,
);
expect(html).toMatch(
/<div class="math-block">\$\$[\s\S]*\\begin\{bmatrix\}[\s\S]*\\end\{bmatrix\}[\s\S]*\$\$<\/div>/,
);
});
it("handles unsupported LaTeX environments gracefully", async () => {
const html = await parseAdvancedmarkup(content);
// Should show a message and plaintext for tabular
expect(html).toMatch(/<div class="unrendered-latex">/);
expect(html).toMatch(
/Unrendered, as it is LaTeX typesetting, not a formula:/,
);
expect(html).toMatch(/\\\\begin\{tabular\}/);
});
it("renders mixed LaTeX and AsciiMath correctly", async () => {
const html = await parseAdvancedmarkup(content);
// Test mixed content
expect(html).toMatch(
/<span class="math-inline">\$\\frac\{1\}\{2\}\$<\/span>/,
);
expect(html).toMatch(/<span class="math-inline">\$1\/2\$<\/span>/);
expect(html).toMatch(
/<div class="math-block">\$\$\\sum_\{i=1\}\^n x_i\$\$<\/div>/,
);
expect(html).toMatch(
/<div class="math-block">\$\$sum_\(i=1\)\^n x_i\$\$<\/div>/,
);
});
it("handles edge cases and regular code blocks", async () => {
const html = await parseAdvancedmarkup(content);
// Test regular code blocks (should remain as code, not math)
expect(html).toMatch(/<code[^>]*>\$19\.99<\/code>/);
expect(html).toMatch(/<code[^>]*>echo &quot;Price: \$100&quot;<\/code>/);
expect(html).toMatch(
/<code[^>]*>const price = \\`\$\$\{amount\}\\`<\/code>/,
);
expect(html).toMatch(/<code[^>]*>color: \$primary-color<\/code>/);
});
});

186
tests/unit/mathProcessing.test.ts

@ -0,0 +1,186 @@
import { describe, expect, it } from "vitest";
import { parseAdvancedmarkup } from "../../src/lib/utils/markup/advancedMarkupParser.ts";
describe("Math Processing in Advanced Markup Parser", () => {
it("should process inline math inside code blocks", async () => {
const input = "Here is some inline math: `$x^2 + y^2 = z^2$` in a sentence.";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-inline">\\(x^2 + y^2 = z^2\\)</span>');
expect(result).toContain("Here is some inline math:");
expect(result).toContain("in a sentence.");
});
it("should process display math inside code blocks", async () => {
const input = "Here is a display equation:\n\n`$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$`\n\nThis is after the equation.";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-display">\\[\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n\\]</span>');
expect(result).toContain('<p class="my-4">Here is a display equation:</p>');
expect(result).toContain('<p class="my-4">This is after the equation.</p>');
});
it("should process both inline and display math in the same code block", async () => {
const input = "Mixed math: `$\\alpha$ and $$\\beta = \\frac{1}{2}$$` in one block.";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-inline">\\(\\alpha\\)</span>');
expect(result).toContain('<span class="math-display">\\[\\beta = \\frac{1}{2}\\]</span>');
expect(result).toContain("Mixed math:");
expect(result).toContain("in one block.");
});
it("should NOT process math outside of code blocks", async () => {
const input = "This math $x^2 + y^2 = z^2$ should not be processed.";
const result = await parseAdvancedmarkup(input);
expect(result).toContain("$x^2 + y^2 = z^2$");
expect(result).not.toContain('<span class="math-inline">');
expect(result).not.toContain('<span class="math-display">');
});
it("should NOT process display math outside of code blocks", async () => {
const input = "This display math $$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$ should not be processed.";
const result = await parseAdvancedmarkup(input);
expect(result).toContain("$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$");
expect(result).not.toContain('<span class="math-inline">');
expect(result).not.toContain('<span class="math-display">');
});
it("should handle code blocks without math normally", async () => {
const input = "Here is some code: `console.log('hello world')` that should not be processed.";
const result = await parseAdvancedmarkup(input);
expect(result).toContain("`console.log('hello world')`");
expect(result).not.toContain('<span class="math-inline">');
expect(result).not.toContain('<span class="math-display">');
});
it("should handle complex math expressions with nested structures", async () => {
const input = "Complex math: `$$\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix} \\cdot \\begin{pmatrix} x \\\\ y \\end{pmatrix} = \\begin{pmatrix} ax + by \\\\ cx + dy \\end{pmatrix}$$`";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-display">');
expect(result).toContain("\\begin{pmatrix}");
expect(result).toContain("\\end{pmatrix}");
expect(result).toContain("\\cdot");
});
it("should handle inline math with special characters", async () => {
const input = "Special chars: `$\\alpha, \\beta, \\gamma, \\delta$` and `$\\sum_{i=1}^{n} x_i$`";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-inline">\\(\\alpha, \\beta, \\gamma, \\delta\\)</span>');
expect(result).toContain('<span class="math-inline">\\(\\sum_{i=1}^{n} x_i\\)</span>');
});
it("should handle multiple math expressions in separate code blocks", async () => {
const input = "First: `$E = mc^2$` and second: `$$F = G\\frac{m_1 m_2}{r^2}$$`";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-inline">\\(E = mc^2\\)</span>');
expect(result).toContain('<span class="math-display">\\[F = G\\frac{m_1 m_2}{r^2}\\]</span>');
});
it("should handle math expressions with line breaks in display mode", async () => {
const input = "Multi-line: `$$\n\\begin{align}\nx &= a + b \\\\\ny &= c + d\n\\end{align}\n$$`";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-display">');
expect(result).toContain("\\begin{align}");
expect(result).toContain("\\end{align}");
expect(result).toContain("x &= a + b");
expect(result).toContain("y &= c + d");
});
it("should handle edge case with empty math expressions", async () => {
const input = "Empty math: `$$` and `$`";
const result = await parseAdvancedmarkup(input);
// Should not crash and should preserve the original content
expect(result).toContain("`$$`");
expect(result).toContain("`$`");
});
it("should handle mixed content with regular text, code, and math", async () => {
const input = `This is a paragraph with regular text.
Here is some code: \`console.log('hello')\`
And here is math: \`$\\pi \\approx 3.14159$\`
And display math: \`$$\n\\int_0^1 x^2 dx = \\frac{1}{3}\n$$\`
And more regular text.`;
const result = await parseAdvancedmarkup(input);
// Should preserve regular text
expect(result).toContain("This is a paragraph with regular text.");
expect(result).toContain("And more regular text.");
// Should preserve regular code blocks
expect(result).toContain("`console.log('hello')`");
// Should process math
expect(result).toContain('<span class="math-inline">\\(\\pi \\approx 3.14159\\)</span>');
expect(result).toContain('<span class="math-display">');
expect(result).toContain("\\int_0^1 x^2 dx = \\frac{1}{3}");
});
it("should handle math expressions with dollar signs in the content", async () => {
const input = "Price math: `$\\text{Price} = \\$19.99$`";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-inline">');
expect(result).toContain("\\text{Price} = \\$19.99");
});
it("should handle display math with dollar signs in the content", async () => {
const input = "Price display: `$$\n\\text{Total} = \\$19.99 + \\$5.99 = \\$25.98\n$$`";
const result = await parseAdvancedmarkup(input);
expect(result).toContain('<span class="math-display">');
expect(result).toContain("\\text{Total} = \\$19.99 + \\$5.99 = \\$25.98");
});
it("should handle JSON content with escaped backslashes", async () => {
// Simulate content from JSON where backslashes are escaped
const jsonContent = "Math from JSON: `$\\\\alpha + \\\\beta = \\\\gamma$`";
const result = await parseAdvancedmarkup(jsonContent);
expect(result).toContain('<span class="math-inline">');
expect(result).toContain("\\\\alpha + \\\\beta = \\\\gamma");
});
it("should handle JSON content with escaped display math", async () => {
// Simulate content from JSON where backslashes are escaped
const jsonContent = "Display math from JSON: `$$\\\\int_0^1 x^2 dx = \\\\frac{1}{3}$$`";
const result = await parseAdvancedmarkup(jsonContent);
expect(result).toContain('<span class="math-display">');
expect(result).toContain("\\\\int_0^1 x^2 dx = \\\\frac{1}{3}");
});
it("should handle JSON content with escaped dollar signs", async () => {
// Simulate content from JSON where dollar signs are escaped
const jsonContent = "Price math from JSON: `$\\\\text{Price} = \\\\\\$19.99$`";
const result = await parseAdvancedmarkup(jsonContent);
expect(result).toContain('<span class="math-inline">');
expect(result).toContain("\\\\text{Price} = \\\\\\$19.99");
});
it("should handle complex JSON content with multiple escaped characters", async () => {
// Simulate complex content from JSON
const jsonContent = "Complex JSON math: `$$\\\\begin{pmatrix} a & b \\\\\\\\ c & d \\\\end{pmatrix} \\\\cdot \\\\begin{pmatrix} x \\\\\\\\ y \\\\end{pmatrix}$$`";
const result = await parseAdvancedmarkup(jsonContent);
expect(result).toContain('<span class="math-display">');
expect(result).toContain("\\\\begin{pmatrix}");
expect(result).toContain("\\\\end{pmatrix}");
expect(result).toContain("\\\\cdot");
expect(result).toContain("\\\\\\\\");
});
});

9
tests/unit/tagExpansion.test.ts

@ -74,11 +74,14 @@ vi.mock("../../src/lib/utils/profileCache", () => ({
batchFetchProfiles: vi.fn( batchFetchProfiles: vi.fn(
async ( async (
pubkeys: string[], pubkeys: string[],
onProgress: (fetched: number, total: number) => void, ndk: any,
onProgress?: (fetched: number, total: number) => void,
) => { ) => {
// Simulate progress updates // Simulate progress updates
onProgress(0, pubkeys.length); if (onProgress) {
onProgress(pubkeys.length, pubkeys.length); onProgress(0, pubkeys.length);
onProgress(pubkeys.length, pubkeys.length);
}
return []; return [];
}, },
), ),

Loading…
Cancel
Save