vowel 0.2.1-beta.1 → 0.2.3

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.
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: Bug report
3
+ about: Report a bug to help improve Vowel.
4
+ title: ""
5
+ labels: "bug"
6
+ assignees: "samlfair"
7
+ ---
8
+
9
+ <!--
10
+ Thanks for taking the time to help build Vowel.
11
+ A well-detailed bug report helps us to solve the problem quickly.
12
+ -->
13
+
14
+ ### Versions
15
+
16
+ - vowel: <!-- eg: v0.1.32 -->
17
+ - node: <!-- eg: v14.15.0 -->
18
+ - os: <!-- eg: macOS 14.5 -->
19
+ - browser: <!-- eg: Chrome 129 -->
20
+
21
+ ### Browser report
22
+
23
+ <!--
24
+ Generate a browser report at https://www.whatsmybrowser.org/
25
+ Copy-paste a link to the report
26
+ -->
27
+
28
+ ### Link to Stackblitz
29
+
30
+ <!--
31
+ If possible, upload a reproduction to Stackblitz
32
+ and copy-paste the link here
33
+ -->
34
+
35
+ ### Steps to reproduce
36
+
37
+ <!-- eg: "Go to X and click on Y" -->
38
+
39
+ ### Expected behavior
40
+
41
+ <!-- eg: "The image will appear below the headline" -->
42
+
43
+ ### Actual behavior
44
+
45
+ <!-- eg: "The image appears above the headline" -->
46
+
47
+ ### Additional technologies
48
+
49
+ <!-- List any other technologies you're using with Vowel -->
50
+
51
+ ### Screenshot
52
+
53
+ <!-- If possible, include a screenshot of the issue -->
54
+
55
+ ### Errors from terminal
56
+
57
+ <!-- If there are any error messages in the terminal, paste them here -->
58
+
59
+ ```
60
+
61
+ ```
62
+
63
+ ### Errors from browser console
64
+
65
+ <!-- If there are any error message in the error console, paste them here -->
66
+
67
+ ```
68
+
69
+ ```
70
+
71
+ ### Additional comments
72
+
73
+ <!-- Include any other relevent information -->
@@ -0,0 +1,7 @@
1
+ contact_links:
2
+ - name: Vowel on Twitter
3
+ url: https://twitter.com/tryvowel
4
+ about: Get in touch with questions or ideas.
5
+ - name: Sam on Twitter
6
+ url: https://twitter.com/samlfair
7
+ about: Get in touch to chat.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Vowel
2
+
3
+ *Markdown websites in milliseconds*
4
+
5
+ - Bundled with [Votive](https://github.com/samlfair/votive)
6
+ - Served with [Voot](https://github.com/samlfair/voot)
7
+
8
+ ## Roadmap
9
+
10
+ ### High priority
11
+
12
+ - [ ] robots.txt
13
+ - [ ] sitemap.xml
14
+ - [ ] 404.html
15
+ - [ ] TUI
16
+ - [ ] Create settings.md
17
+ - [ ] Site title
18
+ - [ ] Domain
19
+ - [ ] Webmentions
20
+ - [ ] Logo
21
+ - [ ] Wordmark
22
+ - [ ] Identity (rel=me)
23
+ - [ ] Filename breadcrumbs
24
+ - [ ] RSS
25
+ - [ ] Sitemap
26
+ - [ ] Create home.md
27
+ - [ ] Create folder settings files
28
+ - [ ] Title
29
+ - [ ] Breadcrumb
30
+ - [ ] Custom CSS
31
+ - [ ] Tags
32
+ - [ ] Customize index fallback
33
+ - [ ] Date format settings
34
+ - [ ] ::mark::
35
+ - [ ] Infer images
36
+ - [ ] Favicon
37
+ - [ ] Webmentions
38
+ - [ ] HTML boilerplate
39
+ - [ ] Page lists
40
+ - [ ] View transitions
41
+ - [ ] Logo
42
+ - [ ] Wordmark
43
+ - [ ] Sort nav items
44
+ - [ ] Canonical URL
45
+ - [ ] Handle external links
46
+ - [ ] Admonitions
47
+ - [ ] Use hgroup for site title, page title, etc
48
+ - [ ] Images as `<figure>`
49
+ - [ ] Hidden routes
50
+ - [ ] Frontmatter settings
51
+ - [ ] HTML
52
+ - [ ] RSS
53
+ - [ ] Sitemap
54
+ - [ ] Heading anchors
55
+ - [ ] Taxonomy pages and smart frontmatter
56
+ - [ ] CSS cache busting
57
+ - [ ] Slogan in homepage title
58
+
59
+ ### Medium priority
60
+
61
+ - [ ] Tests
62
+ - [ ] Break code into multiple files
63
+ - [ ] Image optimization (unpic)
64
+ - [ ] [SVG by mask](https://pqina.nl/blog/set-svg-background-image-fill-color/) and [CSS icons](https://antfu.me/posts/icons-in-pure-css)
65
+ - [ ] WYSIWYG editor
66
+ - [ ] Better signals
67
+ - [ ] File-written callback
68
+ - [ ] Themes
69
+ - [ ] Deploy
70
+ - [ ] Cloudflare pages
71
+ - [ ] GitHub pages
72
+ - [ ] Post-publish work (ping webmentions)
73
+ - [ ] [Desktop app](https://blackboard.sh/electrobun/docs/)
74
+ - [ ] Mermaid
75
+ - [ ] Codeblock syntax highlighting
76
+ - [ ] Extraction utilities (regex in archive)
77
+ - [ ] Smarter frontmatter
78
+ - [ ] Object dl
79
+ - [ ] Array ul
80
+ - [ ] Image
81
+ - [ ] URL
82
+ - [ ] Date
83
+ - [ ] TOC
84
+ - [ ] Versioning/publishing script
85
+ - [ ] Verify all element types form Obsidian
86
+
87
+ ### Low priority
88
+
89
+ - [ ] Footnotes
90
+ - [ ] Frontmatter taxonomies
91
+ - [ ] Recursive frontmatter
92
+ - [ ] Browser search
93
+ - [ ] Pagination
94
+ - [ ] ATProto
@@ -0,0 +1,11 @@
1
+ > Why do I have a folder named ".vercel" in my project?
2
+ The ".vercel" folder is created when you link a directory to a Vercel project.
3
+
4
+ > What does the "project.json" file contain?
5
+ The "project.json" file contains:
6
+ - The ID of the Vercel project that you linked ("projectId")
7
+ - The ID of the user or team your Vercel project is owned by ("orgId")
8
+
9
+ > Should I commit the ".vercel" folder?
10
+ No, you should not share the ".vercel" folder with anyone.
11
+ Upon creation, it will be automatically added to your ".gitignore" file.
@@ -0,0 +1 @@
1
+ {"projectId":"prj_4CUiAmqACnAFX1cjGqW5om7DbK4E","orgId":"team_CpFUbX79Msg6m5IOy2BzFxEE"}
package/getMetadata.js ADDED
@@ -0,0 +1,41 @@
1
+ import { XMLParser } from "fast-xml-parser"
2
+ import urlMetadata from 'url-metadata';
3
+
4
+ export default async function getMetadata(url) {
5
+ try {
6
+ const urlObject = new URL(url);
7
+ const allMetadata = await urlMetadata(urlObject.href, {
8
+ includeResponseBody: true,
9
+ ensureSecureImageRequest: true
10
+ });
11
+
12
+ const parser = new XMLParser({
13
+ unpairedTags: ["!doctype", "meta", "link", "hr", "br", "img"],
14
+ ignoreAttributes: false,
15
+ stopNodes: ["*.pre", "*.script"],
16
+ processEntities: true,
17
+ htmlEntities: true
18
+ })
19
+
20
+ let parsedData = parser.parse(allMetadata.responseBody)
21
+ const webmentionEndpoint = parsedData?.html?.head?.link?.find(link => {
22
+ return link["@_rel"] === "webmention"
23
+ })?.["@_href"]
24
+
25
+ const metadata = {
26
+ image: allMetadata['og:image'],
27
+ ogURL: allMetadata['og:url'],
28
+ canonicalURL: allMetadata.canonical,
29
+ title: allMetadata.title,
30
+ ogTitle: allMetadata['og:title'],
31
+ author: allMetadata.author,
32
+ description: allMetadata.description,
33
+ webmentionEndpoint
34
+ };
35
+
36
+ return { metadata }
37
+ } catch (error) {
38
+ console.log({ fetchingMetadataError: error })
39
+ return
40
+ }
41
+ }
package/index.js CHANGED
@@ -1,13 +1,16 @@
1
- import votive from "votive"
1
+ import voot from "voot"
2
2
  import { h } from 'hastscript'
3
3
  import fs from "fs/promises"
4
4
  import { readFileSync } from "fs"
5
5
  import { rehype } from "rehype"
6
+ import { find } from 'unist-util-find'
6
7
  import { toHast } from 'mdast-util-to-hast'
8
+ import { fromHtml } from 'hast-util-from-html'
7
9
  import { fromMarkdown } from 'mdast-util-from-markdown'
8
10
  import { frontmatter } from "micromark-extension-frontmatter"
9
11
  import { frontmatterFromMarkdown } from 'mdast-util-frontmatter'
10
12
  import { gfmFootnoteFromMarkdown } from "mdast-util-gfm-footnote"
13
+ import { visit } from "unist-util-visit"
11
14
  import { gfmFootnote } from "micromark-extension-gfm-footnote"
12
15
  import { gfmStrikethroughFromMarkdown } from 'mdast-util-gfm-strikethrough'
13
16
  import { gfmStrikethrough } from 'micromark-extension-gfm-strikethrough'
@@ -16,7 +19,8 @@ import { gfmTableFromMarkdown } from 'mdast-util-gfm-table'
16
19
  import { gfmTaskListItem } from 'micromark-extension-gfm-task-list-item' // TODO: Add
17
20
  import { gfmTaskListItemFromMarkdown } from 'mdast-util-gfm-task-list-item' // TODO: Add
18
21
  import { normalizeHeadings } from 'mdast-normalize-headings'
19
- import { toString } from 'mdast-util-to-string'
22
+ import { toString as mdastToString } from 'mdast-util-to-string'
23
+ import { toString as hastToString } from 'hast-util-to-string'
20
24
  import yaml from 'yaml'
21
25
  import extractDate from "./extractDate.js"
22
26
  import { testURL } from "./utils.js"
@@ -26,7 +30,7 @@ import { styleText } from "node:util"
26
30
  const VOWEL_DIR = import.meta.dirname
27
31
 
28
32
 
29
- /** @import {VotiveConfig, VotivePlugin, VotiveProcessor, ReadText, ReadAbstract, ReadFolder, ProcessorWrite, Router} from "votive" */
33
+ /** @import {Runner, ReadPath, VotiveConfig, VotivePlugin, VotiveProcessor, ReadText, ReadAbstract, ReadFolder, ProcessorWrite, Router} from "votive" */
30
34
 
31
35
  /** @type {VotiveProcessor} */
32
36
  const cssWriter = {
@@ -34,6 +38,19 @@ const cssWriter = {
34
38
  write: writeCSS
35
39
  }
36
40
 
41
+ /** @type {VotiveProcessor} */
42
+ const jpegLoader = {
43
+ syntax: "jpeg",
44
+ filter: {
45
+ extensions: [".jpeg", ".jpg"]
46
+ },
47
+ read: {
48
+ path: readImagePath,
49
+ },
50
+ write: writeImage
51
+ }
52
+
53
+
37
54
  /** @type {ProcessorWrite} */
38
55
  function writeCSS(destination, database, config) {
39
56
  return {
@@ -42,28 +59,65 @@ function writeCSS(destination, database, config) {
42
59
  }
43
60
  }
44
61
 
62
+ function readURL(data) {
63
+ const hast = fromHtml(data)
64
+ const metadata = {}
65
+
66
+ visit(hast, (node) => {
67
+ if (node.tagName === "meta") {
68
+ if (node.properties && node.properties.property) {
69
+ metadata[node.properties.property] = node.properties.content
70
+ }
71
+ } else if (node.tagName === "title") {
72
+ metadata.title = hastToString(node)
73
+ } else if (node.tagName === "link") {
74
+ if (node.properties?.rel?.includes("me")) {
75
+ metadata.me = node.properties.href
76
+ } else if (node.properties?.rel?.includes("webmention")) {
77
+ metadata.webmention = node.properties.href
78
+ } else if (node.properties?.rel?.includes("icon")) {
79
+ metadata.icon = node.properties.href
80
+ }
81
+ }
82
+ })
83
+
84
+ return metadata
85
+ }
86
+
45
87
  /** @type {VotiveProcessor} */
46
88
  const markdownReader = {
47
89
  syntax: "mdast",
48
90
  filter: { extensions: [".md"] },
49
91
  read: {
50
92
  text: readMarkdown,
93
+ url: readURL,
51
94
  abstract: readAbstract,
52
95
  folder: readFolder
53
96
  },
54
97
  write: writeHTML
55
98
  }
56
99
 
57
- async function remove() {
100
+
101
+ async function removeCache() {
58
102
  try {
59
- await fs.rm("./docs-source/.votive.db")
60
- console.info(styleText("yellow", "Database cleared"))
103
+ await fs.rm("./.votive.db")
104
+ console.info(styleText("yellow", "Cache cleared"))
61
105
  } catch (e) {
62
106
  console.info(styleText("yellow", "No database cache found"))
63
107
  }
64
108
  }
65
109
 
66
- await remove()
110
+ async function removeDB() {
111
+ try {
112
+ await fs.rmdir("./output")
113
+ console.info(styleText("yellow", "Output cleared"))
114
+ } catch (e) {
115
+ console.info(styleText("yellow", "No output cache found"))
116
+ }
117
+ }
118
+
119
+ await removeCache()
120
+ await removeDB()
67
121
 
68
122
  /** @type {ReadText} */
69
123
  function readMarkdown(string, filePath, destinationPath, database, config) {
@@ -89,6 +143,7 @@ function readMarkdown(string, filePath, destinationPath, database, config) {
89
143
  metadata.inferred_label = pathInfo.name
90
144
  const destinationInfo = path.parse(destinationPath)
91
145
  metadata.prettyURL = (new URL(`${destinationInfo.dir}/${destinationInfo.name}`, "thismessage:/")).pathname
146
+ const jobs = []
92
147
 
93
148
  selectMetadata(metadata)
94
149
 
@@ -103,8 +158,21 @@ function readMarkdown(string, filePath, destinationPath, database, config) {
103
158
  }
104
159
  }
105
160
 
161
+ visit(mdast, (node, index, parent) => {
162
+ if (node.type === "text" && parent.children.length === 1 && parent.type === "paragraph") {
163
+ const validURL = testURL(node.value)
164
+ if (validURL) {
165
+
166
+ jobs.push({
167
+ data: node.value,
168
+ runner: "text",
169
+ destination: destinationPath
170
+ })
171
+ }
172
+ }
173
+ })
106
174
 
107
- return { abstract: mdast, metadata }
175
+ return { abstract: mdast, metadata, jobs }
108
176
  }
109
177
 
110
178
  /** @param {ReturnType<getMetadata>} metadata */
@@ -173,7 +241,7 @@ function getMetadata(tree) {
173
241
 
174
242
  for (let i = 0; i < tree.children.length; i++) {
175
243
  const child = tree.children[i]
176
- const text = toString(child)
244
+ const text = mdastToString(child)
177
245
  switch (child.type) {
178
246
  case "paragraph":
179
247
  if (child.children.length !== 1) {
@@ -197,14 +265,14 @@ function getMetadata(tree) {
197
265
  break
198
266
  }
199
267
  } else {
200
- const inferred_date = extractDate(toString(child))
268
+ const inferred_date = extractDate(mdastToString(child))
201
269
  if (inferred_date) metadata.inferred_date = inferred_date
202
270
  else i = Infinity
203
271
  break
204
272
  }
205
273
  case "heading":
206
274
  if (child.depth === 1) {
207
- metadata.inferred_title = toString(child)
275
+ metadata.inferred_title = mdastToString(child)
208
276
  }
209
277
  break
210
278
  case "yaml":
@@ -261,15 +329,14 @@ function readFolder(folder, database, config, isRoot) {
261
329
  })
262
330
  }
263
331
 
332
+
264
333
  if (isRoot) {
265
334
  // TODO: Create theme job
266
335
  const settings = database.getSettings(config.sourceFolder)
267
- if (!settings.theme || settings.theme === "default") {
336
+ if (!settings.theme || settings.theme[0] === "default") {
268
337
  database.setSetting(folder, "theme", "default")
269
338
  // TODO: Save theme file (reset, typography, default)
270
339
 
271
- console.log(process.cwd())
272
-
273
340
  const resetStylesPath = path.join(VOWEL_DIR, "stylesheets", "ResetStyles.css")
274
341
  const typeStylesPath = path.join(VOWEL_DIR, "stylesheets", "TypographyStyles.css")
275
342
  const defaultStylesPath = path.join(VOWEL_DIR, "stylesheets", "DefaultStyles.css")
@@ -343,7 +410,7 @@ function writeHTML(destination, database, config) {
343
410
 
344
411
  const treeStyleSheets = []
345
412
 
346
- !settings.theme && treeStyleSheets.push(
413
+ !settings.theme || settings.theme.includes("default") && treeStyleSheets.push(
347
414
  h('link', {
348
415
  rel: "stylesheet",
349
416
  href: "/default.css"
@@ -455,6 +522,24 @@ function writeHTML(destination, database, config) {
455
522
 
456
523
  const treeMain = h('main.h-entry', toHast(abstract).children)
457
524
 
525
+ visit(treeMain, (node, index, parent) => {
526
+ if (node.type === "text" && parent.tagName === 'p' && parent.children.length === 1) {
527
+ const validURL = testURL(node.value)
528
+ if (validURL) {
529
+ const metadata = database.getURL(node.value)
530
+ if (metadata) {
531
+ parent.tagName = "article"
532
+ // TODO: Add url to metadata
533
+ parent.children = [
534
+ h("a", { href: node.value },
535
+ h("h2", metadata.title)
536
+ )
537
+ ]
538
+ }
539
+ }
540
+ }
541
+ })
542
+
458
543
  // TODO: Write sidebar
459
544
  const treeSidebar = h('nav.secondary')
460
545
  const treeFooter = h('footer', `© ${new Date().getFullYear()}`)
@@ -522,32 +607,63 @@ function router({ name, dir, inRootDir }) {
522
607
  }
523
608
  }
524
609
 
610
+ /** @type {ReadPath} */
611
+ async function readImagePath(string, database) {
612
+ // TODO: Resize and optimize images
613
+
614
+ return {
615
+ metadata: {},
616
+ abstract: { path: string }
617
+ }
618
+ }
619
+
620
+ /** @type {ProcessorWrite} */
621
+ async function writeImage(destination, database, config) {
622
+ const buffer = await fs.readFile(destination.path)
623
+ return {
624
+ buffer
625
+ }
626
+ }
627
+
525
628
  /** @type {VotivePlugin} */
526
- const vowel = {
629
+ const vowelMarkdown = {
527
630
  name: "vowel",
528
631
  processors: [markdownReader, cssWriter],
529
- runners: {},
530
632
  router
531
633
  }
532
634
 
635
+ /** @type {VotivePlugin} */
636
+ const vowelJpeg = {
637
+ name: "vowel-jpeg",
638
+ processors: [jpegLoader],
639
+ router: ({ name, dir, ext }) => {
640
+ return {
641
+ name, dir, ext
642
+ }
643
+ }
644
+ }
645
+
646
+
647
+
533
648
  /** @type {VotiveConfig} */
534
649
  const config = {
535
- sourceFolder: "./",
650
+ sourceFolder: ".",
536
651
  destinationFolder: "output",
537
652
  plugins: [
538
- vowel
653
+ vowelMarkdown,
654
+ vowelJpeg
539
655
  ]
540
656
  }
541
657
 
542
- const then = performance.now()
543
658
 
544
659
  async function init() {
545
- await fs.rm(config.destinationFolder, { recursive: true, force: true })
546
- await fs.mkdir(config.destinationFolder, { recursive: true })
547
- const cache = await votive(config)
660
+ const then = performance.now()
661
+ // await fs.rm(config.destinationFolder, { recursive: true, force: true })
662
+ // await fs.mkdir(config.destinationFolder, { recursive: true })
663
+ const cache = await voot(config)
664
+ console.log(styleText("red", (performance.now() - then).toFixed(4) + "ms"))
548
665
  }
549
666
 
550
- console.log(styleText("red", (performance.now() - then).toFixed() + "ms"))
551
667
 
552
668
  export default init
553
669
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vowel",
3
3
  "type": "module",
4
- "version": "0.2.1-beta.1",
4
+ "version": "0.2.3",
5
5
  "bin": "bin.js",
6
6
  "main": "index.js",
7
7
  "scripts": {
@@ -16,7 +16,10 @@
16
16
  "license": "MIT",
17
17
  "description": "Markdown websites",
18
18
  "dependencies": {
19
+ "fast-xml-parser": "^5.3.6",
19
20
  "faye-websocket": "^0.11.4",
21
+ "hast-util-from-html": "^2.0.3",
22
+ "hast-util-to-string": "^3.0.1",
20
23
  "hastscript": "^9.0.1",
21
24
  "js-yaml": "^4.1.0",
22
25
  "mdast": "^2.3.2",
@@ -42,11 +45,14 @@
42
45
  "remark-parse": "^11.0.0",
43
46
  "remark-rehype": "^11.1.2",
44
47
  "unified": "^11.0.5",
48
+ "unist-util-find": "^3.0.0",
45
49
  "unist-util-is": "^6.0.0",
46
50
  "unist-util-position": "5.0.0",
47
51
  "unist-util-stringify-position": "^4.0.0",
48
52
  "unist-util-visit": "^5.0.0",
49
53
  "unist-util-visit-parents": "^6.0.0",
54
+ "url-metadata": "^5.4.1",
55
+ "voot": "^0.0.2",
50
56
  "votive": "^0.0.6",
51
57
  "xast-util-sitemap": "^2.0.0",
52
58
  "xast-util-to-xml": "^4.0.0",
package/regex.js CHANGED
@@ -34,11 +34,3 @@ const test = [
34
34
  "Monday 1st Jan 96",
35
35
  "09.09/1999"
36
36
  ]
37
-
38
- test.forEach(string => {
39
- const match = dateRegex.exec(string)
40
- console.log({ match })
41
- if(match) {
42
- console.log({ match })
43
- }
44
- })
package/utils.js CHANGED
@@ -1,8 +1,10 @@
1
1
  /** @param {string} text */
2
2
  export function testURL(text) {
3
- try {
4
- return new URL(text)
5
- } catch(e) {
6
- return
3
+ if (text.match(/^https?:\/\/[\S]+$/)) {
4
+ try {
5
+ return new URL(text)
6
+ } catch (e) {
7
+ return
8
+ }
7
9
  }
8
10
  }
package/books/.votive.db DELETED
Binary file