@maizzle/framework 5.0.0-beta.3 → 5.0.0-beta.31

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.
Files changed (46) hide show
  1. package/package.json +32 -30
  2. package/src/commands/build.js +146 -73
  3. package/src/generators/plaintext.js +26 -23
  4. package/src/generators/render.js +15 -14
  5. package/src/posthtml/defaultComponentsConfig.js +2 -2
  6. package/src/posthtml/defaultConfig.js +13 -3
  7. package/src/posthtml/index.js +38 -9
  8. package/src/posthtml/plugins/envAttributes.js +32 -0
  9. package/src/posthtml/plugins/envTags.js +33 -0
  10. package/src/server/index.js +159 -96
  11. package/src/server/routes/index.js +51 -13
  12. package/src/server/views/404.html +59 -0
  13. package/src/server/views/index.html +162 -14
  14. package/src/transformers/addAttributes.js +2 -3
  15. package/src/transformers/attributeToStyle.js +1 -3
  16. package/src/transformers/baseUrl.js +6 -6
  17. package/src/transformers/comb.js +7 -6
  18. package/src/transformers/core.js +12 -0
  19. package/src/transformers/filters/index.js +1 -2
  20. package/src/transformers/index.js +56 -67
  21. package/src/transformers/inline.js +53 -16
  22. package/src/transformers/markdown.js +14 -7
  23. package/src/transformers/minify.js +4 -3
  24. package/src/transformers/posthtmlMso.js +1 -3
  25. package/src/transformers/prettify.js +4 -3
  26. package/src/transformers/preventWidows.js +15 -65
  27. package/src/transformers/removeAttributes.js +3 -4
  28. package/src/transformers/replaceStrings.js +7 -5
  29. package/src/transformers/safeClassNames.js +1 -2
  30. package/src/transformers/shorthandCss.js +1 -3
  31. package/src/transformers/sixHex.js +1 -3
  32. package/src/transformers/template.js +26 -0
  33. package/src/transformers/urlParameters.js +1 -3
  34. package/src/transformers/useAttributeSizes.js +1 -3
  35. package/src/utils/string.js +89 -0
  36. package/types/build.d.ts +53 -24
  37. package/types/config.d.ts +60 -49
  38. package/types/css/inline.d.ts +20 -0
  39. package/types/css/purge.d.ts +3 -3
  40. package/types/events.d.ts +153 -5
  41. package/types/index.d.ts +4 -3
  42. package/types/posthtml.d.ts +3 -3
  43. package/types/urlParameters.d.ts +1 -1
  44. package/types/widowWords.d.ts +16 -36
  45. package/types/components.d.ts +0 -195
  46. package/types/expressions.d.ts +0 -100
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maizzle/framework",
3
- "version": "5.0.0-beta.3",
3
+ "version": "5.0.0-beta.31",
4
4
  "description": "Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -48,58 +48,60 @@
48
48
  "html-emails"
49
49
  ],
