@maizzle/framework 5.0.0-beta.0 → 5.0.0-beta.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maizzle/framework",
3
- "version": "5.0.0-beta.0",
3
+ "version": "5.0.0-beta.10",
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",
@@ -23,10 +23,10 @@
23
23
  },
24
24
  "scripts": {
25
25
  "dev": "vitest",
26
- "lint": "biome lint ./src ./test",
26
+ "release": "npx np",
27
27
  "pretest": "npm run lint",
28
28
  "test": "vitest run --coverage",
29
- "release": "npx np"
29
+ "lint": "biome lint ./src ./test"
30
30
  },
31
31
  "repository": {
32
32
  "type": "git",
@@ -48,7 +48,7 @@
48
48
  "html-emails"
49
49
  ],
50
50
  "dependencies": {
51
- "@csstools/css-calc": "^1.2.2",
51
+ "@csstools/css-calc": "^1.2.4",
52
52
  "@maizzle/cli": "next",
53
53
  "cheerio": "^1.0.0-rc.12",
54
54
  "chokidar": "^3.6.0",
@@ -64,16 +64,16 @@
64
64
  "istextorbinary": "^9.5.0",
65
65
  "juice": "^10.0.0",
66
66
  "lodash-es": "^4.17.21",
67
- "morphdom": "^2.7.2",
67
+ "morphdom": "^2.7.4",
68
68
  "ora": "^8.0.1",
69
69
  "pathe": "^1.1.2",
70
- "postcss": "^8.4.38",
71
- "postcss-custom-properties": "^13.3.10",
70
+ "postcss": "^8.4.39",
71
+ "postcss-custom-properties": "^13.3.12",
72
72
  "postcss-import": "^16.1.0",
73
73
  "postcss-safe-parser": "^7.0.0",
74
74
  "posthtml": "^0.16.6",
75
75
  "posthtml-attrs-parser": "^1.1.0",
76
- "posthtml-base-url": "^3.1.2",
76
+ "posthtml-base-url": "^3.1.4",
77
77
  "posthtml-component": "^1.1.0",
78
78
  "posthtml-content": "^2.0.1",
79
79
  "posthtml-extra-attributes": "^3.0.0",
@@ -88,11 +88,11 @@
88
88
  "pretty": "^2.0.0",
89
89
  "string-remove-widows": "^4.0.22",
90
90
  "string-strip-html": "^13.4.8",
91
- "tailwindcss": "^3.4.4",
91
+ "tailwindcss": "^3.4.5",
92
92
  "ws": "^8.17.0"
93
93
  },
94
94
  "devDependencies": {
95
- "@biomejs/biome": "^1.8.3",
95
+ "@biomejs/biome": "1.8.3",
96
96
  "@types/js-beautify": "^1.14.3",
97
97
  "@types/markdown-it": "^14.1.1",
98
98
  "@vitest/coverage-v8": "^2.0.1",
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  readFile,
3
3
  writeFile,
4
- copyFile,
5
4
  lstat,
6
5
  mkdir,
7
- rm
6
+ rm,
7
+ cp,
8
8
  } from 'node:fs/promises'
9
9
  import path from 'pathe'
10
10
  import fg from 'fast-glob'
@@ -12,22 +12,31 @@ import { defu as merge } from 'defu'
12
12
 
13
13
  import get from 'lodash/get.js'
14
14
  import isEmpty from 'lodash-es/isEmpty.js'
15
- import { isBinary } from 'istextorbinary'
16
15
 
17
16
  import ora from 'ora'
18
17
  import pico from 'picocolors'
19
18
  import cliTable from 'cli-table3'
20
19
 
21
20
  import { render } from '../generators/render.js'
22
- import { formatTime } from '../utils/string.js'
21
+
22
+ import {
23
+ formatTime,
24
+ getRootDirectories,
25
+ getFileExtensionsFromPattern,
26
+ } from '../utils/string.js'
27
+
23
28
  import { getColorizedFileSize } from '../utils/node.js'
24
- import { readFileConfig } from '../utils/getConfigByFilePath.js'
29
+
25
30
  import {
26
31
  generatePlaintext,
27
32
  handlePlaintextTags,
28
33
  writePlaintextFile
29
34
  } from '../generators/plaintext.js'
30
35
 
