@maizzle/framework 4.0.0-alpha.8 → 4.0.0-alpha.9

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 (33) hide show
  1. package/bin/maizzle +3 -0
  2. package/package.json +8 -4
  3. package/src/commands/serve.js +15 -5
  4. package/src/generators/output/to-string.js +71 -71
  5. package/src/generators/tailwindcss.js +116 -116
  6. package/src/transformers/baseUrl.js +33 -9
  7. package/src/transformers/filters/defaultFilters.js +126 -0
  8. package/src/transformers/{transform.js → filters/index.js} +11 -7
  9. package/src/transformers/index.js +4 -4
  10. package/src/transformers/prettify.js +29 -20
  11. package/src/transformers/removeInlinedSelectors.js +70 -71
  12. package/src/transformers/removeUnusedCss.js +40 -35
  13. package/test/expected/posthtml/component.html +13 -13
  14. package/test/expected/transformers/atimport-in-style.html +13 -11
  15. package/test/expected/transformers/{base-image-url.html → base-url.html} +99 -83
  16. package/test/expected/transformers/filters.html +81 -0
  17. package/test/expected/transformers/preserve-transform-css.html +45 -24
  18. package/test/expected/useConfig.html +9 -9
  19. package/test/fixtures/basic.html +6 -6
  20. package/test/fixtures/posthtml/component.html +19 -19
  21. package/test/fixtures/transformers/{base-image-url.html → base-url.html} +101 -85
  22. package/test/fixtures/transformers/filters.html +87 -0
  23. package/test/fixtures/useConfig.html +9 -9
  24. package/test/stubs/components/component.html +5 -5
  25. package/test/stubs/templates/1.html +1 -1
  26. package/test/stubs/templates/2.test +1 -0
  27. package/test/test-posthtml.js +66 -66
  28. package/test/test-tailwindcss.js +1 -1
  29. package/test/test-todisk.js +43 -29
  30. package/test/test-tostring.js +148 -142
  31. package/test/test-transformers.js +490 -479
  32. package/test/stubs/templates/2.html +0 -1
  33. package/test/stubs/templates/3.mzl +0 -1
package/bin/maizzle ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('@maizzle/cli')
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@maizzle/framework",
3
- "version": "4.0.0-alpha.8",
3
+ "version": "4.0.0-alpha.9",
4
4
  "description": "Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.",
5
5
  "license": "MIT",
6
6
  "main": "src/index.js",
7
+ "bin": {
8
+ "maizzle": "bin/maizzle"
9
+ },
7
10
  "repository": {
8
11
  "type": "git",
9
12
  "url": "https://github.com/maizzle/framework.git"
@@ -33,6 +36,7 @@
33
36
  "release": "np"
34
37
  },
35
38
  "dependencies": {
39
+ "@maizzle/cli": "^1.4.0",
36
40
  "autoprefixer": "^10.4.0",
37
41
  "browser-sync": "^2.26.13",
38
42
  "color-shorthand-hex-to-six-digit": "^3.0.2",
@@ -51,7 +55,7 @@
51
55
  "posthtml": "^0.16.6",
52
56
  "posthtml-attrs-parser": "^0.1.1",
53
57
  "posthtml-base-url": "^1.0.1",
54
- "posthtml-content": "^0.0.3",
58
+ "posthtml-content": "^0.1.0",
55
59
  "posthtml-expressions": "^1.8.1",
56
60
  "posthtml-extend": "^0.6.0",
57
61
  "posthtml-extra-attributes": "^1.0.0",
@@ -62,7 +66,7 @@
62
66
  "posthtml-mso": "^1.0.4",
63
67
  "posthtml-postcss-merge-longhand": "^1.0.2",
64
68
  "posthtml-remove-attributes": "^1.0.0",
65
- "posthtml-safe-class-names": "^1.0.4",
69
+ "posthtml-safe-class-names": "^1.0.8",
66
70
  "posthtml-url-parameters": "^1.0.4",
67
71
  "pretty": "^2.0.0",
68
72
  "prevent-widows": "^1.0.2",
@@ -77,7 +81,7 @@
77
81
  "xo": "0.39.1"
78
82
  },
