mirror of
https://github.com/morten-olsen/morten-olsen.github.io.git
synced 2026-02-08 01:46:28 +01:00
cleanup
This commit is contained in:
12
.eslintrc
Normal file
12
.eslintrc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "@react-native-community",
|
||||||
|
"rules": {
|
||||||
|
"react/react-in-jsx-scope": 0,
|
||||||
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
14
.prettierrc.json
Normal file
14
.prettierrc.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"printWidth": 100,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
31
bin/build/data.ts
Normal file
31
bin/build/data.ts
Normal 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 };
|
||||||
@@ -1,47 +1,33 @@
|
|||||||
import { resolve } from "path";
|
import { resolve } from 'path';
|
||||||
import { createReact } from "../resources/react";
|
import { Observable, getCollectionItems } from '../observable';
|
||||||
import { Observable, getCollectionItems } from "../observable";
|
import { createPage } from '../resources/page';
|
||||||
import { createPage } from "../resources/page";
|
import { Bundler } from '../bundler';
|
||||||
import { createArticles } from "../data/articles";
|
import { forEach } from '../utils/observable';
|
||||||
import { Bundler } from "../bundler";
|
import { createLatex } from '../resources/latex';
|
||||||
import { forEach } from "../utils/observable";
|
import { markdownToLatex } from '../utils/markdown';
|
||||||
import { createEjs } from "../resources/ejs";
|
import { Position } from '../../types';
|
||||||
import { createLatex } from "../resources/latex";
|
import { Config } from '../../types/config';
|
||||||
import { markdownToLatex } from "../utils/markdown";
|
import { getTemplates } from './templates';
|
||||||
import { createPositions } from "../data/positions";
|
import { getData } from './data';
|
||||||
import { createProfile } from "../data/profile";
|
|
||||||
import { Position } from "../../types";
|
|
||||||
|
|
||||||
const build = async () => {
|
const build = async (cwd: string, config: Config) => {
|
||||||
const bundler = new Bundler();
|
const bundler = new Bundler();
|
||||||
const articles = createArticles({
|
const data = getData({
|
||||||
bundler,
|
cwd,
|
||||||
});
|
config,
|
||||||
const positions = createPositions({
|
|
||||||
bundler,
|
|
||||||
});
|
|
||||||
const profile = createProfile({
|
|
||||||
bundler,
|
bundler,
|
||||||
});
|
});
|
||||||
|
const templates = getTemplates(cwd, config);
|
||||||
|
|
||||||
const latex = {
|
const resumeData = Observable.combine({
|
||||||
article: createEjs(resolve("content/templates/latex/article.tex")),
|
articles: data.articles.pipe(getCollectionItems),
|
||||||
resume: createEjs(resolve("content/templates/latex/resume.tex")),
|
// TODO: collection observer
|
||||||
};
|
positions: data.positions.pipe(async (positions) => {
|
||||||
|
|
||||||
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 result: Position[] = [];
|
const result: Position[] = [];
|
||||||
for (const a of positions) {
|
for (const a of positions) {
|
||||||
const item = await a.data;
|
const item = await a.data;
|
||||||
const content = markdownToLatex({
|
const content = markdownToLatex({
|
||||||
root: resolve("content"),
|
root: resolve('content'),
|
||||||
content: item.raw,
|
content: item.raw,
|
||||||
});
|
});
|
||||||
result.push({
|
result.push({
|
||||||
@@ -51,38 +37,43 @@ const build = async () => {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
profile,
|
profile: data.profile,
|
||||||
|
});
|
||||||
|
|
||||||
|
resumeData.subscribe(() => {
|
||||||
|
console.log('resume');
|
||||||
});
|
});
|
||||||
|
|
||||||
const resumeUrl = createLatex({
|
const resumeUrl = createLatex({
|
||||||
bundler,
|
bundler,
|
||||||
path: "/resume",
|
path: '/resume',
|
||||||
data: resumeProps,
|
data: resumeData,
|
||||||
template: latex.resume,
|
template: templates.latex.resume,
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
const props = Observable.combine({
|
const props = Observable.combine({
|
||||||
articles: articles.pipe(getCollectionItems),
|
articles: data.articles.pipe(getCollectionItems),
|
||||||
positions: positions.pipe(getCollectionItems),
|
positions: data.positions.pipe(getCollectionItems),
|
||||||
profile,
|
profile: data.profile,
|
||||||
resumeUrl: new Observable(async () => resumeUrl),
|
resumeUrl: Observable.link([resumeUrl.item], async () => resumeUrl.url),
|
||||||
});
|
});
|
||||||
createPage({
|
createPage({
|
||||||
path: "/",
|
path: '/',
|
||||||
props,
|
props,
|
||||||
template: react.frontpage,
|
template: templates.react.frontpage,
|
||||||
bundler,
|
bundler,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await forEach(articles, async (article) => {
|
await forEach(data.articles, async (article) => {
|
||||||
const { slug } = await article.data;
|
const { slug } = await article.data;
|
||||||
const pdfUrl = createLatex({
|
const pdf = createLatex({
|
||||||
bundler,
|
bundler,
|
||||||
path: resolve("/articles", slug),
|
path: resolve('/articles', slug),
|
||||||
template: latex.article,
|
template: templates.latex.article,
|
||||||
data: Observable.combine({
|
data: Observable.combine({
|
||||||
|
profile: data.profile,
|
||||||
article: article.pipe(async ({ title, cover, root, raw }) => {
|
article: article.pipe(async ({ title, cover, root, raw }) => {
|
||||||
const body = markdownToLatex({
|
const body = markdownToLatex({
|
||||||
root,
|
root,
|
||||||
@@ -98,14 +89,16 @@ const build = async () => {
|
|||||||
});
|
});
|
||||||
const props = Observable.combine({
|
const props = Observable.combine({
|
||||||
article,
|
article,
|
||||||
profile,
|
profile: data.profile,
|
||||||
pdfUrl: new Observable(async () => pdfUrl),
|
pdfUrl: Observable.link([pdf.item], async () => pdf.url),
|
||||||
resumeUrl: new Observable(async () => resumeUrl),
|
});
|
||||||
|
article.subscribe(() => {
|
||||||
|
console.log('updated', slug);
|
||||||
});
|
});
|
||||||
createPage({
|
createPage({
|
||||||
path: `/articles/${slug}`,
|
path: `/articles/${slug}`,
|
||||||
props,
|
props,
|
||||||
template: react.article,
|
template: templates.react.article,
|
||||||
bundler,
|
bundler,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
17
bin/build/templates.ts
Normal file
17
bin/build/templates.ts
Normal 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 };
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { resolve } from "path";
|
import { resolve } from 'path';
|
||||||
import { Observable } from "../observable";
|
import { Observable } from '../observable';
|
||||||
|
|
||||||
type Asset = {
|
type Asset = {
|
||||||
content: string | Buffer;
|
content: string | Buffer;
|
||||||
@@ -17,7 +17,7 @@ class Bundler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public register = (path: string, asset: Observable<Asset>) => {
|
public register = (path: string, asset: Observable<Asset>) => {
|
||||||
const realPath = resolve("/", path);
|
const realPath = resolve('/', path);
|
||||||
if (!this.#assets.has(realPath)) {
|
if (!this.#assets.has(realPath)) {
|
||||||
this.#assets.set(realPath, asset);
|
this.#assets.set(realPath, asset);
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ class Bundler {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public get = (path: string) => {
|
public get = (path: string) => {
|
||||||
const realPath = resolve("/", path);
|
const realPath = resolve('/', path);
|
||||||
return this.#assets.get(realPath);
|
return this.#assets.get(realPath);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import { createGlob } from "../../resources/glob";
|
import { createGlob } from '../../resources/glob';
|
||||||
import { createFile } from "../../resources/file";
|
import { createFile } from '../../resources/file';
|
||||||
import grayMatter from "gray-matter";
|
import grayMatter from 'gray-matter';
|
||||||
import { Article } from "../../../types/article";
|
import { Article } from '../../../types/article';
|
||||||
import { Bundler } from "../../bundler";
|
import { Bundler } from '../../bundler';
|
||||||
import { markdownBundleImages } from "../../utils/markdown";
|
import { markdownBundleImages } from '../../utils/markdown';
|
||||||
import { dirname, resolve } from "path";
|
import { dirname, resolve } from 'path';
|
||||||
import { createImage } from "../../resources/image";
|
import { createImage } from '../../resources/image';
|
||||||
|
|
||||||
type ArticleOptions = {
|
type ArticleOptions = {
|
||||||
|
cwd: string;
|
||||||
|
pattern: string;
|
||||||
bundler: Bundler;
|
bundler: Bundler;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createArticles = ({ bundler }: ArticleOptions) => {
|
const createArticles = ({ bundler, cwd, pattern }: ArticleOptions) => {
|
||||||
const files = createGlob({
|
const files = createGlob({
|
||||||
pattern: "content/articles/**/*.md",
|
pattern,
|
||||||
|
cwd,
|
||||||
create: (path) => {
|
create: (path) => {
|
||||||
const file = createFile({ path });
|
const file = createFile({ path });
|
||||||
const article = file.pipe(async (raw) => {
|
const article = file.pipe(async (raw) => {
|
||||||
@@ -27,12 +30,12 @@ const createArticles = ({ bundler }: ArticleOptions) => {
|
|||||||
});
|
});
|
||||||
const coverUrl = createImage({
|
const coverUrl = createImage({
|
||||||
image: resolve(cwd, cover),
|
image: resolve(cwd, cover),
|
||||||
format: "avif",
|
format: 'avif',
|
||||||
bundler,
|
bundler,
|
||||||
});
|
});
|
||||||
const thumbUrl = createImage({
|
const thumbUrl = createImage({
|
||||||
image: resolve(cwd, cover),
|
image: resolve(cwd, cover),
|
||||||
format: "avif",
|
format: 'avif',
|
||||||
width: 400,
|
width: 400,
|
||||||
bundler,
|
bundler,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import { createGlob } from "../../resources/glob";
|
import { createGlob } from '../../resources/glob';
|
||||||
import { createFile } from "../../resources/file";
|
import { createFile } from '../../resources/file';
|
||||||
import grayMatter from "gray-matter";
|
import grayMatter from 'gray-matter';
|
||||||
import { Bundler } from "../../bundler";
|
import { Bundler } from '../../bundler';
|
||||||
import { markdownBundleImages } from "../../utils/markdown";
|
import { markdownBundleImages } from '../../utils/markdown';
|
||||||
import { dirname } from "path";
|
import { dirname } from 'path';
|
||||||
import { Position } from "../../../types";
|
import { Position } from '../../../types';
|
||||||
import { Observable } from "../../observable";
|
import { Observable } from '../../observable';
|
||||||
|
|
||||||
type PositionOptions = {
|
type PositionOptions = {
|
||||||
|
cwd: string;
|
||||||
|
pattern: string;
|
||||||
bundler: Bundler;
|
bundler: Bundler;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPositions = ({ bundler }: PositionOptions) => {
|
const createPositions = ({ cwd, pattern, bundler }: PositionOptions) => {
|
||||||
const files = createGlob<Observable<Position>>({
|
const files = createGlob<Observable<Position>>({
|
||||||
pattern: "content/resume/positions/**/*.md",
|
pattern,
|
||||||
|
cwd,
|
||||||
create: (path) => {
|
create: (path) => {
|
||||||
|
console.log('c', path);
|
||||||
const file = createFile({ path });
|
const file = createFile({ path });
|
||||||
const position = file.pipe(async (raw) => {
|
const position = file.pipe(async (raw) => {
|
||||||
const { data, content } = grayMatter(raw);
|
const { data, content } = grayMatter(raw);
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
import { resolve } from "path";
|
import { resolve } from 'path';
|
||||||
import { createFile } from "../../resources/file";
|
import { createFile } from '../../resources/file';
|
||||||
import YAML from "yaml";
|
import YAML from 'yaml';
|
||||||
import { Bundler } from "../../bundler";
|
import { Bundler } from '../../bundler';
|
||||||
import { Profile } from "../../../types";
|
import { Profile } from '../../../types';
|
||||||
import { createImage } from "../../resources/image";
|
import { createImage } from '../../resources/image';
|
||||||
|
|
||||||
type ProfileOptions = {
|
type ProfileOptions = {
|
||||||
|
path: string;
|
||||||
|
cwd: string;
|
||||||
bundler: Bundler;
|
bundler: Bundler;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createProfile = ({ bundler }: ProfileOptions) => {
|
const createProfile = ({ cwd, path, bundler }: ProfileOptions) => {
|
||||||
const file = createFile({
|
const file = createFile({
|
||||||
path: resolve("content/profile.yml"),
|
path: resolve(cwd, path),
|
||||||
});
|
});
|
||||||
|
|
||||||
const profile = file.pipe(async (yaml) => {
|
const profile = file.pipe(async (yaml) => {
|
||||||
const data = YAML.parse(yaml);
|
const data = YAML.parse(yaml);
|
||||||
const imagePath = resolve("content", data.image);
|
const imagePath = resolve('content', data.image);
|
||||||
const result: Profile = {
|
const result: Profile = {
|
||||||
...data,
|
...data,
|
||||||
imageUrl: createImage({
|
imageUrl: createImage({
|
||||||
image: imagePath,
|
image: imagePath,
|
||||||
format: "avif",
|
format: 'webp',
|
||||||
bundler,
|
bundler,
|
||||||
}),
|
}),
|
||||||
imagePath,
|
imagePath,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import express, { Express } from "express";
|
import express, { Express } from 'express';
|
||||||
import { Bundler } from "../bundler";
|
import { Bundler } from '../bundler';
|
||||||
import { extname } from "path";
|
import { extname } from 'path';
|
||||||
|
|
||||||
const createServer = (bundler: Bundler): Express => {
|
const createServer = (bundler: Bundler): Express => {
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -8,35 +8,40 @@ const createServer = (bundler: Bundler): Express => {
|
|||||||
let path = req.path;
|
let path = req.path;
|
||||||
let asset = bundler.get(path);
|
let asset = bundler.get(path);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
path = path.endsWith("/") ? path + "index.html" : path + "/index.html";
|
path = path.endsWith('/') ? path + 'index.html' : path + '/index.html';
|
||||||
asset = bundler.get(path);
|
asset = bundler.get(path);
|
||||||
}
|
}
|
||||||
if (asset) {
|
if (asset) {
|
||||||
const ext = extname(path);
|
const ext = extname(path);
|
||||||
asset.data.then((data) => {
|
asset.data
|
||||||
if (ext === ".html") {
|
.then((data) => {
|
||||||
|
if (ext === '.html') {
|
||||||
const unsubscribe = asset!.subscribe(async () => {
|
const unsubscribe = asset!.subscribe(async () => {
|
||||||
await asset?.data;
|
await asset?.data;
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
res.end(`<script>window.location.reload()</script>`);
|
res.end('<script>window.location.reload()</script>');
|
||||||
});
|
});
|
||||||
res.on("close", unsubscribe);
|
res.on('close', unsubscribe);
|
||||||
res.on("finish", unsubscribe);
|
res.on('finish', unsubscribe);
|
||||||
res.on("error", unsubscribe);
|
res.on('error', unsubscribe);
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
"content-type": "text/html;charset=utf-8",
|
'content-type': 'text/html;charset=utf-8',
|
||||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
Pragma: "no-cache",
|
Pragma: 'no-cache',
|
||||||
Expires: "0",
|
Expires: '0',
|
||||||
"keep-alive": "timeout=5, max=100",
|
'keep-alive': 'timeout=5, max=100',
|
||||||
});
|
});
|
||||||
res.write(data.content.toString().replace("</html>", ""));
|
res.write(data.content.toString().replace('</html>', ''));
|
||||||
} else {
|
} else {
|
||||||
res.send(data.content);
|
res.send(data.content);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send(err.message);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(404).send("Not found");
|
res.status(404).send('Not found');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
40
bin/index.ts
40
bin/index.ts
@@ -1,21 +1,35 @@
|
|||||||
import { program } from "commander";
|
import { program } from 'commander';
|
||||||
import { build } from "./build";
|
import { build } from './build';
|
||||||
import { createServer } from "./dev/server";
|
import { createServer } from './dev/server';
|
||||||
import { dirname, join, resolve } from "path";
|
import { dirname, join, resolve } from 'path';
|
||||||
import { mkdir, rm, writeFile } from "fs/promises";
|
import { mkdir, rm, writeFile } from 'fs/promises';
|
||||||
import { existsSync } from "fs";
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
const dev = program.command("dev");
|
const getConfig = (path: string) => {
|
||||||
dev.action(async () => {
|
const resolved = resolve(path);
|
||||||
const bundler = await build();
|
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);
|
const server = createServer(bundler);
|
||||||
server.listen(3000);
|
server.listen(3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
const bundle = program.command("build");
|
const bundle = program.command('build');
|
||||||
bundle.action(async () => {
|
bundle.argument('<config>', 'Path to config file');
|
||||||
const bundler = await build();
|
bundle.action(async (configLocation) => {
|
||||||
const outputDir = resolve("out");
|
const { cwd, config } = getConfig(configLocation);
|
||||||
|
const bundler = await build(cwd, config);
|
||||||
|
const outputDir = resolve('out');
|
||||||
if (existsSync(outputDir)) {
|
if (existsSync(outputDir)) {
|
||||||
rm(outputDir, { recursive: true });
|
rm(outputDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Observable } from "./observable";
|
import { Observable } from './observable';
|
||||||
import { getCollectionItems } from "./utils";
|
import { getCollectionItems } from './utils';
|
||||||
|
|
||||||
describe("observable", () => {
|
describe('observable', () => {
|
||||||
it("should be able to create an observable", async () => {
|
it('should be able to create an observable', async () => {
|
||||||
const observable = new Observable(() => Promise.resolve(1));
|
const observable = new Observable(() => Promise.resolve(1));
|
||||||
expect(observable).toBeDefined();
|
expect(observable).toBeDefined();
|
||||||
const data = await observable.data;
|
const data = await observable.data;
|
||||||
expect(data).toBe(1);
|
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 observable1 = new Observable(() => Promise.resolve(1));
|
||||||
const observable2 = new Observable(() => Promise.resolve(2));
|
const observable2 = new Observable(() => Promise.resolve(2));
|
||||||
const combined = Observable.combine({ observable1, observable2 });
|
const combined = Observable.combine({ observable1, observable2 });
|
||||||
@@ -18,7 +18,7 @@ describe("observable", () => {
|
|||||||
expect(data.observable2).toBe(2);
|
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 observable = new Observable(() => Promise.resolve(1));
|
||||||
const data = await observable.data;
|
const data = await observable.data;
|
||||||
expect(data).toBe(1);
|
expect(data).toBe(1);
|
||||||
@@ -27,20 +27,20 @@ describe("observable", () => {
|
|||||||
expect(data2).toBe(2);
|
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(() =>
|
const observable = new Observable(() =>
|
||||||
Promise.resolve([
|
Promise.resolve([
|
||||||
new Observable(() => Promise.resolve(1)),
|
new Observable(() => Promise.resolve(1)),
|
||||||
new Observable(() => Promise.resolve(2)),
|
new Observable(() => Promise.resolve(2)),
|
||||||
new Observable(() => Promise.resolve(3)),
|
new Observable(() => Promise.resolve(3)),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
const flatten = observable.pipe(getCollectionItems);
|
const flatten = observable.pipe(getCollectionItems);
|
||||||
const data = await flatten.data;
|
const data = await flatten.data;
|
||||||
expect(data).toEqual([1, 2, 3]);
|
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 observable = new Observable(() => Promise.resolve(1));
|
||||||
const spy = jest.fn();
|
const spy = jest.fn();
|
||||||
observable.subscribe(spy);
|
observable.subscribe(spy);
|
||||||
@@ -50,7 +50,7 @@ describe("observable", () => {
|
|||||||
expect(spy).toHaveBeenCalledTimes(1);
|
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 observable1 = new Observable(() => Promise.resolve(1));
|
||||||
const observable2 = new Observable(() => Promise.resolve(2));
|
const observable2 = new Observable(() => Promise.resolve(2));
|
||||||
const combined = Observable.combine({ observable1, observable2 });
|
const combined = Observable.combine({ observable1, observable2 });
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { Observable } from "./observable";
|
export { Observable } from './observable';
|
||||||
export { getCollectionItems } from "./utils";
|
export { getCollectionItems } from './utils';
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class Observable<T> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static combine = <U extends Record<string, Observable<any>>>(
|
static combine = <U extends Record<string, Observable<any>>>(
|
||||||
record: U
|
record: U,
|
||||||
): Observable<ObservableRecord<U>> => {
|
): Observable<ObservableRecord<U>> => {
|
||||||
const loader = () =>
|
const loader = () =>
|
||||||
Object.entries(record).reduce(
|
Object.entries(record).reduce(
|
||||||
@@ -66,7 +66,7 @@ class Observable<T> {
|
|||||||
...(await accP),
|
...(await accP),
|
||||||
[key]: await value.data,
|
[key]: await value.data,
|
||||||
}),
|
}),
|
||||||
{} as any
|
{} as any,
|
||||||
);
|
);
|
||||||
const observable = new Observable<ObservableRecord<U>>(loader);
|
const observable = new Observable<ObservableRecord<U>>(loader);
|
||||||
Object.values(record).forEach((item) => {
|
Object.values(record).forEach((item) => {
|
||||||
@@ -76,6 +76,16 @@ class Observable<T> {
|
|||||||
});
|
});
|
||||||
return observable;
|
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 };
|
export { Observable };
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Observable } from "./observable";
|
import { Observable } from './observable';
|
||||||
|
|
||||||
const getCollectionItems = async <T>(items: Observable<T>[]) => {
|
const getCollectionItems = async <T>(items: Observable<T>[]) => {
|
||||||
return Promise.all(items.map((item) => item.data));
|
return Promise.all(items.map((item) => item.data));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createFile } from "../file";
|
import { createFile } from '../file';
|
||||||
import ejs from "ejs";
|
import ejs from 'ejs';
|
||||||
|
|
||||||
const createEjs = (path: string) => {
|
const createEjs = (path: string) => {
|
||||||
const file = createFile({ path });
|
const file = createFile({ path });
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
import { readFile } from "fs/promises";
|
import { readFile } from 'fs/promises';
|
||||||
import { Observable } from "../../observable";
|
import { Observable } from '../../observable';
|
||||||
import { watch } from "fs";
|
import { watch } from 'fs';
|
||||||
|
|
||||||
type FileOptions = {
|
type FileOptions = {
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createFile = ({ path }: FileOptions) => {
|
const createFile = ({ path }: FileOptions) => {
|
||||||
const file = new Observable(async () => readFile(path, "utf-8"));
|
let watcher: ReturnType<typeof watch> | undefined;
|
||||||
|
const addWatcher = () => {
|
||||||
watch(path, () => {
|
if (watcher) {
|
||||||
|
watcher.close();
|
||||||
|
}
|
||||||
|
watcher = watch(path, () => {
|
||||||
file.recreate();
|
file.recreate();
|
||||||
|
addWatcher();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const file = new Observable(async () => {
|
||||||
|
addWatcher();
|
||||||
|
return readFile(path, 'utf-8');
|
||||||
});
|
});
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
|
|||||||
@@ -1,28 +1,25 @@
|
|||||||
import fastGlob from "fast-glob";
|
import fastGlob from 'fast-glob';
|
||||||
import watchGlob from "glob-watcher";
|
import watchGlob from 'glob-watcher';
|
||||||
import { Observable } from "../../observable";
|
import { Observable } from '../../observable';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
type GlobOptions<T> = {
|
type GlobOptions<T> = {
|
||||||
cwd?: string;
|
cwd: string;
|
||||||
pattern: string;
|
pattern: string;
|
||||||
create?: (path: string) => T;
|
create?: (path: string) => T;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultCreate = (a: any) => a;
|
const defaultCreate = (a: any) => a;
|
||||||
|
|
||||||
const createGlob = <T = string>({
|
const createGlob = <T = string>({ cwd, pattern, create = defaultCreate }: GlobOptions<T>) => {
|
||||||
cwd,
|
|
||||||
pattern,
|
|
||||||
create = defaultCreate,
|
|
||||||
}: GlobOptions<T>) => {
|
|
||||||
const glob = new Observable(async () => {
|
const glob = new Observable(async () => {
|
||||||
const files = await fastGlob(pattern, { cwd });
|
const files = await fastGlob(pattern, { cwd });
|
||||||
return files.map(create);
|
return files.map((path) => create(resolve(cwd, path)));
|
||||||
});
|
});
|
||||||
|
|
||||||
const watcher = watchGlob(pattern, { cwd });
|
const watcher = watchGlob(pattern, { cwd });
|
||||||
watcher.on("add", (path) => {
|
watcher.on('add', (path) => {
|
||||||
glob.set((current) => Promise.resolve([...(current || []), create(path)]));
|
glob.set((current) => Promise.resolve([...(current || []), create(resolve(cwd, path))]));
|
||||||
|
|
||||||
return glob;
|
return glob;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createHash } from "crypto";
|
import { createHash } from 'crypto';
|
||||||
import { Asset, Bundler } from "../../bundler";
|
import { Asset, Bundler } from '../../bundler';
|
||||||
import { Observable } from "../../observable";
|
import { Observable } from '../../observable';
|
||||||
import sharp, { FormatEnum } from "sharp";
|
import sharp, { FormatEnum } from 'sharp';
|
||||||
|
|
||||||
type ImageOptions = {
|
type ImageOptions = {
|
||||||
format: keyof FormatEnum;
|
format: keyof FormatEnum;
|
||||||
@@ -13,8 +13,7 @@ type ImageOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createImage = (options: ImageOptions) => {
|
const createImage = (options: ImageOptions) => {
|
||||||
let path =
|
let path = options.name || createHash('sha256').update(options.image).digest('hex');
|
||||||
options.name || createHash("sha256").update(options.image).digest("hex");
|
|
||||||
if (options.width) {
|
if (options.width) {
|
||||||
path += `-w${options.width}`;
|
path += `-w${options.width}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Asset, Bundler } from "../../bundler";
|
import { Asset, Bundler } from '../../bundler';
|
||||||
import { Observable } from "../../observable";
|
import { Observable } from '../../observable';
|
||||||
import { createEjs } from "../ejs";
|
import { createEjs } from '../ejs';
|
||||||
import { latexToPdf } from "./utils";
|
import { latexToPdf } from './utils';
|
||||||
|
|
||||||
type LatexOptions = {
|
type LatexOptions = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -23,7 +23,11 @@ const createLatex = ({ template, data, path, bundler }: LatexOptions) => {
|
|||||||
};
|
};
|
||||||
return asset;
|
return asset;
|
||||||
});
|
});
|
||||||
return bundler.register(`${path}.pdf`, pdf);
|
const url = bundler.register(`${path}.pdf`, pdf);
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
item: pdf,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export { createLatex };
|
export { createLatex };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import latex from "node-latex";
|
import latex from 'node-latex';
|
||||||
import { Readable } from "stream";
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
const latexToPdf = (doc: string) =>
|
const latexToPdf = (doc: string) =>
|
||||||
new Promise<Buffer>((resolve, reject) => {
|
new Promise<Buffer>((resolve, reject) => {
|
||||||
@@ -8,14 +8,14 @@ const latexToPdf = (doc: string) =>
|
|||||||
input.push(doc);
|
input.push(doc);
|
||||||
input.push(null);
|
input.push(null);
|
||||||
const latexStream = latex(input);
|
const latexStream = latex(input);
|
||||||
latexStream.on("data", (chunk) => {
|
latexStream.on('data', (chunk) => {
|
||||||
chunks.push(Buffer.from(chunk));
|
chunks.push(Buffer.from(chunk));
|
||||||
});
|
});
|
||||||
latexStream.on("finish", () => {
|
latexStream.on('finish', () => {
|
||||||
const result = Buffer.concat(chunks);
|
const result = Buffer.concat(chunks);
|
||||||
resolve(result);
|
resolve(result);
|
||||||
});
|
});
|
||||||
latexStream.on("error", (err) => {
|
latexStream.on('error', (err) => {
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { ComponentType } from "react";
|
import React, { ComponentType } from 'react';
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from 'react-dom/server';
|
||||||
import { HelmetProvider, FilledContext } from "react-helmet-async";
|
import { HelmetProvider, FilledContext } from 'react-helmet-async';
|
||||||
import { Asset, Bundler } from "../../bundler";
|
import { Asset, Bundler } from '../../bundler';
|
||||||
import { Observable } from "../../observable";
|
import { Observable } from '../../observable';
|
||||||
import { ServerStyleSheet } from "styled-components";
|
import { ServerStyleSheet } from 'styled-components';
|
||||||
import { resolve } from "path";
|
import { resolve } from 'path';
|
||||||
|
|
||||||
type PageOptions = {
|
type PageOptions = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -24,8 +24,8 @@ const createPage = (options: PageOptions) => {
|
|||||||
React.createElement(
|
React.createElement(
|
||||||
HelmetProvider,
|
HelmetProvider,
|
||||||
{ context: helmetContext },
|
{ context: helmetContext },
|
||||||
React.createElement(template, props)
|
React.createElement(template, props),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
const bodyHtml = renderToStaticMarkup(body);
|
const bodyHtml = renderToStaticMarkup(body);
|
||||||
const { helmet } = helmetContext;
|
const { helmet } = helmetContext;
|
||||||
@@ -40,7 +40,7 @@ const createPage = (options: PageOptions) => {
|
|||||||
helmet.script?.toString(),
|
helmet.script?.toString(),
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("");
|
.join('');
|
||||||
const html = `<!DOCTYPE html>
|
const html = `<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -55,7 +55,7 @@ const createPage = (options: PageOptions) => {
|
|||||||
return asset;
|
return asset;
|
||||||
});
|
});
|
||||||
|
|
||||||
const path = resolve("/", options.path, "index.html");
|
const path = resolve('/', options.path, 'index.html');
|
||||||
return options.bundler.register(path, page);
|
return options.bundler.register(path, page);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import vm from "vm";
|
import vm from 'vm';
|
||||||
import React, { ComponentType } from "react";
|
import React, { ComponentType } from 'react';
|
||||||
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||||
import commonjs from "@rollup/plugin-commonjs";
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
import json from "@rollup/plugin-json";
|
import json from '@rollup/plugin-json';
|
||||||
import replace from "@rollup/plugin-replace";
|
import replace from '@rollup/plugin-replace';
|
||||||
import sucrase from "@rollup/plugin-sucrase";
|
import sucrase from '@rollup/plugin-sucrase';
|
||||||
import alias from "@rollup/plugin-alias";
|
import alias from '@rollup/plugin-alias';
|
||||||
import externalGlobals from "rollup-plugin-external-globals";
|
import externalGlobals from 'rollup-plugin-external-globals';
|
||||||
import { createScript } from "../script";
|
import { createScript } from '../script';
|
||||||
import orgStyled from "styled-components";
|
import orgStyled from 'styled-components';
|
||||||
import * as styledExports from "styled-components";
|
import * as styledExports from 'styled-components';
|
||||||
import ReactHelmetAsync from "react-helmet-async";
|
import ReactHelmetAsync from 'react-helmet-async';
|
||||||
import { resolve } from "path";
|
import { resolve } from 'path';
|
||||||
|
|
||||||
const styled = orgStyled.bind(null);
|
const styled = orgStyled.bind(null);
|
||||||
for (let key of Object.keys(orgStyled)) {
|
for (let key of Object.keys(orgStyled)) {
|
||||||
if (key === "default") {
|
if (key === 'default') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
(styled as any)[key] = (orgStyled as any)[key];
|
(styled as any)[key] = (orgStyled as any)[key];
|
||||||
}
|
}
|
||||||
for (let key of Object.keys(styledExports)) {
|
for (let key of Object.keys(styledExports)) {
|
||||||
if (key === "default") {
|
if (key === 'default') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
(styled as any)[key] = (styledExports as any)[key];
|
(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 createReact = <TProps = any>(path: string) => {
|
||||||
const script = createScript({
|
const script = createScript({
|
||||||
path,
|
path,
|
||||||
format: "cjs",
|
format: 'cjs',
|
||||||
plugins: [
|
plugins: [
|
||||||
replace({
|
replace({
|
||||||
preventAssignment: true,
|
preventAssignment: true,
|
||||||
"process.env.NODE_ENV": JSON.stringify("production"),
|
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||||
}),
|
}),
|
||||||
alias({
|
alias({
|
||||||
entries: [
|
entries: [{ find: '@', replacement: resolve('content/templates/react') }],
|
||||||
{ find: "@", replacement: resolve("content/templates/react") },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
sucrase({
|
sucrase({
|
||||||
exclude: ["node_modules/**"],
|
exclude: ['node_modules/**'],
|
||||||
transforms: ["jsx", "typescript"],
|
transforms: ['jsx', 'typescript'],
|
||||||
}),
|
}),
|
||||||
nodeResolve({
|
nodeResolve({
|
||||||
browser: true,
|
browser: true,
|
||||||
preferBuiltins: false,
|
preferBuiltins: false,
|
||||||
extensions: [".js", ".ts", ".tsx"],
|
extensions: ['.js', '.ts', '.tsx'],
|
||||||
}),
|
}),
|
||||||
json(),
|
json(),
|
||||||
commonjs({
|
commonjs({
|
||||||
include: /node_modules/,
|
include: /node_modules/,
|
||||||
}),
|
}),
|
||||||
externalGlobals({
|
externalGlobals({
|
||||||
react: "React",
|
react: 'React',
|
||||||
"styled-components": "StyledComponents",
|
'styled-components': 'StyledComponents',
|
||||||
"react-helmet-async": "ReactHelmetAsync",
|
'react-helmet-async': 'ReactHelmetAsync',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Observable } from "../../observable";
|
import { Observable } from '../../observable';
|
||||||
import { InputPluginOption, ModuleFormat, watch } from "rollup";
|
import { InputPluginOption, ModuleFormat, watch } from 'rollup';
|
||||||
|
|
||||||
type ScriptOptions = {
|
type ScriptOptions = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -22,8 +22,8 @@ const build = (options: ScriptOptions, update: (code: string) => void) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
watcher.on("event", async (event) => {
|
watcher.on('event', async (event) => {
|
||||||
if (event.code === "BUNDLE_END") {
|
if (event.code === 'BUNDLE_END') {
|
||||||
const { output } = await event.result.generate({
|
const { output } = await event.result.generate({
|
||||||
format: options.format,
|
format: options.format,
|
||||||
});
|
});
|
||||||
@@ -35,7 +35,7 @@ const build = (options: ScriptOptions, update: (code: string) => void) =>
|
|||||||
update(code);
|
update(code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event.code === "ERROR") {
|
if (event.code === 'ERROR') {
|
||||||
reject(event.error);
|
reject(event.error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -43,7 +43,7 @@ const build = (options: ScriptOptions, update: (code: string) => void) =>
|
|||||||
|
|
||||||
const createScript = (options: ScriptOptions) => {
|
const createScript = (options: ScriptOptions) => {
|
||||||
const script: Observable<string> = new Observable(() =>
|
const script: Observable<string> = new Observable(() =>
|
||||||
build(options, (code) => script.set(() => Promise.resolve(code)))
|
build(options, (code) => script.set(() => Promise.resolve(code))),
|
||||||
);
|
);
|
||||||
|
|
||||||
return script;
|
return script;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { resolve } from "path";
|
import { resolve } from 'path';
|
||||||
import { decode } from "html-entities";
|
import { decode } from 'html-entities';
|
||||||
import { marked } from "marked";
|
import { marked } from 'marked';
|
||||||
import remark from "remark";
|
import remark from 'remark';
|
||||||
import visit from "unist-util-visit";
|
import visit from 'unist-util-visit';
|
||||||
import { Bundler } from "../../bundler";
|
import { Bundler } from '../../bundler';
|
||||||
import { createImage } from "../../resources/image";
|
import { createImage } from '../../resources/image';
|
||||||
import { renderer } from "./latex";
|
import { renderer } from './latex';
|
||||||
|
|
||||||
type MarkdownBundleImagesOptions = {
|
type MarkdownBundleImagesOptions = {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
@@ -13,15 +13,11 @@ type MarkdownBundleImagesOptions = {
|
|||||||
bundler: Bundler;
|
bundler: Bundler;
|
||||||
};
|
};
|
||||||
|
|
||||||
const markdownBundleImages = async ({
|
const markdownBundleImages = async ({ bundler, cwd, content }: MarkdownBundleImagesOptions) => {
|
||||||
bundler,
|
|
||||||
cwd,
|
|
||||||
content,
|
|
||||||
}: MarkdownBundleImagesOptions) => {
|
|
||||||
const result = await remark()
|
const result = await remark()
|
||||||
.use(() => (tree) => {
|
.use(() => (tree) => {
|
||||||
visit(tree, "image", (node) => {
|
visit(tree, 'image', (node) => {
|
||||||
if (!("url" in node)) {
|
if (!('url' in node)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const url = node.url as string;
|
const url = node.url as string;
|
||||||
@@ -29,7 +25,7 @@ const markdownBundleImages = async ({
|
|||||||
const image = createImage({
|
const image = createImage({
|
||||||
image: path,
|
image: path,
|
||||||
bundler,
|
bundler,
|
||||||
format: "webp",
|
format: 'avif',
|
||||||
});
|
});
|
||||||
const newUrl = image;
|
const newUrl = image;
|
||||||
node.url = newUrl;
|
node.url = newUrl;
|
||||||
@@ -46,7 +42,7 @@ type MarkdownToLatexOptions = {
|
|||||||
|
|
||||||
const markdownToLatex = ({ root, content }: MarkdownToLatexOptions) => {
|
const markdownToLatex = ({ root, content }: MarkdownToLatexOptions) => {
|
||||||
const render: any = {
|
const render: any = {
|
||||||
...renderer(0),
|
...renderer(0, root),
|
||||||
};
|
};
|
||||||
const latex = marked(content, {
|
const latex = marked(content, {
|
||||||
renderer: render,
|
renderer: render,
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import { decode } from "html-entities";
|
import { decode } from 'html-entities';
|
||||||
import { existsSync } from "fs";
|
import { existsSync } from 'fs';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
const latexTypes = ["", "section", "subsection", "paragraph", "subparagraph"];
|
const latexTypes = ['', 'section', 'subsection', 'paragraph', 'subparagraph'];
|
||||||
|
|
||||||
const sanitize = (text?: string) => {
|
const sanitize = (text?: string) => {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return "";
|
return '';
|
||||||
}
|
}
|
||||||
return decode(text)
|
return decode(text)
|
||||||
.replace("&", "\\&")
|
.replace('&', '\\&')
|
||||||
.replace("_", "\\_")
|
.replace('_', '\\_')
|
||||||
.replace(/([^\\])\}/g, "$1\\}")
|
.replace(/([^\\])\}/g, '$1\\}')
|
||||||
.replace(/([^\\])\{/g, "$1\\{")
|
.replace(/([^\\])\{/g, '$1\\{')
|
||||||
.replace(/[^\\]\[/g, "\\[")
|
.replace(/[^\\]\[/g, '\\[')
|
||||||
.replace(/#/g, "\\#");
|
.replace(/#/g, '\\#');
|
||||||
};
|
};
|
||||||
|
|
||||||
type Renderer = (depth: number) => {
|
type Renderer = (depth: number) => {
|
||||||
@@ -30,7 +31,7 @@ type Renderer = (depth: number) => {
|
|||||||
image?: (link: string) => string;
|
image?: (link: string) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderer = (outerDepth: number) => ({
|
const renderer = (outerDepth: number, cwd: string) => ({
|
||||||
heading: (text: string, depth: number) => {
|
heading: (text: string, depth: number) => {
|
||||||
return `\\${latexTypes[outerDepth + depth]}{${sanitize(text)}}\n\n`;
|
return `\\${latexTypes[outerDepth + depth]}{${sanitize(text)}}\n\n`;
|
||||||
},
|
},
|
||||||
@@ -76,13 +77,12 @@ const renderer = (outerDepth: number) => ({
|
|||||||
return `\\texttt{${sanitize(code)}}`;
|
return `\\texttt{${sanitize(code)}}`;
|
||||||
},
|
},
|
||||||
image: (link: string) => {
|
image: (link: string) => {
|
||||||
if (!existsSync(link)) {
|
const path = resolve(cwd, link);
|
||||||
return "Online image not supported";
|
if (!existsSync(path)) {
|
||||||
|
return `Online image not supported ${path}`;
|
||||||
}
|
}
|
||||||
return `\\begin{figure}[h!]
|
return `
|
||||||
\\includegraphics[width=0.5\\textwidth]{${link}}
|
\\noindent\\includegraphics[width=\\linewidth]{${path}}
|
||||||
\\centering
|
|
||||||
\\end{figure}
|
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import { Observable } from "../../observable";
|
import { Observable } from '../../observable';
|
||||||
|
|
||||||
const forEach = async <T extends Observable<any[]>>(
|
const forEach = async <T extends Observable<any[]>>(
|
||||||
observable: T,
|
observable: T,
|
||||||
fn: (
|
fn: (
|
||||||
value: T extends Observable<infer U>
|
value: T extends Observable<infer U> ? (U extends Array<infer A> ? A : never) : never,
|
||||||
? U extends Array<infer A>
|
) => Promise<void>,
|
||||||
? A
|
|
||||||
: never
|
|
||||||
: never
|
|
||||||
) => Promise<void>
|
|
||||||
) => {
|
) => {
|
||||||
const knownValues = new Set();
|
const knownValues = new Set();
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
|
|||||||
31
content/config.ts
Normal file
31
content/config.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Config } from '../types/config';
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
profile: {
|
||||||
|
path: 'profile.yml',
|
||||||
|
},
|
||||||
|
frontpage: {
|
||||||
|
react: {
|
||||||
|
template: 'templates/react/pages/frontpage/index.tsx',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resume: {
|
||||||
|
latex: {
|
||||||
|
template: 'templates/latex/resume.tex',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
articles: {
|
||||||
|
pattern: 'articles/**/*.md',
|
||||||
|
react: {
|
||||||
|
template: 'templates/react/pages/article/index.tsx',
|
||||||
|
},
|
||||||
|
latex: {
|
||||||
|
template: 'templates/latex/article.tex',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
positions: {
|
||||||
|
pattern: 'resume/positions/**/*.md',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -5,4 +5,4 @@ from: 2022
|
|||||||
to: Present
|
to: Present
|
||||||
---
|
---
|
||||||
|
|
||||||
Hello world
|
// TODO
|
||||||
|
|||||||
@@ -1,9 +1,28 @@
|
|||||||
\documentclass{article}
|
\documentclass{article}
|
||||||
|
\usepackage[top=2cm, bottom=2cm, left=2cm, right=2cm]{geometry}
|
||||||
\usepackage{graphicx}
|
\usepackage{graphicx}
|
||||||
\usepackage{hyperref}
|
\usepackage{hyperref}
|
||||||
|
\usepackage{multicol}
|
||||||
|
\usepackage{fancyhdr}
|
||||||
|
|
||||||
|
\pagestyle{fancy}
|
||||||
|
\fancyhf{}
|
||||||
|
\rhead{<%-profile.name%> \today}
|
||||||
|
\lhead{<%-article.title%>}
|
||||||
|
\rfoot{Page \thepage}
|
||||||
|
|
||||||
\title{<%-article.title%>}
|
\title{<%-article.title%>}
|
||||||
\begin{document}
|
\begin{document}
|
||||||
\maketitle
|
|
||||||
\includegraphics[width=0.5\textwidth]{<%-article.cover%>}
|
\begin{multicols}{2}
|
||||||
|
\noindent\begin{minipage}{\linewidth}
|
||||||
|
\Huge{<%-article.title%>}
|
||||||
|
\newline
|
||||||
|
\large{By <%-profile.name%>}
|
||||||
|
\vspace{0.5cm}\\
|
||||||
|
\includegraphics[width=\linewidth]{<%-article.cover%>}
|
||||||
|
\vspace{1.5cm}
|
||||||
|
\end{minipage}
|
||||||
<%-article.body%>
|
<%-article.body%>
|
||||||
|
\end{multicols}
|
||||||
\end{document}
|
\end{document}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from 'react';
|
||||||
import styled from "styled-components";
|
import styled from 'styled-components';
|
||||||
import ArticlePreview from "../preview";
|
import ArticlePreview from '../preview';
|
||||||
import { JumboArticlePreview } from "../preview/jumbo";
|
import { JumboArticlePreview } from '../preview/jumbo';
|
||||||
import { MiniArticlePreview } from "../preview/mini";
|
import { MiniArticlePreview } from '../preview/mini';
|
||||||
import { Article } from "types";
|
import { Article } from 'types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
articles: Article[];
|
articles: Article[];
|
||||||
@@ -47,7 +47,7 @@ const ArticleGrid: React.FC<Props> = ({ articles }) => {
|
|||||||
// new Date(b.published).getTime() -
|
// new Date(b.published).getTime() -
|
||||||
// new Date(a.published).getTime()
|
// new Date(a.published).getTime()
|
||||||
// ),
|
// ),
|
||||||
[articles]
|
[articles],
|
||||||
);
|
);
|
||||||
const featured1 = useMemo(() => sorted.slice(0, 1)[0], [sorted]);
|
const featured1 = useMemo(() => sorted.slice(0, 1)[0], [sorted]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from 'react';
|
||||||
import styled from "styled-components";
|
import styled from 'styled-components';
|
||||||
import { Title1 } from "@/typography";
|
import { Title1 } from '@/typography';
|
||||||
import { createTheme } from "@/theme/create";
|
import { createTheme } from '@/theme/create';
|
||||||
import { ThemeProvider } from "@/theme/provider";
|
import { ThemeProvider } from '@/theme/provider';
|
||||||
import { Article } from "types";
|
import { Article } from 'types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
article: Article;
|
article: Article;
|
||||||
@@ -28,7 +28,7 @@ const Wrapper = styled.a`
|
|||||||
const Title = styled(Title1)`
|
const Title = styled(Title1)`
|
||||||
background: ${({ theme }) => theme.colors.primary};
|
background: ${({ theme }) => theme.colors.primary};
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
font-family: "Black Ops One", sans-serif;
|
font-family: 'Black Ops One', sans-serif;
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
@@ -48,7 +48,7 @@ const AsideWrapper = styled.aside<{
|
|||||||
background: ${({ theme }) => theme.colors.primary};
|
background: ${({ theme }) => theme.colors.primary};
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
${({ image }) => (image ? `background-image: url(${image});` : "")}
|
${({ image }) => (image ? `background-image: url(${image});` : '')}
|
||||||
flex: 1;
|
flex: 1;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -63,14 +63,14 @@ const ArticlePreview: React.FC<Props> = ({ article }) => {
|
|||||||
createTheme({
|
createTheme({
|
||||||
baseColor: article.color,
|
baseColor: article.color,
|
||||||
}),
|
}),
|
||||||
[article.color]
|
[article.color],
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Wrapper href={`/articles/${article.slug}`}>
|
<Wrapper href={`/articles/${article.slug}`}>
|
||||||
<AsideWrapper image={article.thumbUrl} />
|
<AsideWrapper image={article.thumbUrl} />
|
||||||
<MetaWrapper>
|
<MetaWrapper>
|
||||||
{article.title.split(" ").map((word, index) => (
|
{article.title.split(' ').map((word, index) => (
|
||||||
<Title key={index}>{word}</Title>
|
<Title key={index}>{word}</Title>
|
||||||
))}
|
))}
|
||||||
</MetaWrapper>
|
</MetaWrapper>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import styled from "styled-components";
|
import styled from 'styled-components';
|
||||||
import { Title1, Body1 } from "@/typography";
|
import { Title1, Body1 } from '@/typography';
|
||||||
import { Article } from "types";
|
import { Article } from 'types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
article: Article;
|
article: Article;
|
||||||
@@ -24,7 +24,7 @@ const Wrapper = styled.a`
|
|||||||
|
|
||||||
const Title = styled(Title1)`
|
const Title = styled(Title1)`
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
font-family: "Black Ops One", sans-serif;
|
font-family: 'Black Ops One', sans-serif;
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
@@ -55,7 +55,7 @@ const AsideWrapper = styled.aside<{
|
|||||||
background: ${({ theme }) => theme.colors.primary};
|
background: ${({ theme }) => theme.colors.primary};
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
${({ image }) => (image ? `background-image: url(${image});` : "")}
|
${({ image }) => (image ? `background-image: url(${image});` : '')}
|
||||||
flex: 1;
|
flex: 1;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from 'react';
|
||||||
import styled from "styled-components";
|
import styled from 'styled-components';
|
||||||
import { Title1 } from "@/typography";
|
import { Title1 } from '@/typography';
|
||||||
import { createTheme } from "@/theme/create";
|
import { createTheme } from '@/theme/create';
|
||||||
import { ThemeProvider } from "@/theme/provider";
|
import { ThemeProvider } from '@/theme/provider';
|
||||||
import { Article } from "types";
|
import { Article } from 'types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
article: Article;
|
article: Article;
|
||||||
@@ -26,7 +26,7 @@ const Title = styled(Title1)`
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
padding: 5px 5px;
|
padding: 5px 5px;
|
||||||
font-family: "Black Ops One", sans-serif;
|
font-family: 'Black Ops One', sans-serif;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
background: ${({ theme }) => theme.colors.background};
|
background: ${({ theme }) => theme.colors.background};
|
||||||
`;
|
`;
|
||||||
@@ -46,7 +46,7 @@ const AsideWrapper = styled.aside<{
|
|||||||
background: ${({ theme }) => theme.colors.primary};
|
background: ${({ theme }) => theme.colors.primary};
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
${({ image }) => (image ? `background-image: url(${image});` : "")}
|
${({ image }) => (image ? `background-image: url(${image});` : '')}
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -61,14 +61,14 @@ const MiniArticlePreview: React.FC<Props> = ({ article }) => {
|
|||||||
createTheme({
|
createTheme({
|
||||||
baseColor: article.color,
|
baseColor: article.color,
|
||||||
}),
|
}),
|
||||||
[article.color]
|
[article.color],
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Wrapper href={`/articles/${article.slug}`}>
|
<Wrapper href={`/articles/${article.slug}`}>
|
||||||
<AsideWrapper image={article.thumbUrl} />
|
<AsideWrapper image={article.thumbUrl} />
|
||||||
<MetaWrapper>
|
<MetaWrapper>
|
||||||
{article.title.split(" ").map((word, index) => (
|
{article.title.split(' ').map((word, index) => (
|
||||||
<Title key={index}>{word}</Title>
|
<Title key={index}>{word}</Title>
|
||||||
))}
|
))}
|
||||||
</MetaWrapper>
|
</MetaWrapper>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FC, ReactNode } from "react"
|
import { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
type HtmlProps = {
|
type HtmlProps = {
|
||||||
body: ReactNode;
|
body: ReactNode;
|
||||||
@@ -18,7 +18,10 @@ const Html: FC<HtmlProps> = ({ body, head, scripts }) => {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Black+Ops+One&family=Merriweather:wght@400;700&display=swap" rel="stylesheet" />
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Black+Ops+One&family=Merriweather:wght@400;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root">{body}</div>
|
<div id="root">{body}</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { ReactNode, useMemo } from "react";
|
import React, { ReactNode, useMemo } from 'react';
|
||||||
import styled from "styled-components";
|
import styled from 'styled-components';
|
||||||
import { createTheme } from "@/theme/create";
|
import { createTheme } from '@/theme/create';
|
||||||
import { ThemeProvider } from "@/theme/provider";
|
import { ThemeProvider } from '@/theme/provider';
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
background: ${({ theme }) => theme.colors.background};
|
background: ${({ theme }) => theme.colors.background};
|
||||||
@@ -25,7 +25,7 @@ const BackgroundWrapper = styled.div<{
|
|||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
${({ image }) => (image ? `background-image: url(${image});` : "")}
|
${({ image }) => (image ? `background-image: url(${image});` : '')}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Content = styled.div`
|
const Content = styled.div`
|
||||||
@@ -50,7 +50,7 @@ const Sheet: React.FC<Props> = ({ color, background, children }) => {
|
|||||||
createTheme({
|
createTheme({
|
||||||
baseColor: color,
|
baseColor: color,
|
||||||
}),
|
}),
|
||||||
[color]
|
[color],
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import styled, { createGlobalStyle } from "styled-components";
|
import styled, { createGlobalStyle } from 'styled-components';
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { Jumbo } from "./typography";
|
import { Jumbo } from '../../typography';
|
||||||
import { createTheme, ThemeProvider } from "./theme";
|
import { createTheme, ThemeProvider } from '../../theme';
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from 'react-helmet-async';
|
||||||
import { Page } from "types";
|
import { Page } from 'types';
|
||||||
|
|
||||||
const GlobalStyle = createGlobalStyle`
|
const GlobalStyle = createGlobalStyle`
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
@@ -36,7 +36,7 @@ const ArticleTitleWord = styled(Jumbo)`
|
|||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
font-family: "Black Ops One", sans-serif;
|
font-family: 'Black Ops One', sans-serif;
|
||||||
background: ${({ theme }) => theme.colors.primary};
|
background: ${({ theme }) => theme.colors.primary};
|
||||||
color: ${({ theme }) => theme.colors.foreground};
|
color: ${({ theme }) => theme.colors.foreground};
|
||||||
@media only screen and (max-width: 900px) {
|
@media only screen and (max-width: 900px) {
|
||||||
@@ -57,7 +57,7 @@ const Wrapper = styled.div`
|
|||||||
|
|
||||||
const ArticleWrapper = styled.article`
|
const ArticleWrapper = styled.article`
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-family: "Merriweather", serif;
|
font-family: 'Merriweather', serif;
|
||||||
|
|
||||||
> p,
|
> p,
|
||||||
ul,
|
ul,
|
||||||
@@ -82,7 +82,7 @@ const ArticleWrapper = styled.article`
|
|||||||
}
|
}
|
||||||
|
|
||||||
> p:first-of-type::first-letter {
|
> p:first-of-type::first-letter {
|
||||||
font-family: "Black Ops One", sans-serif;
|
font-family: 'Black Ops One', sans-serif;
|
||||||
border: solid 5px ${({ theme }) => theme.colors.foreground};
|
border: solid 5px ${({ theme }) => theme.colors.foreground};
|
||||||
margin: 0 1rem 0 0;
|
margin: 0 1rem 0 0;
|
||||||
font-size: 6rem;
|
font-size: 6rem;
|
||||||
@@ -118,7 +118,7 @@ const ArticleWrapper = styled.article`
|
|||||||
padding-right: 40px;
|
padding-right: 40px;
|
||||||
shape-outside: padding-box;
|
shape-outside: padding-box;
|
||||||
position: relative;
|
position: relative;
|
||||||
font-family: "Black Ops One", sans-serif;
|
font-family: 'Black Ops One', sans-serif;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -132,7 +132,7 @@ const ArticleWrapper = styled.article`
|
|||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: "";
|
content: '';
|
||||||
right: 20px;
|
right: 20px;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -162,14 +162,14 @@ const ArticleWrapper = styled.article`
|
|||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
color: ${({ theme }) => theme.colors.primary};
|
color: ${({ theme }) => theme.colors.primary};
|
||||||
content: "\\00BB";
|
content: '\\00BB';
|
||||||
float: left;
|
float: left;
|
||||||
font-size: 6rem;
|
font-size: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: "";
|
content: '';
|
||||||
right: 20px;
|
right: 20px;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -222,14 +222,14 @@ const Download = styled.a`
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-family: "Black Ops One", sans-serif;
|
font-family: 'Black Ops One', sans-serif;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Author = styled.a`
|
const Author = styled.a`
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-family: "Black Ops One", sans-serif;
|
font-family: 'Black Ops One', sans-serif;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -238,7 +238,7 @@ const Author = styled.a`
|
|||||||
color: ${({ theme }) => theme.colors.foreground};
|
color: ${({ theme }) => theme.colors.foreground};
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: "";
|
content: '';
|
||||||
border-bottom: solid 15px ${({ theme }) => theme.colors.primary};
|
border-bottom: solid 15px ${({ theme }) => theme.colors.primary};
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -249,17 +249,13 @@ const Author = styled.a`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ArticlePage: Page<"article"> = ({ article, profile, pdfUrl }) => {
|
const ArticlePage: Page<'article'> = ({ article, profile, pdfUrl }) => {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={createTheme({ baseColor: article.color })}>
|
<ThemeProvider theme={createTheme({ baseColor: article.color })}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||||
rel="preconnect"
|
|
||||||
href="https://fonts.gstatic.com"
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
/>
|
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Black+Ops+One&family=Merriweather:wght@400;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Black+Ops+One&family=Merriweather:wght@400;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
@@ -269,7 +265,7 @@ const ArticlePage: Page<"article"> = ({ article, profile, pdfUrl }) => {
|
|||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Content>
|
<Content>
|
||||||
<Title>
|
<Title>
|
||||||
{article.title.split(" ").map((word, index) => (
|
{article.title.split(' ').map((word, index) => (
|
||||||
<ArticleTitleWord key={index}>{word}</ArticleTitleWord>
|
<ArticleTitleWord key={index}>{word}</ArticleTitleWord>
|
||||||
))}
|
))}
|
||||||
<Author href="/">by {profile.name}</Author>
|
<Author href="/">by {profile.name}</Author>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import styled, { createGlobalStyle } from "styled-components";
|
import styled, { createGlobalStyle } from 'styled-components';
|
||||||
import { ArticleGrid } from "@/components/article/grid";
|
import { ArticleGrid } from '@/components/article/grid';
|
||||||
import { Jumbo } from "@/typography";
|
import { Jumbo } from '@/typography';
|
||||||
import { useMemo } from "react";
|
import { useMemo } from 'react';
|
||||||
import { Sheet } from "./components/sheet";
|
import { Sheet } from '../../components/sheet';
|
||||||
import { ThemeProvider, createTheme } from "./theme";
|
import { ThemeProvider, createTheme } from '@/theme';
|
||||||
import chroma from "chroma-js";
|
import chroma from 'chroma-js';
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from 'react-helmet-async';
|
||||||
import { Page } from "../../../types";
|
import { Page } from 'types';
|
||||||
|
|
||||||
const GlobalStyle = createGlobalStyle`
|
const GlobalStyle = createGlobalStyle`
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
@@ -32,7 +32,7 @@ const Download = styled.a`
|
|||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
font-family: "Black Ops One", sans-serif;
|
font-family: 'Black Ops One', sans-serif;
|
||||||
@media only screen and (max-width: 700px) {
|
@media only screen and (max-width: 700px) {
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
@@ -49,7 +49,7 @@ const Title = styled(Jumbo)`
|
|||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
font-family: "Black Ops One", sans-serif;
|
font-family: 'Black Ops One', sans-serif;
|
||||||
@media only screen and (max-width: 700px) {
|
@media only screen and (max-width: 700px) {
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
@@ -71,7 +71,7 @@ const Arrow = styled.div`
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
content: "↓";
|
content: '↓';
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
@media only screen and (max-width: 700px) {
|
@media only screen and (max-width: 700px) {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
@@ -99,13 +99,13 @@ const ImageBg = styled.picture`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FrontPage: Page<"frontpage"> = ({ articles, profile }) => {
|
const FrontPage: Page<'frontpage'> = ({ articles, profile }) => {
|
||||||
const theme = useMemo(
|
const theme = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createTheme({
|
createTheme({
|
||||||
baseColor: chroma.random().brighten(1).hex(),
|
baseColor: chroma.random().brighten(1).hex(),
|
||||||
}),
|
}),
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -113,11 +113,7 @@ const FrontPage: Page<"frontpage"> = ({ articles, profile }) => {
|
|||||||
<Helmet>
|
<Helmet>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||||
rel="preconnect"
|
|
||||||
href="https://fonts.gstatic.com"
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
/>
|
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Black+Ops+One&family=Merriweather:wght@400;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Black+Ops+One&family=Merriweather:wght@400;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
@@ -130,12 +126,12 @@ const FrontPage: Page<"frontpage"> = ({ articles, profile }) => {
|
|||||||
</ImageBg>
|
</ImageBg>
|
||||||
<Arrow />
|
<Arrow />
|
||||||
<Hero>
|
<Hero>
|
||||||
{"Hi, I'm Morten".split(" ").map((char, index) => (
|
{"Hi, I'm Morten".split(' ').map((char, index) => (
|
||||||
<Title key={index}>{char}</Title>
|
<Title key={index}>{char}</Title>
|
||||||
))}
|
))}
|
||||||
</Hero>
|
</Hero>
|
||||||
<Hero>
|
<Hero>
|
||||||
{"And I make software".split(" ").map((char, index) => (
|
{'And I make software'.split(' ').map((char, index) => (
|
||||||
<Title key={index}>{char}</Title>
|
<Title key={index}>{char}</Title>
|
||||||
))}
|
))}
|
||||||
</Hero>
|
</Hero>
|
||||||
@@ -147,7 +143,7 @@ const FrontPage: Page<"frontpage"> = ({ articles, profile }) => {
|
|||||||
</Sheet>
|
</Sheet>
|
||||||
<Sheet color="#ef23e2">
|
<Sheet color="#ef23e2">
|
||||||
<Hero>
|
<Hero>
|
||||||
{"Table of Content".split(" ").map((char, index) => (
|
{'Table of Content'.split(' ').map((char, index) => (
|
||||||
<Title key={index}>{char}</Title>
|
<Title key={index}>{char}</Title>
|
||||||
))}
|
))}
|
||||||
</Hero>
|
</Hero>
|
||||||
@@ -11,13 +11,9 @@ type CreateOptions = {
|
|||||||
const isBright = (color: chroma.Color) => color.luminance() > 0.4;
|
const isBright = (color: chroma.Color) => color.luminance() > 0.4;
|
||||||
|
|
||||||
const createTheme = (options: CreateOptions = {}) => {
|
const createTheme = (options: CreateOptions = {}) => {
|
||||||
const baseColor = options.baseColor
|
const baseColor = options.baseColor ? chroma(options.baseColor) : chroma.random();
|
||||||
? chroma(options.baseColor)
|
|
||||||
: chroma.random();
|
|
||||||
const text = isBright(baseColor) ? BLACK : WHITE;
|
const text = isBright(baseColor) ? BLACK : WHITE;
|
||||||
const bg = isBright(baseColor)
|
const bg = isBright(baseColor) ? baseColor.luminance(0.9) : baseColor.luminance(0.01);
|
||||||
? baseColor.luminance(0.9)
|
|
||||||
: baseColor.luminance(0.01);
|
|
||||||
const theme: Theme = {
|
const theme: Theme = {
|
||||||
typography: {
|
typography: {
|
||||||
Jumbo: {
|
Jumbo: {
|
||||||
@@ -57,4 +53,3 @@ const createTheme = (options: CreateOptions = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { createTheme };
|
export { createTheme };
|
||||||
|
|
||||||
|
|||||||
1
content/templates/react/theme/global.d.ts
vendored
1
content/templates/react/theme/global.d.ts
vendored
@@ -3,4 +3,3 @@ import { Theme } from './theme';
|
|||||||
declare module 'styled-components' {
|
declare module 'styled-components' {
|
||||||
export interface DefaultTheme extends Theme {}
|
export interface DefaultTheme extends Theme {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +1,43 @@
|
|||||||
import styled from "styled-components";
|
import styled from 'styled-components';
|
||||||
import { Theme, Typography } from "../theme";
|
import { Theme, Typography } from '../theme';
|
||||||
|
|
||||||
interface TextProps {
|
interface TextProps {
|
||||||
color?: keyof Theme["colors"];
|
color?: keyof Theme['colors'];
|
||||||
bold?: boolean;
|
bold?: boolean;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseText = styled.span<TextProps>`
|
const BaseText = styled.span<TextProps>`
|
||||||
${({ theme }) =>
|
${({ theme }) => (theme.font.family ? `font-family: ${theme.font.family};` : '')}
|
||||||
theme.font.family ? `font-family: ${theme.font.family};` : ""}
|
color: ${({ color, theme }) => (color ? theme.colors[color] : theme.colors.foreground)};
|
||||||
color: ${({ color, theme }) =>
|
font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')};
|
||||||
color ? theme.colors[color] : theme.colors.foreground};
|
|
||||||
font-weight: ${({ bold }) => (bold ? "bold" : "normal")};
|
|
||||||
font-size: ${({ theme }) => theme.font.baseSize}px;
|
font-size: ${({ theme }) => theme.font.baseSize}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const get = (name: keyof Theme["typography"], theme: Theme): Typography => {
|
const get = (name: keyof Theme['typography'], theme: Theme): Typography => {
|
||||||
const typography = theme.typography[name];
|
const typography = theme.typography[name];
|
||||||
return typography;
|
return typography;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createTypography = (name: keyof Theme["typography"]) => {
|
const createTypography = (name: keyof Theme['typography']) => {
|
||||||
const Component = styled(BaseText)<TextProps>`
|
const Component = styled(BaseText)<TextProps>`
|
||||||
font-size: ${({ theme }) =>
|
font-size: ${({ theme }) => theme.font.baseSize * (get(name, theme).size || 1)}px;
|
||||||
theme.font.baseSize * (get(name, theme).size || 1)}px;
|
|
||||||
font-weight: ${({ bold, theme }) =>
|
font-weight: ${({ bold, theme }) =>
|
||||||
typeof bold !== "undefined"
|
typeof bold !== 'undefined' ? 'bold' : get(name, theme).weight || 'normal'};
|
||||||
? "bold"
|
${({ theme }) => (get(name, theme).upperCase ? 'text-transform: uppercase;' : '')}
|
||||||
: get(name, theme).weight || "normal"};
|
|
||||||
${({ theme }) =>
|
|
||||||
get(name, theme).upperCase ? "text-transform: uppercase;" : ""}
|
|
||||||
`;
|
`;
|
||||||
return Component;
|
return Component;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Jumbo = createTypography("Jumbo");
|
const Jumbo = createTypography('Jumbo');
|
||||||
const Title2 = createTypography("Title2");
|
const Title2 = createTypography('Title2');
|
||||||
const Title1 = createTypography("Title1");
|
const Title1 = createTypography('Title1');
|
||||||
const Body1 = createTypography("Body1");
|
const Body1 = createTypography('Body1');
|
||||||
const Overline = createTypography("Overline");
|
const Overline = createTypography('Overline');
|
||||||
const Caption = createTypography("Caption");
|
const Caption = createTypography('Caption');
|
||||||
const Link = createTypography("Link");
|
const Link = createTypography('Link');
|
||||||
|
|
||||||
const types: { [key in keyof Theme["typography"]]: typeof BaseText } = {
|
const types: { [key in keyof Theme['typography']]: typeof BaseText } = {
|
||||||
Jumbo,
|
Jumbo,
|
||||||
Title2,
|
Title2,
|
||||||
Title1,
|
Title1,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"yaml": "^2.2.1"
|
"yaml": "^2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@react-native-community/eslint-config": "^3.2.0",
|
||||||
"@types/chroma-js": "^2.4.0",
|
"@types/chroma-js": "^2.4.0",
|
||||||
"@types/ejs": "^3.1.2",
|
"@types/ejs": "^3.1.2",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
@@ -54,7 +55,9 @@
|
|||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/sharp": "^0.31.1",
|
"@types/sharp": "^0.31.1",
|
||||||
"@types/styled-components": "^5.1.26",
|
"@types/styled-components": "^5.1.26",
|
||||||
|
"eslint": "^8.36.0",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
|
"prettier": "^2.8.7",
|
||||||
"ts-jest": "^29.0.5",
|
"ts-jest": "^29.0.5",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.0.2"
|
"typescript": "^5.0.2"
|
||||||
|
|||||||
1241
pnpm-lock.yaml
generated
1241
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
29
types/config.ts
Normal file
29
types/config.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
interface Config {
|
||||||
|
profile: {
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
frontpage: {
|
||||||
|
react: {
|
||||||
|
template: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
articles: {
|
||||||
|
pattern: string;
|
||||||
|
react: {
|
||||||
|
template: string;
|
||||||
|
};
|
||||||
|
latex: {
|
||||||
|
template: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
resume: {
|
||||||
|
latex: {
|
||||||
|
template: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
positions: {
|
||||||
|
pattern: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Config };
|
||||||
Reference in New Issue
Block a user