@maizzle/framework
Version:
Maizzle is a framework that helps you quickly build HTML emails with Tailwind CSS.
223 lines (191 loc) • 6.64 kB
JavaScript
import path from 'pathe'
import posthtml from 'posthtml'
import get from 'lodash-es/get.js'
import { defu as merge } from 'defu'
import { stripHtml } from 'string-strip-html'
import { writeFile, lstat, mkdir } from 'node:fs/promises'
import { getPosthtmlOptions } from '../posthtml/defaultConfig.js'
/**
* Removes HTML tags from a given HTML string based on
* a specified tag name or an array of tag names.
*
* @param {Object} options - The options object.
* @param {string|string[]} [options.tag='not-plaintext'] - The tag name or an array of tag names to remove from the HTML.
* @param {string} [options.html=''] - The HTML string from which to remove the tags.
* @param {Object} [options.config={}] - PostHTML options.
* @returns {string} - The HTML string with the specified tags removed.
*/
const removeTags = ({ tag = 'not-plaintext', html = '', config = {} }) => {
/**
* If the HTML string is empty, return it as is
*/
if (!html) {
return html
}
const posthtmlPlugin = () => tree => {
const process = node => {
if (!node.tag) {
return node
}
/**
* If the tag is a string and it matches the node tag, remove it
*/
if (node.tag === tag) {
return {
tag: false,
content: ['']
}
}
return node
}
return tree.walk(process)
}
const posthtmlOptions = merge(config, getPosthtmlOptions())
return posthtml([posthtmlPlugin()]).process(html, { ...posthtmlOptions }).then(res => res.html)
}
/**
* Handles custom <plaintext> tags and replaces their content based on the tag name.
*
* @param {Object} options - The options object.
* @param {string} [options.html=''] - The HTML string containing custom tags to be processed.
* @param {Object} [options.config={}] - PostHTML options.
* @returns {string} - The modified HTML string after processing custom tags.
*/
export async function handlePlaintextTags(html = '', config = {}) {
/**
* If the HTML string is empty, return early
*/
if (!html) {
return html
}
const posthtmlPlugin = () => tree => {
const process = node => {
/**
* Remove <plaintext> tags and their content from the HTML
*/
if (node.tag === 'plaintext') {
return {
tag: false,
content: ['']
}
}
/**
* Replace <not-plaintext> tags with their content
*/
if (node.tag === 'not-plaintext') {
return {
tag: false,
content: tree.render(node.content)
}
}
return node
}
return tree.walk(process)
}
const posthtmlOptions = merge(config, getPosthtmlOptions())
return posthtml([posthtmlPlugin()]).process(html, { ...posthtmlOptions }).then(res => res.html)
}
/**
* Generate a plaintext representation from the provided HTML.
*
* @param {Object} options - The options object.
* @param {string} [options.html=''] - The HTML string to convert to plaintext.
* @param {Object} [options.config={}] - Configuration object.
* @returns {Promise<string>|void} - The generated plaintext as a string.
*/
export async function generatePlaintext(html = '', config = {}) {
const { posthtml: posthtmlOptions, ...stripOptions } = config
/**
* Remove <not-plaintext> tags and their content from the HTML.
* `config` is an object containing PostHTML options.
*/
html = await removeTags({ tag: 'not-plaintext', html, config: posthtmlOptions })
/**
* Return the plaintext representation from the stripped HTML.
* The `dumpLinkHrefsNearby` option is enabled by default.
*/
return stripHtml(
html,
merge(
stripOptions,
{
dumpLinkHrefsNearby: {
enabled: true,
},
},
)
).result
}
export async function writePlaintextFile(plaintext = '', config = {}) {
if (!plaintext) {
throw new Error('Missing plaintext content.')
}
if (typeof plaintext !== 'string') {
throw new Error('Plaintext content must be a string.')
}
/**
* Get plaintext output path config, i.e `config.plaintext.destination.path`
*
* Fall back to template's build output path and extension, for example:
* `config.build.output.path`
*/
const plaintextConfig = get(config, 'plaintext')
let plaintextOutputPath = get(plaintextConfig, 'output.path', '')
const plaintextExtension = get(plaintextConfig, 'output.extension', 'txt')
/**
* If `plaintext: true` (either from Front Matter or from config),
* output plaintext file in the same location as the HTML file.
*/
if (plaintextConfig === true) {
plaintextOutputPath = ''
}
/**
* If `plaintext: path/to/file.ext` in the FM
* Can't work if set in config.js as file path, because it would be the same for all templates
* We check later if it's a dir path, won't work if it's a file path
*/
if (typeof plaintextConfig === 'string') {
plaintextOutputPath = plaintextConfig
}
// No need to handle if it's an object, since we already set it to that initially
/**
* If the template has a `permalink` key set in the FM, always output plaintext file there
*/
if (typeof config.permalink === 'string') {
plaintextOutputPath = config.permalink
}
/**
* If `plaintextOutputPath` is a file path, output file there.
*
* The file will be output relative to the project root, and the extension
* doesn't matter, it will be replaced with `plaintextExtension`.
*/
if (path.extname(plaintextOutputPath)) {
// Ensure the target directory exists
await lstat(plaintextOutputPath).catch(async () => {
await mkdir(path.dirname(plaintextOutputPath), { recursive: true })
})
// Ensure correct extension is used
plaintextOutputPath = path.join(
path.dirname(plaintextOutputPath),
path.basename(plaintextOutputPath, path.extname(plaintextOutputPath)) + '.' + plaintextExtension
)
return writeFile(plaintextOutputPath, plaintext)
}
/**
* If `plaintextOutputPath` is a directory path, output file there using the Template's name.
*
* The file will be output relative to the `build.output.path` directory.
*/
const templateFileName = get(config, 'build.current.path.name')
plaintextOutputPath = path.join(
get(config, 'build.current.path.dir'),
plaintextOutputPath,
templateFileName + '.' + plaintextExtension
)
// Ensure the target directory exists
await lstat(path.dirname(plaintextOutputPath)).catch(async () => {
await mkdir(path.dirname(plaintextOutputPath), { recursive: true })
})
return writeFile(plaintextOutputPath, plaintext)
}