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-cli/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../dist/index.js');

View File

@@ -0,0 +1,20 @@
{
"name": "@morten-olsen/goodwrites-cli",
"packageManager": "yarn@3.1.0",
"bin": {
"goodwrites": "./bin/index.js"
},
"scripts": {
"cli": "./bin/index.js"
},
"dependencies": {
"@morten-olsen/goodwrites": "workspace:^",
"@morten-olsen/goodwrites-viewer": "workspace:^",
"commander": "^9.4.1",
"fs-extra": "^11.1.0",
"yaml": "^2.1.3"
},
"devDependencies": {
"@types/fs-extra": "^9.0.13"
}
}

View File

@@ -0,0 +1,31 @@
import { program } from 'commander';
import { resolve, dirname } from 'path';
import { createServer } from '@morten-olsen/goodwrites-viewer';
import { readFile } from 'fs-extra';
import yaml from 'yaml';
import { parseDocument } from '@morten-olsen/goodwrites';
const dev = program.command('dev <file>');
dev.action(async (file) => {
const fileLocation = resolve(process.cwd(), file);
const server = createServer({
dev: true,
documentLocation: resolve(fileLocation),
});
server.listen(4005);
});
const build = program.command('build <file>');
build.action(async (file) => {
const fileLocation = resolve(process.cwd(), file);
const location = dirname(fileLocation);
const content = await readFile(fileLocation, 'utf-8');
const document = yaml.parse(content);
const parsed = await parseDocument({
document,
location,
});
console.log(parsed.content);
});
program.parse(process.argv);

View File

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

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"
]
}

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

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

View File

@@ -0,0 +1,37 @@
{
"name": "@morten-olsen/goodwrites-viewer",
"main": "./dist/index.js",
"dependencies": {
"@fontsource/merriweather": "^4.5.14",
"@morten-olsen/goodwrites": "workspace:^",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"babel-loader": "^9.1.0",
"css-loader": "^6.7.2",
"express": "^4.18.2",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"raw-loader": "^4.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.4",
"react-pdf": "^6.2.0",
"react-refresh": "^0.14.0",
"style-loader": "^3.3.1",
"styled-components": "^5.3.6",
"webpack": "^5.75.0",
"webpack-dev-middleware": "^6.0.1",
"webpack-hot-middleware": "^2.25.3"
},
"devDependencies": {
"@types/express": "^4.17.14",
"@types/html-webpack-plugin": "^3.2.6",
"@types/loader-utils": "^2.0.3",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@types/react-pdf": "^5.7.4",
"@types/styled-components": "^5.1.26",
"@types/webpack": "^5.28.0",
"@types/webpack-dev-middleware": "^5.3.0",
"@types/webpack-hot-middleware": "^2.25.6"
}
}

View File

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

View File