50
50
  "dependencies": {
51
- "@csstools/css-calc": "^1.2.4",
51
+ "@csstools/css-calc": "^2.0.1",
52
52
  "@maizzle/cli": "next",
53
- "cheerio": "^1.0.0-rc.12",
53
+ "cheerio": "^1.0.0",
54
54
  "chokidar": "^3.6.0",
55
55
  "cli-table3": "^0.6.5",
56
56
  "color-shorthand-hex-to-six-digit": "^5.0.16",
57
57
  "defu": "^6.1.4",
58
58
  "email-comb": "^7.0.21",
59
- "express": "^4.19.2",
59
+ "express": "^4.21.0",
60
60
  "fast-glob": "^3.3.2",
61
61
  "gray-matter": "^4.0.3",
62
- "html-crush": "^6.0.18",
62
+ "html-crush": "^6.0.19",
63
63
  "is-url-superb": "^6.1.0",
64
64
  "istextorbinary": "^9.5.0",
65
- "juice": "^10.0.0",
65
+ "juice": "^11.0.0",
66
66
  "lodash-es": "^4.17.21",
67
- "morphdom": "^2.7.3",
68
- "ora": "^8.0.1",
67
+ "morphdom": "^2.7.4",
68
+ "ora": "^8.1.0",
69
69
  "pathe": "^1.1.2",
70
- "postcss": "^8.4.39",
71
- "postcss-custom-properties": "^13.3.12",
70
+ "postcss": "^8.4.49",
71
+ "postcss-custom-properties": "^14.0.4",
72
72
  "postcss-import": "^16.1.0",
73
73
  "postcss-safe-parser": "^7.0.0",
74
74
  "posthtml": "^0.16.6",
75
- "posthtml-attrs-parser": "^1.1.0",
76
- "posthtml-base-url": "^3.1.2",
77
- "posthtml-component": "^1.1.0",
78
- "posthtml-content": "^2.0.1",
79
- "posthtml-extra-attributes": "^3.0.0",
80
- "posthtml-markdownit": "^3.0.1",
81
- "posthtml-mso": "^3.0.0",
82
- "posthtml-parser": "^0.12.0",
83
- "posthtml-postcss": "^1.0.0",
84
- "posthtml-postcss-merge-longhand": "^3.1.0",
75
+ "posthtml-attrs-parser": "^1.1.1",
76
+ "posthtml-base-url": "^3.1.4",
77
+ "posthtml-component": "^2.0.0",
78
+ "posthtml-content": "^2.1.0",
79
+ "posthtml-expressions": "^1.11.4",
80
+ "posthtml-extra-attributes": "^3.1.0",
81
+ "posthtml-fetch": "^4.0.0",
82
+ "posthtml-markdownit": "^3.1.0",
83
+ "posthtml-mso": "^3.1.0",
84
+ "posthtml-parser": "^0.12.1",
85
+ "posthtml-postcss": "^1.0.2",
86
+ "posthtml-postcss-merge-longhand": "^3.1.2",
85
87
  "posthtml-render": "^3.0.0",
86
- "posthtml-safe-class-names": "^4.0.2",
87
- "posthtml-url-parameters": "^3.0.0",
88
+ "posthtml-safe-class-names": "^4.1.0",
89
+ "posthtml-url-parameters": "^3.1.0",
90
+ "posthtml-widows": "^1.0.0",
88
91
  "pretty": "^2.0.0",
89
- "string-remove-widows": "^4.0.22",
90
92
  "string-strip-html": "^13.4.8",
91
- "tailwindcss": "^3.4.5",
92
- "ws": "^8.17.0"
93
+ "tailwindcss": "^3.4.15",
94
+ "ws": "^8.18.0"
93
95
  },
94
96
  "devDependencies": {
95
- "@biomejs/biome": "^1.8.3",
97
+ "@biomejs/biome": "1.9.2",
96
98
  "@types/js-beautify": "^1.14.3",
97
- "@types/markdown-it": "^14.1.1",
98
- "@vitest/coverage-v8": "^2.0.1",
99
+ "@types/markdown-it": "^14.1.2",
100
+ "@vitest/coverage-v8": "^2.1.5",
99
101
  "supertest": "^7.0.0",
100
- "vitest": "^2.0.1"
102
+ "vitest": "^2.1.5"
101
103
  },
102
104
  "engines": {
103
- "node": ">=18.0.0"
105
+ "node": ">=18.20"
104
106
  }
105
107
  }
@@ -4,7 +4,8 @@ import {
4
4
  copyFile,
5
5
  lstat,
6
6
  mkdir,
7
- rm
7
+ rm,
8
+ cp,
8
9
  } from 'node:fs/promises'
9
10
  import path from 'pathe'
10
11
  import fg from 'fast-glob'
@@ -12,22 +13,50 @@ import { defu as merge } from 'defu'
12
13
 
13
14
  import get from 'lodash/get.js'
14
15
  import isEmpty from 'lodash-es/isEmpty.js'
15
- import { isBinary } from 'istextorbinary'
16
16
 
17
17
  import ora from 'ora'
18
18
  import pico from 'picocolors'
