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/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 PrintStyle from './utils/print-style.js'
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
- console.log(`${pstyle.redBright + pstyle.bold}[error]${pstyle.reset}[copy] ${pstyle.dim}Cannot copy. Missing 'in' or 'out' property in copy entry:${pstyle.reset} ${JSON.stringify(copyEntry)}`)
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
- console.log(`${pstyle.redBright + pstyle.bold}[error]${pstyle.reset}[copy] ${pstyle.dim}Cannot copy. Source path does not exist:${pstyle.reset} ${matchedPath}`)
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
- console.log(`${pstyle.redBright + pstyle.bold}[error]${pstyle.reset}[copy] ${pstyle.dim}No files matched glob pattern:${pstyle.reset} ${inEntry}`)
66
+ log({ tag: 'copy', error: true, text: 'No files matched glob pattern:', link: inEntry })
69
67
  } else {
70
- console.log(`${pstyle.redBright + pstyle.bold}[error]${pstyle.reset}[copy] ${pstyle.dim}Cannot copy. Source path does not exist:${pstyle.reset} ${inEntry}`)
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
- console.log(`${pstyle.green + pstyle.bold}[copy]${pstyle.reset} ${pstyle.dim}Copied${pstyle.reset} ${pstyle.italic + pstyle.underline}${copyLastPath}${pstyle.reset} ${pstyle.green}(${buildTime(copyStartTime, copyEndTime)})${pstyle.reset}`)
77
+ log({ tag: 'copy', text: 'Copied', link: copyLastPath, time: buildTime(copyStartTime, copyEndTime) })
80
78
  return
81
79
  }
82
80
 
83
- console.log(`${pstyle.green + pstyle.bold}[copy]${pstyle.reset} ${pstyle.dim}Copied${pstyle.reset} ${copyPathCount} paths ${pstyle.green}(${buildTime(copyStartTime, copyEndTime)})${pstyle.reset}`)
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
+ }