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