@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
@@ -2,18 +2,21 @@ import juice from 'juice'
2
2
  import postcss from 'postcss'
3
3
  import get from 'lodash-es/get.js'
4
4
  import has from 'lodash-es/has.js'
5
+ import { defu as merge } from 'defu'
6
+ import * as cheerio from 'cheerio/slim'
5
7
  import remove from 'lodash-es/remove.js'
6
8
  import { render } from 'posthtml-render'
7
9
  import { calc } from '@csstools/css-calc'
8
10
  import isEmpty from 'lodash-es/isEmpty.js'
9
- import * as cheerio from 'cheerio/lib/slim'
10
11
  import safeParser from 'postcss-safe-parser'
11
12
  import isObject from 'lodash-es/isObject.js'
12
13
  import { parser as parse } from 'posthtml-parser'
14
+ import { parseCSSRule } from '../utils/string.js'
13
15
  import { useAttributeSizes } from './useAttributeSizes.js'
16
+ import { getPosthtmlOptions } from '../posthtml/defaultConfig.js'
14
17
 
15
18
  const posthtmlPlugin = (options = {}) => tree => {
16
- return inline(render(tree), options).then(html => parse(html))
19
+ return inline(render(tree), options).then(html => parse(html, getPosthtmlOptions()))
17
20
  }
18
21
 
19
22
  export default posthtmlPlugin
@@ -31,6 +34,26 @@ export async function inline(html = '', options = {}) {
31
34
  options.removeInlinedSelectors = get(options, 'removeInlinedSelectors', true)
32
35
  options.resolveCalc = get(options, 'resolveCalc', true)
33
36
  options.preferUnitlessValues = get(options, 'preferUnitlessValues', true)
37
+ options.safelist = new Set([
38
+ ...get(options, 'safelist', []),
39
+ ...[
40
+ '.body', // Gmail
41
+ '.gmail', // Gmail
42
+ '.apple', // Apple Mail
43
+ '.ios', // Mail on iOS
44
+ '.ox-', // Open-Xchange
45
+ '.outlook', // Outlook.com
46
+ '[data-ogs', // Outlook.com
47
+ '.bloop_container', // Airmail
48
+ '.Singleton', // Apple Mail 10
49
+ '.unused', // Notes 8
50
+ '.moz-text-html', // Thunderbird
51
+ '.mail-detail-content', // Comcast, Libero webmail
52
+ 'edo', // Edison (all)
53
+ '#msgBody', // Freenet uses #msgBody
54
+ '.lang' // Fenced code blocks
55
+ ],
56
+ ])
34
57
 
35
58
  juice.styleToAttribute = get(options, 'styleToAttribute', {})
36
59
  juice.applyWidthAttributes = get(options, 'applyWidthAttributes', true)
@@ -45,12 +68,20 @@ export async function inline(html = '', options = {}) {
45
68
  })
46
69
  }
47
70
 
48
- const $ = cheerio.load(html, { decodeEntities: false, _useHtmlParser2: true })
71
+ const $ = cheerio.load(html, {
72
+ xml: {
73
+ decodeEntities: false,
74
+ xmlMode: false,
75
+ }
76
+ })
49
77
 
50
78
  // Add a `data-embed` attribute to style tags that have the embed attribute
