poops 1.2.3 → 1.3.0
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 +395 -70
- package/lib/copy.js +5 -3
- package/lib/images.js +57 -0
- package/lib/markup/collections.js +39 -17
- package/lib/markup/engines/liquid.js +34 -33
- package/lib/markup/engines/nunjucks.js +44 -36
- package/lib/markup/helpers.js +164 -37
- package/lib/markup/image-cache.js +114 -0
- package/lib/markup/indexer.js +158 -2
- package/lib/markup/renderer.js +60 -0
- package/lib/markups.js +199 -49
- package/lib/postcss.js +16 -30
- package/lib/reactor.js +11 -29
- package/lib/scripts.js +37 -34
- package/lib/styles.js +17 -31
- package/lib/utils/helpers.js +20 -16
- package/lib/utils/log.js +15 -1
- package/lib/utils/minify.js +41 -0
- package/package.json +7 -3
- package/poops.js +120 -32
package/lib/images.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import log from './utils/log.js'
|
|
2
|
+
|
|
3
|
+
// Runs poops-images (https://github.com/stamat/poops-images) as a regular
|
|
4
|
+
// runner when a `config.images` block is present AND the package is installed.
|
|
5
|
+
// poops-images stays an optional peer dependency — sharp never becomes a hard
|
|
6
|
+
// dep of poops. The markup engines read the cache poops-images writes, so this
|
|
7
|
+
// runner must execute before markups.compile().
|
|
8
|
+
export default class Images {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config
|
|
11
|
+
this.processor = null // lazily created on first compile()
|
|
12
|
+
this.disabled = false // no images config, or poops-images not installed
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async init() {
|
|
16
|
+
if (this.processor || this.disabled) return
|
|
17
|
+
if (!this.config.images) { this.disabled = true; return }
|
|
18
|
+
|
|
19
|
+
let ImageProcessor
|
|
20
|
+
try {
|
|
21
|
+
ImageProcessor = (await import('poops-images')).default
|
|
22
|
+
} catch (err) {
|
|
23
|
+
// Native ESM throws ERR_MODULE_NOT_FOUND; CJS-style/jest resolvers throw
|
|
24
|
+
// MODULE_NOT_FOUND. Either means the optional dep simply isn't installed.
|
|
25
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'MODULE_NOT_FOUND') {
|
|
26
|
+
log({ tag: 'image', warn: true, text: 'images config found but poops-images is not installed — run: npm i poops-images' })
|
|
27
|
+
this.disabled = true
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
throw err
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Quiet by default inside poops — one summary line instead of per-image
|
|
34
|
+
// logs drowning the other runners. Opt back in with "verbose": true.
|
|
35
|
+
// A bad images config throws here, propagating to the build's step() so
|
|
36
|
+
// the build is marked failed rather than silently skipping images.
|
|
37
|
+
this.processor = new ImageProcessor({ verbose: false, ...this.config.images })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async compile() {
|
|
41
|
+
await this.init()
|
|
42
|
+
if (!this.processor) return
|
|
43
|
+
const stats = await this.processor.processAll()
|
|
44
|
+
// Route through poops' log so hasLoggedErrors() flips the build exit code.
|
|
45
|
+
// Older poops-images without the errors field: undefined is falsy, no-op.
|
|
46
|
+
if (stats.errors > 0) {
|
|
47
|
+
log({ tag: 'image', error: true, text: `${stats.errors} image(s) failed to process` })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Watch mode: a deleted source image removes its generated variants + cache entry.
|
|
52
|
+
async remove(file) {
|
|
53
|
+
await this.init()
|
|
54
|
+
if (!this.processor) return
|
|
55
|
+
this.processor.removeSource(file)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -2,12 +2,13 @@ import fs from 'node:fs'
|
|
|
2
2
|
import { globSync } from 'glob'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import log from '../utils/log.js'
|
|
5
|
-
import { mkDir } from '../utils/helpers.js'
|
|
6
|
-
import { replaceOutExtensions, getRelativePathPrefix,
|
|
5
|
+
import { mkDir, toPosix } from '../utils/helpers.js'
|
|
6
|
+
import { replaceOutExtensions, getRelativePathPrefix, getPageUrlRelativeToOutput, parseFrontMatter, wordcount } from './helpers.js'
|
|
7
7
|
|
|
8
8
|
export function getSingleCollectionData(markupInDir, collectionName) {
|
|
9
9
|
const collectionData = []
|
|
10
|
-
|
|
10
|
+
// glob patterns must use `/` — on Windows `\` is an escape character
|
|
11
|
+
globSync(toPosix(path.resolve(process.cwd(), markupInDir, collectionName, '**/*.+(html|njk|liquid|md)')), { ignore: ['**/index.+(html|njk|liquid|md)'] }).forEach((file) => {
|
|
11
12
|
let frontMatter = {}
|
|
12
13
|
|
|
13
14
|
let content = ''
|
|
@@ -23,13 +24,16 @@ export function getSingleCollectionData(markupInDir, collectionName) {
|
|
|
23
24
|
if (frontMatter.published === false) return
|
|
24
25
|
|
|
25
26
|
if (!frontMatter.date) {
|
|
26
|
-
|
|
27
|
+
// mtime is only a local-dev approximation — git clone resets it, so CI
|
|
28
|
+
// builds date undated posts "now". The warning is the real fix.
|
|
29
|
+
frontMatter.date = fs.statSync(file).mtime.toISOString().slice(0, 16)
|
|
30
|
+
log({ tag: 'markup', warn: true, text: 'No date in front matter, falling back to file mtime:', link: file })
|
|
27
31
|
}
|
|
28
32
|
frontMatter.wordcount = wordcount(content)
|
|
29
33
|
frontMatter.fileName = path.basename(file)
|
|
30
34
|
frontMatter.filePath = path.relative(process.cwd(), file)
|
|
31
35
|
frontMatter.collection = collectionName
|
|
32
|
-
frontMatter.url = path.join(collectionName, path.basename(frontMatter.filePath))
|
|
36
|
+
frontMatter.url = toPosix(path.join(collectionName, path.basename(frontMatter.filePath)))
|
|
33
37
|
|
|
34
38
|
frontMatter.url = replaceOutExtensions(frontMatter.url)
|
|
35
39
|
|
|
@@ -43,7 +47,7 @@ export function getSingleCollectionData(markupInDir, collectionName) {
|
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
export function collectionAutoDiscovery(markupInDir) {
|
|
46
|
-
const indexFiles = globSync(path.
|
|
50
|
+
const indexFiles = globSync(toPosix(path.resolve(process.cwd(), markupInDir, '**/index.+(html|njk|liquid|md)')))
|
|
47
51
|
|
|
48
52
|
const collectionData = {}
|
|
49
53
|
|
|
@@ -180,11 +184,25 @@ export function buildCollectionPaginationData(collectionData) {
|
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
export function getCollectionIndexFile(markupInDir, collectionName) {
|
|
183
|
-
const indexFiles = globSync(path.
|
|
187
|
+
const indexFiles = globSync(toPosix(path.resolve(process.cwd(), markupInDir, collectionName, 'index.+(html|njk|liquid|md)')))
|
|
184
188
|
if (indexFiles.length === 0) return null
|
|
185
189
|
return indexFiles[0]
|
|
186
190
|
}
|
|
187
191
|
|
|
192
|
+
export function pruneStalePaginationDirs(collectionName, markupInDir, markupOutDir, keepPages) {
|
|
193
|
+
const outDir = path.resolve(process.cwd(), markupOutDir, collectionName)
|
|
194
|
+
if (!fs.existsSync(outDir)) return
|
|
195
|
+
|
|
196
|
+
for (const entry of fs.readdirSync(outDir)) {
|
|
197
|
+
const pageNumber = parseInt(entry, 10)
|
|
198
|
+
// pagination only ever writes out/<name>/2..totalPages/
|
|
199
|
+
if (String(pageNumber) !== entry || pageNumber < 2 || pageNumber <= keepPages) continue
|
|
200
|
+
// numeric dir mirrored from a real source dir — not pagination output
|
|
201
|
+
if (fs.existsSync(path.resolve(process.cwd(), markupInDir, collectionName, entry))) continue
|
|
202
|
+
fs.rmSync(path.join(outDir, entry), { recursive: true, force: true })
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
188
206
|
export function generateCollectionPaginationPages(collectionData, markupInDir, markupOutDir, compileEntryFn, baseURL) {
|
|
189
207
|
if (!collectionData) return []
|
|
190
208
|
|
|
@@ -199,6 +217,11 @@ export function generateCollectionPaginationPages(collectionData, markupInDir, m
|
|
|
199
217
|
collection.pages = [collection.items]
|
|
200
218
|
}
|
|
201
219
|
|
|
220
|
+
// a shrunk page count (or removed index) leaves stale out/<name>/N/ dirs
|
|
221
|
+
pruneStalePaginationDirs(collection.name, markupInDir, markupOutDir, file ? collection.totalPages : 1)
|
|
222
|
+
|
|
223
|
+
if (!file) continue
|
|
224
|
+
|
|
202
225
|
for (let i = 0; i < collection.totalPages; i++) {
|
|
203
226
|
const pageNumber = i + 1
|
|
204
227
|
const pageUrl = pageNumber === 1 ? collection.name : `${collection.name}/${pageNumber}`
|
|
@@ -222,24 +245,23 @@ export function generateCollectionPaginationPages(collectionData, markupInDir, m
|
|
|
222
245
|
prevPageUrl
|
|
223
246
|
}
|
|
224
247
|
|
|
225
|
-
const markupOut = path.
|
|
226
|
-
const fromPath = path.
|
|
248
|
+
const markupOut = path.resolve(process.cwd(), markupOutDir, pageUrl, 'index.html')
|
|
249
|
+
const fromPath = path.resolve(process.cwd(), markupOutDir)
|
|
227
250
|
const markupOutDirFull = path.dirname(markupOut)
|
|
228
251
|
|
|
229
|
-
mkDir(markupOutDirFull)
|
|
230
|
-
|
|
231
252
|
const context = {
|
|
232
253
|
...collectionData,
|
|
233
254
|
[collectionName]: pageSnapshot,
|
|
234
255
|
relativePathPrefix: getRelativePathPrefix(markupOutDirFull, fromPath, baseURL),
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (!file) {
|
|
239
|
-
continue
|
|
256
|
+
// output-relative so page.url matches nav.json/index urls, same as compileDirectory
|
|
257
|
+
_url: getPageUrlRelativeToOutput(markupOut, markupOutDir)
|
|
240
258
|
}
|
|
241
259
|
|
|
242
|
-
|
|
260
|
+
// mkDir only when a page is actually written: no empty pagination dirs
|
|
261
|
+
// for index-less or unpublished (skipped) collection indexes
|
|
262
|
+
const compilePromise = compileEntryFn(file, context).then(({ result, skipped }) => {
|
|
263
|
+
if (skipped) return
|
|
264
|
+
mkDir(markupOutDirFull)
|
|
243
265
|
fs.writeFileSync(markupOut, result)
|
|
244
266
|
})
|
|
245
267
|
compilePromises.push(compilePromise)
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { Liquid } from 'liquidjs'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { discoverImageVariants, parseFrontMatter, groupby,
|
|
4
|
+
import { highlightCode } from '../highlight.js'
|
|
5
|
+
import { marked, renderMarkdown } from '../renderer.js'
|
|
6
|
+
import { discoverImageVariants, parseFrontMatter, groupby, buildImageTag, buildPaginationTag, renderToc } from '../helpers.js'
|
|
7
|
+
import { getImageExif, listImages } from '../image-cache.js'
|
|
7
8
|
import { slugify } from 'book-of-spells'
|
|
8
9
|
import dayjs from 'dayjs'
|
|
9
10
|
|
|
10
|
-
const marked = new Marked({ renderer: highlightRenderer })
|
|
11
|
-
|
|
12
11
|
export default class LiquidEngine {
|
|
13
12
|
constructor(templatesDir, includePaths) {
|
|
14
13
|
const roots = [templatesDir]
|
|
15
14
|
for (const inc of includePaths || []) {
|
|
16
|
-
roots.push(path.
|
|
15
|
+
roots.push(path.resolve(templatesDir, inc))
|
|
17
16
|
}
|
|
18
17
|
// Also add any _* directories as include roots
|
|
19
18
|
try {
|
|
@@ -47,6 +46,7 @@ export default class LiquidEngine {
|
|
|
47
46
|
engine.registerFilter('slugify', (str) => slugify(str))
|
|
48
47
|
engine.registerFilter('jsonify', (obj) => JSON.stringify(obj))
|
|
49
48
|
engine.registerFilter('markdown', (str) => marked.parse(str))
|
|
49
|
+
engine.registerFilter('toc', (html) => renderToc(String(html || '')))
|
|
50
50
|
engine.registerFilter('date', (str, template) => {
|
|
51
51
|
const fmt = template || timeDateFormat
|
|
52
52
|
if (!fmt) return str
|
|
@@ -70,11 +70,19 @@ export default class LiquidEngine {
|
|
|
70
70
|
return content
|
|
71
71
|
})
|
|
72
72
|
engine.registerFilter('srcset', (imagePath) => {
|
|
73
|
-
const outputDir = path.
|
|
73
|
+
const outputDir = path.resolve(process.cwd(), markupOut)
|
|
74
74
|
const { variants } = discoverImageVariants(imagePath, outputDir)
|
|
75
75
|
if (variants.length === 0) return ''
|
|
76
76
|
return variants.map(v => `${v.path} ${v.width}w`).join(', ')
|
|
77
77
|
})
|
|
78
|
+
engine.registerFilter('exif', (imagePath) => {
|
|
79
|
+
const outputDir = path.resolve(process.cwd(), markupOut)
|
|
80
|
+
return getImageExif(imagePath, outputDir)
|
|
81
|
+
})
|
|
82
|
+
engine.registerFilter('images', (dirPath) => {
|
|
83
|
+
const outputDir = path.resolve(process.cwd(), markupOut)
|
|
84
|
+
return listImages(dirPath, outputDir)
|
|
85
|
+
})
|
|
78
86
|
engine.registerFilter('groupby', (arr, key, datePart) => groupby(arr, key, datePart))
|
|
79
87
|
engine.registerFilter('highlight', (code, lang) => {
|
|
80
88
|
const highlighted = highlightCode(code, lang)
|
|
@@ -86,6 +94,7 @@ export default class LiquidEngine {
|
|
|
86
94
|
registerTags(getOutputDir) {
|
|
87
95
|
registerGoogleFontsTag(this.engine)
|
|
88
96
|
registerImageTag(this.engine, getOutputDir)
|
|
97
|
+
registerPaginationTag(this.engine)
|
|
89
98
|
registerHighlightTag(this.engine)
|
|
90
99
|
}
|
|
91
100
|
|
|
@@ -93,13 +102,17 @@ export default class LiquidEngine {
|
|
|
93
102
|
this.globals[key] = value
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
removeGlobal(key) {
|
|
106
|
+
delete this.globals[key]
|
|
107
|
+
}
|
|
108
|
+
|
|
96
109
|
async render(templateName, context) {
|
|
97
110
|
let source
|
|
98
111
|
const frontMatterResult = parseFrontMatter(templateName)
|
|
99
112
|
source = frontMatterResult.content
|
|
100
113
|
|
|
101
114
|
if (path.extname(templateName) === '.md') {
|
|
102
|
-
source =
|
|
115
|
+
source = renderMarkdown(source)
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
const frontMatter = context.page || {}
|
|
@@ -189,32 +202,20 @@ function registerImageTag(engine, getOutputDir) {
|
|
|
189
202
|
}
|
|
190
203
|
|
|
191
204
|
const prefix = (yield ctx.get(['relativePathPrefix'])) || ''
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (isSvg) {
|
|
198
|
-
attrs.unshift(`src="${prefix}${imagePath}"`)
|
|
199
|
-
} else {
|
|
200
|
-
const outputDir = getOutputDir()
|
|
201
|
-
const { src, variants } = discoverImageVariants(imagePath, outputDir)
|
|
202
|
-
const sizes = kwargs.sizes || '100vw'
|
|
203
|
-
attrs.unshift(`src="${prefix}${src}"`)
|
|
204
|
-
if (variants.length > 0) {
|
|
205
|
-
const srcsetVal = variants.map(v => `${prefix}${v.path} ${v.width}w`).join(', ')
|
|
206
|
-
attrs.push(`srcset="${srcsetVal}"`)
|
|
207
|
-
attrs.push(`sizes="${sizes}"`)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
205
|
+
return buildImageTag(imagePath, prefix, kwargs, getOutputDir)
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
}
|
|
210
209
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
210
|
+
function registerPaginationTag(engine) {
|
|
211
|
+
engine.registerTag('pagination', {
|
|
212
|
+
parse(tagToken) {
|
|
213
|
+
this.value = tagToken.args.trim()
|
|
214
|
+
},
|
|
215
|
+
* render(ctx) {
|
|
216
|
+
const pagination = yield this.liquid.evalValue(this.value, ctx)
|
|
217
|
+
const prefix = (yield ctx.get(['relativePathPrefix'])) || ''
|
|
218
|
+
return buildPaginationTag(pagination, prefix)
|
|
218
219
|
}
|
|
219
220
|
})
|
|
220
221
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import { globSync } from 'glob'
|
|
3
|
-
import { Marked } from 'marked'
|
|
4
3
|
import nunjucks from 'nunjucks'
|
|
5
4
|
import path from 'node:path'
|
|
6
|
-
import { discoverImageVariants, parseFrontMatter, groupby,
|
|
7
|
-
import {
|
|
5
|
+
import { discoverImageVariants, parseFrontMatter, groupby, buildImageTag, buildPaginationTag, renderToc } from '../helpers.js'
|
|
6
|
+
import { getImageExif, listImages } from '../image-cache.js'
|
|
7
|
+
import { toPosix } from '../../utils/helpers.js'
|
|
8
|
+
import { highlightCode } from '../highlight.js'
|
|
9
|
+
import { marked, renderMarkdown } from '../renderer.js'
|
|
8
10
|
import { slugify } from 'book-of-spells'
|
|
9
11
|
import dayjs from 'dayjs'
|
|
10
12
|
import log from '../../utils/log.js'
|
|
11
13
|
|
|
12
|
-
const marked = new Marked({ renderer: highlightRenderer })
|
|
13
|
-
|
|
14
14
|
class RelativeLoader extends nunjucks.Loader {
|
|
15
15
|
constructor(templatesDir, includePaths) {
|
|
16
16
|
super()
|
|
@@ -26,7 +26,7 @@ class RelativeLoader extends nunjucks.Loader {
|
|
|
26
26
|
if (this.includePaths) {
|
|
27
27
|
pattern = `{${this.includePaths.join(',')}}/${pattern}`
|
|
28
28
|
}
|
|
29
|
-
fullPath = globSync(path.join(this.templatesDir, pattern))[0]
|
|
29
|
+
fullPath = globSync(toPosix(path.join(this.templatesDir, pattern)))[0]
|
|
30
30
|
}
|
|
31
31
|
if (!fs.existsSync(fullPath)) {
|
|
32
32
|
log({ tag: 'markup', error: true, text: 'Template not found:', link: name })
|
|
@@ -46,7 +46,7 @@ class RelativeLoader extends nunjucks.Loader {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
if (path.extname(fullPath) === '.md') {
|
|
49
|
-
source =
|
|
49
|
+
source = renderMarkdown(source)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
if (frontMatter.layout) {
|
|
@@ -80,6 +80,12 @@ export default class NunjucksEngine {
|
|
|
80
80
|
env.addFilter('slugify', slugify)
|
|
81
81
|
env.addFilter('jsonify', (obj) => JSON.stringify(obj))
|
|
82
82
|
env.addFilter('markdown', (str) => marked.parse(str))
|
|
83
|
+
env.addFilter('toc', (html) => {
|
|
84
|
+
const toc = renderToc(String(html || ''))
|
|
85
|
+
// plain '' (falsy) when there are no headings, so `{% if x | toc %}` can
|
|
86
|
+
// skip an empty TOC column; SafeString otherwise to keep the markup raw
|
|
87
|
+
return toc ? new nunjucks.runtime.SafeString(toc) : ''
|
|
88
|
+
})
|
|
83
89
|
env.addFilter('concat', (arr, value) => {
|
|
84
90
|
if (!Array.isArray(arr)) return [value]
|
|
85
91
|
return arr.concat(value)
|
|
@@ -103,11 +109,19 @@ export default class NunjucksEngine {
|
|
|
103
109
|
return dayjs(date).format(fmt)
|
|
104
110
|
})
|
|
105
111
|
env.addFilter('srcset', (imagePath) => {
|
|
106
|
-
const outputDir = path.
|
|
112
|
+
const outputDir = path.resolve(process.cwd(), markupOut)
|
|
107
113
|
const { variants } = discoverImageVariants(imagePath, outputDir)
|
|
108
114
|
if (variants.length === 0) return ''
|
|
109
115
|
return variants.map(v => `${v.path} ${v.width}w`).join(', ')
|
|
110
116
|
})
|
|
117
|
+
env.addFilter('exif', (imagePath) => {
|
|
118
|
+
const outputDir = path.resolve(process.cwd(), markupOut)
|
|
119
|
+
return getImageExif(imagePath, outputDir)
|
|
120
|
+
})
|
|
121
|
+
env.addFilter('images', (dirPath) => {
|
|
122
|
+
const outputDir = path.resolve(process.cwd(), markupOut)
|
|
123
|
+
return listImages(dirPath, outputDir)
|
|
124
|
+
})
|
|
111
125
|
env.addFilter('groupby', (arr, key, datePart) => groupby(arr, key, datePart))
|
|
112
126
|
env.addFilter('highlight', (code, lang) => {
|
|
113
127
|
const highlighted = highlightCode(code, lang)
|
|
@@ -119,6 +133,7 @@ export default class NunjucksEngine {
|
|
|
119
133
|
registerTags(getOutputDir) {
|
|
120
134
|
this.env.addExtension('GoogleFontsExtension', new GoogleFontsExtension())
|
|
121
135
|
this.env.addExtension('ImageExtension', new ImageExtension(getOutputDir))
|
|
136
|
+
this.env.addExtension('PaginationExtension', new PaginationExtension())
|
|
122
137
|
this.env.addExtension('HighlightExtension', new HighlightExtension())
|
|
123
138
|
}
|
|
124
139
|
|
|
@@ -126,6 +141,10 @@ export default class NunjucksEngine {
|
|
|
126
141
|
this.env.addGlobal(key, value)
|
|
127
142
|
}
|
|
128
143
|
|
|
144
|
+
removeGlobal(key) {
|
|
145
|
+
delete this.env.globals[key]
|
|
146
|
+
}
|
|
147
|
+
|
|
129
148
|
render(templateName, context) {
|
|
130
149
|
return new Promise((resolve, reject) => {
|
|
131
150
|
this.env.getTemplate(templateName).render(context, (error, result) => {
|
|
@@ -208,34 +227,7 @@ export class ImageExtension {
|
|
|
208
227
|
|
|
209
228
|
run(context, imagePath, kwargs) {
|
|
210
229
|
const prefix = context.lookup('relativePathPrefix') || ''
|
|
211
|
-
|
|
212
|
-
const loading = (kwargs && kwargs.loading) || 'lazy'
|
|
213
|
-
const isSvg = imagePath.endsWith('.svg')
|
|
214
|
-
const attrs = [`alt="${alt}"`]
|
|
215
|
-
|
|
216
|
-
if (isSvg) {
|
|
217
|
-
attrs.unshift(`src="${prefix}${imagePath}"`)
|
|
218
|
-
} else {
|
|
219
|
-
const outputDir = this.getOutputDir()
|
|
220
|
-
const { src, variants } = discoverImageVariants(imagePath, outputDir)
|
|
221
|
-
const sizes = (kwargs && kwargs.sizes) || '100vw'
|
|
222
|
-
attrs.unshift(`src="${prefix}${src}"`)
|
|
223
|
-
if (variants.length > 0) {
|
|
224
|
-
const srcsetVal = variants.map(v => `${prefix}${v.path} ${v.width}w`).join(', ')
|
|
225
|
-
attrs.push(`srcset="${srcsetVal}"`)
|
|
226
|
-
attrs.push(`sizes="${sizes}"`)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
attrs.push(`loading="${loading}"`)
|
|
231
|
-
if (kwargs) {
|
|
232
|
-
const skip = new Set(['alt', 'sizes', 'loading'])
|
|
233
|
-
for (const [key, val] of Object.entries(kwargs)) {
|
|
234
|
-
if (key.startsWith('__') || skip.has(key)) continue
|
|
235
|
-
attrs.push(`${key}="${val}"`)
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return new nunjucks.runtime.SafeString(`<img ${attrs.join(' ')}>`)
|
|
230
|
+
return new nunjucks.runtime.SafeString(buildImageTag(imagePath, prefix, kwargs, this.getOutputDir))
|
|
239
231
|
}
|
|
240
232
|
}
|
|
241
233
|
|
|
@@ -260,3 +252,19 @@ export class HighlightExtension {
|
|
|
260
252
|
)
|
|
261
253
|
}
|
|
262
254
|
}
|
|
255
|
+
|
|
256
|
+
export class PaginationExtension {
|
|
257
|
+
constructor() { this.tags = ['pagination'] }
|
|
258
|
+
|
|
259
|
+
parse(parser, nodes) {
|
|
260
|
+
const tok = parser.nextToken()
|
|
261
|
+
const args = parser.parseSignature(null, true)
|
|
262
|
+
parser.advanceAfterBlockEnd(tok.value)
|
|
263
|
+
return new nodes.CallExtension(this, 'run', args)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
run(context, pagination) {
|
|
267
|
+
const prefix = context.lookup('relativePathPrefix') || ''
|
|
268
|
+
return new nunjucks.runtime.SafeString(buildPaginationTag(pagination, prefix))
|
|
269
|
+
}
|
|
270
|
+
}
|