vowel 0.6.3 → 0.6.4

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,147 @@
1
+ import { toString as mdastToString } from 'mdast-util-to-string'
2
+ import extractDate from "./../../extractDate.js"
3
+ import { testURL, toTitleCase } from "./../../utils.js"
4
+ import yaml from 'yaml'
5
+ import path from "node:path"
6
+
7
+
8
+ /**
9
+ - menu_item: true | false
10
+ - secret_url: true | false
11
+ */
12
+
13
+ /**
14
+ * @param {object} tree
15
+ * @param {string} filePath
16
+ * @param {string} targetPath
17
+ */
18
+ function getMetadata(tree, filePath, targetPath) {
19
+ const metadata = {}
20
+
21
+ for (let i = 0; i < tree.children.length; i++) {
22
+ const child = tree.children[i]
23
+ const text = mdastToString(child)
24
+ switch (child.type) {
25
+ case "paragraph":
26
+ if (child.children.length !== 1) {
27
+ if (!metadata.fm_description && !metadata.inferred_description) {
28
+ metadata.inferred_description = text
29
+ }
30
+ i = Infinity
31
+ break
32
+ } else if (child.children[0].type === "image") {
33
+ metadata.inferred_image = child.children[0].url
34
+ metadata.inferred_alt_text = child.children[0].alt
35
+ break
36
+ } else if (testURL(text)) {
37
+ const url = new URL(text)
38
+ if (text.match(/\.(jpeg|jpg|png)$/)) {
39
+ metadata.inferred_image = url
40
+ break
41
+ } else {
42
+ metadata.inferred_link = url
43
+ break
44
+ }
45
+ } else {
46
+ const inferred_date = extractDate(mdastToString(child))
47
+ if (inferred_date) {
48
+ metadata.inferred_date = inferred_date
49
+ } else {
50
+ if (!metadata.fm_description && !metadata.inferred_description) {
51
+ metadata.inferred_description = text
52
+ }
53
+ // tree.children.splice(0, i + 1)
54
+ i = Infinity
55
+ }
56
+ break
57
+ }
58
+ case "heading":
59
+ if (child.depth === 1) {
60
+ metadata.inferred_title = mdastToString(child) || toTitleCase(path.parse(filePath).name)
61
+ tree.children.splice(0, i + 1)
62
+ } else {
63
+ i = Infinity;
64
+ }
65
+ break
66
+ case "yaml":
67
+ const frontmatter = yaml.parse(child.value)
68
+ for (const key in frontmatter) {
69
+ const reservedProperties = ["rss_item", "sitemap_item", "html_file", "global_menu_item", "local_menu_item", "secret_key", "theme", "logo", "wordmark", "breadcrumb"]
70
+ if (reservedProperties.includes(key)) {
71
+ metadata[key] = frontmatter[key]
72
+ } else {
73
+ metadata["fm_" + key] = frontmatter[key]
74
+ }
75
+ }
76
+ break
77
+ default:
78
+ i = Infinity
79
+ break
80
+ }
81
+ }
82
+
83
+
84
+ const pathInfo = path.parse(filePath)
85
+
86
+ metadata.inferred_label = toTitleCase(pathInfo.name)
87
+
88
+ if (targetPath) {
89
+ const targetInfo = path.parse(targetPath)
90
+ const name = targetInfo.name === "index" ? "" : targetInfo.name
91
+ // FIXME the prettyURL should include the preceding slash
92
+ metadata.prettyURL = (new URL(`${targetInfo.dir}/${name}`, "thismessage:/")).pathname
93
+ }
94
+
95
+ selectMetadata(metadata)
96
+
97
+ return metadata
98
+ }
99
+
100
+
101
+
102
+ function selectMetadata(metadata) {
103
+ const date =
104
+ metadata.fm_date
105
+ || metadata.inferred_date
106
+
107
+ if (date) {
108
+ metadata.date = date
109
+ }
110
+
111
+ const title =
112
+ metadata.fm_title
113
+ || metadata.inferred_title
114
+ || metadata.inferred_label
115
+
116
+ if (title) {
117
+ metadata.title = title
118
+ }
119
+
120
+ const breadcrumb =
121
+ metadata.fm_breadcrumb
122
+ || metadata.title
123
+ || metadata.inferred_label
124
+
125
+ if (breadcrumb) {
126
+ metadata.breadcrumb = breadcrumb
127
+ }
128
+
129
+ const description =
130
+ metadata.fm_description
131
+ || metadata.tagline
132
+ || metadata.inferred_description
133
+
134
+ if (description) {
135
+ metadata.description = description
136
+ }
137
+
138
+ const image =
139
+ metadata.fm_image
140
+ || metadata.inferred_image
141
+
142
+ if (image) {
143
+ metadata.image = image
144
+ }
145
+ }
146
+
147
+ export default getMetadata
@@ -0,0 +1,18 @@
1
+ function generateRobots() {
2
+ return `User-agent: Google-Extended
3
+ Allow: /
4
+
5
+ User-agent: Googlebot-Image
6
+ Disallow: /
7
+
8
+ User-agent: GPTBot
9
+ Disallow: /
10
+
11
+ User-agent: ChatGPT-User
12
+ Disallow: /
13
+
14
+ User-agent: CCBot
15
+ Disallow: /`
16
+ }
17
+
18
+ export default generateRobots
@@ -1,13 +1,12 @@
1
- import path from "node:path"
2
1
  import { transform } from "lightningcss"