@@ -0,0 +1,62 @@
import React, { FC } from 'react';
import '@fontsource/merriweather/index.css';
import styled, { createGlobalStyle } from 'styled-components';
import { Block } from './components/block';
import { useDoc } from './features/doc';
import { Document, Page } from 'react-pdf/dist/esm/entry.webpack5';
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
}
html {
font-size: 16px;
}
h1, h2, h3,h4, h5, h6 {
margin: 0;
}
html, body, body > #__next {
height: 100%;
}
body {
background: #fff;
width: 100%;
max-width: 800px;
margin: auto;
font-family: 'Merriweather', sans-serif;
}
a {
text-decoration: none;
color: #3498db;
}
`;
const Cover = styled.img`
max-width: 100%;
`;
const Title = styled.h1`
font-size: 3rem;
line-height: 3.8rem;
margin-bottom: 0px;
`;
const App: FC = () => {
const doc = useDoc();
return (
<div>
<GlobalStyle />
<Title>{doc.title}</Title>
<Cover src={doc.cover} />
<Document file={doc.pdf}>
<Page pageNumber={1} renderTextLayer={false} />
</Document>
{doc.parts.map((part, index) => (
<Block key={index} block={part} doc={doc} />
))}
<a href={(doc as any).pdf}>PDF {(doc as any).pdf}</a>
</div>
);
};
export { App };

View File

@@ -0,0 +1,105 @@
import React, { FC } from 'react';
import ReactMarkdown from 'react-markdown';
import styled from 'styled-components';
import { DocumentResult } from '@morten-olsen/goodwrites';
type Block = DocumentResult['parts'][0];
interface BlockProps {
block: Block;
doc: DocumentResult;
}
const getBreadcrumb = (block: Block, doc: DocumentResult) => {
const path = [...block.path];
const elements: any[] = [
{
title: '#',
},
];
path.reduce((acc, part) => {
const parent = acc[part as keyof typeof acc] as any;
elements.push({
...parent,
title: parent.title ?? part,
});
return parent.content;
}, doc.original.content as any);
return elements.map((s: any) => s.title).join(' > ');
};
const Wrapper = styled.div`
margin: 8px 0;
border-radius: 5px;
position: relative;
`;
const Note = styled.div`
background-color: #efefef;
text-align: center;
`;
const Content = styled.div<{
level: number;
border?: string;
}>`
letter-spacing: 0.5px;
line-height: 2.1rem;
border: solid 5px ${(props) => props.border || '#fff'};
padding: 8px 32px;
p:first-of-type::first-letter {
font-size: 6rem;
float: left;
padding: 1rem;
margin: 0px 2rem;
font-weight: 100;
margin-left: 0rem;
}
p + p::first-letter {
margin-left: 1.8rem;
}
`;
const Breacrumb = styled.div`
font-size: 10px;
text-align: center;
font-family: monospace;
text-transform: uppercase;
font-weight: bold;
opacity: 0.5;
`;
const getBorderColor = (state: Block['state']) => {
switch (state) {
case 'first-draft':
return '#3498db';
case 'final-draft':
return '#2ecc71';
case 'revisions':
return '#f1c40f';
case 'placeholder':
return '#e74c3c';
}
return '#fff';
};
const Block: FC<BlockProps> = ({ block, doc }) => {
return (
<Wrapper>
<Breacrumb>{getBreadcrumb(block, doc)}</Breacrumb>
{block.content && (
<Content level={1} border={getBorderColor(block.state)}>
<ReactMarkdown>{block.content}</ReactMarkdown>
</Content>
)}
{block.notes && <Note>{block.notes}</Note>}
</Wrapper>
);
};
export { Block };

View File

@@ -0,0 +1,49 @@
import React, {
createContext,
useContext,
FC,
useState,
useEffect,
ReactNode,
} from 'react';
import { DocumentResult } from '@morten-olsen/goodwrites';
type Document = DocumentResult;
declare var __DOC_LOCATION__: string;
type DocContextValue = {
doc: Document;
};
type DocProviderProps = {
children: ReactNode;
};
const DocContext = createContext<DocContextValue>(null as any);
const DocProvider: FC<DocProviderProps> = ({ children }) => {
const [doc, setDoc] = useState<Document>(
require(__DOC_LOCATION__) as Document
);
useEffect(() => {
const hot = (module as any).hot;
if (hot) {
hot.accept('../../../../demo-article/index.yml', () => {
const newDoc = require(__DOC_LOCATION__) as Document;
setDoc(newDoc);
});
}
}, []);
return <DocContext.Provider value={{ doc }}>{children}</DocContext.Provider>;
};
const useDoc = () => {
const { doc } = useContext(DocContext);
return doc;
};
export { DocProvider, DocContext, useDoc };

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './app';
import { DocProvider } from './features/doc';
const createRootElement = () => {
const rootContainer = document.createElement('div');
rootContainer.setAttribute('id', 'root');
document.body.appendChild(rootContainer);
return rootContainer;
};
const rootElement = document.getElementById('root') || createRootElement();
const Root = () => {
return (
<DocProvider>
<App />
</DocProvider>
);
};
createRoot(rootElement).render(<Root />);

View File

@@ -0,0 +1,82 @@
import express from 'express';
import webpack, { Configuration } from 'webpack';
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import { join } from 'path';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import hotMiddleware from 'webpack-hot-middleware';
import devMiddleware from 'webpack-dev-middleware';
type WebpackOptions = {
dev?: boolean;
documentLocation: string;
};
const createWebpack = ({
documentLocation,
dev = false,
}: WebpackOptions): Configuration => ({
mode: dev ? 'development' : 'production',
context: join(__dirname, 'ui'),
entry: ['webpack-hot-middleware/client', join(__dirname, 'ui', 'index.js')],
module: {
rules: [
{
test: documentLocation,
loader: require.resolve('@morten-olsen/goodwrites-webpack-loader'),
},
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
plugins: [require.resolve('react-refresh/babel')],
},
},
],
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.(png|jpe?g|gif)$/i,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/',
},
},
],
},
],
},
plugins: [
new HtmlWebpackPlugin({}),
new webpack.HotModuleReplacementPlugin(),
new webpack.DefinePlugin({
__DOC_LOCATION__: JSON.stringify(documentLocation),
}),
new ReactRefreshWebpackPlugin(),
],
});
const createServer = (options: WebpackOptions) => {
const server = express();
const webpackConfig = createWebpack(options);
const compiler = webpack(webpackConfig);
server.use(devMiddleware(compiler));
server.use(hotMiddleware(compiler));
return server;
};
export { createWebpack, createServer };

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
{
"name": "@morten-olsen/goodwrites-webpack-loader",
"main": "./dist/index.js",
"devDependencies": {
"@types/fs-extra": "^9.0.13",
"@types/marked": "^4.0.7",
"@types/webpack": "^5.28.0",
"loader-utils": "^3.2.1",
"yaml": "^2.1.3"
},
"dependencies": {
"@morten-olsen/goodwrites": "workspace:^",
"@morten-olsen/goodwrites-latex": "workspace:^",
"fs-extra": "^11.1.0",
"marked": "^4.0.12",
"node-latex": "^3.1.0",
"slugify": "^1.6.5",
"stream": "^0.0.2"
}
}

View File

@@ -0,0 +1,3 @@
import { webpackLoader } from './loader';
export default webpackLoader;

View File

@@ -0,0 +1,88 @@
import * as webpack from 'webpack';
import * as loaderUtils from 'loader-utils';
import {
replaceImages,
Document,
parseDocument,
} from '@morten-olsen/goodwrites';
import { join, dirname, basename } from 'path';
import yaml from 'yaml';
import { readFileSync } from 'fs-extra';
import { toPdf } from '@morten-olsen/goodwrites-latex';
import slugify from 'slugify';
type LoaderOptions = {
publicPath?: string;
outputPath?: string;
};
function webpackLoader(
this: webpack.LoaderContext<LoaderOptions>,
contents: string = ''
) {
const options: LoaderOptions = this.getOptions();
const publicPath =
options.publicPath ?? '';
const outputPath =
options.outputPath ?? this._compilation?.outputOptions.path ?? '';
const callback = this.async();
const source = this.resourcePath;
const run = async () => {
const document: Document = yaml.parse(contents);
const slug = slugify(document.title, { lower: true });
const addFile = (fileLocation: string) => {
const content = readFileSync(fileLocation);
const filename = basename(fileLocation);
const targetName = loaderUtils.interpolateName(
this as any,
`/goodwrite/${slug}/[hash]-${filename}`,
{
content,
}
);
const targetLocation = targetName;
this.emitFile(join(outputPath, targetLocation), content);
this.addDependency(fileLocation);
return join(publicPath, targetName);
};
const location = dirname(source);
const parsed = await parseDocument({
document,
location,
});
const markdown = await replaceImages(parsed, {
replaceImage: (image) => {
return addFile(image);
}
});
if (markdown.cover) {
markdown.cover = await addFile(join(location, markdown.cover));
}
const pdf = await toPdf(parsed);
const pdfName = loaderUtils.interpolateName(
this as any,
`/goodwrite/[hash]/${slug}.pdf`,
{
content: pdf,
}
);
this.emitFile(join(outputPath, pdfName), pdf);
return {
...markdown,
pdf: join(publicPath, pdfName),
};
};
run()
.then((content) => {
content.files.forEach((file) => (file ? this.addDependency(file) : null));
callback(null, `module.exports=${JSON.stringify(content)}`);
})
.catch((error) => {
callback(error);
});
}
export { webpackLoader };

View File

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

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

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

View File

@@ -0,0 +1,16 @@
{
"name": "@morten-olsen/goodwrites",
"packageManager": "yarn@3.1.0",
"main": "./dist/index.js",
"dependencies": {
"@sinclair/typebox": "^0.25.10",
"fs-extra": "^11.1.0",
"reading-time": "^1.5.0",
"remark": "^13",
"remark-behead": "^2.3.3",
"unist-util-visit": "^2.0.3"
},
"devDependencies": {
"@types/fs-extra": "^9.0.13"
}
}

3
packages/goodwrites/src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare module 'remark-behead' {
export default any;
}

View File

@@ -0,0 +1,2 @@
export * from './types';
export * from './parse';

View File

@@ -0,0 +1,213 @@
import { Block, Document, LengthTarget } from '../types';
import { resolve, dirname } from 'path';
import { readFile } from 'fs-extra';
import remark from 'remark';
import remarkBehead from 'remark-behead';
import visit from 'unist-util-visit';
import readingTime from 'reading-time';
type Options = {
depth?: number;
document: Document;
location: string;
};
type ParseBlockOptions = {
block: Block | Document;
location: string;
path: (string | number)[];
depth: number;
};
type ReplaceImageOptions = {
input: string;
cwd: string;
fn?: (location: string) => string;
};
type BlockResult = {
content: string;
type: 'title' | 'file' | 'meta';
file?: string;
stats?: ReturnType<typeof readingTime>;
state?: Exclude<Block, string>['state'];
path: (string | number)[];
length?: LengthTarget;
cwd: string;
level?: number;
notes?: string;
};
const replaceImagesAction = async ({ input, cwd, fn }: ReplaceImageOptions) => {
const walk = () => (tree: any) => {
visit(tree, 'image', (node: any) => {
if (!fn || !node.url.startsWith('./')) {
return;
}
const resolvedLocation = fn(resolve('/', cwd, node.url));
node.url = resolvedLocation;
});
};
const markdown = String(
await remark()
.use(walk as any)
.process(input)
);
return markdown;
};
const partsToArray = (parts: Block | Block[]) => {
if (Array.isArray(parts)) {
return parts;
}
return [parts];
};
const parseBlock = async ({
block,
location,
path,
depth,
}: ParseBlockOptions) => {
const result: BlockResult[] = [];
if (typeof block === 'string') {
block = {
file: block,
};
}
if ('file' in block) {
const contentDepth = block.title ? depth + 1 : depth;
const fileLocation = resolve(location, block.file);
const fileContent = await readFile(fileLocation, 'utf-8');
const markdown = String(
await remark()
.use(remarkBehead, { depth: contentDepth })
.process(fileContent)
);
const title = block.title
? ''.padStart(depth + 1, '#') + ' ' + block.title + '\n'
: '';
result.push({
content: `${title}${markdown}`,
file: fileLocation,
type: 'file',
path,
cwd: dirname(fileLocation),
level: depth,
length: block.lenght,
state: block.state,
stats: readingTime(markdown),
notes: block.notes,
});
} else if (block.title) {
result.push({
content: ''.padStart(depth + 1, '#') + ' ' + block.title,
type: 'title',
level: depth,
cwd: location,
state: 'state' in block ? block.state : undefined,
notes: 'notes' in block ? block.notes : undefined,
path,
});
depth += 1;
} else if ('notes' in block) {
result.push({
content: '',
type: 'meta',
level: depth,
cwd: location,
notes: 'notes' in block ? block.notes : undefined,
state: block.state,
path,
});
}
if ('content' in block) {
const sectionMarkdown = await Promise.all(
partsToArray(block.content).map((s, index) =>
parseBlock({
block: s,
path: [...path, index],
location,
depth,
})
)
);
result.push(...sectionMarkdown.flat());
}
return result;
};
const parseDocument = async ({ document, location, depth = 1 }: Options) => {
const parts = partsToArray(document.content);
const parsedParts = await Promise.all(
parts.map((block, index) =>
parseBlock({
block,
location,
depth,
path: [index],
})
)
);
const partsList = parsedParts.flat();
const content = partsList.map((p) => p.content).join('\n\n');
const stats = readingTime(content);
const files = partsList.map((p) => p.file).filter(Boolean);
return {
title: document.title,
content,
cover: document.cover,
cwd: location,
meta: document.meta,
stats,
files,
original: document,
parts: partsList,
};
};
type ToMarkdownOptions = {
replaceImage?: (location: string) => string;
};
const blockToMarkdown = async (
block: BlockResult,
{ replaceImage }: ToMarkdownOptions
) => {
if (replaceImage) {
return replaceImagesAction({
input: block.content,
fn: replaceImage,
cwd: block.cwd,
});
}
return block.content;
};
const replaceImages = async (
result: DocumentResult,
options: ToMarkdownOptions = {}
) => {
const blocks = await Promise.all(
result.parts.map(async (block) => ({
...block,
content: await blockToMarkdown(block, options),
}))
);
return {
...result,
parts: blocks,
content: blocks.map((b) => b.content).join('\n\n'),
};
};
type DocumentResult = ReturnType<typeof parseDocument> extends Promise<infer U>
? U & { pdf?: string }
: never;
export type { DocumentResult };
export { parseDocument, replaceImages };

View File

@@ -0,0 +1,37 @@
type TimeTarget = {
minutes: number;
seconds: number;
};
type WordTarget = {
words: number;
};
type LengthTarget = TimeTarget | WordTarget;
type BaseBlock = {
type?: string;
title?: string;
lenght?: LengthTarget;
notes?: string;
state?: 'placeholder' | 'first-draft' | 'revisions' | 'final-draft' | 'final';
};
type FileBlock = BaseBlock & {
file: string;
};
type BlocksBlock = BaseBlock & {
content: Block[];
};
type Block = string | FileBlock | BlocksBlock;
export type {
Block,
BlocksBlock,
FileBlock,
LengthTarget,
TimeTarget,
WordTarget,
};

View File

@@ -0,0 +1,10 @@
import { Block } from './block';
type Document = {
title: string;
cover?: string;
meta?: any;
content: Block | Block[];
};
export type { Document };

View File

@@ -0,0 +1,2 @@
export * from './block';
export * from './document';

View File

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

5
packages/markdown-loader/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,11 @@
{
"name": "@morten-olsen/markdown-loader",
"main": "./dist/index.js",
"devDependencies": {
"remark": "^13"
},
"dependencies": {
"front-matter": "^4.0.2",
"fs-extra": "^11.1.0"
}
}

View File

@@ -0,0 +1,3 @@
import { webpackLoader } from './loader';
export default webpackLoader;

View File

@@ -0,0 +1,27 @@
import * as webpack from 'webpack';
import fm from 'front-matter';
function webpackLoader(
this: webpack.LoaderContext<{}>,
contents: string = ''
) {
const callback = this.async();
const run = async () => {
const { attributes, body } = fm(contents);
return {
attributes,
body,
};
};
run()
.then((content) => {
callback(null, `module.exports=${JSON.stringify(content)}`);
})
.catch((error) => {
callback(error);
});
}
export { webpackLoader };

View File

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