Skip to content

Commit

Permalink
feat: preservedSelectors option for inliner
Browse files Browse the repository at this point in the history
  • Loading branch information
cossssmin committed Nov 19, 2024
1 parent 2917c80 commit 3598cf7
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 23 deletions.
49 changes: 26 additions & 23 deletions src/transformers/inline.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import juice from 'juice'
import postcss from 'postcss'
import get from 'lodash-es/get.js'
import has from 'lodash-es/has.js'
import { defu as merge } from 'defu'
import * as cheerio from 'cheerio/slim'
import remove from 'lodash-es/remove.js'
import { render } from 'posthtml-render'
Expand Down Expand Up @@ -33,6 +34,26 @@ export async function inline(html = '', options = {}) {
options.removeInlinedSelectors = get(options, 'removeInlinedSelectors', true)
options.resolveCalc = get(options, 'resolveCalc', true)
options.preferUnitlessValues = get(options, 'preferUnitlessValues', true)
options.preservedSelectors = new Set([
...get(options, 'preservedSelectors', []),
...[
'.body', // Gmail
'.gmail', // Gmail
'.apple', // Apple Mail
'.ios', // Mail on iOS
'.ox-', // Open-Xchange
'.outlook', // Outlook.com
'[data-ogs', // Outlook.com
'.bloop_container', // Airmail
'.Singleton', // Apple Mail 10
'.unused', // Notes 8
'.moz-text-html', // Thunderbird
'.mail-detail-content', // Comcast, Libero webmail
'edo', // Edison (all)
'#msgBody', // Freenet uses #msgBody
'.lang' // Fenced code blocks
],
])

juice.styleToAttribute = get(options, 'styleToAttribute', {})
juice.applyWidthAttributes = get(options, 'applyWidthAttributes', true)
Expand Down Expand Up @@ -106,26 +127,8 @@ export async function inline(html = '', options = {}) {
}
)

const preservedClasses = new Set([
'.body', // Gmail
'.gmail', // Gmail
'.apple', // Apple Mail
'.ios', // Mail on iOS
'.ox-', // Open-Xchange
'.outlook', // Outlook.com
'[data-ogs', // Outlook.com
'.bloop_container', // Airmail
'.Singleton', // Apple Mail 10
'.unused', // Notes 8
'.moz-text-html', // Thunderbird
'.mail-detail-content', // Comcast, Libero webmail
'edo', // Edison (all)
'#msgBody', // Freenet uses #msgBody
'.lang' // Fenced code blocks
])

// Precompile a single regex to match any substring from the preservedClasses set
const combinedPattern = Array.from(preservedClasses)
const combinedPattern = Array.from(options.preservedSelectors)
.map(pattern => pattern.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')) // Escape special regex chars
.join('|') // Combine all patterns into a single regex pattern with 'OR' (|)

Expand All @@ -137,7 +140,7 @@ export async function inline(html = '', options = {}) {
root.walkAtRules(rule => {
if (['media', 'supports'].includes(rule.name)) {
rule.walkRules(rule => {
preservedClasses.add(rule.selector)
options.preservedSelectors.add(rule.selector)
})
}
})
Expand Down Expand Up @@ -185,12 +188,12 @@ export async function inline(html = '', options = {}) {
// Preserve pseudo selectors
// TODO: revisit pseudos list
if ([':hover', ':active', ':focus', ':visited', ':link', ':before', ':after'].some(i => selector.includes(i))) {
preservedClasses.add(selector)
options.preservedSelectors.add(selector)
}

if (options.removeInlinedSelectors) {
// Remove the rule in the <style> tag as long as it's not a preserved class
if (!preservedClasses.has(selector) && !combinedRegex.test(selector)) {
if (!options.preservedSelectors.has(selector) && !combinedRegex.test(selector)) {
rule.remove()
}

Expand Down Expand Up @@ -249,7 +252,7 @@ export async function inline(html = '', options = {}) {
// If the class has been inlined in the style attribute...
if (has(inlineStyles, prop)) {
// Try to remove the classes that have been inlined
if (![...preservedClasses].some(item => item.endsWith(name) || item.startsWith(name))) {
if (![...options.preservedSelectors].some(item => item.includes(name))) {
remove(classList, classToRemove => name.includes(classToRemove))
}

Expand Down
23 changes: 23 additions & 0 deletions test/transformers/inlineCSS.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,29 @@ describe.concurrent('Inline CSS', () => {
</table>`))
})

test('Preserves user-defined selectors', async () => {
const result = await inlineCSS(`
<style>
.bar {margin: 0}
.variant\\:foo {color: blue}
</style>
<p class="bar">test</p>
<span class="variant:foo"></span>`,
{
removeInlinedSelectors: true,
preservedSelectors: ['foo', '.bar'],
})

expect(cleanString(result)).toBe(cleanString(`
<style>
.bar {margin: 0}
.variant\\:foo {color: blue}
</style>
<p class="bar" style="margin: 0">test</p>
<span class="variant:foo" style="color: blue"></span>`
))
})

test('Preserves inlined selectors', async () => {
const result = await inlineCSS(html, {
removeInlinedSelectors: false,
Expand Down

0 comments on commit 3598cf7

Please sign in to comment.