Skip to content

Commit

Permalink
tests passing 🎉
Browse files Browse the repository at this point in the history
  • Loading branch information
dmca-glasgow committed Jan 26, 2022
1 parent 0a69521 commit 587dda4
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 128 deletions.
11 changes: 6 additions & 5 deletions compiler/src/knitr/__test__/knitr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,23 +230,23 @@ describe('knitr', () => {
{ noSyntaxHighlight: false }
);
expect(md).toContain(
'beetles\\$propkilled <- beetles\\$killed / beetles\\$number'
'beetles$propkilled <- beetles$killed / beetles$number'
);
expect(ignoreWhitespace(html)).toContain(
ignoreWhitespace(`
<div class="code-wrapper">
<pre>
<code>
beetles
\\<span class="token operator">$</span>
<span class="token operator">$</span>
propkilled
<span class="token operator">&#x3C;-</span>
beetles
\\<span class="token operator">$</span>
<span class="token operator">$</span>
killed
<span class="token operator">/</span>
beetles
\\<span class="token operator">$</span>
<span class="token operator">$</span>
number
</code>
</pre>
Expand Down Expand Up @@ -310,7 +310,7 @@ describe('knitr', () => {
</div>
<div class="code-wrapper python-error-output">
<h6 class="console-heading">Python Console</h6>
<pre><code>Error in py_call_impl(callable, dots\\$args, dots\\$keywords): NameError: name 'a' is not defined
<pre><code>Error in py_call_impl(callable, dots$args, dots$keywords): NameError: name 'a' is not defined
Detailed traceback:
File "&#x3C;string>", line 1, in &#x3C;module></code></pre>
</div>
Expand All @@ -329,6 +329,7 @@ describe('knitr', () => {
<h6 class="console-heading">R Console</h6>
<pre><code>Error in eval(expr, envir, enclos): object 'b' not found</code></pre>
</div>
<p></p>
`);

expect(ignoreWhitespace(html)).toBe(result);
Expand Down
16 changes: 16 additions & 0 deletions compiler/src/latex/__test__/latex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ import {
} from '../../test-utils/test-processor';

describe('latex', () => {
it('should render inline latex', async () => {
const { md, html } = await testProcessor(String.raw`
\begin{align} a=b \label{eqn:chainrule} \end{align}
Here is an example of $Y$ inline tex
`);

const expectedMd = unindentStringAndTrim(`
:blockMath[0]
Here is an example of :inlineMath[1] inline tex
`);

expect(md).toBe(expectedMd);
});

it('should add references correctly', async () => {
const id = 'eqn:chainrule';

Expand Down
50 changes: 50 additions & 0 deletions compiler/src/latex/directive-to-tex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// import { Literal } from 'hast';
// import { Parent, Root } from 'mdast';
// import { visit } from 'unist-util-visit';

// import { Context } from '../context';

// export function aliasDirectiveToTex(ctx: Context) {
// return (tree: Root) => {
// visit(tree, 'textDirective', (node) => {
// switch (node.name) {
// case 'inlineMath': {
// const tex = getStoredTex(node, ctx);
// node.data = getTexSpan(tex, 'inline');
// break;
// }
// case 'blockMath': {
// const tex = getStoredTex(node, ctx);
// node.data = getTexSpan(tex, 'block');
// break;
// }
// }
// });
// };
// }

// function getStoredTex(node: Parent, ctx: Context) {
// if (!ctx.mmlStore) {
// throw new Error(`[tex-directive-to-text]: no ctx.mmlStore`);
// }
// const idx = getTexIdx(node);
// return ctx.mmlStore[idx];
// }

// function getTexIdx(node: Parent) {
// const firstChild = node.children[0] as Literal;
// return Number(firstChild.value || 0);
// }

// function getTexSpan(tex: string, display: 'inline' | 'block') {
// return {
// hName: 'span',
// hProperties: { className: ['tex'] },
// hChildren: [
// {
// type: 'text',
// value: display === 'inline' ? `$${tex}$` : `$$\n${tex}\n$$`,
// },
// ],
// };
// }
189 changes: 66 additions & 123 deletions compiler/src/latex/tex-to-directive.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
LiteAdaptor,
liteAdaptor,
} from 'mathjax-full/js/adaptors/liteAdaptor.js';
import { liteAdaptor } from 'mathjax-full/js/adaptors/liteAdaptor.js';
import { MathDocument } from 'mathjax-full/js/core/MathDocument.js';
import * as MathItem from 'mathjax-full/js/core/MathItem.js';
import { SerializedMmlVisitor } from 'mathjax-full/js/core/MmlTree/SerializedMmlVisitor.js';
Expand All @@ -13,7 +10,6 @@ import { VFile } from 'vfile';

import { Context } from '../context';
import { assertNoTexTabular } from '../linter/assert-no-tex-tabular';
// import { assertNoTexTabular } from '../linter/assert-no-tex-tabular';
import { failMessage } from '../utils/message';

// This custom MathJax implementation has had to diverge from the provided demos found
Expand All @@ -33,40 +29,15 @@ import { failMessage } from '../utils/message';

// I convert the TeX to MathML and store it memory for use later (in directive-to-svg.ts).

type ExtractedMath = {
start: number;
end: number;
mml: string;
tex: string;
display: boolean;
};

export function texToAliasDirective(file: VFile, ctx: Context) {
// simple regex tests
assertNoTexTabular(file);

const md = file.value as string;

const store = buildMmlStore(md);
const result = replaceTexWithPlaceholder(md, store, file);

// add store to ctx
ctx.mmlStore = store.map((o) => o.mml);

// replace md in VFile
file.value = postParse(result);

return file;
}

// This is based on https://github.com/mathjax/MathJax-demos-node/blob/f70342b69533dbc24b460f6d6ef341dfa7856414/direct/tex2mml-page
// except I don't return the HTML, instead I compile a list of the extracted LaTeX converted to MathML
function buildMmlStore(md: string) {
const store: ExtractedMath[] = [];

const store: string[] = [];
const adaptor = liteAdaptor();
RegisterHTMLHandler(adaptor);
const visitor = new SerializedMmlVisitor();
RegisterHTMLHandler(adaptor);

const doc = mathjax.document(md, {
InputJax: new TeX({
Expand All @@ -84,99 +55,65 @@ function buildMmlStore(md: string) {
[`\\[`, `\\]`],
],
}),
renderActions: {
typeset: [MathItem.STATE.TYPESET, storeMml(adaptor, visitor, store)],
},
// wrap verbatim latex with <div class="mathjax-ignore"></div>
ignoreHtmlClass: 'mathjax-ignore',
renderActions: {
typeset: [
MathItem.STATE.TYPESET,
({ math }: MathDocument<any, any, any>) => {
for (const item of Array.from(math)) {
let newMarkdown = '';

// convert to MathML
const mml = visitor.visitTree(item.root);
assertNoMmlError(mml, file);

// escaped dollar sign...
if (item.math === '$') {
newMarkdown = '$';
}

// double backslash...
else if (item.math === '\\') {
newMarkdown = '\\\\';
}

// reference link...
else if (isReferenceLink(item.math)) {
const refNum = extractRefNumFromMml(mml, item.math, file);
const anchor = extractAnchorLinkFromMml(
mml,
item.math,
file
);
newMarkdown = `[${refNum}](${anchor})`;
}

// normal use case (equation)...
else {
store.push(mml);
const type = item.display ? 'blockMath' : 'inlineMath';
newMarkdown = `:${type}[${store.length - 1}]`;
}

const tree = adaptor.parse(newMarkdown, 'text/html');
item.typesetRoot = adaptor.firstChild(adaptor.body(tree));
}
},
],
},
});

doc.render();

return store;
}

function storeMml(
adaptor: LiteAdaptor,
visitor: SerializedMmlVisitor,
store: ExtractedMath[]
) {
return ({ math }: MathDocument<any, any, any>) => {
for (const item of Array.from(math)) {
if (item.start.n === undefined) {
throw new Error('start is undefined');
}
if (item.end.n === undefined) {
throw new Error('end is undefined');
}

// MathJax appears to pick this up in error...
if (item.math !== '$') {
store.push({
start: item.start.n,
end: item.end.n,
tex: item.math,
display: item.display,
// convert to MML
mml: visitor.visitTree(item.root),
});
}

// this is only necessary for the MathJax "typeset"
// renderAction to complete without error
const tree = adaptor.parse('**unused**', 'text/html');
item.typesetRoot = adaptor.firstChild(adaptor.body(tree));
}
};
}

function replaceTexWithPlaceholder(
md: string,
store: ExtractedMath[],
file: VFile
) {
// Replace it with a placeholder for use later
return store
.map((item, idx) => ({ ...item, idx }))
.reverse()
.reduce((acc, item) => {
const placeholder = createPlaceholder(item, file);
const prev = acc.slice(0, item.start);
const next = acc.slice(item.end);
return prev + placeholder + next;
}, md);
}

type ExtractedMathWithIdx = ExtractedMath & {
idx: number;
};

function createPlaceholder(item: ExtractedMathWithIdx, file: VFile) {
// escaped dollar sign...
if (item.tex === '$') {
return '$';
}

// double backslash...
if (item.tex === '\\') {
return '\\\\';
}

assertNoMmlError(item.mml, file);
// add store to ctx
ctx.mmlStore = store;

// debug
// console.log(item);
doc.render();

// reference link...
if (isReferenceLink(item.tex)) {
const refNum = extractRefNumFromMml(item, file);
const anchor = extractAnchorLinkFromMml(item, file);
return `[${refNum}](${anchor})`;
}
// replace md in VFile
const result = adaptor.innerHTML(adaptor.body(doc.document));
file.value = postParse(result);

// normal use case (equation)...
const type = item.display ? 'blockMath' : 'inlineMath';
return `:${type}[${item.idx}]`;
return file;
}

function assertNoMmlError(mml: string, file: VFile) {
Expand All @@ -190,7 +127,7 @@ function isReferenceLink(tex: string) {
return /^\\ref\{(.+)\}$/.test(tex);
}

function extractRefNumFromMml({ mml, tex }: ExtractedMath, file: VFile) {
function extractRefNumFromMml(mml: string, tex: string, file: VFile) {
const match = mml.match(/<mtext>(.+)<\/mtext>/);
if (match === null) {
failMessage(file, `Invalid reference: ${tex}`);
Expand All @@ -205,10 +142,7 @@ function extractRefNumFromMml({ mml, tex }: ExtractedMath, file: VFile) {
return match[1] as string;
}

function extractAnchorLinkFromMml(
{ mml, tex }: ExtractedMath,
file: VFile
) {
function extractAnchorLinkFromMml(mml: string, tex: string, file: VFile) {
const match = mml.match(/<mrow href="(.+)" class="MathJax_ref">/);
if (match === null) {
failMessage(file, `Reference has no anchor link: ${tex}`);
Expand All @@ -219,10 +153,19 @@ function extractAnchorLinkFromMml(

function postParse(html: string) {
let result = html;
result = unprotectHtml(result);
result = removeUnresolvedLabels(result);
return result;
}

// https://github.com/mathjax/MathJax-src/blob/41565a97529c8de57cb170e6a67baf311e61de13/ts/adaptors/lite/Parser.ts#L399-L403
function unprotectHtml(html: string) {
return html
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>');
}

function removeUnresolvedLabels(html: string) {
return html.replace(/\\label{def:.*?}/gm, '');
}
2 changes: 2 additions & 0 deletions compiler/src/mdast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { VFile } from 'vfile';

import { Context } from '../context';
import { aliasDirectiveToSvg } from '../latex/directive-to-svg';
// import { aliasDirectiveToTex } from '../latex/directive-to-tex';
import { createSvg } from '../utils/icons';
import { codeBlocks } from './code-blocks';
import { embedAssetUrl } from './embed-asset-url';
Expand Down Expand Up @@ -38,6 +39,7 @@ export async function mdastPhase(file: VFile, ctx: Context) {
.use(embedAssetUrl)
.use(youtubeVideos)
.use(aliasDirectiveToSvg, ctx)
// .use(aliasDirectiveToTex, ctx)
.use(codeBlocks, ctx)
.use(images, ctx)
.use(pagebreaks);
Expand Down

0 comments on commit 587dda4

Please sign in to comment.