├── .babelrc
├── .circleci
└── config.yml
├── .editorconfig
├── .gitignore
├── .npmignore
├── .nvmrc
├── LICENSE
├── README.md
├── index.js
├── package-lock.json
├── package.json
└── src
├── __tests__
├── 0-3.spec.js
├── __fixtures__
│ └── 0-3
│ │ ├── article-mobiledoc.json
│ │ ├── bold-mobiledoc.json
│ │ ├── cheat-commerce-mobiledoc.json
│ │ ├── complex-nested-markup-mobiledoc.json
│ │ ├── italic-mobiledoc.json
│ │ ├── link-mobiledoc.json
│ │ ├── multi-paragraph-mobiledoc.json
│ │ ├── nested-markup-mobiledoc.json
│ │ ├── simple-mobiledoc.json
│ │ ├── strikethrough-mobiledoc.json
│ │ └── underline-mobiledoc.json
└── __snapshots__
│ └── 0-3.spec.js.snap
├── index.js
└── renderers
└── 0-3.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", {
4 | "shippedProposals": true
5 | }],
6 | "@babel/preset-react"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Some default settings to use for each job
2 | defaults: &defaults
3 | docker:
4 | - image: circleci/node:8.9.4
5 | working_directory: ~/mobiledoc-react-renderer
6 | environment:
7 | TZ: "/usr/share/zoneinfo/America/New_York"
8 |
9 | version: 2
10 | jobs:
11 | build:
12 | <<: *defaults
13 | steps:
14 | - checkout
15 | - restore_cache:
16 | key: node-modules-cache-v2-{{ arch }}-{{ checksum "package-lock.json" }}
17 | - run:
18 | name: Setup NPM
19 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
20 | - run:
21 | name: Install Dependencies
22 | command: npm install
23 | - save_cache:
24 | key: node-modules-cache-v2-{{ arch }}-{{ checksum "package-lock.json" }}
25 | paths:
26 | - node_modules
27 | - run:
28 | name: Build Application
29 | command: npm run build
30 | - persist_to_workspace:
31 | root: .
32 | paths:
33 | - node_modules
34 | - dist
35 | test:
36 | <<: *defaults
37 | steps:
38 | - checkout
39 | - attach_workspace:
40 | at: .
41 | - run:
42 | environment:
43 | NODE_ENV: test
44 | name: Test
45 | command: npm run test
46 |
47 | workflows:
48 | version: 2
49 | build-test:
50 | jobs:
51 | - build:
52 | filters:
53 | branches:
54 | only: master
55 | - test:
56 | requires:
57 | - build
58 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | charset = utf-8
9 | indent_style = space
10 | indent_size = 2
11 | insert_final_newline = true
12 | trim_trailing_whitespace = true
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | /node_modules/
3 | dist
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea
2 | /node_modules/
3 | /src
4 | /.editorconfig
5 | /.gitignore
6 | /.circleci
7 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 8.11.3
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 thedailybeast
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Mobiledoc React Renderer
2 |
3 | Mobiledoc server and client rendering for [Mobiledoc-kit](https://github.com/bustlelabs/mobiledoc-kit).
4 |
5 | This renderer is used across [www.thedailybeast.com](https://www.thedailybeast.com) to perform both client and server rendering of our React app. It supports rendering of markups, atoms, cards and custom sections of any mobiledoc (0.3.0 or greater).
6 |
7 | [](https://circleci.com/gh/dailybeast/mobiledoc-react-renderer)
8 |
9 | ### Usage
10 |
11 | This renderer is intended to be used from react components, for example:
12 |
13 | ```javascript
14 | import PropTypes from 'prop-types';
15 | import { Component } from 'react';
16 | import MobiledocReactRenderer from 'mobiledoc-react-renderer';
17 |
18 | const mobiledoc = {
19 | "atoms": [],
20 | "cards": [],
21 | "markups": [],
22 | "sections": [
23 | [
24 | 1,
25 | "p",
26 | [
27 | [0, [], 0, "Hello world!"]
28 | ]
29 | ]
30 | ],
31 | "version": "0.3.0"
32 | };
33 |
34 | export default class Mobiledoc extends Component {
35 | static propTypes = {
36 | mobiledoc: PropTypes.object.isRequired
37 | };
38 |
39 | constructor(props) {
40 | super(props);
41 |
42 | const options = { atoms: [], cards: [], markups: [] };
43 |
44 | this.renderer = new MobiledocReactRenderer(options);
45 | }
46 |
47 | render() {
48 | return this.renderer.render(this.props.mobiledoc);
49 | }
50 | }
51 |
52 | ```
53 |
54 | ### Tests
55 | To run the unit tests use: `npm test`
56 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import Renderer_0_3, { // eslint-disable-line
2 | MOBILE_DOC_VERSION_0_3_0,
3 | MOBILE_DOC_VERSION_0_3_1
4 | } from './renderers/0-3';
5 |
6 | // ///////////////////////////////////////
7 | //
8 | // This will be factored out into a seperate NPM package
9 | // and made publically available as TDB's first open-source
10 | // contribution.
11 | //
12 | // NOTE: Currently unsupported mobiledoc features are:
13 | // native image card, default card renderers
14 | //
15 | // ///////////////////////////////////////
16 |
17 | export default class MobiledocReactRenderer {
18 | constructor ({ atoms = [], cards = [], markups = [] }) {
19 | this.options = {
20 | atoms,
21 | cards,
22 | markups
23 | };
24 | }
25 |
26 | render (mobiledoc) {
27 | const { version } = mobiledoc;
28 | switch (version) {
29 | case MOBILE_DOC_VERSION_0_3_0:
30 | case MOBILE_DOC_VERSION_0_3_1:
31 | return new Renderer_0_3(mobiledoc, this.options).render();
32 | default:
33 | return null;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "The Daily Beast",
3 | "dependencies": {
4 | "uuid": "^3.2.1"
5 | },
6 | "peerDependencies": {
7 | "react": "^16.2.0"
8 | },
9 | "jest": {
10 | "testPathIgnorePatterns": [
11 | "node_modules",
12 | "dist"
13 | ]
14 | },
15 | "description": "Mobiledoc renderer that outputs valid react jsx",
16 | "devDependencies": {
17 | "@babel/cli": "^7.0.0-beta.41",
18 | "@babel/core": "^7.0.0-beta.41",
19 | "@babel/preset-env": "^7.0.0-beta.41",
20 | "@babel/preset-react": "^7.0.0-beta.41",
21 | "babel-core": "^7.0.0-bridge.0",
22 | "babel-jest": "^23.0.0-alpha.0",
23 | "brace-expansion": "^1.1.11",
24 | "jest": "^22.4.2",
25 | "react": "^16.2.0",
26 | "react-dom": "^16.2.1",
27 | "react-test-renderer": "^16.2.0",
28 | "semistandard": "^12.0.1"
29 | },
30 | "license": "MIT",
31 | "main": "dist/index.js",
32 | "name": "@dailybeast/mobiledoc-react-renderer",
33 | "scripts": {
34 | "build": "rimraf dist && babel src -d dist",
35 | "jest": "jest",
36 | "lint": "semistandard",
37 | "prepare": "npm run build",
38 | "prepublishOnly": "npm run build && npm run test",
39 | "start": "babel-node src/index.js",
40 | "test": "semistandard && jest"
41 | },
42 | "version": "1.5.0"
43 | }
44 |
--------------------------------------------------------------------------------
/src/__tests__/0-3.spec.js:
--------------------------------------------------------------------------------
1 | /* global describe, it, expect */
2 |
3 | const { resolve: pathResolve } = require('path');
4 | const { StringDecoder } = require('string_decoder');
5 | const { readFile } = require('fs');
6 |
7 | const snapshot = require('react-test-renderer');
8 | const { renderToStaticMarkup } = require('react-dom/server');
9 |
10 | const MobiledocReactRenderer = require('..').default;
11 |
12 | function getFixture (path, ...rest) {
13 | const decoder = new StringDecoder('utf8');
14 | const fixture = pathResolve(path, '__fixtures__', rest.join('/'));
15 |
16 | return new Promise((resolve, reject) => {
17 | readFile(fixture, (err, data) => {
18 | if (err) {
19 | console.log(err);
20 | reject(err);
21 | } else {
22 | resolve(JSON.parse(decoder.write(data)));
23 | }
24 | });
25 | });
26 | }
27 |
28 | describe('React Mobiledoc renderer', () => {
29 | describe('0.3', () => {
30 | describe('basic formatting', () => {
31 | it('renders a basic mobiledoc', async () => {
32 | const mobiledoc = await getFixture(__dirname, '0-3', 'simple-mobiledoc.json');
33 |
34 | const rendered = new MobiledocReactRenderer({}).render(mobiledoc);
35 |
36 | expect(renderToStaticMarkup(rendered)).toBe('
');
37 | });
38 |
39 | it('renders a bold markup in mobiledoc', async () => {
40 | const mobiledoc = await getFixture(__dirname, '0-3', 'bold-mobiledoc.json');
41 |
42 | const rendered = new MobiledocReactRenderer({}).render(mobiledoc);
43 |
44 | expect(renderToStaticMarkup(rendered)).toBe('');
45 | });
46 |
47 | it('renders a italic markup in mobiledoc', async () => {
48 | const mobiledoc = await getFixture(__dirname, '0-3', 'italic-mobiledoc.json');
49 |
50 | const rendered = new MobiledocReactRenderer({}).render(mobiledoc);
51 |
52 | expect(renderToStaticMarkup(rendered)).toBe('');
53 | });
54 |
55 | it('renders a underlined markup in mobiledoc', async () => {
56 | const mobiledoc = await getFixture(__dirname, '0-3', 'underline-mobiledoc.json');
57 |
58 | const rendered = new MobiledocReactRenderer({}).render(mobiledoc);
59 |
60 | expect(renderToStaticMarkup(rendered)).toBe('');
61 | });
62 |
63 | it('renders a strikethrough markup in mobiledoc', async () => {
64 | const mobiledoc = await getFixture(__dirname, '0-3', 'strikethrough-mobiledoc.json');
65 |
66 | const rendered = new MobiledocReactRenderer({}).render(mobiledoc);
67 |
68 | expect(renderToStaticMarkup(rendered)).toBe('');
69 | });
70 |
71 | it('renders links in mobiledoc', async () => {
72 | const mobiledoc = await getFixture(__dirname, '0-3', 'link-mobiledoc.json');
73 |
74 | const rendered = new MobiledocReactRenderer({}).render(mobiledoc);
75 |
76 | expect(renderToStaticMarkup(rendered)).toBe('');
77 | });
78 |
79 | it('renders nested markup', async () => {
80 | const mobiledoc = await getFixture(__dirname, '0-3', 'nested-markup-mobiledoc.json');
81 |
82 | const rendered = new MobiledocReactRenderer({}).render(mobiledoc);
83 |
84 | expect(renderToStaticMarkup(rendered)).toBe('');
85 | });
86 |
87 | it('renders complex nested markup', async () => {
88 | const mobiledoc = await getFixture(__dirname, '0-3', 'complex-nested-markup-mobiledoc.json');
89 |
90 | const rendered = new MobiledocReactRenderer({}).render(mobiledoc);
91 |
92 | expect(renderToStaticMarkup(rendered)).toBe('Some nested markup then normal
');
93 | });
94 |
95 | it('renders multiple paragraphs', async () => {
96 | const mobiledoc = await getFixture(__dirname, '0-3', 'multi-paragraph-mobiledoc.json');
97 |
98 | const rendered = new MobiledocReactRenderer({}).render(mobiledoc);
99 |
100 | expect(renderToStaticMarkup(rendered)).toBe('This is the first paragraph.
This is the second paragraph.
');
101 | });
102 | });
103 |
104 | describe('Real content', () => {
105 | it('renders complex cheat mobiledocs', async () => {
106 | const mobiledoc = await getFixture(__dirname, '0-3', 'cheat-commerce-mobiledoc.json');
107 |
108 | const rendered = new MobiledocReactRenderer({}).render(mobiledoc);
109 | const tree = snapshot.create(rendered).toJSON();
110 |
111 | expect(tree).toMatchSnapshot();
112 | });
113 |
114 | it('renders complex article mobiledocs', async () => {
115 | const mobiledoc = await getFixture(__dirname, '0-3', 'article-mobiledoc.json');
116 |
117 | const rendered = new MobiledocReactRenderer({}).render(mobiledoc);
118 | const tree = snapshot.create(rendered).toJSON();
119 |
120 | expect(tree).toMatchSnapshot();
121 | });
122 | });
123 | });
124 | });
125 |
--------------------------------------------------------------------------------
/src/__tests__/__fixtures__/0-3/article-mobiledoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "atoms": [
3 | ],
4 | "cards": [
5 | ],
6 | "markups": [
7 | [
8 | "a",
9 | [
10 | "href",
11 | "https://www.thedailybeast.com/the-role-of-meghan-markles-lifetime-princess"
12 | ]
13 | ],
14 | [
15 | "a",
16 | [
17 | "href",
18 | "http://www.dailymail.co.uk/femail/article-5489001/Prince-Harry-teaching-Meghan-drive-manually-left.html"
19 | ]
20 | ],
21 | [
22 | "em"
23 | ],
24 | [
25 | "a",
26 | [
27 | "href",
28 | "https://www.thedailybeast.com/prince-harry-and-meghan-markle-to-marry-in-spring"
29 | ]
30 | ],
31 | [
32 | "a",
33 | [
34 | "href",
35 | "https://www.thedailybeast.com/if-she-marries-prince-harry-meghan-markle-will-shatter-a-royal-race-taboo"
36 | ]
37 | ],
38 | [
39 | "a",
40 | [
41 | "href",
42 | "https://www.express.co.uk/news/royal/930005/Meghan-Markle-Prince-Harry-royal-wedding-SAS-training-kidnap "
43 | ]
44 | ]
45 | ],
46 | "sections": [
47 | [
48 | 1,
49 | "p",
50 | [
51 | [
52 | 0,
53 | [
54 | ],
55 | 0,
56 | "For American visitors to the U.K., it is up there with the lack of air conditioning and low water pressure in the shower. "
57 | ],
58 | [
59 | 0,
60 | [
61 | 0
62 | ],
63 | 1,
64 | "Meghan Markle"
65 | ],
66 | [
67 | 0,
68 | [
69 | ],
70 | 0,
71 | " is learning how to cope with one of the great inconveniences of British life by learning to drive stick."
72 | ]
73 | ]
74 | ],
75 | [
76 | 1,
77 | "p",
78 | [
79 | [
80 | 0,
81 | [
82 | ],
83 | 0,
84 | "According to a report this weekend in the "
85 | ],
86 | [
87 | 0,
88 | [
89 | 1,
90 | 2
91 | ],
92 | 2,
93 | "Mail on Sunday"
94 | ],
95 | [
96 | 0,
97 | [
98 | ],
99 | 0,
100 | ", "
101 | ],
102 | [
103 | 0,
104 | [
105 | 3
106 | ],
107 | 1,
108 | "Meghan"
109 | ],
110 | [
111 | 0,
112 | [
113 | ],
114 | 0,
115 | " is receiving lessons from none other than her boyfriend, "
116 | ],
117 | [
118 | 0,
119 | [
120 | 4
121 | ],
122 | 1,
123 | "Prince Harry"
124 | ],
125 | [
126 | 0,
127 | [
128 | ],
129 | 0,
130 | "."
131 | ]
132 | ]
133 | ],
134 | [
135 | 1,
136 | "p",
137 | [
138 | [
139 | 0,
140 | [
141 | ],
142 | 0,
143 | "A source told the paper Markle has been seen brushing up on her manual gear-shift skills inside the grounds at Kensington Palace, where she lives with Prince Harry, 33, as she prepares for their May 19 wedding."
144 | ]
145 | ]
146 | ],
147 | [
148 | 1,
149 | "p",
150 | [
151 | [
152 | 0,
153 | [
154 | ],
155 | 0,
156 | "Like the vast majority of American-taught drivers, Markle learned to drive on an automatic transmission. In the U.K., automatics, once a rarity, are becoming more popular, but still only account for around a quarter of cars on the road. "
157 | ]
158 | ]
159 | ],
160 | [
161 | 1,
162 | "p",
163 | [
164 | [
165 | 0,
166 | [
167 | ],
168 | 0,
169 | "Visitors to the U.K. with a full driving license in their home country are legally allowed to drive for up to a year on British roads. "
170 | ]
171 | ]
172 | ],
173 | [
174 | 1,
175 | "p",
176 | [
177 | [
178 | 0,
179 | [
180 | ],
181 | 0,
182 | "Although all of the official royal fleet is believed to be automatics, Markle is learning to drive manual in case of an emergency, sources told "
183 | ],
184 | [
185 | 0,
186 | [
187 | 2
188 | ],
189 | 1,
190 | "The Mail"
191 | ],
192 | [
193 | 0,
194 | [
195 | ],
196 | 0,
197 | ". "
198 | ]
199 | ]
200 | ],
201 | [
202 | 1,
203 | "p",
204 | [
205 | [
206 | 0,
207 | [
208 | ],
209 | 0,
210 | "The security aspect to the driving lessons is crucial, and it has also emerged that Meghan has taken part in a mock kidnap by SAS troops during a two-day course."
211 | ]
212 | ]
213 | ],
214 | [
215 | 1,
216 | "p",
217 | [
218 | [
219 | 0,
220 | [
221 | ],
222 | 0,
223 | "One former SAS officer told "
224 | ],
225 | [
226 | 0,
227 | [
228 | 5,
229 | 2
230 | ],
231 | 2,
232 | "The Sunday Express"
233 | ],
234 | [
235 | 0,
236 | [
237 | ],
238 | 0,
239 | " the course at the SAS HQ in Herefordshire had been “devised to frighten the life out of anyone.” "
240 | ]
241 | ]
242 | ],
243 | [
244 | 1,
245 | "p",
246 | [
247 | [
248 | 0,
249 | [
250 | ],
251 | 0,
252 | "During one scenario, troops used live ammunition as they “saved” Meghan, 36, from kidnappers."
253 | ]
254 | ]
255 | ],
256 | [
257 | 1,
258 | "p",
259 | [
260 | [
261 | 0,
262 | [
263 | ],
264 | 0,
265 | "In 1983, when Princess Diana was in a similar training session, her hair caught fire, although it appears no such disaster befell Meghan. "
266 | ]
267 | ]
268 | ],
269 | [
270 | 1,
271 | "p",
272 | [
273 | [
274 | 0,
275 | [
276 | ],
277 | 0,
278 | "One former SAS man told the "
279 | ],
280 | [
281 | 0,
282 | [
283 | 2
284 | ],
285 | 1,
286 | "Express"
287 | ],
288 | [
289 | 0,
290 | [
291 | ],
292 | 0,
293 | ": “The men enact a kidnap situation, during which she will have been treated as a hostage, with the area being stormed by the SAS.”"
294 | ]
295 | ]
296 | ],
297 | [
298 | 1,
299 | "p",
300 | [
301 | [
302 | 0,
303 | [
304 | ],
305 | 0,
306 | "Meghan is expected to appear for the first time publicly in the company of the queen Monday, at the Commonwealth Day church service in London."
307 | ]
308 | ]
309 | ]
310 | ],
311 | "version": "0.3.0"
312 | }
313 |
--------------------------------------------------------------------------------
/src/__tests__/__fixtures__/0-3/bold-mobiledoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "atoms": [
3 | ],
4 | "cards": [
5 | ],
6 | "markups": [
7 | [
8 | "strong"
9 | ]
10 | ],
11 | "sections": [
12 | [
13 | 1,
14 | "p",
15 | [
16 | [
17 | 0,
18 | [
19 | 0
20 | ],
21 | 1,
22 | "Hello world!"
23 | ]
24 | ]
25 | ]
26 | ],
27 | "version": "0.3.0"
28 | }
29 |
--------------------------------------------------------------------------------
/src/__tests__/__fixtures__/0-3/cheat-commerce-mobiledoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "atoms": [
3 | ],
4 | "cards": [
5 | ],
6 | "markups": [
7 | [
8 | "a",
9 | [
10 | "href",
11 | "https://shop.thedailybeast.com/sales/klikr-universal-remote-control-black?utm_source=thedailybeast.com&utm_medium=referral&utm_campaign=klikr-universal-remote-control-black&utm_term=scsf-275364&utm_content=a0x1a000003bH7l&scsonar=1"
12 | ]
13 | ],
14 | [
15 | "strong"
16 | ],
17 | [
18 | "em"
19 | ],
20 | [
21 | "a",
22 | [
23 | "href",
24 | "https://www.thedailybeast.com/keyword/scouted"
25 | ]
26 | ],
27 | [
28 | "i",
29 | [
30 | "href",
31 | null
32 | ]
33 | ],
34 | [
35 | "a",
36 | [
37 | "href",
38 | "https://flipboard.com/@thedailybeast/scouted-t57nqmfky"
39 | ]
40 | ]
41 | ],
42 | "sections": [
43 | [
44 | 1,
45 | "p",
46 | [
47 | [
48 | 0,
49 | [
50 | ],
51 | 0,
52 | "Your smartphone is the IRL equivalent of a magic wand. It can do all sorts of extraordinary things, from tracking your footsteps to summoning Chinese takeout to your doorstep. But if you have the "
53 | ],
54 | [
55 | 0,
56 | [
57 | 0
58 | ],
59 | 1,
60 | "KlikR Universal Remote Control"
61 | ],
62 | [
63 | 0,
64 | [
65 | ],
66 | 0,
67 | ", it can also be the master of your household electronics, too. Here’s how it works? Stick this Kickstarter-funded, coin-sized accessory on or next to any remote-controlled infrared device, and you’ll immediately be able to control it via the companion app. This means you can turn on the AC, flip TV channels, and even bring your ancient ceiling fan back to life with just your phone. It's also equipped with voice commands, smart pausing and muting, and has the ability to organize all of your devices in your home, room by room. Normally, the KlikR Universal Remote Control costs $29, but "
68 | ],
69 | [
70 | 0,
71 | [
72 | 0
73 | ],
74 | 1,
75 | "you can get one today for only $19.99"
76 | ],
77 | [
78 | 0,
79 | [
80 | ],
81 | 0,
82 | ". Save an additional 10% when using code "
83 | ],
84 | [
85 | 0,
86 | [
87 | 1
88 | ],
89 | 1,
90 | "MADMARCH10"
91 | ],
92 | [
93 | 0,
94 | [
95 | ],
96 | 0,
97 | " at checkout. "
98 | ]
99 | ]
100 | ],
101 | [
102 | 1,
103 | "p",
104 | [
105 | [
106 | 0,
107 | [
108 | 2,
109 | 3
110 | ],
111 | 1,
112 | "Scouted"
113 | ],
114 | [
115 | 0,
116 | [
117 | 4
118 | ],
119 | 1,
120 | " is here to surface products that you might like."
121 | ],
122 | [
123 | 0,
124 | [
125 | ],
126 | 0,
127 | " "
128 | ],
129 | [
130 | 0,
131 | [
132 | 5
133 | ],
134 | 1,
135 | "Follow us on Flipboard"
136 | ],
137 | [
138 | 0,
139 | [
140 | ],
141 | 0,
142 | " for more. "
143 | ],
144 | [
145 | 0,
146 | [
147 | 4
148 | ],
149 | 2,
150 | "Please note that if you buy something featured in one of our posts, The Daily Beast may collect a share of sales. "
151 | ]
152 | ]
153 | ]
154 | ],
155 | "version": "0.3.0"
156 | }
157 |
--------------------------------------------------------------------------------
/src/__tests__/__fixtures__/0-3/complex-nested-markup-mobiledoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "atoms": [
3 | ],
4 | "cards": [
5 | ],
6 | "markups": [
7 | [
8 | "strong"
9 | ],
10 | [
11 | "em"
12 | ]
13 | ],
14 | "sections": [
15 | [
16 | 1,
17 | "p",
18 | [
19 | [
20 | 0,
21 | [
22 | ],
23 | 0,
24 | "Some "
25 | ],
26 | [
27 | 0,
28 | [
29 | 0
30 | ],
31 | 0,
32 | "nested "
33 | ],
34 | [
35 | 0,
36 | [
37 | 1
38 | ],
39 | 2,
40 | "markup "
41 | ],
42 | [
43 | 0,
44 | [
45 | ],
46 | 0,
47 | "then normal"
48 | ]
49 | ]
50 | ]
51 | ],
52 | "version": "0.3.0"
53 | }
54 |
--------------------------------------------------------------------------------
/src/__tests__/__fixtures__/0-3/italic-mobiledoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "atoms": [
3 | ],
4 | "cards": [
5 | ],
6 | "markups": [
7 | [
8 | "em"
9 | ]
10 | ],
11 | "sections": [
12 | [
13 | 1,
14 | "p",
15 | [
16 | [
17 | 0,
18 | [
19 | 0
20 | ],
21 | 1,
22 | "Hello world!"
23 | ]
24 | ]
25 | ]
26 | ],
27 | "version": "0.3.0"
28 | }
29 |
--------------------------------------------------------------------------------
/src/__tests__/__fixtures__/0-3/link-mobiledoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "atoms": [
3 | ],
4 | "cards": [
5 | ],
6 | "markups": [
7 | [
8 | "a",
9 | [
10 | "href",
11 | "https://www.thedailybeast.com"
12 | ]
13 | ]
14 | ],
15 | "sections": [
16 | [
17 | 1,
18 | "p",
19 | [
20 | [
21 | 0,
22 | [
23 | ],
24 | 0,
25 | "A link "
26 | ],
27 | [
28 | 0,
29 | [
30 | 0
31 | ],
32 | 1,
33 | "Hello world!"
34 | ]
35 | ]
36 | ]
37 | ],
38 | "version": "0.3.0"
39 | }
40 |
--------------------------------------------------------------------------------
/src/__tests__/__fixtures__/0-3/multi-paragraph-mobiledoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "atoms": [
3 | ],
4 | "cards": [
5 | ],
6 | "markups": [
7 | ],
8 | "sections": [
9 | [
10 | 1,
11 | "p",
12 | [
13 | [
14 | 0,
15 | [
16 | ],
17 | 0,
18 | "This is the first paragraph."
19 | ]
20 | ]
21 | ],
22 | [
23 | 1,
24 | "p",
25 | [
26 | [
27 | 0,
28 | [
29 | ],
30 | 0,
31 | "This is the second paragraph."
32 | ]
33 | ]
34 | ]
35 | ],
36 | "version": "0.3.0"
37 | }
38 |
--------------------------------------------------------------------------------
/src/__tests__/__fixtures__/0-3/nested-markup-mobiledoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "atoms": [
3 | ],
4 | "cards": [
5 | ],
6 | "markups": [
7 | [
8 | "strong"
9 | ],
10 | [
11 | "em"
12 | ]
13 | ],
14 | "sections": [
15 | [
16 | 1,
17 | "p",
18 | [
19 | [
20 | 0,
21 | [
22 | ],
23 | 0,
24 | "Some "
25 | ],
26 | [
27 | 0,
28 | [
29 | 0
30 | ],
31 | 0,
32 | "nested "
33 | ],
34 | [
35 | 0,
36 | [
37 | 1
38 | ],
39 | 2,
40 | "markup"
41 | ]
42 | ]
43 | ]
44 | ],
45 | "version": "0.3.0"
46 | }
47 |
--------------------------------------------------------------------------------
/src/__tests__/__fixtures__/0-3/simple-mobiledoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "atoms": [
3 | ],
4 | "cards": [
5 | ],
6 | "markups": [
7 | ],
8 | "sections": [
9 | [
10 | 1,
11 | "p",
12 | [
13 | [
14 | 0,
15 | [
16 | ],
17 | 0,
18 | "Hello world!"
19 | ]
20 | ]
21 | ]
22 | ],
23 | "version": "0.3.0"
24 | }
25 |
--------------------------------------------------------------------------------
/src/__tests__/__fixtures__/0-3/strikethrough-mobiledoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "atoms": [
3 | ],
4 | "cards": [
5 | ],
6 | "markups": [
7 | [
8 | "s"
9 | ]
10 | ],
11 | "sections": [
12 | [
13 | 1,
14 | "p",
15 | [
16 | [
17 | 0,
18 | [
19 | 0
20 | ],
21 | 1,
22 | "Hello world!"
23 | ]
24 | ]
25 | ]
26 | ],
27 | "version": "0.3.0"
28 | }
29 |
--------------------------------------------------------------------------------
/src/__tests__/__fixtures__/0-3/underline-mobiledoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "atoms": [
3 | ],
4 | "cards": [
5 | ],
6 | "markups": [
7 | [
8 | "u"
9 | ]
10 | ],
11 | "sections": [
12 | [
13 | 1,
14 | "p",
15 | [
16 | [
17 | 0,
18 | [
19 | 0
20 | ],
21 | 1,
22 | "Hello world!"
23 | ]
24 | ]
25 | ]
26 | ],
27 | "version": "0.3.0"
28 | }
29 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/0-3.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`React Mobiledoc renderer 0.3 Real content renders complex article mobiledocs 1`] = `
4 |
7 |
8 | For American visitors to the U.K., it is up there with the lack of air conditioning and low water pressure in the shower.
9 |
12 | Meghan Markle
13 |
14 | is learning how to cope with one of the great inconveniences of British life by learning to drive stick.
15 |
16 |
17 | According to a report this weekend in the
18 |
21 |
22 | Mail on Sunday
23 |
24 |
25 | ,
26 |
29 | Meghan
30 |
31 | is receiving lessons from none other than her boyfriend,
32 |
35 | Prince Harry
36 |
37 | .
38 |
39 |
40 | A source told the paper Markle has been seen brushing up on her manual gear-shift skills inside the grounds at Kensington Palace, where she lives with Prince Harry, 33, as she prepares for their May 19 wedding.
41 |
42 |
43 | Like the vast majority of American-taught drivers, Markle learned to drive on an automatic transmission. In the U.K., automatics, once a rarity, are becoming more popular, but still only account for around a quarter of cars on the road.
44 |
45 |
46 | Visitors to the U.K. with a full driving license in their home country are legally allowed to drive for up to a year on British roads.
47 |
48 |
49 | Although all of the official royal fleet is believed to be automatics, Markle is learning to drive manual in case of an emergency, sources told
50 |
51 | The Mail
52 |
53 | .
54 |
55 |
56 | The security aspect to the driving lessons is crucial, and it has also emerged that Meghan has taken part in a mock kidnap by SAS troops during a two-day course.
57 |
58 |
59 | One former SAS officer told
60 |
63 |
64 | The Sunday Express
65 |
66 |
67 | the course at the SAS HQ in Herefordshire had been “devised to frighten the life out of anyone.”
68 |
69 |
70 | During one scenario, troops used live ammunition as they “saved” Meghan, 36, from kidnappers.
71 |
72 |
73 | In 1983, when Princess Diana was in a similar training session, her hair caught fire, although it appears no such disaster befell Meghan.
74 |
75 |
76 | One former SAS man told the
77 |
78 | Express
79 |
80 | : “The men enact a kidnap situation, during which she will have been treated as a hostage, with the area being stormed by the SAS.”
81 |
82 |
83 | Meghan is expected to appear for the first time publicly in the company of the queen Monday, at the Commonwealth Day church service in London.
84 |
85 |
86 | `;
87 |
88 | exports[`React Mobiledoc renderer 0.3 Real content renders complex cheat mobiledocs 1`] = `
89 |
92 |
93 | Your smartphone is the IRL equivalent of a magic wand. It can do all sorts of extraordinary things, from tracking your footsteps to summoning Chinese takeout to your doorstep. But if you have the
94 |
97 | KlikR Universal Remote Control
98 |
99 | , it can also be the master of your household electronics, too. Here’s how it works? Stick this Kickstarter-funded, coin-sized accessory on or next to any remote-controlled infrared device, and you’ll immediately be able to control it via the companion app. This means you can turn on the AC, flip TV channels, and even bring your ancient ceiling fan back to life with just your phone. It's also equipped with voice commands, smart pausing and muting, and has the ability to organize all of your devices in your home, room by room. Normally, the KlikR Universal Remote Control costs $29, but
100 |
103 | you can get one today for only $19.99
104 |
105 | . Save an additional 10% when using code
106 |
107 | MADMARCH10
108 |
109 | at checkout.
110 |
111 |
112 |
113 |
116 | Scouted
117 |
118 |
121 | is here to surface products that you might like.
122 |
123 |
124 |
127 | Follow us on Flipboard
128 |
129 | for more.
130 |
133 | Please note that if you buy something featured in one of our posts, The Daily Beast may collect a share of sales.
134 |
135 |
136 |
137 |
138 | `;
139 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Renderer_0_3, { // eslint-disable-line
2 | MOBILE_DOC_VERSION_0_3_0,
3 | MOBILE_DOC_VERSION_0_3_1
4 | } from './renderers/0-3';
5 |
6 | export default class MobiledocReactRenderer {
7 | constructor ({ atoms = [], cards = [], markups = [], sections = [], additionalProps = {}, className = 'Mobiledoc' }) {
8 | this.options = {
9 | atoms,
10 | cards,
11 | markups,
12 | sections,
13 | className,
14 | additionalProps
15 | };
16 | }
17 |
18 | render (mobiledoc) {
19 | const { version } = mobiledoc;
20 | switch (version) {
21 | case MOBILE_DOC_VERSION_0_3_0:
22 | case MOBILE_DOC_VERSION_0_3_1:
23 | return new Renderer_0_3(mobiledoc, this.options).render();
24 | default:
25 | return null;
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/renderers/0-3.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import uuidV4 from 'uuid/v4';
3 |
4 | export const MOBILE_DOC_VERSION_0_3_0 = '0.3.0';
5 | export const MOBILE_DOC_VERSION_0_3_1 = '0.3.1';
6 |
7 | export const MARKUP_SECTION_TYPE = 1;
8 | export const IMAGE_SECTION_TYPE = 2;
9 | export const LIST_SECTION_TYPE = 3;
10 | export const CARD_SECTION_TYPE = 10;
11 |
12 | export const MARKUP_MARKER_TYPE = 0;
13 | export const ATOM_MARKER_TYPE = 1;
14 |
15 | export default class Renderer {
16 | constructor (mobiledoc, {
17 | atoms = [],
18 | cards = [],
19 | sections = [],
20 | markups = [],
21 | className,
22 | additionalProps = {}
23 | }) {
24 | this.mobiledoc = mobiledoc;
25 | this.className = className;
26 | this.atoms = atoms;
27 | this.cards = cards;
28 | this.markups = markups;
29 | this.sections = sections;
30 | this.additionalProps = additionalProps;
31 |
32 | this.renderCallbacks = [];
33 | }
34 |
35 | render () {
36 | const renderedSections = {this.renderSections()}
;
37 | this.renderCallbacks.forEach(cb => cb());
38 |
39 | return renderedSections;
40 | }
41 |
42 | renderSections () {
43 | return this.mobiledoc.sections.map((section, index) => this.renderSection(section, index));
44 | }
45 |
46 | renderSection (section, nodeKey) {
47 | const [type] = section;
48 |
49 | switch (type) {
50 | case MARKUP_SECTION_TYPE:
51 | return this.renderMarkupSection(section, nodeKey);
52 | case LIST_SECTION_TYPE:
53 | return this.renderListSection(section, nodeKey);
54 | case CARD_SECTION_TYPE:
55 | return this.renderCardSection(section, nodeKey);
56 | default:
57 | return null;
58 | }
59 | }
60 |
61 | renderMarkupSection ([type, TagName, markers], nodeKey) {
62 | let Element;
63 | const customSection = this.sections.find(s => s.name === TagName);
64 |
65 | if (customSection) {
66 | const Section = customSection.component;
67 | Element = ;
68 | } else {
69 | Element = {[]};
70 | }
71 |
72 | return this.renderMarkersOnElement(Element, markers);
73 | }
74 |
75 | renderListSection ([type, TagName, markers], nodeKey) {
76 | const items = markers.map((item, index) => this.renderMarkersOnElement({[]}, item));
77 |
78 | return {items};
79 | }
80 |
81 | renderAtomSection (atomIndex) {
82 | const [name, text, payload] = this.mobiledoc.atoms[atomIndex];
83 | const atom = this.atoms.find(a => a.name === name);
84 |
85 | if (atom) {
86 | const key = `${name}-${text.length}`;
87 | const env = {
88 | name,
89 | isInEditor: false,
90 | dom: 'dom'
91 | };
92 | const options = {};
93 | const props = {
94 | key,
95 | env,
96 | options,
97 | payload: { ...payload, ...this.additionalProps },
98 | text
99 | };
100 |
101 | return atom.render(props);
102 | }
103 |
104 | return null;
105 | }
106 |
107 | renderCardSection ([type, index], nodeKey) {
108 | const [name, payload] = this.mobiledoc.cards[index];
109 | const card = this.cards.find(c => c.name === name);
110 |
111 | if (card) {
112 | const env = {
113 | name,
114 | isInEditor: false,
115 | dom: 'dom',
116 | didRender: (callback) => this.registerRenderCallback(callback),
117 | onTeardown: (callback) => this.registerRenderCallback(callback)
118 | };
119 | const options = {};
120 | const props = {
121 | env,
122 | options,
123 | payload: { ...payload, key: nodeKey, ...this.additionalProps }
124 | };
125 |
126 | return card.render(props);
127 | }
128 |
129 | return null;
130 | }
131 |
132 | renderMarkersOnElement (element, markers) {
133 | const elements = [element];
134 | const pushElement = (openedElement) => {
135 | element.props.children.push(openedElement);
136 | elements.push(openedElement);
137 | element = openedElement;
138 | };
139 |
140 | markers.forEach(marker => {
141 | let [type, openTypes, closeCount, value] = marker; // eslint-disable-line prefer-const
142 |
143 | openTypes.forEach(openType => {
144 | const [TagName, attrs] = this.mobiledoc.markups[openType];
145 | const props = Object.assign({ children: [] }, this.parseProps(attrs));
146 |
147 | if (TagName) {
148 | const definedMarkup = this.markups.find(markup => markup.name === TagName);
149 | if (definedMarkup) {
150 | const { render: Markup } = definedMarkup;
151 | pushElement();
152 | } else {
153 | pushElement();
154 | }
155 | } else {
156 | closeCount -= 1;
157 | }
158 | });
159 |
160 | switch (type) {
161 | case MARKUP_MARKER_TYPE:
162 | element.props.children.push(value);
163 | break;
164 | case ATOM_MARKER_TYPE:
165 | element.props.children.push(this.renderAtomSection(value));
166 | break;
167 | default:
168 | }
169 |
170 | for (let j = 0, m = closeCount; j < m; j += 1) {
171 | elements.pop();
172 | element = elements[elements.length - 1];
173 | }
174 | });
175 |
176 | return element;
177 | }
178 |
179 | parseProps (attrs) {
180 | if (attrs) {
181 | return {
182 | [attrs[0]]: attrs[1]
183 | };
184 | }
185 |
186 | return null;
187 | }
188 |
189 | registerRenderCallback (cb) {
190 | this.renderCallbacks.push(cb);
191 | }
192 | }
193 |
--------------------------------------------------------------------------------