diff --git a/lib/renderer-factory.js b/lib/renderer-factory.js index d8f0fd3..b00ca5f 100644 --- a/lib/renderer-factory.js +++ b/lib/renderer-factory.js @@ -3,7 +3,8 @@ } from './renderers/0-2'; import Renderer_0_3, { MOBILEDOC_VERSION_0_3_0, - MOBILEDOC_VERSION_0_3_1 + MOBILEDOC_VERSION_0_3_1, + MOBILEDOC_VERSION_0_3_2 } from './renderers/0-3'; import RENDER_TYPE from './utils/render-type'; @@ -89,6 +90,7 @@ return new Renderer_0_2(mobiledoc, this.options).render(); case MOBILEDOC_VERSION_0_3_0: case MOBILEDOC_VERSION_0_3_1: + case MOBILEDOC_VERSION_0_3_2: return new Renderer_0_3(mobiledoc, this.options).render(); default: throw new Error(`Unexpected Mobiledoc version "${version}"`); diff --git a/lib/renderers/0-3.js b/lib/renderers/0-3.js index b1297c5..9aa97af 100644 --- a/lib/renderers/0-3.js +++ b/lib/renderers/0-3.js @@ -26,7 +26,7 @@ import { export const MOBILEDOC_VERSION_0_3_0 = '0.3.0'; export const MOBILEDOC_VERSION_0_3_1 = '0.3.1'; -export const MOBILEDOC_VERSION = MOBILEDOC_VERSION_0_3_0; +export const MOBILEDOC_VERSION_0_3_2 = '0.3.2'; const IMAGE_SECTION_TAG_NAME = 'img'; @@ -34,6 +34,7 @@ function validateVersion(version) { switch (version) { case MOBILEDOC_VERSION_0_3_0: case MOBILEDOC_VERSION_0_3_1: + case MOBILEDOC_VERSION_0_3_2: return; default: throw new Error(`Unexpected Mobiledoc version "${version}"`); @@ -374,14 +375,15 @@ export default class Renderer { return rendered || createTextNode(this.dom, ''); } - renderMarkupSection([type, tagName, markers]) { + renderMarkupSection([type, tagName, markers, attributes = []]) { tagName = tagName.toLowerCase(); if (!isValidSectionTagName(tagName, MARKUP_SECTION_TYPE)) { return; } + let attrsObj = reduceAttributes(attributes); let renderer = this.sectionElementRendererFor(tagName); - let element = renderer(tagName, this.dom); + let element = renderer(tagName, this.dom, attrsObj); this.renderMarkersOnElement(element, markers); return element; diff --git a/lib/utils/array-utils.js b/lib/utils/array-utils.js index be194a4..b43f93d 100644 --- a/lib/utils/array-utils.js +++ b/lib/utils/array-utils.js @@ -7,3 +7,35 @@ export function includes(array, detectValue) { } return false; } + + +/** + * @param {Array} array of key1,value1,key2,value2,... + * @return {Object} {key1:value1, key2:value2, ...} + * @private + */ +export function kvArrayToObject(array) { + if (!Array.isArray(array)) { return {}; } + + const obj = {}; + for (let i = 0; i < array.length; i+=2) { + let [key, value] = [array[i], array[i+1]]; + obj[key] = value; + } + return obj; +} + +/** + * @param {Object} {key1:value1, key2:value2, ...} + * @return {Array} array of key1,value1,key2,value2,... + * @private + */ +export function objectToSortedKVArray(obj) { + const keys = Object.keys(obj).sort(); + const result = []; + keys.forEach(k => { + result.push(k); + result.push(obj[k]); + }); + return result; +} diff --git a/lib/utils/element-utils.js b/lib/utils/element-utils.js new file mode 100644 index 0000000..b9d3f68 --- /dev/null +++ b/lib/utils/element-utils.js @@ -0,0 +1,14 @@ +import { dasherize } from './string-utils'; + +function setData(element, name, value) { + if (element.dataset) { + element.dataset[name] = value; + } else { + const dataName = `data-${dasherize(name)}`; + return element.setAttribute(dataName, value); + } +} + +export { + setData +}; diff --git a/lib/utils/render-utils.js b/lib/utils/render-utils.js index be799b8..aeffeef 100644 --- a/lib/utils/render-utils.js +++ b/lib/utils/render-utils.js @@ -4,11 +4,33 @@ import { import { sanitizeHref } from './sanitization-utils'; +import { setData } from './element-utils'; +import { camelize } from './string-utils'; -export function defaultSectionElementRenderer(tagName, dom) { +export const VALID_ATTRIBUTES = [ + 'data-md-text-align' +]; + +function _isValidAttribute(attr) { + return VALID_ATTRIBUTES.indexOf(attr) !== -1; +} + +function handleMarkupSectionAttribute(element, attributeKey, attributeValue) { + if (!_isValidAttribute(attributeKey)) { + throw new Error(`Cannot use attribute: ${attributeKey}`); + } + + setData(element, camelize(attributeKey.replace('data-', '')), attributeValue); +} + +export function defaultSectionElementRenderer(tagName, dom, attrsObj = {}) { let element; if (isMarkupSectionElementName(tagName)) { element = dom.createElement(tagName); + + Object.keys(attrsObj).forEach(k => { + handleMarkupSectionAttribute(element, k, attrsObj[k]); + }); } else { element = dom.createElement('div'); element.setAttribute('class', tagName); diff --git a/lib/utils/string-utils.js b/lib/utils/string-utils.js new file mode 100644 index 0000000..115663e --- /dev/null +++ b/lib/utils/string-utils.js @@ -0,0 +1,29 @@ +function dasherize(string) { + return string.replace(/[A-Z]/g, (match, offset) => { + const lower = match.toLowerCase(); + + return (offset === 0 ? lower : '-' + lower); + }); +} + +function camelize(text, separator = '-') { + const words = text.split(separator); + + let result = ""; + for (let i = 0 ; i < words.length ; i += 1) { + if (i === 0) { + result += words[0].toLowerCase(); + } else { + const word = words[i]; + const capitalizedWord = word.charAt(0).toUpperCase() + word.slice(1); + result += capitalizedWord; + } + } + + return result; +} + +export { + dasherize, + camelize +}; diff --git a/tests/helpers/create-mobiledoc.js b/tests/helpers/create-mobiledoc.js index 2fbc2a2..febfdbb 100644 --- a/tests/helpers/create-mobiledoc.js +++ b/tests/helpers/create-mobiledoc.js @@ -1,4 +1,4 @@ -const MOBILEDOC_VERSION_0_3_1 = '0.3.1'; +const MOBILEDOC_VERSION_0_3_2 = '0.3.2'; import { MARKUP_SECTION_TYPE, CARD_SECTION_TYPE @@ -8,7 +8,9 @@ import { ATOM_MARKER_TYPE } from 'mobiledoc-dom-renderer/utils/marker-types'; -export function createBlankMobiledoc({version=MOBILEDOC_VERSION_0_3_1}={}) { +import { objectToSortedKVArray } from 'mobiledoc-dom-renderer/utils/array-utils'; + +export function createBlankMobiledoc({version=MOBILEDOC_VERSION_0_3_2}={}) { return { version, atoms: [], @@ -18,7 +20,7 @@ export function createBlankMobiledoc({version=MOBILEDOC_VERSION_0_3_1}={}) { }; } -export function createMobiledocWithAtom({version=MOBILEDOC_VERSION_0_3_1, atom}={}) { +export function createMobiledocWithAtom({version=MOBILEDOC_VERSION_0_3_2, atom}={}) { return { version, atoms: [atom], @@ -32,7 +34,7 @@ export function createMobiledocWithAtom({version=MOBILEDOC_VERSION_0_3_1, atom}= }; } -export function createMobiledocWithCard({version=MOBILEDOC_VERSION_0_3_1, card}={}) { +export function createMobiledocWithCard({version=MOBILEDOC_VERSION_0_3_2, card}={}) { return { version, atoms: [], @@ -46,7 +48,7 @@ export function createMobiledocWithCard({version=MOBILEDOC_VERSION_0_3_1, card}= }; } -export function createSimpleMobiledoc({sectionName='p', text='hello world', markup=null, version=MOBILEDOC_VERSION_0_3_1}={}) { +export function createSimpleMobiledoc({sectionName='p', text='hello world', markup=null, version=MOBILEDOC_VERSION_0_3_2, attributes={}}={}) { let openedMarkups = markup ? [0] : []; let closedMarkups = markup ? 1 : 0; let markups = markup ? [markup] : []; @@ -58,8 +60,10 @@ export function createSimpleMobiledoc({sectionName='p', text='hello world', mark markups: markups, sections: [ [MARKUP_SECTION_TYPE, sectionName, [ - [MARKUP_MARKER_TYPE, openedMarkups, closedMarkups, text]] - ] + [MARKUP_MARKER_TYPE, openedMarkups, closedMarkups, text] + ], + objectToSortedKVArray(attributes) + ], ] }; } diff --git a/tests/unit/renderers/0-3-test.js b/tests/unit/renderers/0-3-test.js index cba2f3c..a78488a 100644 --- a/tests/unit/renderers/0-3-test.js +++ b/tests/unit/renderers/0-3-test.js @@ -29,6 +29,7 @@ import { const { test, module } = QUnit; const MOBILEDOC_VERSION_0_3_0 = '0.3.0'; const MOBILEDOC_VERSION_0_3_1 = '0.3.1'; +const MOBILEDOC_VERSION_0_3_2 = '0.3.2'; const dataUri = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="; @@ -71,6 +72,36 @@ test('renders 0.3.0 markup section "pull-quote" as div with class', (assert) => assert.equal(outerHTML(sectionEl), '
hello world
'); +}); + +test('throws when given invalid attribute', (assert) => { + let mobiledoc = createSimpleMobiledoc({ + version: MOBILEDOC_VERSION_0_3_2, + sectionName: 'p', + text: 'hello world', + attributes: { 'data-md-bad-attribute': 'something' } + }); + + assert.throws( + () => { renderer.render(mobiledoc) }, // jshint ignore: line + new RegExp(`Cannot use attribute: data-md-bad-attribute`) + ); +}); + test('renders 0.3.1 markup section "pull-quote" as div with class', (assert) => { let mobiledoc = createSimpleMobiledoc({ version: MOBILEDOC_VERSION_0_3_1, diff --git a/tests/unit/utils/array-utils-test.js b/tests/unit/utils/array-utils-test.js new file mode 100644 index 0000000..3342661 --- /dev/null +++ b/tests/unit/utils/array-utils-test.js @@ -0,0 +1,52 @@ +/* global QUnit */ + +import { + kvArrayToObject, + objectToSortedKVArray +} from 'mobiledoc-dom-renderer/utils/array-utils'; + +const { test, module } = QUnit; + +module('Unit: Mobiledoc DOM Renderer - Array utils'); + +test('#kvArrayToObject', (assert) => { + assert.deepEqual( + kvArrayToObject([]), + {} + ); + assert.deepEqual( + kvArrayToObject(['data-md-text-align', 'center']), + { + 'data-md-text-align': 'center' + } + ); +}); + +test('#objectToSortedKVArray', (assert) => { + assert.deepEqual( + objectToSortedKVArray({}), + [] + ); + assert.deepEqual( + objectToSortedKVArray( + { + 'data-md-text-align': 'center' + } + ), + [ + 'data-md-text-align', 'center' + ] + ); + assert.deepEqual( + objectToSortedKVArray( + { + 'data-md-text-align': 'center', + 'data-md-color': 'red' + } + ), + [ + 'data-md-color', 'red', + 'data-md-text-align', 'center' + ] + ); +}); diff --git a/tests/unit/utils/element-utils-test.js b/tests/unit/utils/element-utils-test.js new file mode 100644 index 0000000..446612e --- /dev/null +++ b/tests/unit/utils/element-utils-test.js @@ -0,0 +1,33 @@ +/* global QUnit, SimpleDOM */ + +import { + setData +} from 'mobiledoc-dom-renderer/utils/element-utils'; + +const { test, module } = QUnit; + +module('Unit: Mobiledoc DOM Renderer - Element utils'); + +test('#setData - in browser', (assert) => { + const element = document.createElement('p'); + + setData(element, 'mdTextAlign', 'center'); + + assert.deepEqual( + JSON.parse(JSON.stringify(element.dataset)), + { + 'mdTextAlign': 'center' + } + ); +}); + +test('#setData - in SimpleDOM', (assert) => { + const element = (new SimpleDOM.Document()).createElement('p'); + + setData(element, 'mdTextAlign', 'center'); + + assert.equal( + element.getAttribute('data-md-text-align'), + 'center' + ); +}); diff --git a/tests/unit/utils/string-utils-test.js b/tests/unit/utils/string-utils-test.js new file mode 100644 index 0000000..b22dffc --- /dev/null +++ b/tests/unit/utils/string-utils-test.js @@ -0,0 +1,24 @@ +/* global QUnit */ + +import { + dasherize, + camelize +} from 'mobiledoc-dom-renderer/utils/string-utils'; + +const { test, module } = QUnit; + +module('Unit: Mobiledoc DOM Renderer - String utils'); + +test('#dasherize', (assert) => { + assert.equal( + dasherize('mdTextAlign'), + 'md-text-align' + ); +}); + +test('#camelize', (assert) => { + assert.equal( + camelize('md-text-align'), + 'mdTextAlign' + ); +});