79
83
  "engines": {
80
- "node": ">=14.19.1"
84
+ "node": ">=14.0.0"
81
85
  },
82
86
  "ava": {
83
87
  "files": [
@@ -41,13 +41,27 @@ const serve = async (env = 'local', config = {}) => {
41
41
  const globalPaths = [
42
42
  'src/**',
43
43
  get(config, 'build.tailwind.config', 'tailwind.config.js'),
44
- [...new Set(get(config, 'build.browsersync.watch', []))]
44
+ ...new Set(get(config, 'build.browsersync.watch', []))
45
45
  ]
46
46
 
47
47
  // Watch for Template file changes
48
48
  browsersync()
49
49
  .watch(templatePaths)
50
50
  .on('change', async file => {
51
+ if (config.events && typeof config.events.beforeCreate === 'function') {
52
+ await config.events.beforeCreate(config)
53
+ }
54
+
55
+ // Don't render if file type is not configured
56
+ // eslint-disable-next-line
57
+ const filetypes = templates.reduce((acc, template) => {
58
+ return [...acc, ...get(template, 'filetypes', ['html'])]
59
+ }, [])
60
+
61
+ if (!filetypes.includes(path.extname(file).slice(1))) {
62
+ return
63
+ }
64
+
51
65
  if (get(config, 'build.console.clear')) {
52
66
  clearConsole()
53
67
  }
@@ -58,10 +72,6 @@ const serve = async (env = 'local', config = {}) => {
58
72
 
59
73
  file = file.replace(/\\/g, '/')
60
74
 
61
- if (config.events && typeof config.events.beforeCreate === 'function') {
62
- await config.events.beforeCreate(config)
63
- }
64
-
65
75
  const renderOptions = {
66
76
  maizzle: config,
67
77
  ...config.events
@@ -1,71 +1,71 @@
1
- const fm = require('front-matter')
2
- const {get, merge} = require('lodash')
3
- const posthtml = require('../posthtml')
4
- const Tailwind = require('../tailwindcss')
5
- const Transformers = require('../../transformers')
6
- const posthtmlMSO = require('../../transformers/posthtmlMso')
7
- const Config = require('../config')
8
-
9
- module.exports = async (html, options) => {
10
- process.env.NODE_ENV = get(options, 'maizzle.env', 'local')
11
-
12
- if (typeof html !== 'string') {
13
- throw new TypeError(`first argument must be an HTML string, received ${html}`)
14
- }
15
-
16
- if (html.length === 0) {
17
- throw new RangeError('received empty string')
18
- }
19
-
20
- const fileConfig = await Config.getMerged(process.env.NODE_ENV)
21
-
22
- let config = merge(fileConfig, get(options, 'maizzle', {}))
23
-
24
- const tailwindConfig = get(options, 'tailwind.config', {})
25
- const cssString = get(options, 'tailwind.css', '')
26
-
27
- let {frontmatter} = fm(html)
28
-
29
- if (frontmatter) {
30
- frontmatter = await posthtml(frontmatter, config)
31
- }
32
-
33
- html = `---\n${frontmatter}\n---\n\n${fm(html).body}`
34
-
35
- config = merge({applyTransformers: true}, config, fm(html).attributes)
36
-
37
- if (typeof get(options, 'tailwind.compiled') === 'string') {
38
- config.css = options.tailwind.compiled
39
- } else {
40
- config.css = await Tailwind.compile(cssString, html, tailwindConfig, config)
41
- }
42
-
43
- if (options && typeof options.beforeRender === 'function') {
44
- html = await options.beforeRender(html, config)
45
- }
46
-
47
- html = await posthtml(html, config)
48
-
49
- while (Object.keys(fm(html).attributes).length > 0) {
50
- html = fm(html).body
51
- }
52
-
53
- if (options && typeof options.afterRender === 'function') {
54
- html = await options.afterRender(html, config)
55
- }
56
-
57
- if (config.applyTransformers) {
58
- html = await Transformers.process(html, config)
59
- }
60
-
61
- if (options && typeof options.afterTransformers === 'function') {
62
- html = await options.afterTransformers(html, config)
63
- }
64
-
65
- html = await posthtmlMSO(html, config)
66
-
67
- return {
68
- html,
69
- config
70
- }
71
- }
1
+ const fm = require('front-matter')
2
+ const {get, merge} = require('lodash')
3
+ const posthtml = require('../posthtml')
4
+ const Tailwind = require('../tailwindcss')
5
+ const Transformers = require('../../transformers')
6
+ const posthtmlMSO = require('../../transformers/posthtmlMso')
7
+ const Config = require('../config')
8
+
9
+ module.exports = async (html, options) => {
10
+ process.env.NODE_ENV = get(options, 'maizzle.env', 'local')
11
+
12
+ if (typeof html !== 'string') {
13
+ throw new TypeError(`first argument must be an HTML string, received ${html}`)
14
+ }
15
+
16
+ if (html.length === 0) {
17
+ throw new RangeError('received empty string')
18
+ }
19
+
20
+ const fileConfig = await Config.getMerged(process.env.NODE_ENV)
21
+
22
+ let config = merge(fileConfig, get(options, 'maizzle', {}))
23
+
24
+ const tailwindConfig = get(options, 'tailwind.config', {})
25
+ const cssString = get(options, 'tailwind.css', '')
26
+
27
+ let {frontmatter} = fm(html)
28
+
29
+ if (frontmatter) {
30
+ frontmatter = await posthtml(frontmatter, config)
31
+ }
32
+
33
+ html = `---\n${frontmatter}\n---\n\n${fm(html).body}`
34
+
35
+ config = merge({applyTransformers: true}, config, fm(html).attributes)
36
+
37
+ if (typeof get(options, 'tailwind.compiled') === 'string') {
38
+ config.css = options.tailwind.compiled
39
+ } else {
40
+ config.css = await Tailwind.compile(cssString, html, tailwindConfig, config)
41
+ }
42
+
43
+ if (options && typeof options.beforeRender === 'function') {
44
+ html = await options.beforeRender(html, config)
45
+ }
46
+
47
+ html = await posthtml(html, config)
48
+
49
+ while (Object.keys(fm(html).attributes).length > 0) {
50
+ html = fm(html).body
51
+ }
52
+
53
+ if (options && typeof options.afterRender === 'function') {
54
+ html = await options.afterRender(html, config)
55
+ }
56
+
57
+ if (config.applyTransformers) {
58
+ html = await Transformers.process(html, config)
59
+ }
60
+
61
+ if (options && typeof options.afterTransformers === 'function') {
62
+ html = await options.afterTransformers(html, config)
63
+ }
64
+
65
+ html = await posthtmlMSO(html, config)
66
+
67
+ return {
68
+ html,
69
+ config
70
+ }
71
+ }
@@ -1,116 +1,116 @@
1
- const path = require('path')
2
- const fs = require('fs-extra')
3
- const postcss = require('postcss')
4
- const tailwindcss = require('tailwindcss')
5
- const postcssImport = require('postcss-import')
6
- const postcssNested = require('tailwindcss/nesting')
7
- const {requireUncached} = require('../utils/helpers')
8
- const mergeLonghand = require('postcss-merge-longhand')
9
- const {get, isObject, isEmpty, merge} = require('lodash')
10
-
11
- module.exports = {
12
- compile: async (css = '', html = '', tailwindConfig = {}, maizzleConfig = {}, spinner = null) => {
13
- tailwindConfig = (isObject(tailwindConfig) && !isEmpty(tailwindConfig)) ? tailwindConfig : get(maizzleConfig, 'build.tailwind.config', 'tailwind.config.js')
14
-
15
- // Compute the Tailwind config to use
16
- const userConfig = () => {
17
- // If a custom config object was passed, use that
18
- if (isObject(tailwindConfig) && !isEmpty(tailwindConfig)) {
19
- return tailwindConfig
20
- }
21
-
22
- /**
23
- * Try loading a fresh tailwind.config.js, with fallback to an empty object.
24
- * This will use the default Tailwind config (with rem units etc)
25
- */
26
- try {
27
- return requireUncached(path.resolve(process.cwd(), tailwindConfig))
28
- } catch {
29
- return {}
30
- }
31
- }
32
-
33
- // Merge user's Tailwind config on top of a 'base' config
34
- const config = merge({
35
- important: true,
36
- content: {
37
- files: [
38
- './src/**/*.*',
39
- {raw: html, extension: 'html'}
40
- ]
41
- }
42
- }, userConfig())
43
-
44
- // Add back the `{raw: html}` option if user provided own config
45
- if (Array.isArray(config.content)) {
46
- config.content = {
47
- files: [
48
- ...config.content,
49
- './src/**/*.*',
50
- {raw: html, extension: 'html'}
51
- ]
52
- }
53
- }
54
-
55
- // Include all `build.templates.source` paths when scanning for selectors to preserve
56
- const buildTemplates = get(maizzleConfig, 'build.templates')
57
-
58
- if (buildTemplates) {
59
- const templateObjects = Array.isArray(buildTemplates) ? buildTemplates : [buildTemplates]
60
- const templateSources = templateObjects.map(template => {
61
- const source = get(template, 'source')
62
-
63
- if (typeof source === 'function') {
64
- const sources = source(maizzleConfig)
65
-
66
- if (Array.isArray(sources)) {
67
- sources.map(s => config.content.files.push(s))
68
- } else if (typeof sources === 'string') {
69
- config.content.files.push(sources)
70
- }
71
-
72
- // Must return a valid `content` entry
73
- return {raw: '', extension: 'html'}
74
- }
75
-
76
- // Support single-file sources i.e. src/templates/index.html
77
- if (typeof source === 'string' && Boolean(path.extname(source))) {
78
- config.content.files.push(source)
79
-
80
- return {raw: '', extension: 'html'}
81
- }
82
-
83
- return `${source}/**/*.*`
84
- })
85
-
86
- config.content.files.push(...templateSources)
87
- }
88
-
89
- const userFilePath = get(maizzleConfig, 'build.tailwind.css', path.join(process.cwd(), 'src/css/tailwind.css'))
90
- const userFileExists = await fs.pathExists(userFilePath)
91
-
92
- if (userFileExists) {
93
- css = await fs.readFile(path.resolve(userFilePath), 'utf8') + css
94
- } else {
95
- css = `@import "tailwindcss/components"; @import "tailwindcss/utilities"; ${css}`
96
- }
97
-
98
- return postcss([
99
- postcssImport({path: path.dirname(userFilePath)}),
100
- postcssNested(),
101
- tailwindcss(config),
102
- maizzleConfig.env === 'local' ? () => {} : mergeLonghand(),
103
- ...get(maizzleConfig, 'build.postcss.plugins', [])
104
- ])
105
- .process(css, {from: undefined})
106
- .then(result => result.css)
107
- .catch(error => {
108
- console.error(error)
109
- if (spinner) {
110
- spinner.stop()
111
- }
112
-
113
- throw new Error(`Tailwind CSS compilation failed`)
114
- })
115
- }
116
- }
1
+ const path = require('path')
2
+ const fs = require('fs-extra')
3
+ const postcss = require('postcss')
4
+ const tailwindcss = require('tailwindcss')
5
+ const postcssImport = require('postcss-import')
6
+ const postcssNested = require('tailwindcss/nesting')
7
+ const {requireUncached} = require('../utils/helpers')
8
+ const mergeLonghand = require('postcss-merge-longhand')
9
+ const {get, isObject, isEmpty, merge} = require('lodash')
10
+
11
+ module.exports = {
12
+ compile: async (css = '', html = '', tailwindConfig = {}, maizzleConfig = {}, spinner = null) => {
13
+ tailwindConfig = (isObject(tailwindConfig) && !isEmpty(tailwindConfig)) ? tailwindConfig : get(maizzleConfig, 'build.tailwind.config', 'tailwind.config.js')
14
+
15
+ // Compute the Tailwind config to use
16
+ const userConfig = () => {
17
+ // If a custom config object was passed, use that
18
+ if (isObject(tailwindConfig) && !isEmpty(tailwindConfig)) {
19
+ return tailwindConfig
20
+ }
21
+
22
+ /**
23
+ * Try loading a fresh tailwind.config.js, with fallback to an empty object.
24
+ * This will use the default Tailwind config (with rem units etc)
25
+ */
26
+ try {
27
+ return requireUncached(path.resolve(process.cwd(), tailwindConfig))
28
+ } catch {
29
+ return {}
30
+ }
31
+ }
32
+
33
+ // Merge user's Tailwind config on top of a 'base' config
34
+ const config = merge({
35
+ important: true,
36
+ content: {
37
+ files: [
38
+ './src/**/*.*',
39
+ {raw: html, extension: 'html'}
40
+ ]
41
+ }
42
+ }, userConfig())
43
+
44
+ // Add back the `{raw: html}` option if user provided own config
45
+ if (Array.isArray(config.content)) {
46
+ config.content = {
47
+ files: [
48
+ ...config.content,
49
+ './src/**/*.*',
50
+ {raw: html, extension: 'html'}
51
+ ]
52
+ }
53
+ }
54
+
55
+ // Include all `build.templates.source` paths when scanning for selectors to preserve
56
+ const buildTemplates = get(maizzleConfig, 'build.templates')
57
+
58
+ if (buildTemplates) {
59
+ const templateObjects = Array.isArray(buildTemplates) ? buildTemplates : [buildTemplates]
60
+ const templateSources = templateObjects.map(template => {
61
+ const source = get(template, 'source')
62
+
63
+ if (typeof source === 'function') {
64
+ const sources = source(maizzleConfig)
65
+
66
+ if (Array.isArray(sources)) {
67
+ sources.map(s => config.content.files.push(s))
68
+ } else if (typeof sources === 'string') {
69
+ config.content.files.push(sources)
70
+ }
71
+
72
+ // Must return a valid `content` entry
73
+ return {raw: '', extension: 'html'}
74
+ }
75
+
76
+ // Support single-file sources i.e. src/templates/index.html
77
+ if (typeof source === 'string' && Boolean(path.extname(source))) {
78
+ config.content.files.push(source)
79
+
80
+ return {raw: '', extension: 'html'}
81
+ }
82
+
83
+ return `${source}/**/*.*`
84
+ })
85
+
86
+ config.content.files.push(...templateSources)
87
+ }
88
+
89
+ const userFilePath = get(maizzleConfig, 'build.tailwind.css', path.join(process.cwd(), 'src/css/tailwind.css'))
90
+ const userFileExists = await fs.pathExists(userFilePath)
91
+
92
+ if (userFileExists) {
93
+ css = await fs.readFile(path.resolve(userFilePath), 'utf8') + css
94
+ } else {
95
+ css = `@import "tailwindcss/components"; @import "tailwindcss/utilities"; ${css}`
96
+ }
97
+
98
+ return postcss([
99
+ postcssImport({path: path.dirname(userFilePath)}),
100
+ postcssNested(),
101
+ tailwindcss(config),
102
+ maizzleConfig.env === 'local' ? () => {} : mergeLonghand(),
103
+ ...get(maizzleConfig, 'build.postcss.plugins', [])
104
+ ])
105
+ .process(css, {from: undefined})
106
+ .then(result => result.css)
107
+ .catch(error => {
108
+ console.error(error)
109
+ if (spinner) {
110
+ spinner.stop()
111
+ }
112
+
113
+ throw new Error(`Tailwind CSS compilation failed`)
114
+ })
115
+ }
116
+ }
@@ -3,17 +3,41 @@ const isUrl = require('is-url-superb')
3
3
  const baseUrl = require('posthtml-base-url')
4
4
  const {get, isObject, isEmpty} = require('lodash')
5
5
 
6
- // VML backgrounds need regex because they're inside HTML comments :(
6
+ /**
7
+ * VML backgrounds must be handled with regex because
8
+ * they're inside HTML comments.
9
+ */
7
10
  const rewriteVMLs = (html, url) => {
8
- const vImageMatch = html.match(/(<v:image.+)(src=['"]([^'"]+)['"])/)
9
- const vFillMatch = html.match(/(<v:fill.+)(src=['"]([^'"]+)['"])/)
11
+ // Handle <v:image>
12
+ const vImageMatches = html.match(/<v:image[^>]+src="?([^"\s]+)"/g)
10
13
 
11
- if (vImageMatch && !isUrl(vImageMatch[3])) {
12
- html = html.replace(vImageMatch[0], `${vImageMatch[1]}src="${url}${vImageMatch[3]}"`)
14
+ if (vImageMatches) {
15
+ vImageMatches.forEach(match => {
16
+ const vImage = match.match(/<v:image[^>]+src="?([^"\s]+)"/)
17
+ const vImageSrc = vImage[1]
18
+
19
+ if (!isUrl(vImageSrc)) {
20
+ const vImageSrcUrl = url + vImageSrc
21
+ const vImageReplace = vImage[0].replace(vImageSrc, vImageSrcUrl)
22
+ html = html.replace(vImage[0], vImageReplace)
23
+ }
24
+ })
13
25
  }
14
26
 
15
- if (vFillMatch && !isUrl(vFillMatch[3])) {
16
- html = html.replace(vFillMatch[0], `${vFillMatch[1]}src="${url}${vFillMatch[3]}"`)
27
+ // Handle <v:fill>
28
+ const vFillMatches = html.match(/<v:fill[^>]+src="?([^"\s]+)"/g)
29
+
30
+ if (vFillMatches) {
31
+ vFillMatches.forEach(match => {
32
+ const vFill = match.match(/<v:fill[^>]+src="?([^"\s]+)"/)
33
+ const vFillSrc = vFill[1]
34
+
35
+ if (!isUrl(vFillSrc)) {
36
+ const vFillSrcUrl = url + vFillSrc
37
+ const vFillReplace = vFill[0].replace(vFillSrc, vFillSrcUrl)
38
+ html = html.replace(vFill[0], vFillReplace)
39
+ }
40
+ })
17
41
  }
18
42
 
19
43
  return html
@@ -23,7 +47,7 @@ module.exports = async (html, config = {}, direct = false) => {
23
47
  const url = direct ? config : get(config, 'baseURL')
24
48
  const posthtmlOptions = get(config, 'build.posthtml.options', {})
25
49
 
26
- // `baseUrl` as a string
50
+ // Handle `baseUrl` as a string
27
51
  if (typeof url === 'string' && url.length > 0) {
28
52
  html = rewriteVMLs(html, url)
29
53
 
@@ -34,7 +58,7 @@ module.exports = async (html, config = {}, direct = false) => {
34
58
  .then(result => result.html)
35
59
  }
36
60
 
37
- // `baseUrl: {}`
61
+ // Handle `baseUrl` as an object
38
62
  if (isObject(url) && !isEmpty(url)) {
39
63
  html = rewriteVMLs(html, url.url)
40
64
 
@@ -0,0 +1,126 @@
1
+ const escapeMap = {
2
+ '&': '&amp;',
3
+ '<': '&lt;',
4
+ '>': '&gt;',
5
+ '"': '&#34;',
6
+ '\'': '&#39;'
7
+ }
8
+
9
+ const unescapeMap = {
10
+ '&amp;': '&',
11
+ '&lt;': '<',
12
+ '&gt;': '>',
13
+ '&#34;': '"',
14
+ '&#39;': '\''
15
+ }
16
+
17
+ const unescape = string => string.replace(/&(amp|lt|gt|#34|#39);/g, m => unescapeMap[m])
18
+
19
+ const append = (content, attribute) => content + attribute
20
+ const capitalize = content => content.charAt(0).toUpperCase() + content.slice(1)
21
+ const ceil = content => Math.ceil(Number.parseFloat(content))
22
+ const divide = (content, attribute) => Number.parseFloat(content) / Number.parseFloat(attribute)
23
+ const escape = content => content.replace(/["&'<>]/g, m => escapeMap[m])
24
+ const escapeOnce = content => escape(unescape(content))
25
+ const floor = content => Math.floor(Number.parseFloat(content))
26
+ const lowercase = content => content.toLowerCase()
27
+ const lstrip = content => content.replace(/^\s+/, '')
28
+ const minus = (content, attribute) => Number.parseFloat(content) - Number.parseFloat(attribute)
29
+ const modulo = (content, attribute) => Number.parseFloat(content) % Number.parseFloat(attribute)
30
+ const multiply = (content, attribute) => Number.parseFloat(content) * Number.parseFloat(attribute)
31
+ const newlineToBr = content => content.replace(/\n/g, '<br>')
32
+ const plus = (content, attribute) => Number.parseFloat(content) + Number.parseFloat(attribute)
33
+ const prepend = (content, attribute) => attribute + content
34
+
35
+ const remove = (content, attribute) => {
36
+ const regex = new RegExp(attribute, 'g')
37
+ return content.replace(regex, '')
38
+ }
39
+
40
+ const removeFirst = (content, attribute) => content.replace(attribute, '')
41
+ const replace = (content, attribute) => {
42
+ const [search, replace] = attribute.split('|')
43
+ const regex = new RegExp(search, 'g')
44
+ return content.replace(regex, replace)
45
+ }
46
+
47
+ const replaceFirst = (content, attribute) => {
48
+ const [search, replace] = attribute.split('|')
49
+ return content.replace(search, replace)
50
+ }
51
+
52
+ const round = content => Math.round(Number.parseFloat(content))
53
+ const rstrip = content => content.replace(/\s+$/, '')
54
+ const uppercase = content => content.toUpperCase()
55
+ const size = content => content.length
56
+ const slice = (content, attribute) => {
57
+ try {
58
+ const [start, end] = attribute.split(',')
59
+ return content.slice(start, end)
60
+ } catch {
61
+ return content.slice(attribute)
62
+ }
63
+ }
64
+
65
+ const stripNewlines = content => content.replace(/\n/g, '')
66
+ const trim = content => content.trim()
67
+ const truncate = (content, attribute) => {
68
+ try {
69
+ const [length, omission] = attribute.split(',')
70
+ return content.length > Number.parseInt(length, 10) ?
71
+ content.slice(0, length) + (omission || '...') :
72
+ content
73
+ } catch {
74
+ const length = Number.parseInt(attribute, 10)
75
+ return content.length > length ? content.slice(0, length) + '...' : content
76
+ }
77
+ }
78
+
79
+ const truncateWords = (content, attribute) => {
80
+ try {
81
+ const [length, omission] = attribute.split(',')
82
+ return content.split(' ').slice(0, Number.parseInt(length, 10)).join(' ') + (omission || '...')
83
+ } catch {
84
+ const length = Number.parseInt(attribute, 10)
85
+ return content.split(' ').slice(0, length).join(' ') + '...'
86
+ }
87
+ }
88
+
89
+ // eslint-disable-next-line
90
+ const urlDecode = content => content.split('+').map(decodeURIComponent).join(' ')
91
+ // eslint-disable-next-line
92
+ const urlEncode = content => content.split(' ').map(encodeURIComponent).join('+')
93
+
94
+ exports.append = append
95
+ exports.capitalize = capitalize
96
+ exports.ceil = ceil
97
+ exports['divide-by'] = divide
98
+ exports.divide = divide
99
+ exports.escape = escape
100
+ exports['escape-once'] = escapeOnce
101
+ exports.floor = floor
102
+ exports.lowercase = lowercase
103
+ exports.lstrip = lstrip
104
+ exports.minus = minus
105
+ exports.modulo = modulo
106
+ exports.multiply = multiply
107
+ exports['newline-to-br'] = newlineToBr
108
+ exports.plus = plus
109
+ exports.prepend = prepend
110
+ exports.remove = remove
111
+ exports['remove-first'] = removeFirst
112
+ exports.replace = replace
113
+ exports['replace-first'] = replaceFirst
114
+ exports.round = round
115
+ exports.rstrip = rstrip
116
+ exports.uppercase = uppercase
117
+ exports.size = size
118
+ exports.slice = slice
119
+ exports.strip = trim
120
+ exports['strip-newlines'] = stripNewlines
121
+ exports.times = multiply
122
+ exports.trim = trim
123
+ exports.truncate = truncate
124
+ exports['truncate-words'] = truncateWords
125
+ exports['url-decode'] = urlDecode
126
+ exports['url-encode'] = urlEncode