Skip to content

Commit 8aa03e1

Browse files
authored
Merge pull request #430 from mgreminger/export-fixes
Document export fixes
2 parents 0e02e23 + 44f7e0d commit 8aa03e1

14 files changed

Lines changed: 252 additions & 20 deletions

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"plotly.js-basic-dist": "github:mgreminger/plotly.js#dist-basic-for-ep",
5353
"quick-lru": "^6.1.1",
5454
"quill": "^2.0.3",
55-
"quill-delta-to-markdown": "github:mgreminger/quill-delta-to-markdown#1319cfae28ef37574660d5ef715d9a5b14a01370",
55+
"quill-delta-to-markdown": "github:mgreminger/quill-delta-to-markdown#3f134c7af645b0f351103488d3cdd35ee09ff297",
5656
"workbox-window": "^6.5.4",
5757
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
5858
}

src/MathCell.svelte

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
isMathCellResult,
88
isRenderResult} from "./resultTypes";
99
import type { CodeFunctionQueryStatement, QueryStatement, SubQueryStatement } from "./parser/types";
10-
import { convertUnits, unitsValid } from "./utility";
10+
import { convertUnits, unitsValid, prepLatexForMarkdown } from "./utility";
1111
import type { MathCellConfig } from "./sheet/Sheet";
1212
import type MathCell from "./cells/MathCell.svelte";
1313
import PlotCell from "./cells/PlotCell.svelte";
@@ -60,6 +60,8 @@
6060
let renderElementHTML: HTMLElement = $state();
6161
6262
export function getMarkdown(centerEquations: boolean) {
63+
const sourceLatex = prepLatexForMarkdown(mathCell.mathField.latex);
64+
6365
if (!renderResult) {
6466
const queryStatement = Boolean(mathCell.mathField?.statement?.type === "query");
6567
let errorMessage = "";
@@ -73,9 +75,9 @@
7375
const result = queryStatement ? `${resultLatex} ${resultUnitsLatex}` : "";
7476
7577
if (centerEquations) {
76-
return `$$ ${mathCell.mathField.latex} ${result} ${errorMessage} $$\n\n`;
78+
return `$$ ${sourceLatex} ${result} ${errorMessage} $$\n\n`;
7779
} else {
78-
const latex = `${mathCell.mathField.latex} ${result} ${errorMessage}`.trim();
80+
const latex = `${sourceLatex} ${result} ${errorMessage}`.trim();
7981
if (latex) {
8082
return `$${latex}$ <!-- inline -->\n\n`;
8183
} else {
@@ -91,7 +93,7 @@
9193
return `\`\`\`\n${result.value}\n\`\`\`\n\n`;
9294
}
9395
} else {
94-
return `$${mathCell.mathField.latex.trim()} \\text{Render result not available at time of export}$\n\n`;
96+
return `$${sourceLatex} \\text{Render result not available at time of export}$\n\n`;
9597
}
9698
}
9799

src/PiecewiseCell.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import Add from "carbon-icons-svelte/lib/Add.svelte";
1414
import RowDelete from "carbon-icons-svelte/lib/RowDelete.svelte";
1515
import IconButton from "./IconButton.svelte";
16+
import { prepLatexForMarkdown } from "./utility";
1617
1718
interface Props {
1819
index: number;
@@ -37,13 +38,13 @@
3738
let numRows = $derived(piecewiseCell.expressionFields.length);
3839
3940
export function getMarkdown(centerEquations: boolean) {
40-
let result = `${piecewiseCell.parameterField.latex} = \\begin{cases} `;
41+
let result = `${prepLatexForMarkdown(piecewiseCell.parameterField.latex)} = \\begin{cases} `;
4142
4243
for (const [i, expression] of piecewiseCell.expressionFields.entries()) {
4344
if (i < piecewiseCell.conditionFields.length) {
44-
result += `${expression.latex} &: \\quad ${piecewiseCell.conditionFields[i].latex} \\\\ `;
45+
result += `${prepLatexForMarkdown(expression.latex)} &: \\quad ${prepLatexForMarkdown(piecewiseCell.conditionFields[i].latex)} \\\\ `;
4546
} else {
46-
result += `${expression.latex} &: \\quad \\text{otherwise} \\end{cases}`;
47+
result += `${prepLatexForMarkdown(expression.latex)} &: \\quad \\text{otherwise} \\end{cases}`;
4748
}
4849
}
4950

src/SystemCell.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import Add from "carbon-icons-svelte/lib/Add.svelte";
1414
import RowDelete from "carbon-icons-svelte/lib/RowDelete.svelte";
1515
import IconButton from "./IconButton.svelte";
16+
import { prepLatexForMarkdown } from "./utility";
1617
1718
interface Props {
1819
index: number;
@@ -54,7 +55,7 @@
5455
let result = `${startDelimiter}\\text{System} = \\begin{cases} `;
5556
5657
for (const [row, expression] of systemCell.expressionFields.entries()) {
57-
result += `${expression.latex} & `;
58+
result += `${prepLatexForMarkdown(expression.latex)} & `;
5859
if (row < systemCell.expressionFields.length - 1) {
5960
result += " \\\\ ";
6061
}

src/TableCell.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import ShowDataCards from "carbon-icons-svelte/lib/ShowDataCards.svelte";
2525
import Row from "carbon-icons-svelte/lib/Row.svelte";
2626
import IconButton from "./IconButton.svelte";
27+
import { prepLatexForMarkdown } from "./utility";
2728
2829
interface Props {
2930
index: number;
@@ -72,7 +73,7 @@
7273
7374
for (const [col, parameter] of tableCell.parameterFields.entries()) {
7475
if (tableCell.rhsFields[row][col].latex.replaceAll(/\\:?/g,'').trim() !== "") {
75-
columnExpressions.push(`${parameter.latex} & = \\quad ${tableCell.rhsFields[row][col].latex} ${tableCell.parameterUnitFields[col].latex}`);
76+
columnExpressions.push(`${prepLatexForMarkdown(parameter.latex)} & = \\quad ${prepLatexForMarkdown(tableCell.rhsFields[row][col].latex)} ${prepLatexForMarkdown(tableCell.parameterUnitFields[col].latex)}`);
7677
}
7778
}
7879

src/Updates.svelte

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@
2525
}
2626
</style>
2727

28+
<em>August 1, 2026 (Permalink: <a href="https://20260801.engineeringpaper.xyz" target="_blank">20260801.engineeringpaper.xyz</a>)</em>
29+
<h4>Document Export Improvements</h4>
30+
<p>
31+
Several PDF and DOCX document export issues have been fixed. Special characters in math field
32+
comments no longer cause math rendering issues in exported documents.
33+
Leading spaces in documentation cells no longer causes export issues.
34+
Nested numbered and bulleted lists are now exported properly. @ symbols in documentation cells
35+
no longer causes PDF export errors. Finally, data table column headers only include the portion
36+
before the equals sign when exported as a document to prevent overlapping column headers. For
37+
example, a calculated column of <span class="math">{String.raw`\(x=r+s+t\)`}</span>
38+
will use <span class="math">{String.raw`\(x\)`}</span> as the column header in the exported document.
39+
</p>
40+
<br>
41+
2842
<em>July 30, 2026 (Permalink: <a href="https://20260730.engineeringpaper.xyz" target="_blank">20260730.engineeringpaper.xyz</a>)</em>
2943
<h4>Document Export Improvements</h4>
3044
<p>

src/cells/DataTableCell.svelte.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { format } from "mathjs";
22
import { BaseCell, type DatabaseDataTableCell } from "./BaseCell";
33
import { MathField } from "./MathField.svelte";
44
import type { Statement, UnitsStatement } from "../parser/types";
5-
import { arraysEqual, getArraySI } from "../utility";
5+
import { arraysEqual, getArraySI, prepLatexForMarkdown } from "../utility";
66
import { convertLatexToAsciiMath } from "mathlive";
77
import type { NumberFormatOptions } from "../sheet/Sheet";
88

@@ -753,7 +753,13 @@ export default class DataTableCell extends BaseCell {
753753
}
754754

755755
async getSheetRows(forMarkdown = false, columnFormatOptions?: NumberFormatOptions[]): Promise<string[][]> {
756-
let headers = this.parameterFields.map(field => field.latex);
756+
let headers: string[]
757+
758+
if (forMarkdown) {
759+
headers = this.parameterFields.map(field => prepLatexForMarkdown(field.latex.split("=")[0]));
760+
} else {
761+
headers = this.parameterFields.map(field => field.latex);
762+
}
757763

758764
if (forMarkdown) {
759765
headers = headers.map(header => `$${header.trim()}$`);

src/stores.svelte.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ type AppState = {
6464
}
6565

6666
const appState: AppState = $state<AppState>({
67-
currentVersion: 20260730,
67+
currentVersion: 20260801,
6868
termsVersion: 20240110,
6969

7070
unsavedChange: false,

src/utility.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,86 @@ export function checkPyodideRuntime(fileVersion: number): RuntimeInfo {
328328
}
329329
}
330330
}
331+
332+
export function prepLatexForMarkdown(latex: string): string {
333+
// Dictionary mapping MathLive's text macros/escapes to Pandoc-safe equivalents
334+
const replacements: Record<string, string> = {
335+
'\\textbraceleft': '\\{{}',
336+
'\\textbraceright': '\\}{}',
337+
'\\lbrack': '[',
338+
'\\rbrack': ']',
339+
'\\!': '!',
340+
'\\%': '\\%{}',
341+
'\\&': '\\&{}',
342+
'\\textasciicircum': '\\textasciicircum{}',
343+
'\\#': '\\#{}',
344+
'\\$': '\\${}',
345+
'\\textasteriskcentered': '*',
346+
'\\textasciitilde': '\\textasciitilde{}',
347+
'\\_': '\\_{}',
348+
'\\textbackslash': '\\textbackslash{}'
349+
};
350+
351+
// Escape special regex characters in keys and join them for a single-pass replace
352+
const regexStr = Object.keys(replacements)
353+
.map(key => key.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'))
354+
.join('|');
355+
356+
const replacementRegex = new RegExp(regexStr, 'g');
357+
358+
let result = '';
359+
let currentIndex = 0;
360+
const textCommand = '\\text{';
361+
362+
while (currentIndex < latex.length) {
363+
// 1. Find the next \text{ command
364+
const textCommandIdx = latex.indexOf(textCommand, currentIndex);
365+
366+
// If no more \text{ commands exist, append the rest of the string and exit
367+
if (textCommandIdx === -1) {
368+
result += latex.slice(currentIndex);
369+
break;
370+
}
371+
372+
// Append everything before and including "\text{" to the result
373+
const contentBefore = latex.slice(currentIndex, textCommandIdx + textCommand.length);
374+
result += contentBefore;
375+
currentIndex = textCommandIdx + textCommand.length;
376+
377+
// 2. Find the matching closing brace '}' for this \text{ block
378+
let braceCount = 1;
379+
let searchIndex = currentIndex;
380+
381+
while (searchIndex < latex.length && braceCount > 0) {
382+
const char = latex[searchIndex];
383+
const prevChar = latex[searchIndex - 1];
384+
385+
// Only count braces that are not escaped
386+
if (char === '{' && prevChar !== '\\') {
387+
braceCount++;
388+
} else if (char === '}' && prevChar !== '\\') {
389+
braceCount--;
390+
}
391+
searchIndex++;
392+
}
393+
394+
// 3. Extract the inner text, apply replacements, and append
395+
if (braceCount === 0) {
396+
// Successfully found the bounds of the \text{} block
397+
const innerText = latex.slice(currentIndex, searchIndex - 1);
398+
399+
const sanitizedText = innerText.replace(replacementRegex, match => {
400+
return replacements[match];
401+
});
402+
403+
result += sanitizedText + '}';
404+
currentIndex = searchIndex;
405+
} else {
406+
// Fallback: If braces are unbalanced for some reason, just append and proceed
407+
result += latex.slice(currentIndex, searchIndex);
408+
currentIndex = searchIndex;
409+
}
410+
}
411+
412+
return result.trim();
413+
}

0 commit comments

Comments
 (0)