36
+ import { readFileConfig } from '../utils/getConfigByFilePath.js'
37
+
38
+ import { transformers } from '../transformers/index.js'
39
+
31
40
  /**
32
41
  * Compile templates and output to the build directory.
33
42
  * Returns a promise containing an object with files output and the config object.
@@ -72,9 +81,19 @@ export default async (config = {}) => {
72
81
  head: ['File name', 'File size', 'Build time'].map(item => pico.bold(item)),
73
82
  })
74
83
 
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]
84
+ /**
85
+ * Determine paths to handle
86
+ *
87
+ * 1. Resolve globs in `build.content` to folders that should be copied over to `build.output.path`
88
+ * 2. Check that templates to be built, actually exist
89
+ */
90
+ const contentPaths = get(config, 'build.content', 'src/templates/**/*.html')
91
+
92
+ // 1. Resolve globs in `build.content` to folders that should be copied over to `build.output.path`
93
+ const rootDirs = await getRootDirectories(contentPaths)
94
+
95
+ // 2. Check that templates to be built, actually exist
96
+ const templateFolders = Array.isArray(contentPaths) ? contentPaths : [contentPaths]
78
97
  const templatePaths = await fg.glob([...new Set(templateFolders)])
79
98
 
80
99
  // If there are no templates to build, throw error
@@ -82,37 +101,44 @@ export default async (config = {}) => {
82
101
  throw new Error(`No templates found in ${pico.inverse(templateFolders)}`)
83
102
  }
84
103
 
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('/')
89
- })
90
-
91
104
  /**
92
- * Check for binary files
105
+ * Copy source directories to destination
93
106
  *
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.
107
+ * Copies each `build.content` path to the `build.output.path` directory.
98
108
  */
99
- const binaryPaths = await fg.glob([...new Set(baseDirs.map(base => `${base}/**/*.*`))])
100
- .then(paths => paths.filter(file => isBinary(file)))
109
+ for await (const rootDir of rootDirs) {
110
+ await cp(rootDir, buildOutputPath, { recursive: true })
111
+ }
101
112
 
102
113
  /**
103
- * Render templates
114
+ * Get a list of files to render, from the output directory
104
115
  *
105
- * Render each template and write the output to the output directory,
106
- * preserving the relative path.
116
+ * Uses all file extensions from non-negated glob paths in `build.content`
117
+ * to determine which files to render from the output directory.
107
118
  */
108
- for await (const templatePath of templatePaths) {
109
- const templateBuildStartTime = Date.now()
119
+ const outputExtensions = new Set()
110
120
 
111
- // Determine the base directory the template belongs to
112
- const baseDir = baseDirs.find(base => templatePath.startsWith(base))
121
+ for (const pattern of contentPaths) {
122
+ outputExtensions.add(...getFileExtensionsFromPattern(pattern))
123
+ }
113
124
 
114
- // Compute the relative path
115
- const relativePath = path.relative(baseDir, templatePath)
125
+ /**
126
+ * Create a list of templates to compile
127
+ */
128
+ const extensions = outputExtensions.size > 1 ? `{${[...outputExtensions].join(',')}}` : 'html'
129
+
130
+ const templatesToCompile = await fg.glob(
131
+ path.join(
132
+ buildOutputPath,
133
+ `**/*.${extensions}`
134
+ )
135
+ )
136
+
137
+ /**
138
+ * Render templates
139
+ */
140
+ for await (const templatePath of templatesToCompile) {
141
+ const templateBuildStartTime = Date.now()
116
142
 
117
143
  /**
118
144
  * Add the current template path to the config
@@ -122,8 +148,6 @@ export default async (config = {}) => {
122
148
  */
123
149
  config.build.current = {
124
150
  path: path.parse(templatePath),
125
- baseDir,
126
- relativePath,
127
151
  }
128
152
 
129
153
  const html = await readFile(templatePath, 'utf8')
@@ -158,8 +182,9 @@ export default async (config = {}) => {
158
182
  * We do this before generating plaintext, so that
159
183
  * any paths will already have been created.
160
184
  */
161
- const outputPathFromConfig = get(rendered.config, 'permalink', path.join(buildOutputPath, relativePath))
185
+ const outputPathFromConfig = get(rendered.config, 'permalink', templatePath)
162
186
  const parsedOutputPath = path.parse(outputPathFromConfig)
187
+ // This keeps original file extension if no output extension is set
163
188
  const extension = get(rendered.config, 'build.output.extension', parsedOutputPath.ext.slice(1))
164
189
  const outputPath = `${parsedOutputPath.dir}/${parsedOutputPath.name}.${extension}`
165
190
 
@@ -175,62 +200,50 @@ export default async (config = {}) => {
175
200
  await writeFile(outputPath, rendered.html)
176
201
 
177
202
  /**
178
- * Add file to CLI table for build summary logging
203
+ * Remove original file if its path is different
204
+ * from the final destination path.
179
205
  */
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
- ])
206
+ if (outputPath !== templatePath) {
207
+ await rm(templatePath)
186
208
  }
209
+
210
+ /**
211
+ * Add file to CLI table for build summary logging
212
+ */
213
+ table.push([
214
+ path.relative(get(rendered.config, 'build.output.path'), outputPath),
215
+ getColorizedFileSize(rendered.html),
216
+ formatTime(Date.now() - templateBuildStartTime)
217
+ ])
187
218
  }
188
219
 
189
220
  /**
190
221
  * Copy static files
191
222
  *
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' }, ...]
223
+ * TODO: support an array of objects with source and destination,
224
+ * i.e. static: [{ source: 'src/assets', destination: 'assets' }, ...]
196
225
  */
226
+ const staticSourcePaths = getRootDirectories([...new Set(get(config, 'build.static.source', []))])
197
227
 
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)
205
- }
206
-
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
- }
228
+ for await (const rootDir of staticSourcePaths) {
229
+ await cp(rootDir, path.join(buildOutputPath, get(config, 'build.static.destination')), { recursive: true })
219
230
  }
