mirror of
https://github.com/morten-olsen/morten-olsen.github.io.git
synced 2026-02-08 01:46:28 +01:00
feat: init
This commit is contained in:
5
packages/goodwrites/.gitignore
vendored
Normal file
5
packages/goodwrites/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
/node_modules/
|
||||
/*.logs
|
||||
/.yarn/
|
||||
/dist/
|
||||
16
packages/goodwrites/package.json
Normal file
16
packages/goodwrites/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@morten-olsen/goodwrites",
|
||||
"packageManager": "yarn@3.1.0",
|
||||
"main": "./dist/index.js",
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.25.10",
|
||||
"fs-extra": "^11.1.0",
|
||||
"reading-time": "^1.5.0",
|
||||
"remark": "^13",
|
||||
"remark-behead": "^2.3.3",
|
||||
"unist-util-visit": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^9.0.13"
|
||||
}
|
||||
}
|
||||
3
packages/goodwrites/src/global.d.ts
vendored
Normal file
3
packages/goodwrites/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module 'remark-behead' {
|
||||
export default any;
|
||||
}
|
||||
2
packages/goodwrites/src/index.ts
Normal file
2
packages/goodwrites/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './types';
|
||||
export * from './parse';
|
||||
213
packages/goodwrites/src/parse/index.ts
Normal file
213
packages/goodwrites/src/parse/index.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { Block, Document, LengthTarget } from '../types';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { readFile } from 'fs-extra';
|
||||
import remark from 'remark';
|
||||
import remarkBehead from 'remark-behead';
|
||||
import visit from 'unist-util-visit';
|
||||
import readingTime from 'reading-time';
|
||||
|
||||
type Options = {
|
||||
depth?: number;
|
||||
document: Document;
|
||||
location: string;
|
||||
};
|
||||
|
||||
type ParseBlockOptions = {
|
||||
block: Block | Document;
|
||||
location: string;
|
||||
path: (string | number)[];
|
||||
depth: number;
|
||||
};
|
||||
|
||||
type ReplaceImageOptions = {
|
||||
input: string;
|
||||
cwd: string;
|
||||
fn?: (location: string) => string;
|
||||
};
|
||||
|
||||
type BlockResult = {
|
||||
content: string;
|
||||
type: 'title' | 'file' | 'meta';
|
||||
file?: string;
|
||||
stats?: ReturnType<typeof readingTime>;
|
||||
state?: Exclude<Block, string>['state'];
|
||||
path: (string | number)[];
|
||||
length?: LengthTarget;
|
||||
cwd: string;
|
||||
level?: number;
|
||||
notes?: string;
|
||||
};
|
||||
|
||||
const replaceImagesAction = async ({ input, cwd, fn }: ReplaceImageOptions) => {
|
||||
const walk = () => (tree: any) => {
|
||||
visit(tree, 'image', (node: any) => {
|
||||
if (!fn || !node.url.startsWith('./')) {
|
||||
return;
|
||||
}
|
||||
const resolvedLocation = fn(resolve('/', cwd, node.url));
|
||||
node.url = resolvedLocation;
|
||||
});
|
||||
};
|
||||
const markdown = String(
|
||||
await remark()
|
||||
.use(walk as any)
|
||||
.process(input)
|
||||
);
|
||||
|
||||
return markdown;
|
||||
};
|
||||
|
||||
const partsToArray = (parts: Block | Block[]) => {
|
||||
if (Array.isArray(parts)) {
|
||||
return parts;
|
||||
}
|
||||
return [parts];
|
||||
};
|
||||
|
||||
const parseBlock = async ({
|
||||
block,
|
||||
location,
|
||||
path,
|
||||
depth,
|
||||
}: ParseBlockOptions) => {
|
||||
const result: BlockResult[] = [];
|
||||
if (typeof block === 'string') {
|
||||
block = {
|
||||
file: block,
|
||||
};
|
||||
}
|
||||
|
||||
if ('file' in block) {
|
||||
const contentDepth = block.title ? depth + 1 : depth;
|
||||
const fileLocation = resolve(location, block.file);
|
||||
const fileContent = await readFile(fileLocation, 'utf-8');
|
||||
const markdown = String(
|
||||
await remark()
|
||||
.use(remarkBehead, { depth: contentDepth })
|
||||
.process(fileContent)
|
||||
);
|
||||
const title = block.title
|
||||
? ''.padStart(depth + 1, '#') + ' ' + block.title + '\n'
|
||||
: '';
|
||||
|
||||
result.push({
|
||||
content: `${title}${markdown}`,
|
||||
file: fileLocation,
|
||||
type: 'file',
|
||||
path,
|
||||
cwd: dirname(fileLocation),
|
||||
level: depth,
|
||||
length: block.lenght,
|
||||
state: block.state,
|
||||
stats: readingTime(markdown),
|
||||
notes: block.notes,
|
||||
});
|
||||
} else if (block.title) {
|
||||
result.push({
|
||||
content: ''.padStart(depth + 1, '#') + ' ' + block.title,
|
||||
type: 'title',
|
||||
level: depth,
|
||||
cwd: location,
|
||||
state: 'state' in block ? block.state : undefined,
|
||||
notes: 'notes' in block ? block.notes : undefined,
|
||||
path,
|
||||
});
|
||||
depth += 1;
|
||||
} else if ('notes' in block) {
|
||||
result.push({
|
||||
content: '',
|
||||
type: 'meta',
|
||||
level: depth,
|
||||
cwd: location,
|
||||
notes: 'notes' in block ? block.notes : undefined,
|
||||
state: block.state,
|
||||
path,
|
||||
});
|
||||
}
|
||||
if ('content' in block) {
|
||||
const sectionMarkdown = await Promise.all(
|
||||
partsToArray(block.content).map((s, index) =>
|
||||
parseBlock({
|
||||
block: s,
|
||||
path: [...path, index],
|
||||
location,
|
||||
depth,
|
||||
})
|
||||
)
|
||||
);
|
||||
result.push(...sectionMarkdown.flat());
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const parseDocument = async ({ document, location, depth = 1 }: Options) => {
|
||||
const parts = partsToArray(document.content);
|
||||
const parsedParts = await Promise.all(
|
||||
parts.map((block, index) =>
|
||||
parseBlock({
|
||||
block,
|
||||
location,
|
||||
depth,
|
||||
path: [index],
|
||||
})
|
||||
)
|
||||
);
|
||||
const partsList = parsedParts.flat();
|
||||
const content = partsList.map((p) => p.content).join('\n\n');
|
||||
const stats = readingTime(content);
|
||||
const files = partsList.map((p) => p.file).filter(Boolean);
|
||||
return {
|
||||
title: document.title,
|
||||
content,
|
||||
cover: document.cover,
|
||||
cwd: location,
|
||||
meta: document.meta,
|
||||
stats,
|
||||
files,
|
||||
original: document,
|
||||
parts: partsList,
|
||||
};
|
||||
};
|
||||
|
||||
type ToMarkdownOptions = {
|
||||
replaceImage?: (location: string) => string;
|
||||
};
|
||||
|
||||
const blockToMarkdown = async (
|
||||
block: BlockResult,
|
||||
{ replaceImage }: ToMarkdownOptions
|
||||
) => {
|
||||
if (replaceImage) {
|
||||
return replaceImagesAction({
|
||||
input: block.content,
|
||||
fn: replaceImage,
|
||||
cwd: block.cwd,
|
||||
});
|
||||
}
|
||||
return block.content;
|
||||
};
|
||||
|
||||
const replaceImages = async (
|
||||
result: DocumentResult,
|
||||
options: ToMarkdownOptions = {}
|
||||
) => {
|
||||
const blocks = await Promise.all(
|
||||
result.parts.map(async (block) => ({
|
||||
...block,
|
||||
content: await blockToMarkdown(block, options),
|
||||
}))
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
parts: blocks,
|
||||
content: blocks.map((b) => b.content).join('\n\n'),
|
||||
};
|
||||
};
|
||||
|
||||
type DocumentResult = ReturnType<typeof parseDocument> extends Promise<infer U>
|
||||
? U & { pdf?: string }
|
||||
: never;
|
||||
|
||||
export type { DocumentResult };
|
||||
export { parseDocument, replaceImages };
|
||||
37
packages/goodwrites/src/types/block.ts
Normal file
37
packages/goodwrites/src/types/block.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
type TimeTarget = {
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
};
|
||||
|
||||
type WordTarget = {
|
||||
words: number;
|
||||
};
|
||||
|
||||
type LengthTarget = TimeTarget | WordTarget;
|
||||
|
||||
type BaseBlock = {
|
||||
type?: string;
|
||||
title?: string;
|
||||
lenght?: LengthTarget;
|
||||
notes?: string;
|
||||
state?: 'placeholder' | 'first-draft' | 'revisions' | 'final-draft' | 'final';
|
||||
};
|
||||
|
||||
type FileBlock = BaseBlock & {
|
||||
file: string;
|
||||
};
|
||||
|
||||
type BlocksBlock = BaseBlock & {
|
||||
content: Block[];
|
||||
};
|
||||
|
||||
type Block = string | FileBlock | BlocksBlock;
|
||||
|
||||
export type {
|
||||
Block,
|
||||
BlocksBlock,
|
||||
FileBlock,
|
||||
LengthTarget,
|
||||
TimeTarget,
|
||||
WordTarget,
|
||||
};
|
||||
10
packages/goodwrites/src/types/document.ts
Normal file
10
packages/goodwrites/src/types/document.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Block } from './block';
|
||||
|
||||
type Document = {
|
||||
title: string;
|
||||
cover?: string;
|
||||
meta?: any;
|
||||
content: Block | Block[];
|
||||
};
|
||||
|
||||
export type { Document };
|
||||
2
packages/goodwrites/src/types/index.ts
Normal file
2
packages/goodwrites/src/types/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './block';
|
||||
export * from './document';
|
||||
9
packages/goodwrites/tsconfig.json
Normal file
9
packages/goodwrites/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"./src"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user