19
19
  import cliTable from 'cli-table3'
20
20
 
21
21
  import { render } from '../generators/render.js'
22
- import { formatTime } from '../utils/string.js'
22
+
23
+ import {
24
+ formatTime,
25
+ getRootDirectories,
26
+ getFileExtensionsFromPattern,
27
+ } from '../utils/string.js'
28
+
23
29
  import { getColorizedFileSize } from '../utils/node.js'
24
- import { readFileConfig } from '../utils/getConfigByFilePath.js'
30
+
25
31
  import {
26
32
  generatePlaintext,
27
33
  handlePlaintextTags,
28
34
  writePlaintextFile
29
35
  } from '../generators/plaintext.js'
30
36
 
37
+ import { readFileConfig } from '../utils/getConfigByFilePath.js'
38
+
39
+ /**
40
+ * Ensures that a directory exists, creating it if needed.
41
+ *
42
+ * @param {string} filePath - The path to the file to check.
43
+ */
44
+ async function ensureDirectoryExistence(filePath) {
45
+ const dirname = path.dirname(filePath)
46
+ await mkdir(dirname, { recursive: true })
47
+ }
48
+
49
+ /**
50
+ * Copy a file from source to target.
51
+ *
52
+ * @param {string} source - The source file path.
53
+ * @param {string} target - The target file path.
54
+ */
55
+ async function copyFileAsync(source, target) {
56
+ await ensureDirectoryExistence(target)
57
+ await copyFile(source, target)
58
+ }
59
+
31
60
  /**
32
61
  * Compile templates and output to the build directory.
33
62
  * Returns a promise containing an object with files output and the config object.
@@ -41,7 +70,10 @@ export default async (config = {}) => {
41
70
  try {
42
71
  const startTime = Date.now()
43
72
 
44
- // Compute config
73
+ /**
74
+ * Read the config file for this environment,
75
+ * merging it with the default config.
76
+ */
45
77
  config = await readFileConfig(config).catch(() => { throw new Error('Could not compute config') })
46
78
 
47
79
  /**
@@ -72,9 +104,12 @@ export default async (config = {}) => {
72
104
  head: ['File name', 'File size', 'Build time'].map(item => pico.bold(item)),
73
105
  })
74
106
 
75
- // Determine paths of templates to build
76
- const userFilePaths = get(config, 'build.content', 'src/templates/**/*.html')
77
- const templateFolders = Array.isArray(userFilePaths) ? userFilePaths : [userFilePaths]
107
+ /**
108
+ * Check that templates to be built, actually exist
109
+ */
110
+ const contentPaths = get(config, 'build.content', ['src/templates/**/*.html'])
111
+
112
+ const templateFolders = Array.isArray(contentPaths) ? contentPaths : [contentPaths]
78
113
  const templatePaths = await fg.glob([...new Set(templateFolders)])
79
114
 
80
115
  // If there are no templates to build, throw error
@@ -82,37 +117,86 @@ export default async (config = {}) => {
82
117
  throw new Error(`No templates found in ${pico.inverse(templateFolders)}`)
83
118
  }
84
119
 
