vowel 0.6.2 → 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.
@@ -1,11 +1,8 @@
1
- import extractDate from "./../../extractDate.js"
2
1
  import path from "node:path"
3
- import rehypeDocument from 'rehype-document'
4
- import rehypePresetMinify from "rehype-preset-minify"
5
- import rehypeStringify from "rehype-stringify"
6
- import yaml from 'yaml'
7
2
  import { fromHtml } from 'hast-util-from-html'
8
3
  import { fromMarkdown } from 'mdast-util-from-markdown'
4
+ import { highlightMarkFromMarkdown } from "mdast-util-highlight-mark"
5
+ import { highlightMark } from "micromark-extension-highlight-mark"
9
6
  import { frontmatter } from "micromark-extension-frontmatter"
10
7
  import { frontmatterFromMarkdown } from 'mdast-util-frontmatter'
11
8
  import { gfmFootnote } from "micromark-extension-gfm-footnote"
@@ -13,235 +10,50 @@ import { gfmFootnoteFromMarkdown } from "mdast-util-gfm-footnote"
13
10
  import { gfmStrikethrough } from 'micromark-extension-gfm-strikethrough'
14
11
  import { gfmStrikethroughFromMarkdown } from 'mdast-util-gfm-strikethrough'
15
12
  import { gfmTable } from 'micromark-extension-gfm-table'
16
- import rehypeGithubAlert from "rehype-github-alert"
17
13
  import { gfmTableFromMarkdown } from 'mdast-util-gfm-table'
18
- import { h } from 'hastscript'
19
14
  import { normalizeHeadings } from 'mdast-normalize-headings'
20
15
  import { readFileSync } from "fs"
21
- import { remove } from "unist-util-remove"
22
- import { testURL, testHashtags, createHashtagPage, toTitleCase, hashtagRegexSingle, createImagePaths, imageSizes, imageExts } from "./../../utils.js"
16
+ import { testURL, testHashtags, createHashtagPage, toTitleCase, hashtagRegexSingle } from "./../../utils.js"
23
17
  import { toHast } from 'mdast-util-to-hast'
24
18
  import { toString as hastToString } from 'hast-util-to-string'
25
- import { toString as mdastToString } from 'mdast-util-to-string'
26
- import { unified } from "unified"
27
19
  import { visit } from "unist-util-visit"
28
- import toc from "@jsdevtools/rehype-toc"
29
- import slug from "rehype-slug"
30
- import * as unpic from "unpic"
20
+ import getMetadata from "./metadata.js"
21
+ import generateRobots from "./robots.js"
22
+ import { h } from "hastscript"
23
+ import { hash } from "node:crypto"
24
+ import { styleText } from "node:util"
31
25
 
32
26
  const VOWEL_DIR = path.normalize(path.join(import.meta.dirname, "../../"))
33
27
 
34
28
  /** @import * as Votive from "votive" */
35
29
  /** @import * as Vowel from "./../../index.js" */
36
30
 
37
-
38
- const robots = `User-agent: Google-Extended
39
- Allow: /
40
-
41
- User-agent: Googlebot-Image
42
- Disallow: /
43
-
44
- User-agent: GPTBot
45
- Disallow: /
46
-
47
- User-agent: ChatGPT-User
48
- Disallow: /
49
-
50
- User-agent: CCBot
51
- Disallow: /`
52
-
53
-
54
-
55
- /**
56
- * @param {string} imagePath
57
- * @param {Votive.Database} database
58
- * @param {string} dependent
59
- */
60
- function createDynamicImage(imagePath, database, dependent, alt, itemprop) {
61
- // TODO: Hardcode image height and width
62
- const parsed = path.parse(imagePath);
63
- const isURL = testURL(imagePath)
64
- const isImg = !parsed.ext.search(/^.(png|jpeg|jpg)$/);
65
-
66
- if (isURL) {
67
- const parsedImageURL = unpic.parseUrl(imagePath)
68
- if (parsedImageURL) {
69
-
70
- const sources = [...imageExts, "jpeg"].map((format, index) => {
71
-
72
- const isImg = index === imageExts.length
73
- // TODO: Ignore images that have options already specified
74
- const urls = imageSizes.map(size => {
75
- return unpic.transformUrl({
76
- format: format,
77
- url: parsedImageURL.src,
78
- provider: parsedImageURL.cdn,
79
- width: size
80
- })
81
- })
82
-
83
- const sizes = urls.map((url, index) => `${url} ${imageSizes[index]}w`).join(", ")
84
-
85
- return h(isImg ? "img" : "source", {
86
- type: "image/" + format,
87
- srcset: sizes,
88
- src: isImg && urls.at(-1),
89
- loading: isImg && "lazy",
90
- sizes: "100vw",
91
- alt: isImg && alt
92
- })
93
- })
94
-
95
- return h("picture", { itemprop: itemprop && "image" }, sources)
96
- }
97
- }
98
-
99
- if (!isImg) return
100
-
101
- const relativePath = imagePath.startsWith("/") ? path.relative("/", imagePath) : imagePath
102
- const image = database.target.getWithTrackers(relativePath, dependent)
103
- console.log({ image, relativePath })
104
- if (!image) return
105
- const formats = createImagePaths(image.abstract.sourcePath, "./", image.abstract.uuid)
106
-
107
-
108
- const sources = formats.map((format, index) => {
109
- const isImg = index === formats.length - 1
110
-
111
- const sizes = format.map((size, index) => `/${size} ${imageSizes[index]}w`).join(", ")
112
- const type = "image/" + path.extname(format[0]).slice(1)
113
- return h(isImg ? "img" : "source", {
114
- type,
115
- srcset: sizes,
116
- loading: isImg && "lazy",
117
- src: isImg && "/" + format.at(-1),
118
- sizes: "100vw",
119
- alt: isImg && alt
120
- })
121
- })
122
-
123
-
124
- return h("picture", { itemprop: itemprop && "image" }, sources)
125
- }
126
-
127
- /**
128
- * @param {object} metadata
129
- * @param {string} url
130
- * @param {Votive.Database} database
131
- * @param {Votive.VotiveConfig} config
132
- */
133
- function makeHead(metadata, url, database, config) {
134
-
135
- const treeMainHead = []
136
-
137
- if (metadata.title) treeMainHead.push(
138
- h("h1", metadata.title)
139
- )
140
-
141
- if (metadata.date) {
142
- const date = new Date(metadata.date)
143
- treeMainHead.push(
144
- h("time",
145
- {
146
- datetime: date.toISOString(),
147
- itemprop: "date"
148
- },
149
- date.toLocaleDateString("en-US", {
150
- year: "numeric",
151
- month: "long",
152
- day: "numeric"
153
- })
154
- )
155
- )
156
- }
157
-
158
-
159
-
160
-
161
- if (metadata.image || (metadata.first_image && url)) {
162
- const metaImage = metadata.image || metadata.first_image
163
-
164
- treeMainHead.push(
165
- createDynamicImage(metaImage, database, url, null, true)
166
- )
167
- }
168
-
169
- if (metadata.fm_description) treeMainHead.push(
170
- h("p",
171
- {
172
- itemprop: "description"
173
- },
174
- metadata.fm_description
175
- )
176
- )
177
-
178
- if (url) return h('a', { href: url }, treeMainHead)
179
-
180
- return treeMainHead
181
- }
182
-
183
-
184
- function readURL(data) {
185
- const hast = fromHtml(data)
186
- const metadata = {}
187
-
188
- // TODO: Remove to a separate processor
189
- visit(hast, (node) => {
190
- if (node.tagName === "meta") {
191
- if (node.properties && node.properties.property) {
192
- metadata[node.properties.property] = node.properties.content
193
- }
194
- } else if (node.tagName === "title") {
195
- metadata.title = hastToString(node)
196
- } else if (node.tagName === "link") {
197
- if (node.properties?.rel?.includes("me")) {
198
- metadata.me = node.properties.href
199
- } else if (node.properties?.rel?.includes("webmention")) {
200
- metadata.webmention = node.properties.href
201
- } else if (node.properties?.rel?.includes("icon")) {
202
- metadata.icon = node.properties.href
203
- }
204
- }
205
- })
206
-
207
- return metadata
208
- }
209
-
210
-
211
31
  /** @type {Votive.ReadText} */