51
- $('style[embed]').each((i, el) => {
79
+ $('style[embed]:not([data-embed])').each((i, el) => {
52
80
  $(el).attr('data-embed', '')
53
81
  })
82
+ $('style[data-embed]:not([embed])').each((i, el) => {
83
+ $(el).attr('embed', '')
84
+ })
54
85
 
55
86
  /**
56
87
  * Inline the CSS
@@ -96,14 +127,20 @@ export async function inline(html = '', options = {}) {
96
127
  }
97
128
  )
98
129
 
99
- const preservedClasses = new Set()
130
+ // Precompile a single regex to match any substring from the preservedClasses set
131
+ const combinedPattern = Array.from(options.safelist)
132
+ .map(pattern => pattern.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')) // Escape special regex chars
133
+ .join('|') // Combine all patterns into a single regex pattern with 'OR' (|)
134
+
135
+ const combinedRegex = new RegExp(combinedPattern)
136
+
100
137
  const selectors = new Set()
101
138
 
102
139
  // Preserve selectors in at rules
103
140
  root.walkAtRules(rule => {
104
141
  if (['media', 'supports'].includes(rule.name)) {
105
142
  rule.walkRules(rule => {
106
- preservedClasses.add(rule.selector)
143
+ options.safelist.add(rule.selector)
107
144
  })
108
145
  }
109
146
  })
@@ -116,7 +153,7 @@ export async function inline(html = '', options = {}) {
116
153
  rule.walkDecls(decl => {
117
154
  // Resolve calc() values to static values
118
155
  if (options.resolveCalc) {
119
- decl.value = decl.value.includes('calc(') ? calc(decl.value) : decl.value
156
+ decl.value = decl.value.includes('calc(') ? calc(decl.value, { precision: 2 }) : decl.value
120
157
  }
121
158
 
122
159
  declarations.add(decl)
@@ -130,10 +167,10 @@ export async function inline(html = '', options = {}) {
130
167
  */
131
168
  if (options.resolveCSSVariables) {
132
169
  Array.from(declarations)
133
- /**
134
- * Consider only declarations with a value that includes any of the other declarations' property
135
- * So a decl like color(var(--text-color)) will be removed if there's a decl with a property of --text-color
136
- * */
170
+ /**
171
+ * Consider only declarations with a value that includes any of the other declarations' property
172
+ * So a decl like color(var(--text-color)) will be removed if there's a decl with a property of --text-color
173
+ * */
137
174
  .filter(decl =>
138
175
  Array.from(declarations).some(otherDecl => decl.value.includes(otherDecl.prop))
139
176
  || decl.prop.startsWith('--')
@@ -151,12 +188,12 @@ export async function inline(html = '', options = {}) {
151
188
  // Preserve pseudo selectors
152
189
  // TODO: revisit pseudos list
153
190
  if ([':hover', ':active', ':focus', ':visited', ':link', ':before', ':after'].some(i => selector.includes(i))) {
154
- preservedClasses.add(selector)
191
+ options.safelist.add(selector)
155
192
  }
156
193
 
157
194
  if (options.removeInlinedSelectors) {
158
195
  // Remove the rule in the <style> tag as long as it's not a preserved class
159
- if (!preservedClasses.has(selector)) {
196
+ if (!options.safelist.has(selector) && !combinedRegex.test(selector)) {
160
197
  rule.remove()
161
198
  }
162
199
 
@@ -179,10 +216,10 @@ export async function inline(html = '', options = {}) {
179
216
 
180
217
  if (styleAttr) {
181
218
  inlineStyles = styleAttr.split(';').reduce((acc, i) => {
182
- let [property, value] = i.split(':').map(i => i.trim())
219
+ let { property, value } = parseCSSRule(i)
183
220
 
184
221
  if (value && options.resolveCalc) {
185
- value = value.includes('calc') ? calc(value) : value
222
+ value = value.includes('calc') ? calc(value, { precision: 2 }) : value
186
223
  }
187
224
 
188
225
  if (value && options.preferUnitlessValues) {
@@ -215,7 +252,7 @@ export async function inline(html = '', options = {}) {
215
252
  // If the class has been inlined in the style attribute...
216
253
  if (has(inlineStyles, prop)) {
217
254
  // Try to remove the classes that have been inlined
218
- if (![...preservedClasses].some(item => item.endsWith(name) || item.startsWith(name))) {
255
+ if (![...options.safelist].some(item => item.includes(name))) {
219
256
  remove(classList, classToRemove => name.includes(classToRemove))
220
257
  }
221
258
 
@@ -1,19 +1,26 @@
1
1
  import posthtml from 'posthtml'
2
- import { defu as merge } from 'defu'
3
2
  import md from 'posthtml-markdownit'
4
- import posthtmlConfig from '../posthtml/defaultConfig.js'
5
3
 
6
- export async function markdown(html = '', options = {}, posthtmlOptions = {}) {
4
+ export async function markdown(input = '', options = {}, posthtmlOptions = {}) {
7
5
  /**
8
- * Automatically wrap in <md> tag, unless manual mode is enabled
9
- * With manual mode, user must wrap the markdown content in a <md> tag
6
+ * If no input is provided, return an empty string.
7
+ */
8
+ if (!input) {
9
+ return ''
10
+ }
11
+
12
+ /**
13
+ * Automatically wrap in <md> tag, unless manual mode is enabled.
14
+ *
15
+ * With manual mode, user must wrap the input in a <md> tag.
16
+ *
10
17
  * https://github.com/posthtml/posthtml-markdownit#usage
11
18
  */
12
- html = options.manual ? html : `<md>${html}</md>`
19
+ input = options.manual ? input : `<md>${input}</md>`
13
20
 
14
21
  return posthtml([
15
22
  md(options)
16
23
  ])
17
- .process(html, merge(posthtmlOptions, posthtmlConfig))
24
+ .process(input, posthtmlOptions)
18
25
  .then(result => result.html)
19
26
  }
@@ -3,16 +3,17 @@ import { crush } from 'html-crush'
3
3
  import { defu as merge } from 'defu'
4
4
  import { render } from 'posthtml-render'
5
5
  import { parser as parse } from 'posthtml-parser'
6
- import posthtmlConfig from '../posthtml/defaultConfig.js'
6
+ import { getPosthtmlOptions } from '../posthtml/defaultConfig.js'
7
7
 
8
8
  const posthtmlPlugin = (options = {}) => tree => {
9
9
  options = merge(options, {
10
10
  removeLineBreaks: true,
11
11
  })
12
12
 
13
+ const posthtmlConfig = getPosthtmlOptions()
13
14
  const { result: html } = crush(render(tree), options)
14
15
 
15
- return parse(html)
16
+ return parse(html, posthtmlConfig)
16
17
  }
17
18
 
18
19
  export default posthtmlPlugin
@@ -21,6 +22,6 @@ export async function minify(html = '', options = {}, posthtmlOptions = {}) {
21
22
  return posthtml([
22
23
  posthtmlPlugin(options)
23
24
  ])
24
- .process(html, merge(posthtmlOptions, posthtmlConfig))
25
+ .process(html, posthtmlOptions)
25
26
  .then(result => result.html)
26
27
  }
@@ -1,7 +1,5 @@
1
1
  import posthtml from 'posthtml'
2
- import { defu as merge } from 'defu'
3
2
  import posthtmlMso from 'posthtml-mso'
4
- import posthtmlConfig from '../posthtml/defaultConfig.js'
5
3
 
6
4
  export default function posthtmlPlugin(options = {}) {
7
5
  return posthtmlMso(options)
@@ -11,6 +9,6 @@ export async function useMso(html = '', options = {}, posthtmlOptions = {}) {
11
9
  return posthtml([
12
10
  posthtmlPlugin(options)
13
11
  ])
14
- .process(html, merge(posthtmlOptions, posthtmlConfig))
12
+ .process(html, posthtmlOptions)
15
13
  .then(result => result.html)
16
14
  }
@@ -2,7 +2,8 @@ import pretty from 'pretty'
2
2
  import posthtml from 'posthtml'
3
3
  import { defu as merge } from 'defu'
4
4
  import { render } from 'posthtml-render'
5
- import posthtmlConfig from '../posthtml/defaultConfig.js'
5
+ import { parser as parse } from 'posthtml-parser'
6
+ import { getPosthtmlOptions } from '../posthtml/defaultConfig.js'
6
7
 
7
8
  const posthtmlPlugin = (options = {}) => tree => {
8
9
  const defaultConfig = {
@@ -14,7 +15,7 @@ const posthtmlPlugin = (options = {}) => tree => {
14
15
 
15
16
  const config = merge(options, defaultConfig)
16
17
 
17
- return pretty(render(tree), config)
18
+ return parse(pretty(render(tree), config), getPosthtmlOptions())
18
19
  }
19
20
 
20
21
  export default posthtmlPlugin
@@ -23,6 +24,6 @@ export async function prettify(html = '', options = {}, posthtmlOptions = {}) {
23
24
  return posthtml([
24
25
  posthtmlPlugin(options)
25
26
  ])
26
- .process(html, merge(posthtmlOptions, posthtmlConfig))
27
+ .process(html, posthtmlOptions)
27
28
  .then(result => result.html)
28
29
  }
@@ -1,87 +1,37 @@
1
1
  import posthtml from 'posthtml'
2
+ import posthtmlWidows from 'posthtml-widows'
2
3
  import { defu as merge } from 'defu'
3
- import { removeWidows } from 'string-remove-widows'
4
- import posthtmlConfig from '../posthtml/defaultConfig.js'
5
4
 
6
- const posthtmlPlugin = (options = {}) => tree => {
5
+ export default function posthtmlPlugin(options = {}) {
7
6
  options = merge(options, {
8
- minWordCount: 3,
9
- attrName: 'prevent-widows'
7
+ minWords: 3
10
8
  })
11
9
 
12
- // Ignore defaults
10
+ // Custom ignores
13
11
  const mappings = [
14
- // Jinja-like
15
- {
16
- heads: '{{',
17
- tails: '}}'
18
- },
19
- {
20
- heads: ['{% if', '{%- if'],
21
- tails: ['{% endif', '{%- endif']
22
- },
23
- {
24
- heads: ['{% for', '{%- for'],
25
- tails: ['{% endfor', '{%- endfor']
26
- },
27
- {
28
- heads: ['{%', '{%-'],
29
- tails: ['%}', '-%}']
30
- },
31
- {
32
- heads: '{#',
33
- tails: '#}'
34
- },
35
- // ASP/Hexo-like
36
- {
37
- heads: ['<%', '<%=', '<%-'],
38
- tails: ['%>', '=%>', '-%>']
39
- },
40
12
  // MSO comments
41
13
  {
42
- heads: '<!--[',
43
- tails: ']>'
14
+ start: '<!--[',
15
+ end: ']>'
44
16
  },
45
17
  // <![endif]-->
46
18
  {
47
- heads: '<![',
48
- tails: ']--><'
19
+ start: '<![',
20
+ end: ']--><'
49
21
  }
50
22
  ]
51
23
 
52
24
  if (Array.isArray(options.ignore)) {
53
- options.ignore.forEach(pair => mappings.push(pair))
54
- }
55
-
56
- if (typeof options.ignore !== 'string') {
57
- options.ignore = mappings
58
- }
59
-
60
- const process = node => {
61
- if (node.attrs && Object.keys(node.attrs).includes(options.attrName)) {
62
- const widowsRemovedString = removeWidows(tree.render(node.content), options).res
63
- node.content = tree.render(tree.parser(widowsRemovedString))
64
- delete node.attrs[options.attrName]
65
- }
66
-
67
- return node
25
+ options.ignore = options.ignore.concat(mappings)
68
26
  }
69
27
 
70
- return tree.walk(process)
28
+ return posthtmlWidows(options)
71
29
  }
72
30
 
73
- export default posthtmlPlugin
74
-
75
31
  export async function preventWidows(html = '', options = {}, posthtmlOptions = {}) {
76
- // Apply only to elements that contain the `prevent-widows` attribute
77
- if (options.withAttributes) {
78
- return posthtml([
79
- posthtmlPlugin(options)
80
- ])
81
- .process(html, merge(posthtmlOptions, posthtmlConfig))
82
- .then(result => result.html)
83
- }
84
-
85
- // Apply to all elements
86
- return removeWidows(html, options).res
32
+ return posthtml([
33
+ posthtmlPlugin(options)
34
+ ])
35
+ .process(html, posthtmlOptions)
36
+ .then(result => result.html)
87
37
  }
@@ -1,7 +1,6 @@
1
1
  import posthtml from 'posthtml'
2
2
  import get from 'lodash-es/get.js'
3
- import { defu as merge } from 'defu'
4
- import posthtmlConfig from '../posthtml/defaultConfig.js'
3
+ import { getPosthtmlOptions } from '../posthtml/defaultConfig.js'
5
4
 
6
5
  /**
7
6
  * Remove empty attributes with PostHTML
@@ -49,8 +48,8 @@ export default posthtmlPlugin
49
48
 
50
49
  export async function removeAttributes(html = '', attributes = [], posthtmlOptions = {}) {
51
50
  return posthtml([
52
- posthtmlPlugin(attributes, merge(posthtmlOptions, posthtmlConfig))
51
+ posthtmlPlugin(attributes, getPosthtmlOptions(posthtmlOptions))
53
52
  ])
54
- .process(html, merge(posthtmlOptions, posthtmlConfig))
53
+ .process(html, getPosthtmlOptions())
55
54
  .then(result => result.html)
56
55
  }
@@ -1,9 +1,8 @@
1
1
  import posthtml from 'posthtml'
2
- import { defu as merge } from 'defu'
3
2
  import { render } from 'posthtml-render'
4
3
  import isEmpty from 'lodash-es/isEmpty.js'
5
4
  import { parser as parse } from 'posthtml-parser'
6
- import posthtmlConfig from '../posthtml/defaultConfig.js'
5
+ import { getPosthtmlOptions } from '../posthtml/defaultConfig.js'
7
6
 
8
7
  const posthtmlPlugin = (replacements = {}) => tree => {
9
8
  if (!isEmpty(replacements)) {
@@ -14,10 +13,13 @@ const posthtmlPlugin = (replacements = {}) => tree => {
14
13
  render(tree).replace(patterns, matched => {
15
14
  for (const [regex, replacement] of regexes) {
16
15
  if (regex.test(matched)) {
17
- return replacement
16
+ return matched.replace(regex, replacement)
18
17
  }
19
18
  }
20
- })
19
+
20
+ return matched
21
+ }),
22
+ getPosthtmlOptions()
21
23
  )
22
24
  }
23
25
 
@@ -30,6 +32,6 @@ export async function replaceStrings(html = '', replacements = {}, posthtmlOptio
30
32
  return posthtml([
31
33
  posthtmlPlugin(replacements)
32
34
  ])
33
- .process(html, merge(posthtmlOptions, posthtmlConfig))
35
+ .process(html, posthtmlOptions)
34
36
  .then(result => result.html)
35
37
  }
@@ -1,6 +1,5 @@
1
1
  import posthtml from 'posthtml'
2
2
  import { defu as merge } from 'defu'
3
- import posthtmlConfig from '../posthtml/defaultConfig.js'
4
3
  import posthtmlSafeClassNames from 'posthtml-safe-class-names'
5
4
 
6
5
  export default function posthtmlPlugin(options = {}) {
@@ -24,6 +23,6 @@ export async function safeClassNames(html = '', options = {}, posthtmlOptions =
24
23
  return posthtml([
25
24
  posthtmlPlugin(options)
26
25
  ])
27
- .process(html, merge(posthtmlOptions, posthtmlConfig))
26
+ .process(html, posthtmlOptions)
28
27
  .then(result => result.html)
29
28
  }
@@ -1,6 +1,4 @@
1
1
  import posthtml from 'posthtml'
2
- import { defu as merge } from 'defu'
3
- import posthtmlConfig from '../posthtml/defaultConfig.js'
4
2
  import posthtmlMergeLonghand from 'posthtml-postcss-merge-longhand'
5
3
 
6
4
  export default function posthtmlPlugin(options = {}) {
@@ -17,6 +15,6 @@ export async function shorthandCSS(html = '', options = {}, posthtmlOptions = {}
17
15
  return posthtml([
18
16
  posthtmlPlugin(options)
19
17
  ])
20
- .process(html, merge(posthtmlOptions, posthtmlConfig))
18
+ .process(html, posthtmlOptions)
21
19
  .then(result => result.html)
22
20
  }
@@ -1,7 +1,5 @@
1
1
  import posthtml from 'posthtml'
2
- import { defu as merge } from 'defu'
3
2
  import { conv } from 'color-shorthand-hex-to-six-digit'
4
- import posthtmlConfig from '../posthtml/defaultConfig.js'
5
3
 
6
4
  const posthtmlPlugin = () => tree => {
7
5
  const targets = new Set(['bgcolor', 'color'])
@@ -27,6 +25,6 @@ export async function sixHEX(html = '', posthtmlOptions = {}) {
27
25
  return posthtml([
28
26
  posthtmlPlugin()
29
27
  ])
30
- .process(html, merge(posthtmlOptions, posthtmlConfig))
28
+ .process(html, posthtmlOptions)
31
29
  .then(result => result.html)
32
30
  }
@@ -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
+ if (node.tag === 'template') {
9
+ // Preserve <template> tags marked as such
10
+ if ('attrs' in node && 'preserve' in node.attrs) {
11
+ node.attrs.preserve = false
12
+
13
+ return node
14
+ }
15
+
16
+ // Remove the <template> tag
17
+ node.tag = false
18
+ }
19
+
20
+ return node
21
+ }
22
+
23
+ return tree.walk(process)
24
+ }
25
+
26
+ export default posthtmlPlugin
@@ -1,8 +1,6 @@
1
1
  import posthtml from 'posthtml'
2
2
  import get from 'lodash-es/get.js'
3
- import { defu as merge } from 'defu'
4
3
  import urlParameters from 'posthtml-url-parameters'
5
- import posthtmlConfig from '../posthtml/defaultConfig.js'
6
4
 
7
5
  export default function posthtmlPlugin(options = {}) {
8
6
  const { _options, ...parameters } = options
@@ -17,6 +15,6 @@ export async function addURLParams(html = '', options = {}, posthtmlOptions = {}
17
15
  return posthtml([
18
16
  posthtmlPlugin(options)
19
17
  ])
20
- .process(html, merge(posthtmlOptions, posthtmlConfig))
18
+ .process(html, posthtmlOptions)
21
19
  .then(result => result.html)
22
20
  }
@@ -1,8 +1,6 @@
1
1
  import postcss from 'postcss'
2
2
  import posthtml from 'posthtml'
3
3
  import get from 'lodash-es/get.js'
4
- import { defu as merge } from 'defu'
5
- import posthtmlConfig from '../posthtml/defaultConfig.js'
6
4
 
7
5
  const posthtmlPlugin = (mappings = {}) => tree => {
8
6
  if (!Object.keys(mappings).length) {
@@ -60,6 +58,6 @@ export async function useAttributeSizes(html = '', mappings = {}, posthtmlOption
60
58
  return posthtml([
61
59
  posthtmlPlugin(mappings)
62
60
  ])
63
- .process(html, merge(posthtmlOptions, posthtmlConfig))
61
+ .process(html, posthtmlOptions)
64
62
  .then(result => result.html)
65
63
  }
@@ -115,3 +115,92 @@ 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
+ }
197
+
198
+ /**
199
+ * Normalize a string by removing extra whitespace.
200
+ *
201
+ * @param {String} str The string to clean
202
+ * @returns {String} The cleaned string
203
+ */
204
+ export function cleanString(str) {
205
+ return str.replace(/\s+/g, ' ').trim()
206
+ }