220
231
 
221
- const compiledFiles = await fg.glob(path.join(config.build.output.path, '**/*'))
232
+ const allOutputFiles = await fg.glob(path.join(buildOutputPath, '**/*'))
222
233
 
223
234
  /**
224
235
  * Run `afterBuild` event
225
236
  */
226
237
  if (typeof config.afterBuild === 'function') {
227
- await config.afterBuild({ files: compiledFiles, config, render })
238
+ await config.afterBuild({
239
+ config,
240
+ files: allOutputFiles,
241
+ transform: transformers,
242
+ })
228
243
  }
229
244
 
230
245
  /**
231
246
  * Log a build summary if enabled in the config
232
- *
233
- * Need to first clear the spinner
234
247
  */
235
248
 
236
249
  spinner.clear()
@@ -239,10 +252,10 @@ export default async (config = {}) => {
239
252
  console.log(table.toString() + '\n')
240
253
  }
241
254
 
242
- spinner.succeed(`Build completed in ${formatTime(Date.now() - startTime)}`)
255
+ spinner.succeed(`Built ${table.length} template${table.length > 1 ? 's' : ''} in ${formatTime(Date.now() - startTime)}`)
243
256
 
244
257
  return {
245
- files: compiledFiles,
258
+ files: allOutputFiles,
246
259
  config
247
260
  }
