vowel 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +8 -0
- package/.vscode/settings.json +3 -0
- package/README.md +123 -27
- package/bin.js +30 -0
- package/content/.cache.json +627 -0
- package/content/.obsidian/app.json +3 -0
- package/content/.obsidian/appearance.json +3 -0
- package/content/.obsidian/core-plugins-migration.json +30 -0
- package/content/.obsidian/core-plugins.json +20 -0
- package/content/.obsidian/workspace.json +168 -0
- package/content/about.md +3 -0
- package/content/assets/open-props.css +1630 -0
- package/content/assets/styles-2.css +128 -0
- package/content/assets/styles-3.css +275 -0
- package/content/docs/file-structure.md +31 -0
- package/content/docs/folder-settings.md +22 -0
- package/content/docs/home.md +8 -0
- package/content/docs/images.md +10 -0
- package/content/docs/pages.md +64 -0
- package/content/docs/quickstart.md +52 -0
- package/content/docs/run-build-deploy.md +20 -0
- package/content/docs/settings.md +4 -0
- package/content/docs/styling.md +10 -0
- package/content/docs/taxonomies.md +37 -0
- package/content/home.md +73 -0
- package/content/roadmap.md +81 -0
- package/content/settings.md +12 -0
- package/content/vercel.json +5 -0
- package/jsconfig.json +18 -0
- package/package.json +37 -19
- package/server.js +80 -0
- package/src/app.d.ts +12 -0
- package/src/app.html +12 -0
- package/src/lib/components/Breadcrumbs.svelte +19 -0
- package/src/lib/components/DefaultStyles.svelte +126 -0
- package/src/lib/components/FrontMatterTaxonomy.svelte +48 -0
- package/src/lib/components/Frontmatter.svelte +54 -0
- package/src/lib/components/FrontmatterProperty.svelte +72 -0
- package/src/lib/components/Markdown/Image.svelte +48 -0
- package/src/lib/components/Markdown/Link.svelte +10 -0
- package/src/lib/components/Markdown/LinkPreview.svelte +39 -0
- package/src/lib/components/Markdown/Text.svelte +6 -0
- package/src/lib/components/Markdown/index.svelte +84 -0
- package/src/lib/components/Markdown/validators.js +29 -0
- package/src/lib/components/Nav.svelte +39 -0
- package/src/lib/components/Page.svelte +59 -0
- package/src/lib/components/Sitemap.svelte +38 -0
- package/src/lib/components/index.js +9 -0
- package/src/lib/index.js +1 -0
- package/src/lib/utilities/buildURL.js +18 -0
- package/src/lib/utilities/checkFileExists.js +16 -0
- package/src/lib/utilities/createFolderClass.js +4 -0
- package/src/lib/utilities/createPageClass.js +4 -0
- package/src/lib/utilities/getFileLabel.js +18 -0
- package/src/lib/utilities/getFolder.js +16 -0
- package/src/lib/utilities/getFolderLabel.js +8 -0
- package/src/lib/utilities/getPage.js +24 -0
- package/src/lib/utilities/getPagesByFolder.js +93 -0
- package/src/lib/utilities/index.js +20 -0
- package/src/lib/utilities/isActiveLink.js +12 -0
- package/src/lib/utilities/isObject.js +8 -0
- package/src/lib/utilities/loadCache.js +28 -0
- package/src/lib/utilities/mutateMarkdownAST.js +59 -0
- package/src/lib/utilities/mutateMarkdownFrontmatter.js +115 -0
- package/src/lib/utilities/parseDate.js +43 -0
- package/src/lib/utilities/processMarkdownFiles.js +212 -0
- package/src/lib/utilities/readMarkdownFile.js +134 -0
- package/src/lib/utilities/regexPatterns.js +12 -0
- package/src/lib/utilities/resolveHomeDirPath.js +5 -0
- package/src/lib/utilities/writeCache.js +14 -0
- package/src/routes/[...path]/+layout.server.js +71 -0
- package/src/routes/[...path]/+page.server.js +22 -0
- package/src/routes/[...path]/+page.svelte +125 -0
- package/src/routes/feed.xml/+server.js +120 -0
- package/src/routes/robots.txt/+server.js +54 -0
- package/src/routes/sitemap.xml/+server.js +68 -0
- package/static/favicon.png +0 -0
- package/static/styles.css +0 -0
- package/svelte.config.js +32 -0
- package/vercel.json +5 -0
- package/vite.config.js +72 -0
- package/index.js +0 -28
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import urlMetadata from 'url-metadata';
|
|
2
|
+
|
|
3
|
+
export default async function mutateMarkdownAST(ast, cache) {
|
|
4
|
+
const promises = ast.map(async (node) => {
|
|
5
|
+
// TODO: Improve this URL regex
|
|
6
|
+
if (node.type === 'paragraph') {
|
|
7
|
+
// URLs
|
|
8
|
+
if (
|
|
9
|
+
node.children?.length === 1 &&
|
|
10
|
+
node.children[0]?.value?.match(/^https?:\/\/\S+$/) &&
|
|
11
|
+
!node.children[0]?.children
|
|
12
|
+
) {
|
|
13
|
+
node.type = 'url';
|
|
14
|
+
const url = node.children[0].value;
|
|
15
|
+
if (!cache[node.value]) {
|
|
16
|
+
try {
|
|
17
|
+
const urlObject = new URL(url);
|
|
18
|
+
const response = await urlMetadata(urlObject.href, {
|
|
19
|
+
includeResponseBody: false,
|
|
20
|
+
ensureSecureImageRequest: true
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const metadata = {
|
|
24
|
+
image: response['og:image'],
|
|
25
|
+
ogURL: response['og:url'],
|
|
26
|
+
canonicalURL: response.canonical,
|
|
27
|
+
title: response.title,
|
|
28
|
+
ogTitle: response['og:title'],
|
|
29
|
+
author: response.author,
|
|
30
|
+
description: response.description
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
cache[url] = metadata;
|
|
34
|
+
node.metadata = metadata;
|
|
35
|
+
node.value = url;
|
|
36
|
+
|
|
37
|
+
delete node.children;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
node.value = url;
|
|
40
|
+
console.log(`Error on URL in content: ${url}`);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
node.metadata = cache[node.value];
|
|
44
|
+
}
|
|
45
|
+
} else if (node.children[0].type === 'image') {
|
|
46
|
+
node.type = 'figure';
|
|
47
|
+
if (node?.children[1]?.type === 'text') {
|
|
48
|
+
node.children[1].type = 'figcaption';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (node.children) {
|
|
54
|
+
await mutateMarkdownAST(node.children, cache);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await Promise.all(promises);
|
|
59
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import urlMetadata from 'url-metadata';
|
|
2
|
+
import { regexPatterns, isObject, parseDate } from '.';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Description
|
|
6
|
+
* @param {any} value - Any frontmatter value
|
|
7
|
+
* @returns {any}
|
|
8
|
+
*/
|
|
9
|
+
function imputeType(value) {
|
|
10
|
+
if (value === null || value === undefined || Number.isNaN(value)) return 'nullish';
|
|
11
|
+
if (typeof value === 'boolean') return 'boolean';
|
|
12
|
+
if (typeof value === 'number') return 'number';
|
|
13
|
+
if (parseDate(value)) return 'date';
|
|
14
|
+
if (isObject(value)) return 'object';
|
|
15
|
+
if (Array.isArray(value)) return 'array';
|
|
16
|
+
if (typeof value === 'string') {
|
|
17
|
+
if (value.match(regexPatterns.img)) return 'image';
|
|
18
|
+
if (value.match(regexPatterns.pdf)) return 'pdf';
|
|
19
|
+
if (value.match(regexPatterns.url)) return 'url';
|
|
20
|
+
if (value.match(regexPatterns.path)) return 'path';
|
|
21
|
+
return 'string';
|
|
22
|
+
}
|
|
23
|
+
console.error('Unknown frontmatter data type: ' + value);
|
|
24
|
+
return 'other';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default async function mutateMarkdownFrontmatter(frontmatter, cache) {
|
|
28
|
+
const keys = Object.keys(frontmatter);
|
|
29
|
+
|
|
30
|
+
const promises = keys.map(async (key) => {
|
|
31
|
+
const input = frontmatter[key];
|
|
32
|
+
|
|
33
|
+
const type = imputeType(input);
|
|
34
|
+
|
|
35
|
+
switch (type) {
|
|
36
|
+
case 'object': {
|
|
37
|
+
await mutateMarkdownFrontmatter(input, cache);
|
|
38
|
+
frontmatter[key] = {
|
|
39
|
+
type: 'object',
|
|
40
|
+
output: input
|
|
41
|
+
};
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case 'array': {
|
|
45
|
+
await mutateMarkdownFrontmatter(input, cache);
|
|
46
|
+
frontmatter[key] = {
|
|
47
|
+
type: 'array',
|
|
48
|
+
output: input
|
|
49
|
+
};
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 'date': {
|
|
53
|
+
frontmatter[key] = {
|
|
54
|
+
type: 'date',
|
|
55
|
+
output: parseDate(input),
|
|
56
|
+
input
|
|
57
|
+
};
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case 'image': {
|
|
61
|
+
frontmatter[key] = {
|
|
62
|
+
type: 'image',
|
|
63
|
+
output: input,
|
|
64
|
+
input
|
|
65
|
+
};
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'pdf': {
|
|
69
|
+
frontmatter[key] = {
|
|
70
|
+
type: 'pdf',
|
|
71
|
+
output: input,
|
|
72
|
+
input
|
|
73
|
+
};
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case 'url': {
|
|
77
|
+
if (!cache[input]) {
|
|
78
|
+
try {
|
|
79
|
+
const url = new URL(input);
|
|
80
|
+
const metadata = await urlMetadata(url.href, {
|
|
81
|
+
includeResponseBody: false,
|
|
82
|
+
ensureSecureImageRequest: true
|
|
83
|
+
});
|
|
84
|
+
cache[input] = metadata;
|
|
85
|
+
|
|
86
|
+
frontmatter[key] = {
|
|
87
|
+
type: 'url',
|
|
88
|
+
output: metadata,
|
|
89
|
+
input
|
|
90
|
+
};
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.error(`Error on URL in frontmatter: ${frontmatter[key]}`);
|
|
93
|
+
frontmatter[key] = {
|
|
94
|
+
type: 'string',
|
|
95
|
+
output: input,
|
|
96
|
+
input
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
frontmatter[key] = {
|
|
101
|
+
type: 'url',
|
|
102
|
+
output: cache[input],
|
|
103
|
+
input
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case 'nullish': {
|
|
109
|
+
frontmatter[key] = undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await Promise.all(promises);
|
|
115
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import dates from 'any-date-parser';
|
|
2
|
+
import ago from 'any-date-parser/src/formats/ago/ago.js';
|
|
3
|
+
import chinese from 'any-date-parser/src/formats/chinese/chinese.js';
|
|
4
|
+
import dayMonth from 'any-date-parser/src/formats/dayMonth/dayMonth.js';
|
|
5
|
+
import dayMonthname from 'any-date-parser/src/formats/dayMonthname/dayMonthname.js';
|
|
6
|
+
import monthDay from 'any-date-parser/src/formats/monthDay/monthDay.js';
|
|
7
|
+
import monthnameDay from 'any-date-parser/src/formats/monthnameDay/monthnameDay.js';
|
|
8
|
+
import today from 'any-date-parser/src/formats/today/today.js';
|
|
9
|
+
|
|
10
|
+
dates.removeFormat(ago);
|
|
11
|
+
dates.removeFormat(chinese);
|
|
12
|
+
dates.removeFormat(dayMonth);
|
|
13
|
+
dates.removeFormat(dayMonthname);
|
|
14
|
+
dates.removeFormat(monthDay);
|
|
15
|
+
dates.removeFormat(monthnameDay);
|
|
16
|
+
dates.removeFormat(today);
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
Left in the following formats:
|
|
20
|
+
|
|
21
|
+
microsoftJson
|
|
22
|
+
dayMonthYear
|
|
23
|
+
dayMonthnameYear
|
|
24
|
+
monthDayYear
|
|
25
|
+
monthnameDayYear
|
|
26
|
+
twitter
|
|
27
|
+
yearMonthDay
|
|
28
|
+
atSeconds
|
|
29
|
+
time12Hours
|
|
30
|
+
time24Hours
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export default function parseDate(maybeDate) {
|
|
34
|
+
if (!maybeDate) return false;
|
|
35
|
+
if (maybeDate instanceof Date) return maybeDate;
|
|
36
|
+
if (typeof maybeDate === 'number') return false;
|
|
37
|
+
if (typeof maybeDate === 'string' && maybeDate.match(/^\d+$/)) return false;
|
|
38
|
+
const parsedDate = dates.attempt(maybeDate);
|
|
39
|
+
if (parsedDate.invalid) return false;
|
|
40
|
+
const { month, day, year } = parsedDate;
|
|
41
|
+
const date = new Date(`${year}-${month}-${day}`);
|
|
42
|
+
return date;
|
|
43
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import { buildURL, checkFileExists, readMarkdownFile } from '.';
|
|
5
|
+
|
|
6
|
+
function DefaultFile(url) {
|
|
7
|
+
return {
|
|
8
|
+
ast: [
|
|
9
|
+
{
|
|
10
|
+
type: 'paragraph',
|
|
11
|
+
children: [
|
|
12
|
+
{
|
|
13
|
+
type: 'h1',
|
|
14
|
+
value: 'hey there',
|
|
15
|
+
// value: url + '?count=10',
|
|
16
|
+
position: {
|
|
17
|
+
start: {
|
|
18
|
+
line: 5,
|
|
19
|
+
column: 1,
|
|
20
|
+
offset: 25
|
|
21
|
+
},
|
|
22
|
+
end: {
|
|
23
|
+
line: 5,
|
|
24
|
+
column: 8,
|
|
25
|
+
offset: 32
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
position: {
|
|
31
|
+
start: {
|
|
32
|
+
line: 5,
|
|
33
|
+
column: 1,
|
|
34
|
+
offset: 25
|
|
35
|
+
},
|
|
36
|
+
end: {
|
|
37
|
+
line: 5,
|
|
38
|
+
column: 8,
|
|
39
|
+
offset: 32
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
url
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function DefaultDirectory(path) {
|
|
49
|
+
return {
|
|
50
|
+
$: DefaultFile(path),
|
|
51
|
+
_: DefaultFile(path)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @typedef {Object} AdditionalFileProperties
|
|
56
|
+
* @property {string} [url] - URL to the page for the file.
|
|
57
|
+
* @property {number} [date] - Document date.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/** @typedef {import("./readMarkdownFile").ParsedMarkdown & AdditionalFileProperties} MarkdownFile */
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {Object} Subdirectory
|
|
64
|
+
* @description A subdirectory of markdown files
|
|
65
|
+
* @property {MarkdownFile} $ A home file
|
|
66
|
+
* @property {MarkdownFile} _ A settingsfile
|
|
67
|
+
* @property {Object.<string, Object>} [additionalProps] Files or child directories.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @typedef {Object.<string, MarkdownFile | Subdirectory>} Directory
|
|
72
|
+
* @description A directory of markdown files
|
|
73
|
+
* @property {MarkdownFile} $ A home file.
|
|
74
|
+
* @property {MarkdownFile} _ A settings file.
|
|
75
|
+
* @property {Object.<string, MarkdownFile | Subdirectory>} [additionalProps] Files or child directories.
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Recursively read a directory of markdown files and return an object of the files.
|
|
80
|
+
* @param {string} directoryPath - The directory to read.
|
|
81
|
+
* @param {string} parents - A string representing the parent directories.
|
|
82
|
+
* @param {import('./loadCache').Cache} cache
|
|
83
|
+
* @returns {Promise<Directory>}
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Recursively read a directory of markdown files and return an object of the files.
|
|
88
|
+
* @param {string} folderPath - The directory to read.
|
|
89
|
+
* @param {string} parents - A string representing the parent directories.
|
|
90
|
+
* @param {import('./loadCache').Cache} cache
|
|
91
|
+
* @returns {Promise<Directory>}
|
|
92
|
+
*/
|
|
93
|
+
async function readFolder(folderPath, parents, cache) {
|
|
94
|
+
function filterFiles(file) {
|
|
95
|
+
if (file.name.startsWith('.')) return false;
|
|
96
|
+
if (file.name.startsWith('README')) return false;
|
|
97
|
+
if (file.isFile() && !file.name.endsWith('.md')) return false;
|
|
98
|
+
if (file.name === 'node_modules') return false;
|
|
99
|
+
if (file.name === 'assets') return false;
|
|
100
|
+
if (file.name === 'templates') return false;
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let files = (await fs.readdir(folderPath, { withFileTypes: true })).filter(filterFiles);
|
|
105
|
+
|
|
106
|
+
const promises = files.map(async (file) => await readFile(file, parents, cache, folderPath));
|
|
107
|
+
|
|
108
|
+
const folder = (await Promise.all(promises)).reduce((acc, obj) => ({ ...acc, ...obj }), {});
|
|
109
|
+
|
|
110
|
+
if (typeof folder === 'object' && !folder?.$) {
|
|
111
|
+
folder.$ = DefaultFile(parents || '/');
|
|
112
|
+
folder.$.imputedProperties = {
|
|
113
|
+
fileName: parents.split('/').at(-1) || 'Home'
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (typeof folder === 'object' && !folder?._) {
|
|
118
|
+
folder._ = DefaultFile(parents);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return folder;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function readFile(file, parents, cache, folderPath) {
|
|
125
|
+
const route = path.parse(file.name).name;
|
|
126
|
+
const url = buildURL(parents, route);
|
|
127
|
+
const filePath = path.join(folderPath, file.name);
|
|
128
|
+
|
|
129
|
+
const extension = path.extname(file.name);
|
|
130
|
+
|
|
131
|
+
if (file.isFile() && extension === '.md') {
|
|
132
|
+
const startLoad = performance.now();
|
|
133
|
+
const shortPath = parents + '/' + file.name;
|
|
134
|
+
|
|
135
|
+
const { ast, frontmatter, imputedProperties } = await readMarkdownFile(filePath, cache);
|
|
136
|
+
const loadTime = (performance.now() - startLoad).toFixed(2);
|
|
137
|
+
console.log(`📄 ${shortPath} (${loadTime}ms)`);
|
|
138
|
+
if (frontmatter.published == false) return;
|
|
139
|
+
if (file.name === 'home.md' || file.name === 'settings.md') {
|
|
140
|
+
const key = file.name === 'home.md' ? '$' : '_';
|
|
141
|
+
return {
|
|
142
|
+
[key]: {
|
|
143
|
+
...frontmatter,
|
|
144
|
+
imputedProperties,
|
|
145
|
+
ast,
|
|
146
|
+
url
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
[route]: {
|
|
152
|
+
$: {
|
|
153
|
+
...frontmatter,
|
|
154
|
+
imputedProperties,
|
|
155
|
+
ast,
|
|
156
|
+
url
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** @type Directory */
|
|
163
|
+
if (file.isDirectory()) {
|
|
164
|
+
const settingsPath = path.join(folderPath, file.name, 'settings.md');
|
|
165
|
+
const settingsFileExists = await checkFileExists(settingsPath);
|
|
166
|
+
|
|
167
|
+
/** @type {MarkdownFile} */
|
|
168
|
+
const _ = settingsFileExists ? await readMarkdownFile(settingsPath, cache) : DefaultFile(url);
|
|
169
|
+
|
|
170
|
+
const shortPath = parents + '/' + file.name;
|
|
171
|
+
|
|
172
|
+
const startLoad = performance.now();
|
|
173
|
+
const folder = await readFolder(path.join(folderPath, file.name), url, cache);
|
|
174
|
+
|
|
175
|
+
const loadTime = (performance.now() - startLoad).toFixed(2);
|
|
176
|
+
console.log(`📁 ${shortPath} (${loadTime}ms)`);
|
|
177
|
+
|
|
178
|
+
if (!folder.hasOwnProperty('$')) {
|
|
179
|
+
const $ = DefaultFile(url);
|
|
180
|
+
Object.assign(folder, { $ });
|
|
181
|
+
}
|
|
182
|
+
if (!folder.hasOwnProperty('_')) {
|
|
183
|
+
const _ = DefaultFile(url);
|
|
184
|
+
Object.assign(folder, { _ });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
[file.name]: folder
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @typedef {Object} ProcessedFiles
|
|
195
|
+
* @description The return value of the processing function.
|
|
196
|
+
* @property {Directory} folder - The root directory.
|
|
197
|
+
* @property {Cache} finalCache - The application cache.
|
|
198
|
+
*/
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Recursively reads the markdown files in a directory and returns a representative object.
|
|
202
|
+
* @param {import('./loadCache').Cache} cache
|
|
203
|
+
* @returns {Promise<ProcessedFiles>}
|
|
204
|
+
*/
|
|
205
|
+
export default async function processMarkdownFiles(cache) {
|
|
206
|
+
/** @type {Array<string>} */
|
|
207
|
+
// @ts-ignore
|
|
208
|
+
const [homeDir] = $home;
|
|
209
|
+
const folder = await readFolder(homeDir, '', cache);
|
|
210
|
+
|
|
211
|
+
return { folder, finalCache: cache };
|
|
212
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { remark } from 'remark';
|
|
3
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
4
|
+
import remarkHTML from 'remark-html';
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
import { toString } from 'mdast-util-to-string';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
import { mutateMarkdownAST, mutateMarkdownFrontmatter } from '.';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} FrontmatterNode
|
|
13
|
+
* @property {"yaml"} type - The type is yaml.
|
|
14
|
+
* @property {string} value - The value of the node.
|
|
15
|
+
* @property {Object} [position] - The position of the node.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse a frontmatter node to a JS object.
|
|
20
|
+
* @param {FrontmatterNode | undefined} frontmatterNode - A markdown node of type "yaml".
|
|
21
|
+
* @returns {Object} A JS object containing the frontmatter
|
|
22
|
+
*/
|
|
23
|
+
function parseFrontmatter(frontmatterNode) {
|
|
24
|
+
if (!frontmatterNode) return {};
|
|
25
|
+
const frontmatter = yaml.load(frontmatterNode.value);
|
|
26
|
+
if (frontmatter && typeof frontmatter === 'object') {
|
|
27
|
+
return frontmatter;
|
|
28
|
+
}
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function deduceDescriptionFromAST(ast) {
|
|
33
|
+
for (let i = 0; i < ast?.children?.length; i++) {
|
|
34
|
+
if (ast?.children?.[i]?.type === 'paragraph') {
|
|
35
|
+
const firstParagraph = toString(ast[i]);
|
|
36
|
+
if (firstParagraph.length > 150) {
|
|
37
|
+
return firstParagraph.slice(0, 150);
|
|
38
|
+
}
|
|
39
|
+
return firstParagraph;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function demoteHeadings(ast, start) {
|
|
46
|
+
for (let i = start; i < ast.length; i++) {
|
|
47
|
+
const node = ast[i];
|
|
48
|
+
if (node.type === 'heading') {
|
|
49
|
+
node.depth = node.depth + 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function checkHeadings(ast, start = 1) {
|
|
55
|
+
for (let i = start; i < ast.length; i++) {
|
|
56
|
+
if (ast[i]?.depth === 1) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractTitleFromHeading(node) {
|
|
64
|
+
return node?.children?.[0].value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function imputeTitleFromAST(ast) {
|
|
68
|
+
if (ast?.[0]?.depth === 1) {
|
|
69
|
+
return extractTitleFromHeading(ast[0]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @typedef {Object} ParsedMarkdown
|
|
77
|
+
* @property {Object.<string, *>} [frontmatter] - Metadata for the document.
|
|
78
|
+
* @property {Array<Object>} ast - The content of the document.
|
|
79
|
+
* @property {Object} imputedProperties - Metadata imputed from content.
|
|
80
|
+
* @property {string} [url] - URL for the page.
|
|
81
|
+
* @description An object containing the parsed frontmatter and filtered AST.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reads a Markdown file and parses its content into frontmatter and abstract syntax tree (AST).
|
|
86
|
+
* It also filters out YAML frontmatter nodes from the AST.
|
|
87
|
+
*
|
|
88
|
+
* @async
|
|
89
|
+
* @function
|
|
90
|
+
* @param {string} filePath - The path to the Markdown file.
|
|
91
|
+
* @param {Object} cache - An optional cache object that can be passed to mutate the AST.
|
|
92
|
+
* @returns {Promise<ParsedMarkdown>}
|
|
93
|
+
* @throws {Error} Will throw an error if the file cannot be read or parsed.
|
|
94
|
+
*/
|
|
95
|
+
export default async function readMarkdownFile(filePath, cache) {
|
|
96
|
+
const fileName = path.basename(filePath.slice(0, -3));
|
|
97
|
+
|
|
98
|
+
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
99
|
+
const markdownParser = remark().use(remarkFrontmatter, ['yaml']).use(remarkHTML);
|
|
100
|
+
|
|
101
|
+
const parsedMarkdown = markdownParser.parse(fileContent);
|
|
102
|
+
const frontmatterNode = parsedMarkdown.children.find((node) => node.type === 'yaml');
|
|
103
|
+
const frontmatter = frontmatterNode?.type === 'yaml' ? parseFrontmatter(frontmatterNode) : {};
|
|
104
|
+
const ast = remark().use(remarkFrontmatter).parse(fileContent);
|
|
105
|
+
const filteredAST = ast.children.filter((child) => child.type !== 'yaml');
|
|
106
|
+
|
|
107
|
+
const imputedTitle = imputeTitleFromAST(filteredAST);
|
|
108
|
+
|
|
109
|
+
if (imputedTitle) {
|
|
110
|
+
filteredAST.shift();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (imputedTitle && checkHeadings(filteredAST, 1)) {
|
|
114
|
+
demoteHeadings(filteredAST, 1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!frontmatter.title && imputedTitle) frontmatter.title = imputedTitle;
|
|
118
|
+
|
|
119
|
+
const imputedProperties = {
|
|
120
|
+
description: deduceDescriptionFromAST(ast),
|
|
121
|
+
title: imputedTitle,
|
|
122
|
+
fileName
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const promises = [mutateMarkdownAST(ast.children, cache)];
|
|
126
|
+
|
|
127
|
+
// Don't mutate frontmatter on the settings pages
|
|
128
|
+
if (!filePath.endsWith('settings.md'))
|
|
129
|
+
promises.push(mutateMarkdownFrontmatter(frontmatter, cache));
|
|
130
|
+
|
|
131
|
+
await Promise.all(promises);
|
|
132
|
+
|
|
133
|
+
return { frontmatter, ast: filteredAST, imputedProperties };
|
|
134
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const img = /^https?:\/\/\S+(avif|gif|heif|jpeg|jpg|png|tiff|webp)(\?\S+)?/;
|
|
2
|
+
const pdf = /^https?:\/\/\S+(pdf)(\?\S+)?/;
|
|
3
|
+
const url = /^https?:\/\/\S+$/;
|
|
4
|
+
const path = /^\/[\/\S]+$/;
|
|
5
|
+
// const humanDate = /^((Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|January|February|March|April|May|June|July|August|September|October|November|December)[\d\040,\/(nd|rd|st|th)]{8,12}|[\d\040,\/(nd|rd|st|th)]{1,5}(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|January|February|March|April|May|June|July|August|September|October|November|December),?\040\d{2,4})$/;
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
url,
|
|
9
|
+
img,
|
|
10
|
+
path,
|
|
11
|
+
pdf
|
|
12
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { resolveHomeDirPath } from '.';
|
|
3
|
+
|
|
4
|
+
export default async function writeCache(data, homeDir) {
|
|
5
|
+
const cachePath = resolveHomeDirPath('.cache.json', homeDir);
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const jsonData = JSON.stringify(data, null, 2);
|
|
9
|
+
await fs.writeFile(cachePath, jsonData, 'utf8');
|
|
10
|
+
console.log('Cache updated');
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error('Error updating cache data:', error);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { loadCache, writeCache, processMarkdownFiles } from '$lib/utilities';
|
|
2
|
+
import { access } from 'fs/promises';
|
|
3
|
+
import { constants } from 'fs';
|
|
4
|
+
import { normalize, join, basename } from 'path';
|
|
5
|
+
|
|
6
|
+
export const prerender = true;
|
|
7
|
+
// export const csr = false;
|
|
8
|
+
// export const ssr = false;
|
|
9
|
+
|
|
10
|
+
let contentCache;
|
|
11
|
+
let contentCacheTime;
|
|
12
|
+
|
|
13
|
+
async function checkFileExists(filePath, homeDir) {
|
|
14
|
+
try {
|
|
15
|
+
await access(join(homeDir, filePath), constants.F_OK);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @type {import('./$types').PageLoad} */
|
|
23
|
+
export async function load() {
|
|
24
|
+
const startLoad = performance.now();
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const contentCacheDuration = 60000; // Cache for 1 minute
|
|
27
|
+
const initialURLCache = await loadCache($home[0]);
|
|
28
|
+
|
|
29
|
+
const cssExists = await checkFileExists(normalize('/assets/styles.css'), $home[0]);
|
|
30
|
+
|
|
31
|
+
if (cssExists) console.log('Found CSS file at /assets/styles.css');
|
|
32
|
+
else console.log('No CSS file found as /assets/styles.css');
|
|
33
|
+
|
|
34
|
+
const faviconExists = await checkFileExists(normalize('/assets/favicon.png'), $home[0]);
|
|
35
|
+
|
|
36
|
+
const files = {
|
|
37
|
+
css: {
|
|
38
|
+
exists: cssExists,
|
|
39
|
+
path: join($home[0], 'assets', 'styles.css')
|
|
40
|
+
},
|
|
41
|
+
favicon: {
|
|
42
|
+
exists: faviconExists,
|
|
43
|
+
path: join($home[0], 'assets', 'favicon.png')
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let data = {};
|
|
48
|
+
|
|
49
|
+
if (contentCache && now - contentCacheTime < contentCacheDuration) {
|
|
50
|
+
console.log('Using content cache');
|
|
51
|
+
data = contentCache;
|
|
52
|
+
} else {
|
|
53
|
+
console.log('Not using content cache');
|
|
54
|
+
data = await processMarkdownFiles(initialURLCache);
|
|
55
|
+
contentCache = data;
|
|
56
|
+
contentCacheTime = now;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// const { folder: website, finalCache } = await processMarkdownFiles(initialCache);
|
|
60
|
+
const { folder: website, finalCache } = data;
|
|
61
|
+
|
|
62
|
+
if (JSON.stringify(initialURLCache) !== JSON.stringify(finalCache)) {
|
|
63
|
+
writeCache(finalCache, $home[0]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(`Load time: ${(performance.now() - startLoad).toFixed(2)} ms`);
|
|
67
|
+
|
|
68
|
+
const folderName = basename($home[0]);
|
|
69
|
+
|
|
70
|
+
return { website, homeDir: $home[0], folderName, files };
|
|
71
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getPagesByFolder, processMarkdownFiles, loadCache } from '../../lib/utilities';
|
|
2
|
+
|
|
3
|
+
/** @type {import('./$types').PageLoad} */
|
|
4
|
+
export async function load({ params }) {
|
|
5
|
+
const { path } = params;
|
|
6
|
+
const segments = path ? params.path.split('/') : [];
|
|
7
|
+
return { path, segments };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const prerender = true;
|
|
11
|
+
|
|
12
|
+
export async function entries() {
|
|
13
|
+
const initialCache = await loadCache($home[0]);
|
|
14
|
+
|
|
15
|
+
const { folder: website } = await processMarkdownFiles(initialCache);
|
|
16
|
+
|
|
17
|
+
const pages = getPagesByFolder(website, '/', false).map((page) => {
|
|
18
|
+
return { path: page.url?.replace('/', '') };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return pages;
|
|
22
|
+
}
|