poops 1.1.0 → 1.2.1
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/README.md +515 -55
- package/lib/copy.js +7 -9
- package/lib/markup/collections.js +247 -0
- package/lib/markup/engines/liquid.js +240 -0
- package/lib/markup/engines/nunjucks.js +261 -0
- package/lib/markup/helpers.js +170 -0
- package/lib/markup/highlight.js +77 -0
- package/lib/markup/indexer.js +154 -0
- package/lib/markup/stop-words-en.json +25 -0
- package/lib/markups.js +223 -459
- package/lib/postcss.js +127 -0
- package/lib/{ssg.js → reactor.js} +36 -25
- package/lib/scripts.js +12 -13
- package/lib/styles.js +20 -12
- package/lib/utils/helpers.js +0 -56
- package/lib/utils/log.js +64 -0
- package/package.json +21 -3
- package/poops.js +103 -112
- package/lib/utils/print-style.js +0 -72
package/lib/copy.js
CHANGED
|
@@ -8,9 +8,7 @@ import {
|
|
|
8
8
|
} from './utils/helpers.js'
|
|
9
9
|
import fs from 'node:fs'
|
|
10
10
|
import path from 'node:path'
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
const pstyle = new PrintStyle()
|
|
11
|
+
import log from './utils/log.js'
|
|
14
12
|
|
|
15
13
|
export default class Copy {
|
|
16
14
|
constructor(config) {
|
|
@@ -26,7 +24,7 @@ export default class Copy {
|
|
|
26
24
|
|
|
27
25
|
for (const copyEntry of this.config.copy) {
|
|
28
26
|
if (!copyEntry.in || !copyEntry.out) {
|
|
29
|
-
|
|
27
|
+
log({ tag: 'copy', error: true, text: `Cannot copy. Missing 'in' or 'out' property in copy entry: ${JSON.stringify(copyEntry)}` })
|
|
30
28
|
continue
|
|
31
29
|
}
|
|
32
30
|
|
|
@@ -53,7 +51,7 @@ export default class Copy {
|
|
|
53
51
|
copyLastPath = inEntry
|
|
54
52
|
copyPathCount++
|
|
55
53
|
} else {
|
|
56
|
-
|
|
54
|
+
log({ tag: 'copy', error: true, text: 'Cannot copy. Source path does not exist:', link: matchedPath })
|
|
57
55
|
}
|
|
58
56
|
}
|
|
59
57
|
continue
|
|
@@ -65,9 +63,9 @@ export default class Copy {
|
|
|
65
63
|
copyPathCount++
|
|
66
64
|
} else {
|
|
67
65
|
if (hasGlobMagic) {
|
|
68
|
-
|
|
66
|
+
log({ tag: 'copy', error: true, text: 'No files matched glob pattern:', link: inEntry })
|
|
69
67
|
} else {
|
|
70
|
-
|
|
68
|
+
log({ tag: 'copy', error: true, text: 'Cannot copy. Source path does not exist:', link: inEntry })
|
|
71
69
|
}
|
|
72
70
|
}
|
|
73
71
|
}
|
|
@@ -76,11 +74,11 @@ export default class Copy {
|
|
|
76
74
|
const copyEndTime = performance.now()
|
|
77
75
|
if (copyPathCount === 0) return
|
|
78
76
|
if (copyPathCount === 1) {
|
|
79
|
-
|
|
77
|
+
log({ tag: 'copy', text: 'Copied', link: copyLastPath, time: buildTime(copyStartTime, copyEndTime) })
|
|
80
78
|
return
|
|
81
79
|
}
|
|
82
80
|
|
|
83
|
-
|
|
81
|
+
log({ tag: 'copy', text: `Copied ${copyPathCount} paths`, time: buildTime(copyStartTime, copyEndTime) })
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
async unlink(file, copyPaths) {
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import { globSync } from 'glob'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import log from '../utils/log.js'
|
|
5
|
+
import { mkDir } from '../utils/helpers.js'
|
|
6
|
+
import { replaceOutExtensions, getRelativePathPrefix, getPageUrl, parseFrontMatter } from './helpers.js'
|
|
7
|
+
|
|
8
|
+
export function getSingleCollectionData(markupInDir, collectionName) {
|
|
9
|
+
const collectionData = []
|
|
10
|
+
globSync(path.join(process.cwd(), markupInDir, collectionName, '**/*.+(html|njk|liquid|md)'), { ignore: ['**/index.+(html|njk|liquid|md)'] }).forEach((file) => {
|
|
11
|
+
let frontMatter = {}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const frontMatterResult = parseFrontMatter(file)
|
|
15
|
+
frontMatter = frontMatterResult.frontMatter
|
|
16
|
+
} catch (err) {
|
|
17
|
+
log({ tag: 'error', text: 'Failed parsing front matter:', link: file })
|
|
18
|
+
console.error(err)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (frontMatter.published === false) return
|
|
22
|
+
|
|
23
|
+
if (!frontMatter.date) {
|
|
24
|
+
frontMatter.date = fs.statSync(file).ctime.toISOString().slice(0, 16)
|
|
25
|
+
}
|
|
26
|
+
frontMatter.fileName = path.basename(file)
|
|
27
|
+
frontMatter.filePath = path.relative(process.cwd(), file)
|
|
28
|
+
frontMatter.collection = collectionName
|
|
29
|
+
frontMatter.url = path.join(collectionName, path.basename(frontMatter.filePath))
|
|
30
|
+
|
|
31
|
+
frontMatter.url = replaceOutExtensions(frontMatter.url)
|
|
32
|
+
|
|
33
|
+
if (!frontMatter.title) {
|
|
34
|
+
frontMatter.title = path.basename(frontMatter.filePath, path.extname(frontMatter.filePath))
|
|
35
|
+
}
|
|
36
|
+
collectionData.push(frontMatter)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return collectionData
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function collectionAutoDiscovery(markupInDir) {
|
|
43
|
+
const indexFiles = globSync(path.join(process.cwd(), markupInDir, '/**/index.+(html|njk|liquid|md)'))
|
|
44
|
+
|
|
45
|
+
const collectionData = {}
|
|
46
|
+
|
|
47
|
+
for (const indexFile of indexFiles) {
|
|
48
|
+
let frontMatter = {}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const frontMatterResult = parseFrontMatter(indexFile)
|
|
52
|
+
frontMatter = frontMatterResult.frontMatter
|
|
53
|
+
} catch (err) {
|
|
54
|
+
log({ tag: 'error', text: 'Failed parsing front matter:', link: indexFile })
|
|
55
|
+
console.error(err)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!frontMatter.collection) continue
|
|
59
|
+
|
|
60
|
+
if (frontMatter.collection === true) {
|
|
61
|
+
frontMatter.collection = path.basename(path.dirname(indexFile))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const collectionName = frontMatter.collection.trim()
|
|
65
|
+
|
|
66
|
+
if (collectionName === '') continue
|
|
67
|
+
|
|
68
|
+
frontMatter.name = collectionName
|
|
69
|
+
const collection = buildCollectionObject(markupInDir, frontMatter)
|
|
70
|
+
if (!collection) continue
|
|
71
|
+
collectionData[collection.name] = collection
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return collectionData
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getCollectionDataBasedOnConfig(markupInDir, collectionConfig) {
|
|
78
|
+
if (!collectionConfig) return {}
|
|
79
|
+
|
|
80
|
+
const items = Array.isArray(collectionConfig)
|
|
81
|
+
? collectionConfig
|
|
82
|
+
: [collectionConfig]
|
|
83
|
+
|
|
84
|
+
const collectionData = {}
|
|
85
|
+
|
|
86
|
+
for (let item of items) {
|
|
87
|
+
if (typeof item === 'string') item = { name: item }
|
|
88
|
+
if (!item || !item.name) continue
|
|
89
|
+
const collection = buildCollectionObject(markupInDir, item)
|
|
90
|
+
if (collection) collectionData[item.name] = collection
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return collectionData
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildCollectionObject(markupInDir, collectionProtoObject) {
|
|
97
|
+
const collection = {
|
|
98
|
+
name: collectionProtoObject.name,
|
|
99
|
+
items: getSingleCollectionData(markupInDir, collectionProtoObject.name)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (collection.items.length === 0) return null
|
|
103
|
+
|
|
104
|
+
if (collectionProtoObject.paginate && !isNaN(parseInt(collectionProtoObject.paginate))) {
|
|
105
|
+
collection.paginate = parseInt(collectionProtoObject.paginate)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (collectionProtoObject.sort) {
|
|
109
|
+
collection.sort = collectionProtoObject.sort
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof collection.sort === 'string') {
|
|
113
|
+
collection.sort = { by: collection.sort }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!collection.sort) {
|
|
117
|
+
collection.sort = { by: 'date' }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!collection.sort.by) {
|
|
121
|
+
collection.sort.by = 'date'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (collection.sort.by === 'date') {
|
|
125
|
+
collection.sort.type = 'date'
|
|
126
|
+
} else {
|
|
127
|
+
collection.sort.type = 'alphabetical'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!collection.sort.order) {
|
|
131
|
+
collection.sort.order = collection.sort.type === 'date' ? 'desc' : 'asc'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
collection.items.sort((a, b) => {
|
|
135
|
+
if (collection.sort.type === 'date') {
|
|
136
|
+
if (collection.sort.order === 'asc') {
|
|
137
|
+
return new Date(a[collection.sort.by]) - new Date(b[collection.sort.by])
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return new Date(b[collection.sort.by]) - new Date(a[collection.sort.by])
|
|
141
|
+
} else {
|
|
142
|
+
const aVal = a[collection.sort.by]
|
|
143
|
+
const bVal = b[collection.sort.by]
|
|
144
|
+
if (aVal === bVal) return 0
|
|
145
|
+
if (collection.sort.order === 'asc') {
|
|
146
|
+
return aVal > bVal ? 1 : -1
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return aVal < bVal ? 1 : -1
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
return collection
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function buildCollectionPaginationData(collectionData) {
|
|
157
|
+
if (!collectionData) return
|
|
158
|
+
|
|
159
|
+
for (const collectionName of Object.keys(collectionData)) {
|
|
160
|
+
const collection = collectionData[collectionName]
|
|
161
|
+
|
|
162
|
+
if (!collection.paginate) continue
|
|
163
|
+
|
|
164
|
+
collection.pages = []
|
|
165
|
+
let pageItems = []
|
|
166
|
+
for (const item of collection.items) {
|
|
167
|
+
if (pageItems.length === collection.paginate) {
|
|
168
|
+
collection.pages.push(pageItems)
|
|
169
|
+
pageItems = []
|
|
170
|
+
}
|
|
171
|
+
pageItems.push(item)
|
|
172
|
+
}
|
|
173
|
+
collection.pages.push(pageItems)
|
|
174
|
+
|
|
175
|
+
collection.totalPages = collection.pages.length
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function getCollectionIndexFile(markupInDir, collectionName) {
|
|
180
|
+
const indexFiles = globSync(path.join(process.cwd(), markupInDir, collectionName, 'index.+(html|njk|liquid|md)'))
|
|
181
|
+
if (indexFiles.length === 0) return null
|
|
182
|
+
return indexFiles[0]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function generateCollectionPaginationPages(collectionData, markupInDir, markupOutDir, compileEntryFn) {
|
|
186
|
+
if (!collectionData) return []
|
|
187
|
+
|
|
188
|
+
const compilePromises = []
|
|
189
|
+
|
|
190
|
+
for (const collectionName of Object.keys(collectionData)) {
|
|
191
|
+
const collection = collectionData[collectionName]
|
|
192
|
+
const file = getCollectionIndexFile(markupInDir, collectionName)
|
|
193
|
+
|
|
194
|
+
if (!collection.totalPages || collection.totalPages === 0) {
|
|
195
|
+
collection.totalPages = 1
|
|
196
|
+
collection.pages = [collection.items]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < collection.totalPages; i++) {
|
|
200
|
+
const pageNumber = i + 1
|
|
201
|
+
const pageUrl = pageNumber === 1 ? collection.name : `${collection.name}/${pageNumber}`
|
|
202
|
+
const nextPage = pageNumber === collection.totalPages ? null : pageNumber + 1
|
|
203
|
+
const nextPageUrl = pageNumber === collection.totalPages ? null : `${collection.name}/${pageNumber + 1}`
|
|
204
|
+
let prevPage = pageNumber === 1 ? null : pageNumber - 1
|
|
205
|
+
let prevPageUrl = pageNumber === 1 ? null : `${collection.name}/${pageNumber - 1}`
|
|
206
|
+
if (prevPage === 1) {
|
|
207
|
+
prevPageUrl = collection.name
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Snapshot per-page properties to avoid async mutation
|
|
211
|
+
const pageSnapshot = {
|
|
212
|
+
...collection,
|
|
213
|
+
pageItems: collection.pages[i],
|
|
214
|
+
pageNumber,
|
|
215
|
+
pageUrl,
|
|
216
|
+
nextPage,
|
|
217
|
+
nextPageUrl,
|
|
218
|
+
prevPage,
|
|
219
|
+
prevPageUrl
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const markupOut = path.join(process.cwd(), markupOutDir, pageUrl, 'index.html')
|
|
223
|
+
const fromPath = path.join(process.cwd(), markupOutDir)
|
|
224
|
+
const markupOutDirFull = path.dirname(markupOut)
|
|
225
|
+
|
|
226
|
+
mkDir(markupOutDirFull)
|
|
227
|
+
|
|
228
|
+
const context = {
|
|
229
|
+
...collectionData,
|
|
230
|
+
[collectionName]: pageSnapshot,
|
|
231
|
+
relativePathPrefix: getRelativePathPrefix(markupOutDirFull, fromPath),
|
|
232
|
+
_url: getPageUrl(markupOut)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!file) {
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const compilePromise = compileEntryFn(file, context).then(({ result }) => {
|
|
240
|
+
fs.writeFileSync(markupOut, result)
|
|
241
|
+
})
|
|
242
|
+
compilePromises.push(compilePromise)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return compilePromises
|
|
247
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { Liquid } from 'liquidjs'
|
|
4
|
+
import { Marked } from 'marked'
|
|
5
|
+
import { highlightRenderer, highlightCode } from '../highlight.js'
|
|
6
|
+
import { discoverImageVariants, parseFrontMatter } from '../helpers.js'
|
|
7
|
+
import { slugify } from 'book-of-spells'
|
|
8
|
+
import dayjs from 'dayjs'
|
|
9
|
+
|
|
10
|
+
const marked = new Marked({ renderer: highlightRenderer })
|
|
11
|
+
|
|
12
|
+
export default class LiquidEngine {
|
|
13
|
+
constructor(templatesDir, includePaths) {
|
|
14
|
+
const roots = [templatesDir]
|
|
15
|
+
for (const inc of includePaths || []) {
|
|
16
|
+
roots.push(path.join(templatesDir, inc))
|
|
17
|
+
}
|
|
18
|
+
// Also add any _* directories as include roots
|
|
19
|
+
try {
|
|
20
|
+
const entries = fs.readdirSync(templatesDir, { withFileTypes: true })
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
if (entry.isDirectory() && entry.name.startsWith('_')) {
|
|
23
|
+
roots.push(path.join(templatesDir, entry.name))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch { /* ignore */ }
|
|
27
|
+
|
|
28
|
+
this.engine = new Liquid({
|
|
29
|
+
root: roots,
|
|
30
|
+
extname: '.liquid',
|
|
31
|
+
cache: false,
|
|
32
|
+
dynamicPartials: true,
|
|
33
|
+
strictFilters: false,
|
|
34
|
+
strictVariables: false,
|
|
35
|
+
jsTruthy: true
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
this.globals = {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get fileExtension() { return '.liquid' }
|
|
42
|
+
get indexableExtensions() { return new Set(['.html', '.md', '.liquid']) }
|
|
43
|
+
get markupExtensions() { return 'html|xml|rss|atom|json|liquid|md' }
|
|
44
|
+
|
|
45
|
+
registerFilters({ timeDateFormat, markupOut }) {
|
|
46
|
+
const engine = this.engine
|
|
47
|
+
engine.registerFilter('slugify', (str) => slugify(str))
|
|
48
|
+
engine.registerFilter('jsonify', (obj) => JSON.stringify(obj))
|
|
49
|
+
engine.registerFilter('markdown', (str) => marked.parse(str))
|
|
50
|
+
engine.registerFilter('date', (str, template) => {
|
|
51
|
+
const fmt = template || timeDateFormat
|
|
52
|
+
if (!fmt) return str
|
|
53
|
+
const date = !str || (typeof str === 'string' && str.trim() === '') ? new Date() : new Date(str)
|
|
54
|
+
return dayjs(date).format(fmt)
|
|
55
|
+
})
|
|
56
|
+
engine.registerFilter('concat', (arr, value) => {
|
|
57
|
+
if (!Array.isArray(arr)) return [value]
|
|
58
|
+
return arr.concat(value)
|
|
59
|
+
})
|
|
60
|
+
engine.registerFilter('push', (arr, value) => {
|
|
61
|
+
if (!Array.isArray(arr)) return [value]
|
|
62
|
+
arr.push(value)
|
|
63
|
+
return arr
|
|
64
|
+
})
|
|
65
|
+
engine.registerFilter('svg', (filePath) => {
|
|
66
|
+
const fullPath = path.resolve(process.cwd(), filePath)
|
|
67
|
+
if (!fs.existsSync(fullPath)) return ''
|
|
68
|
+
const content = fs.readFileSync(fullPath, 'utf-8').trim()
|
|
69
|
+
if (!/^(<\?xml[^?]*\?>\s*)?<svg[\s>]/i.test(content)) return ''
|
|
70
|
+
return content
|
|
71
|
+
})
|
|
72
|
+
engine.registerFilter('srcset', (imagePath) => {
|
|
73
|
+
const outputDir = path.join(process.cwd(), markupOut)
|
|
74
|
+
const { variants } = discoverImageVariants(imagePath, outputDir)
|
|
75
|
+
if (variants.length === 0) return ''
|
|
76
|
+
return variants.map(v => `${v.path} ${v.width}w`).join(', ')
|
|
77
|
+
})
|
|
78
|
+
engine.registerFilter('highlight', (code, lang) => {
|
|
79
|
+
const highlighted = highlightCode(code, lang)
|
|
80
|
+
const langClass = lang ? ` language-${lang}` : ''
|
|
81
|
+
return `<pre><code class="hljs${langClass}">${highlighted}</code></pre>`
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
registerTags(getOutputDir) {
|
|
86
|
+
registerGoogleFontsTag(this.engine)
|
|
87
|
+
registerImageTag(this.engine, getOutputDir)
|
|
88
|
+
registerHighlightTag(this.engine)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setGlobal(key, value) {
|
|
92
|
+
this.globals[key] = value
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async render(templateName, context) {
|
|
96
|
+
let source
|
|
97
|
+
const frontMatterResult = parseFrontMatter(templateName)
|
|
98
|
+
source = frontMatterResult.content
|
|
99
|
+
|
|
100
|
+
if (path.extname(templateName) === '.md') {
|
|
101
|
+
source = marked.parse(source)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const frontMatter = context.page || {}
|
|
105
|
+
if (frontMatter.layout) {
|
|
106
|
+
source = `{% layout '${frontMatter.layout}${this.fileExtension}' %}{% block content %}${source}{% endblock %}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return this.engine.parseAndRender(source, { ...this.globals, ...context }, {
|
|
110
|
+
globals: this.globals
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async renderString(source, context) {
|
|
115
|
+
return this.engine.parseAndRender(source, { ...this.globals, ...context }, {
|
|
116
|
+
globals: this.globals
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Liquid Tags ---
|
|
122
|
+
|
|
123
|
+
function registerGoogleFontsTag(engine) {
|
|
124
|
+
engine.registerTag('googleFonts', {
|
|
125
|
+
parse(tagToken) {
|
|
126
|
+
this.value = tagToken.args.trim()
|
|
127
|
+
},
|
|
128
|
+
* render(ctx) {
|
|
129
|
+
const fonts = yield this.liquid.evalValue(this.value, ctx)
|
|
130
|
+
if (!fonts || (Array.isArray(fonts) && fonts.length === 0)) return ''
|
|
131
|
+
|
|
132
|
+
const fontList = typeof fonts === 'string' ? [fonts] : fonts
|
|
133
|
+
const display = 'swap'
|
|
134
|
+
|
|
135
|
+
const families = fontList.map(font => {
|
|
136
|
+
if (typeof font === 'string') return `family=${font.replace(/\s+/g, '+')}`
|
|
137
|
+
const name = font.name || font.family || ''
|
|
138
|
+
const weights = font.weights || font.wght
|
|
139
|
+
let param = `family=${name.replace(/\s+/g, '+')}`
|
|
140
|
+
if (weights) {
|
|
141
|
+
const wList = Array.isArray(weights) ? weights : [weights]
|
|
142
|
+
param += `:wght@${wList.join(';')}`
|
|
143
|
+
}
|
|
144
|
+
if (font.ital) {
|
|
145
|
+
param = param.replace(':wght@', ':ital,wght@')
|
|
146
|
+
const wList = param.match(/@(.+)$/)[1].split(';')
|
|
147
|
+
const expanded = []
|
|
148
|
+
for (const w of wList) { expanded.push(`0,${w}`); expanded.push(`1,${w}`) }
|
|
149
|
+
param = param.replace(/@.+$/, `@${expanded.join(';')}`)
|
|
150
|
+
}
|
|
151
|
+
return param
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const url = `https://fonts.googleapis.com/css2?${families.join('&')}&display=${display}`
|
|
155
|
+
return `<link rel="preconnect" href="https://fonts.googleapis.com">\n
|
|
156
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>\n
|
|
157
|
+
<link href="${url}" rel="stylesheet">`
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function registerImageTag(engine, getOutputDir) {
|
|
163
|
+
engine.registerTag('image', {
|
|
164
|
+
parse(tagToken) {
|
|
165
|
+
this.args = tagToken.args
|
|
166
|
+
},
|
|
167
|
+
* render(ctx) {
|
|
168
|
+
const argsStr = this.args.trim()
|
|
169
|
+
const parts = argsStr.split(',').map(s => s.trim())
|
|
170
|
+
|
|
171
|
+
let imagePath = parts[0]
|
|
172
|
+
if (imagePath.startsWith('"') || imagePath.startsWith("'")) {
|
|
173
|
+
imagePath = imagePath.slice(1, -1)
|
|
174
|
+
} else {
|
|
175
|
+
imagePath = yield ctx.get(imagePath.split('.'))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const kwargs = {}
|
|
179
|
+
for (let i = 1; i < parts.length; i++) {
|
|
180
|
+
const colonIdx = parts[i].indexOf(':')
|
|
181
|
+
if (colonIdx === -1) continue
|
|
182
|
+
const key = parts[i].slice(0, colonIdx).trim()
|
|
183
|
+
let val = parts[i].slice(colonIdx + 1).trim()
|
|
184
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
185
|
+
val = val.slice(1, -1)
|
|
186
|
+
}
|
|
187
|
+
kwargs[key] = val
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const prefix = (yield ctx.get(['relativePathPrefix'])) || ''
|
|
191
|
+
const alt = kwargs.alt || ''
|
|
192
|
+
const loading = kwargs.loading || 'lazy'
|
|
193
|
+
const isSvg = imagePath.endsWith('.svg')
|
|
194
|
+
const attrs = [`alt="${alt}"`]
|
|
195
|
+
|
|
196
|
+
if (isSvg) {
|
|
197
|
+
attrs.unshift(`src="${prefix}${imagePath}"`)
|
|
198
|
+
} else {
|
|
199
|
+
const outputDir = getOutputDir()
|
|
200
|
+
const { src, variants } = discoverImageVariants(imagePath, outputDir)
|
|
201
|
+
const sizes = kwargs.sizes || '100vw'
|
|
202
|
+
attrs.unshift(`src="${prefix}${src}"`)
|
|
203
|
+
if (variants.length > 0) {
|
|
204
|
+
const srcsetVal = variants.map(v => `${prefix}${v.path} ${v.width}w`).join(', ')
|
|
205
|
+
attrs.push(`srcset="${srcsetVal}"`)
|
|
206
|
+
attrs.push(`sizes="${sizes}"`)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
attrs.push(`loading="${loading}"`)
|
|
211
|
+
const skip = new Set(['alt', 'sizes', 'loading'])
|
|
212
|
+
for (const [key, val] of Object.entries(kwargs)) {
|
|
213
|
+
if (skip.has(key)) continue
|
|
214
|
+
attrs.push(`${key}="${val}"`)
|
|
215
|
+
}
|
|
216
|
+
return `<img ${attrs.join(' ')}>`
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function registerHighlightTag(engine) {
|
|
222
|
+
engine.registerTag('highlight', {
|
|
223
|
+
parse(tagToken, remainTokens) {
|
|
224
|
+
this.lang = tagToken.args.trim()
|
|
225
|
+
this.templates = []
|
|
226
|
+
const stream = this.liquid.parser.parseStream(remainTokens)
|
|
227
|
+
.on('tag:endhighlight', () => stream.stop())
|
|
228
|
+
.on('template', (tpl) => this.templates.push(tpl))
|
|
229
|
+
.on('end', () => { throw new Error('tag {% highlight %} not closed with {% endhighlight %}') })
|
|
230
|
+
stream.start()
|
|
231
|
+
},
|
|
232
|
+
* render(ctx) {
|
|
233
|
+
const code = yield this.liquid.renderer.renderTemplates(this.templates, ctx)
|
|
234
|
+
const lang = this.lang
|
|
235
|
+
const highlighted = highlightCode(code, lang)
|
|
236
|
+
const langClass = lang ? ` language-${lang}` : ''
|
|
237
|
+
return `<pre><code class="hljs${langClass}">${highlighted}</code></pre>`
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
}
|