mirror of
https://github.com/morten-olsen/morten-olsen.github.io.git
synced 2026-02-08 01:46:28 +01:00
feat: init
This commit is contained in:
5
packages/goodwrites-latex/.gitignore
vendored
Normal file
5
packages/goodwrites-latex/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
/node_modules/
|
||||
/*.logs
|
||||
/.yarn/
|
||||
/dist/
|
||||
14
packages/goodwrites-latex/package.json
Normal file
14
packages/goodwrites-latex/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
packages/goodwrites-latex/src/index.ts
Normal file
1
packages/goodwrites-latex/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './parser';
|
||||
67
packages/goodwrites-latex/src/parser/index.ts
Normal file
67
packages/goodwrites-latex/src/parser/index.ts
Normal 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 };
|
||||
118
packages/goodwrites-latex/src/parser/utils.ts
Normal file
118
packages/goodwrites-latex/src/parser/utils.ts
Normal 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 };
|
||||
9
packages/goodwrites-latex/tsconfig.json
Normal file
9
packages/goodwrites-latex/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"./src"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user