248
261
  } catch (error) {
@@ -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,8 +149,8 @@ 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
  /**
@@ -158,15 +158,12 @@ export async function writePlaintextFile(plaintext = '', templateConfig = {}) {
158
158
  */
159
159
  if (plaintextConfig === true) {
160
160
  // If the template has a `permalink` key set in the FM
161
- if (typeof templateConfig.permalink === 'string') {
161
+ if (typeof config.permalink === 'string') {
162
162
  // Output plaintext at the `permalink` path
163
- plaintextOutputPath = templateConfig.permalink
163
+ plaintextOutputPath = config.permalink
164
164
  } else {
165
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
- )
166
+ plaintextOutputPath = get(config, 'build.output.path')
170
167
  }
171
168
  }
172
169
 
@@ -186,8 +183,8 @@ export async function writePlaintextFile(plaintext = '', templateConfig = {}) {
186
183
  */
187
184
  if (path.extname(plaintextOutputPath)) {
188
185
  // Ensure the target directory exists
189
- await lstat(path.dirname(plaintextOutputPath)).catch(async () => {
190
- await mkdir(path.dirname(plaintextOutputPath), { recursive: true })
186
+ await lstat(plaintextOutputPath).catch(async () => {
187
+ await mkdir(plaintextOutputPath, { recursive: true })
191
188
  })
192
189
 
193
190
  // Ensure correct extension is used
@@ -196,17 +193,19 @@ export async function writePlaintextFile(plaintext = '', templateConfig = {}) {
196
193
  path.basename(plaintextOutputPath, path.extname(plaintextOutputPath)) + '.' + plaintextExtension
197
194
  )
198
195
 
196
+ console.log('plaintextOutputPath', plaintextOutputPath);
197
+
199
198
  return writeFile(plaintextOutputPath, plaintext)
200
199
  }
201
200
 
202
201
  /**
203
202
  * If `plaintextOutputPath` is a directory path, output file there, using the template's name
204
203
  */
205
- const templateFileName = get(templateConfig, 'build.current.path.name')
204
+ const templateFileName = get(config, 'build.current.path.name')
206
205
 
207
206
  plaintextOutputPath = path.join(
208
- plaintextOutputPath,
209
- get(templateConfig, 'build.current.path.dir'),
207
+ path.dirname(plaintextOutputPath),
208
+ get(config, 'build.current.path.dir'),
210
209
  templateFileName + '.' + plaintextExtension
211
210
  )
212
211
 
@@ -5,7 +5,7 @@ import { defu as merge } from 'defu'
5
5
  import expressions from 'posthtml-expressions'
6
6
  import { parseFrontMatter } from '../utils/node.js'
7
7
  import { process as compilePostHTML } from '../posthtml/index.js'
8
- import { run as useTransformers } from '../transformers/index.js'
8
+ import { run as useTransformers, transformers } from '../transformers/index.js'
9
9
 
10
10
  export async function render(html = '', config = {}) {
11
11
  if (typeof html !== 'string') {
@@ -65,7 +65,8 @@ export async function render(html = '', config = {}) {
65
65
  content = await templateConfig.beforeRender(({
66
66
  html: content,
67
67
  config: templateConfig,
68
- render
68
+ posthtml: compilePostHTML,
69
+ transform: transformers,
69
70
  })) ?? content
70
71
  }
71
72
 
@@ -85,7 +86,8 @@ export async function render(html = '', config = {}) {
85
86
  compiled.html = await templateConfig.afterRender(({
86
87
  html: compiled.html,
87
88
  config: templateConfig,
88
- render
89
+ posthtml: compilePostHTML,
90
+ transform: transformers,
89
91
  })) ?? compiled.html
90
92
  }
91
93
 
@@ -117,7 +119,8 @@ export async function render(html = '', config = {}) {
117
119
  compiled.html = await templateConfig.afterTransformers(({
118
120
  html: compiled.html,
119
121
  config: templateConfig,
120
- render
122
+ posthtml: compilePostHTML,
123
+ transform: transformers,
121
124
  })) ?? compiled.html
122
125
  }
123
126
 
@@ -7,6 +7,8 @@ import components from 'posthtml-component'
7
7
  import posthtmlPostcss from 'posthtml-postcss'
8
8
  import defaultPosthtmlConfig from './defaultConfig.js'
9
9
  import expandLinkTag from './plugins/expandLinkTag.js'
10
+ import envAttributes from './plugins/envAttributes.js'
11
+ import envTags from './plugins/envTags.js'
10
12
 
11
13
  // PostCSS
12
14
  import tailwindcss from 'tailwindcss'
@@ -50,6 +52,8 @@ export async function process(html = '', config = {}) {
50
52
 
51
53
  return posthtml([
52
54
  ...get(config, 'posthtml.plugins.before', []),
55
+ envTags(config.env),
56
+ envAttributes(config.env),
53
57
  expandLinkTag,
54
58
  postcssPlugin,
55
59
  components(
@@ -0,0 +1,32 @@
1
+ const plugin = (env => tree => {
2
+ const process = node => {
3
+ // Return the original node if no environment is set
4
+ if (!env) {
5
+ return node
6
+ }
7
+
8
+ if (node.attrs) {
9
+ for (const attr in node.attrs) {
10
+ const suffix = `-${env}`
11
+
12
+ // Find attributes on this node that have this suffix
13
+ if (attr.endsWith(suffix)) {
14
+ const key = attr.slice(0, -suffix.length)
15
+ const value = node.attrs[attr]
16
+
17
+ // Change the attribute without the suffix to have the value of the suffixed attribute
18
+ node.attrs[key] = value
19
+
20
+ // Remove the attribute with the suffix
21
+ node.attrs[attr] = false
22
+ }
23
+ }
24
+ }
25
+
26
+ return node
27
+ }
28
+
29
+ return tree.walk(process)
30
+ })
31
+
32
+ export default plugin
@@ -0,0 +1,33 @@
1
+ const plugin = (env => tree => {
2
+ const process = node => {
3
+ env = env || 'local'
4
+
5
+ // Return the original node if it doesn't have a tag
6
+ if (!node.tag) {
7
+ return node
8
+ }
9
+
10
+ const tagEnv = node.tag.split(':').pop()
11
+
12
+ // Tag targets current env, remove it and return its content
13
+ if (node.tag === `env:${env}`) {
14
+ node.tag = false
15
+ }
16
+
17
+ // Tag doesn't target current env, remove it completely
18
+ if (
19
+ typeof node.tag === 'string'
20
+ && node.tag.startsWith('env:')
21
+ && tagEnv !== env
22
+ ) {
23
+ node.content = []
24
+ node.tag = false
25
+ }
26
+
27
+ return node
28
+ }
29
+
30
+ return tree.walk(process)
31
+ })
32
+
33
+ export default plugin
@@ -3,7 +3,8 @@ var lastKnownScrollPosition = 0
3
3
 
4
4
  function connectWebSocket() {
5
5
  if (!('WebSocket' in window)) {
6
- return
6
+ // Force reload if WebSocket is not supported
7
+ window.location.reload()
7
8
  }
8
9
 
9
10
  const { hostname, port } = window.location
@@ -1,14 +1,15 @@
1
1
  import express from 'express'
2
2
  const router = express.Router()
3
3
  import fs from 'node:fs/promises'
4
- import { dirname, join } from 'pathe'
4
+ import { cwd } from 'node:process'
5
5
  import { fileURLToPath } from 'node:url'
6
+ import { dirname, join, resolve } from 'pathe'
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url))
8
9
 
9
10
  router.get('/hmr.js', async (req, res) => {
10
11
  const morphdomScript = await fs.readFile(
11
- join(__dirname, '../../../node_modules/morphdom/dist/morphdom-umd.js'),
12
+ resolve(cwd(), 'node_modules/morphdom/dist/morphdom-umd.js'),
12
13
  'utf8'
13
14
  )
14
15
 
@@ -11,6 +11,18 @@ const posthtmlPlugin = (config = {}) => tree => {
11
11
  node.content = ['']
12
12
  }
13
13
 
14
+ /**
15
+ * Custom attributes to prevent inlining CSS from <style> tags
16
+ */
17
+ if (
18
+ node.tag === 'style'
19
+ && (node.attrs?.['no-inline'] || node.attrs?.embed)
20
+ ) {
21
+ node.attrs['no-inline'] = false
22
+ node.attrs.embed = false
23
+ node.attrs['data-embed'] = true
24
+ }
25
+
14
26
  return node
15
27
  }
16
28
 
@@ -8,6 +8,7 @@ import minify from './minify.js'
8
8
  import baseUrl from './baseUrl.js'
9
9
  import inlineCSS from './inline.js'
10
10
  import prettify from './prettify.js'
11
+ import templateTag from './template.js'
11
12
  import filters from './filters/index.js'
12
13
  import markdown from 'posthtml-markdownit'
13
14
  import posthtmlMso from './posthtmlMso.js'
@@ -56,9 +57,9 @@ export async function run(html = '', config = {}) {
56
57
  * Rewrite Tailwind CSS class names to email-safe alternatives,
57
58
  * unless explicitly disabled
58
59
  */
59
- if (get(config, 'css.safeClassNames') !== false) {
60
+ if (get(config, 'css.safe') !== false) {
60
61
  posthtmlPlugins.push(
61
- safeClassNames(get(config, 'css.safeClassNames', {}))
62
+ safeClassNames(get(config, 'css.safe', {}))
62
63
  )
63
64
  }
64
65
 
@@ -251,7 +252,14 @@ export async function run(html = '', config = {}) {
251
252
  }
252
253
 
253
254
  /**
254
- * 18. Replace strings
255
+ * 18. <template> tags
256
+ *
257
+ * Replace <template> tags with their content
258
+ */
259
+ posthtmlPlugins.push(templateTag())
260
+
261
+ /**
262
+ * 19. Replace strings
255
263
  *
256
264
  * Replace strings through regular expressions
257
265
  */
@@ -267,3 +275,23 @@ export async function run(html = '', config = {}) {
267
275
  html: result.html,
268
276
  }))
269
277
  }
278
+
279
+ export const transformers = {
280
+ comb,
281
+ sixHex,
282
+ minify,
283
+ baseUrl,
284
+ inlineCSS,
285
+ prettify,
286
+ filters,
287
+ markdown,
288
+ posthtmlMso,
289
+ shorthandCss,
290
+ preventWidows,
291
+ addAttributes,
292
+ urlParameters,
293
+ safeClassNames,
294
+ replaceStrings,
295
+ attributeToStyle,
296
+ removeAttributes,
297
+ }
@@ -10,6 +10,7 @@ import * as cheerio from 'cheerio/lib/slim'
10
10
  import safeParser from 'postcss-safe-parser'
11
11
  import isObject from 'lodash-es/isObject.js'
12
12
  import { parser as parse } from 'posthtml-parser'
13
+ import { parseCSSRule } from '../utils/string.js'
13
14
  import { useAttributeSizes } from './useAttributeSizes.js'
14
15
 
15
16
  const posthtmlPlugin = (options = {}) => tree => {
@@ -116,7 +117,7 @@ export async function inline(html = '', options = {}) {
116
117
  rule.walkDecls(decl => {
117
118
  // Resolve calc() values to static values
118
119
  if (options.resolveCalc) {
119
- decl.value = decl.value.includes('calc(') ? calc(decl.value) : decl.value
120
+ decl.value = decl.value.includes('calc(') ? calc(decl.value, {precision: 2}) : decl.value
120
121
  }
121
122
 
122
123
  declarations.add(decl)
@@ -179,10 +180,10 @@ export async function inline(html = '', options = {}) {
179
180
 
180
181
  if (styleAttr) {
181
182
  inlineStyles = styleAttr.split(';').reduce((acc, i) => {
182
- let [property, value] = i.split(':').map(i => i.trim())
183
+ let { property, value } = parseCSSRule(i)
183
184
 
184
185
  if (value && options.resolveCalc) {
185
- value = value.includes('calc') ? calc(value) : value
186
+ value = value.includes('calc') ? calc(value, {precision: 2}) : value
186
187
  }
187
188
 
188
189
  if (value && options.preferUnitlessValues) {
@@ -0,0 +1,26 @@
1
+ const posthtmlPlugin = (() => tree => {
2
+ const process = node => {
3
+ // Return the original node if it doesn't have a tag
4
+ if (!node.tag) {
5
+ return node
6
+ }
7
+
8
+ // Preserve <template> tags marked as such
9
+ if (node.tag === 'template' && node.attrs?.preserve) {
10
+ node.attrs.preserve = false
11
+
12
+ return node
13
+ }
14
+
15
+ // Replace <template> tags with their content
16
+ if (node.tag === 'template') {
17
+ node.tag = false
18
+ }
19
+
20
+ return node
21
+ }
22
+
23
+ return tree.walk(process)
24
+ })
25
+
26
+ export default posthtmlPlugin
package/src/utils/node.js CHANGED
@@ -1,7 +1,13 @@
1
+ import path from 'pathe'
1
2
  import os from 'node:os'
2
3
  import gm from 'gray-matter'
3
4
  import pico from 'picocolors'
4
5
  import { humanFileSize } from './string.js'
6
+ import {
7
+ copyFile,
8
+ mkdir,
9
+ readdir
10
+ } from 'node:fs/promises'
5
11
 
6
12
  // Return a local IP address
7
13
  export function getLocalIP() {
@@ -115,3 +115,82 @@ export function humanFileSize(bytes, si=false, dp=2) {
115
115
 
116
116
  return bytes.toFixed(dp) + ' ' + units[u]
117
117
  }
118
+
119
+ /**
120
+ * Get the root directories from a list of glob patterns.
121
+ *
122
+ * @param {array} patterns List of glob patterns.
123
+ * @returns {array} List of root directories.
124
+ */
125
+ export function getRootDirectories(patterns = []) {
126
+ if (!Array.isArray(patterns)) {
127
+ return []
128
+ }
129
+
130
+ if (patterns.length === 0) {
131
+ return []
132
+ }
133
+
134
+ return [...new Set(
135
+ patterns
136
+ .filter(pattern => !pattern.startsWith('!'))
137
+ .map(pattern => {
138
+ // If the pattern doesn't include wildcards, use it as is
139
+ if (!pattern.includes('*')) {
140
+ return pattern.replace(/\/$/, '') // Remove trailing slash if present
141
+ }
142
+ // For patterns with wildcards, get the part before the first wildcard
143
+ const parts = pattern.split(/[*{]/)[0].split('/')
144
+ return parts.slice(0, -1).join('/')
145
+ })
146
+ .filter(Boolean)
147
+ )]
148
+ }
149
+
150
+ /**
151
+ * Get the file extensions from a glob pattern.
152
+ * @param {*} pattern
153
+ * @returns
154
+ */
155
+ export function getFileExtensionsFromPattern(pattern) {
156
+ const starExtPattern = /\.([^\*\{\}]+)$/ // Matches .ext but not .* or .{ext}
157
+ const bracePattern = /\.{([^}]+)}$/ // Matches .{ext} or .{ext,ext}
158
+ const wildcardPattern = /\.\*$/ // Matches .*
159
+
160
+ if (wildcardPattern.test(pattern)) {
161
+ return ['html'] // We default to 'html' if the pattern is a wildcard
162
+ }
163
+
164
+ const braceMatch = pattern.match(bracePattern);
165
+ if (braceMatch) {
166
+ return braceMatch[1].split(',') // Split and return extensions inside braces
167
+ }
168
+
169
+ const starExtMatch = pattern.match(starExtPattern)
170
+ if (starExtMatch) {
171
+ return [starExtMatch[1]] // Return single extension
172
+ }
173
+
174
+ return ['html'] // No recognizable extension pattern, default to 'html'
175
+ }
176
+
177
+ export function parseCSSRule(rule) {
178
+ // Step 1: Trim the input string
179
+ rule = rule.trim()
180
+
181
+ // Step 2: Find the index of the first colon
182
+ const colonIndex = rule.indexOf(':')
183
+
184
+ // Step 3: Extract property and value parts
185
+ if (colonIndex === -1) {
186
+ return {
187
+ property: '',
188
+ value: ''
189
+ }
190
+ }
191
+
192
+ const property = rule.slice(0, colonIndex).trim()
193
+ const value = rule.slice(colonIndex + 1).trim()
194
+
195
+ return { property, value }
196
+ }
package/types/build.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import ComponentsConfig from './components';
2
2
  import type { SpinnerName } from 'cli-spinners';
3
+ import type ExpressionsConfig from './expressions';
3
4
 
4
5
  export default interface BuildConfig {
5
6
  /**
@@ -8,7 +9,7 @@ export default interface BuildConfig {
8
9
  components?: ComponentsConfig;
9
10
 
10
11
  /**
11
- * Directory where Maizzle should look for Templates to compile.
12
+ * Paths where Maizzle should look for Templates to compile.
12
13
  *
13
14
  * @default ['src/templates/**\/*.html']
14
15
  *
@@ -16,12 +17,17 @@ export default interface BuildConfig {
16
17
  * ```
17
18
  * export default {
18
19
  * build: {
19
- * files: ['src/templates/**\/*.html']
20
+ * content: ['src/templates/**\/*.html']
20
21
  * }
21
22
  * }
22
23
  * ```
23
24
  */
24
- files?: string | string[];
25
+ content?: string | string[];
26
+
27
+ /**
28
+ Configure expressions.
29
+ */
30
+ expressions?: ExpressionsConfig;
25
31
 
26
32
  /**
27
33
  * Define the output path for compiled Templates, and what file extension they should use.
package/types/config.d.ts CHANGED
@@ -136,7 +136,7 @@ export default interface Config {
136
136
  * ```
137
137
  * export default {
138
138
  * css: {
139
- * safeClassNames: {
139
+ * safe: {
140
140
  * ':': '__',
141
141
  * '!': 'i-',
142
142
  * }
@@ -144,7 +144,7 @@ export default interface Config {
144
144
  * }
145
145
  * ```
146
146
  */
147
- safeClassNames?: boolean | Record<string, string>;
147
+ safe?: boolean | Record<string, string>;
148
148
 
149
149
  /**
150
150
  * Ensure that all your HEX colors inside `bgcolor` and `color` attributes are defined with six digits.