Skip to content

Commit

Permalink
refactor to handle new cards
Browse files Browse the repository at this point in the history
fixes #17
  • Loading branch information
bantic committed Nov 19, 2015
1 parent 7f942c6 commit 99279d5
Show file tree
Hide file tree
Showing 6 changed files with 454 additions and 203 deletions.
23 changes: 8 additions & 15 deletions lib/cards/image.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import {
createElement,
appendChild
} from '../utils/dom';
import { createElement } from '../utils/dom';
import { RENDER_TYPE } from '../';

const ImageCard = {
export default {
name: 'image',
display: {
setup(element, options, env, payload) {
if (payload.src) {
let img = createElement('img');
img.src = payload.src;
appendChild(element, img);
}
}
type: RENDER_TYPE,
render({payload}) {
let img = createElement('img');
img.src = payload.src;
return img;
}
};

export default ImageCard;
8 changes: 5 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Renderer from './renderer';
import RenderFactory from './render-factory';

export const RENDER_TYPE = 'dom';

export function registerGlobal(window) {
window.MobiledocDOMRenderer = Renderer;
window.MobiledocDOMRenderer = RenderFactory;
}

export default Renderer;
export default RenderFactory;
41 changes: 41 additions & 0 deletions lib/render-factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Renderer from './renderer';
import { RENDER_TYPE } from './';

/**
* runtime DOM renderer
* renders a mobiledoc to DOM
*
* input: mobiledoc
* output: DOM
*/

function validateCards(cards) {
if (!Array.isArray(cards)) {
throw new Error('`cards` must be passed as an array, not an object.');
}

for (let i=0; i < cards.length; i++) {
let card = cards[i];
if (card.type !== RENDER_TYPE) {
throw new Error(`Card "${card.name}" must be of type "${RENDER_TYPE}", is type "${card.type}"`);
}
if (!card.render) {
throw new Error(`Card "${card.name}" must define \`render\``);
}
}
}

export default class RendererFactory {
constructor({cards, atoms, cardOptions, unknownCardHandler}={}) {
cards = cards || [];
validateCards(cards);
atoms = atoms || [];
cardOptions = cardOptions || {};

this.state = { cards, atoms, cardOptions, unknownCardHandler };
}

render(mobiledoc) {
return new Renderer(mobiledoc, this.state).render();
}
}
264 changes: 165 additions & 99 deletions lib/renderer.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
import {
createElement,
appendChild,
removeChild,
createTextNode,
setAttribute
setAttribute,
createDocumentFragment
} from './utils/dom';
import ImageCard from './cards/image';
import { RENDER_TYPE } from './';

const MARKUP_SECTION_TYPE = 1;
const IMAGE_SECTION_TYPE = 2;
const LIST_SECTION_TYPE = 3;
const CARD_SECTION_TYPE = 10;

/**
* runtime DOM renderer
* renders a mobiledoc to DOM
*
* input: mobiledoc
* output: DOM
*/

function createElementFromMarkerType([tagName, attributes]=['', []]){
let element = createElement(tagName);
attributes = attributes || [];
Expand All @@ -31,118 +26,189 @@ function createElementFromMarkerType([tagName, attributes]=['', []]){
return element;
}

function renderMarkersOnElement(element, markers, renderState) {
let elements = [element];
let currentElement = element;
function validateVersion(version) {
if (version !== '0.2.0') {
throw new Error(`Unexpected Mobiledoc version "${version}"`);
}
}

export default class Renderer {
constructor(mobiledoc, state) {
let { cards, cardOptions, atoms, unknownCardHandler } = state;
let { version, sections: sectionData } = mobiledoc;
validateVersion(version);

const [markerTypes, sections] = sectionData;

this.root = createDocumentFragment();
this.markerTypes = markerTypes;
this.sections = sections;
this.cards = cards;
this.atoms = atoms;
this.cardOptions = cardOptions;
this.unknownCardHandler = unknownCardHandler || this._defaultUnknownCardHandler;

this._teardownCallbacks = [];
this._renderedChildNodes = [];
}

get _defaultUnknownCardHandler() {
return ({env: {name}}) => {
throw new Error(`Card "${name}" not found but no unknownCardHandler was registered`);
};
}

render() {
this.sections.forEach(section => {
appendChild(this.root, this.renderSection(section));
});
// maintain a reference to child nodes so they can be cleaned up later by teardown
this._renderedChildNodes = Array.prototype.slice.call(this.root.childNodes);
return { result: this.root, teardown: () => this.teardown() };
}

for (let i=0, l=markers.length; i<l; i++) {
let marker = markers[i];
let [openTypes, closeTypes, text] = marker;
teardown() {
for (let i=0; i < this._teardownCallbacks.length; i++) {
this._teardownCallbacks[i]();
}
for (let i=0; i < this._renderedChildNodes.length; i++) {
let node = this._renderedChildNodes[i];
if (node.parentNode) {
removeChild(node.parentNode, node);
}
}
}

for (let j=0, m=openTypes.length; j<m; j++) {
let markerType = renderState.markerTypes[openTypes[j]];
let openedElement = createElementFromMarkerType(markerType);
appendChild(currentElement, openedElement);
elements.push(openedElement);
currentElement = openedElement;
renderSection(section) {
const [type] = section;
switch (type) {
case MARKUP_SECTION_TYPE:
return this.renderMarkupSection(section);
case IMAGE_SECTION_TYPE:
return this.renderImageSection(section);
case LIST_SECTION_TYPE:
return this.renderListSection(section);
case CARD_SECTION_TYPE:
return this.renderCardSection(section);
default:
throw new Error(`Cannot render mobiledoc section of type "${type}"`);
}
}

renderMarkersOnElement(element, markers) {
let elements = [element];
let currentElement = element;

appendChild(currentElement, createTextNode(text));
for (let i=0, l=markers.length; i<l; i++) {
let marker = markers[i];
let [openTypes, closeTypes, text] = marker;

for (let j=0, m=closeTypes; j<m; j++) {
elements.pop();
currentElement = elements[elements.length - 1];
for (let j=0, m=openTypes.length; j<m; j++) {
let markerType = this.markerTypes[openTypes[j]];
let openedElement = createElementFromMarkerType(markerType);
appendChild(currentElement, openedElement);
elements.push(openedElement);
currentElement = openedElement;
}

appendChild(currentElement, createTextNode(text));

for (let j=0, m=closeTypes; j<m; j++) {
elements.pop();
currentElement = elements[elements.length - 1];
}
}
}
}

function renderListItem(markers, renderState) {
const element = createElement('li');
renderMarkersOnElement(element, markers, renderState);
return element;
}
renderListItem(markers) {
const element = createElement('li');
this.renderMarkersOnElement(element, markers);
return element;
}

function renderListSection([type, tagName, listItems], renderState) {
const element = createElement(tagName);
listItems.forEach(li => {
appendChild(element, (renderListItem(li, renderState)));
});
return element;
}
renderListSection([type, tagName, listItems]) {
const element = createElement(tagName);
listItems.forEach(li => {
appendChild(element, (this.renderListItem(li)));
});
return element;
}

function renderImageSection([type, src]) {
let element = createElement('img');
element.src = src;
return element;
}
renderImageSection([type, src]) {
let element = createElement('img');
element.src = src;
return element;
}

function renderCardSection([type, name, payload], renderState) {
let { cards } = renderState;
let card = cards[name];
if (!card) {
throw new Error(`Cannot render unknown card named ${name}`);
findCard(name) {
for (let i=0; i < this.cards.length; i++) {
if (this.cards[i].name === name) {
return this.cards[i];
}
}
if (name === ImageCard.name) {
return ImageCard;
}
return this._createUnknownCard(name);
}
if (!payload) {
payload = {};

_createUnknownCard(name) {
return {
name,
type: RENDER_TYPE,
render: this.unknownCardHandler
};
}
let element = createElement('div');
let cardOptions = renderState.options.cardOptions || {};
card.display.setup(element, cardOptions, {name}, payload);
return element;
}

function renderMarkupSection([type, tagName, markers], renderState) {
const element = createElement(tagName);
renderMarkersOnElement(element, markers, renderState);
return element;
}
_createCardAgument(card, payload={}) {
let env = {
name: card.name,
isInEditor: false,
onTeardown: (callback) => this._registerTeardownCallback(callback)
};

function renderSection(section, renderState) {
const [type] = section;
switch (type) {
case MARKUP_SECTION_TYPE:
return renderMarkupSection(section, renderState);
case IMAGE_SECTION_TYPE:
return renderImageSection(section, renderState);
case LIST_SECTION_TYPE:
return renderListSection(section, renderState);
case CARD_SECTION_TYPE:
return renderCardSection(section, renderState);
default:
throw new Error('Unimplement renderer for type ' + type);
let options = this.cardOptions;

return { env, options, payload };
}
}

function validateVersion(version) {
if (version !== '0.2.0') {
throw new Error(`Unexpected Mobiledoc version "${version}"`);
_registerTeardownCallback(callback) {
this._teardownCallbacks.push(callback);
}
}

export default class Renderer {
/**
* @param {Mobiledoc} mobiledoc
* @param {DOMNode} [rootElement] defaults to an empty div
* @param {Object} [cards] Each top-level property on the object is considered
* to be a card's name, its value is an object with `setup` and (optional) `teardown`
* properties
* @return DOMNode
*/
render({version, sections: sectionData}, root=createElement('div'), cards={}, options={}) {
validateVersion(version);
const [markerTypes, sections] = sectionData;
cards.image = cards.image || ImageCard;
const renderState = {root, markerTypes, cards, options};
renderCardSection([type, name, payload]) {
let card = this.findCard(name);

let cardWrapper = this._createCardElement();
let cardArg = this._createCardAgument(card, payload);
let rendered = card.render(cardArg);

this._validateCardRender(rendered, card.name);

if (rendered) {
appendChild(cardWrapper, rendered);
}
return cardWrapper;
}

_createCardElement() {
return createElement('div');
}

if (Array.isArray(cards)) {
throw new Error('`cards` must be passed as an object, not an array.');
_validateCardRender(rendered, cardName) {
if (!rendered) {
return;
}

sections.forEach(section => {
let rendered = renderSection(section, renderState);
appendChild(renderState.root, rendered);
});
if (typeof rendered !== 'object') {
throw new Error(`Card "${cardName}" must render ${RENDER_TYPE}, but result was "${rendered}"`);
}
}

return root;
renderMarkupSection([type, tagName, markers]) {
const element = createElement(tagName);
this.renderMarkersOnElement(element, markers);
return element;
}
}

Loading

0 comments on commit 99279d5

Please sign in to comment.