From 9c10bcdc77794565e50a70d1ddde9df84debf82b Mon Sep 17 00:00:00 2001 From: Maxime Brazeilles Date: Fri, 18 Mar 2016 16:29:37 +0100 Subject: [PATCH 001/226] MJML-core almost alive... --- bench/index.js | 14 +- package.json | 3 +- src/Error.js | 4 - src/MJMLElementsCollection.js | 39 +-- src/MJMLRenderer.js | 77 ++++++ src/README.MD | 2 +- src/cli.js | 4 +- src/components/Body.js | 49 ---- src/components/Button.js | 106 -------- src/components/Column.js | 90 ------- src/components/Divider.js | 67 ----- src/components/Html.js | 41 --- src/components/Image.js | 111 -------- src/components/Invoice/Item.js | 67 ----- src/components/Invoice/index.js | 145 ---------- src/components/List.js | 57 ---- src/components/Location.js | 76 ------ src/components/Raw.js | 36 --- src/components/Section.js | 125 --------- src/components/Social.js | 262 ------------------- src/components/Table.js | 50 ---- src/components/Text.js | 58 ---- src/components/decorators/MJMLElement.js | 5 +- src/documentParser.js | 23 +- src/{mjml2html.js => helpers/post_render.js} | 86 +----- src/index.js | 2 +- test/input-output.spec.js | 14 +- 27 files changed, 127 insertions(+), 1486 deletions(-) create mode 100644 src/MJMLRenderer.js delete mode 100644 src/components/Body.js delete mode 100644 src/components/Button.js delete mode 100644 src/components/Column.js delete mode 100644 src/components/Divider.js delete mode 100644 src/components/Html.js delete mode 100644 src/components/Image.js delete mode 100644 src/components/Invoice/Item.js delete mode 100644 src/components/Invoice/index.js delete mode 100644 src/components/List.js delete mode 100644 src/components/Location.js delete mode 100644 src/components/Raw.js delete mode 100644 src/components/Section.js delete mode 100644 src/components/Social.js delete mode 100644 src/components/Table.js delete mode 100644 src/components/Text.js rename src/{mjml2html.js => helpers/post_render.js} (68%) diff --git a/bench/index.js b/bench/index.js index 2762de432..0935e7403 100644 --- a/bench/index.js +++ b/bench/index.js @@ -1,11 +1,11 @@ -var Benchmark = require('benchmark') -var fs = require('fs') -var mjml = require('../lib/index') -var path = require('path') -var template = fs.readFileSync(path.resolve(__dirname, './template.mjml')).toString() +const Benchmark = require('benchmark') +const fs = require('fs') +const { MJMLRenderer } = require('../lib/index') +const path = require('path') +const template = fs.readFileSync(path.resolve(__dirname, './template.mjml')).toString() -var bench = new Benchmark('mjml2html', function () { - mjml.mjml2html(template) +const bench = new Benchmark('mjml2html', function () { + new MJMLRenderer(template).render() }, { minSamples: 100, onComplete: function (e) { diff --git a/package.json b/package.json index 0d7d3c694..7794dc868 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,8 @@ "lodash": "^4.6.1", "numeral": "^1.5.3", "react": "^0.14.7", - "react-dom": "^0.14.7" + "react-dom": "^0.14.7", + "warning": "^2.1.0" }, "browser": { "cheerio": false, diff --git a/src/Error.js b/src/Error.js index 22a200888..bab634d0d 100644 --- a/src/Error.js +++ b/src/Error.js @@ -39,10 +39,6 @@ export const EmptyMJMLError = error('EmptyMJMLError', 2) */ export const NullElementError = error('EmptyMJMLError', 3) -/* - * When encounter an unknown MJML Element while transpiling - */ -export const UnknownMJMLElement = error('UnknownMJMLElement', 5) /* * TODO: Warnings diff --git a/src/MJMLElementsCollection.js b/src/MJMLElementsCollection.js index 90e2724b4..8d0038208 100644 --- a/src/MJMLElementsCollection.js +++ b/src/MJMLElementsCollection.js @@ -1,39 +1,8 @@ -import Body from './components/Body' -import Button from './components/Button' -import Column from './components/Column' -import Divider from './components/Divider' -import Html from './components/Html' -import Image from './components/Image' -import Invoice from './components/Invoice' -import InvoiceItem from './components/Invoice/Item' -import List from './components/List' -import Location from './components/Location' -import Raw from './components/Raw' -import Section from './components/Section' -import Social from './components/Social' -import Text from './components/Text' - -const MJMLStandardElements = { - 'body': Body, - 'button': Button, - 'column': Column, - 'divider': Divider, - 'html': Html, - 'image': Image, - 'invoice-item': InvoiceItem, - 'invoice': Invoice, - 'list': List, - 'location': Location, - 'raw': Raw, - 'section': Section, - 'social': Social, - 'text': Text -} - -export const endingTags = ['mj-text', 'mj-html', 'mj-button', 'mj-list', 'mj-raw', 'mj-table', 'mj-invoice-item', 'mj-location'] +const MJMLElements = {} +export const endingTags = [] export const registerElement = (tagName, element, options = {}) => { - MJMLStandardElements[tagName] = element + MJMLElements[tagName] = element if (options.endingTag) { endingTags.push(`mj-${tagName}`) @@ -42,4 +11,4 @@ export const registerElement = (tagName, element, options = {}) => { return true } -export default MJMLStandardElements +export default MJMLElements diff --git a/src/MJMLRenderer.js b/src/MJMLRenderer.js new file mode 100644 index 000000000..262f83bfd --- /dev/null +++ b/src/MJMLRenderer.js @@ -0,0 +1,77 @@ +import { EmptyMJMLError } from './Error' +import { html as beautify } from 'js-beautify' +import { minify } from 'html-minifier' +import { parseInstance } from './helpers/mjml' +import defaultContainer from './configs/defaultContainer' +import documentParser from './documentParser' +import dom from './helpers/dom' +import {insertColumnMediaQuery, fixLegacyAttrs, fixOutlookLayout, clean, removeCDATA } from './helpers/post_render' +import getFontsImports from './helpers/getFontsImports' +import MJMLElementsCollection from './MJMLElementsCollection' +import React from 'react' +import ReactDOMServer from 'react-dom/server' + +const debug = require('debug')('mjml-engine/mjml2html') + +export default class MJMLRenderer { + constructor(content, options) { + this.content = content + this.options = options + + if (typeof this.content == 'string') { + this.parseDocument() + } + } + + parseDocument() { + debug('Start parsing document') + this.content = documentParser(this.content) + debug('Content parsed.') + } + + render() { + if (!this.content) { + throw new EmptyMJMLError(`.render: No MJML to render in options ${this.options.toString()}`) + } + + const rootElemComponent = React.createElement(MJMLElementsCollection[this.content.tagName.substr(3)], { mjml: parseInstance(this.content) }) + + debug('Render to static markup') + const renderedMJML = ReactDOMServer.renderToStaticMarkup(rootElemComponent) + + debug('React rendering done. Continue with special overrides.') + + const MJMLDocument = defaultContainer({ title: this.options.title, content: renderedMJML, fonts: getFontsImports({ content: renderedMJML }) }) + + return this._postRender(MJMLDocument) + } + + _postRender(MJMLDocument) { + let $ = dom.parseHTML(MJMLDocument) + + $ = insertColumnMediaQuery(this.$) + $ = fixLegacyAttrs(this.$) + $ = fixOutlookLayout(this.$) + $ = clean(this.$) + + let finalMJMLDocument = dom.getHTML($) + finalMJMLDocument = removeCDATA(MJMLDocument) + + if (this.options.beautify && beautify) { + finalMJMLDocument = beautify(finalMJMLDocument, { + indent_size: 2, + wrap_attributes_indent_size: 2 + }) + } + + if (this.options.minify && minify) { + finalMJMLDocument = minify(finalMJMLDocument, { + collapseWhitespace: true, + removeEmptyAttributes: true, + minifyCSS: true + }) + } + + return finalMJMLDocument + } +} diff --git a/src/README.MD b/src/README.MD index d27b44517..5826fd77f 100644 --- a/src/README.MD +++ b/src/README.MD @@ -9,7 +9,7 @@ The `mjml-engine` is the core of the MJML renderer. It exposes the MJML parser a The engine is composed of multiple parts: - [`documentParser`](https://github.com/mjmlio/mjml/blob/master/src/documentParser.js) : Parse the markup MJML string and return a JSON representation - - [`mjml2html`](https://github.com/mjmlio/mjml/blob/master/src/mjml2html.js) : Turn a JSON MJML representation and returns an HTML string using react components + - [`MJMLRenderer`](https://github.com/mjmlio/mjml/blob/master/src/MJMLRenderer.js) : Turn a JSON MJML representation and returns an HTML string using react components - [`MJMLElementsCollection`](https://github.com/mjmlio/mjml/blob/master/src/MJMLElementsCollection.js) : Contains the standard MJML elements exposed to the API ## Changelog diff --git a/src/cli.js b/src/cli.js index 776429ab0..bd07f8244 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,4 +1,4 @@ -import { mjml2html, version as V } from './index' +import { MJMLRenderer, version as V } from './index' import fs from 'fs' const capitalize = name => name.charAt(0).toUpperCase() + name.slice(1).toLowerCase().replace(/-/g, '') @@ -53,7 +53,7 @@ const readStdin = promisify(stdinToBuffer) */ const render = (bufferPromise, { min, output, stdout }) => { bufferPromise - .then(mjml => mjml2html(mjml.toString(), { minify: min })) + .then(mjml => new MJMLRenderer(mjml.toString(), { minify: min })) .then(result => stdout ? process.stdout.write(result) : write(output, result)) .catch(error) } diff --git a/src/components/Body.js b/src/components/Body.js deleted file mode 100644 index 59733109c..000000000 --- a/src/components/Body.js +++ /dev/null @@ -1,49 +0,0 @@ -import { widthParser } from '../helpers/mjAttribute' -import MJMLElement from './decorators/MJMLElement' -import React, { Component } from 'react' - -/** - * This is the starting point of your email. It is a unique and mandatory component. It corresponds to the HTML tag. - */ -@MJMLElement({ - tagName: 'mj-body', - attributes: { - 'width': '600' - }, - inheritedAttributes: [ - 'width' - ] -}) -class Body extends Component { - - styles = this.getStyles() - - getStyles () { - const { mjAttribute } = this.props - - return { - div: { - backgroundColor: mjAttribute('background-color'), - fontSize: mjAttribute('font-size') - } - } - } - - render () { - const { renderWrappedOutlookChildren, mjAttribute, children } = this.props - const { width } = widthParser(mjAttribute('width')) - - return ( -
- {renderWrappedOutlookChildren(children)} -
- ) - } - -} - -export default Body diff --git a/src/components/Button.js b/src/components/Button.js deleted file mode 100644 index d35d95fb4..000000000 --- a/src/components/Button.js +++ /dev/null @@ -1,106 +0,0 @@ -import _ from 'lodash' -import MJMLColumnElement from './decorators/MJMLColumnElement' -import React, { Component } from 'react' - -/** - * Displays a customizable button - */ -@MJMLColumnElement({ - tagName: 'mj-button', - content: '', - attributes: { - 'align': 'center', - 'background-color': '#414141', - 'border-radius': '3px', - 'border': 'none', - 'color': '#ffffff', - 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', - 'font-size': '13px', - 'font-weight': 'normal', - 'href': '', - 'padding': '10px 25px', - 'text-decoration': 'none', - 'vertical-align': 'middle' - } -}) -class Button extends Component { - - static baseStyles = { - a: { - display: 'inline-block', - textDecoration: 'none' - } - } - - styles = this.getStyles() - - getStyles () { - const { mjAttribute } = this.props - - return _.merge({}, this.constructor.baseStyles, { - td: { - background: mjAttribute('background-color'), - borderRadius: mjAttribute('border-radius'), - color: mjAttribute('color'), - cursor: 'auto', - fontStyle: mjAttribute('font-style') - }, - table: { - border: mjAttribute('border'), - borderRadius: mjAttribute('border-radius') - }, - a: { - background: mjAttribute('background-color'), - border: `1px solid ${mjAttribute('background-color')}`, - borderRadius: mjAttribute('border-radius'), - color: mjAttribute('color'), - fontFamily: mjAttribute('font-family'), - fontSize: mjAttribute('font-size'), - fontStyle: mjAttribute('font-style'), - fontWeight: mjAttribute('font-weight'), - padding: mjAttribute('padding'), - textDecoration: mjAttribute('text-decoration') - } - }) - } - - renderButton () { - const { mjContent, mjAttribute } = this.props - - return ( - - ) - } - - render () { - const { mjAttribute } = this.props - - return ( - - - - - - -
- {this.renderButton()} -
- ) - } - -} - -export default Button diff --git a/src/components/Column.js b/src/components/Column.js deleted file mode 100644 index 56010d1df..000000000 --- a/src/components/Column.js +++ /dev/null @@ -1,90 +0,0 @@ -import _ from 'lodash' -import { widthParser } from '../helpers/mjAttribute' -import MJMLElement from './decorators/MJMLElement' -import React, { Component } from 'react' - -/** - * Columns are the basic containers for your content. They must be located under mj-section tags in order to be considered by the engine - */ -@MJMLElement({ - tagName: 'mj-column' -}) -class Column extends Component { - - static baseStyles = { - div: { - verticalAlign: 'top' - } - } - - styles = this.getStyles() - - getStyles () { - const { mjAttribute } = this.props - - return _.merge({}, this.constructor.baseStyles, { - div: { - display: 'inline-block', - verticalAlign: mjAttribute('vertical-align'), - fontSize: '13', - textAlign: 'left', - width: '100%', - minWidth: mjAttribute('width') - }, - table: { - verticalAlign: mjAttribute('vertical-align'), - background: mjAttribute('background-color') - } - }) - } - - getColumnClass () { - const { mjAttribute, sibling } = this.props - const width = mjAttribute('width') - - if (width == undefined) { - return `mj-column-per-${parseInt(100 / sibling)}` - } - - const { width: parsedWidth, unit } = widthParser(width) - - switch (unit) { - case '%': - return `mj-column-per-${parsedWidth}` - - case 'px': - default: - return `mj-column-px-${parsedWidth}` - } - } - - render () { - const { mjAttribute, children, sibling } = this.props - const width = mjAttribute('width') || (100 / sibling) - const mjColumnClass = this.getColumnClass() - - return ( -
- - - {children} - -
-
- ) - } - -} - -export default Column diff --git a/src/components/Divider.js b/src/components/Divider.js deleted file mode 100644 index d5cb89e5d..000000000 --- a/src/components/Divider.js +++ /dev/null @@ -1,67 +0,0 @@ -import _ from 'lodash' -import MJMLColumnElement from './decorators/MJMLColumnElement' -import React, { Component } from 'react' -import { widthParser } from '../helpers/mjAttribute' - -/** - * Displays a customizable divider - */ -@MJMLColumnElement({ - tagName: 'mj-divider', - attributes: { - 'border-color': '#000000', - 'border-style': 'solid', - 'border-width': '4px', - 'padding': '10px 25px', - 'width': '100%' - } -}) -class Divider extends Component { - - static baseStyles = { - p: { - fontSize: '1px', - margin: '0 auto' - } - } - - styles = this.getStyles() - - getStyles() { - const { mjAttribute } = this.props - - return _.merge({}, this.constructor.baseStyles, { - p: { - borderTop: `${mjAttribute('border-width')} ${mjAttribute('border-style')} ${mjAttribute('border-color')}`, - width: mjAttribute('width') - } - }) - } - - outlookWidth() { - const { mjAttribute } = this.props - const parentWidth = parseInt(mjAttribute('parentWidth')) - const {width, unit} = widthParser(mjAttribute('width')) - - switch(unit) { - case '%': { - return parentWidth * width / 100 - } - default: { - return width - } - } - } - - render() { - return ( -

- ) - } - -} - -export default Divider diff --git a/src/components/Html.js b/src/components/Html.js deleted file mode 100644 index ab4d1a308..000000000 --- a/src/components/Html.js +++ /dev/null @@ -1,41 +0,0 @@ -import _ from 'lodash' -import MJMLColumnElement from './decorators/MJMLColumnElement' -import React, { Component } from 'react' - -/** - * Displays raw html - */ -@MJMLColumnElement({ - tagName: 'mj-html', - content: '', - attributes: { - 'padding': 0 - } -}) -class Html extends Component { - - static baseStyles = { - div: { - fontSize: '13px' - } - } - - styles = this.getStyles() - - getStyles () { - return _.merge({}, this.constructor.baseStyles, {}) - } - - render () { - const { mjContent } = this.props - - return ( -

- ) - } - -} - -export default Html diff --git a/src/components/Image.js b/src/components/Image.js deleted file mode 100644 index e18eead3d..000000000 --- a/src/components/Image.js +++ /dev/null @@ -1,111 +0,0 @@ -import _ from 'lodash' -import MJMLColumnElement from './decorators/MJMLColumnElement' -import React, { Component } from 'react' - -/** - * Displays an image to your email. It is mostly similar to the HTML img tag - */ -@MJMLColumnElement({ - tagName: 'mj-image', - attributes: { - 'height': 'auto', - 'padding': '10px 25px', - 'align': 'center', - 'alt': '', - 'border': 'none', - 'href': '', - 'src': '', - 'target': '_blank' - } -}) -class Image extends Component { - - static baseStyles = { - table: { - borderCollapse: 'collapse', - borderSpacing: '0' - }, - img: { - border: 'none', - display: 'block', - outline: 'none', - textDecoration: 'none', - width: '100%' - } - } - - styles = this.getStyles() - - getContentWidth () { - const { mjAttribute, getPadding } = this.props - const parentWidth = mjAttribute('parentWidth') - - const width = _.min([parseInt(mjAttribute('width')), parseInt(parentWidth)]) - - const paddingRight = getPadding('right') - const paddingLeft = getPadding('left') - const widthOverflow = paddingLeft + paddingRight + width - parseInt(parentWidth) - - return widthOverflow > 0 ? width - widthOverflow : width - } - - getStyles () { - const { mjAttribute } = this.props - - return _.merge({}, this.constructor.baseStyles, { - img: { - border: mjAttribute('border'), - height: mjAttribute('height') - } - }) - } - - renderImage () { - const { mjAttribute } = this.props - - const img = ( - {mjAttribute('alt')} - ) - - if (mjAttribute('href') != '') { - return ( - - {img} - - ) - } - else { - return img - } - } - - render () { - const { mjAttribute } = this.props - - return ( - - - - - - -
- {this.renderImage()} -
- ) - } - -} - -export default Image diff --git a/src/components/Invoice/Item.js b/src/components/Invoice/Item.js deleted file mode 100644 index 09e5bbbcf..000000000 --- a/src/components/Invoice/Item.js +++ /dev/null @@ -1,67 +0,0 @@ -import _ from 'lodash' -import MJMLElement from '../decorators/MJMLElement' -import React, { Component } from 'react' - -@MJMLElement({ - tagName: 'mj-invoice-item', - attributes: { - 'color': '#747474', - 'font-family': 'Roboto, Ubuntu, Helvetica, Arial, sans-serif', - 'font-size': '14px', - 'name': '', - 'padding': '10px 20px', - 'price': 0, - 'quantity': 0, - 'text-align': 'left' - } -}) -class InvoiceItem extends Component { - - static baseStyles = { - td: { - fontWeight: 500, - lineHeight: 1 - }, - name: { - wordBreak: 'break-all' - }, - quantity: { - textAlign: 'right' - } - } - - styles = this.getStyles() - - getStyles () { - const { mjAttribute } = this.props - - const styles = _.merge({}, this.constructor.baseStyles, { - td: { - color: mjAttribute('color'), - fontFamily: mjAttribute('font-family'), - fontSize: mjAttribute('font-size'), - padding: mjAttribute('padding'), - textAlign: mjAttribute('text-align') - } - }) - - styles.name = _.merge({}, styles.td, styles.name) - styles.quantity = _.merge({}, styles.td, styles.quantity) - - return styles - } - - render () { - const { mjAttribute } = this.props - - return ( - - {mjAttribute('name')} - {mjAttribute('price')} - {mjAttribute('quantity')} - - ) - } -} - -export default InvoiceItem diff --git a/src/components/Invoice/index.js b/src/components/Invoice/index.js deleted file mode 100644 index 7bc9b36d0..000000000 --- a/src/components/Invoice/index.js +++ /dev/null @@ -1,145 +0,0 @@ -import _ from 'lodash' -import MJMLElement from '../decorators/MJMLElement' -import MJTable from '../Table' -import numeral from 'numeral' -import React, { Component } from 'react' - -@MJMLElement({ - tagName: 'mj-invoice', - attributes: { - 'border': '1px solid #ecedee', - 'color': '#b9b9b9', - 'font-family': 'Roboto, Ubuntu, Helvetica, Arial, sans-serif', - 'font-size': '13px', - 'intl': 'name:Name;price:Price;quantity:Quantity', - 'line-height': '22px' - } -}) -class Invoice extends Component { - - static baseStyles = { - th: { - fontWeight: 700, - padding: '10px 20px', - textAlign: 'left', - textTransform: 'uppercase' - } - } - - static intl = { - name: 'Name', - price: 'Price', - quantity: 'Quantity', - total: 'Total:' - } - - constructor (props) { - super(props) - - const format = props.mjAttribute('format') - const currencies = format.match(/([^-\d.,])/g) - - this.items = props.mjChildren().filter(child => child.get('tagName') === 'mj-invoice-item') - this.format = format.replace(/([^-\d.,])/g, '$') - this.currency = (currencies) ? currencies[0] : null - } - - styles = this.getStyles() - - getStyles () { - const { mjAttribute } = this.props - - const styles = _.merge({}, this.constructor.baseStyles, { - table: { - color: mjAttribute('color'), - fontFamily: mjAttribute('font-family'), - fontSize: mjAttribute('font-size'), - lineHeight: mjAttribute('line-height') - }, - th: { - fontFamily: mjAttribute('font-family'), - fontSize: mjAttribute('font-size'), - lineHeight: mjAttribute('line-height') - }, - thead: { - borderBottom: mjAttribute('border') - }, - tfoot: { - borderTop: mjAttribute('border') - }, - total: { - fontFamily: mjAttribute('font-family'), - fontSize: mjAttribute('font-size'), - fontWeight: 700, - lineHeight: mjAttribute('line-height'), - padding: '10px 20px', - textAlign: 'right' - } - }) - - styles.thQuantity = _.merge({}, styles.th, { textAlign: 'right' }) - - return styles - } - - getIntl () { - const { mjAttribute } = this.props - - const intl = _.cloneDeep(this.constructor.intl) - - mjAttribute('intl').split(';').forEach(t => { - if (t && t.indexOf(':') != -1) { - t = t.split(':') - intl[t[0].trim()] = t[1].trim() - } - }) - - return intl - } - - getTotal () { - const format = this.format - const currency = this.currency - - const total = this.items.reduce((prev, item) => { - const unitPrice = parseFloat(numeral().unformat(item.getIn(['attributes', 'price']))) - const quantity = parseInt(item.getIn(['attributes', 'quantity'])) - - return prev + unitPrice * quantity - }, 0) - - return numeral(total).format(format).replace(/([^-\d.,])/g, currency) - } - - render () { - const { renderChildren } = this.props - const intl = this.getIntl() - const total = this.getTotal() - - return ( - - - - {intl['name']} - {intl['price']} - {intl['quantity']} - - - - {renderChildren()} - - - - {intl['total']} - {total} - - - - ) - } - -} - -export default Invoice diff --git a/src/components/List.js b/src/components/List.js deleted file mode 100644 index 8052549df..000000000 --- a/src/components/List.js +++ /dev/null @@ -1,57 +0,0 @@ -import _ from 'lodash' -import MJMLColumnElement from './decorators/MJMLColumnElement' -import React, { Component } from 'react' - -/** - * mj-list enable you to create an unordered or ordered list - */ -@MJMLColumnElement({ - tagName: 'mj-list', - content: '', - attributes: { - 'align': 'left', - 'color': '#000000', - 'font-family': 'Ubuntu, Helvetica, Arial, sans-serif', - 'font-size': '13px', - 'line-height': '22px', - 'padding': '10px 25px' - } -}) -class List extends Component { - - static baseStyles = { - ul: { - display: 'inline-block', - paddingLeft: '20px', - textAlign: 'left' - } - } - - styles = this.getStyles() - - getStyles () { - const { mjAttribute } = this.props - - return _.merge({}, this.constructor.baseStyles, { - ul: { - color: mjAttribute('color'), - fontFamily: mjAttribute('font-family'), - fontSize: mjAttribute('font-size'), - lineHeight: mjAttribute('line-height') - } - }) - } - - render () { - const { mjContent } = this.props - - return ( -