feat: init

This commit is contained in:
Morten Olsen
2022-12-06 09:12:53 +01:00
commit 3f5e941446
115 changed files with 13148 additions and 0 deletions

5
packages/goodwrites-latex/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/node_modules/
/*.logs
/.yarn/
/dist/

View File

@@ -0,0 +1,14 @@
{
"name": "@morten-olsen/goodwrites-latex",
"main": "./dist/index.js",
"devDependencies": {
"@types/marked": "^4.0.7"
},
"dependencies": {
"@morten-olsen/goodwrites": "workspace:^",
"html-entities": "^2.3.3",
"marked": "^4.0.12",
"node-latex": "^3.1.0",
"stream": "^0.0.2"
}
}

View File

@@ -0,0 +1 @@
export * from './parser';

View File

@@ -0,0 +1,67 @@
import { DocumentResult, replaceImages } from '@morten-olsen/goodwrites';
import { marked } from 'marked';
import { resolve } from 'path';
import { latexToPdf, renderer, Renderer, sanitize } from './utils';
type WrapOptions = {
body: string;
cover: {
title: string;
image?: string;
};
};
type Parser = {
wrap?: (options: WrapOptions) => string;
renderer?: Renderer;
};
const defaultWrap = (options: WrapOptions) => {
return `
\\documentclass{article}
\\usepackage{graphicx}
\\usepackage{hyperref}
\\title{${sanitize(options.cover.title)}}
\\begin{document}
\\maketitle
${
options.cover.image
? `\\includegraphics[width=\\textwidth]{${options.cover.image}}`
: ''
}
${options.body}
\\end{document}
`;
};
const toLatex = async (document: DocumentResult, parser?: Parser) => {
const { content } = await replaceImages(document, {
replaceImage: (a) => a,
});
const wrap = parser?.wrap || defaultWrap;
const baseRender = renderer(0);
const render = {
...baseRender,
...(parser?.renderer?.(0) || {}),
};
marked.use({ renderer: render });
const markdown = marked.parse(content);
const wrapped = wrap({
body: markdown,
cover: {
title: document.title,
image: document.cover ? resolve(document.cwd, document.cover) : undefined,
},
});
return wrapped;
};
const toPdf = async (document: DocumentResult, parser?: Parser) => {
const latex = await toLatex(document, parser);
const pdf = await latexToPdf(latex);
return pdf;
};
export type { Parser, WrapOptions };
export { toLatex, toPdf };

View File

@@ -0,0 +1,118 @@
import { decode } from 'html-entities';
import latex from 'node-latex';
import { Readable } from 'stream';
import { existsSync } from 'fs';
const sanitize = (text?: string) => {
if (!text) {
return '';
}
return decode(text)
.replace('&', '\\&')
.replace('_', '\\_')
.replace(/([^\\])\}/g, '$1\\}')
.replace(/([^\\])\{/g, '$1\\{')
.replace(/[^\\]\[/g, '\\[')
.replace(/#/g, '\\#');
};
const sanitizeUrl = (text?: string) => {
if (!text) {
return '';
}
return decode(text)
.replace(/\//g, '\\/');
};
const latexTypes = ['', '', 'section', 'subsection', 'paragraph', 'subparagraph'];
type Renderer = (depth: number) => {
heading?: (text: string, depth: number) => string;
code?: (input: string) => string;
text?: (input: string) => string;
paragraph?: (input: string) => string;
list?: (input: string) => string;
listitem?: (input: string) => string;
link?: (href: string, text: string) => string;
strong?: (text: string) => string;
em?: (text: string) => string;
codespan?: (code: string) => string;
image?: (link: string) => string;
};
const renderer = (outerDepth: number) => ({
heading: (text: string, depth: number) => {
return `\\${latexTypes[outerDepth + depth]}{${sanitize(text)}}\n\n`;
},
code: (input: string) => {
return `
\\begin{lstlisting}
${input}
\\end{lstlisting}
`;
},
text: (input: string) => {
return sanitize(input);
},
paragraph: (input: string) => {
return `${input}\n\n`;
},
list: (input: string) => {
return `
\\begin{itemize}
${input}
\\end{itemize}
`;
},
listitem: (input: string) => {
return `\\item{${input}}`;
},
link: (href: string, text: string) => {
console.log('LINK TEXT', text, sanitize(href));
if (!text || text === href) {
return `\\url{${sanitize(href)}}`;
}
return `${sanitize(text)} (\\url{${sanitize(href)}})`;
},
strong: (text: string) => {
return `\\textbf{${sanitize(text)}}`;
},
em: (text: string) => {
return `\\textbf{${sanitize(text)}}`;
},
codespan: (code: string) => {
return `\\texttt{${sanitize(code)}}`;
},
image: (link: string) => {
if (!existsSync(link)) {
return 'Online image not supported';
}
return `\\begin{figure}[h!]
\\includegraphics[width=0.5\\textwidth]{${link}}
\\centering
\\end{figure}
`;
},
});
const latexToPdf = (doc: string) =>
new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
const input = new Readable();
input.push(doc);
input.push(null);
const latexStream = latex(input);
latexStream.on('data', (chunk) => {
chunks.push(Buffer.from(chunk));
});
latexStream.on('finish', () => {
const result = Buffer.concat(chunks);
resolve(result);
});
latexStream.on('error', (err) => {
reject(err);
});
});
export type { Renderer };
export { sanitize, renderer, latexToPdf };

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": [
"./src"
]
}