This commit is contained in:
Morten Olsen
2023-03-28 08:10:46 +02:00
parent 9b1a067d56
commit 7adf03c83f
44 changed files with 1780 additions and 411 deletions

31
bin/build/data.ts Normal file
View File

@@ -0,0 +1,31 @@
import { Config } from '../../types/config';
import { Bundler } from '../bundler';
import { createArticles } from '../data/articles';
import { createPositions } from '../data/positions';
import { createProfile } from '../data/profile';
type GetDataOptions = {
cwd: string;
config: Config;
bundler: Bundler;
};
const getData = ({ cwd, config, bundler }: GetDataOptions) => ({
articles: createArticles({
cwd,
pattern: config.articles.pattern,
bundler,
}),
positions: createPositions({
cwd,
pattern: config.positions.pattern,
bundler,
}),
profile: createProfile({
cwd,
path: config.profile.path,
bundler,
}),
});
export { getData };

View File

@@ -1,47 +1,33 @@
import { resolve } from "path";
import { createReact } from "../resources/react";
import { Observable, getCollectionItems } from "../observable";
import { createPage } from "../resources/page";
import { createArticles } from "../data/articles";
import { Bundler } from "../bundler";
import { forEach } from "../utils/observable";
import { createEjs } from "../resources/ejs";
import { createLatex } from "../resources/latex";
import { markdownToLatex } from "../utils/markdown";
import { createPositions } from "../data/positions";
import { createProfile } from "../data/profile";
import { Position } from "../../types";
import { resolve } from 'path';
import { Observable, getCollectionItems } from '../observable';
import { createPage } from '../resources/page';
import { Bundler } from '../bundler';
import { forEach } from '../utils/observable';
import { createLatex } from '../resources/latex';
import { markdownToLatex } from '../utils/markdown';
import { Position } from '../../types';
import { Config } from '../../types/config';
import { getTemplates } from './templates';
import { getData } from './data';
const build = async () => {
const build = async (cwd: string, config: Config) => {
const bundler = new Bundler();
const articles = createArticles({
bundler,
});
const positions = createPositions({
bundler,
});
const profile = createProfile({
const data = getData({
cwd,
config,
bundler,
});
const templates = getTemplates(cwd, config);
const latex = {
article: createEjs(resolve("content/templates/latex/article.tex")),
resume: createEjs(resolve("content/templates/latex/resume.tex")),
};
const react = {
article: createReact(resolve("content/templates/react/article.tsx")),
frontpage: createReact(resolve("content/templates/react/frontpage.tsx")),
};
const resumeProps = Observable.combine({
articles: articles.pipe(getCollectionItems),
positions: positions.pipe(async (positions) => {
const resumeData = Observable.combine({
articles: data.articles.pipe(getCollectionItems),
// TODO: collection observer
positions: data.positions.pipe(async (positions) => {
const result: Position[] = [];
for (const a of positions) {
const item = await a.data;
const content = markdownToLatex({
root: resolve("content"),
root: resolve('content'),
content: item.raw,
});
result.push({
@@ -51,38 +37,43 @@ const build = async () => {
}
return result;
}),
profile,
profile: data.profile,
});
resumeData.subscribe(() => {
console.log('resume');
});
const resumeUrl = createLatex({
bundler,
path: "/resume",
data: resumeProps,
template: latex.resume,
path: '/resume',
data: resumeData,
template: templates.latex.resume,
});
{
const props = Observable.combine({
articles: articles.pipe(getCollectionItems),
positions: positions.pipe(getCollectionItems),
profile,
resumeUrl: new Observable(async () => resumeUrl),
articles: data.articles.pipe(getCollectionItems),
positions: data.positions.pipe(getCollectionItems),
profile: data.profile,
resumeUrl: Observable.link([resumeUrl.item], async () => resumeUrl.url),
});
createPage({
path: "/",
path: '/',
props,
template: react.frontpage,
template: templates.react.frontpage,
bundler,
});
}
await forEach(articles, async (article) => {
await forEach(data.articles, async (article) => {
const { slug } = await article.data;
const pdfUrl = createLatex({
const pdf = createLatex({
bundler,
path: resolve("/articles", slug),
template: latex.article,
path: resolve('/articles', slug),
template: templates.latex.article,
data: Observable.combine({
profile: data.profile,
article: article.pipe(async ({ title, cover, root, raw }) => {
const body = markdownToLatex({
root,
@@ -98,14 +89,16 @@ const build = async () => {
});
const props = Observable.combine({
article,
profile,
pdfUrl: new Observable(async () => pdfUrl),
resumeUrl: new Observable(async () => resumeUrl),
profile: data.profile,
pdfUrl: Observable.link([pdf.item], async () => pdf.url),
});
article.subscribe(() => {
console.log('updated', slug);
});
createPage({
path: `/articles/${slug}`,
props,
template: react.article,
template: templates.react.article,
bundler,
});
});

17
bin/build/templates.ts Normal file
View File

@@ -0,0 +1,17 @@
import { resolve } from 'path';
import { Config } from '../../types/config';
import { createEjs } from '../resources/ejs';
import { createReact } from '../resources/react';
const getTemplates = (cwd: string, config: Config) => ({
latex: {
article: createEjs(resolve(cwd, config.articles.latex.template)),
resume: createEjs(resolve(cwd, config.resume.latex.template)),
},
react: {
article: createReact(resolve(cwd, config.articles.react.template)),
frontpage: createReact(resolve(cwd, config.frontpage.react.template)),
},
});
export { getTemplates };

View File

@@ -1,5 +1,5 @@
import { resolve } from "path";
import { Observable } from "../observable";
import { resolve } from 'path';
import { Observable } from '../observable';
type Asset = {
content: string | Buffer;
@@ -17,7 +17,7 @@ class Bundler {
}
public register = (path: string, asset: Observable<Asset>) => {
const realPath = resolve("/", path);
const realPath = resolve('/', path);
if (!this.#assets.has(realPath)) {
this.#assets.set(realPath, asset);
}
@@ -25,7 +25,7 @@ class Bundler {
};
public get = (path: string) => {
const realPath = resolve("/", path);
const realPath = resolve('/', path);
return this.#assets.get(realPath);
};
}

View File

@@ -1,19 +1,22 @@
import { createGlob } from "../../resources/glob";
import { createFile } from "../../resources/file";
import grayMatter from "gray-matter";
import { Article } from "../../../types/article";
import { Bundler } from "../../bundler";
import { markdownBundleImages } from "../../utils/markdown";
import { dirname, resolve } from "path";
import { createImage } from "../../resources/image";
import { createGlob } from '../../resources/glob';
import { createFile } from '../../resources/file';
import grayMatter from 'gray-matter';
import { Article } from '../../../types/article';
import { Bundler } from '../../bundler';
import { markdownBundleImages } from '../../utils/markdown';
import { dirname, resolve } from 'path';
import { createImage } from '../../resources/image';
type ArticleOptions = {
cwd: string;
pattern: string;
bundler: Bundler;
};
const createArticles = ({ bundler }: ArticleOptions) => {
const createArticles = ({ bundler, cwd, pattern }: ArticleOptions) => {
const files = createGlob({
pattern: "content/articles/**/*.md",
pattern,
cwd,
create: (path) => {
const file = createFile({ path });
const article = file.pipe(async (raw) => {
@@ -27,12 +30,12 @@ const createArticles = ({ bundler }: ArticleOptions) => {
});
const coverUrl = createImage({
image: resolve(cwd, cover),
format: "avif",
format: 'avif',
bundler,
});
const thumbUrl = createImage({
image: resolve(cwd, cover),
format: "avif",
format: 'avif',
width: 400,
bundler,
});

View File

@@ -1,20 +1,24 @@
import { createGlob } from "../../resources/glob";
import { createFile } from "../../resources/file";
import grayMatter from "gray-matter";
import { Bundler } from "../../bundler";
import { markdownBundleImages } from "../../utils/markdown";
import { dirname } from "path";
import { Position } from "../../../types";
import { Observable } from "../../observable";
import { createGlob } from '../../resources/glob';
import { createFile } from '../../resources/file';
import grayMatter from 'gray-matter';
import { Bundler } from '../../bundler';
import { markdownBundleImages } from '../../utils/markdown';
import { dirname } from 'path';
import { Position } from '../../../types';
import { Observable } from '../../observable';
type PositionOptions = {
cwd: string;
pattern: string;
bundler: Bundler;
};
const createPositions = ({ bundler }: PositionOptions) => {
const createPositions = ({ cwd, pattern, bundler }: PositionOptions) => {
const files = createGlob<Observable<Position>>({
pattern: "content/resume/positions/**/*.md",
pattern,
cwd,
create: (path) => {
console.log('c', path);
const file = createFile({ path });
const position = file.pipe(async (raw) => {
const { data, content } = grayMatter(raw);

View File

@@ -1,27 +1,29 @@
import { resolve } from "path";
import { createFile } from "../../resources/file";
import YAML from "yaml";
import { Bundler } from "../../bundler";
import { Profile } from "../../../types";
import { createImage } from "../../resources/image";
import { resolve } from 'path';
import { createFile } from '../../resources/file';
import YAML from 'yaml';
import { Bundler } from '../../bundler';
import { Profile } from '../../../types';
import { createImage } from '../../resources/image';
type ProfileOptions = {
path: string;
cwd: string;
bundler: Bundler;
};
const createProfile = ({ bundler }: ProfileOptions) => {
const createProfile = ({ cwd, path, bundler }: ProfileOptions) => {
const file = createFile({
path: resolve("content/profile.yml"),
path: resolve(cwd, path),
});
const profile = file.pipe(async (yaml) => {
const data = YAML.parse(yaml);
const imagePath = resolve("content", data.image);
const imagePath = resolve('content', data.image);
const result: Profile = {
...data,
imageUrl: createImage({
image: imagePath,
format: "avif",
format: 'webp',
bundler,
}),
imagePath,

View File

@@ -1,6 +1,6 @@
import express, { Express } from "express";
import { Bundler } from "../bundler";
import { extname } from "path";
import express, { Express } from 'express';
import { Bundler } from '../bundler';
import { extname } from 'path';
const createServer = (bundler: Bundler): Express => {
const app = express();
@@ -8,35 +8,40 @@ const createServer = (bundler: Bundler): Express => {
let path = req.path;
let asset = bundler.get(path);
if (!asset) {
path = path.endsWith("/") ? path + "index.html" : path + "/index.html";
path = path.endsWith('/') ? path + 'index.html' : path + '/index.html';
asset = bundler.get(path);
}
if (asset) {
const ext = extname(path);
asset.data.then((data) => {
if (ext === ".html") {
const unsubscribe = asset!.subscribe(async () => {
await asset?.data;
unsubscribe();
res.end(`<script>window.location.reload()</script>`);
});
res.on("close", unsubscribe);
res.on("finish", unsubscribe);
res.on("error", unsubscribe);
res.writeHead(200, {
"content-type": "text/html;charset=utf-8",
"Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
Expires: "0",
"keep-alive": "timeout=5, max=100",
});
res.write(data.content.toString().replace("</html>", ""));
} else {
res.send(data.content);
}
});
asset.data
.then((data) => {
if (ext === '.html') {
const unsubscribe = asset!.subscribe(async () => {
await asset?.data;
unsubscribe();
res.end('<script>window.location.reload()</script>');
});
res.on('close', unsubscribe);
res.on('finish', unsubscribe);
res.on('error', unsubscribe);
res.writeHead(200, {
'content-type': 'text/html;charset=utf-8',
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0',
'keep-alive': 'timeout=5, max=100',
});
res.write(data.content.toString().replace('</html>', ''));
} else {
res.send(data.content);
}
})
.catch((err) => {
console.error(err);
res.status(500).send(err.message);
});
} else {
res.status(404).send("Not found");
res.status(404).send('Not found');
}
});

View File

@@ -1,21 +1,35 @@
import { program } from "commander";
import { build } from "./build";
import { createServer } from "./dev/server";
import { dirname, join, resolve } from "path";
import { mkdir, rm, writeFile } from "fs/promises";
import { existsSync } from "fs";
import { program } from 'commander';
import { build } from './build';
import { createServer } from './dev/server';
import { dirname, join, resolve } from 'path';
import { mkdir, rm, writeFile } from 'fs/promises';
import { existsSync } from 'fs';
const dev = program.command("dev");
dev.action(async () => {
const bundler = await build();
const getConfig = (path: string) => {
const resolved = resolve(path);
const module = require(resolved);
const config = module.default || module;
return {
cwd: dirname(resolved),
config,
};
};
const dev = program.command('dev');
dev.argument('<config>', 'Path to config file');
dev.action(async (configLocation) => {
const { cwd, config } = getConfig(configLocation);
const bundler = await build(cwd, config);
const server = createServer(bundler);
server.listen(3000);
});
const bundle = program.command("build");
bundle.action(async () => {
const bundler = await build();
const outputDir = resolve("out");
const bundle = program.command('build');
bundle.argument('<config>', 'Path to config file');
bundle.action(async (configLocation) => {
const { cwd, config } = getConfig(configLocation);
const bundler = await build(cwd, config);
const outputDir = resolve('out');
if (existsSync(outputDir)) {
rm(outputDir, { recursive: true });
}

View File

@@ -1,15 +1,15 @@
import { Observable } from "./observable";
import { getCollectionItems } from "./utils";
import { Observable } from './observable';
import { getCollectionItems } from './utils';
describe("observable", () => {
it("should be able to create an observable", async () => {
describe('observable', () => {
it('should be able to create an observable', async () => {
const observable = new Observable(() => Promise.resolve(1));
expect(observable).toBeDefined();
const data = await observable.data;
expect(data).toBe(1);
});
it("should be able to combine observables", async () => {
it('should be able to combine observables', async () => {
const observable1 = new Observable(() => Promise.resolve(1));
const observable2 = new Observable(() => Promise.resolve(2));
const combined = Observable.combine({ observable1, observable2 });
@@ -18,7 +18,7 @@ describe("observable", () => {
expect(data.observable2).toBe(2);
});
it("should be able to update observable", async () => {
it('should be able to update observable', async () => {
const observable = new Observable(() => Promise.resolve(1));
const data = await observable.data;
expect(data).toBe(1);
@@ -27,20 +27,20 @@ describe("observable", () => {
expect(data2).toBe(2);
});
it("should be able to extract collection items", async () => {
it('should be able to extract collection items', async () => {
const observable = new Observable(() =>
Promise.resolve([
new Observable(() => Promise.resolve(1)),
new Observable(() => Promise.resolve(2)),
new Observable(() => Promise.resolve(3)),
])
]),
);
const flatten = observable.pipe(getCollectionItems);
const data = await flatten.data;
expect(data).toEqual([1, 2, 3]);
});
it("should update observable when subscribed", async () => {
it('should update observable when subscribed', async () => {
const observable = new Observable(() => Promise.resolve(1));
const spy = jest.fn();
observable.subscribe(spy);
@@ -50,7 +50,7 @@ describe("observable", () => {
expect(spy).toHaveBeenCalledTimes(1);
});
it("should update combined observable when subscribed", async () => {
it('should update combined observable when subscribed', async () => {
const observable1 = new Observable(() => Promise.resolve(1));
const observable2 = new Observable(() => Promise.resolve(2));
const combined = Observable.combine({ observable1, observable2 });

View File

@@ -1,2 +1,2 @@
export { Observable } from "./observable";
export { getCollectionItems } from "./utils";
export { Observable } from './observable';
export { getCollectionItems } from './utils';

View File

@@ -58,7 +58,7 @@ class Observable<T> {
};
static combine = <U extends Record<string, Observable<any>>>(
record: U
record: U,
): Observable<ObservableRecord<U>> => {
const loader = () =>
Object.entries(record).reduce(
@@ -66,7 +66,7 @@ class Observable<T> {
...(await accP),
[key]: await value.data,
}),
{} as any
{} as any,
);
const observable = new Observable<ObservableRecord<U>>(loader);
Object.values(record).forEach((item) => {
@@ -76,6 +76,16 @@ class Observable<T> {
});
return observable;
};
static link = <T>(observables: Observable<any>[], generate: () => Promise<T>) => {
const observable = new Observable<T>(generate);
observables.forEach((item) => {
item.subscribe(() => {
observable.recreate();
});
});
return observable;
};
}
export { Observable };

View File

@@ -1,4 +1,4 @@
import { Observable } from "./observable";
import { Observable } from './observable';
const getCollectionItems = async <T>(items: Observable<T>[]) => {
return Promise.all(items.map((item) => item.data));

View File

@@ -1,5 +1,5 @@
import { createFile } from "../file";
import ejs from "ejs";
import { createFile } from '../file';
import ejs from 'ejs';
const createEjs = (path: string) => {
const file = createFile({ path });

View File

@@ -1,16 +1,26 @@
import { readFile } from "fs/promises";
import { Observable } from "../../observable";
import { watch } from "fs";
import { readFile } from 'fs/promises';
import { Observable } from '../../observable';
import { watch } from 'fs';
type FileOptions = {
path: string;
};
const createFile = ({ path }: FileOptions) => {
const file = new Observable(async () => readFile(path, "utf-8"));
let watcher: ReturnType<typeof watch> | undefined;
const addWatcher = () => {
if (watcher) {
watcher.close();
}
watcher = watch(path, () => {
file.recreate();
addWatcher();
});
};
watch(path, () => {
file.recreate();
const file = new Observable(async () => {
addWatcher();
return readFile(path, 'utf-8');
});
return file;

View File

@@ -1,28 +1,25 @@
import fastGlob from "fast-glob";
import watchGlob from "glob-watcher";
import { Observable } from "../../observable";
import fastGlob from 'fast-glob';
import watchGlob from 'glob-watcher';
import { Observable } from '../../observable';
import { resolve } from 'path';
type GlobOptions<T> = {
cwd?: string;
cwd: string;
pattern: string;
create?: (path: string) => T;
};
const defaultCreate = (a: any) => a;
const createGlob = <T = string>({
cwd,
pattern,
create = defaultCreate,
}: GlobOptions<T>) => {
const createGlob = <T = string>({ cwd, pattern, create = defaultCreate }: GlobOptions<T>) => {
const glob = new Observable(async () => {
const files = await fastGlob(pattern, { cwd });
return files.map(create);
return files.map((path) => create(resolve(cwd, path)));
});
const watcher = watchGlob(pattern, { cwd });
watcher.on("add", (path) => {
glob.set((current) => Promise.resolve([...(current || []), create(path)]));
watcher.on('add', (path) => {
glob.set((current) => Promise.resolve([...(current || []), create(resolve(cwd, path))]));
return glob;
});

View File

@@ -1,7 +1,7 @@
import { createHash } from "crypto";
import { Asset, Bundler } from "../../bundler";
import { Observable } from "../../observable";
import sharp, { FormatEnum } from "sharp";
import { createHash } from 'crypto';
import { Asset, Bundler } from '../../bundler';
import { Observable } from '../../observable';
import sharp, { FormatEnum } from 'sharp';
type ImageOptions = {
format: keyof FormatEnum;
@@ -13,8 +13,7 @@ type ImageOptions = {
};
const createImage = (options: ImageOptions) => {
let path =
options.name || createHash("sha256").update(options.image).digest("hex");
let path = options.name || createHash('sha256').update(options.image).digest('hex');
if (options.width) {
path += `-w${options.width}`;
}

View File

@@ -1,7 +1,7 @@
import { Asset, Bundler } from "../../bundler";
import { Observable } from "../../observable";
import { createEjs } from "../ejs";
import { latexToPdf } from "./utils";
import { Asset, Bundler } from '../../bundler';
import { Observable } from '../../observable';
import { createEjs } from '../ejs';
import { latexToPdf } from './utils';
type LatexOptions = {
path: string;
@@ -23,7 +23,11 @@ const createLatex = ({ template, data, path, bundler }: LatexOptions) => {
};
return asset;
});
return bundler.register(`${path}.pdf`, pdf);
const url = bundler.register(`${path}.pdf`, pdf);
return {
url,
item: pdf,
};
};
export { createLatex };

View File

@@ -1,5 +1,5 @@
import latex from "node-latex";
import { Readable } from "stream";
import latex from 'node-latex';
import { Readable } from 'stream';
const latexToPdf = (doc: string) =>
new Promise<Buffer>((resolve, reject) => {
@@ -8,14 +8,14 @@ const latexToPdf = (doc: string) =>
input.push(doc);
input.push(null);
const latexStream = latex(input);
latexStream.on("data", (chunk) => {
latexStream.on('data', (chunk) => {
chunks.push(Buffer.from(chunk));
});
latexStream.on("finish", () => {
latexStream.on('finish', () => {
const result = Buffer.concat(chunks);
resolve(result);
});
latexStream.on("error", (err) => {
latexStream.on('error', (err) => {
reject(err);
});
});

View File

@@ -1,10 +1,10 @@
import React, { ComponentType } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { HelmetProvider, FilledContext } from "react-helmet-async";
import { Asset, Bundler } from "../../bundler";
import { Observable } from "../../observable";
import { ServerStyleSheet } from "styled-components";
import { resolve } from "path";
import React, { ComponentType } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { HelmetProvider, FilledContext } from 'react-helmet-async';
import { Asset, Bundler } from '../../bundler';
import { Observable } from '../../observable';
import { ServerStyleSheet } from 'styled-components';
import { resolve } from 'path';
type PageOptions = {
path: string;
@@ -24,8 +24,8 @@ const createPage = (options: PageOptions) => {
React.createElement(
HelmetProvider,
{ context: helmetContext },
React.createElement(template, props)
)
React.createElement(template, props),
),
);
const bodyHtml = renderToStaticMarkup(body);
const { helmet } = helmetContext;
@@ -40,7 +40,7 @@ const createPage = (options: PageOptions) => {
helmet.script?.toString(),
]
.filter(Boolean)
.join("");
.join('');
const html = `<!DOCTYPE html>
<html lang="en">
<head>
@@ -55,7 +55,7 @@ const createPage = (options: PageOptions) => {
return asset;
});
const path = resolve("/", options.path, "index.html");
const path = resolve('/', options.path, 'index.html');
return options.bundler.register(path, page);
};

View File

@@ -1,27 +1,27 @@
import vm from "vm";
import React, { ComponentType } from "react";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
import replace from "@rollup/plugin-replace";
import sucrase from "@rollup/plugin-sucrase";
import alias from "@rollup/plugin-alias";
import externalGlobals from "rollup-plugin-external-globals";
import { createScript } from "../script";
import orgStyled from "styled-components";
import * as styledExports from "styled-components";
import ReactHelmetAsync from "react-helmet-async";
import { resolve } from "path";
import vm from 'vm';
import React, { ComponentType } from 'react';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import replace from '@rollup/plugin-replace';
import sucrase from '@rollup/plugin-sucrase';
import alias from '@rollup/plugin-alias';
import externalGlobals from 'rollup-plugin-external-globals';
import { createScript } from '../script';
import orgStyled from 'styled-components';
import * as styledExports from 'styled-components';
import ReactHelmetAsync from 'react-helmet-async';
import { resolve } from 'path';
const styled = orgStyled.bind(null);
for (let key of Object.keys(orgStyled)) {
if (key === "default") {
if (key === 'default') {
continue;
}
(styled as any)[key] = (orgStyled as any)[key];
}
for (let key of Object.keys(styledExports)) {
if (key === "default") {
if (key === 'default') {
continue;
}
(styled as any)[key] = (styledExports as any)[key];
@@ -30,34 +30,32 @@ for (let key of Object.keys(styledExports)) {
const createReact = <TProps = any>(path: string) => {
const script = createScript({
path,
format: "cjs",
format: 'cjs',
plugins: [
replace({
preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify("production"),
'process.env.NODE_ENV': JSON.stringify('production'),
}),
alias({
entries: [
{ find: "@", replacement: resolve("content/templates/react") },
],
entries: [{ find: '@', replacement: resolve('content/templates/react') }],
}),
sucrase({
exclude: ["node_modules/**"],
transforms: ["jsx", "typescript"],
exclude: ['node_modules/**'],
transforms: ['jsx', 'typescript'],
}),
nodeResolve({
browser: true,
preferBuiltins: false,
extensions: [".js", ".ts", ".tsx"],
extensions: ['.js', '.ts', '.tsx'],
}),
json(),
commonjs({
include: /node_modules/,
}),
externalGlobals({
react: "React",
"styled-components": "StyledComponents",
"react-helmet-async": "ReactHelmetAsync",
react: 'React',
'styled-components': 'StyledComponents',
'react-helmet-async': 'ReactHelmetAsync',
}),
],
});

View File

@@ -1,5 +1,5 @@
import { Observable } from "../../observable";
import { InputPluginOption, ModuleFormat, watch } from "rollup";
import { Observable } from '../../observable';
import { InputPluginOption, ModuleFormat, watch } from 'rollup';
type ScriptOptions = {
path: string;
@@ -13,7 +13,7 @@ const build = (options: ScriptOptions, update: (code: string) => void) =>
const watcher = watch({
input: options.path,
plugins: options.plugins,
onwarn: () => { },
onwarn: () => {},
output: {
format: options.format,
},
@@ -22,8 +22,8 @@ const build = (options: ScriptOptions, update: (code: string) => void) =>
},
});
watcher.on("event", async (event) => {
if (event.code === "BUNDLE_END") {
watcher.on('event', async (event) => {
if (event.code === 'BUNDLE_END') {
const { output } = await event.result.generate({
format: options.format,
});
@@ -35,7 +35,7 @@ const build = (options: ScriptOptions, update: (code: string) => void) =>
update(code);
}
}
if (event.code === "ERROR") {
if (event.code === 'ERROR') {
reject(event.error);
}
});
@@ -43,7 +43,7 @@ const build = (options: ScriptOptions, update: (code: string) => void) =>
const createScript = (options: ScriptOptions) => {
const script: Observable<string> = new Observable(() =>
build(options, (code) => script.set(() => Promise.resolve(code)))
build(options, (code) => script.set(() => Promise.resolve(code))),
);
return script;

View File

@@ -1,11 +1,11 @@
import { resolve } from "path";
import { decode } from "html-entities";
import { marked } from "marked";
import remark from "remark";
import visit from "unist-util-visit";
import { Bundler } from "../../bundler";
import { createImage } from "../../resources/image";
import { renderer } from "./latex";
import { resolve } from 'path';
import { decode } from 'html-entities';
import { marked } from 'marked';
import remark from 'remark';
import visit from 'unist-util-visit';
import { Bundler } from '../../bundler';
import { createImage } from '../../resources/image';
import { renderer } from './latex';
type MarkdownBundleImagesOptions = {
cwd: string;
@@ -13,15 +13,11 @@ type MarkdownBundleImagesOptions = {
bundler: Bundler;
};
const markdownBundleImages = async ({
bundler,
cwd,
content,
}: MarkdownBundleImagesOptions) => {
const markdownBundleImages = async ({ bundler, cwd, content }: MarkdownBundleImagesOptions) => {
const result = await remark()
.use(() => (tree) => {
visit(tree, "image", (node) => {
if (!("url" in node)) {
visit(tree, 'image', (node) => {
if (!('url' in node)) {
return;
}
const url = node.url as string;
@@ -29,7 +25,7 @@ const markdownBundleImages = async ({
const image = createImage({
image: path,
bundler,
format: "webp",
format: 'avif',
});
const newUrl = image;
node.url = newUrl;
@@ -46,7 +42,7 @@ type MarkdownToLatexOptions = {
const markdownToLatex = ({ root, content }: MarkdownToLatexOptions) => {
const render: any = {
...renderer(0),
...renderer(0, root),
};
const latex = marked(content, {
renderer: render,

View File

@@ -1,19 +1,20 @@
import { decode } from "html-entities";
import { existsSync } from "fs";
import { decode } from 'html-entities';
import { existsSync } from 'fs';
import { resolve } from 'path';
const latexTypes = ["", "section", "subsection", "paragraph", "subparagraph"];
const latexTypes = ['', 'section', 'subsection', 'paragraph', 'subparagraph'];
const sanitize = (text?: string) => {
if (!text) {
return "";
return '';
}
return decode(text)
.replace("&", "\\&")
.replace("_", "\\_")
.replace(/([^\\])\}/g, "$1\\}")
.replace(/([^\\])\{/g, "$1\\{")
.replace(/[^\\]\[/g, "\\[")
.replace(/#/g, "\\#");
.replace('&', '\\&')
.replace('_', '\\_')
.replace(/([^\\])\}/g, '$1\\}')
.replace(/([^\\])\{/g, '$1\\{')
.replace(/[^\\]\[/g, '\\[')
.replace(/#/g, '\\#');
};
type Renderer = (depth: number) => {
@@ -30,7 +31,7 @@ type Renderer = (depth: number) => {
image?: (link: string) => string;
};
const renderer = (outerDepth: number) => ({
const renderer = (outerDepth: number, cwd: string) => ({
heading: (text: string, depth: number) => {
return `\\${latexTypes[outerDepth + depth]}{${sanitize(text)}}\n\n`;
},
@@ -76,13 +77,12 @@ const renderer = (outerDepth: number) => ({
return `\\texttt{${sanitize(code)}}`;
},
image: (link: string) => {
if (!existsSync(link)) {
return "Online image not supported";
const path = resolve(cwd, link);
if (!existsSync(path)) {
return `Online image not supported ${path}`;
}
return `\\begin{figure}[h!]
\\includegraphics[width=0.5\\textwidth]{${link}}
\\centering
\\end{figure}
return `
\\noindent\\includegraphics[width=\\linewidth]{${path}}
`;
},
});

View File

@@ -1,14 +1,10 @@
import { Observable } from "../../observable";
import { Observable } from '../../observable';
const forEach = async <T extends Observable<any[]>>(
observable: T,
fn: (
value: T extends Observable<infer U>
? U extends Array<infer A>
? A
: never
: never
) => Promise<void>
value: T extends Observable<infer U> ? (U extends Array<infer A> ? A : never) : never,
) => Promise<void>,
) => {
const knownValues = new Set();
const update = async () => {