From bed4959a237f64caf0a75cc09795a3328c5681a9 Mon Sep 17 00:00:00 2001 From: Matthew Beale Date: Tue, 7 May 2019 09:20:50 -0400 Subject: [PATCH 1/3] Implement Mobiledoc 0.3.2 --- assets/demo/demo.js | 4 +- assets/demo/index.html | 16 +- package.json | 4 +- src/css/mobiledoc-kit.css | 21 + src/js/editor/editor.js | 4 + src/js/editor/post.js | 19 + src/js/models/_attributable.js | 25 ++ src/js/models/list-section.js | 6 +- src/js/models/markup-section.js | 7 +- src/js/models/post-node-builder.js | 8 +- src/js/parsers/mobiledoc/0-3-2.js | 176 ++++++++ src/js/parsers/mobiledoc/index.js | 4 + src/js/renderers/editor-dom.js | 14 +- src/js/renderers/mobiledoc/0-3-2.js | 159 +++++++ src/js/renderers/mobiledoc/index.js | 9 +- tests/acceptance/editor-list-test.js | 15 + tests/acceptance/editor-post-editor-test.js | 13 + tests/helpers/mobiledoc.js | 3 + tests/helpers/sections.js | 11 + tests/unit/editor/editor-test.js | 6 +- tests/unit/editor/post-test.js | 116 ++++++ tests/unit/models/card-test.js | 6 + tests/unit/models/list-section-test.js | 23 ++ tests/unit/models/markup-section-test.js | 22 + tests/unit/parsers/mobiledoc/0-3-2-test.js | 311 ++++++++++++++ tests/unit/renderers/mobiledoc/0-3-2-test.js | 413 +++++++++++++++++++ yarn.lock | 16 +- 27 files changed, 1402 insertions(+), 29 deletions(-) create mode 100644 src/js/models/_attributable.js create mode 100644 src/js/parsers/mobiledoc/0-3-2.js create mode 100644 src/js/renderers/mobiledoc/0-3-2.js create mode 100644 tests/helpers/sections.js create mode 100644 tests/unit/parsers/mobiledoc/0-3-2-test.js create mode 100644 tests/unit/renderers/mobiledoc/0-3-2-test.js diff --git a/assets/demo/demo.js b/assets/demo/demo.js index 9dd1a1f08..2e17a1798 100644 --- a/assets/demo/demo.js +++ b/assets/demo/demo.js @@ -37,9 +37,9 @@ let activateButtons = (parentSelector, editor) => { $(`${parentSelector} button`).click(function() { let button = $(this); let action = button.data('action'); - let arg = button.data('arg'); + let args = button.data('args').split(','); - editor[action](arg); + editor[action](...args); }); }; diff --git a/assets/demo/index.html b/assets/demo/index.html index 8c5e0ed83..6751da790 100644 --- a/assets/demo/index.html +++ b/assets/demo/index.html @@ -56,12 +56,16 @@
- - - - - - + + + + + + + + + +
diff --git a/package.json b/package.json index c77259b6b..a77b531a8 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ ], "license": "MIT", "dependencies": { - "mobiledoc-dom-renderer": "0.6.5", - "mobiledoc-text-renderer": "0.3.2" + "mobiledoc-dom-renderer": "0.7.0", + "mobiledoc-text-renderer": "0.4.0" }, "devDependencies": { "broccoli": "^1.1.3", diff --git a/src/css/mobiledoc-kit.css b/src/css/mobiledoc-kit.css index 7c56c6447..18af81e69 100644 --- a/src/css/mobiledoc-kit.css +++ b/src/css/mobiledoc-kit.css @@ -62,6 +62,27 @@ max-width: 100%; } +.__mobiledoc-editor [data-md-text-align='left'] { + text-align: left; +} + +.__mobiledoc-editor [data-md-text-align='center'] { + text-align: center; +} + +.__mobiledoc-editor [data-md-text-align='right'] { + text-align: right; +} + +.__mobiledoc-editor [data-md-text-align='justify'] { + text-align: justify; +} + +.__mobiledoc-editor ol, +.__mobiledoc-editor ul { + list-style-position: inside; +} + /** * Cards */ diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js index 838c04a9a..51e62a453 100644 --- a/src/js/editor/editor.js +++ b/src/js/editor/editor.js @@ -953,6 +953,10 @@ class Editor { this.run(postEditor => postEditor.toggleSection(tagName, this.range)); } + setAttribute(key, value) { + this.run(postEditor => postEditor.setAttribute(key, value, this.range)); + } + /** * Finds and runs the first matching key command for the event * diff --git a/src/js/editor/post.js b/src/js/editor/post.js index 3051078dd..32cacaef7 100644 --- a/src/js/editor/post.js +++ b/src/js/editor/post.js @@ -812,6 +812,25 @@ class PostEditor { this.setRange(nextRange); } + setAttribute(key, value, range) { + range = toRange(range); + let { post } = this.editor; + let attribute = `data-md-${key}`; + + post.walkMarkerableSections(range, section => { + if (section.isListItem) { + section = section.parent; + } + + if (section.getAttribute(attribute) !== value) { + section.setAttribute(attribute, value); + this._markDirty(section); + } + }); + + this.setRange(range); + } + _isSameSectionType(section, sectionTagName) { return section.isListItem ? section.parent.tagName === sectionTagName : diff --git a/src/js/models/_attributable.js b/src/js/models/_attributable.js new file mode 100644 index 000000000..bea980c61 --- /dev/null +++ b/src/js/models/_attributable.js @@ -0,0 +1,25 @@ +export const VALID_ATTRIBUTES = [ + 'data-md-text-align' +]; + +/* + * A "mixin" to add section attribute support + * to markup and list sections. + */ +export function attributable(ctx) { + ctx.attributes = {}; + + ctx.setAttribute = (key, value) => { + if (!VALID_ATTRIBUTES.includes(key)) { + throw new Error(`Invalid attribute "${key}" was passed. Constrain attributes to the spec-compliant whitelist.`); + } + ctx.attributes[key] = value; + }; + ctx.removeAttribute = key => { + delete ctx.attributes[key]; + }; + ctx.getAttribute = key => ctx.attributes[key]; + ctx.eachAttribute = cb => { + Object.entries(ctx.attributes).forEach(([k,v]) => cb(k,v)); + }; +} diff --git a/src/js/models/list-section.js b/src/js/models/list-section.js index bd0426c83..59efaf625 100644 --- a/src/js/models/list-section.js +++ b/src/js/models/list-section.js @@ -4,6 +4,7 @@ import { LIST_SECTION_TYPE } from './types'; import Section from './_section'; import { normalizeTagName } from '../utils/dom-utils'; import assert from '../utils/assert'; +import { attributable } from './_attributable'; export const VALID_LIST_SECTION_TAGNAMES = [ 'ul', 'ol' @@ -12,12 +13,15 @@ export const VALID_LIST_SECTION_TAGNAMES = [ export const DEFAULT_TAG_NAME = VALID_LIST_SECTION_TAGNAMES[0]; export default class ListSection extends Section { - constructor(tagName=DEFAULT_TAG_NAME, items=[]) { + constructor(tagName=DEFAULT_TAG_NAME, items=[], attributes={}) { super(LIST_SECTION_TYPE); this.tagName = tagName; this.isListSection = true; this.isLeafSection = false; + attributable(this); + Object.entries(attributes).forEach(([k,v]) => this.setAttribute(k, v)); + this.items = new LinkedList({ adoptItem: i => { assert(`Cannot insert non-list-item to list (is: ${i.type})`, diff --git a/src/js/models/markup-section.js b/src/js/models/markup-section.js index ad6b21c75..0766a19a7 100644 --- a/src/js/models/markup-section.js +++ b/src/js/models/markup-section.js @@ -2,6 +2,7 @@ import Markerable from './_markerable'; import { normalizeTagName } from '../utils/dom-utils'; import { contains } from '../utils/array-utils'; import { MARKUP_SECTION_TYPE } from './types'; +import { attributable } from './_attributable'; // valid values of `tagName` for a MarkupSection export const VALID_MARKUP_SECTION_TAGNAMES = [ @@ -33,8 +34,12 @@ export const MARKUP_SECTION_ELEMENT_NAMES = [ export const DEFAULT_TAG_NAME = VALID_MARKUP_SECTION_TAGNAMES[8]; const MarkupSection = class MarkupSection extends Markerable { - constructor(tagName=DEFAULT_TAG_NAME, markers=[]) { + constructor(tagName=DEFAULT_TAG_NAME, markers=[], attributes={}) { super(MARKUP_SECTION_TYPE, tagName, markers); + + attributable(this); + Object.entries(attributes).forEach(([k,v]) => this.setAttribute(k, v)); + this.isMarkupSection = true; } diff --git a/src/js/models/post-node-builder.js b/src/js/models/post-node-builder.js index 2ba5e7b21..16755d6a8 100644 --- a/src/js/models/post-node-builder.js +++ b/src/js/models/post-node-builder.js @@ -78,9 +78,9 @@ class PostNodeBuilder { * @param {Marker[]} [markers=[]] * @return {MarkupSection} */ - createMarkupSection(tagName=DEFAULT_MARKUP_SECTION_TAG_NAME, markers=[], isGenerated=false) { + createMarkupSection(tagName=DEFAULT_MARKUP_SECTION_TAG_NAME, markers=[], isGenerated=false, attributes={}) { tagName = normalizeTagName(tagName); - const section = new MarkupSection(tagName, markers); + const section = new MarkupSection(tagName, markers, attributes); if (isGenerated) { section.isGenerated = true; } @@ -88,9 +88,9 @@ class PostNodeBuilder { return section; } - createListSection(tagName=DEFAULT_LIST_SECTION_TAG_NAME, items=[]) { + createListSection(tagName=DEFAULT_LIST_SECTION_TAG_NAME, items=[], attributes={}) { tagName = normalizeTagName(tagName); - const section = new ListSection(tagName, items); + const section = new ListSection(tagName, items, attributes); section.builder = this; return section; } diff --git a/src/js/parsers/mobiledoc/0-3-2.js b/src/js/parsers/mobiledoc/0-3-2.js new file mode 100644 index 000000000..877df5692 --- /dev/null +++ b/src/js/parsers/mobiledoc/0-3-2.js @@ -0,0 +1,176 @@ +import { + MOBILEDOC_MARKUP_SECTION_TYPE, + MOBILEDOC_IMAGE_SECTION_TYPE, + MOBILEDOC_LIST_SECTION_TYPE, + MOBILEDOC_CARD_SECTION_TYPE, + MOBILEDOC_MARKUP_MARKER_TYPE, + MOBILEDOC_ATOM_MARKER_TYPE +} from 'mobiledoc-kit/renderers/mobiledoc/0-3-2'; +import { kvArrayToObject, filter } from "../../utils/array-utils"; +import assert from 'mobiledoc-kit/utils/assert'; + +/* + * Parses from mobiledoc -> post + */ +export default class MobiledocParser { + constructor(builder) { + this.builder = builder; + } + + /** + * @param {Mobiledoc} + * @return {Post} + */ + parse({ sections, markups: markerTypes, cards: cardTypes, atoms: atomTypes }) { + try { + const post = this.builder.createPost(); + + this.markups = []; + this.markerTypes = this.parseMarkerTypes(markerTypes); + this.cardTypes = this.parseCardTypes(cardTypes); + this.atomTypes = this.parseAtomTypes(atomTypes); + this.parseSections(sections, post); + + return post; + } catch (e) { + assert(`Unable to parse mobiledoc: ${e.message}`, false); + } + } + + parseMarkerTypes(markerTypes) { + return markerTypes.map((markerType) => this.parseMarkerType(markerType)); + } + + parseMarkerType([tagName, attributesArray]) { + const attributesObject = kvArrayToObject(attributesArray || []); + return this.builder.createMarkup(tagName, attributesObject); + } + + parseCardTypes(cardTypes) { + return cardTypes.map((cardType) => this.parseCardType(cardType)); + } + + parseCardType([cardName, cardPayload]) { + return [cardName, cardPayload]; + } + + parseAtomTypes(atomTypes) { + return atomTypes.map((atomType) => this.parseAtomType(atomType)); + } + + parseAtomType([atomName, atomValue, atomPayload]) { + return [atomName, atomValue, atomPayload]; + } + + parseSections(sections, post) { + sections.forEach((section) => this.parseSection(section, post)); + } + + parseSection(section, post) { + let [type] = section; + switch(type) { + case MOBILEDOC_MARKUP_SECTION_TYPE: + this.parseMarkupSection(section, post); + break; + case MOBILEDOC_IMAGE_SECTION_TYPE: + this.parseImageSection(section, post); + break; + case MOBILEDOC_CARD_SECTION_TYPE: + this.parseCardSection(section, post); + break; + case MOBILEDOC_LIST_SECTION_TYPE: + this.parseListSection(section, post); + break; + default: + assert('Unexpected section type ${type}', false); + } + } + + getAtomTypeFromIndex(index) { + const atomType = this.atomTypes[index]; + assert(`No atom definition found at index ${index}`, !!atomType); + return atomType; + } + + getCardTypeFromIndex(index) { + const cardType = this.cardTypes[index]; + assert(`No card definition found at index ${index}`, !!cardType); + return cardType; + } + + parseCardSection([, cardIndex], post) { + const [name, payload] = this.getCardTypeFromIndex(cardIndex); + const section = this.builder.createCardSection(name, payload); + post.sections.append(section); + } + + parseImageSection([, src], post) { + const section = this.builder.createImageSection(src); + post.sections.append(section); + } + + parseMarkupSection([, tagName, markers, attributesArray], post) { + const section = this.builder.createMarkupSection(tagName); + post.sections.append(section); + if (attributesArray) { + Object.entries(kvArrayToObject(attributesArray)).forEach(([key, value]) => { + section.setAttribute(key, value); + }); + } + this.parseMarkers(markers, section); + // Strip blank markers after they have been created. This ensures any + // markup they include has been correctly populated. + filter(section.markers, m => m.isBlank).forEach(m => { + section.markers.remove(m); + }); + } + + parseListSection([, tagName, items, attributesArray], post) { + const section = this.builder.createListSection(tagName); + post.sections.append(section); + if (attributesArray) { + Object.entries(kvArrayToObject(attributesArray)).forEach(([key, value]) => { + section.setAttribute(key, value); + }); + } + this.parseListItems(items, section); + } + + parseListItems(items, section) { + items.forEach(i => this.parseListItem(i, section)); + } + + parseListItem(markers, section) { + const item = this.builder.createListItem(); + this.parseMarkers(markers, item); + section.items.append(item); + } + + parseMarkers(markers, parent) { + markers.forEach(m => this.parseMarker(m, parent)); + } + + parseMarker([type, markerTypeIndexes, closeCount, value], parent) { + markerTypeIndexes.forEach(index => { + this.markups.push(this.markerTypes[index]); + }); + + const marker = this.buildMarkerType(type, value); + parent.markers.append(marker); + + this.markups = this.markups.slice(0, this.markups.length-closeCount); + } + + buildMarkerType(type, value) { + switch (type) { + case MOBILEDOC_MARKUP_MARKER_TYPE: + return this.builder.createMarker(value, this.markups.slice()); + case MOBILEDOC_ATOM_MARKER_TYPE: { + const [atomName, atomValue, atomPayload] = this.getAtomTypeFromIndex(value); + return this.builder.createAtom(atomName, atomValue, atomPayload, this.markups.slice()); + } + default: + assert(`Unexpected marker type ${type}`, false); + } + } +} diff --git a/src/js/parsers/mobiledoc/index.js b/src/js/parsers/mobiledoc/index.js index 5c5b3b7f4..5c111ca72 100644 --- a/src/js/parsers/mobiledoc/index.js +++ b/src/js/parsers/mobiledoc/index.js @@ -1,10 +1,12 @@ import MobiledocParser_0_2 from './0-2'; import MobiledocParser_0_3 from './0-3'; import MobiledocParser_0_3_1 from './0-3-1'; +import MobiledocParser_0_3_2 from './0-3-2'; import { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_2 } from 'mobiledoc-kit/renderers/mobiledoc/0-2'; import { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3 } from 'mobiledoc-kit/renderers/mobiledoc/0-3'; import { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3_1 } from 'mobiledoc-kit/renderers/mobiledoc/0-3-1'; +import { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3_2 } from 'mobiledoc-kit/renderers/mobiledoc/0-3-2'; import assert from 'mobiledoc-kit/utils/assert'; function parseVersion(mobiledoc) { @@ -21,6 +23,8 @@ export default { return new MobiledocParser_0_3(builder).parse(mobiledoc); case MOBILEDOC_VERSION_0_3_1: return new MobiledocParser_0_3_1(builder).parse(mobiledoc); + case MOBILEDOC_VERSION_0_3_2: + return new MobiledocParser_0_3_2(builder).parse(mobiledoc); default: assert(`Unknown version of mobiledoc parser requested: ${version}`, false); diff --git a/src/js/renderers/editor-dom.js b/src/js/renderers/editor-dom.js index f6aa500ab..3d4bb83e4 100644 --- a/src/js/renderers/editor-dom.js +++ b/src/js/renderers/editor-dom.js @@ -79,6 +79,12 @@ function penultimateParentOf(element, parentElement) { return element; } +function setSectionAttributesOnElement(section, element) { + section.eachAttribute((key, value) => { + element.setAttribute(key, value); + }); +} + function renderMarkupSection(section) { let element; if (MARKUP_SECTION_ELEMENT_NAMES.indexOf(section.tagName) !== -1) { @@ -88,11 +94,17 @@ function renderMarkupSection(section) { addClassName(element, section.tagName); } + setSectionAttributesOnElement(section, element); + return element; } function renderListSection(section) { - return document.createElement(section.tagName); + let element = document.createElement(section.tagName); + + setSectionAttributesOnElement(section, element); + + return element; } function renderListItem() { diff --git a/src/js/renderers/mobiledoc/0-3-2.js b/src/js/renderers/mobiledoc/0-3-2.js new file mode 100644 index 000000000..861697a31 --- /dev/null +++ b/src/js/renderers/mobiledoc/0-3-2.js @@ -0,0 +1,159 @@ +import {visit, visitArray, compile} from '../../utils/compiler'; +import { objectToSortedKVArray } from '../../utils/array-utils'; +import { + POST_TYPE, + MARKUP_SECTION_TYPE, + LIST_SECTION_TYPE, + LIST_ITEM_TYPE, + MARKER_TYPE, + MARKUP_TYPE, + IMAGE_SECTION_TYPE, + CARD_TYPE, + ATOM_TYPE +} from '../../models/types'; + +export const MOBILEDOC_VERSION = '0.3.2'; +export const MOBILEDOC_MARKUP_SECTION_TYPE = 1; +export const MOBILEDOC_IMAGE_SECTION_TYPE = 2; +export const MOBILEDOC_LIST_SECTION_TYPE = 3; +export const MOBILEDOC_CARD_SECTION_TYPE = 10; + +export const MOBILEDOC_MARKUP_MARKER_TYPE = 0; +export const MOBILEDOC_ATOM_MARKER_TYPE = 1; + +const visitor = { + [POST_TYPE](node, opcodes) { + opcodes.push(['openPost']); + visitArray(visitor, node.sections, opcodes); + }, + [MARKUP_SECTION_TYPE](node, opcodes) { + opcodes.push(['openMarkupSection', node.tagName, objectToSortedKVArray(node.attributes)]); + visitArray(visitor, node.markers, opcodes); + }, + [LIST_SECTION_TYPE](node, opcodes) { + opcodes.push(['openListSection', node.tagName, objectToSortedKVArray(node.attributes)]); + visitArray(visitor, node.items, opcodes); + }, + [LIST_ITEM_TYPE](node, opcodes) { + opcodes.push(['openListItem']); + visitArray(visitor, node.markers, opcodes); + }, + [IMAGE_SECTION_TYPE](node, opcodes) { + opcodes.push(['openImageSection', node.src]); + }, + [CARD_TYPE](node, opcodes) { + opcodes.push(['openCardSection', node.name, node.payload]); + }, + [MARKER_TYPE](node, opcodes) { + opcodes.push(['openMarker', node.closedMarkups.length, node.value]); + visitArray(visitor, node.openedMarkups, opcodes); + }, + [MARKUP_TYPE](node, opcodes) { + opcodes.push(['openMarkup', node.tagName, objectToSortedKVArray(node.attributes)]); + }, + [ATOM_TYPE](node, opcodes) { + opcodes.push(['openAtom', node.closedMarkups.length, node.name, node.value, node.payload]); + visitArray(visitor, node.openedMarkups, opcodes); + } +}; + +const postOpcodeCompiler = { + openMarker(closeCount, value) { + this.markupMarkerIds = []; + this.markers.push([ + MOBILEDOC_MARKUP_MARKER_TYPE, + this.markupMarkerIds, + closeCount, + value || '' + ]); + }, + openMarkupSection(tagName, attributes) { + this.markers = []; + this.sections.push([MOBILEDOC_MARKUP_SECTION_TYPE, tagName, this.markers, attributes]); + }, + openListSection(tagName, attributes) { + this.items = []; + this.sections.push([MOBILEDOC_LIST_SECTION_TYPE, tagName, this.items, attributes]); + }, + openListItem() { + this.markers = []; + this.items.push(this.markers); + }, + openImageSection(url) { + this.sections.push([MOBILEDOC_IMAGE_SECTION_TYPE, url]); + }, + openCardSection(name, payload) { + const index = this._addCardTypeIndex(name, payload); + this.sections.push([MOBILEDOC_CARD_SECTION_TYPE, index]); + }, + openAtom(closeCount, name, value, payload) { + const index = this._addAtomTypeIndex(name, value, payload); + this.markupMarkerIds = []; + this.markers.push([ + MOBILEDOC_ATOM_MARKER_TYPE, + this.markupMarkerIds, + closeCount, + index + ]); + }, + openPost() { + this.atomTypes = []; + this.cardTypes = []; + this.markerTypes = []; + this.sections = []; + this.result = { + version: MOBILEDOC_VERSION, + atoms: this.atomTypes, + cards: this.cardTypes, + markups: this.markerTypes, + sections: this.sections + }; + }, + openMarkup(tagName, attributes) { + const index = this._findOrAddMarkerTypeIndex(tagName, attributes); + this.markupMarkerIds.push(index); + }, + _addCardTypeIndex(cardName, payload) { + let cardType = [cardName, payload]; + this.cardTypes.push(cardType); + return this.cardTypes.length - 1; + }, + _addAtomTypeIndex(atomName, atomValue, payload) { + let atomType = [atomName, atomValue, payload]; + this.atomTypes.push(atomType); + return this.atomTypes.length - 1; + }, + _findOrAddMarkerTypeIndex(tagName, attributesArray) { + if (!this._markerTypeCache) { this._markerTypeCache = {}; } + const key = `${tagName}-${attributesArray.join('-')}`; + + let index = this._markerTypeCache[key]; + if (index === undefined) { + let markerType = [tagName]; + if (attributesArray.length) { markerType.push(attributesArray); } + this.markerTypes.push(markerType); + + index = this.markerTypes.length - 1; + this._markerTypeCache[key] = index; + } + + return index; + } +}; + +/** + * Render from post -> mobiledoc + */ +export default { + /** + * @param {Post} + * @return {Mobiledoc} + */ + render(post) { + let opcodes = []; + visit(visitor, post, opcodes); + let compiler = Object.create(postOpcodeCompiler); + compile(compiler, opcodes); + return compiler.result; + } +}; diff --git a/src/js/renderers/mobiledoc/index.js b/src/js/renderers/mobiledoc/index.js index e3e8b21e4..da1622900 100644 --- a/src/js/renderers/mobiledoc/index.js +++ b/src/js/renderers/mobiledoc/index.js @@ -1,9 +1,10 @@ import MobiledocRenderer_0_2, { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_2 } from './0-2'; import MobiledocRenderer_0_3, { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3 } from './0-3'; import MobiledocRenderer_0_3_1, { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3_1 } from './0-3-1'; +import MobiledocRenderer_0_3_2, { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3_2 } from './0-3-2'; import assert from 'mobiledoc-kit/utils/assert'; -export const MOBILEDOC_VERSION = MOBILEDOC_VERSION_0_3_1; +export const MOBILEDOC_VERSION = MOBILEDOC_VERSION_0_3_2; export default { render(post, version) { @@ -12,10 +13,12 @@ export default { return MobiledocRenderer_0_2.render(post); case MOBILEDOC_VERSION_0_3: return MobiledocRenderer_0_3.render(post); - case undefined: - case null: case MOBILEDOC_VERSION_0_3_1: return MobiledocRenderer_0_3_1.render(post); + case undefined: + case null: + case MOBILEDOC_VERSION_0_3_2: + return MobiledocRenderer_0_3_2.render(post); default: assert(`Unknown version of mobiledoc renderer requested: ${version}`, false); } diff --git a/tests/acceptance/editor-list-test.js b/tests/acceptance/editor-list-test.js index ab3ff4aae..15844c83f 100644 --- a/tests/acceptance/editor-list-test.js +++ b/tests/acceptance/editor-list-test.js @@ -448,3 +448,18 @@ test('selecting list item and deleting leaves following section intact', (assert assert.hasNoElement('#editor li:contains(abc)', 'li text is removed'); assert.hasElement('#editor li:contains(X)', 'text is inserted'); }); + +test('list sections may contain attributes', (assert) => { + const mobiledoc = Helpers.mobiledoc.build(({post, listSection, listItem, marker}) => { + return post([ + listSection('ul', [ + listItem([marker('abc')]), listItem() + ], {'data-md-text-align': 'center'}) + ]); + }); + + editor = new Editor({mobiledoc}); + editor.render(editorElement); + + assert.hasElement('#editor ul[data-md-text-align="center"]'); +}); diff --git a/tests/acceptance/editor-post-editor-test.js b/tests/acceptance/editor-post-editor-test.js index 7538c112c..58ea49203 100644 --- a/tests/acceptance/editor-post-editor-test.js +++ b/tests/acceptance/editor-post-editor-test.js @@ -265,3 +265,16 @@ test('postEditor reads editor range, sets it with #setRange', (assert) => { assert.ok(editor.range.isEqual(newRange), 'newRange is rendered after run'); }); + +test('markup sections may contain attributes', (assert) => { + const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) => { + return post([ + markupSection('p', [marker('123')], false, {'data-md-text-align': 'center'}) + ]); + }); + + editor = new Editor({mobiledoc}); + editor.render(editorElement); + + assert.hasElement('#editor p[data-md-text-align="center"]'); +}); diff --git a/tests/helpers/mobiledoc.js b/tests/helpers/mobiledoc.js index 9b22a017e..12ccb60ec 100644 --- a/tests/helpers/mobiledoc.js +++ b/tests/helpers/mobiledoc.js @@ -3,6 +3,7 @@ import mobiledocRenderers from 'mobiledoc-kit/renderers/mobiledoc'; import MobiledocRenderer_0_2, { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_2 } from 'mobiledoc-kit/renderers/mobiledoc/0-2'; import MobiledocRenderer_0_3, { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3 } from 'mobiledoc-kit/renderers/mobiledoc/0-3'; import MobiledocRenderer_0_3_1, { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3_1 } from 'mobiledoc-kit/renderers/mobiledoc/0-3-1'; +import MobiledocRenderer_0_3_2, { MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_3_2 } from 'mobiledoc-kit/renderers/mobiledoc/0-3-2'; import Editor from 'mobiledoc-kit/editor/editor'; import Range from 'mobiledoc-kit/utils/cursor/range'; import { mergeWithOptions } from 'mobiledoc-kit/utils/merge'; @@ -27,6 +28,8 @@ function build(treeFn, version) { return MobiledocRenderer_0_3.render(post); case MOBILEDOC_VERSION_0_3_1: return MobiledocRenderer_0_3_1.render(post); + case MOBILEDOC_VERSION_0_3_2: + return MobiledocRenderer_0_3_2.render(post); case undefined: case null: return mobiledocRenderers.render(post); diff --git a/tests/helpers/sections.js b/tests/helpers/sections.js new file mode 100644 index 000000000..9f3f53b75 --- /dev/null +++ b/tests/helpers/sections.js @@ -0,0 +1,11 @@ +export const VALID_ATTRIBUTES = [ + { key: 'data-md-text-align', value: 'center' }, + { key: 'data-md-text-align', value: 'justify' }, + { key: 'data-md-text-align', value: 'left' }, + { key: 'data-md-text-align', value: 'right' } +]; + +export const INVALID_ATTRIBUTES = [ + { key: 'data-foo', value: 'baz' } +]; + diff --git a/tests/unit/editor/editor-test.js b/tests/unit/editor/editor-test.js index c74b485ff..dc0a93559 100644 --- a/tests/unit/editor/editor-test.js +++ b/tests/unit/editor/editor-test.js @@ -189,6 +189,9 @@ test('#serialize serializes to MOBILEDOC_VERSION by default', (assert) => { let mobiledoc3_1 = Helpers.mobiledoc.build(({post, markupSection, marker}) => { return post([markupSection('p', [marker('abc')])]); }, '0.3.1'); + let mobiledoc3_2 = Helpers.mobiledoc.build(({post, markupSection, marker}) => { + return post([markupSection('p', [marker('abc')])]); + }, '0.3.2'); editor = Helpers.mobiledoc.renderInto(editorElement, ({post, markupSection, marker}) => { return post([markupSection('p', [marker('abc')])]); @@ -197,7 +200,8 @@ test('#serialize serializes to MOBILEDOC_VERSION by default', (assert) => { assert.deepEqual(editor.serialize('0.2.0'), mobiledoc2, 'serializes 0.2.0'); assert.deepEqual(editor.serialize('0.3.0'), mobiledoc3, 'serializes 0.3.0'); assert.deepEqual(editor.serialize('0.3.1'), mobiledoc3_1, 'serializes 0.3.1'); - assert.deepEqual(editor.serialize(), mobiledoc3_1, 'serializes 0.3.1 by default'); + assert.deepEqual(editor.serialize('0.3.2'), mobiledoc3_2, 'serializes 0.3.2'); + assert.deepEqual(editor.serialize(), mobiledoc3_2, 'serializes 0.3.2 by default'); assert.throws( () => editor.serialize('unknown'), diff --git a/tests/unit/editor/post-test.js b/tests/unit/editor/post-test.js index 9062b5360..2e5ceb08a 100644 --- a/tests/unit/editor/post-test.js +++ b/tests/unit/editor/post-test.js @@ -651,6 +651,122 @@ test('moveSectionDown moves it down', (assert) => { 'moveSectionDown is no-op when card is at bottom'); }); +test('#setAttribute on empty Mobiledoc does nothing', (assert) => { + let post = Helpers.postAbstract.build(({post, markupSection}) => { + return post([]); + }); + + mockEditor = renderBuiltAbstract(post, mockEditor); + const range = Range.blankRange(); + + postEditor = new PostEditor(mockEditor); + postEditor.setAttribute('text-align', 'center', range); + postEditor.complete(); + + assert.postIsSimilar(postEditor.editor.post, post); +}); + +test('#setAttribute sets attribute of a single section', (assert) => { + let post = Helpers.postAbstract.build(({post, markupSection}) => { + return post([markupSection('p')]); + }); + + mockEditor = renderBuiltAbstract(post, mockEditor); + const range = Range.create(post.sections.head, 0); + + assert.deepEqual( + post.sections.head.attributes, + {} + ); + + postEditor = new PostEditor(mockEditor); + postEditor.setAttribute('text-align', 'center', range); + postEditor.complete(); + + assert.deepEqual( + post.sections.head.attributes, + { + 'data-md-text-align': 'center' + } + ); +}); + +test('#setAttribute sets attribute of multiple sections', (assert) => { + let post = Helpers.postAbstract.build( + ({post, markupSection, marker, cardSection}) => { + return post([ + markupSection('p', [marker('abc')]), + cardSection('my-card'), + markupSection('p', [marker('123')]) + ]); + }); + + mockEditor = renderBuiltAbstract(post, mockEditor); + const range = Range.create(post.sections.head, 0, + post.sections.tail, 2); + + postEditor = new PostEditor(mockEditor); + postEditor.setAttribute('text-align', 'center', range); + postEditor.complete(); + + assert.deepEqual( + post.sections.head.attributes, + { + 'data-md-text-align': 'center' + } + ); + assert.ok(post.sections.objectAt(1).isCardSection); + assert.deepEqual( + post.sections.tail.attributes, + { + 'data-md-text-align': 'center' + } + ); +}); + +test('#setAttribute sets attribute of a single list', (assert) => { + let post = Helpers.postAbstract.build( + ({post, listSection, listItem, marker, markup}) => { + return post([listSection('ul', [ + listItem([marker('a')]), + listItem([marker('def')]), + ])]); + }); + + mockEditor = renderBuiltAbstract(post, mockEditor); + let range = Range.create(post.sections.head.items.head, 0); + + postEditor = new PostEditor(mockEditor); + postEditor.setAttribute('text-align', 'center', range); + postEditor.complete(); + + assert.deepEqual( + post.sections.head.attributes, + { + 'data-md-text-align': 'center' + } + ); +}); + +test('#setAttribute when cursor is in non-markerable section changes nothing', (assert) => { + let post = Helpers.postAbstract.build( + ({post, markupSection, marker, cardSection}) => { + return post([ + cardSection('my-card') + ]); + }); + + mockEditor = renderBuiltAbstract(post, mockEditor); + const range = post.sections.head.headPosition().toRange(); + + postEditor = new PostEditor(mockEditor); + postEditor.setAttribute('text-align', 'center', range); + postEditor.complete(); + + assert.ok(post.sections.head.isCardSection, 'card section not changed'); + assert.positionIsEqual(mockEditor._renderedRange.head, post.sections.head.headPosition()); +}); + test('#toggleSection changes single section to and from tag name', (assert) => { let post = Helpers.postAbstract.build(({post, markupSection}) => { return post([markupSection('p')]); diff --git a/tests/unit/models/card-test.js b/tests/unit/models/card-test.js index 54fc1a485..99b6ecdde 100644 --- a/tests/unit/models/card-test.js +++ b/tests/unit/models/card-test.js @@ -32,3 +32,9 @@ test('cloning a card copies payload', (assert) => { card.payload.foo = 'other foo'; assert.equal(card2.payload.foo, 'bar', 'card2 payload not updated'); }); + +test('card cannot have attributes', (assert) => { + const card = builder.createCardSection('card-name'); + + assert.equal(card.attributes, undefined); +}); diff --git a/tests/unit/models/list-section-test.js b/tests/unit/models/list-section-test.js index f6abce45d..4d8c97789 100644 --- a/tests/unit/models/list-section-test.js +++ b/tests/unit/models/list-section-test.js @@ -1,5 +1,6 @@ import PostNodeBuilder from 'mobiledoc-kit/models/post-node-builder'; import TestHelpers from '../../test-helpers'; +import { VALID_ATTRIBUTES, INVALID_ATTRIBUTES } from '../../helpers/sections'; const {module, test} = TestHelpers; @@ -13,6 +14,27 @@ module('Unit: List Section', { } }); +for (let attribute of VALID_ATTRIBUTES) { + // eslint-disable-next-line no-loop-func + test(`a section can have attribute "${attribute.key}" with value "${attribute.value}`, (assert) => { + const s1 = builder.createListSection('ol', [], { [attribute.key]: attribute.value }); + assert.deepEqual( + s1.attributes, + { [attribute.key]: attribute.value }, + 'Attribute set at instantiation' + ); + }); +} + +for (let attribute of INVALID_ATTRIBUTES) { + // eslint-disable-next-line no-loop-func + test(`a section throws when invalid attribute "${attribute.key}" is passed to a marker`, (assert) => { + assert.throws(() => { + builder.createListSection('ul', [], { [attribute.key]: attribute.value }); + }); + }); +} + test('cloning a list section creates the same type of list section', (assert) => { let item = builder.createListItem([builder.createMarker('abc')]); let list = builder.createListSection('ol', [item]); @@ -22,3 +44,4 @@ test('cloning a list section creates the same type of list section', (assert) => assert.equal(list.items.length, cloned.items.length); assert.equal(list.items.head.text, cloned.items.head.text); }); + diff --git a/tests/unit/models/markup-section-test.js b/tests/unit/models/markup-section-test.js index aaca33221..ca5868be6 100644 --- a/tests/unit/models/markup-section-test.js +++ b/tests/unit/models/markup-section-test.js @@ -1,6 +1,7 @@ import PostNodeBuilder from 'mobiledoc-kit/models/post-node-builder'; import Helpers from '../../test-helpers'; import Position from 'mobiledoc-kit/utils/cursor/position'; +import { VALID_ATTRIBUTES, INVALID_ATTRIBUTES } from '../../helpers/sections'; const {module, test} = Helpers; @@ -22,6 +23,27 @@ test('a section can append a marker', (assert) => { assert.equal(s1.markers.length, 1); }); +for (let attribute of VALID_ATTRIBUTES) { + // eslint-disable-next-line no-loop-func + test(`a section can have attribute "${attribute.key}" with value "${attribute.value}`, (assert) => { + const s1 = builder.createMarkupSection('P', [], false, { [attribute.key]: attribute.value }); + assert.deepEqual( + s1.attributes, + { [attribute.key]: attribute.value }, + 'Attribute set at instantiation' + ); + }); +} + +for (let attribute of INVALID_ATTRIBUTES) { + // eslint-disable-next-line no-loop-func + test(`a section throws when invalid attribute "${attribute.key}" is passed to a marker`, (assert) => { + assert.throws(() => { + builder.createMarkupSection('P', [], false, attribute); + }); + }); +} + test('#isBlank returns true if the text length is zero for two markers', (assert) => { const m1 = builder.createMarker(''); const m2 = builder.createMarker(''); diff --git a/tests/unit/parsers/mobiledoc/0-3-2-test.js b/tests/unit/parsers/mobiledoc/0-3-2-test.js new file mode 100644 index 000000000..dcca74672 --- /dev/null +++ b/tests/unit/parsers/mobiledoc/0-3-2-test.js @@ -0,0 +1,311 @@ +import MobiledocParser from 'mobiledoc-kit/parsers/mobiledoc/0-3-2'; +import { MOBILEDOC_VERSION } from 'mobiledoc-kit/renderers/mobiledoc/0-3-2'; +import PostNodeBuilder from 'mobiledoc-kit/models/post-node-builder'; + +const DATA_URL = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="; +import Helpers from '../../../test-helpers'; +const { module, test } = Helpers; + +let parser, builder, post; + +module('Unit: Parsers: Mobiledoc 0.3.2', { + beforeEach() { + builder = new PostNodeBuilder(); + parser = new MobiledocParser(builder); + post = builder.createPost(); + }, + afterEach() { + parser = null; + builder = null; + post = null; + } +}); + +test('#parse empty doc returns an empty post', (assert) => { + const mobiledoc = { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [] + }; + + const parsed = parser.parse(mobiledoc); + assert.equal(parsed.sections.length, 0, '0 sections'); +}); + +test('#parse empty markup section returns an empty post', (assert) => { + const mobiledoc = { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [ + [1, 'p', []] + ] + }; + + const section = builder.createMarkupSection('p'); + post.sections.append(section); + assert.deepEqual(parser.parse(mobiledoc), post); +}); + +test('#parse doc without marker types', (assert) => { + const mobiledoc = { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [ + [ + 1,'P', [[0, [], 0, 'hello world']] + ] + ] + }; + const parsed = parser.parse(mobiledoc); + + let section = builder.createMarkupSection('P', [], false); + let marker = builder.createMarker('hello world'); + section.markers.append(marker); + post.sections.append(section); + + assert.deepEqual( + parsed, + post + ); +}); + +test('#parse doc with blank marker', (assert) => { + const mobiledoc = { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [ + [ + 1,'P', [[0, [], 0, '']] + ] + ] + }; + const parsed = parser.parse(mobiledoc); + + let section = builder.createMarkupSection('P', [], false); + post.sections.append(section); + + assert.deepEqual( + parsed, + post + ); +}); + +test('#parse doc with marker type', (assert) => { + const mobiledoc = { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [ + ['B'], + ['A', ['href', 'google.com']] + ], + sections: [ + [ + 1,'P', [ + [0, [1], 0, 'hello'], // a tag open + [0, [0], 1, 'brave new'], // b tag open/close + [0, [], 1, 'world'] // a tag close + ] + ] + ] + }; + const parsed = parser.parse(mobiledoc); + + let section = builder.createMarkupSection('P', [], false); + let aMarkerType = builder.createMarkup('A', {href:'google.com'}); + let bMarkerType = builder.createMarkup('B'); + + let markers = [ + builder.createMarker('hello', [aMarkerType]), + builder.createMarker('brave new', [aMarkerType, bMarkerType]), + builder.createMarker('world', [aMarkerType]) + ]; + markers.forEach(marker => section.markers.append(marker)); + post.sections.append(section); + + assert.deepEqual( + parsed, + post + ); +}); + +test('#parse doc with image section', (assert) => { + const mobiledoc = { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [ + [2, DATA_URL] + ] + }; + + const parsed = parser.parse(mobiledoc); + + let section = builder.createImageSection(DATA_URL); + post.sections.append(section); + assert.deepEqual( + parsed, + post + ); +}); + +test('#parse doc with custom card type', (assert) => { + const mobiledoc = { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [ + ['custom-card', {}] + ], + markups: [], + sections: [ + [10, 0] + ] + }; + + const parsed = parser.parse(mobiledoc); + + let section = builder.createCardSection('custom-card'); + post.sections.append(section); + assert.deepEqual( + parsed, + post + ); +}); + +test('#parse doc with custom atom type', (assert) => { + const mobiledoc = { + version: MOBILEDOC_VERSION, + atoms: [ + ['mention', '@bob', { id: 42 }] + ], + cards: [], + markups: [], + sections: [ + [ + 1,'P', [ + [1, [], 0, 0] + ] + ] + ] + }; + + const parsed = parser.parse(mobiledoc); + + let section = builder.createMarkupSection('P', [], false); + let atom = builder.createAtom('mention', '@bob', { id: 42 }); + section.markers.append(atom); + post.sections.append(section); + + assert.deepEqual( + parsed, + post + ); +}); + +test('#parse a mobile doc with list-section and list-item', (assert) => { + const mobiledoc = { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [ + [3, 'ul', [ + [[0, [], 0, "first item"]], + [[0, [], 0, "second item"]] + ]] + ] + }; + + const parsed = parser.parse(mobiledoc); + + const items = [ + builder.createListItem([builder.createMarker('first item')]), + builder.createListItem([builder.createMarker('second item')]) + ]; + const section = builder.createListSection('ul', items); + post.sections.append(section); + assert.deepEqual( + parsed, + post + ); +}); + +test('#parse doc with paragraph with text alignment', (assert) => { + const mobiledoc = { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [ + [ + 1, + 'P', + [[0, [], 0, 'hello world']], + ['data-md-text-align', 'center'] + ] + ] + }; + + const parsed = parser.parse(mobiledoc); + + let section = builder.createMarkupSection( + 'P', + [], + false, + { 'data-md-text-align': 'center' } + ); + let marker = builder.createMarker('hello world'); + section.markers.append(marker); + post.sections.append(section); + + assert.deepEqual( + parsed, + post + ); +}); + +test('#parse a mobile doc with list-section with text align', (assert) => { + const mobiledoc = { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [ + [ + 3, + 'ul', + [ + [[0, [], 0, "first item"]], + [[0, [], 0, "second item"]] + ], + ['data-md-text-align', 'center'] + ] + ] + }; + + const parsed = parser.parse(mobiledoc); + + const items = [ + builder.createListItem([builder.createMarker('first item')]), + builder.createListItem([builder.createMarker('second item')]) + ]; + const section = builder.createListSection( + 'ul', + items, + { 'data-md-text-align': 'center' } + ); + post.sections.append(section); + assert.deepEqual( + parsed, + post + ); +}); diff --git a/tests/unit/renderers/mobiledoc/0-3-2-test.js b/tests/unit/renderers/mobiledoc/0-3-2-test.js new file mode 100644 index 000000000..f18857886 --- /dev/null +++ b/tests/unit/renderers/mobiledoc/0-3-2-test.js @@ -0,0 +1,413 @@ +import MobiledocRenderer, { MOBILEDOC_VERSION } from 'mobiledoc-kit/renderers/mobiledoc/0-3-2'; +import PostNodeBuilder from 'mobiledoc-kit/models/post-node-builder'; +import { normalizeTagName } from 'mobiledoc-kit/utils/dom-utils'; +import Helpers from '../../../test-helpers'; + +const { module, test } = Helpers; +function render(post) { + return MobiledocRenderer.render(post); +} +let builder; + +module('Unit: Mobiledoc Renderer 0.3.2', { + beforeEach() { + builder = new PostNodeBuilder(); + } +}); + +test('renders a blank post', (assert) => { + let post = builder.createPost(); + let mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [] + }); +}); + +test('renders a post with marker', (assert) => { + const post = Helpers.postAbstract.build(({post, markupSection, marker, markup}) => { + return post([ + markupSection('p', [marker('Hi', [markup('strong')])]) + ]); + }); + const mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [['strong']], + sections: [ + [1, normalizeTagName('P'), [[0, [0], 1, 'Hi']], []] + ] + }); +}); + +test('renders a post section with markers sharing a markup', (assert) => { + const post = Helpers.postAbstract.build(({post, markupSection, marker, markup}) => { + const strong = markup('strong'); + return post([ + markupSection('p', [marker('Hi', [strong]), marker(' Guy', [strong])]) + ]); + }); + let mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [['strong']], + sections: [ + [ + 1, + normalizeTagName('P'), + [ + [0, [0], 0, 'Hi'], + [0, [], 1, ' Guy'] + ], + [] + ] + ] + }); +}); + +test('renders a post with markers with markers with complex attributes', (assert) => { + let link1,link2; + const post = Helpers.postAbstract.build(({post, markupSection, marker, markup}) => { + link1 = markup('a', {href:'bustle.com'}); + link2 = markup('a', {href:'other.com'}); + return post([ + markupSection('p', [ + marker('Hi', [link1]), + marker(' Guy', [link2]), + marker(' other guy', [link1]) + ]) + ]); + }); + let mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [ + ['a', ['href', 'bustle.com']], + ['a', ['href', 'other.com']] + ], + sections: [ + [ + 1, + normalizeTagName('P'), + [ + [0, [0], 1, 'Hi'], + [0, [1], 1, ' Guy'], + [0, [0], 1, ' other guy'] + ], + [] + ] + ] + }); +}); + +test('renders a post with image', (assert) => { + let url = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="; + let post = builder.createPost(); + let section = builder.createImageSection(url); + post.sections.append(section); + + let mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [ + [2, url] + ] + }); +}); + +test('renders a post with image and null src', (assert) => { + let post = builder.createPost(); + let section = builder.createImageSection(); + post.sections.append(section); + + let mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [ + [2, null] + ] + }); +}); + +test('renders a post with atom', (assert) => { + const post = Helpers.postAbstract.build(({post, markupSection, marker, atom}) => { + return post([ + markupSection('p', [ + marker('Hi'), + atom('mention', '@bob', { id: 42 }) + ]) + ]); + }); + + let mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [ + ['mention', '@bob', { id: 42 }] + ], + cards: [], + markups: [], + sections: [ + [ + 1, + normalizeTagName('P'), + [ + [0, [], 0, 'Hi'], + [1, [], 0, 0] + ], + [] + ] + ] + }); +}); + +test('renders a post with atom and markup', (assert) => { + const post = Helpers.postAbstract.build(({post, markupSection, marker, markup, atom}) => { + const strong = markup('strong'); + return post([ + markupSection('p', [ + atom('mention', '@bob', { id: 42 }, [strong]) + ]) + ]); + }); + + let mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [ + ['mention', '@bob', { id: 42 }] + ], + cards: [], + markups: [['strong']], + sections: [ + [ + 1, + normalizeTagName('P'), + [ + [1, [0], 1, 0] + ], + [] + ] + ] + }); +}); + +test('renders a post with atom inside markup', (assert) => { + const post = Helpers.postAbstract.build(({post, markupSection, marker, markup, atom}) => { + const strong = markup('strong'); + return post([ + markupSection('p', [ + marker('Hi ', [strong]), + atom('mention', '@bob', { id: 42 }, [strong]), + marker(' Bye', [strong]) + ]) + ]); + }); + + let mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [ + ['mention', '@bob', { id: 42 }] + ], + cards: [], + markups: [['strong']], + sections: [ + [ + 1, + normalizeTagName('P'), + [ + [0, [0], 0, 'Hi '], + [1, [], 0, 0], + [0, [], 1, ' Bye'] + ], + [] + ] + ] + }); +}); + +test('renders a post with card', (assert) => { + let cardName = 'super-card'; + let payload = { bar: 'baz' }; + let post = builder.createPost(); + let section = builder.createCardSection(cardName, payload); + post.sections.append(section); + + let mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [ + [cardName, payload] + ], + markups: [], + sections: [ + [10, 0] + ] + }); +}); + +test('renders a post with multiple cards with identical payloads', (assert) => { + let cardName = 'super-card'; + let payload1 = { bar: 'baz' }; + let payload2 = { bar: 'baz' }; + let post = builder.createPost(); + + let section1 = builder.createCardSection(cardName, payload1); + post.sections.append(section1); + + let section2 = builder.createCardSection(cardName, payload2); + post.sections.append(section2); + + let mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [ + [cardName, payload1], + [cardName, payload2] + ], + markups: [], + sections: [ + [10, 0], + [10, 1] + ] + }); +}); + +test('renders a post with cards with differing payloads', (assert) => { + let cardName = 'super-card'; + let payload1 = { bar: 'baz1' }; + let payload2 = { bar: 'baz2' }; + let post = builder.createPost(); + + let section1 = builder.createCardSection(cardName, payload1); + post.sections.append(section1); + + let section2 = builder.createCardSection(cardName, payload2); + post.sections.append(section2); + + let mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [ + [cardName, payload1], + [cardName, payload2] + ], + markups: [], + sections: [ + [10, 0], + [10, 1] + ] + }); +}); + +test('renders a post with a list', (assert) => { + const items = [ + builder.createListItem([builder.createMarker('first item')]), + builder.createListItem([builder.createMarker('second item')]) + ]; + const section = builder.createListSection('ul', items); + const post = builder.createPost([section]); + + const mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [ + [ + 3, + 'ul', + [ + [[0, [], 0, 'first item']], + [[0, [], 0, 'second item']] + ], + [] + ] + ] + }); +}); + +test('renders an aside as markup section', (assert) => { + const post = Helpers.postAbstract.build(({post, markupSection, marker}) => { + return post([markupSection('aside', [marker('abc')])]); + }); + const mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [ + [ + 1, + 'aside', + [[0, [], 0, 'abc']], + [] + ] + ] + }); +}); + +test('renders a post with a paragraph with attribute', (assert) => { + const post = Helpers.postAbstract.build(({post, markupSection, marker, markup}) => { + return post([ + markupSection('p', [], true, { 'data-md-text-align': 'center' }) + ]); + }); + const mobiledoc = render(post); + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [ + [ + 1, + normalizeTagName('P'), + [], + ['data-md-text-align', 'center'] + ] + ] + }); +}); + +test('renders a post with a list with attribute', (assert) => { + const section = builder.createListSection('ul', [], { 'data-md-text-align': 'center' }); + const post = builder.createPost([section]); + const mobiledoc = render(post); + + assert.deepEqual(mobiledoc, { + version: MOBILEDOC_VERSION, + atoms: [], + cards: [], + markups: [], + sections: [ + [ + 3, + normalizeTagName('UL'), + [], + ['data-md-text-align', 'center'] + ] + ] + }); +}); diff --git a/yarn.lock b/yarn.lock index 3c5aafeb3..c8c8643c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4312,15 +4312,15 @@ mktemp@~0.4.0: resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b" integrity sha1-bQUVYRyKjITkhKogABKbmOmB/ws= -mobiledoc-dom-renderer@0.6.5: - version "0.6.5" - resolved "https://registry.yarnpkg.com/mobiledoc-dom-renderer/-/mobiledoc-dom-renderer-0.6.5.tgz#56c0302c4f9c30840ab5b9b20dfe905aed1e437b" - integrity sha1-VsAwLE+cMIQKtbmyDf6QWu0eQ3s= +mobiledoc-dom-renderer@0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/mobiledoc-dom-renderer/-/mobiledoc-dom-renderer-0.7.0.tgz#53ab5f14dd612b16f03513390e5cbcc2b89f6979" + integrity sha512-A+gT6D4Ru3DKY7ZYOBRORmwhRJ7rDj2vy75D2dWuZS5NgX0mCmGs0yN7qs48YlxvfCif8RFpYsaaPg6Kc3MdJg== -mobiledoc-text-renderer@0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/mobiledoc-text-renderer/-/mobiledoc-text-renderer-0.3.2.tgz#126a167a6cf8b6cd7e58c85feb18043603834580" - integrity sha1-EmoWemz4ts1+WMhf6xgENgODRYA= +mobiledoc-text-renderer@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/mobiledoc-text-renderer/-/mobiledoc-text-renderer-0.4.0.tgz#473fbe50aa6cde2c3b449752f3b984834dc824d2" + integrity sha512-+Tzfo0hhUFxS0n5FWZ0nf6WUrvnVmsxaIdq0CyeLYD1lk8oW2ml+6WLdeLlzKM5OYYi3PWV6NR9HCUG01cdvWQ== modify-values@^1.0.0: version "1.0.1" From 672d13eb30475ce90c11c6ff7859b6e5d99b7de1 Mon Sep 17 00:00:00 2001 From: Yoran Brondsema Date: Mon, 15 Jul 2019 12:33:20 +0200 Subject: [PATCH 2/3] Replace text for buttons in demo editor with icons --- assets/demo/demo.css | 20 +++++++++++++++++++ assets/demo/index.html | 45 ++++++++++++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/assets/demo/demo.css b/assets/demo/demo.css index 063dedd74..1ec85485d 100644 --- a/assets/demo/demo.css +++ b/assets/demo/demo.css @@ -125,6 +125,26 @@ table tr { .toolbar { text-align: center; + display: flex; +} + +.toolbar-section { + display: flex; + margin: 0 12px; +} +.toolbar-section:first-child { margin-left: 0; } +.toolbar-section:last-child { margin-right: 0; } + +.toolbar-section button { + display: flex; + align-items: center; + margin: 0 4px; +} +.toolbar-section button:first-child { margin-left: 0; } +.toolbar-section button:last-child { margin-right: 0; } + +.toolbar-section button svg { + height: 24px; } #editor-wrapper { diff --git a/assets/demo/index.html b/assets/demo/index.html index 6751da790..b7c20eab2 100644 --- a/assets/demo/index.html +++ b/assets/demo/index.html @@ -56,16 +56,41 @@
- - - - - - - - - - + +
+ + +
+
+ + +
+
+ + +
+
+ + + + +
From aae978d54bdd30885598caa39a5a28a69d540595 Mon Sep 17 00:00:00 2001 From: Yoran Brondsema Date: Tue, 16 Jul 2019 14:57:55 +0200 Subject: [PATCH 3/3] Document the new setAttribute API --- assets/demo/index.html | 6 ++++-- src/js/editor/editor.js | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/assets/demo/index.html b/assets/demo/index.html index b7c20eab2..afa336134 100644 --- a/assets/demo/index.html +++ b/assets/demo/index.html @@ -202,8 +202,7 @@

Basic Editor

Toolbar Buttons

- Use the editor.toggleMarkup and editor.toggleSelection methods to modify - the text. + Use the editor.toggleMarkup, editor.toggleSection and editor.setAttribute methods to modify the text.

@@ -215,6 +214,9 @@

Toolbar Buttons

$('button.strong').click(() => { editor.toggleMarkup('strong'); }); + $('button.center').click(() => { + editor.setAttribute('text-align', 'center'); + });
diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js index 51e62a453..2bad7054c 100644 --- a/src/js/editor/editor.js +++ b/src/js/editor/editor.js @@ -953,6 +953,14 @@ class Editor { this.run(postEditor => postEditor.toggleSection(tagName, this.range)); } + /** + * Sets an attribute for the current active section(s). + * + * @param {String} key The attribute. The only valid attribute is 'text-align'. + * @param {String} value The value of the attribute. + * @public + * @see PostEditor#setAttribute + */ setAttribute(key, value) { this.run(postEditor => postEditor.setAttribute(key, value, this.range)); }