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.
Files changed (82) hide show
  1. package/.prettierrc +8 -0
  2. package/.vscode/settings.json +3 -0
  3. package/README.md +123 -27
  4. package/bin.js +30 -0
  5. package/content/.cache.json +627 -0
  6. package/content/.obsidian/app.json +3 -0
  7. package/content/.obsidian/appearance.json +3 -0
  8. package/content/.obsidian/core-plugins-migration.json +30 -0
  9. package/content/.obsidian/core-plugins.json +20 -0
  10. package/content/.obsidian/workspace.json +168 -0
  11. package/content/about.md +3 -0
  12. package/content/assets/open-props.css +1630 -0
  13. package/content/assets/styles-2.css +128 -0
  14. package/content/assets/styles-3.css +275 -0
  15. package/content/docs/file-structure.md +31 -0
  16. package/content/docs/folder-settings.md +22 -0
  17. package/content/docs/home.md +8 -0
  18. package/content/docs/images.md +10 -0
  19. package/content/docs/pages.md +64 -0
  20. package/content/docs/quickstart.md +52 -0
  21. package/content/docs/run-build-deploy.md +20 -0
  22. package/content/docs/settings.md +4 -0
  23. package/content/docs/styling.md +10 -0
  24. package/content/docs/taxonomies.md +37 -0
  25. package/content/home.md +73 -0
  26. package/content/roadmap.md +81 -0
  27. package/content/settings.md +12 -0
  28. package/content/vercel.json +5 -0
  29. package/jsconfig.json +18 -0
  30. package/package.json +37 -19
  31. package/server.js +80 -0
  32. package/src/app.d.ts +12 -0
  33. package/src/app.html +12 -0
  34. package/src/lib/components/Breadcrumbs.svelte +19 -0
  35. package/src/lib/components/DefaultStyles.svelte +126 -0
  36. package/src/lib/components/FrontMatterTaxonomy.svelte +48 -0
  37. package/src/lib/components/Frontmatter.svelte +54 -0
  38. package/src/lib/components/FrontmatterProperty.svelte +72 -0
  39. package/src/lib/components/Markdown/Image.svelte +48 -0
  40. package/src/lib/components/Markdown/Link.svelte +10 -0
  41. package/src/lib/components/Markdown/LinkPreview.svelte +39 -0
  42. package/src/lib/components/Markdown/Text.svelte +6 -0
  43. package/src/lib/components/Markdown/index.svelte +84 -0
  44. package/src/lib/components/Markdown/validators.js +29 -0
  45. package/src/lib/components/Nav.svelte +39 -0
  46. package/src/lib/components/Page.svelte +59 -0
  47. package/src/lib/components/Sitemap.svelte +38 -0
  48. package/src/lib/components/index.js +9 -0
  49. package/src/lib/index.js +1 -0
  50. package/src/lib/utilities/buildURL.js +18 -0
  51. package/src/lib/utilities/checkFileExists.js +16 -0
  52. package/src/lib/utilities/createFolderClass.js +4 -0
  53. package/src/lib/utilities/createPageClass.js +4 -0
  54. package/src/lib/utilities/getFileLabel.js +18 -0
  55. package/src/lib/utilities/getFolder.js +16 -0
  56. package/src/lib/utilities/getFolderLabel.js +8 -0
  57. package/src/lib/utilities/getPage.js +24 -0
  58. package/src/lib/utilities/getPagesByFolder.js +93 -0
  59. package/src/lib/utilities/index.js +20 -0
  60. package/src/lib/utilities/isActiveLink.js +12 -0
  61. package/src/lib/utilities/isObject.js +8 -0
  62. package/src/lib/utilities/loadCache.js +28 -0
  63. package/src/lib/utilities/mutateMarkdownAST.js +59 -0
  64. package/src/lib/utilities/mutateMarkdownFrontmatter.js +115 -0
  65. package/src/lib/utilities/parseDate.js +43 -0
  66. package/src/lib/utilities/processMarkdownFiles.js +212 -0
  67. package/src/lib/utilities/readMarkdownFile.js +134 -0
  68. package/src/lib/utilities/regexPatterns.js +12 -0
  69. package/src/lib/utilities/resolveHomeDirPath.js +5 -0
  70. package/src/lib/utilities/writeCache.js +14 -0
  71. package/src/routes/[...path]/+layout.server.js +71 -0
  72. package/src/routes/[...path]/+page.server.js +22 -0
  73. package/src/routes/[...path]/+page.svelte +125 -0
  74. package/src/routes/feed.xml/+server.js +120 -0
  75. package/src/routes/robots.txt/+server.js +54 -0
  76. package/src/routes/sitemap.xml/+server.js +68 -0
  77. package/static/favicon.png +0 -0
  78. package/static/styles.css +0 -0
  79. package/svelte.config.js +32 -0
  80. package/vercel.json +5 -0
  81. package/vite.config.js +72 -0
  82. package/index.js +0 -28
