Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion src/tests/transformers/shorthandCss.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import { describe, it, expect } from 'vitest'
import { describe, it, expect, vi } from 'vitest'
import { shorthandCss, type ShorthandCssOptions } from '../../transformers/shorthandCss.ts'

/**
* Count postcss runs: the transformer builds a fresh postcss
* processor per style value it merges, which calls this
* plugin creator once.
*/
const mergeLonghandCalls = vi.hoisted(() => ({ count: 0 }))

vi.mock('postcss-merge-longhand', async (importOriginal) => {
const { default: plugin } = await importOriginal<typeof import('postcss-merge-longhand')>()
const counted = Object.assign((...args: Parameters<typeof plugin>) => {
mergeLonghandCalls.count++
return plugin(...args)
}, { postcss: true as const })
return { default: counted }
})

function run(html: string, options?: ShorthandCssOptions | boolean): string {
if (options === false) return html
const opts = (options === true || options == null) ? {} : options
Expand All @@ -18,6 +34,33 @@ describe('shorthandCss', () => {
expect(result).not.toContain('margin-left:')
expect(result).not.toContain('margin-right:')
})

it('merges repeated identical style values on every element', () => {
const style = 'padding-top: 4px; padding-right: 8px; padding-bottom: 4px; padding-left: 8px'
const html = `<p style="${style}">A</p><td style="${style}">B</td><!--[if mso]><td style="${style}"><![endif]-->`
const result = shorthandCss(html)
expect(result.match(/padding: 4px 8px/g)).toHaveLength(3)
expect(result).not.toContain('padding-top:')
})

it('runs postcss once per distinct style value', () => {
const a = 'padding-top: 4px; padding-right: 8px; padding-bottom: 4px; padding-left: 8px'
const b = 'margin-top: 2px; margin-right: 2px; margin-bottom: 2px; margin-left: 2px'
const html = `<p style="${a}">A</p><td style="${a}">B</td><div style="${b}">C</div><!--[if mso]><td style="${a}"><![endif]-->`

mergeLonghandCalls.count = 0
shorthandCss(html)
expect(mergeLonghandCalls.count).toBe(2)
})

it('does not reuse cached style values across calls', () => {
const html = '<p style="padding-top: 4px; padding-right: 8px; padding-bottom: 4px; padding-left: 8px">A</p>'

mergeLonghandCalls.count = 0
shorthandCss(html)
shorthandCss(html)
expect(mergeLonghandCalls.count).toBe(2)
})
})

describe('margin shorthand', () => {
Expand Down
19 changes: 14 additions & 5 deletions src/transformers/shorthandCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,25 +99,34 @@ export function shorthandCssDom(dom: ChildNode[], options: ShorthandCssOptions =
const allowedTags = options.tags ?? []
const hasTagFilter = allowedTags.length > 0

/**
* Utility-generated inline styles repeat heavily across elements, so
* each distinct value only goes through postcss once per call.
*/
const cache = new Map<string, string>()

/**
* Merge longhand within a single inline-style value. Returns the merged
* string when shorter, otherwise the original. Wraps the value in a
* dummy selector since postcss-merge-longhand operates on rules.
*/
const mergeStyleValue = (styleValue: string): string => {
let result = cache.get(styleValue)
if (result !== undefined) return result

result = styleValue
try {
const { css } = postcss()
.use(mergeLonghand)
.use(mergeBorder)
.process(`div { ${styleValue} }`, { parser: safeParser })
const match = css.match(/div\s*\{\s*([^}]+)\s*\}/)
if (match && match[1]) {
const merged = match[1].trim()
if (merged !== styleValue) return merged
}
if (match && match[1]) result = match[1].trim()
}
catch {}
return styleValue

cache.set(styleValue, result)
return result
}

walk(dom, (node) => {
Expand Down
Loading