212
- function readFile(string, filePath, destinationPath, database, config) {
32
+ function readFile(string, filePath, targetPath, api, config) {
33
+ const urls = []
34
+
213
35
  const mdast = fromMarkdown(string, {
214
36
  // Micromark extensions
215
37
  extensions: [
216
38
  frontmatter(),
217
39
  gfmFootnote(),
218
40
  gfmStrikethrough(),
219
- gfmTable()
41
+ gfmTable(),
42
+ highlightMark()
220
43
  ],
221
44
  mdastExtensions: [
222
45
  frontmatterFromMarkdown(),
223
46
  gfmFootnoteFromMarkdown(),
224
47
  gfmStrikethroughFromMarkdown(),
225
- gfmTableFromMarkdown()
48
+ gfmTableFromMarkdown(),
49
+ highlightMarkFromMarkdown
226
50
  ]
227
51
  })
228
52
 
229
53
  normalizeHeadings(mdast)
230
54
  const pathInfo = path.parse(filePath)
231
55
 
232
- const metadata = getMetadata(mdast, filePath)
233
-
234
- metadata.inferred_label = toTitleCase(pathInfo.name)
235
-
236
- if (destinationPath) {
237
- const destinationInfo = path.parse(destinationPath)
238
- const name = destinationInfo.name === "index" ? "" : destinationInfo.name
239
- // FIXME the prettyURL should include the preceding slash
240
- metadata.prettyURL = (new URL(`${destinationInfo.dir}/${name}`, "thismessage:/")).pathname
241
- }
242
- const jobs = []
243
-
244
- selectMetadata(metadata)
56
+ const metadata = getMetadata(mdast, filePath, targetPath)
245
57
 
246
58
  if (!metadata.image) {
247
59
  const firstImageParagraph = mdast.children.find(child => child.children && child.children[0].type === "image")
@@ -250,17 +62,20 @@ function readFile(string, filePath, destinationPath, database, config) {
250
62
  }
251
63
  }
252
64
 
65
+
253
66
  if (metadata.fm_published === false) return
254
67
 
255
- if (pathInfo.base === "settings.md") {
256
- for (const key in metadata) {
257
- database.setting.create(
258
- pathInfo.dir,
259
- key,
260
- metadata[key],
261
- filePath
262
- )
263
- }
68
+ const secretFileName = metadata.secret_key && hash("MD5", filePath + metadata.secret_key)
69
+ const secretFileInfo = secretFileName && router({ name: secretFileName, dir: pathInfo.dir.split(path.sep), ext: ".html" })
70
+ const secretFilePath = secretFileInfo && path.format({ name: secretFileInfo.name, dir: secretFileInfo.dir.join(path.sep), ext: secretFileInfo.ext })
71
+ if(secretFilePath) {
72
+ const secretPrettyFilePath = "/" + secretFilePath.slice(0, -5)
73
+ metadata.prettyURL = secretPrettyFilePath
74
+ metadata.local_menu_item ??= false
75
+ metadata.global_menu_item ??= false
76
+ metadata.sitemap_item ??= false
77
+ metadata.rss_item ??= false
78
+ console.info(`${styleText("dim", "build: ")}: secret file path for ${filePath}: ${secretPrettyFilePath}`)
264
79
  }
265
80
 
266
81
 
@@ -270,10 +85,10 @@ function readFile(string, filePath, destinationPath, database, config) {
270
85
 
271
86
  if (validURL) {
272
87
 
273
- jobs.push({
88
+ urls.push({
274
89
  data: node.value,
275
90
  runner: "text",
276
- destination: destinationPath
91
+ target: targetPath
277
92
  })
278
93
 
279
94
  return
@@ -326,25 +141,22 @@ function readFile(string, filePath, destinationPath, database, config) {
326
141
 
327
142
  const markdown = `/tags/**`
328
143
  const mdast = fromMarkdown(markdown)
329
-
330
- const extant = database.target.get("tags.html")
331
-
332
- // FIXME: Update abstract format
333
- if (!extant) {
334
- // Delete if unnecessary
335
- database.target.create({
336
- path: `tags.html`,
337
- abstract: mdast,
338
- metadata: {
339
- breadcrumb: "Tags",
340
- title: "Tags",
341
- prettyURL: "/tags",
342
- }
343
- })
344
- }
345
-
346
- database.dependency.track({}, "tags", null, destinationPath, "tags.html")
347
- database.target.markStale("tags.html")
144
+ const hast = toHast(mdast)
145
+
146
+ // target.create() is upsert-safe and this is a deterministic
147
+ // function of a fixed string - re-creating it once per
148
+ // hashtag-bearing file converges to a no-op after the first,
149
+ // without needing to check whether it already exists first.
150
+ api.createTarget({
151
+ path: `tags.html`,
152
+ abstract: hast,
153
+ metadata: {
154
+ breadcrumb: "Tags",
155
+ title: "Tags",
156
+ prettyURL: "/tags",
157
+ hastAbstract: hast,
158
+ }
159
+ })
348
160
 
349
161
  if (hashtags) {
350
162
  if (!metadata.tags) {
@@ -366,10 +178,11 @@ function readFile(string, filePath, destinationPath, database, config) {
366
178
  title: title,
367
179
  prettyURL: `/tags/${hashtag}`,
368
180
  type: "tag",
369
- tag: hashtag
181
+ tag: hashtag,
182
+ hastAbstract: abstract,
370
183
  }
371
184
 
372
- const created = database.target.create({
185
+ const created = api.createTarget({
373
186
  path: `tags/${hashtag}.html`,
374
187
  abstract,
375
188
  metadata: tagMetadata
@@ -384,7 +197,7 @@ function readFile(string, filePath, destinationPath, database, config) {
384
197
 
385
198
  if (filePath === "settings.md") {
386
199
  if (metadata.fm_domain) {
387
- database.target.create({
200
+ api.createTarget({
388
201
  path: "sitemap.xml",
389
202
  abstract: {},
390
203
  metadata: {
@@ -393,7 +206,7 @@ function readFile(string, filePath, destinationPath, database, config) {
393
206
  }
394
207
  })
395
208
 
396
- database.target.create({
209
+ api.createTarget({
397
210
  path: "feed.xml",
398
211
  abstract: {},
399
212
  metadata: {
@@ -405,154 +218,58 @@ function readFile(string, filePath, destinationPath, database, config) {
405
218
  }
406
219
 
407
220
 
408
- return { abstract: mdast, metadata, jobs }
409
- }
410
-
411
- /** @param {ReturnType<getMetadata>} metadata */
412
- function selectMetadata(metadata) {
413
- const date =
414
- metadata.fm_date
415
- || metadata.inferred_date
416
-
417
- if (date) {
418
- metadata.date = date
419
- }
420
-
421
- const title =
422
- metadata.fm_title
423
- || metadata.inferred_title
424
- || metadata.inferred_label
425
-
426
- if (title) {
427
- metadata.title = title
428
- }
429
-
430
- const breadcrumb =
431
- metadata.fm_breadcrumb
432
- || metadata.title
433
- || metadata.inferred_label
434
-
435
- if (breadcrumb) {
436
- metadata.breadcrumb = breadcrumb
437
- }
438
-
439
- const description =
440
- metadata.fm_description
441
- || metadata.tagline
442
- || metadata.inferred_description
443
-
444
- if (description) {
445
- metadata.description = description
446
- }
447
-
448
- const image =
449
- metadata.fm_image
450
- || metadata.inferred_image
221
+ const hast = toHast(mdast, {
222
+ unknownHandler: (_, n, p) => n.type === "highlight" && h("mark", n.children)
223
+ })
451
224
 
452
- if (image) {
453
- metadata.image = image
225
+ return {
226
+ abstract: hast,
227
+ filePath: secretFilePath,
228
+ write: metadata.html_file ?? true,
229
+ metadata: { ...metadata, hastAbstract: hast },
230
+ data: "hello",
231
+ settings: pathInfo.base === "settings.md" ? metadata : undefined
454
232
  }
455
233
  }
456
234
 
457
- /**
458
- * @param {string} text
459
- */
460
- function truncateText(text) {
461
- // const match = text.match(/^(?<desc>((\b.+?){28})\S)\s(?<etc>.+)$/)
462
- // if (!match) {
463
- return text
464
- // } else if (match.groups.etc) {
465
- // console.log("truncate 4")
466
- // return match.groups.desc + "..."
467
- // } else {
468
- // console.log("truncate 5")
469
- // return match.groups.desc
470
- // }
471
- }
472
-
473
- /** @param {object} tree */
474
- function getMetadata(tree, filePath) {
475
- const metadata = {}
476
-
477
- for (let i = 0; i < tree.children.length; i++) {
478
- const child = tree.children[i]
479
- const text = mdastToString(child)
480
- switch (child.type) {
481
- case "paragraph":
482
- if (child.children.length !== 1) {
483
- if (!metadata.fm_description && !metadata.inferred_description) {
484
- metadata.inferred_description = text
485
- }
486
- i = Infinity
487
- break
488
- } else if (child.children[0].type === "image") {
489
- metadata.inferred_image = child.children[0].url
490
- metadata.inferred_alt_text = child.children[0].alt
491
- break
492
- } else if (testURL(text)) {
493
- const url = new URL(text)
494
- if (text.match(/\.(jpeg|jpg|png)$/)) {
495
- metadata.inferred_image = url
496
- break
497
- } else {
498
- metadata.inferred_link = url
499
- break
500
- }
501
- } else {
502
- const inferred_date = extractDate(mdastToString(child))
503
- if (inferred_date) {
504
- metadata.inferred_date = inferred_date
505
- } else {
506
- if (!metadata.fm_description && !metadata.inferred_description) {
507
- metadata.inferred_description = text
508
- }
509
- // tree.children.splice(0, i + 1)
510
- i = Infinity
511
- }
512
- break
513
- }
514
- case "heading":
515
- if (child.depth === 1) {
516
- metadata.inferred_title = mdastToString(child) || toTitleCase(path.parse(filePath).name)
517
- tree.children.splice(0, i + 1)
518
- } else {
519
- i = Infinity;
520
- }
521
- break
522
- case "yaml":
523
- const frontmatter = yaml.parse(child.value)
524
- for (const key in frontmatter) {
525
- metadata["fm_" + key] = frontmatter[key]
526
- }
527
- break
528
- default:
529
- i = Infinity
530
- break
531
- }
532
- }
533
-
534
- return metadata
535
235
 
536
- }
537
236
 
538
237
  /** @type {Votive.ReadAbstract} */
539
- function transformFile(abstract, database, config) {
540
- const jobs = []
541
- return { abstract, jobs }
238
+ function transformFile(abstract, settings, api, config) {
239
+ const urls = []
240
+ return { abstract, urls }
542
241
  }
543
242
 
544
243
  /** @type {Votive.ReadFolder} */
545
- function readFolder(folder, database, config, isRoot) {
244
+ function readFolder(folder, settings, api, config, isRoot) {
546
245
  if (folder === "") {
547
- database.target.create({
246
+ api.createTarget({
548
247
  path: "robots.txt",
549
248
  abstract: {
550
- content: robots
249
+ content: generateRobots()
551
250
  },
552
251
  metadata: {}
553
252
  })
554
253
  }
555
- const settings = database.setting.getByFolder(folder)
254
+
255
+ const pageNotFound = api.target("404.html")
256
+
257
+ if(!pageNotFound) {
258
+ const abstract = toHast(fromMarkdown(`# 404\n\nPage not found.`))
259
+ api.createTarget({
260
+ abstract,
261
+ metadata: {
262
+ title: "Page not found",
263
+ breadcrumb: "404",
264
+ prettyURL: "404.html",
265
+ hastAbstract: abstract,
266
+ },
267
+ path: "404.html",
268
+ extension: ".html"
269
+ })
270
+ }
271
+
272
+ const newSettings = {}
556
273
 
557
274
  const folderInfo = path.parse(folder)
558
275
 
@@ -569,8 +286,8 @@ function readFolder(folder, database, config, isRoot) {
569
286
  })
570
287
 
571
288
 
572
- const aliasFile = database.target.get(aliasPath)
573
- const indexFile = database.target.get(indexPath)
289
+ const aliasFile = api.target(aliasPath)
290
+ const indexFile = api.target(indexPath)
574
291
 
575
292
  if (!isRoot) {
576
293
  if (!aliasFile) {
@@ -584,15 +301,16 @@ function readFolder(folder, database, config, isRoot) {
584
301
 
585
302
  const indexPath = prettyURL + "/*"
586
303
 
587
- const abstract = fromMarkdown(`# ${title}\n\n${indexPath}`)
588
- database.target.create({
304
+ const abstract = toHast(fromMarkdown(`# ${title}\n\n${indexPath}`))
305
+ api.createTarget({
589
306
  abstract,
590
307
  path: aliasPath,
591
- syntax: "html",
308
+ extension: ".html",
592
309
  metadata: {
593
310
  title: toTitleCase(folderInfo.name),
594
311
  breadcrumb: toTitleCase(folderInfo.name),
595
- prettyURL
312
+ prettyURL,
313
+ hastAbstract: abstract,
596
314
  }
597
315
  })
598
316
  }
@@ -603,135 +321,91 @@ function readFolder(folder, database, config, isRoot) {
603
321
  const indexPath = prettyURL + "/*"
604
322
 
605
323
  const abstract = fromMarkdown(`# ${title}\n\n${indexPath}`)
606
- database.target.create({
324
+ api.createTarget({
607
325
  abstract,
608
326
  path: "index.html",
609
- syntax: "html",
327
+ extension: "html",
610
328
  metadata: {
611
329
  title,
612
330
  breadcrumb: title,
613
- prettyURL: "/"
331
+ prettyURL: "/",
332
+ hastAbstract: abstract,
614
333
  }
615
334
  })
616
335
  }
617
336
  }
618
337
 
619
- // if (!indexFile && (!isRoot && !aliasFile)) {
620
- // database.target.create({
621
- // metadata: {
622
- // title: toTitleCase(folderInfo.name),
623
- // breadcrumb: toTitleCase(folderInfo.name),
624
- // prettyURL: "/" + path.normalize(path.format({
625
- // dir: path.join(folderInfo.dir),
626
- // name: folderInfo.name
627
- // }))
628
- // },
629
- // path: aliasPath,
630
- // abstract: {},
631
- // syntax: "html"
632
- // })
633
- // }
634
-
635
-
636
-
637
-
638
338
  if (isRoot) {
639
- const settings = database.setting.getByFolder(config.sourceFolder)
640
-
641
- setTheme(settings)
642
-
643
- function setTheme(settings) {
644
- const themes = [
645
- "reset",
646
- "typography",
647
- "default"
648
- ]
649
-
339
+ const themes = ["reset", "typography", "default"]
340
+ const existingTheme = settings.fm_theme?.[0]?.at(-1)
650
341
 
651
- if (themes.includes(settings.fm_theme?.[""][0]) || !settings.fm_theme) {
652
- if (!settings.fm_theme) database.setting.create("", "theme", "default")
342
+ if (!existingTheme || themes.includes(existingTheme)) {
343
+ if (!existingTheme) newSettings.theme = "default"
653
344
 
654
- const theme = settings.fm_theme?.[""][0] || "default"
345
+ const theme = existingTheme || "default"
655
346
 
656
- if (themes.includes(theme)) {
347
+ if (themes.includes(theme)) {
348
+ newSettings.stylesheets = ["reset.css"]
657
349
 
658
- database.setting.create(
659
- "",
660
- "stylesheets",
661
- "reset.css"
662
- )
663
-
664
- const resetStylesPath = path.join(VOWEL_DIR, "stylesheets", "ResetStyles.css")
665
- const resetStyles = readFileSync(resetStylesPath, "utf-8")
666
-
667
- database.target.create({
668
- path: "reset.css",
669
- abstract: { css: resetStyles },
670
- metadata: {},
671
- syntax: "css"
672
- })
350
+ const resetStylesPath = path.join(VOWEL_DIR, "stylesheets", "ResetStyles.css")
351
+ const resetStyles = readFileSync(resetStylesPath, "utf-8")
673
352
 
674
- if (theme === "reset") return
353
+ api.createTarget({
354
+ path: "reset.css",
355
+ abstract: { css: resetStyles },
356
+ metadata: {},
357
+ extension: "css"
358
+ })
675
359
 
676
- database.setting.create(
677
- "",
678
- "stylesheets",
679
- "typography.css"
680
- )
360
+ if (theme !== "reset") {
361
+ newSettings.stylesheets.push("typography.css")
681
362
 
682
363
  const typeStylesPath = path.join(VOWEL_DIR, "stylesheets", "TypographyStyles.css")
683
364
  const typeStyles = readFileSync(typeStylesPath, "utf-8")
684
365
 
685
- database.target.create({
366
+ api.createTarget({
686
367
  path: "typography.css",
687
368
  abstract: { css: typeStyles },
688
369
  metadata: {},
689
- syntax: "css"
370
+ extension: "css"
690
371
  })
691
372
 
692
- if (theme === "typography") return
693
-
694
- database.setting.create(
695
- "",
696
- "stylesheets",
697
- "default.css"
698
- )
373
+ if (theme !== "typography") {
374
+ newSettings.stylesheets.push("default.css")
699
375
 
700
- const defaultStylesPath = path.join(VOWEL_DIR, "stylesheets", "DefaultStyles.css")
701
- const defaultStyles = readFileSync(defaultStylesPath, "utf-8")
376
+ const defaultStylesPath = path.join(VOWEL_DIR, "stylesheets", "DefaultStyles.css")
377
+ const defaultStyles = readFileSync(defaultStylesPath, "utf-8")
702
378
 
703
- database.target.create({
704
- path: "default.css",
705
- abstract: { css: defaultStyles },
706
- metadata: {},
707
- syntax: "css"
708
- })
379
+ api.createTarget({
380
+ path: "default.css",
381
+ abstract: { css: defaultStyles },
382
+ metadata: {},
383
+ extension: "css"
384
+ })
385
+ }
709
386
  }
710
387
  }
711
388
  }
712
389
 
713
- const site_title = settings.fm_title
714
- || settings.inferred_title
390
+ const site_title = settings.fm_title?.[0]?.at(-1)
391
+ || settings.inferred_title?.[0]?.at(-1)
715
392
  || (indexFile && indexFile.metadata.title)
716
393
 
717
- if (site_title && !settings.title) {
718
- database.setting.create("", "title", site_title)
394
+ if (site_title && !settings.title?.[0]?.length) {
395
+ newSettings.title = site_title
719
396
  }
720
397
 
721
- const tagline = settings.fm_tagline
722
- && settings.fm_tagline[0]
723
- || settings.inferred_description
724
- && settings.inferred_description[0]
398
+ const tagline = settings.fm_tagline?.[0]?.at(-1)
399
+ || settings.inferred_description?.[0]?.at(-1)
725
400
 
726
401
  if (tagline) {
727
- database.setting.create("", "tagline", tagline)
402
+ newSettings.tagline = tagline
728
403
  }
729
404
 
730
- const icon = settings.fm_icon
731
- && settings.fm_icon[0]
405
+ const icon = settings.fm_icon?.[0]?.at(-1)
732
406
 
733
407
  if (icon) {
734
- database.setting.create("", "icon", icon)
408
+ newSettings.icon = icon
735
409
  }
736
410
  }
737
411
 
@@ -741,602 +415,12 @@ function readFolder(folder, database, config, isRoot) {
741
415
  || toTitleCase(folder.split(path.sep).at(-1))
742
416
  || "Home"
743
417
 
744
-
745
-
746
- database.setting.create(folder, "breadcrumbs", breadcrumb)
747
-
748
- return {
749
- jobs: [],
750
- destinations: []
751
- }
752
- }
753
-
754
- /** @type {Votive.ProcessorWrite} */
755
- function writeFile(destination, database, config) {
756
- const isRoot = destination.path === "index.html"
757
-
758
- // console.log(destination.metadata)
759
-
760
- if (destination.metadata.type === "tag") {
761
- if (!destination.metadata.tag) return false
762
-
763
- const pages = database.target.getManyWithTrackers({
764
- recursive: true,
765
- dependent: destination,
766
- query: {
767
- tags: destination.metadata.tag
768
- }
769
- })
770
-
771
- if (!pages.length) return false
772
- }
773
-
774
- const settings = database.setting.getByFolder(destination.dir + path.sep)
775
-
776
- const { abstract, metadata, ...rest } = destination
777
-
778
- if (metadata.tags) metadata.tags = JSON.parse(metadata.tags)
779
-
780
- /** @param {string} filePath */
781
- function listFolders(filePath) {
782
- if (!filePath) return []
783
-
784
- const pathInfo = path.parse(filePath)
785
- const dir = pathInfo.dir && pathInfo.dir
786
- return [...listFolders(
787
- dir
788
- ), filePath]
789
- }
790
-
791
- const parsedPath = path.parse("" + destination.path)
792
-
793
- const destinationAsDir = path.relative("", path.format({
794
- dir: parsedPath.dir,
795
- name: parsedPath.name
796
- }))
797
-
798
- const ancestorFolders = listFolders(rest.dir)
799
- ancestorFolders.unshift("")
800
-
801
- const family = [...ancestorFolders, destinationAsDir].flatMap(folder => {
802
- // FIXME typing
803
- return database.target.getManyWithTrackers({
804
- folder: Array.isArray(folder) ? path.join(...folder) : folder,
805
- recursive: false,
806
- dependent: destination.path,
807
- query: {}
808
- })
809
- }).filter(({ path, dir }) => {
810
- return path && path !== "tags.html" && dir !== "tags"
811
- })
812
-
813
- const treeStyleSheets = []
814
-
815
- Object.values(settings.stylesheets).forEach(file => {
816
- file.forEach(sheet => {
817
- const cacheBuster = Math.random().toString(36).slice(2, 10);
818
- treeStyleSheets.push(
819
- h('link', {
820
- rel: "stylesheet",
821
- href: `/${sheet}?${cacheBuster}`
822
- })
823
- )
824
- })
825
- })
826
-
827
- function createTitle() {
828
- if (isRoot) {
829
- const title = [settings?.title?.[0] || metadata?.title, settings?.fm_tagline?.[""]?.[0]]
830
- .filter(a => a)
831
- .join(" - ")
832
-
833
- return title
834
- }
835
-
836
- // FIXME Check that this works properly
837
- if (metadata.title && settings.title) {
838
- const titles = [metadata.title, ...Object.values(settings.title).flatMap(a => a).reverse()]
839
- return titles.join(" - ")
840
- }
841
-
842
- if (metadata.title || settings.title) {
843
- return metadata.title || settings.title.reverse()
844
- }
845
-
846
- return "Website"
847
- }
848
-
849
-
850
- const title = createTitle()
851
-
852
- const treeHead = h('head', [
853
- h('meta', {
854
- charset: "UTF-8",
855
- }),
856
- h("meta", {
857
- name: "viewport",
858
- content: "width=device-width, initial-scale=1.0"
859
- }),
860
- h("meta", {
861
- "http-equiv": "X-UA-Compatible",
862
- content: "ie-edge"
863
- }),
864
- h('title', title),
865
- h('meta', {
866
- property: "og:title",
867
- content: title
868
- }),
869
- h('meta', {
870
- property: "og:description",
871
- content: metadata.description,
872
- }),
873
- ...treeStyleSheets,
874
- ])
875
-
876
- if (settings.fm_domain) {
877
- const settingsDomain = settings.fm_domain[""][0]
878
- const domain = settingsDomain.startsWith("http")
879
- ? settingsDomain
880
- : "https://" + settingsDomain
881
-
882
- const { href } = new URL(metadata.prettyURL, domain)
883
-
884
- treeHead.children.push(
885
- h("meta", {
886
- property: "og:url",
887
- content: href
888
- }),
889
- h("link", {
890
- rel: "canonical",
891
- href
892
- })
893
- )
894
- }
895
-
896
-
897
- if (metadata.image) {
898
- treeHead.children.push(h("meta", {
899
- property: "og:image",
900
- content: metadata.image
901
- }))
902
- }
903
-
904
- /* FIXME this could be a section title */
905
- if (settings.title) {
906
- treeHead.children.push(h("meta", {
907
- property: "og:site_name",
908
- content: settings.fm_title?.[""][0]
909
- }))
910
- }
911
-
912
- /* FIXME Properly handle this image */
913
- if (settings.icon) {
914
- treeHead.children.push(h("link", {
915
- href: "/" + settings.icon[0],
916
- rel: "icon",
917
- type: "image/png"
918
- }))
919
- }
920
-
921
- function treeNavItems(navItem) {
922
- return h('a', {
923
- href: navItem.metadata.prettyURL,
924
- "aria-current": metadata.prettyURL === navItem.metadata.prettyURL ? 'page' : null
925
- }, navItem.metadata.breadcrumb)
926
- }
927
-
928
- function navItemFilter(nav_item) {
929
- return !nav_item.metadata.date
930
- && nav_item.path !== "index.html"
931
- && nav_item.syntax === ".html"
932
- && nav_item.path
933
- }
934
-
935
- function sort_items(a, b) {
936
- if (typeof a === "number" && typeof b === "number") return a - b
937
- if (typeof b === "number") return -1
938
- if (typeof a === "number") return 1
939
- if (a.metadata.breadcrumb && b.metadata.breadcrumb) return String(a.metadata.breadcrumb).localeCompare(String(b.metadata.breadcrumb))
940
- }
941
-
942
-
943
-
944
- function treeNavFolder(navFolder) {
945
- const sorted = navFolder
946
- .filter(navItemFilter)
947
- .toSorted(sort_items)
948
-
949
- return h('nav', sorted.map(treeNavItems))
950
- }
951
-
952
-
953
- const groupedNavs = Object.groupBy(family, ({ dir }) => dir)
954
-
955
- const treeNav = Object.entries(groupedNavs)
956
- .sort(([a], [b]) => a.length - b.length)
957
- .map(([k, v]) => treeNavFolder(v))
958
- .filter(folder => folder.children.length)
959
-
960
- let treeBreadcrumbs = []
961
-
962
- const breadcrumbs = Object.entries(settings.breadcrumbs)
963
- .sort(([a], [b]) => a.length - b.length)
964
- .map(([path, [label]]) => ["/" + path, label])
965
-
966
- treeBreadcrumbs.push(
967
- ...breadcrumbs.map(([path, label]) => {
968
- return h('a', {
969
- href: path
970
- }, label)
971
- })
972
- )
973
-
974
- if (!isRoot) {
975
- treeBreadcrumbs.push(
976
- h('a', {
977
- href: destination.metadata.prettyURL,
978
- 'aria-current': 'page'
979
- }, metadata.breadcrumb)
980
- )
981
- }
982
-
983
- const headerElements = []
984
-
985
- const homeLink = []
986
-
987
- // TODO better color handling https://antfu.me/posts/icons-in-pure-css
988
- if (settings.fm_logo && settings.fm_logo[""]) {
989
- headerElements.push(
990
- h('a.logo', {
991
- href: "/",
992
- alt: "",
993
- rel: "home",
994
- "style": `mask-image: url("/${settings.fm_logo[""]}")`
995
- }, h("img", {
996
- src: "/" + settings.fm_logo[""]
997
- }))
998
- )
999
- /*
1000
- if (settings.fm_logo[""][0].endsWith(".svg")) {
1001
- const [svg] = settings.fm_logo[""]
1002
- const svgTarget = database.target.getWithTrackers(svg, destination.path)
1003
- if (svgTarget.metadata.monochrome) {
1004
- // FIXME aspect ratio
1005
- headerElements.push(
1006
- h('a.logo', {
1007
- style: `background-color: currentColor; mask-image: url('${(new URL(settings.fm_logo[""], "thismessage://")).pathname}'); mask-size: 100% 100%;`,
1008
- href: "/",
1009
- rel: "home",
1010
- alt: ""
1011
- })
1012
- )
1013
- } else {
1014
- headerElements.push(
1015
- h('a.logo', {
1016
- style: `background: url(${(new URL(settings.fm_logo[""], "thismessage://")).pathname}) norepeat center; background-color: transparent; background-size: 100% 100%;`,
1017
- href: "/",
1018
- rel: "home",
1019
- alt: ""
1020
- })
1021
- )
1022
- }
1023
- }
1024
- */
1025
- }
1026
-
1027
- if (settings.fm_wordmark && settings.fm_wordmark[""]) {
1028
- headerElements.push(h("a.wordmark", {
1029
- href: "/",
1030
- rel: "home"
1031
- }, h("img", {
1032
- src: "/" + settings.fm_wordmark[""]
1033
- })))
1034
- }
1035
-
1036
- if (settings.title && settings.title[""]) {
1037
- headerElements.push(h('a.title', { href: "/", rel: "home" }, settings.title[""]))
1038
- }
1039
-
1040
- if (settings.fm_tagline && settings.fm_tagline[""]) {
1041
- headerElements.push(h('p.tagline', settings.fm_tagline[""][0]))
1042
- }
1043
-
1044
- const treeHeader = h('header', [
1045
- ...headerElements,
1046
- ...treeNav,
1047
- ])
1048
-
1049
- const treeContent = toHast(abstract)
1050
-
1051
- function testPaths(node, i, p) {
1052
- if (node.type !== 'element') return
1053
- if (node.tagName !== 'p') return
1054
- if (node.children.length !== 1) return
1055
- if (!node.children[0].value) return
1056
- return Boolean(node.children[0].value.match(/^\/\S*$/))
1057
- }
1058
-
1059
- const slugger = unified()
1060
- .use(slug)
1061
- .use(toc, {
1062
- customizeTOC: (toc) => {
1063
- toc.properties = {
1064
- "aria-label": "Contents"
1065
- }
1066
- }
1067
- })
1068
-
1069
- const treeContentSlugged = slugger.runSync(treeContent)
1070
-
1071
- const treeMainHead = makeHead(metadata, null, database, config)
1072
-
1073
- visit(treeContent, { tagName: "img" }, (node, index, parent) => {
1074
- const { src, alt } = node.properties
1075
- const image = createDynamicImage(src, database, destination.path.path, alt)
1076
- if (index === 0 && parent.children.length > 1) {
1077
- console.log(parent.children)
1078
- const [_, ...caption] = parent.children
1079
- parent.children = [
1080
- h("figure", [image, h("figcaption", caption)])
1081
- ]
1082
- } else {
1083
- parent.children.splice(index, 1, image)
1084
- }
1085
- })
1086
-
1087
- visit(treeContent, testPaths, ({ children: [child] }, i, p) => {
1088
-
1089
- // const recursive = child.value.endsWith("**")
1090
- // const many = child.value.endsWith("*")
1091
-
1092
- const url = new URL(child.value, "thismessage://")
1093
- const { dir, base } = path.parse(url.pathname)
1094
- const recursive = base === "**"
1095
- const many = base === "*" || base === "**"
1096
-
1097
- if (!many) {
1098
- const targetFilePathInfo = path.parse(child.value)
1099
- targetFilePathInfo.ext ||= ".html"
1100
- delete targetFilePathInfo.base
1101
- const targetFilePath = path.relative("/", path.format(targetFilePathInfo))
1102
- const target = database.target.getWithTrackers(targetFilePath, destination.path)
1103
-
1104
- if (target) {
1105
- const article = h('article', makeHead(target.metadata, target.metadata.prettyURL, database, config))
1106
-
1107
- p.children.splice(i, 1, article)
1108
- }
1109
-
1110
- }
1111
-
1112
- if (many) {
1113
- const folder = path.relative("/", dir)
1114
- // const url = new URL(child.value, "thismessage://")
1115
- const count = url.searchParams.get("count")
1116
- const tag = url.searchParams.get("tag")
1117
- const query = tag
1118
- ? { tags: tag }
1119
- : {}
1120
-
1121
- const targets = database.target.getManyWithTrackers({
1122
- folder,
1123
- recursive,
1124
- query,
1125
- dependent: destination.path
1126
- })
1127
-
1128
- if (count) targets.splice(Number(count))
1129
-
1130
- const escapedDir = folder.replace("_", "--").replace(path.sep, "_")
1131
- const escapedDirs = escapedDir.split("_").filter(a => a).map((segment, index, array) => {
1132
- return "_" + array.slice(0, index + 1).join("_")
1133
- })
1134
- escapedDirs.unshift("_")
1135
- const dirClasses = escapedDirs.join(".")
1136
-
1137
- const list = h(`ul.${dirClasses}`,
1138
- targets.map(target => {
1139
- return h('li',
1140
- h('article', makeHead(target.metadata, target.metadata.prettyURL, database, config))
1141
- )
1142
- })
1143
- )
1144
-
1145
- p.children.splice(i, 1, list)
1146
- }
1147
-
1148
- })
1149
-
1150
- remove(treeContent, (n, i, p) => p.type === "root" && n.tagName === "h1")
1151
-
1152
-
1153
- // function copyTreeWithoutArticles(tree) {
1154
- // if (tree.tagName !== 'article') {
1155
- // return {
1156
- // type: tree.type,
1157
- // tagName: tree.tagName,
1158
- // properties: tree.properties,
1159
- // children: tree.children?.map(copyTreeWithoutArticles)
1160
- // }
1161
- // }
1162
- // }
1163
-
1164
-
1165
-
1166
-
1167
- const treeMain = h('main',
1168
- {
1169
- itemscope: true
1170
- },
1171
- [
1172
- h('nav', {
1173
- 'aria-label': 'Breadcrumbs'
1174
- }, treeBreadcrumbs),
1175
- treeMainHead,
1176
- treeContent
1177
- ])
1178
-
1179
- visit(treeMain, (node, index, parent) => {
1180
- /* URLs */ if (node.type === "text" && parent.tagName === 'p' && parent.children.length === 1) {
1181
- const validURL = testURL(node.value)
1182
- if (validURL) {
1183
- const metadata = database.url.get(node.value)
1184
- if (metadata) {
1185
- parent.tagName = "article"
1186
- parent.children = [
1187
- h("a", { href: node.value },
1188
- h("h2", metadata.title)
1189
- )
1190
- ]
1191
- }
1192
- }
1193
- } /* GFM Alerts */ else if (node.tagName === "blockquote") {
1194
- if (node.children[1]
1195
- && node.children[1].tagName === "p"
1196
- && node.children[1].children.length === 1
1197
- && node.children[1].children[0].type === "text"
1198
- ) {
1199
- const matches = node.children[1].children[0].value.match(/^\[!(\w+)\]$/)
1200
-
1201
- if (matches) {
1202
- const [_, alertLabel] = matches
1203
-
1204
- node.tagName = "aside"
1205
- node.properties = {
1206
- class: `alert ${alertLabel.toLowerCase()}`
1207
- }
1208
-
1209
- node.children.splice(0, 2, {
1210
- type: "element",
1211
- tagName: "h2",
1212
- children: [
1213
- {
1214
- value: toTitleCase(alertLabel),
1215
- type: "text"
1216
- }
1217
- ]
1218
- })
1219
- }
1220
-
1221
- }
1222
- }
1223
- })
1224
-
1225
- const everything = database.target.getManyWithTrackers({
1226
- folder: "",
1227
- recursive: true,
1228
- dependent: destination.path,
1229
- }).filter(target => target.path
1230
- && target.path.endsWith(".html")
1231
- && !target.metadata.date
1232
- )
1233
-
1234
- const homeFile = everything.find(item => item.path === "index.html" && item.dir === "")
1235
-
1236
- const globalNavItems = everything.filter(item => {
1237
- return item.dir === ""
1238
- && item.path !== "index.html"
1239
- && item.path !== "tags.html"
1240
- })
1241
- .map(getChildren)
1242
-
1243
- globalNavItems.unshift(homeFile)
1244
-
1245
- function getChildren(item) {
1246
- const children = everything.filter(child => {
1247
- return "/" + child.dir === item.metadata.prettyURL
1248
- && child.path !== "index.html"
1249
- })
1250
-
1251
- const populatedChildren = children.length > 0 && children.map(child => {
1252
- return getChildren(child)
1253
- })
1254
-
1255
- const node = {
1256
- path: "/" + item.path,
1257
- metadata: item.metadata
1258
- }
1259
-
1260
- if (populatedChildren) node.children = populatedChildren
1261
-
1262
- return node
1263
- }
1264
-
1265
- function treeNavItem(item) {
1266
- if (item.children) {
1267
- return h('li', [
1268
- h('a', { href: item.path }, item.metadata.breadcrumb),
1269
- treeNavList(item.children)
1270
- ])
1271
- }
1272
-
1273
- return h('li',
1274
- h('a', { href: item.path }, item.metadata.breadcrumb)
1275
- )
1276
- }
1277
-
1278
- function treeNavList(items) {
1279
- return h('ul',
1280
- items.filter(a => a).map(treeNavItem)
1281
- )
1282
- }
1283
-
1284
- const treeGlobalNav = h('nav',
1285
- treeNavList(globalNavItems)
1286
- )
1287
-
1288
- const treeAside = h('aside', treeGlobalNav)
1289
-
1290
- const treeFooter = h('footer', [
1291
- h('section.copyright', `© ${new Date().getFullYear()}`),
1292
- h('section.shoutout', [
1293
- "Made with ",
1294
- h('a', {
1295
- href: "https://vowel.cc"
1296
- }, "Vowel"),
1297
- ])
1298
- ])
1299
-
1300
- const pageClass = destination.metadata.prettyURL
1301
- .split("/")
1302
- .filter(a => a)
1303
- .map(a => a.replace("_", ""))
1304
- .join("_")
1305
- || "home"
1306
-
1307
- const treeBody = h(`body.${pageClass}`, [
1308
- treeHeader,
1309
- treeMain,
1310
- treeAside,
1311
- treeFooter
1312
- ])
1313
-
1314
- const tree = h(
1315
- null,
1316
- [
1317
- {
1318
- type: "doctype",
1319
- name: 'html'
1320
- },
1321
- h('html',
1322
- {
1323
- lang: "en"
1324
- },
1325
- [
1326
- treeHead,
1327
- treeBody
1328
- ]
1329
- )
1330
- ]
1331
- )
1332
-
1333
- const data = unified()
1334
- .use(rehypePresetMinify)
1335
- .use(rehypeStringify)
1336
- .stringify(tree)
418
+ newSettings.breadcrumbs = breadcrumb
1337
419
 
1338
420
  return {
1339
- data
421
+ urls: [],
422
+ settings: newSettings,
423
+ targets: []
1340
424
  }
1341
425
  }
1342
426
 
@@ -1377,22 +461,16 @@ const readMarkdown = {
1377
461
  extensions: [".md"],
1378
462
  format: "text",
1379
463
  readFile,
1380
- readResource: readURL,
1381
464
  transformFile,
1382
465
  readFolder,
1383
466
  }
1384
467
 
1385
- const writeHTML = {
1386
- extensions: [".html"],
1387
- format: "text",
1388
- writeFile
1389
- }
1390
468
 
1391
469
 
1392
470
  /** @type {Votive.VotivePlugin} */
1393
471
  const vowelMarkdownPlugin = {
1394
472
  name: "vowel",
1395
- processors: [readMarkdown, writeHTML],
473
+ processors: [readMarkdown],
1396
474
  router
1397
475
  }
1398
476