@@ -0,0 +1,73 @@
1
+ # Home
2
+
3
+ Vowel is a very simple Markdown- and CSS-based website generator.
4
+
5
+ To try it out, `cd` into a directory of markdown files and run:
6
+
7
+ ```
8
+ npx -p svelte@next -p vowel@latest npx vowel
9
+ ```
10
+
11
+ (This `npx` command forces the Svelte 5 peer depedency, without which the app will throw an error.)
12
+
13
+ ![A woman dancing](https://littlefair.imgix.net/florence-tango.jpeg?width=2048&auto=format%2Ccompress) Here's a photo
14
+
15
+ Vowel is a minimal website framework for developers who love to write CSS and read RSS.
16
+
17
+ Vowel will generate a feature-rich HTML website from a directory of markdown files. The output is plain HTML. You can style it however you want with your own CSS.
18
+
19
+ See a [demo on StackBlitz](https://stackblitz.com/edit/stackblitz-starters-nh7m7v?file=home.md). (**Error:** This demo might not work due conflicts in Svelte 5's peer dependencies.)
20
+
21
+ Vowel is created by Sam Littlefair. If you have questions or comments, [reach out to me on Twitter](https://twitter.com/samlfair).
22
+
23
+ **Warning!** This project is still in early development. There are many bugs and the API is changing quickly.
24
+
25
+ # Features
26
+
27
+ ## 🗺️ Sitemap
28
+
29
+ Submit your website to search engines with a sitemap that lists all of your pages.
30
+
31
+ ## 📡 RSS
32
+
33
+ Deliver your content to RSS readers with an Atom feed that lists your full posts in reverse-chronology.
34
+
35
+ ## ⚡️ Speed
36
+
37
+ Get a blazing-fast, Svelte-rendered website.
38
+
39
+ ## 🗿 Static generation
40
+
41
+ Load pages quickly with plain HTML.
42
+
43
+ ## 🔒 Copyright
44
+
45
+ Display an up-to-date copyright in the footer of your website.
46
+
47
+ ## 🔗 Rich link previews
48
+
49
+ Create rich links with titles and images, both internally and externally.
50
+
51
+ ## 🚏 Navigation
52
+
53
+ Structure your content with a header, nav, sitemap, and breadcrumbs.
54
+
55
+ ## 🙃 Emoji favicon
56
+
57
+ Use any emoji you want for your website's icon.
58
+
59
+ ## 🖇️ Custom taxonomies
60
+
61
+ Go beyond tags to create authors and categories.
62
+
63
+ ## 🧮 Advanced frontmatter
64
+
65
+ Display dates, lists, nested properties, links, and images in your frontmatter.
66
+
67
+ ## 👩‍🎤 Live editing
68
+
69
+ See updates live as you save.
70
+
71
+ # Try it
72
+
73
+ /docs/quickstart
@@ -0,0 +1,81 @@
1
+ ---
2
+ description: What's planned.
3
+ ---
4
+
5
+ # Roadmap
6
+
7
+ ## MVP
8
+
9
+ - [x] Add all element types supported in Obsidian to markdown
10
+ - [x] Add rich link previews
11
+ - [x] Add RSS
12
+ - [x] Add robots.txt
13
+ - [x] AI
14
+ - [x] Search
15
+ - [x] Image
16
+ - [x] Typing
17
+ - [x] Add sitemap
18
+ - [x] Make taxonomies linkable in frontmatter (if a corresponding folder exists)
19
+ - [x] Add header metadata (meta tags)
20
+ - [x] CSS handling
21
+ - [x] Favicon handling
22
+ - [x] Docs
23
+ - [x] Build action
24
+
25
+ ## V2
26
+
27
+ - [x] Fallback homepages
28
+ - [ ] Create post list element (`#tags/blue`, `@tags/blue`, `~tags/blue`)
29
+ - [/] CSS defaults (work in progress)
30
+ - [ ] CSS layout [presets](https://github.com/swyxio/spark-joy?tab=readme-ov-file#drop-in-css-frameworks)
31
+ - [x] Images as <figure>s
32
+ - [x] Frontmatter interpolation (title and description)
33
+ - [ ] Frontmatter interpolation (image)
34
+ - [x] Better favicon and styles handling
35
+ - [x] Ignore README files
36
+ - [ ] Add more header metadata
37
+ - [ ] Define folder settings
38
+ - [ ] Title
39
+ - [ ] Breadrumb
40
+ - [x] Define special page properties
41
+ - [x] Title
42
+ - [x] Description
43
+ - [x] Meta image
44
+ - [x] Date
45
+ - [ ] Modified date
46
+ - [ ] Tags
47
+ - [ ] Author/Authors
48
+ - [ ] Make file names kebab-cased for URLs and links
49
+ - [x] Include breadcrumbs in page titles ~~(optionally)~~
50
+ - [ ] Strikethrough
51
+ - [ ] Add hidden routes (`$`)
52
+ - [x] Image alt text
53
+ - [ ] Code highlighter
54
+ - [x] Deploy action (Vercel)
55
+ - [x] Add anchor links for headings
56
+ - [ ] Post TOC
57
+ - [ ] GitHub-style notes
58
+ - [ ] Add in-browser search
59
+ - [x] Figure out how to stop components from remounting on every change
60
+ - [ ] Make RSS optional
61
+ - [ ] Pagination
62
+ - [x] Demote headings global option (headings are now demoted intelligently)
63
+
64
+ ## Wish list
65
+
66
+ - [ ] Automatic file creation
67
+ - [ ] Image serving and optimization
68
+ - [ ] GUI
69
+ - [ ] More deploy actions
70
+ - [ ] Optional JSON interactivity (ratings, comments, SubPubHub)
71
+ - [ ] Redirects
72
+ - [ ] Advanced markdown
73
+ - [ ] Highlight
74
+ - [ ] Sanitize
75
+ - [ ] Tables
76
+ - [ ] Strike
77
+ - [ ] Task lists
78
+ - [ ] Footnotes
79
+ - [ ] Mermaid
80
+ - [ ] Math
81
+ - [ ] Wikilinks
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: Vowel
3
+ slogan: Markup for your Markdown
4
+ breadcrumb: Home
5
+ domain: 'https://getvowel.com'
6
+ author: Sam Littlefair
7
+ icon: 🔥
8
+ robots:
9
+ google: true
10
+ google_images: true
11
+ ai: true
12
+ ---
@@ -0,0 +1,5 @@
1
+ {
2
+ "cleanUrls": true,
3
+ "outputDirectory": ".output",
4
+ "buildCommand": null
5
+ }
package/jsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ // "extends": "./.svelte-kit/tsconfig.json",
3
+ "compilerOptions": {
4
+ "allowJs": true,
5
+ "checkJs": true,
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "resolveJsonModule": true,
9
+ "skipLibCheck": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "moduleResolution": "bundler"
13
+ }
14
+ // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
15
+ //
16
+ // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
17
+ // from the referenced tsconfig.json - TypeScript does not merge them in
18
+ }
package/package.json CHANGED
@@ -1,21 +1,39 @@
1
1
  {
2
- "name": "vowel",
3
- "version": "0.0.2",
4
- "description": "tiny library for working with vowels",
5
- "main": "index.js",
6
- "repository": "pacocoursey/vowel",
7
- "author": "Paco Coursey <paco@pacocoursey.com> (pacocoursey.com)",
8
- "keywords": [
9
- "english",
10
- "alphabet",
11
- "vowel",
12
- "consonant",
13
- "string"
14
- ],
15
- "license": "MIT",
16
- "scripts": {
17
- "start": ""
18
- },
19
- "dependencies": {},
20
- "devDependencies": {}
2
+ "name": "vowel",
3
+ "version": "0.1.0",
4
+ "bin": "./bin.js",
5
+ "scripts": {
6
+ "dev": "vite dev",
7
+ "build": "vite build",
8
+ "preview": "vite preview",
9
+ "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
10
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
11
+ "lint": "prettier --check .",
12
+ "format": "prettier --write ."
13
+ },
14
+ "type": "module",
15
+ "dependencies": {
16
+ "@picocss/pico": "^1.5.10",
17
+ "@sveltejs/adapter-auto": "^3.0.0",
18
+ "@sveltejs/adapter-static": "^3.0.1",
19
+ "@sveltejs/kit": "^2.5.6",
20
+ "@sveltejs/vite-plugin-svelte": "^4.0.0-next.3",
21
+ "any-date-parser": "^1.5.4",
22
+ "change-case": "^5.4.1",
23
+ "js-yaml": "^4.1.0",
24
+ "milligram": "^1.4.1",
25
+ "object-path": "^0.11.8",
26
+ "open-props": "^1.6.13",
27
+ "prettier": "^3.0.0",
28
+ "prettier-plugin-svelte": "^3.0.0",
29
+ "remark": "^15.0.1",
30
+ "remark-frontmatter": "^5.0.0",
31
+ "remark-html": "^16.0.1",
32
+ "spectre.css": "^0.5.9",
33
+ "svelte": "^5.0.0-next.121",
34
+ "typescript": "^5.0.0",
35
+ "url-metadata": "^3.4.9",
36
+ "vite": "^5.0.12",
37
+ "xml": "^1.0.1"
38
+ }
21
39
  }
package/server.js ADDED
@@ -0,0 +1,80 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { createServer, build } from 'vite';
3
+ import { writeFile, mkdir, readFile } from 'fs/promises';
4
+ import { existsSync } from 'fs';
5
+ import { dirname, join } from 'path';
6
+
7
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
8
+
9
+ // Check if this is a package build
10
+ const isBuild = process.argv[2] === 'build';
11
+
12
+ // If this is not a build, read the process argument
13
+ const arg = !isBuild ? process.argv[2] : false;
14
+
15
+ // Check if this is a user (process argument) or package dev (cwd)
16
+ const $home = [arg || join(process.cwd(), 'content')];
17
+
18
+ const define = {
19
+ $home
20
+ };
21
+
22
+ const config = {
23
+ // any valid user config options, plus `mode` and `configFile`
24
+ configFile: join(__dirname, 'vite.config.js'),
25
+ root: __dirname,
26
+ server: {
27
+ port: 1337
28
+ },
29
+ define
30
+ };
31
+
32
+ const vercelDefaults = {
33
+ cleanUrls: true,
34
+ outputDirectory: '.output',
35
+ buildCommand: null
36
+ };
37
+
38
+ async function buildProject() {
39
+ const outputDir = join($home[0], '.output');
40
+ const vercelConfigPath = join($home[0], 'vercel.json');
41
+ const vercelDirPath = join($home[0], '.vercel');
42
+ const projectJsonPath = join(vercelDirPath, 'project.json');
43
+ let projectData = null;
44
+
45
+ try {
46
+ if (existsSync(projectJsonPath)) {
47
+ const data = await readFile(projectJsonPath, 'utf-8');
48
+ projectData = JSON.parse(data);
49
+ }
50
+
51
+ await build(config);
52
+
53
+ if (projectData !== null) {
54
+ await mkdir(vercelDirPath, { recursive: true });
55
+ await writeFile(projectJsonPath, JSON.stringify(projectData, null, 2), 'utf-8');
56
+ }
57
+
58
+ const jsonData = JSON.stringify(vercelDefaults, null, 2);
59
+ await writeFile(vercelConfigPath, jsonData, 'utf-8');
60
+ await writeFile(join($home[0], '.output', 'vercel.json'), jsonData, 'utf-8');
61
+ console.log('Build completed successfully.');
62
+ } catch (err) {
63
+ console.error('Build failed:', err);
64
+ }
65
+ }
66
+
67
+ async function runServer() {
68
+ const server = await createServer(config);
69
+ await server.listen();
70
+
71
+ server.printUrls();
72
+ server.bindCLIShortcuts({ print: true });
73
+ }
74
+
75
+ // If build mode
76
+ if (isBuild || process.argv[3] === 'true') {
77
+ buildProject();
78
+ } else {
79
+ runServer();
80
+ }
package/src/app.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ // See https://kit.svelte.dev/docs/types#app
2
+ // for information about these interfaces
3
+ declare global {
4
+ namespace App {
5
+ // interface Error {}
6
+ // interface Locals {}
7
+ // interface PageData {}
8
+ // interface Platform {}
9
+ }
10
+ }
11
+
12
+ export {};
package/src/app.html ADDED
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ %sveltekit.head%
7
+ </head>
8
+
9
+ <body>
10
+ <div class="container">%sveltekit.body%</div>
11
+ </body>
12
+ </html>
@@ -0,0 +1,19 @@
1
+ <script>
2
+ import { page } from '$app/stores';
3
+ import { getFolderLabel, getFolder } from '$lib/utilities';
4
+
5
+ let { level } = $props();
6
+
7
+ const path = $derived($page.data.segments.slice(0, level).join('/'));
8
+ // The page for the current crumb
9
+ const crumbyPage = $derived(getFolder($page.data.website, path));
10
+ const folderLabel = $derived(getFolderLabel(crumbyPage));
11
+
12
+ const active = $derived(level === $page.data.segments.length);
13
+ </script>
14
+
15
+ <a aria-current={active ? 'page' : undefined} href={crumbyPage?.['$'].url}>{folderLabel}</a>
16
+ {#if !active}
17
+ <span aria-hidden="true" class="separator"></span>
18
+ <svelte:self level={level + 1} />
19
+ {/if}
@@ -0,0 +1,126 @@
1
+ <style>
2
+ :global(:root) {
3
+ --font-sans: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue,
4
+ helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
5
+ --font-serif: Iowan Old Style, Apple Garamond, Baskerville, Times New Roman, Droid Serif, Times,
6
+ Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
7
+ --font-mono: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
8
+ }
9
+
10
+ :global(:where(html, body, .page)) {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ :global(:where(.container)) {
17
+ height: 100vh;
18
+ overflow-y: scroll;
19
+ font-family: var(--font-sans);
20
+ }
21
+
22
+ :global(:where(.page)) {
23
+ max-width: 60ch;
24
+ margin: auto;
25
+ min-height: 100vh;
26
+ display: flex;
27
+ flex-direction: column;
28
+ padding: 1rem;
29
+ overflow-x: hidden;
30
+ }
31
+
32
+ /* Header */
33
+
34
+ :global(:where(nav)) {
35
+ display: flex;
36
+ column-gap: 1rem;
37
+ row-gap: 0.5rem;
38
+ flex-wrap: wrap;
39
+ justify-content: flex-start;
40
+ }
41
+
42
+ :global(:where(nav + nav)) {
43
+ margin-top: 1rem;
44
+ }
45
+
46
+ :global(:where(nav > a)) {
47
+ text-wrap: nowrap;
48
+ }
49
+
50
+ :global(:where(header a.site-title)) {
51
+ color: unset;
52
+ text-decoration: none;
53
+ font-weight: bold;
54
+ font-size: 2em;
55
+ }
56
+
57
+ :global(:where(nav, aside.sitemap) :where([aria-current='page'], [aria-current='true'])) {
58
+ text-decoration: none;
59
+ color: unset;
60
+ cursor: default;
61
+ }
62
+
63
+ :global(:where(header .breadcrumbs)) {
64
+ margin-top: 2rem;
65
+ }
66
+
67
+ :global(:where(header .breadcrumbs a)) {
68
+ font-size: 0.9em;
69
+ text-decoration: none;
70
+ color: unset;
71
+ }
72
+
73
+ :global(:where(header .breadcrumbs .separator):after) {
74
+ content: '/';
75
+ }
76
+
77
+ /* Content block elements */
78
+
79
+ :global(:where(pre)) {
80
+ padding: 1rem;
81
+ background: #eee;
82
+ overflow-x: auto;
83
+ }
84
+
85
+ :global(:where(article)) {
86
+ border: 8px ridge #eee;
87
+ padding-inline: 1rem;
88
+ }
89
+
90
+ :global(:where(article + article)) {
91
+ margin-top: 1rem;
92
+ }
93
+
94
+ /* Content inline elements */
95
+
96
+ :global(:where(code)) {
97
+ background: #eee;
98
+ margin: -3px -1px;
99
+ padding: 3px 4px;
100
+ border-radius: 5px;
101
+ }
102
+
103
+ /* Aside */
104
+
105
+ :global(:where(aside.sitemap)) {
106
+ margin-top: auto;
107
+ }
108
+
109
+ :global([aria-current='page']) {
110
+ color: unset;
111
+ text-decoration: none;
112
+ cursor: unset;
113
+ }
114
+
115
+ /* Footer */
116
+
117
+ :global(:where(footer)) {
118
+ text-align: center;
119
+ }
120
+
121
+ /* Pages */
122
+
123
+ :global(:where(.page.home > main > h1)) {
124
+ display: none;
125
+ }
126
+ </style>
@@ -0,0 +1,48 @@
1
+ <script>
2
+ import { sentenceCase } from 'change-case';
3
+ // @ts-ignore
4
+ import { getFileLabel, getPage } from '$lib/utilities';
5
+
6
+ /** @type {{ property: any, key: string }}*/
7
+ let { property, key, website } = $props();
8
+
9
+ const path = key + '/' + property;
10
+ const page = getPage(website, path);
11
+
12
+ /**
13
+ * Check if a value is an object
14
+ * @param {any} value - Value to check.
15
+ * @returns {boolean}
16
+ */
17
+ function isObject(value) {
18
+ return typeof value === 'object' && !Array.isArray(value) && value !== null;
19
+ }
20
+
21
+ /**
22
+ * Check if a value is an object
23
+ * @param {any} value - Value to check.
24
+ * @returns {boolean}
25
+ */
26
+ function isArray(value) {
27
+ return typeof value === 'object' && Array.isArray(value) && value !== null;
28
+ }
29
+ </script>
30
+
31
+ <dl>
32
+ <dt class={key}>{sentenceCase(key)}</dt>
33
+ <dd class={key}>
34
+ {#if isArray(property)}
35
+ <ul>
36
+ {#each property as item}
37
+ {@const path = key + '/' + item}
38
+ {@const page = getPage(website, path)}
39
+ <li>
40
+ <a href={page.url}>{getFileLabel(page)}</a>
41
+ </li>
42
+ {/each}
43
+ </ul>
44
+ {:else}
45
+ <a href={page.url}>{getFileLabel(page)}</a>
46
+ {/if}
47
+ </dd>
48
+ </dl>
@@ -0,0 +1,54 @@
1
+ <script>
2
+ import { FrontmatterProperty, FrontmatterTaxonomy } from './';
3
+
4
+ /**
5
+ * @typedef {import("../utilities/processMarkdownFiles").MarkdownFile} MarkdownFile
6
+ */
7
+
8
+ /**
9
+ * @type {{props: {properties: MarkdownFile}}}
10
+ */
11
+ let { props } = $props();
12
+ // TODO: Normalize frontmatter props
13
+ let { properties, website } = $derived(props);
14
+
15
+ const excludedProperties = [
16
+ 'title',
17
+ 'description',
18
+ 'type',
19
+ 'ast',
20
+ 'url',
21
+ 'frontmatter',
22
+ 'published',
23
+ 'date',
24
+ 'image',
25
+ 'breadcrumb',
26
+ 'imputedProperties'
27
+ ];
28
+
29
+ // const { website } = $page.data;
30
+
31
+ /**
32
+ * Fetch a given property from a page.
33
+ * @param {MarkdownFile} properties - A markdown file.
34
+ * @param {string} key - The name of a property
35
+ * @returns {any}
36
+ */
37
+ function getProperty(properties, key) {
38
+ // @ts-ignore
39
+ return properties[key];
40
+ }
41
+ </script>
42
+
43
+ {#if properties}
44
+ {#each Object.keys(properties) as key}
45
+ {#if !excludedProperties.includes(key)}
46
+ {@const property = getProperty(properties, key)}
47
+ {#if website.hasOwnProperty(key)}
48
+ <FrontmatterTaxonomy {property} {key} {website} />
49
+ {:else}
50
+ <FrontmatterProperty {property} {key} />
51
+ {/if}
52
+ {/if}
53
+ {/each}
54
+ {/if}
@@ -0,0 +1,72 @@
1
+ <script>
2
+ import { sentenceCase } from 'change-case';
3
+ import Frontmatter from './Frontmatter.svelte';
4
+ import Image from './Markdown/Image.svelte';
5
+
6
+ /** @type {{ property: any, key: string }}*/
7
+ let { property, key } = $props();
8
+ </script>
9
+
10
+ {#if property}
11
+ <dl class={key}>
12
+ <dt>{sentenceCase(key)}</dt>
13
+ <dd>
14
+ {@render propertySwitch(property)}
15
+ </dd>
16
+ </dl>
17
+ {/if}
18
+
19
+ {#snippet array(items)}
20
+ <ul>
21
+ {#each items as item}
22
+ <li>
23
+ {@render propertySwitch(item)}
24
+ </li>
25
+ {/each}
26
+ </ul>
27
+ {/snippet}
28
+
29
+ {#snippet propertySwitch(value)}
30
+ {#if value.type === 'array'}
31
+ {@render array(value.output)}
32
+ {:else if value.type === 'object'}
33
+ <Frontmatter props={{ properties: value.output }} />
34
+ {:else if value.type === 'image'}
35
+ {@render image(value.output)}
36
+ {:else if value.type === 'date'}
37
+ {@render date(value.output)}
38
+ {:else if value.type === 'url'}
39
+ {@render url(value.output)}
40
+ {:else}
41
+ {value.output || value}
42
+ {/if}
43
+ {/snippet}
44
+
45
+ {#snippet image(value)}
46
+ <Image node={{ url: value, alt: '' }} />
47
+ {/snippet}
48
+
49
+ {#snippet date(value)}
50
+ {@const date = new Date(value)}
51
+ <time datetime={date.toISOString()}>{date.toLocaleDateString('en-US')}</time>
52
+ {/snippet}
53
+
54
+ {#snippet url(value)}
55
+ <article>
56
+ <a href={value.url}>
57
+ {#if value['og:image']}
58
+ <img src={value['og:image']} alt="" />
59
+ {/if}
60
+ <h2>
61
+ {value['og:title'] || value.title}
62
+ </h2>
63
+ <p>
64
+ {value['og:description']}
65
+ </p>
66
+ </a>
67
+ </article>
68
+ {/snippet}
69
+
70
+ {#snippet pdf(value)}
71
+ <a href={value.url}>{value.url}</a>
72
+ {/snippet}