feature(html/mjml): updates/reduction to underlying html - #3059
feature(html/mjml): updates/reduction to underlying html#3059dazgreer wants to merge 21 commits into
Conversation
guigui64
left a comment
There was a problem hiding this comment.
Code looks good to me.
Just one remark on this:
removed all tags [potential small breaking change for any user added CSS that relies on it in a selector]
I think it is a breaking change (not a small one) and you should list it in the release notes.
3842f49 to
3184a11
Compare
d23ff3b to
01e2d5e
Compare
8ca10ab to
1f4e431
Compare
1f4e431 to
9549fec
Compare
9ac8c4a to
09954e2
Compare
- mj-title throws an error if missing or empty - optional Outlook and dark mode support - add space to preview text - comprehensive tidy of HTML to reduce code bloat - updated tests and docs
mjml-core - Updated Prettier to use single quotes inside of CSS - Broken out the accordion CSS into its own style block as it was breaking other CSS and causing non functional components, e.g. carousel in Gmail and updated test mjml-section - fixed issues caused by removing background-size and background-repeat as default attributes whereby the default values were used to determine VML settings. Created automated test - removed multiple declarations of the background color and concatenated two divs that were split because of this - updated table to use role=“none”
cheerio is a webpack external for the browser bundle, so calling load() inside mergeHeadStyleBlocks() crashed the smoke test with 'Cannot read properties of undefined (reading load)'. Replace the cheerio DOM walk with a plain character scanner that tokenises <head> content into plain-style / whitespace / other segments and merges consecutive eligible <style> blocks inline. The merged output is identical; the import of load from cheerio is retained for the mj-html-attributes feature at the call-site that is already correctly guarded by an isEmpty() check. Also fixes no-continue lint errors by using an 'advanced' flag instead of continue statements in the tokenizer loop.
…t tests - extracted mergeHeadStyleBlocks into its own helper module and imported - added test file covering 29 unit tests.
…ibutes - background-color applied to body tag in mj-body - background-color applied to parent table tag instead of both td tags in mj-accordion-title
- now scans mj-wrapper when deciding whether to include the xmlns:v namespace. - now changes the support-outlook-classic="false" attribute to a string before comparing, correctly evaluating to false. Added automated test
- Updated extraction boundaries from tbody to the raw div itself in in mj-raw - Removed fallback fonts from mj-text - Updated indentation outpu Co-authored-by: Copilot <copilot@github.com>
- Refactored expressions for simplicity - Updated printWidth value for Prettier - Removed mj-title validation and testing
…r following rebase The beautify tests were written against the old Prettier formatter. Now that mjml-core uses js-beautify, two fixtures needed updating: - "keeps raw html comment spacing": js-beautify does not insert a space after HTML comments, so expectedBeautified now matches actual output. Added skipFragmentDiffCheck: true since the fragment is unchanged by beautify (the document-level check still asserts overall reformatting). - "wraps long raw html start tags": deleted. js-beautify uses wrap_line_length: 0 by default and does not split long attribute lists across lines. The same input is already covered by the passing standalone it() "keeps long raw html start tags and attributes intact while beautifying". Also deleted a duplicate standalone it() for the comment-spacing case that searched for <tbody> as its start token. MJML never emits that tag inside an mj-raw block, so the test always failed with "Missing start token". Co-authored-by: Copilot <copilot@github.com>
Now that beautification uses js-beautify instead of prettier, prettier-ignore comments serve no purpose and should not appear in rendered output. - Removed the post-render strip of /* prettier-ignore */ and <!-- prettier-ignore --> from mjml-core - Removed /* prettier-ignore */ comments in mj-accordion and mj-carousel
…apple message formatting - Updated column-align default and inheritance from text-align as the colum-align default was overriding the user specified text-align value - removed <meta name="x-apple-disable-message-reformatting"> from skeleton as it's legacy code (iOS9 and bellow) Co-authored-by: Copilot <copilot@github.com>
…ed meta for CSS - reintroduced mso-padding-alt for mj-button with multiline="true" to work the same as the current MJML - added function to remove 1px padding to compensate for the added border - removed <meta name="format-detection"> as it was ineffective. Replaced with CSS - Added regression tests for border comensation fix
- shorthand cover declared in background attribute was not working in Yahoo. Updated to remove and add in its own explicit background-size attribute
- updated docs due to changed column-align default in mj-section - restored default font-size to mj-image so ALT text shows when images are missing - updated mj-preview default characters based onsupport testing - restored defult background-size for mj-section - fixed issue where adding background-url to mj-section causes left padding issues in child mj-column and mj-button and added regression tests
…ormed-head regression test - added explicit bounds checks before positional indexing in head scanner - removed advanced state flag and simplify scanner control flow (lint-safe if/else) - preserved existing merge behavior for plain/attributed style blocks and comments - added regression test for malformed short token in head (<head><!</head>) to ensure no throw and unchanged output - synced compiled helper output with source changes
- restored shorthand 'cover' to fix issue with display in Outlook classic - restored background-color when background-url is set to fix display issue in Gmail/Yahoo on iOS
- changed max-height and max-width to height and width which prevents the spacing characters bleeding into the email body in some email clients
…tons - only emit VML button class and button-specific mso margin rules for left-aligned mj-button instances - added section class vml only when a section emits left-aligned button VML rules - kept non-left-aligned button sections free of unnecessary VML hooks - removed mj-outlook-group-fix class emission from column output and align section gutter expectations - add regression coverage for section/button Outlook behavior and template syntax sanitization under minify with mixed multiline/property/value tokens - clean up unused import in mj-hero
351710b to
1b4dd1d
Compare
Merges feature/html-updates-and-tidy improvements into feature/dark-mode-support: - Cherry-Picked Commits: - f35a14f - fix(mj-preview): updated CSS to remove space in email body - 1b4dd1d - fix(mj-section): scope Outlook VML button helpers to left-aligned buttons - Manually updates - 846bcc1 - fix(eslint): remove unused widthParser import from mjml-hero - bcc63dd - fix(mj-section): restored background-color with background-url and fix undefined background-size - 435ff0c - fix(mj-button/format-detection): reintroduced mso-padding-alt / swapped meta for CSS Plus additional changes to mjml-core, mjml-head-preview, and mjml-image that are currently staged. This commit completes the merge of feature/html-updates-and-tidy into feature/dark-mode-support with all PR #3059 changes applied and tested.
…e-support Merge PR #3059 HTML optimizations into dark-mode-support branch. Resolved merge conflicts: - mjml-carousel: Accepted refactored CSS structure + fix hover selector scoping - Changed `.mj-carousel-thumbnail:hover` to `.mj-carousel-${ID}-thumbnail:hover` - Preserved dark-mode hooks - mjml-section, mjml-social, mjml-hero, mjml-image: Already integrated in prior commits - All other conflicts resolved with incoming
…e-support Merge PR #3059 HTML optimizations into dark-mode-support branch. Resolves merge conflicts: - mjml-carousel: Accept refactored CSS structure + fix hover selector scoping - Changed `.mj-carousel-thumbnail:hover` to `.mj-carousel-${ID}-thumbnail:hover` - Preserves dark-mode hooks and regression test compliance - All other conflicted files: Resolved by keeping feature/dark-mode-support versions - README conflicts: Resolved across 12+ component packages All 283 tests passing.
There was a problem hiding this comment.
Pull request overview
This PR reduces generated email HTML, introduces configurable Outlook/dark-mode support, and updates component rendering, documentation, and regression coverage.
Changes:
- Simplifies generated markup, styles, conditional comments, and table structures.
- Adds Outlook-classic, dark-mode, preview-spacing, alignment, and button options.
- Updates component defaults, compatibility fixes, documentation, and tests.
Reviewed changes
Copilot reviewed 61 out of 61 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
packages/mjml/test/utils.js |
Safely extracts CSS properties. |
packages/mjml/test/template-syntax-sanitization.test.js |
Tests mixed template syntax. |
packages/mjml/test/tableWidth.test.js |
Updates table-width expectations. |
packages/mjml/test/support-outlook-classic.test.js |
Tests Outlook option parsing. |
packages/mjml/test/section-gutter.test.js |
Updates reduced column markup expectations. |
packages/mjml/test/section-button-outlook-margin.test.js |
Tests VML button alignment. |
packages/mjml/test/section-background-url-no-background-size.test.js |
Tests section background defaults. |
packages/mjml/test/button-inner-padding-border.test.js |
Tests button border compensation. |
packages/mjml/test/beautify-output.test.js |
Updates beautification fixtures. |
packages/mjml-wrapper/src/index.js |
Uses shared MSO helpers. |
packages/mjml-wrapper/README.md |
Updates wrapper defaults. |
packages/mjml-validator/src/rules/validTag.js |
Permits root mjml validation. |
packages/mjml-validator/src/index.js |
Validates formerly skipped roots. |
packages/mjml-text/src/index.js |
Simplifies text and Outlook markup. |
packages/mjml-text/README.md |
Updates text defaults. |
packages/mjml-table/src/index.js |
Omits redundant table widths/layouts. |
packages/mjml-table/README.md |
Updates table defaults. |
packages/mjml-social/src/SocialElement.js |
Reduces social-element markup. |
packages/mjml-social/src/Social.js |
Simplifies social tables and conditionals. |
packages/mjml-social/README.md |
Updates social defaults. |
packages/mjml-section/src/index.js |
Adds alignment and VML handling changes. |
packages/mjml-section/README.md |
Documents section API changes. |
packages/mjml-parser-xml/test/preprocessors.test.js |
Adds a title to preprocessing coverage. |
packages/mjml-navbar/src/NavbarLink.js |
Simplifies Outlook link rendering. |
packages/mjml-navbar/src/Navbar.js |
Reduces and deduplicates hamburger output. |
packages/mjml-navbar/README.md |
Updates navbar defaults. |
packages/mjml-image/src/index.js |
Reduces image markup and conditional CSS. |
packages/mjml-image/README.md |
Updates image target defaults. |
packages/mjml-hero/src/index.js |
Simplifies hero and VML markup. |
packages/mjml-hero/README.md |
Updates hero defaults. |
packages/mjml-head-title/README.md |
Adds title-validation documentation. |
packages/mjml-head-preview/src/index.js |
Adds preview filler options. |
packages/mjml-head-preview/README.md |
Documents preview filler attributes. |
packages/mjml-group/src/index.js |
Simplifies group markup and classes. |
packages/mjml-group/README.md |
Updates group defaults. |
packages/mjml-divider/src/index.js |
Selects table or paragraph rendering. |
packages/mjml-core/tests/mergeHeadStyleBlocks.test.js |
Tests style-block merging. |
packages/mjml-core/src/index.js |
Initializes options and post-processes styles. |
packages/mjml-core/src/helpers/styles.js |
Consolidates generated style blocks. |
packages/mjml-core/src/helpers/skeleton.js |
Reduces skeleton and adds feature flags. |
packages/mjml-core/src/helpers/mergeOutlookConditionnals.js |
Supports updated MSO conditionals. |
packages/mjml-core/src/helpers/mergeHeadStyleBlocks.js |
Merges adjacent head styles. |
packages/mjml-core/src/helpers/mediaQueries.js |
Consolidates media-query CSS. |
packages/mjml-core/src/helpers/fonts.js |
Uses CSS-only font imports. |
packages/mjml-core/src/helpers/conditionalTag.js |
Adds configurable Outlook conditionals. |
packages/mjml-core/src/createComponent.js |
Omits empty and false attributes. |
packages/mjml-column/src/index.js |
Reduces columns and adds VML corrections. |
packages/mjml-column/README.md |
Updates column defaults. |
packages/mjml-carousel/src/CarouselImage.js |
Simplifies carousel images and targets. |
packages/mjml-carousel/src/Carousel.js |
Deduplicates carousel CSS and markup. |
packages/mjml-carousel/README.md |
Updates carousel target defaults. |
packages/mjml-button/src/index.js |
Changes button sizing and multiline behavior. |
packages/mjml-button/README.md |
Documents button API changes. |
packages/mjml-body/src/index.js |
Simplifies body markup and language output. |
packages/mjml-body/src/helpers/preview.js |
Updates hidden preview sizing. |
packages/mjml-accordion/src/AccordionTitle.js |
Consolidates title styles. |
packages/mjml-accordion/src/AccordionText.js |
Reduces accordion text markup. |
packages/mjml-accordion/src/AccordionElement.js |
Removes redundant element styles. |
packages/mjml-accordion/src/Accordion.js |
Consolidates accordion CSS and markup. |
packages/mjml-accordion/README.md |
Updates accordion defaults. |
doc/components_1.md |
Updates generated HTML and root options. |
Suppressed comments (3)
packages/mjml-section/src/index.js:459
- Section padding accepts percentages, but
getShorthandAttrValueparses away the unit and this rebuilds every value as pixels. For example,padding="10%"becomes a VML inset of10pxwhile Outlook's normal padding is disabled, producing substantially different spacing. Preserve the declared units or calculate the percentage against the relevant dimensions.
packages/mjml-column/src/index.js:534 - The same unit loss affects button outer padding:
padding-left="25%"is emitted in the VML correction rule as25px. Since%is allowed for button padding, left-aligned buttons in background sections receive the wrong Outlook margin. Preserve the unit or resolve the percentage against the button container.
if (isLeftAlignedButton && hasSectionBackground) {
const buttonClassName = `vml-button-${genRandomHexString(6)}`
const buttonLeftPadding = `${component.getShorthandAttrValue('padding', 'left')}px`
packages/mjml-button/src/index.js:122
- This second
mso-padding-altdeclaration also survivessupport-outlook-classic="false". Apply the same global support check here so the option consistently removes Outlook-only CSS.
...(this.getAttribute('multiline') === true && { 'mso-padding-alt': '0px' }),
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| const tokens = [] | ||
| let pos = 0 | ||
| while (pos < headInner.length) { |
There was a problem hiding this comment.
I've updated so that scanner emits malformed remaining input and sets pos = headInner.length for:
- Unterminated <!-- comments
- Unterminated plain <style>
- Attributed <style> without a closing tag
- Attributed <style ... without a closing >
Also added regression testing for these cases
| } else if (nt.type === 'plain-style') { | ||
| combinedCss += `\n${nt.css}` |
There was a problem hiding this comment.
I've updated to detect a later plain <style> block whose CSS begins with @import or @charset and keeps it separate instead of concatenating it after earlier CSS.
Added regression tests for both cases
| if (globalData && includeSharedStyles) { | ||
| globalData.carouselSharedStylesEmitted = true | ||
| } | ||
|
|
||
| const carouselCss = ` | ||
| if (!length) return '' |
There was a problem hiding this comment.
I've updated to check length and returns before setting globalData.carouselSharedStylesEmitted = true.
Added regression test for this case
| const icoFontFamilySet = | ||
| this.props && this.props.rawAttrs && | ||
| Object.prototype.hasOwnProperty.call(this.props.rawAttrs, 'ico-font-family') |
There was a problem hiding this comment.
Updated to preserve resolved global and class attributes in rawAttrs.
Added regression testing for both cases to demonstrate inheritance
| ...(this.context.hasSectionBackgroundUrl === true && { | ||
| 'mso-para-margin-left': `${this.getShorthandAttrValue('padding', 'left')}px`, | ||
| }), |
There was a problem hiding this comment.
I've updated to add a unit-preserving getPaddingLeft() function and applied it in two instances
- the column gutter preserves
%instead of appendingpx. - the left aligned button VML margin
Added test verifying both of these
| - The validator emits a warning when `mj-title` does not exist or is empty or whitespace-only. | ||
|
|
||
| Note: MJML always outputs a `<title>` element in the final HTML. If you omit `mj-title`, the `<title>` is included but may be empty; provide a meaningful `mj-title` to avoid empty titles. | ||
|
|
||
| In projects using `validationLevel: 'strict'`, this warning becomes an error and will stop rendering. |
There was a problem hiding this comment.
I've removed this from the docs. I initially added validation but removed it as overreach.
| } | ||
|
|
||
| static defaultAttributes = { | ||
| 'fill-space': '0', |
There was a problem hiding this comment.
I haven't added space by default (to prevent a breaking change) but this does gives the user control over adding it.
I have normalised negative values to 0, added regression testing and updated documentation for clarity
| 'padding-right': this.getAttribute('padding-right'), | ||
| 'padding-top': this.getAttribute('padding-top'), | ||
| 'text-align': this.getAttribute('text-align'), | ||
| ...(hasBackground && { 'mso-padding-alt': '0' }), |
There was a problem hiding this comment.
I've updated to include supportOutlookClassic from the global context and emit mso-padding-alt:0 only when it's not set to false.
line 459 doesn't contain an mso-padding-alt declaration.
| 'font-style': this.getAttribute('font-style'), | ||
| height: this.getAttribute('height'), | ||
| 'mso-padding-alt': this.getAttribute('inner-padding'), | ||
| ...(this.getAttribute('multiline') === true && { 'mso-padding-alt': this.getContentPadding() }), |
There was a problem hiding this comment.
I've updated to gate the mso-padding-alt declaration on both multiline="true" AND supportOutlookClassic in two instances.
Added regression testing for this case
- Updated mergeHeadStyleBlocks so that scanner emits malformed remaining input and sets `pos = headInner.length` for various cases, thus preventing malformed. Added regression testing. - Updated `mj-carousel` to check `length` and returns before setting `globalData.carouselSharedStylesEmitted = true` to prevent empty carousel from omitting shared carousel CSS. Added regression testing - Updated to `mj-navbar` to preserve resolved global and class attributes in `rawAttrs`. Added regression testing - Updated `mj-column` to add a unit-preserving `getPaddingLeft()` function and applied it in two instances. Added regression testing - Removed `mj-title` validation explanation from docs as validation was perviously removed - Normalised negative values of `fill-space` to `0` for `mj-head-preview`. Added regression testing and updated documentation for clarity - Updated CSS reset in skeleton so Outlook rule is only emitted when `supportOutlookClassic` isn't enabled. - Updated `mj-section` to include `supportOutlookClassic` from the global context and emit `mso-padding-alt:0` only when it's not set to `false`. - Updated `mj-button` to gate the `mso-padding-alt` declaration on both `multiline="true"` AND `supportOutlookClassic` in two instances. Added regression testing
Highlights
mj-titlethrows an error if missing or emptyDetails
Features
mj-titlethrows an error if missing or empty (added test, updated doc)support-outlook-classicoption tomjmltag to remove support for Outlook (removes ghost tables and other Outlook specific code. Set to `true by default) (updated doc)mj-previewto add blank space after preview text (updated doc) [Fixes Create automatically white space after <mj-preview> text #1829]HTML changes
General
class=“”orstyle=“”| IEfrom all conditional commentshtml
xmlnsattribute as no longer neededxmlns:oattribute whensupport-outlook-classic=“true"xmlns:vifbackground-urlis present onmj-heroormj-sectionmeta
charsettagX-UA-Compatibletag as no longer usefuluser-scalable=yesadded format detection tags to prevent clients from creating links from dates, contact details etcsupport-dark-modeoption (on<mjml>tag) to allow dark mode supporthead (skeleton)
.mj-outlook-group-fixCSS as only applies to older unsupported Outlook versions (2003 and less)-msprefixed reset styles whensupport-outlook-classic="false"-webkit-text-size-adjusttotext-size-adjustmarginfrom13pxto1em<link>font import, now uses just@importCSS:rootCSS behindsupport-dark-modeoption (on<mjml>tag) to allow dark mode supportstyle (skeleton)
type=“text/css”as no longer required<style>blocks where possiblemj-column-per-100as it’s declared by default in the tagtable
role=“presentation”torole=“none”<tbody>tags [breaking change for any user added CSS that relies on it in a selector]font-family declarations
HelveticaandArialas fallbacks [small breaking change as fonts will change to system default sans-serif font]mj-body
xml:lang=“”(populated from globalData language, declared onmjmltag)word-spacingas declared on the childdivmj-accordion
border-bottomdeclaration to0and now only outputs when border is not already set to0ornonevertical-alignas it’s default to the tag<style>block as it broke other things in Gmailmj-accordion-element
paddingattribute as it’s default to the tagmj-accordion-title
background-colorattribute to parenttabletag rather than bothtdtagsmj-button
text-transform,font-weightattributes as default to the tagtargetattribute as not necessary in emaildisplaytoblockand removedmso-padding-altdeclaration to solve issue where the full button is not clickable when thewidthis setmultilineoption to allow users to negate issues with the above in Outlook classic when button text wrapscursor: autoset on<td>andmargin: 0set on<a>mj-carousel
prettier-ignorecomment to fallback image as Prettier was breaking itmso-hide-allattributes as the carousel is not displayed in Outlookborder-radiusfrom Outlook fallback image as Outlook does not display ittargeton links andheighton image as not needed.[owa]class as no longer usedmj-carousel-image
targetattributecheckedfor first input rather thanchecked="checked"mj-column
directionattribute as default to the tagvertical-aligndeclaration fromtableas set in childrenmj-outlook-group-fixclass as only applies to older unsupported Outlook versions (2003 and less)mj-divider
tableorp(based onsupport-outlook-classic) and only outputs onepxunits for html tablewidthas they don’t workmax-widthto bothtableandpto better handle widthmj-group
directionattribute as default to the tag'text-align': 'left'from div as defaultmj-outlook-group-fixclass as only applies to older unsupported Outlook versions (2003 and less)mj-hero
background-urlandpaddingdefaultsbackground-repeatdeclaration inbackgroundshorthand valuebackground-positiondeclaration to singlecenter</td>tags for fluid height hero as self-closing tag is not validvertical-aligndeclaration from<tr>tagmj-image
fluid-on-mobileCSS when not settdand changed CSS selector to accommodatetext-decoration,outlineandas attributes as default to the tagfont-sizetargetattribute as not necessary in emailmj-navbar
font-family/font-size/line-heightwhen declaring inmj-attributes>mj-all. Previously it was using its ownico-text-decorationattribute as default to the tag'mso-hide': 'all'and'-moz-user-select': 'none'as not requiredmj-navbar-link
font-weightas attribute as default to the tagtargetattribute as not necessary in emailmj-section
direction,background-repeatandas attributes as default to the tagbackground-sizebackground-coloror color reference inbackgroundas unnecessarily declared multiple timestext-alignoption tocolumn-alignas it was poorly named. Aliased the deprecated option for backwards compatibility [updated docs]mj-social
targetattribute as not necessary in emailwidthdeclarations [Fixes MJ-SOCIAL - Outlook app android/iOS interpret wrong the icon-padding. #3043]mj-social-element
text-alignandvertical-alignas attributes as default to the tagmj-table
widthdeclaration when user sets toautoas default to tagtable-layoutattribute as default to the tag. If the user adds, it will also not be be written outmj-wrapper
text-alignfrom docs as it doesn't do anything