@@ -30,9 +30,25 @@ const DEFAULT_ENTITIES: Record<string, string> = {
3030}
3131
3232/**
33- * Replace literal Unicode characters in text nodes with their HTML entity
34- * equivalents (zero-width joiners, non-breaking spaces, smart quotes,
35- * dashes, etc.) for better email-client rendering.
33+ * Node types {@link entitiesDom} encodes. Both default to `true`.
34+ */
35+ export interface EntitiesScope {
36+ /** Encode entities in text nodes */
37+ text ?: boolean
38+ /**
39+ * Encode entities in comment nodes. MSO conditional comment content is
40+ * rendered by Outlook, and comment data survives parse round-trips
41+ * un-decoded — so encoding here protects whitespace-only conditionals
42+ * (e.g. `<Outlook> </Outlook>` spacers) from being collapsed
43+ * or removed by email-comb and html-crush downstream.
44+ */
45+ comments ?: boolean
46+ }
47+
48+ /**
49+ * Replace literal Unicode characters in text and comment nodes with their
50+ * HTML entity equivalents (zero-width joiners, non-breaking spaces, smart
51+ * quotes, dashes, etc.) for better email-client rendering.
3652 *
3753 * @param html HTML string to transform.
3854 * @param custom Extra entries merged on top of the built-in entity map, or
@@ -52,24 +68,27 @@ const DEFAULT_ENTITIES: Record<string, string> = {
5268 * // Disable the transform
5369 * entities('hello world', false)
5470 */
55- export function entities ( html : string , custom : EntitiesConfig = true ) : string {
56- return serialize ( entitiesDom ( parse ( html ) , custom ) )
71+ export function entities ( html : string , custom : EntitiesConfig = true , scope : EntitiesScope = { } ) : string {
72+ return serialize ( entitiesDom ( parse ( html ) , custom , scope ) )
5773}
5874
5975/**
6076 * DOM-form of {@link entities} used by the internal transformer pipeline.
6177 * Takes a parsed DOM, returns a parsed DOM — avoids redundant
6278 * serialize/parse round-trips when chained with other transformers.
6379 */
64- export function entitiesDom ( dom : ChildNode [ ] , custom : EntitiesConfig = true ) : ChildNode [ ] {
80+ export function entitiesDom ( dom : ChildNode [ ] , custom : EntitiesConfig = true , scope : EntitiesScope = { } ) : ChildNode [ ] {
6581 if ( ! custom ) return dom
6682
6783 const map = typeof custom === 'object'
6884 ? merge ( custom as Record < string , string > , DEFAULT_ENTITIES )
6985 : DEFAULT_ENTITIES
7086
7187 walk ( dom , ( node ) => {
72- if ( node . type === 'text' ) {
88+ if (
89+ ( node . type === 'text' && scope . text !== false )
90+ || ( node . type === 'comment' && scope . comments !== false )
91+ ) {
7392 for ( const [ char , entity ] of Object . entries ( map ) ) {
7493 node . data = node . data . split ( char ) . join ( entity )
7594 }
0 commit comments