85
- const baseDirs = templateFolders.filter(p => !p.startsWith('!')).map(p => {
86
- const parts = p.split('/')
87
- // remove the glob part (e.g., **/*.html):
88
- return parts.filter(part => !part.includes('*')).join('/')
120
+ /**
121
+ * Copy source directories to destination
122
+ *
123
+ * Copies each `build.content` path to the `build.output.path` directory.
124
+ */
125
+ let from = get(config, 'build.output.from', ['src/templates', 'src'])
126
+
127
+ const globPathsToCopy = contentPaths.map(glob => {
128
+ // Keep negated paths as they are
129
+ if (glob.startsWith('!')) {
130
+ return glob
131
+ }
132
+
133
+ // Keep single-file sources as they are
134
+ if (!/\*/.test(glob)) {
135
+ return glob
136
+ }
137
+
138
+ // Update non-negated paths to target all files, avoiding duplication
139
+ return glob.replace(/\/\*\*\/\*\.\{.*?\}$|\/\*\*\/\*\.[^/]*$|\/*\.[^/]*$/, '/**/*')
89
140
  })
90
141
 
142
+ try {
143
+ from = Array.isArray(from) ? from : [from]
144
+
145
+ /**
146
+ * Copy files from source to destination
147
+ *
148
+ * The array/set conversion is to remove duplicates
149
+ */
150
+ for (const file of await fg(Array.from(new Set(globPathsToCopy)))) {
151
+ let relativePath
152
+ for (const dir of from) {
153
+ if (file.startsWith(dir)) {
154
+ relativePath = path.relative(dir, file)
155
+ break
156
+ }
157
+ }
158
+ if (!relativePath) {
159
+ relativePath = path.relative('.', file)
160
+ }
161
+
162
+ const targetPath = path.join(buildOutputPath, relativePath)
163
+ await copyFileAsync(file, targetPath)
164
+ }
165
+ } catch (error) {
166
+ console.error('Error while processing pattern:', error)
167
+ }
168
+
91
169
  /**
92
- * Check for binary files
170
+ * Get a list of files to render, from the output directory
93
171
  *
94
- * We store paths to binary files in a separate array, because we don't want
95
- * to render them. These files will be treated as static files and will
96
- * be copied directly to the output directory, just like the
97
- * `build.static` folders.
172
+ * Uses all file extensions from non-negated glob paths in `build.content`
173
+ * to determine which files to render from the output directory.
98
174
  */
99
- const binaryPaths = await fg.glob([...new Set(baseDirs.map(base => `${base}/**/*.*`))])
100
- .then(paths => paths.filter(file => isBinary(file)))
175
+ const outputExtensions = new Set()
176
+
177
+ for (const pattern of contentPaths) {
178
+ outputExtensions.add(...getFileExtensionsFromPattern(pattern))
179
+ }
101
180
 
102
181
  /**
103
- * Render templates
104
- *
105
- * Render each template and write the output to the output directory,
106
- * preserving the relative path.
182
+ * Create a list of templates to compile
107
183
  */
108
- for await (const templatePath of templatePaths) {
109
- const templateBuildStartTime = Date.now()
184
+ const extensions = outputExtensions.size > 1
185
+ ? `{${[...outputExtensions].join(',')}}`
186
+ : [...outputExtensions][0] || 'html'
110
187
 
111
- // Determine the base directory the template belongs to
112
- const baseDir = baseDirs.find(base => templatePath.startsWith(base))
188
+ const templatesToCompile = await fg.glob(
189
+ path.join(
190
+ buildOutputPath,
191
+ `**/*.${extensions}`
192
+ )
193
+ )
113
194
 
114
- // Compute the relative path
115
- const relativePath = path.relative(baseDir, templatePath)
195
+ /**
196
+ * Render templates
197
+ */
198
+ for await (const templatePath of templatesToCompile) {
199
+ const templateBuildStartTime = Date.now()
116
200
 
117
201
  /**
118
202
  * Add the current template path to the config
@@ -122,8 +206,6 @@ export default async (config = {}) => {
122
206
  */
123
207
  config.build.current = {
124
208
  path: path.parse(templatePath),
125
- baseDir,
126
- relativePath,
127
209
  }
128
210
 
129
211
  const html = await readFile(templatePath, 'utf8')
@@ -141,12 +223,15 @@ export default async (config = {}) => {
141
223
  if (Boolean(plaintextConfig) || !isEmpty(plaintextConfig)) {
142
224
  const posthtmlOptions = get(rendered.config, 'posthtml.options', {})
143
225
 
144
- const plaintext = await generatePlaintext(rendered.html, merge(plaintextConfig, posthtmlOptions))
145
- rendered.html = await handlePlaintextTags(rendered.html, posthtmlOptions)
146
- await writePlaintextFile(plaintext, rendered.config)
226
+ await writePlaintextFile(
227
+ await generatePlaintext(rendered.html, merge(plaintextConfig, posthtmlOptions)),
228
+ rendered.config
229
+ )
147
230
  .catch(error => {
148
231
  throw new Error(`Error writing plaintext file: ${error}`)
149
232
  })
233
+
234
+ rendered.html = await handlePlaintextTags(rendered.html, posthtmlOptions)
150
235
  }
151
236
 
152
237
  /**
@@ -158,8 +243,9 @@ export default async (config = {}) => {
158
243
  * We do this before generating plaintext, so that
159
244
  * any paths will already have been created.
160
245
  */
161
- const outputPathFromConfig = get(rendered.config, 'permalink', path.join(buildOutputPath, relativePath))
246
+ const outputPathFromConfig = get(rendered.config, 'permalink', templatePath)
162
247
  const parsedOutputPath = path.parse(outputPathFromConfig)
248
+ // This keeps original file extension if no output extension is set
163
249
  const extension = get(rendered.config, 'build.output.extension', parsedOutputPath.ext.slice(1))
164
250
  const outputPath = `${parsedOutputPath.dir}/${parsedOutputPath.name}.${extension}`
165
251
 
@@ -175,62 +261,49 @@ export default async (config = {}) => {
175
261
  await writeFile(outputPath, rendered.html)
176
262
 
177
263
  /**
178
- * Add file to CLI table for build summary logging
264
+ * Remove original file if its path is different
265
+ * from the final destination path.
179
266
  */
180
- if (config.build.summary) {
181
- table.push([
182
- path.relative(get(rendered.config, 'build.output.path'), outputPath),
183
- getColorizedFileSize(rendered.html),
184
- formatTime(Date.now() - templateBuildStartTime)
185
- ])
267
+ if (outputPath !== templatePath) {
268
+ await rm(templatePath)
186
269
  }
270
+
271
+ /**
272
+ * Add file to CLI table for build summary logging
273
+ */
274
+ table.push([
275
+ path.relative(get(rendered.config, 'build.output.path'), outputPath),
276
+ getColorizedFileSize(rendered.html),
277
+ formatTime(Date.now() - templateBuildStartTime)
278
+ ])
187
279
  }
188
280
 
189
281
  /**
190
282
  * Copy static files
191
283
  *
192
- * Copy binary files that are alongside templates as well as
193
- * files from `build.static`, to the output directory.
194
- *
195
- * TODO: support an array of objects with source and destination, i.e. static: [{ source: 'src/assets', destination: 'assets' }, ...]
284
+ * TODO: support an array of objects with source and destination,
285
+ * i.e. static: [{ source: 'src/assets', destination: 'assets' }, ...]
196
286
  */
287
+ const staticSourcePaths = getRootDirectories([...new Set(get(config, 'build.static.source', []))])
197
288
 
198
- // Copy binary files that are alongside templates
199
- for await (const binaryPath of binaryPaths) {
200
- const relativePath = path.relative(get(config, 'build.current.baseDir'), binaryPath)
201
- const outputPath = path.join(get(config, 'build.output.path'), get(config, 'build.static.destination'), relativePath)
202
-
203
- await mkdir(path.dirname(outputPath), { recursive: true })
204
- await copyFile(binaryPath, outputPath)
289
+ for await (const rootDir of staticSourcePaths) {
290
+ await cp(rootDir, path.join(buildOutputPath, get(config, 'build.static.destination')), { recursive: true })
205
291
  }
206
292
 
207
- // Copy files from `build.static`
208
- const staticSourcePaths = await fg.glob([...new Set(get(config, 'build.static.source', []))])
209
- .then(paths => paths.filter(file => isBinary(file)))
210
-
211
- if (!isEmpty(staticSourcePaths)) {
212
- for await (const staticPath of staticSourcePaths) {
213
- const relativePath = path.relative(get(config, 'build.current.baseDir'), staticPath)
214
- const outputPath = path.join(get(config, 'build.output.path'), get(config, 'build.static.destination'), relativePath)
215
-
216
- await mkdir(path.dirname(outputPath), { recursive: true })
217
- await copyFile(staticPath, outputPath)
218
- }
219
- }
220
-
221
- const compiledFiles = await fg.glob(path.join(config.build.output.path, '**/*'))
293
+ const allOutputFiles = await fg.glob(path.join(buildOutputPath, '**/*'))
222
294
 
223
295
  /**
224
296
  * Run `afterBuild` event
225
297
  */
226
298
  if (typeof config.afterBuild === 'function') {
227
- await config.afterBuild({ files: compiledFiles, config, render })
299
+ await config.afterBuild({
300
+ config,
301
+ files: allOutputFiles,
302
+ })
228
303
  }
229
304
 
230
305
  /**
231
306
  * Log a build summary if enabled in the config
232
- *
233
- * Need to first clear the spinner
234
307
  */
235
308
 
236
309
  spinner.clear()
@@ -239,10 +312,10 @@ export default async (config = {}) => {
239
312
  console.log(table.toString() + '\n')
240
313
  }
241
314
 
242
- spinner.succeed(`Build completed in ${formatTime(Date.now() - startTime)}`)
315
+ spinner.succeed(`Built ${table.length} template${table.length > 1 ? 's' : ''} in ${formatTime(Date.now() - startTime)}`)
243
316
 
244
317
  return {
245
- files: compiledFiles,
318
+ files: allOutputFiles,
246
319
  config
247
320
  }
248
321
  } catch (error) {
@@ -3,8 +3,8 @@ import posthtml from 'posthtml'
3
3
  import get from 'lodash-es/get.js'
4
4
  import { defu as merge } from 'defu'
5
5
  import { stripHtml } from 'string-strip-html'
6
- import defaultConfig from '../posthtml/defaultConfig.js'
7
6
  import { writeFile, lstat, mkdir } from 'node:fs/promises'
7
+ import { getPosthtmlOptions } from '../posthtml/defaultConfig.js'
8
8
 
9
9
  /**
10
10
  * Removes HTML tags from a given HTML string based on
@@ -46,7 +46,7 @@ const removeTags = ({ tag = 'not-plaintext', html = '', config = {} }) => {
46
46
  return tree.walk(process)
47
47
  }
48
48
 
49
- const posthtmlOptions = merge(defaultConfig, config)
49
+ const posthtmlOptions = merge(config, getPosthtmlOptions())
50
50
 
51
51
  return posthtml([posthtmlPlugin()]).process(html, { ...posthtmlOptions }).then(res => res.html)
52
52
  }
@@ -95,7 +95,7 @@ export async function handlePlaintextTags(html = '', config = {}) {
95
95
  return tree.walk(process)
96
96
  }
97
97
 
98
- const posthtmlOptions = merge(defaultConfig, config)
98
+ const posthtmlOptions = merge(config, getPosthtmlOptions())
99
99
 
100
100
  return posthtml([posthtmlPlugin()]).process(html, { ...posthtmlOptions }).then(res => res.html)
101
101
  }
@@ -134,7 +134,7 @@ export async function generatePlaintext(html = '', config = {}) {
134
134
  ).result
135
135
  }
136
136
 
137
- export async function writePlaintextFile(plaintext = '', templateConfig = {}) {
137
+ export async function writePlaintextFile(plaintext = '', config = {}) {
138
138
  if (!plaintext) {
139
139
  throw new Error('Missing plaintext content.')
140
140
  }
@@ -149,25 +149,16 @@ export async function writePlaintextFile(plaintext = '', templateConfig = {}) {
149
149
  * Fall back to template's build output path and extension, for example:
150
150
  * `config.build.output.path`
151
151
  */
152
- const plaintextConfig = get(templateConfig, 'plaintext')
153
- let plaintextOutputPath = get(plaintextConfig, 'output.path', get(templateConfig, 'build.output.path'))
152
+ const plaintextConfig = get(config, 'plaintext')
153
+ let plaintextOutputPath = get(plaintextConfig, 'output.path', get(config, 'build.output.path'))
154
154
  const plaintextExtension = get(plaintextConfig, 'output.extension', 'txt')
155
155
 
156
156
  /**
157
- * If `plaintext: true` (either from Front Matter or from config)
157
+ * If `plaintext: true` (either from Front Matter or from config),
158
+ * output plaintext file in the same location as the HTML file.
158
159
  */
159
160
  if (plaintextConfig === true) {
160
- // If the template has a `permalink` key set in the FM
161
- if (typeof templateConfig.permalink === 'string') {
162
- // Output plaintext at the `permalink` path
163
- plaintextOutputPath = templateConfig.permalink
164
- } else {
165
- // Output plaintext at the same directory as the HTML file
166
- plaintextOutputPath = path.join(
167
- get(templateConfig, 'build.output.path'),
168
- get(templateConfig, 'build.current.relativePath')
169
- )
170
- }
161
+ plaintextOutputPath = get(config, 'build.output.path')
171
162
  }
172
163
 
173
164
  /**
@@ -182,11 +173,21 @@ export async function writePlaintextFile(plaintext = '', templateConfig = {}) {
182
173
  // No need to handle if it's an object, since we already set it to that initially
183
174
 
184
175
  /**
185
- * If `plaintextOutputPath` is a file path, output file there
176
+ * If the template has a `permalink` key set in the FM, always output plaintext file there
177
+ */
178
+ if (typeof config.permalink === 'string') {
179
+ plaintextOutputPath = config.permalink
180
+ }
181
+
182
+ /**
183
+ * If `plaintextOutputPath` is a file path, output file there.
184
+ *
185
+ * The file will be output relative to the project root, and the extension
186
+ * doesn't matter, it will be replaced with `plaintextExtension`.
186
187
  */
187
188
  if (path.extname(plaintextOutputPath)) {
188
189
  // Ensure the target directory exists
189
- await lstat(path.dirname(plaintextOutputPath)).catch(async () => {
190
+ await lstat(plaintextOutputPath).catch(async () => {
190
191
  await mkdir(path.dirname(plaintextOutputPath), { recursive: true })
191
192
  })
192
193
 
@@ -200,13 +201,15 @@ export async function writePlaintextFile(plaintext = '', templateConfig = {}) {
200
201
  }
201
202
 
202
203
  /**
203
- * If `plaintextOutputPath` is a directory path, output file there, using the template's name
204
+ * If `plaintextOutputPath` is a directory path, output file there using the Template's name.
205
+ *
206
+ * The file will be output relative to the `build.output.path` directory.
204
207
  */
205
- const templateFileName = get(templateConfig, 'build.current.path.name')
208
+ const templateFileName = get(config, 'build.current.path.name')
206
209
 
207
210
  plaintextOutputPath = path.join(
211
+ get(config, 'build.current.path.dir'),
208
212
  plaintextOutputPath,
209
- get(templateConfig, 'build.current.path.dir'),
210
213
  templateFileName + '.' + plaintextExtension
211
214
  )
212
215
 
@@ -4,6 +4,7 @@ import { cwd } from 'node:process'
4
4
  import { defu as merge } from 'defu'
5
5
  import expressions from 'posthtml-expressions'
6
6
  import { parseFrontMatter } from '../utils/node.js'
7
+ import { getPosthtmlOptions } from '../posthtml/defaultConfig.js'
7
8
  import { process as compilePostHTML } from '../posthtml/index.js'
8
9
  import { run as useTransformers } from '../transformers/index.js'
9
10
 
@@ -40,7 +41,7 @@ export async function render(html = '', config = {}) {
40
41
  })
41
42
  ]
42
43
  )
43
- .process(matter)
44
+ .process(matter, getPosthtmlOptions())
44
45
  .then(({ html }) => parseFrontMatter(`---${html}\n---`))
45
46
 
46
47
  const templateConfig = merge(matterData, config)
@@ -57,19 +58,21 @@ export async function render(html = '', config = {}) {
57
58
  *
58
59
  * @param {Object} options
59
60
  * @param {string} options.html - The HTML to be transformed
61
+ * @param {Object} options.matter - The front matter data
60
62
  * @param {Object} options.config - The current template config
61
- * @param {function} options.render - The render function
62
63
  * @returns {string} - The transformed HTML, or the original one if nothing was returned
63
64
  */
64
65
  if (typeof templateConfig.beforeRender === 'function') {
65
66
  content = await templateConfig.beforeRender(({
66
67
  html: content,
68
+ matter: matterData,
67
69
  config: templateConfig,
68
- render
69
70
  })) ?? content
70
71
  }
71
72
 
72
- // Compile PostHTML
73
+ /**
74
+ * Compile PostHTML
75
+ */
73
76
  const compiled = await compilePostHTML(content, templateConfig)
74
77
 
75
78
  /**
@@ -77,19 +80,18 @@ export async function render(html = '', config = {}) {
77
80
  *
78
81
  * @param {Object} options
79
82
  * @param {string} options.html - The HTML to be transformed
83
+ * @param {Object} options.matter - The front matter data
80
84
  * @param {Object} options.config - The current template config
81
- * @param {function} options.render - The render function
82
85
  * @returns {string} - The transformed HTML, or the original one if nothing was returned
83
86
  */
84
87
  if (typeof templateConfig.afterRender === 'function') {
85
88
  compiled.html = await templateConfig.afterRender(({
86
89
  html: compiled.html,
87
- config: templateConfig,
88
- render
90
+ matter: matterData,
91
+ config: compiled.config,
89
92
  })) ?? compiled.html
90
93
  }
91
94
 
92
- // Run Transformers
93
95
  /**
94
96
  * Run Transformers
95
97
  *
@@ -100,29 +102,28 @@ export async function render(html = '', config = {}) {
100
102
  * @returns {string} - The transformed HTML
101
103
  */
102
104
  if (templateConfig.useTransformers !== false) {
103
- compiled.html = await useTransformers(compiled.html, templateConfig).then(({ html }) => html)
105
+ compiled.html = await useTransformers(compiled.html, compiled.config).then(({ html }) => html)
104
106
  }
105
107
 
106
- // Run `afterTransformers` event
107
108
  /**
108
109
  * Run `afterTransformers` event
109
110
  *
110
111
  * @param {Object} options
111
112
  * @param {string} options.html - The HTML to be transformed
113
+ * @param {Object} options.matter - The front matter data
112
114
  * @param {Object} options.config - The current template config
113
- * @param {function} options.render - The render function
114
115
  * @returns {string} - The transformed HTML, or the original one if nothing was returned
115
116
  */
116
117
  if (typeof templateConfig.afterTransformers === 'function') {
117
118
  compiled.html = await templateConfig.afterTransformers(({
118
119
  html: compiled.html,
119
- config: templateConfig,
120
- render
120
+ matter: matterData,
121
+ config: compiled.config,
121
122
  })) ?? compiled.html
122
123
  }
123
124
 
124
125
  return {
125
- config: templateConfig,
126
+ config: compiled.config,
126
127
  html: compiled.html,
127
128
  }
128
129
  }
@@ -1,8 +1,8 @@
1
1
  export default {
2
2
  root: './',
3
- folders: ['src/components', 'src/layouts', 'src/templates'],
4
- fileExtension: 'html',
5
3
  tag: 'component',
4
+ fileExtension: 'html',
5
+ folders: ['src/components', 'src/layouts', 'src/templates'],
6
6
  expressions: {
7
7
  loopTags: ['each', 'for'],
8
8
  missingLocal: '{local}',
@@ -1,4 +1,14 @@
1
- export default {
2
- recognizeNoValueAttribute: true,
3
- recognizeSelfClosing: true
1
+ import { defu as merge } from 'defu'
2
+
3
+ export function getPosthtmlOptions(userConfigOptions = {}) {
4
+ return merge(
5
+ userConfigOptions,
6
+ {
7
+ recognizeNoValueAttribute: true,
8
+ recognizeSelfClosing: true,
9
+ directives: [
10
+ { name: '?php', start: '<', end: '>' },
11
+ ],
12
+ }
13
+ )
4
14
  }