vowel 0.7.1-beta.0 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +41 -239
- package/bin.js +91 -31
- package/cli.js +32 -0
- package/config.js +12 -0
- package/extractDate.js +1 -1
- package/package.json +8 -34
- package/pathDate.js +69 -0
- package/plugins/assets/index.js +1 -1
- package/plugins/atproto/body.js +3 -1
- package/plugins/atproto/index.js +5 -45
- package/plugins/atproto/publish.js +42 -11
- package/plugins/atproto/record.js +47 -4
- package/plugins/deploy-cloudflare/index.js +4 -1
- package/plugins/export/index.js +131 -0
- package/plugins/html/bundle/index.js +101 -44
- package/plugins/html/editor/Editor.svelte +9 -5
- package/plugins/html/editor/create-session.js +3 -1
- package/plugins/html/editor/directives.js +12 -0
- package/plugins/html/editor/frontmatter.js +77 -12
- package/plugins/html/editor/ingest.js +29 -31
- package/plugins/html/editor/main.js +5 -1
- package/plugins/html/editor/serialize.js +8 -4
- package/plugins/html/editor/socket.js +34 -4
- package/plugins/html/image.js +88 -27
- package/plugins/html/index.js +237 -109
- package/plugins/html/partials.js +41 -17
- package/plugins/html/socialLinks.js +13 -11
- package/plugins/html/writeRules.js +234 -42
- package/plugins/images/favicon.png +0 -0
- package/plugins/images/index.js +69 -8
- package/plugins/markdown/colorScheme.js +70 -14
- package/plugins/markdown/frontmatter.js +18 -8
- package/plugins/markdown/index.js +94 -25
- package/plugins/markdown/metadata.js +132 -32
- package/plugins/markdown/typography.js +11 -11
- package/plugins/styles/index.js +4 -4
- package/plugins/xml/index.js +17 -10
- package/stylesheets/DefaultStyles.css +163 -108
- package/stylesheets/TypographyStyles.css +148 -81
- package/utils.js +74 -21
|
@@ -1,10 +1,54 @@
|
|
|
1
1
|
import { toString as mdastToString } from 'mdast-util-to-string'
|
|
2
2
|
import extractDate, { dateSpan } from "./../../extractDate.js"
|
|
3
|
-
import { testURL,
|
|
3
|
+
import { testURL, labelFromName } from "./../../utils.js"
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
import { displayPath } from "../../secretPaths.js"
|
|
6
|
+
import pathDate from "../../pathDate.js"
|
|
6
7
|
import { coerce, parseFrontmatter, toISODate } from "./frontmatter.js"
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* An HTML comment, or several in a row with nothing else between them.
|
|
11
|
+
* It stays in the page, but metadata never reads it (per Sam): a
|
|
12
|
+
* comment is not a title, a date or a description, and it doesn't take
|
|
13
|
+
* a block's place in the scan for one.
|
|
14
|
+
* @param {any} node
|
|
15
|
+
*/
|
|
16
|
+
function isComment(node) {
|
|
17
|
+
return node.type === "html" && /^\s*(?:<!--[\s\S]*?-->\s*)+$/.test(node.value)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A node's text for metadata: mdastToString with its HTML comments left
|
|
22
|
+
* out, at any depth. Where a comment sat between two words, the spaces
|
|
23
|
+
* either side of it close up to one.
|
|
24
|
+
* @param {any} node
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function metadataText(node) {
|
|
28
|
+
if (isComment(node)) return ""
|
|
29
|
+
if (!node.children) return mdastToString(node)
|
|
30
|
+
const text = node.children.map(metadataText).join("")
|
|
31
|
+
if (!node.children.some(isComment)) return text
|
|
32
|
+
return text.replace(/[ \t]{2,}/g, " ")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* An image the page's text names, as the browser resolves it from the
|
|
37
|
+
* page: `pic.jpg` and `./pic.jpg` are the page's folder's, `../pic.jpg`
|
|
38
|
+
* its parent's, `/pic.jpg` the root's. Stored as a path from the root,
|
|
39
|
+
* the way the html write resolves the same image (`fromPage` in
|
|
40
|
+
* writeRules.js), because a listing card and `og:image` read it from
|
|
41
|
+
* pages in other folders. An external url is left as written. (A
|
|
42
|
+
* frontmatter `image:` is a setting-style path instead: `resolvePath`.)
|
|
43
|
+
* @param {string} url - as written
|
|
44
|
+
* @param {string} folder - the page's target folder, project-relative ("" at the root)
|
|
45
|
+
*/
|
|
46
|
+
export function imageFromPage(url, folder) {
|
|
47
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(url) || url.startsWith("//")) return url
|
|
48
|
+
const base = new URL(folder ? `/${folder.split(path.sep).join("/")}/` : "/", "thismessage://")
|
|
49
|
+
return decodeURIComponent(new URL(url.replaceAll("#", "%23"), base).pathname)
|
|
50
|
+
}
|
|
51
|
+
|
|
8
52
|
|
|
9
53
|
/**
|
|
10
54
|
- menu_item: true | false
|
|
@@ -35,12 +79,11 @@ export const reservedProperties = [
|
|
|
35
79
|
"rss_item",
|
|
36
80
|
"sitemap_item",
|
|
37
81
|
"html_file",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
82
|
+
"global_nav_item",
|
|
83
|
+
"local_nav_item",
|
|
40
84
|
"theme",
|
|
41
85
|
"logo",
|
|
42
|
-
"wordmark"
|
|
43
|
-
"breadcrumb"
|
|
86
|
+
"wordmark"
|
|
44
87
|
]
|
|
45
88
|
|
|
46
89
|
/**
|
|
@@ -50,7 +93,7 @@ export const reservedProperties = [
|
|
|
50
93
|
* @param {object} tree
|
|
51
94
|
*/
|
|
52
95
|
export function findTitleNode(tree) {
|
|
53
|
-
const [first] = tree.children.filter(child => child.type !== "yaml")
|
|
96
|
+
const [first] = tree.children.filter(child => child.type !== "yaml" && !isComment(child))
|
|
54
97
|
if (!first) return null
|
|
55
98
|
if (first.type !== "heading" || first.depth !== 1) return null
|
|
56
99
|
return first
|
|
@@ -72,10 +115,10 @@ export function normalizeHeadingLevels(tree) {
|
|
|
72
115
|
}
|
|
73
116
|
|
|
74
117
|
/**
|
|
75
|
-
* Marks a recognized date
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
118
|
+
* Marks a recognized date so it renders as <time>, keeping the author's
|
|
119
|
+
* wording. The editor reads the element's text back, so that wording
|
|
120
|
+
* survives the round trip and the markup is simply re-inferred on the
|
|
121
|
+
* next build.
|
|
79
122
|
* @param {object} paragraph
|
|
80
123
|
* @param {Date} date
|
|
81
124
|
*/
|
|
@@ -88,44 +131,59 @@ function markAsTime(paragraph, date) {
|
|
|
88
131
|
}
|
|
89
132
|
|
|
90
133
|
/**
|
|
91
|
-
* Recognizable data that may precede the content.
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
134
|
+
* Recognizable data that may precede the content. A block is data only
|
|
135
|
+
* when the property it would fill is still empty: a date line on a page
|
|
136
|
+
* whose frontmatter has a `date:`, or a second date line, is content,
|
|
137
|
+
* and the scan stops there (per Sam). Returns what the block is: "date"
|
|
138
|
+
* or "image" for natural metadata, which the html plugin lifts out of
|
|
139
|
+
* the content and renders with the page's metadata (the source is never
|
|
140
|
+
* changed); "skip" for a line that is neither metadata nor content (a
|
|
141
|
+
* listing, a bare url); false for content. Adding another inferred
|
|
142
|
+
* property later (an ISBN, say) means adding a branch here, not new
|
|
143
|
+
* plumbing.
|
|
95
144
|
*
|
|
96
145
|
* @param {object} block
|
|
97
146
|
* @param {object} metadata
|
|
147
|
+
* @param {string} folder - the page's target folder, for a relative image
|
|
98
148
|
* @returns {boolean} whether the block was recognizable data
|
|
99
149
|
*/
|
|
100
|
-
function recognizeData(block, metadata) {
|
|
150
|
+
function recognizeData(block, metadata, folder) {
|
|
101
151
|
if (block.type !== "paragraph") return false
|
|
102
|
-
|
|
152
|
+
const children = block.children.filter(child => !isComment(child))
|
|
153
|
+
const [child] = children
|
|
103
154
|
|
|
104
|
-
const
|
|
155
|
+
const imageFilled = Boolean(metadata.fm_image || metadata.inferred_image)
|
|
105
156
|
|
|
106
|
-
|
|
107
|
-
|
|
157
|
+
// An image, alone or with its caption after it on the same line: the
|
|
158
|
+
// html write makes the pair a <figure> and lifts it whole. The caption
|
|
159
|
+
// is not a property of the page.
|
|
160
|
+
if (child?.type === "image") {
|
|
161
|
+
if (imageFilled) return false
|
|
162
|
+
metadata.inferred_image = imageFromPage(child.url, folder)
|
|
108
163
|
metadata.inferred_alt_text = child.alt
|
|
109
|
-
return
|
|
164
|
+
return "image"
|
|
110
165
|
}
|
|
111
166
|
|
|
112
|
-
|
|
167
|
+
if (children.length !== 1) return false
|
|
168
|
+
|
|
169
|
+
const text = metadataText(block)
|
|
113
170
|
|
|
114
171
|
// A reference or a listing directive (`/blog/post`, `/blog/**`,
|
|
115
172
|
// `./drafts/*`) is an instruction to the html plugin, not prose - it
|
|
116
173
|
// must not become the page's description, which is what a section
|
|
117
174
|
// index's own listing line was doing. Same test as writeRules'
|
|
118
175
|
// directive rule.
|
|
119
|
-
if (child.type === "text" && /^(\/|\.\.?\/)\S*$/.test(text.trim())) return
|
|
176
|
+
if (child.type === "text" && /^(\/|\.\.?\/)\S*$/.test(text.trim())) return "skip"
|
|
120
177
|
|
|
121
178
|
if (testURL(text)) {
|
|
122
179
|
const url = new URL(text)
|
|
123
180
|
if (text.match(/\.(jpeg|jpg|png)$/)) {
|
|
181
|
+
if (imageFilled) return false
|
|
124
182
|
metadata.inferred_image = url
|
|
125
183
|
} else {
|
|
126
184
|
metadata.inferred_link = url
|
|
127
185
|
}
|
|
128
|
-
return
|
|
186
|
+
return "skip"
|
|
129
187
|
}
|
|
130
188
|
|
|
131
189
|
// A date is a property of the page only when the author wrote it as
|
|
@@ -144,12 +202,28 @@ function recognizeData(block, metadata) {
|
|
|
144
202
|
|
|
145
203
|
const date = extractDate(text)
|
|
146
204
|
if (!date) return false
|
|
205
|
+
if (metadata.fm_date || metadata.inferred_date) return false
|
|
147
206
|
|
|
148
207
|
// Stored as ISO, like a frontmatter date (frontmatter.js, toISODate):
|
|
149
208
|
// one format in the column, which sorts lexically.
|
|
150
209
|
metadata.inferred_date = toISODate(date)
|
|
151
210
|
markAsTime(block, date)
|
|
152
|
-
return
|
|
211
|
+
return "date"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Marks a block as natural metadata - written in the text, not the
|
|
216
|
+
* frontmatter - for the html plugin to lift out of the content
|
|
217
|
+
* (`data-metadata`, which it removes): "date", "image", "description",
|
|
218
|
+
* or "end" for the rule that closed the description, which renders as
|
|
219
|
+
* nothing. A description is marked as one where it is rendered.
|
|
220
|
+
* @param {object} block
|
|
221
|
+
* @param {"date" | "image" | "description" | "end"} kind
|
|
222
|
+
*/
|
|
223
|
+
function markAsNatural(block, kind) {
|
|
224
|
+
const itemprop = kind === "description" ? { itemprop: "description" } : {}
|
|
225
|
+
const hProperties = { ...block.data?.hProperties, ...itemprop, dataMetadata: kind }
|
|
226
|
+
block.data = { ...block.data, hProperties }
|
|
153
227
|
}
|
|
154
228
|
|
|
155
229
|
/**
|
|
@@ -201,19 +275,37 @@ function getMetadata(tree, filePath, targetPath, report = () => {}) {
|
|
|
201
275
|
|
|
202
276
|
const titleNode = findTitleNode(tree)
|
|
203
277
|
if (titleNode) {
|
|
204
|
-
metadata.inferred_title =
|
|
278
|
+
metadata.inferred_title = metadataText(titleNode).trim() || labelFromName(path.parse(filePath).name)
|
|
205
279
|
}
|
|
206
280
|
|
|
207
281
|
// Everything up to the first block that is not recognizable data is
|
|
208
282
|
// metadata; that first block is where the content begins, and it also
|
|
209
283
|
// supplies the description.
|
|
210
|
-
const blocks = tree.children.filter(child => child.type !== "yaml" && child !== titleNode)
|
|
284
|
+
const blocks = tree.children.filter(child => child.type !== "yaml" && child !== titleNode && !isComment(child))
|
|
285
|
+
|
|
286
|
+
for (const [index, block] of blocks.entries()) {
|
|
287
|
+
const recognized = recognizeData(block, metadata, folder)
|
|
288
|
+
if (recognized === "skip") continue
|
|
289
|
+
if (recognized) {
|
|
290
|
+
markAsNatural(block, recognized)
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
211
293
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
294
|
+
// One paragraph and then a rule: the author ended their metadata
|
|
295
|
+
// with the rule, so the paragraph *is* the description - stated,
|
|
296
|
+
// not guessed, so it outranks a slogan. With a `description:` in the
|
|
297
|
+
// frontmatter it is an ordinary paragraph, and the rule an ordinary
|
|
298
|
+
// rule. (`text` then `---` on the next line is a heading in
|
|
299
|
+
// markdown, not a paragraph and a rule; it takes a blank line.)
|
|
300
|
+
const next = blocks[index + 1]
|
|
301
|
+
if (!metadata.fm_description && block.type === "paragraph" && next?.type === "thematicBreak") {
|
|
302
|
+
metadata.declared_description = metadataText(block).trim()
|
|
303
|
+
markAsNatural(block, "description")
|
|
304
|
+
markAsNatural(next, "end")
|
|
305
|
+
break
|
|
306
|
+
}
|
|
215
307
|
|
|
216
|
-
if (!metadata.fm_description) metadata.inferred_description =
|
|
308
|
+
if (!metadata.fm_description) metadata.inferred_description = metadataText(block).trim()
|
|
217
309
|
break
|
|
218
310
|
}
|
|
219
311
|
|
|
@@ -229,7 +321,14 @@ function getMetadata(tree, filePath, targetPath, report = () => {}) {
|
|
|
229
321
|
// sees, so it is the one place the salt has to be stripped.
|
|
230
322
|
const pathInfo = path.parse(displayPath(filePath))
|
|
231
323
|
|
|
232
|
-
|
|
324
|
+
// A date the author wrote wins; failing that, the one the path spells
|
|
325
|
+
// (`journal/2024/april/1.md`).
|
|
326
|
+
if (!metadata.fm_date && !metadata.inferred_date) {
|
|
327
|
+
const date = pathDate(displayPath(filePath))
|
|
328
|
+
if (date) metadata.inferred_date = toISODate(date)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
metadata.inferred_label = labelFromName(pathInfo.name)
|
|
233
332
|
|
|
234
333
|
// What a [[wikilink]] matches, the way Obsidian matches: the filename
|
|
235
334
|
// without its extension, case-insensitively, plus any `aliases:` the
|
|
@@ -283,7 +382,8 @@ function selectMetadata(metadata) {
|
|
|
283
382
|
|
|
284
383
|
const description =
|
|
285
384
|
metadata.fm_description
|
|
286
|
-
|| metadata.
|
|
385
|
+
|| metadata.declared_description
|
|
386
|
+
|| metadata.fm_slogan
|
|
287
387
|
|| metadata.inferred_description
|
|
288
388
|
|
|
289
389
|
if (description) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// the document for live preview, so preview and build cannot disagree.
|
|
10
10
|
//
|
|
11
11
|
// No bundled fonts, and no font setting: a family is CSS - the author
|
|
12
|
-
// sets `--
|
|
12
|
+
// sets `--fontfamily-sans-brand` (and the @font-face behind it) in their own
|
|
13
13
|
// stylesheet, where the sheet's stacks put a system fallback after it.
|
|
14
14
|
// Fonts by url is tasks/2-in-progress/load-fonts.md. Free of node
|
|
15
15
|
// builtins.
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* @property {string} variable - the custom property in TypographyStyles.css
|
|
21
21
|
* @property {string} label
|
|
22
22
|
* @property {string} [hint]
|
|
23
|
-
* @property {"x" | "ch" | ""} unit - "x" is a multiple of --
|
|
23
|
+
* @property {"x" | "ch" | ""} unit - "x" is a multiple of --fontsize-root
|
|
24
24
|
* @property {number} min
|
|
25
25
|
* @property {number} max
|
|
26
26
|
* @property {number} step
|
|
@@ -29,14 +29,14 @@
|
|
|
29
29
|
|
|
30
30
|
/** @type {TypographySetting[]} */
|
|
31
31
|
export const rampSettings = [
|
|
32
|
-
{ key: "heading-size", variable: "--
|
|
33
|
-
{ key: "heading-size-decay", variable: "--
|
|
34
|
-
{ key: "h1-letter-spacing", variable: "--
|
|
35
|
-
{ key: "h6-letter-spacing", variable: "--
|
|
36
|
-
{ key: "letter-spacing-decay", variable: "--
|
|
37
|
-
{ key: "h1-weight", variable: "--
|
|
38
|
-
{ key: "h6-weight", variable: "--
|
|
39
|
-
{ key: "weight-decay", variable: "--
|
|
32
|
+
{ key: "heading-size", variable: "--fontsize-delta", label: "Heading size", hint: "How much larger h1 is than body text", unit: "x", min: 0.25, max: 4, step: 0.05, default: 2 },
|
|
33
|
+
{ key: "heading-size-decay", variable: "--fontsize-decay", label: "Size decay", hint: "Higher makes subheadings smaller", unit: "", min: 0.5, max: 6, step: 0.1, default: 3 },
|
|
34
|
+
{ key: "h1-letter-spacing", variable: "--letterspacing-h1", label: "h1 letter spacing", unit: "ch", min: -0.1, max: 0.1, step: 0.005, default: -0.01 },
|
|
35
|
+
{ key: "h6-letter-spacing", variable: "--letterspacing-h6", label: "h6 letter spacing", unit: "ch", min: -0.1, max: 0.1, step: 0.005, default: 0 },
|
|
36
|
+
{ key: "letter-spacing-decay", variable: "--letterspacing-decay", label: "Spacing decay", unit: "", min: 0.5, max: 6, step: 0.1, default: 2 },
|
|
37
|
+
{ key: "h1-weight", variable: "--fontweight-h1", label: "h1 weight", unit: "", min: 100, max: 900, step: 10, default: 800 },
|
|
38
|
+
{ key: "h6-weight", variable: "--fontweight-h6", label: "h6 weight", unit: "", min: 100, max: 900, step: 10, default: 600 },
|
|
39
|
+
{ key: "weight-decay", variable: "--fontweight-decay", label: "Weight decay", unit: "", min: 0.5, max: 6, step: 0.1, default: 2 }
|
|
40
40
|
]
|
|
41
41
|
|
|
42
42
|
/**
|
|
@@ -57,7 +57,7 @@ function themeSettings(theme) {
|
|
|
57
57
|
function rampValue(setting, value) {
|
|
58
58
|
const clamped = Math.min(setting.max, Math.max(setting.min, value))
|
|
59
59
|
const number = String(Number(clamped.toFixed(4)))
|
|
60
|
-
if (setting.unit === "x") return `calc(${number} * var(--
|
|
60
|
+
if (setting.unit === "x") return `calc(${number} * var(--fontsize-root))`
|
|
61
61
|
return `${number}${setting.unit}`
|
|
62
62
|
}
|
|
63
63
|
|
package/plugins/styles/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { styleText } from "node:util"
|
|
|
4
4
|
import { transform } from "lightningcss"
|
|
5
5
|
import { hash } from "node:crypto"
|
|
6
6
|
import { resolveTheme, themeStylesheets, themeKey } from "./theme.js"
|
|
7
|
-
import { themeColorSchemeCSS } from "../markdown/colorScheme.js"
|
|
7
|
+
import { themeColorSchemeCSS, colorParams } from "../markdown/colorScheme.js"
|
|
8
8
|
import { typographyCSS } from "../markdown/typography.js"
|
|
9
9
|
|
|
10
10
|
/** @import * as Votive from "votive" */
|
|
@@ -54,7 +54,7 @@ function createStubs({ api, settings }) {
|
|
|
54
54
|
return themeStylesheets(theme, rootTheme).map(sheet => {
|
|
55
55
|
// Generated sheets carry the part of the theme they are generated
|
|
56
56
|
// from, so they re-expand exactly when that part changes.
|
|
57
|
-
if (sheet.startsWith("colors")) return { path: sheet, params:
|
|
57
|
+
if (sheet.startsWith("colors")) return { path: sheet, params: colorParams(config) }
|
|
58
58
|
if (sheet.startsWith("type")) return { path: sheet, params: { theme: config } }
|
|
59
59
|
return { path: sheet }
|
|
60
60
|
})
|
|
@@ -80,8 +80,8 @@ function expandStubs({ path: sourcePath, params }) {
|
|
|
80
80
|
// Reports and falls back rather than throwing: a site with unusable
|
|
81
81
|
// colours should lose its palette, not its build.
|
|
82
82
|
return {
|
|
83
|
-
text: themeColorSchemeCSS(params
|
|
84
|
-
console.warn(`${styleText("dim", "build: ")}${styleText("yellow",
|
|
83
|
+
text: themeColorSchemeCSS(params ?? {}, error => (
|
|
84
|
+
console.warn(`${styleText("dim", "build: ")}${styleText("yellow", error.message)}`)
|
|
85
85
|
))
|
|
86
86
|
}
|
|
87
87
|
}
|
package/plugins/xml/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import xml from "xml"
|
|
2
|
-
import {
|
|
2
|
+
import { showPages, isPageWithoutFile } from "./../../utils.js"
|
|
3
3
|
import { entryContent } from "./entryContent.js"
|
|
4
4
|
|
|
5
5
|
/** @import * as Votive from "votive" */
|
|
@@ -134,7 +134,10 @@ const processor = {
|
|
|
134
134
|
// the entries kept read it. `feed_limit` in settings.md raises or
|
|
135
135
|
// lowers it.
|
|
136
136
|
const limit = Number(settings.lastNonNull("fm_feed_limit")) || DEFAULT_FEED_LIMIT
|
|
137
|
-
|
|
137
|
+
// A page with no file (`html_file: false`) is an entry too, with
|
|
138
|
+
// its body and no <link>: Atom needs an <id> and content, not a
|
|
139
|
+
// link, which is what makes a feed-only post possible.
|
|
140
|
+
const pages = showPages(api, {
|
|
138
141
|
query: {
|
|
139
142
|
"!": {
|
|
140
143
|
rss_item: false
|
|
@@ -194,21 +197,25 @@ const processor = {
|
|
|
194
197
|
|
|
195
198
|
feed.push(
|
|
196
199
|
...pages.map((page) => {
|
|
200
|
+
// The url the page has, or would have: an Atom <id> is an
|
|
201
|
+
// identifier, so a page without a file keeps the one it would
|
|
202
|
+
// have, and keeps it if it gets a file later.
|
|
197
203
|
const url = (new URL(page.path, domain)).href
|
|
204
|
+
const link = isPageWithoutFile(page) ? [] : [{
|
|
205
|
+
link: {
|
|
206
|
+
_attr: {
|
|
207
|
+
rel: 'alternate',
|
|
208
|
+
href: url
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}]
|
|
198
212
|
|
|
199
213
|
/** @type {object[]} */
|
|
200
214
|
const entry = [
|
|
201
215
|
{
|
|
202
216
|
title: page.metadata.title
|
|
203
217
|
},
|
|
204
|
-
|
|
205
|
-
link: {
|
|
206
|
-
_attr: {
|
|
207
|
-
rel: 'alternate',
|
|
208
|
-
href: url
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
},
|
|
218
|
+
...link,
|
|
212
219
|
{ id: url },
|
|
213
220
|
{ updated: page.metadata.date }
|
|
214
221
|
];
|