mailtrain/client/src/templates/mosaico/mjml-mosaico.js

735 lines
25 KiB
JavaScript

'use strict';
import htmlparser from 'htmlparser2'
import min from 'lodash/min';
import {BodyComponent, HeadComponent, MJML} from "../../lib/mjml";
import shortid from "shortid";
function getId() {
return shortid.generate();
}
const parents = [];
function pushParent(parent) {
parents.push(parent);
}
function popParent() {
parents.pop();
}
function getParent() {
if (parents.length > 0) {
return parents[parents.length - 1];
}
}
function handleMosaicoAttributes(block, src) {
let newSrc = src;
let offset = 0;
const parser = new htmlparser.Parser(
{
onopentag: (name, attrs) => {
const fragment = src.substring(parser.startIndex, parser.endIndex);
const tagAttrsRe = RegExp(`(<\\s*${name})((?:\\s+[a-z0-9-_]+\\s*=\\s*"[^"]*")*)`, 'i');
const [ , tagStr, attrsStr] = fragment.match(tagAttrsRe);
const attrsRe = new RegExp(/([a-z0-9-_]+)\s*=\s*"([^"]*)"/g);
const attrsMatches = attrsStr.matchAll(attrsRe);
let newFragment = tagStr;
for (const attrMatch of attrsMatches) {
if (attrMatch[1] === 'mj-mosaico-editable') {
const propertyId = attrMatch[2];
block.addMosaicoProperty(propertyId);
newFragment += ` data-ko-editable="${propertyId}"`;
} else if (attrMatch[1] === 'mj-mosaico-display') {
const propertyId = attrMatch[2];
block.addMosaicoProperty(propertyId);
newFragment += ` data-ko-display="${propertyId}"`;
} else {
newFragment += ` ${attrMatch[0]}`;
}
}
newSrc = newSrc.substring(0, parser.startIndex + offset) + newFragment + newSrc.substring(parser.endIndex + offset);
offset += newFragment.length - fragment.length;
}
},
{
recognizeCDATA: true,
decodeEntities: false,
recognizeSelfClosing: true,
lowerCaseAttributeNames: false,
}
);
parser.write(src);
parser.end();
return newSrc;
}
class MjMosaicoProperty extends HeadComponent {
static endingTag = true;
static allowedAttributes = {
'property-id': 'string',
label: 'string',
options: 'string',
values: 'string',
type: 'enum(image,link,visible,properties,select)'
};
handler() {
const { add } = this.context;
let extra = '';
const type = this.getAttribute('type');
if (type === 'image') {
extra = `${extra} properties: src url alt;`;
} else if (type === 'link') {
extra = `${extra} properties: text url;`;
} else if (type === 'visible') {
extra = `${extra} extend: visible;`;
} else if (type === 'properties') {
extra = `${extra} properties: ${this.getAttribute('values')};`;
} else if (type === 'select') {
extra = `${extra} widget: select; options: ${this.getAttribute('options')};`;
}
add('style', ` @supports -ko-blockdefs { ${this.getAttribute('property-id')} { label: ${this.getAttribute('label')};${extra} } }`);
}
}
class MjMosaicoContainer extends BodyComponent {
static endingTag = false;
render() {
return `
<div data-ko-container="main" data-ko-wrap="false">
${this.renderChildren()}
</div>
`;
}
}
class MjMosaicoConditionalDisplay extends BodyComponent {
constructor(initialDatas = {}) {
super(initialDatas);
const propertyId = this.getAttribute('property-id');
this.propertyId = propertyId || `display_${getId()}`;
const parentBlock = getParent();
if (parentBlock) {
parentBlock.addMosaicoProperty(this.propertyId);
}
}
componentHeadStyle = breakpoint => {
const label = this.getAttribute('label');
if (label) {
return `
@supports -ko-blockdefs {
${this.propertyId} { label: ${label}; extend: visible }
}
`;
} else {
return '';
}
};
static endingTag = false;
static allowedAttributes = {
'property-id': 'string',
'label': 'string'
};
render() {
const { children } = this.props;
return `
<div
${this.htmlAttributes({
"data-ko-display": this.propertyId,
class: this.getAttribute('css-class'),
'data-ko-block': this.blockId
})}
>
<table
${this.htmlAttributes({
border: '0',
cellpadding: '0',
cellspacing: '0',
role: 'presentation',
style: 'table',
width: '100%',
})}
>
${this.renderChildren(children, {
renderer: component => component.constructor.isRawElement() ? component.render() : `
<tr>
<td
${component.htmlAttributes({
align: component.getAttribute('align'),
'vertical-align': component.getAttribute('vertical-align'),
class: component.getAttribute('css-class'),
style: {
background: component.getAttribute('container-background-color'),
'font-size': '0px',
padding: component.getAttribute('padding'),
'padding-top': component.getAttribute('padding-top'),
'padding-right': component.getAttribute('padding-right'),
'padding-bottom': component.getAttribute('padding-bottom'),
'padding-left': component.getAttribute('padding-left'),
'word-break': 'break-word',
},
})}
>
${component.render()}
</td>
</tr>
`
})}
</table>
</div>
`;
}
}
class MjMosaicoBlock extends BodyComponent {
constructor(initialDatas = {}) {
super(initialDatas);
const blockId = this.getAttribute('block-id');
this.blockId = blockId || `block_${getId()}`;
this.mosaicoProperties = [];
}
componentHeadStyle = breakpoint => {
const propertiesOut = this.mosaicoProperties.length > 0 ? `; properties: ${this.mosaicoProperties.join(' ')}`: '';
return `
@supports -ko-blockdefs {
${this.blockId} { label: ${this.getAttribute('label')}${propertiesOut} }
}
`;
};
static endingTag = false;
static allowedAttributes = {
'block-id': 'string',
'label': 'string'
};
addMosaicoProperty(property) {
this.mosaicoProperties.push(property);
}
render() {
pushParent(this);
const result = `
<div
${this.htmlAttributes({
class: this.getAttribute('css-class'),
'data-ko-block': this.blockId
})}
>
${this.renderChildren()}
</div>
`;
popParent();
return handleMosaicoAttributes(this, result);
}
}
class MjMosaicoInnerBlock extends BodyComponent {
constructor(initialDatas = {}) {
super(initialDatas);
const blockId = this.getAttribute('block-id');
this.blockId = blockId || `block_${getId()}`;
this.mosaicoProperties = [];
}
componentHeadStyle = breakpoint => {
const propertiesOut = this.mosaicoProperties.length > 0 ? `; properties: ${this.mosaicoProperties.join(' ')}`: '';
return `
@supports -ko-blockdefs {
${this.blockId} { label: ${this.getAttribute('label')}${propertiesOut} }
}
`;
};
static endingTag = false;
static allowedAttributes = {
'block-id': 'string',
'label': 'string'
};
addMosaicoProperty(property) {
this.mosaicoProperties.push(property);
}
render() {
const { children } = this.props;
pushParent(this);
const result = `
<div
${this.htmlAttributes({
class: this.getAttribute('css-class'),
'data-ko-block': this.blockId
})}
>
<table
${this.htmlAttributes({
border: '0',
cellpadding: '0',
cellspacing: '0',
role: 'presentation',
style: 'table',
width: '100%',
})}
>
${this.renderChildren(children, {
renderer: component => component.constructor.isRawElement() ? component.render() : `
<tr>
<td
${component.htmlAttributes({
align: component.getAttribute('align'),
'vertical-align': component.getAttribute('vertical-align'),
class: component.getAttribute('css-class'),
style: {
background: component.getAttribute('container-background-color'),
'font-size': '0px',
padding: component.getAttribute('padding'),
'padding-top': component.getAttribute('padding-top'),
'padding-right': component.getAttribute('padding-right'),
'padding-bottom': component.getAttribute('padding-bottom'),
'padding-left': component.getAttribute('padding-left'),
'word-break': 'break-word',
},
})}
>
${component.render()}
</td>
</tr>
`
})}
</table>
</div>
`;
popParent();
return handleMosaicoAttributes(this, result);
}
}
// Adapted from https://github.com/mjmlio/mjml/blob/master/packages/mjml-image/src/index.js
class MjMosaicoImage extends BodyComponent {
constructor(initialDatas = {}) {
super(initialDatas);
const propertyId = this.getAttribute('property-id');
this.propertyId = propertyId || `image_${getId()}`;
const parentBlock = getParent();
if (parentBlock) {
parentBlock.addMosaicoProperty(this.propertyId);
}
}
static tagOmission = true;
static allowedAttributes = {
'property-id': 'string',
'placeholder-height': 'integer',
'href-editable': 'boolean',
alt: 'string',
href: 'string',
name: 'string',
title: 'string',
rel: 'string',
align: 'enum(left,center,right)',
border: 'string',
'border-bottom': 'string',
'border-left': 'string',
'border-right': 'string',
'border-top': 'string',
'border-radius': 'unit(px,%){1,4}',
'container-background-color': 'color',
'fluid-on-mobile': 'boolean',
padding: 'unit(px,%){1,4}',
'padding-bottom': 'unit(px,%)',
'padding-left': 'unit(px,%)',
'padding-right': 'unit(px,%)',
'padding-top': 'unit(px,%)',
target: 'string',
width: 'unit(px)',
height: 'unit(px)',
};
static defaultAttributes = {
align: 'center',
border: '0',
height: 'auto',
padding: '10px 25px',
target: '_blank',
'placeholder-height': '400',
'href-editable': false
};
getStyles() {
const width = this.getContentWidth();
const fullWidth = this.getAttribute('full-width') === 'full-width';
return {
img: {
border: this.getAttribute('border'),
'border-left': this.getAttribute('left'),
'border-right': this.getAttribute('right'),
'border-top': this.getAttribute('top'),
'border-bottom': this.getAttribute('bottom'),
'border-radius': this.getAttribute('border-radius'),
display: 'block',
outline: 'none',
'text-decoration': 'none',
height: this.getAttribute('height'),
'min-width': fullWidth ? '100%' : null,
width: '100%',
'max-width': fullWidth ? '100%' : null,
},
td: {
width: fullWidth ? null : width,
},
table: {
'min-width': fullWidth ? '100%' : null,
'max-width': fullWidth ? '100%' : null,
width: fullWidth ? width : null,
'border-collapse': 'collapse',
'border-spacing': '0px',
},
}
}
getContentWidth() {
const width = this.getAttribute('width')
? parseInt(this.getAttribute('width'), 10)
: Infinity;
const {box} = this.getBoxWidths();
return min([box, width])
}
renderImage() {
const height = this.getAttribute('height');
const img = `
<img
${this.htmlAttributes({
alt: this.getAttribute('alt'),
height: height && (height === 'auto' ? undefined : parseInt(height, 10)),
style: 'img',
title: this.getAttribute('title'),
width: this.getContentWidth(),
'data-ko-editable': this.propertyId + '.src',
'data-ko-placeholder-height': this.getAttribute('placeholder-height'),
src: "[PLACEHOLDER]"
})}
/>
`;
if (this.getAttribute('href-editable')) {
return `
<a
${this.htmlAttributes({
'data-ko-link': this.propertyId + '.url',
href: this.getAttribute('href') || '',
target: this.getAttribute('target'),
rel: this.getAttribute('rel'),
name: this.getAttribute('name'),
})}
>
${img}
</a>
`;
} else if (this.getAttribute('href')) {
return `
<a
${this.htmlAttributes({
href: this.getAttribute('href'),
target: this.getAttribute('target'),
rel: this.getAttribute('rel'),
name: this.getAttribute('name'),
})}
>
${img}
</a>
`;
}
return img
}
headStyle = breakpoint => `
@media only screen and (max-width:${breakpoint}) {
table.full-width-mobile { width: 100% !important; }
td.full-width-mobile { width: auto !important; }
}
`
render() {
return `
<table
${this.htmlAttributes({
border: '0',
cellpadding: '0',
cellspacing: '0',
role: 'presentation',
style: 'table',
class:
this.getAttribute('fluid-on-mobile')
? 'full-width-mobile'
: null,
})}
>
<tbody>
<tr>
<td ${this.htmlAttributes({
style: 'td',
class:
this.getAttribute('fluid-on-mobile')
? 'full-width-mobile'
: null,
})}>
${this.renderImage()}
</td>
</tr>
</tbody>
</table>
`;
}
}
// Adapted from https://github.com/mjmlio/mjml/blob/master/packages/mjml-button/src/index.js
class MjMosaicoButton extends BodyComponent {
constructor(initialDatas = {}) {
super(initialDatas);
const propertyId = this.getAttribute('property-id');
this.propertyId = propertyId || `button_${getId()}`;
const parentBlock = getParent();
if (parentBlock) {
parentBlock.addMosaicoProperty(this.propertyId);
}
}
static endingTag = true;
static allowedAttributes = {
'property-id': 'string',
align: 'enum(left,center,right)',
'background-color': 'color',
'border-bottom': 'string',
'border-left': 'string',
'border-radius': 'string',
'border-right': 'string',
'border-top': 'string',
border: 'string',
color: 'color',
'container-background-color': 'color',
'font-family': 'string',
'font-size': 'unit(px)',
'font-style': 'string',
'font-weight': 'string',
height: 'unit(px,%)',
name: 'string',
'inner-padding': 'unit(px,%)',
'line-height': 'unit(px,%)',
'padding-bottom': 'unit(px,%)',
'padding-left': 'unit(px,%)',
'padding-right': 'unit(px,%)',
'padding-top': 'unit(px,%)',
padding: 'unit(px,%){1,4}',
rel: 'string',
target: 'string',
'text-decoration': 'string',
'text-transform': 'string',
'vertical-align': 'enum(top,bottom,middle)',
'text-align': 'enum(left,right,center)',
width: 'unit(px,%)',
};
static defaultAttributes = {
align: 'center',
'background-color': '#414141',
border: 'none',
'border-radius': '3px',
color: '#ffffff',
'font-family': 'Ubuntu, Helvetica, Arial, sans-serif',
'font-size': '13px',
'font-weight': 'normal',
'inner-padding': '10px 25px',
'line-height': '120%',
padding: '10px 25px',
target: '_blank',
'text-decoration': 'none',
'text-transform': 'none',
'vertical-align': 'middle',
};
getStyles() {
return {
table: {
'border-collapse': 'separate',
width: this.getAttribute('width'),
'line-height': '100%',
},
td: {
border: this.getAttribute('border'),
'border-bottom': this.getAttribute('border-bottom'),
'border-left': this.getAttribute('border-left'),
'border-radius': this.getAttribute('border-radius'),
'border-right': this.getAttribute('border-right'),
'border-top': this.getAttribute('border-top'),
cursor: 'auto',
'font-style': this.getAttribute('font-style'),
height: this.getAttribute('height'),
padding: this.getAttribute('inner-padding'),
'text-align': this.getAttribute('text-align'),
background: this.getAttribute('background-color'),
},
content: {
background: this.getAttribute('background-color'),
color: this.getAttribute('color'),
'font-family': this.getAttribute('font-family'),
'font-size': this.getAttribute('font-size'),
'font-style': this.getAttribute('font-style'),
'font-weight': this.getAttribute('font-weight'),
'line-height': this.getAttribute('line-height'),
Margin: '0',
'text-decoration': this.getAttribute('text-decoration'),
'text-transform': this.getAttribute('text-transform'),
},
}
}
render() {
return `
<table
${this.htmlAttributes({
border: '0',
cellpadding: '0',
cellspacing: '0',
role: 'presentation',
style: 'table',
})}
>
<tr>
<td
${this.htmlAttributes({
align: 'center',
bgcolor:
this.getAttribute('background-color') === 'none'
? undefined
: this.getAttribute('background-color'),
role: 'presentation',
style: 'td',
valign: this.getAttribute('vertical-align'),
})}
>
<a
${this.htmlAttributes({
rel: this.getAttribute('rel'),
name: this.getAttribute('name'),
style: 'content',
target: this.getAttribute('target'),
'data-ko-editable': this.getAttribute('property-id') + '.text',
'data-ko-link': this.getAttribute('property-id') + '.url'
})}
>
${this.getContent()}
</a>
</td>
</tr>
</table>
`;
}
}
const mjmlInstance = new MJML();
mjmlInstance.registerComponent(MjMosaicoContainer);
mjmlInstance.registerComponent(MjMosaicoConditionalDisplay);
mjmlInstance.registerComponent(MjMosaicoBlock);
mjmlInstance.registerComponent(MjMosaicoInnerBlock);
mjmlInstance.registerComponent(MjMosaicoImage);
mjmlInstance.registerComponent(MjMosaicoButton);
mjmlInstance.registerComponent(MjMosaicoProperty);
mjmlInstance.registerDependencies({
'mj-mosaico-container': ['mj-mosaico-block', 'mj-mosaico-inner-block'],
'mj-body': ['mj-mosaico-container', 'mj-mosaico-block'],
'mj-section': ['mj-mosaico-container', 'mj-mosaico-block'],
'mj-column': ['mj-mosaico-container', 'mj-mosaico-inner-block', 'mj-mosaico-image', 'mj-mosaico-button', 'mj-mosaico-conditional-display'],
'mj-mosaico-block': ['mj-section', 'mj-column'],
'mj-mosaico-conditional-display': [
'mj-mosaico-image', 'mj-mosaico-button',
'mj-accordion', 'mj-button', 'mj-carousel', 'mj-divider', 'mj-html', 'mj-image', 'mj-invoice', 'mj-list',
'mj-location', 'mj-raw', 'mj-social', 'mj-spacer', 'mj-table', 'mj-text', 'mj-navbar'
],
'mj-mosaico-inner-block': [
'mj-mosaico-image', 'mj-mosaico-button',
'mj-accordion', 'mj-button', 'mj-carousel', 'mj-divider', 'mj-html', 'mj-image', 'mj-invoice', 'mj-list',
'mj-location', 'mj-raw', 'mj-social', 'mj-spacer', 'mj-table', 'mj-text', 'mj-navbar'
],
'mj-head': ['mj-mosaico-property']
});
mjmlInstance.addToHeader(`
<style type="text/css">
@supports -ko-blockdefs {
visible { label: Visible?; widget: boolean }
color { label: Color; widget: color }
text { label: Paragraph; widget: text }
image { label: Image; properties: src url alt }
link { label: Link; properties: text url }
url { label: Link; widget: url }
src { label: Image; widget: src }
alt {
label: Alternative Text;
widget: text;
help: Alternative text will be shown on email clients that does not download image automatically;
}
template { label: Page }
}
</style>
`);
export default function mjml2html(src) {
return mjmlInstance.mjml2html(src);
}