3
2
 
4
3
  /** @import * as Votive from "votive" */
5
4
 
6
5
  /** @type {Votive.ProcessorWrite} */
7
- function writeCSS(destination, database, config) {
6
+ function writeCSS(target, settings, api, config) {
8
7
  const { code, map } = transform({
9
- filename: destination.path,
10
- code: Buffer.from(destination.abstract.css),
8
+ filename: target.path,
9
+ code: Buffer.from(target.abstract.css),
11
10
  minify: true,
12
11
  targets: {
13
12
  chrome: 140 << 16,
@@ -27,15 +26,7 @@ function writeCSS(destination, database, config) {
27
26
 
28
27
 
29
28
  /** @type {Votive.ReadText} */
30
- function readCSS(text, filePath, destinationPath, database, config) {
31
- const pathInfo = path.parse(filePath)
32
-
33
- database.setting.create(
34
- pathInfo.dir,
35
- "stylesheets",
36
- filePath
37
- )
38
-
29
+ function readCSS(text, filePath, targetPath, api, config) {
39
30
  const metadata = {}
40
31
  const abstract = {
41
32
  css: text
@@ -43,7 +34,8 @@ function readCSS(text, filePath, destinationPath, database, config) {
43
34
 
44
35
  return {
45
36
  metadata,
46
- abstract
37
+ abstract,
38
+ settings: { stylesheets: [filePath] }
47
39
  }
48
40
  }
49
41
 
@@ -0,0 +1,91 @@
1
+ import { parseURLMetadata } from "votive/internals"
2
+
3
+ /** @import * as Votive from "votive" */
4
+
5
+ const EXTERNAL_LINK_RE = /^https?:\/\/\S+$/
6
+
7
+ /**
8
+ * Matches a markdown paragraph whose only content is a bare URL - see
9
+ * CLAUDE.md: "a URL that is the only child of a paragraph" -
10
+ * Dinosaurs lived 65,000,000 years ago.
11
+ *
12
+ * https://dinosaurs.com/timeline
13
+ *
14
+ * We still find their fossils today.
15
+ * Same shape html/index.js's own `testPaths` already matches for
16
+ * internal single-link paragraphs (one text-only child) - this is the
17
+ * external-URL counterpart.
18
+ * @param {any} node
19
+ */
20
+ function isExternalLinkParagraph(node) {
21
+ if (node.type !== "element") return false
22
+ if (node.tagName !== "p") return false
23
+ if (node.children.length !== 1) return false
24
+ const [child] = node.children
25
+ if (!child || typeof child.value !== "string") return false
26
+ return EXTERNAL_LINK_RE.test(child.value)
27
+ }
28
+
29
+ /**
30
+ * Walks a target's abstract for link-preview paragraphs and queues one
31
+ * fetch task per URL found, so the write side (html/index.js) can render
32
+ * a preview card once the data is cached. Deliberately does not fetch
33
+ * anything itself - tasks are only ever run when the caller invokes
34
+ * runFetches() (see votive's fetchURLs.js), and a URL already cached
35
+ * (success or in a failure cooldown) is skipped automatically, so this
36
+ * never re-fetches the same link on every build.
37
+ * @type {Votive.ReadAbstract}
38
+ */
39
+ function transformFile(abstract, settings, api, config, targetFilePath) {
40
+ const urls = []
41
+
42
+ function walk(node) {
43
+ if (!node || typeof node !== "object") return
44
+ if (isExternalLinkParagraph(node)) {
45
+ urls.push({
46
+ data: node.children[0].value,
47
+ runner: "text",
48
+ target: targetFilePath
49
+ })
50
+ return
51
+ }
52
+ if (Array.isArray(node.children)) node.children.forEach(walk)
53
+ }
54
+
55
+ walk(abstract)
56
+
57
+ return { abstract, urls }
58
+ }
59
+
60
+ /**
61
+ * Parses a fetched page into the flat shape link-preview rendering
62
+ * wants, preferring OpenGraph, then standard meta tags, then Twitter
63
+ * Card, for each field independently.
64
+ * @param {string} html
65
+ * @returns {{ title: string, description: string, image: string | undefined }}
66
+ */
67
+ function parseLinkPreview(html) {
68
+ const meta = parseURLMetadata(html)
69
+ return {
70
+ title: meta.openGraph.title || meta.title || meta.twitterCard.title || "",
71
+ description: meta.openGraph.description || meta.description || meta.twitterCard.description || "",
72
+ image: meta.openGraph.image || meta.twitterCard.image || undefined
73
+ }
74
+ }
75
+
76
+ /** @type {Votive.VotiveProcessor} */
77
+ const linkPreviewProcessor = {
78
+ extensions: [".html"],
79
+ format: "text",
80
+ transformFile,
81
+ readURL: parseLinkPreview
82
+ }
83
+
84
+ /** @type {Votive.VotivePlugin} */
85
+ const vowelURLsPlugin = {
86
+ name: "vowel-urls",
87
+ processors: [linkPreviewProcessor]
88
+ }
89
+
90
+ export default vowelURLsPlugin
91
+ export { isExternalLinkParagraph, parseLinkPreview }
@@ -14,7 +14,7 @@ async function writeFile(target) {
14
14
  const processor = {
15
15
  extensions: [".svg"],
16
16
  format: "text",
17
- readFile: (text, filePath, destinationPath, database) => {
17
+ readFile: (text, filePath, targetPath, api) => {
18
18
  const monochrome = text.includes("currentColor") || Boolean(text.match(/#000\b/))
19
19
  return {
20
20
  abstract: { svg: text },
@@ -7,18 +7,23 @@ import xml from "xml"
7
7
  const processor = {
8
8
  extensions: [".xml"],
9
9
  format: "text",
10
- writeFile: (target, database) => {
10
+ writeFile: (target, settings, api) => {
11
11
  if (target.path === "sitemap.xml") {
12
-
13
12
  // FIXME move the filter to SQLite
14
- const pages = database.target
15
- .getManyWithTrackers({
13
+ const pages = api
14
+ .targets({
16
15
  folder: "",
17
16
  recursive: true,
18
- query: {},
19
- dependent: "sitemap.xml"
17
+ query: {
18
+ "!": {
19
+ "|": {
20
+ "sitemap_item": false,
21
+ "html_file": false
22
+ }
23
+ }
24
+ },
20
25
  })
21
- .filter(a => a.syntax === ".html" && a.path)
26
+ .filter(a => a.extension === ".html" && a.path)
22
27
 
23
28
  const domain = target.metadata.domain
24
29
  && target.metadata.domain.startsWith("http")
@@ -58,19 +63,22 @@ const processor = {
58
63
 
59
64
  return {
60
65
  data: sitemap,
61
- jobs: []
66
+ urls: []
62
67
  }
63
68
 
64
69
  } else if (target.path === "feed.xml") {
65
70
 
66
- const pages = database.target.getManyWithTrackers({
67
- query: {},
71
+ const pages = api.targets({
72
+ query: {
73
+ "!": {
74
+ rss_item: false
75
+ }
76
+ },
68
77
  folder: "",
69
78
  recursive: true,
70
- dependent: "feed.xml"
79
+ orderBy: { property: "date", direction: "desc" }
71
80
  })
72
81
  .filter(a => a.metadata.date)
73
- .sort((a, b) => b.metadata.date - a.metadata.date)
74
82
 
75
83
  const domain = target.metadata.domain
76
84
  && target.metadata.domain.startsWith("http")
@@ -87,7 +95,7 @@ const processor = {
87
95
  },
88
96
  {
89
97
  title: target.metadata.title
90
- }
98
+ },
91
99
  )
92
100
 
93
101
 
@@ -119,7 +127,6 @@ const processor = {
119
127
  ...pages.map((page) => {
120
128
  const url = (new URL(page.path, domain)).href
121
129
 
122
-
123
130
  /** @type {object[]} */
124
131
  const entry = [
125
132
  {
@@ -151,6 +158,10 @@ const processor = {
151
158
  });
152
159
  }
153
160
 
161
+ entry.push({
162
+ content: page.data
163
+ })
164
+
154
165
  // TODO consider rendering full page in RSS
155
166
 
156
167
  /*
@@ -0,0 +1,237 @@
1
+ /*
2
+ * Syntax highlighting theme for code blocks (rehype-highlight), based on
3
+ * highlight.js's GitHub theme. Light by default, dark under
4
+ * prefers-color-scheme - see highlight.js/styles/github.css and
5
+ * github-dark.css for the source themes this was combined from.
6
+ */
7
+ pre code.hljs {
8
+ display: block;
9
+ overflow-x: auto;
10
+ padding: 1em
11
+ }
12
+ code.hljs {
13
+ padding: 3px 5px
14
+ }
15
+ /*!
16
+ Theme: GitHub
17
+ Description: Light theme as seen on github.com
18
+ Author: github.com
19
+ Maintainer: @Hirse
20
+ Updated: 2021-05-15
21
+
22
+ Outdated base version: https://github.com/primer/github-syntax-light
23
+ Current colors taken from GitHub's CSS
24
+ */
25
+ .hljs {
26
+ color: #24292e;
27
+ background: #ffffff
28
+ }
29
+ .hljs-doctag,
30
+ .hljs-keyword,
31
+ .hljs-meta .hljs-keyword,
32
+ .hljs-template-tag,
33
+ .hljs-template-variable,
34
+ .hljs-type,
35
+ .hljs-variable.language_ {
36
+ /* prettylights-syntax-keyword */
37
+ color: #d73a49
38
+ }
39
+ .hljs-title,
40
+ .hljs-title.class_,
41
+ .hljs-title.class_.inherited__,
42
+ .hljs-title.function_ {
43
+ /* prettylights-syntax-entity */
44
+ color: #6f42c1
45
+ }
46
+ .hljs-attr,
47
+ .hljs-attribute,
48
+ .hljs-literal,
49
+ .hljs-meta,
50
+ .hljs-number,
51
+ .hljs-operator,
52
+ .hljs-variable,
53
+ .hljs-selector-attr,
54
+ .hljs-selector-class,
55
+ .hljs-selector-id {
56
+ /* prettylights-syntax-constant */
57
+ color: #005cc5
58
+ }
59
+ .hljs-regexp,
60
+ .hljs-string,
61
+ .hljs-meta .hljs-string {
62
+ /* prettylights-syntax-string */
63
+ color: #032f62
64
+ }
65
+ .hljs-built_in,
66
+ .hljs-symbol {
67
+ /* prettylights-syntax-variable */
68
+ color: #e36209
69
+ }
70
+ .hljs-comment,
71
+ .hljs-code,
72
+ .hljs-formula {
73
+ /* prettylights-syntax-comment */
74
+ color: #6a737d
75
+ }
76
+ .hljs-name,
77
+ .hljs-quote,
78
+ .hljs-selector-tag,
79
+ .hljs-selector-pseudo {
80
+ /* prettylights-syntax-entity-tag */
81
+ color: #22863a
82
+ }
83
+ .hljs-subst {
84
+ /* prettylights-syntax-storage-modifier-import */
85
+ color: #24292e
86
+ }
87
+ .hljs-section {
88
+ /* prettylights-syntax-markup-heading */
89
+ color: #005cc5;
90
+ font-weight: bold
91
+ }
92
+ .hljs-bullet {
93
+ /* prettylights-syntax-markup-list */
94
+ color: #735c0f
95
+ }
96
+ .hljs-emphasis {
97
+ /* prettylights-syntax-markup-italic */
98
+ color: #24292e;
99
+ font-style: italic
100
+ }
101
+ .hljs-strong {
102
+ /* prettylights-syntax-markup-bold */
103
+ color: #24292e;
104
+ font-weight: bold
105
+ }
106
+ .hljs-addition {
107
+ /* prettylights-syntax-markup-inserted */
108
+ color: #22863a;
109
+ background-color: #f0fff4
110
+ }
111
+ .hljs-deletion {
112
+ /* prettylights-syntax-markup-deleted */
113
+ color: #b31d28;
114
+ background-color: #ffeef0
115
+ }
116
+ .hljs-char.escape_,
117
+ .hljs-link,
118
+ .hljs-params,
119
+ .hljs-property,
120
+ .hljs-punctuation,
121
+ .hljs-tag {
122
+ /* purposely ignored */
123
+
124
+ }
125
+
126
+ @media (prefers-color-scheme: dark) {
127
+ /*!
128
+ Theme: GitHub Dark
129
+ Description: Dark theme as seen on github.com
130
+ Author: github.com
131
+ Maintainer: @Hirse
132
+ Updated: 2021-05-15
133
+
134
+ Outdated base version: https://github.com/primer/github-syntax-dark
135
+ Current colors taken from GitHub's CSS
136
+ */
137
+ .hljs {
138
+ color: #c9d1d9;
139
+ background: #0d1117
140
+ }
141
+ .hljs-doctag,
142
+ .hljs-keyword,
143
+ .hljs-meta .hljs-keyword,
144
+ .hljs-template-tag,
145
+ .hljs-template-variable,
146
+ .hljs-type,
147
+ .hljs-variable.language_ {
148
+ /* prettylights-syntax-keyword */
149
+ color: #ff7b72
150
+ }
151
+ .hljs-title,
152
+ .hljs-title.class_,
153
+ .hljs-title.class_.inherited__,
154
+ .hljs-title.function_ {
155
+ /* prettylights-syntax-entity */
156
+ color: #d2a8ff
157
+ }
158
+ .hljs-attr,
159
+ .hljs-attribute,
160
+ .hljs-literal,
161
+ .hljs-meta,
162
+ .hljs-number,
163
+ .hljs-operator,
164
+ .hljs-variable,
165
+ .hljs-selector-attr,
166
+ .hljs-selector-class,
167
+ .hljs-selector-id {
168
+ /* prettylights-syntax-constant */
169
+ color: #79c0ff
170
+ }
171
+ .hljs-regexp,
172
+ .hljs-string,
173
+ .hljs-meta .hljs-string {
174
+ /* prettylights-syntax-string */
175
+ color: #a5d6ff
176
+ }
177
+ .hljs-built_in,
178
+ .hljs-symbol {
179
+ /* prettylights-syntax-variable */
180
+ color: #ffa657
181
+ }
182
+ .hljs-comment,
183
+ .hljs-code,
184
+ .hljs-formula {
185
+ /* prettylights-syntax-comment */
186
+ color: #8b949e
187
+ }
188
+ .hljs-name,
189
+ .hljs-quote,
190
+ .hljs-selector-tag,
191
+ .hljs-selector-pseudo {
192
+ /* prettylights-syntax-entity-tag */
193
+ color: #7ee787
194
+ }
195
+ .hljs-subst {
196
+ /* prettylights-syntax-storage-modifier-import */
197
+ color: #c9d1d9
198
+ }
199
+ .hljs-section {
200
+ /* prettylights-syntax-markup-heading */
201
+ color: #1f6feb;
202
+ font-weight: bold
203
+ }
204
+ .hljs-bullet {
205
+ /* prettylights-syntax-markup-list */
206
+ color: #f2cc60
207
+ }
208
+ .hljs-emphasis {
209
+ /* prettylights-syntax-markup-italic */
210
+ color: #c9d1d9;
211
+ font-style: italic
212
+ }
213
+ .hljs-strong {
214
+ /* prettylights-syntax-markup-bold */
215
+ color: #c9d1d9;
216
+ font-weight: bold
217
+ }
218
+ .hljs-addition {
219
+ /* prettylights-syntax-markup-inserted */
220
+ color: #aff5b4;
221
+ background-color: #033a16
222
+ }
223
+ .hljs-deletion {
224
+ /* prettylights-syntax-markup-deleted */
225
+ color: #ffdcd7;
226
+ background-color: #67060c
227
+ }
228
+ .hljs-char.escape_,
229
+ .hljs-link,
230
+ .hljs-params,
231
+ .hljs-property,
232
+ .hljs-punctuation,
233
+ .hljs-tag {
234
+ /* purposely ignored */
235
+
236
+ }
237
+ }
package/utils.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import {fromMarkdown} from 'mdast-util-from-markdown'
2
2
  import {toHast} from 'mdast-util-to-hast'
3
- import {toHtml} from 'hast-util-to-html'
4
3
  import path from "node:path"
5
4
  import { randomUUID } from "node:crypto"
6
5
 
@@ -75,6 +74,7 @@ export function toTitleCase(string) {
75
74
  export function createHashtagPage(tag) {
76
75
  const markdown = `# ${toTitleCase(tag)}\n\n/**?tag=${tag}`
77
76
  const mdast = fromMarkdown(markdown)
77
+ const hast = toHast(mdast)
78
78
 
79
- return mdast
79
+ return hast
80
80
  }