initial
This commit is contained in:
commit
d4176bfada
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
yarn.lock
|
||||
*.log
|
||||
dist/
|
||||
.env
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "mobiledoc-wp-renderer",
|
||||
"version": "0.0.1",
|
||||
"description": "Renders Mobiledoc input to WordPress block output",
|
||||
"keywords": [
|
||||
"mobiledoc",
|
||||
"mobiledoc-renderer"
|
||||
],
|
||||
"main": "dist",
|
||||
"scripts": {
|
||||
"build": "babel src -d dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import RendererFactory from './renderer-factory';
|
||||
import RENDER_TYPE from './utils/render-type';
|
||||
|
||||
export { RENDER_TYPE };
|
||||
|
||||
export function registerGlobal(window) {
|
||||
window.MobiledocWPRenderer = RendererFactory;
|
||||
}
|
||||
|
||||
export default RendererFactory;
|
|
@ -0,0 +1,105 @@
|
|||
import { registerStore } from './utils/wp-utils.js';
|
||||
|
||||
// import Renderer_0_2, {
|
||||
// MOBILEDOC_VERSION as MOBILEDOC_VERSION_0_2
|
||||
// } from './renderers/0-2';
|
||||
import Renderer_0_3, {
|
||||
MOBILEDOC_VERSION_0_3_0,
|
||||
MOBILEDOC_VERSION_0_3_1,
|
||||
MOBILEDOC_VERSION_0_3_2
|
||||
} from './renderers/0-3';
|
||||
import RENDER_TYPE from './utils/render-type';
|
||||
|
||||
/**
|
||||
* runtime WordPress renderer
|
||||
* renders a mobiledoc to WordPress blocks
|
||||
*
|
||||
* input: mobiledoc
|
||||
* output: text
|
||||
*/
|
||||
|
||||
function validateCards(cards) {
|
||||
if (!Array.isArray(cards)) {
|
||||
throw new Error('`cards` must be passed as an array');
|
||||
}
|
||||
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}", was "${card.type}"`);
|
||||
}
|
||||
if (!card.render) {
|
||||
throw new Error(`Card "${card.name}" must define \`render\``);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateAtoms(atoms) {
|
||||
if (!Array.isArray(atoms)) {
|
||||
throw new Error('`atoms` must be passed as an array');
|
||||
}
|
||||
for (let i=0; i < atoms.length; i++) {
|
||||
let atom = atoms[i];
|
||||
if (atom.type !== RENDER_TYPE) {
|
||||
throw new Error(`Atom "${atom.name}" must be type "${RENDER_TYPE}", was "${atom.type}"`);
|
||||
}
|
||||
if (!atom.render) {
|
||||
throw new Error(`Atom "${atom.name}" must define \`render\``);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class RendererFactory {
|
||||
constructor({
|
||||
cards=[],
|
||||
atoms=[],
|
||||
cardOptions={},
|
||||
unknownCardHandler,
|
||||
unknownAtomHandler,
|
||||
markupElementRenderer={},
|
||||
sectionElementRenderer={},
|
||||
dom,
|
||||
serializer,
|
||||
createBlock,
|
||||
markupSanitizer=null
|
||||
}={}) {
|
||||
validateCards(cards);
|
||||
validateAtoms(atoms);
|
||||
|
||||
if (!dom) {
|
||||
if (typeof window === 'undefined') {
|
||||
throw new Error('A `dom` option must be provided to the renderer when running without window.document');
|
||||
}
|
||||
dom = window.document;
|
||||
}
|
||||
|
||||
this.options = {
|
||||
cards,
|
||||
atoms,
|
||||
cardOptions,
|
||||
unknownCardHandler,
|
||||
unknownAtomHandler,
|
||||
markupElementRenderer,
|
||||
sectionElementRenderer,
|
||||
dom,
|
||||
serializer,
|
||||
createBlock,
|
||||
markupSanitizer
|
||||
};
|
||||
}
|
||||
|
||||
render(mobiledoc) {
|
||||
let { version } = mobiledoc;
|
||||
switch (version) {
|
||||
// case MOBILEDOC_VERSION_0_2:
|
||||
// case undefined:
|
||||
// case null:
|
||||
// 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}"`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,395 @@
|
|||
import { createTextNode } from '../utils/dom';
|
||||
import RENDER_TYPE from '../utils/render-type';
|
||||
import {
|
||||
MARKUP_SECTION_TYPE,
|
||||
IMAGE_SECTION_TYPE,
|
||||
LIST_SECTION_TYPE,
|
||||
CARD_SECTION_TYPE
|
||||
} from '../utils/section-types';
|
||||
import {
|
||||
isValidSectionTagName,
|
||||
isValidMarkerType
|
||||
} from '../utils/tag-names';
|
||||
import {
|
||||
reduceAttributes
|
||||
} from '../utils/sanitization-utils';
|
||||
import {
|
||||
defaultSectionElementRenderer,
|
||||
defaultMarkupElementRenderer
|
||||
} from '../utils/render-utils';
|
||||
|
||||
import {
|
||||
MARKUP_MARKER_TYPE,
|
||||
ATOM_MARKER_TYPE
|
||||
} from '../utils/marker-types';
|
||||
|
||||
import {
|
||||
createImage,
|
||||
createList,
|
||||
blockRenderer
|
||||
} from '../utils/wp-utils';
|
||||
|
||||
export const MOBILEDOC_VERSION_0_3_0 = '0.3.0';
|
||||
export const MOBILEDOC_VERSION_0_3_1 = '0.3.1';
|
||||
export const MOBILEDOC_VERSION_0_3_2 = '0.3.2';
|
||||
|
||||
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}"`);
|
||||
}
|
||||
}
|
||||
|
||||
export default class Renderer {
|
||||
constructor(mobiledoc, state) {
|
||||
|
||||
let {
|
||||
cards,
|
||||
cardOptions,
|
||||
atoms,
|
||||
unknownCardHandler,
|
||||
unknownAtomHandler,
|
||||
markupElementRenderer,
|
||||
sectionElementRenderer,
|
||||
dom,
|
||||
serializer,
|
||||
createBlock
|
||||
} = state;
|
||||
let {
|
||||
version,
|
||||
sections,
|
||||
atoms: atomTypes,
|
||||
cards: cardTypes,
|
||||
markups: markerTypes
|
||||
} = mobiledoc;
|
||||
validateVersion(version);
|
||||
|
||||
this.dom = dom;
|
||||
this.serializer = serializer;
|
||||
this.createBlock = createBlock;
|
||||
this.root = [];
|
||||
this.sections = sections;
|
||||
this.atomTypes = atomTypes;
|
||||
this.cardTypes = cardTypes;
|
||||
this.markerTypes = markerTypes;
|
||||
this.cards = cards;
|
||||
this.atoms = atoms;
|
||||
this.cardOptions = cardOptions;
|
||||
this.unknownCardHandler = unknownCardHandler || this._defaultUnknownCardHandler;
|
||||
this.unknownAtomHandler = unknownAtomHandler || this._defaultUnknownAtomHandler;
|
||||
|
||||
this.sectionElementRenderer = {
|
||||
'__default__': defaultSectionElementRenderer
|
||||
};
|
||||
Object.keys(sectionElementRenderer).forEach(key => {
|
||||
this.sectionElementRenderer[key.toLowerCase()] = sectionElementRenderer[key];
|
||||
});
|
||||
|
||||
this.markupElementRenderer = {
|
||||
'__default__': defaultMarkupElementRenderer
|
||||
};
|
||||
Object.keys(markupElementRenderer).forEach(key => {
|
||||
this.markupElementRenderer[key.toLowerCase()] = markupElementRenderer[key];
|
||||
});
|
||||
|
||||
this._renderCallbacks = [];
|
||||
this._teardownCallbacks = [];
|
||||
}
|
||||
|
||||
get _defaultUnknownCardHandler() {
|
||||
return ({env: {name}}) => {
|
||||
throw new Error(`Card "${name}" not found but no unknownCardHandler was registered`);
|
||||
};
|
||||
}
|
||||
|
||||
get _defaultUnknownAtomHandler() {
|
||||
return ({env: {name}}) => {
|
||||
throw new Error(`Atom "${name}" not found but no unknownAtomHandler was registered`);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
this.sections.forEach(section => {
|
||||
let rendered = this.renderSection(section);
|
||||
if (rendered) {
|
||||
this.root.push(rendered);
|
||||
}
|
||||
});
|
||||
for (let i=0; i < this._renderCallbacks.length; i++) {
|
||||
this._renderCallbacks[i]();
|
||||
}
|
||||
return { result: this.root, teardown: () => this.teardown() };
|
||||
}
|
||||
|
||||
teardown() {
|
||||
for (let i=0; i < this._teardownCallbacks.length; i++) {
|
||||
this._teardownCallbacks[i]();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
let pushElement = (openedElement) => {
|
||||
currentElement.appendChild(openedElement);
|
||||
elements.push(openedElement);
|
||||
currentElement = openedElement;
|
||||
};
|
||||
|
||||
for (let i=0, l=markers.length; i<l; i++) {
|
||||
let marker = markers[i];
|
||||
let [type, openTypes, closeCount, value] = marker;
|
||||
|
||||
for (let j=0, m=openTypes.length; j<m; j++) {
|
||||
let markerType = this.markerTypes[openTypes[j]];
|
||||
let [tagName, attrs=[]] = markerType;
|
||||
|
||||
if (isValidMarkerType(tagName)) {
|
||||
pushElement(this.renderMarkupElement(tagName, attrs));
|
||||
} else {
|
||||
closeCount--;
|
||||
}
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case MARKUP_MARKER_TYPE:
|
||||
currentElement.appendChild(createTextNode(this.dom, value));
|
||||
break;
|
||||
case ATOM_MARKER_TYPE:
|
||||
currentElement.appendChild(this._renderAtom(value));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown markup type (${type})`);
|
||||
}
|
||||
|
||||
for (let j=0, m=closeCount; j<m; j++) {
|
||||
elements.pop();
|
||||
currentElement = elements[elements.length - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param attrs Array
|
||||
*/
|
||||
renderMarkupElement(tagName, attrs) {
|
||||
tagName = tagName.toLowerCase();
|
||||
attrs = reduceAttributes(attrs);
|
||||
|
||||
let renderer = this.markupElementRendererFor(tagName);
|
||||
return renderer(tagName, this.dom, attrs);
|
||||
}
|
||||
|
||||
markupElementRendererFor(tagName) {
|
||||
return this.markupElementRenderer[tagName] ||
|
||||
this.markupElementRenderer.__default__;
|
||||
}
|
||||
|
||||
renderListItem(markers) {
|
||||
const element = this.dom.createElement('li');
|
||||
this.renderMarkersOnElement(element, markers);
|
||||
return element;
|
||||
}
|
||||
|
||||
renderListSection([type, tagName, listItems]) {
|
||||
if (!isValidSectionTagName(tagName, LIST_SECTION_TYPE)) {
|
||||
return;
|
||||
}
|
||||
const element = this.dom.createElement(tagName);
|
||||
listItems.forEach(li => {
|
||||
element.appendChild(this.renderListItem(li));
|
||||
});
|
||||
const items = this.serializer.serializeChildren(element);
|
||||
return this.createBlock(... createList(tagName === 'ol', items));
|
||||
}
|
||||
|
||||
renderImageSection([type, src]) {
|
||||
return this.createBlock(... createImage(src));
|
||||
}
|
||||
|
||||
findCard(name) {
|
||||
for (let i=0; i < this.cards.length; i++) {
|
||||
if (this.cards[i].name === name) {
|
||||
return this.cards[i];
|
||||
}
|
||||
}
|
||||
return this._createUnknownCard(name);
|
||||
}
|
||||
|
||||
_findCardByIndex(index) {
|
||||
let cardType = this.cardTypes[index];
|
||||
if (!cardType) {
|
||||
throw new Error(`No card definition found at index ${index}`);
|
||||
}
|
||||
|
||||
let [ name, payload ] = cardType;
|
||||
let card = this.findCard(name);
|
||||
|
||||
return {
|
||||
card,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
_createUnknownCard(name) {
|
||||
return {
|
||||
name,
|
||||
type: RENDER_TYPE,
|
||||
render: this.unknownCardHandler
|
||||
};
|
||||
}
|
||||
|
||||
_createCardArgument(card, payload={}) {
|
||||
let env = {
|
||||
name: card.name,
|
||||
isInEditor: false,
|
||||
dom: this.dom,
|
||||
serializer: this.serializer,
|
||||
didRender: (callback) => this._registerRenderCallback(callback),
|
||||
onTeardown: (callback) => this._registerTeardownCallback(callback)
|
||||
};
|
||||
|
||||
let options = this.cardOptions;
|
||||
|
||||
return { env, options, payload };
|
||||
}
|
||||
|
||||
_registerTeardownCallback(callback) {
|
||||
this._teardownCallbacks.push(callback);
|
||||
}
|
||||
|
||||
_registerRenderCallback(callback) {
|
||||
this._renderCallbacks.push(callback);
|
||||
}
|
||||
|
||||
renderCardSection([type, index]) {
|
||||
let { card, payload } = this._findCardByIndex(index);
|
||||
|
||||
let cardArg = this._createCardArgument(card, payload);
|
||||
let rendered = card.render(cardArg);
|
||||
|
||||
this._validateCardRender(rendered, card.name);
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
_validateCardRender(rendered, cardName) {
|
||||
if (!rendered) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof rendered !== 'object') {
|
||||
throw new Error(`Card "${cardName}" must render ${RENDER_TYPE}, but result was "${rendered}"`);
|
||||
}
|
||||
}
|
||||
|
||||
findAtom(name) {
|
||||
for (let i=0; i < this.atoms.length; i++) {
|
||||
if (this.atoms[i].name === name) {
|
||||
return this.atoms[i];
|
||||
}
|
||||
}
|
||||
return this._createUnknownAtom(name);
|
||||
}
|
||||
|
||||
_createUnknownAtom(name) {
|
||||
return {
|
||||
name,
|
||||
type: RENDER_TYPE,
|
||||
render: this.unknownAtomHandler
|
||||
};
|
||||
}
|
||||
|
||||
_createAtomArgument(atom, value, payload) {
|
||||
let env = {
|
||||
name: atom.name,
|
||||
isInEditor: false,
|
||||
dom: this.dom,
|
||||
onTeardown: (callback) => this._registerTeardownCallback(callback)
|
||||
};
|
||||
|
||||
let options = this.cardOptions;
|
||||
|
||||
return { env, options, value, payload };
|
||||
}
|
||||
|
||||
_validateAtomRender(rendered, atomName) {
|
||||
if (!rendered) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof rendered !== 'object') {
|
||||
throw new Error(`Atom "${atomName}" must render ${RENDER_TYPE}, but result was "${rendered}"`);
|
||||
}
|
||||
}
|
||||
|
||||
_findAtomByIndex(index) {
|
||||
let atomType = this.atomTypes[index];
|
||||
if (!atomType) {
|
||||
throw new Error(`No atom definition found at index ${index}`);
|
||||
}
|
||||
|
||||
let [ name, value, payload ] = atomType;
|
||||
let atom = this.findAtom(name);
|
||||
|
||||
return {
|
||||
atom,
|
||||
value,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
_renderAtom(index) {
|
||||
let { atom, value, payload } = this._findAtomByIndex(index);
|
||||
|
||||
let atomArg = this._createAtomArgument(atom, value, payload);
|
||||
let rendered = atom.render(atomArg);
|
||||
|
||||
this._validateAtomRender(rendered, atom.name);
|
||||
|
||||
return rendered || createTextNode(this.dom, '');
|
||||
}
|
||||
|
||||
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, attrsObj);
|
||||
|
||||
this.renderMarkersOnElement(element, markers);
|
||||
|
||||
var html = this.serializer.serializeChildren(element);
|
||||
return this.createBlock(... blockRenderer[tagName](html));
|
||||
}
|
||||
|
||||
sectionElementRendererFor(tagName) {
|
||||
return this.sectionElementRenderer[tagName] ||
|
||||
this.sectionElementRenderer.__default__;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
export function includes(array, detectValue) {
|
||||
for (let i=0;i < array.length;i++) {
|
||||
let value = array[i];
|
||||
if (value === detectValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
function addHTMLSpaces(text) {
|
||||
let nbsp = '\u00A0';
|
||||
return text.replace(/ /g, ' ' + nbsp);
|
||||
}
|
||||
|
||||
export function createTextNode(dom, text) {
|
||||
return dom.createTextNode(addHTMLSpaces(text));
|
||||
}
|
||||
|
||||
export function normalizeTagName(tagName) {
|
||||
return tagName.toLowerCase();
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export const MARKUP_MARKER_TYPE = 0;
|
||||
export const ATOM_MARKER_TYPE = 1;
|
|
@ -0,0 +1 @@
|
|||
export default 'wp';
|
|
@ -0,0 +1,57 @@
|
|||
import {
|
||||
isMarkupSectionElementName
|
||||
} from '../utils/tag-names';
|
||||
import {
|
||||
sanitizeHref
|
||||
} from './sanitization-utils';
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
element.setAttribute(attributeKey, 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);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function sanitizeAttribute(tagName, attrName, attrValue) {
|
||||
if (tagName === 'a' && attrName === 'href') {
|
||||
return sanitizeHref(attrValue);
|
||||
} else {
|
||||
return attrValue;
|
||||
}
|
||||
}
|
||||
|
||||
export function defaultMarkupElementRenderer(tagName, dom, attrsObj) {
|
||||
let element = dom.createElement(tagName);
|
||||
Object.keys(attrsObj).forEach(attrName => {
|
||||
let attrValue = attrsObj[attrName];
|
||||
attrValue = sanitizeAttribute(tagName, attrName, attrValue);
|
||||
element.setAttribute(attrName, attrValue);
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { includes } from './array-utils';
|
||||
|
||||
const PROTOCOL_REGEXP = /^([a-z0-9.+-]+:)/i;
|
||||
|
||||
const badProtocols = [
|
||||
'javascript:', // jshint ignore:line
|
||||
'vbscript:' // jshint ignore:line
|
||||
];
|
||||
|
||||
function getProtocol(url) {
|
||||
let matches = url && url.match(PROTOCOL_REGEXP);
|
||||
let protocol = (matches && matches[0]) || ':';
|
||||
return protocol;
|
||||
}
|
||||
|
||||
export function sanitizeHref(url) {
|
||||
let protocol = getProtocol(url).toLowerCase();
|
||||
if (includes(badProtocols, protocol)) {
|
||||
return `unsafe:${url}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param attributes array
|
||||
* @return obj with normalized attribute names (lowercased)
|
||||
*/
|
||||
export function reduceAttributes(attributes) {
|
||||
let obj = {};
|
||||
for (let i = 0; i < attributes.length; i += 2) {
|
||||
let key = attributes[i];
|
||||
let val = attributes[i+1];
|
||||
obj[key.toLowerCase()] = val;
|
||||
}
|
||||
return obj;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export const MARKUP_SECTION_TYPE = 1;
|
||||
export const IMAGE_SECTION_TYPE = 2;
|
||||
export const LIST_SECTION_TYPE = 3;
|
||||
export const CARD_SECTION_TYPE = 10;
|
|
@ -0,0 +1,48 @@
|
|||
import {
|
||||
MARKUP_SECTION_TYPE,
|
||||
LIST_SECTION_TYPE
|
||||
} from './section-types';
|
||||
import { normalizeTagName } from './dom';
|
||||
|
||||
const MARKUP_SECTION_TAG_NAMES = [
|
||||
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pull-quote', 'aside'
|
||||
].map(normalizeTagName);
|
||||
|
||||
const MARKUP_SECTION_ELEMENT_NAMES = [
|
||||
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'aside'
|
||||
].map(normalizeTagName);
|
||||
|
||||
const LIST_SECTION_TAG_NAMES = [
|
||||
'ul', 'ol'
|
||||
].map(normalizeTagName);
|
||||
|
||||
const MARKUP_TYPES = [
|
||||
'b', 'i', 'strong', 'em', 'a', 'u', 'sub', 'sup', 's', 'code'
|
||||
].map(normalizeTagName);
|
||||
|
||||
function contains(array, item) {
|
||||
return array.indexOf(item) !== -1;
|
||||
}
|
||||
|
||||
export function isValidSectionTagName(tagName, sectionType) {
|
||||
tagName = normalizeTagName(tagName);
|
||||
|
||||
switch (sectionType) {
|
||||
case MARKUP_SECTION_TYPE:
|
||||
return contains(MARKUP_SECTION_TAG_NAMES, tagName);
|
||||
case LIST_SECTION_TYPE:
|
||||
return contains(LIST_SECTION_TAG_NAMES, tagName);
|
||||
default:
|
||||
throw new Error(`Cannot validate tagName for unknown section type "${sectionType}"`);
|
||||
}
|
||||
}
|
||||
|
||||
export function isMarkupSectionElementName(tagName) {
|
||||
tagName = normalizeTagName(tagName);
|
||||
return contains(MARKUP_SECTION_ELEMENT_NAMES, tagName);
|
||||
}
|
||||
|
||||
export function isValidMarkerType(type) {
|
||||
type = normalizeTagName(type);
|
||||
return contains(MARKUP_TYPES, type);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
export const createImage = url =>
|
||||
["core/image", {
|
||||
url: url
|
||||
}]
|
||||
|
||||
export const createList = (ordered, values) =>
|
||||
["core/list", {
|
||||
ordered: ordered,
|
||||
values: values
|
||||
}]
|
||||
|
||||
export const createQuote = content =>
|
||||
["core/quote", {
|
||||
content: `<p>${content}</p>`
|
||||
}]
|
||||
|
||||
export const createHeading = level => content =>
|
||||
["core/heading", {
|
||||
content: content,
|
||||
level: level
|
||||
}]
|
||||
|
||||
export const createParagraph = content =>
|
||||
["core/paragraph", {
|
||||
content: content
|
||||
}]
|
||||
|
||||
export const blockRenderer = {
|
||||
aside: () => { throw new Error(`not implemented`) },
|
||||
blockquote: createQuote,
|
||||
h1: createHeading(1),
|
||||
h2: createHeading(2),
|
||||
h3: createHeading(3),
|
||||
h4: createHeading(4),
|
||||
h5: createHeading(5),
|
||||
h6: createHeading(6),
|
||||
p: createParagraph
|
||||
}
|
||||
|
Loading…
Reference in New Issue