vowel 0.2.1-beta.1 → 0.2.2

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,91 @@
1
+ # Vowel
2
+
3
+ *Markdown websites in milliseconds*
4
+
5
+ ## Roadmap
6
+
7
+ ### High priority
8
+
9
+ - [ ] robots.txt
10
+ - [ ] sitemap.xml
11
+ - [ ] 404.html
12
+ - [ ] TUI
13
+ - [ ] Create settings.md
14
+ - [ ] Site title
15
+ - [ ] Domain
16
+ - [ ] Webmentions
17
+ - [ ] Logo
18
+ - [ ] Wordmark
19
+ - [ ] Identity (rel=me)
20
+ - [ ] Filename breadcrumbs
21
+ - [ ] RSS
22
+ - [ ] Sitemap
23
+ - [ ] Create home.md
24
+ - [ ] Create folder settings files
25
+ - [ ] Title
26
+ - [ ] Breadcrumb
27
+ - [ ] Custom CSS
28
+ - [ ] Tags
29
+ - [ ] Customize index fallback
30
+ - [ ] Date format settings
31
+ - [ ] ::mark::
32
+ - [ ] Infer images
33
+ - [ ] Favicon
34
+ - [ ] Webmentions
35
+ - [ ] HTML boilerplate
36
+ - [ ] Page lists
37
+ - [ ] View transitions
38
+ - [ ] Logo
39
+ - [ ] Wordmark
40
+ - [ ] Sort nav items
41
+ - [ ] Canonical URL
42
+ - [ ] Handle external links
43
+ - [ ] Admonitions
44
+ - [ ] Use hgroup for site title, page title, etc
45
+ - [ ] Images as `<figure>`
46
+ - [ ] Hidden routes
47
+ - [ ] Frontmatter settings
48
+ - [ ] HTML
49
+ - [ ] RSS
50
+ - [ ] Sitemap
51
+ - [ ] Heading anchors
52
+ - [ ] Taxonomy pages and smart frontmatter
53
+ - [ ] CSS cache busting
54
+ - [ ] Slogan in homepage title
55
+
56
+ ### Medium priority
57
+
58
+ - [ ] Tests
59
+ - [ ] Break code into multiple files
60
+ - [ ] Image optimization (unpic)
61
+ - [ ] [SVG by mask](https://pqina.nl/blog/set-svg-background-image-fill-color/) and [CSS icons](https://antfu.me/posts/icons-in-pure-css)
62
+ - [ ] WYSIWYG editor
63
+ - [ ] Better signals
64
+ - [ ] File-written callback
65
+ - [ ] Themes
66
+ - [ ] Deploy
67
+ - [ ] Cloudflare pages
68
+ - [ ] GitHub pages
69
+ - [ ] Post-publish work (ping webmentions)
70
+ - [ ] [Desktop app](https://blackboard.sh/electrobun/docs/)
71
+ - [ ] Mermaid
72
+ - [ ] Codeblock syntax highlighting
73
+ - [ ] Extraction utilities (regex in archive)
74
+ - [ ] Smarter frontmatter
75
+ - [ ] Object dl
76
+ - [ ] Array ul
77
+ - [ ] Image
78
+ - [ ] URL
79
+ - [ ] Date
80
+ - [ ] TOC
81
+ - [ ] Versioning/publishing script
82
+ - [ ] Verify all element types form Obsidian
83
+
84
+ ### Low priority
85
+
86
+ - [ ] Footnotes
87
+ - [ ] Frontmatter taxonomies
88
+ - [ ] Recursive frontmatter
89
+ - [ ] Browser search
90
+ - [ ] Pagination
91
+ - [ ] 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.2",
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