Skip to content

Commit

Permalink
Add cardParsers to SectionParser
Browse files Browse the repository at this point in the history
This allows runtime-defined overrides for parsed content. This is mainly
useful for handling pasted content.
  • Loading branch information
bantic committed Oct 26, 2015
1 parent 051d267 commit 1c880f3
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 44 deletions.
3 changes: 2 additions & 1 deletion src/js/editor/editor.js
Expand Up @@ -72,6 +72,7 @@ class Editor {
this._elementListeners = [];
this._views = [];
this.isEditable = null;
this._cardParsers = options.cardParsers || [];

// FIXME: This should merge onto this.options
mergeWithOptions(this, defaults, options);
Expand Down Expand Up @@ -657,7 +658,7 @@ class Editor {
this.handleDeletion();
}

let pastedPost = parsePostFromPaste(event, this.builder);
let pastedPost = parsePostFromPaste(event, this.builder, this._cardParsers);

let nextPosition;
this.run(postEditor => {
Expand Down
4 changes: 4 additions & 0 deletions src/js/models/card.js
Expand Up @@ -17,6 +17,10 @@ export default class Card extends Section {
this.setInitialMode(DEFAULT_INITIAL_MODE);
}

get isBlank() {
return false;
}

clone() {
const payload = shallowCopyObject(this.payload);
return this.builder.createCardSection(this.name, payload);
Expand Down
14 changes: 9 additions & 5 deletions src/js/parsers/dom.js
Expand Up @@ -50,23 +50,27 @@ function remapTagName(tagName) {
* Parses DOM element -> Post
*/
export default class DOMParser {
constructor(builder) {
constructor(builder, options={}) {
this.builder = builder;
this.sectionParser = new SectionParser(this.builder);
this.sectionParser = new SectionParser(this.builder, options);
}

parse(element) {
const post = this.builder.createPost();
let rootElement = detectRootElement(element);

this._eachChildNode(rootElement, child => {
let section = this.parseSection(child);
this.appendSection(post, section);
let sections = this.parseSections(child);
this.appendSections(post, sections);
});

return post;
}

appendSections(post, sections) {
forEach(sections, section => this.appendSection(post, section));
}

appendSection(post, section) {
if (section.isBlank) {
return;
Expand All @@ -88,7 +92,7 @@ export default class DOMParser {
forEach(nodes, node => callback(node));
}

parseSection(element) {
parseSections(element) {
return this.sectionParser.parse(element);
}

Expand Down
5 changes: 3 additions & 2 deletions src/js/parsers/html.js
Expand Up @@ -3,14 +3,15 @@ import assert from '../utils/assert';
import DOMParser from './dom';

export default class HTMLParser {
constructor(builder) {
constructor(builder, options={}) {
assert('Must pass builder to HTMLParser', builder);
this.builder = builder;
this.options = options;
}

parse(html) {
let dom = parseHTML(html);
let parser = new DOMParser(this.builder);
let parser = new DOMParser(this.builder, this.options);
return parser.parse(dom);
}
}
97 changes: 72 additions & 25 deletions src/js/parsers/section.js
Expand Up @@ -26,73 +26,119 @@ import {
* @return {Section}
*/
export default class SectionParser {
constructor(builder) {
constructor(builder, options={}) {
this.builder = builder;
this.cardParsers = options.cardParsers || [];
}

parse(element) {
const section = this.createSectionFromElement(element);
const markups = this.markupsFromElement(element);
const state = {section, markups, text:''};
this.sections = [];
this.state = {};

this._updateStateFromElement(element);

let childNodes = isTextNode(element) ? [element] : element.childNodes;

forEach(childNodes, el => {
this.parseNode(el, state);
this.parseNode(el);
});

// close a trailing text nodes if it exists
if (state.text.length) {
let marker = this.builder.createMarker(state.text, state.markups);
state.section.markers.append(marker);
}
this._closeCurrentSection();

return section;
return this.sections;
}

parseNode(node, state) {
parseNode(node) {
if (!this.state.section) {
this._updateStateFromElement(node);
}

switch (node.nodeType) {
case TEXT_NODE:
this.parseTextNode(node, state);
this.parseTextNode(node);
break;
case ELEMENT_NODE:
this.parseElementNode(node, state);
this.parseElementNode(node);
break;
default:
throw new Error(`parseNode got unexpected element type ${node.nodeType} ` + node);
}
}

parseElementNode(element, state) {
const markups = this.markupsFromElement(element);
parseCard(element) {
let { builder } = this;

for (let i=0; i<this.cardParsers.length; i++) {
let card = this.cardParsers[i].parse(element, builder);
if (card) {
this._closeCurrentSection();
this.sections.push(card);
return true;
}
}
}

parseElementNode(element) {
let { state } = this;

let parsedCard = this.parseCard(element);
if (parsedCard) {
return;
}
const markups = this._markupsFromElement(element);
if (markups.length && state.text.length) {
this._createMarkerFromState(state);
this._createMarker();
}
state.markups.push(...markups);

forEach(element.childNodes, (node) => {
this.parseNode(node, state);
this.parseNode(node);
});

if (markups.length && state.text.length) {
// create the marker started for this node
this._createMarkerFromState(state);
this._createMarker();
}

// pop the current markups from the stack
state.markups.splice(-markups.length, markups.length);
}

parseTextNode(textNode, state) {
parseTextNode(textNode) {
let { state } = this;
state.text += textNode.textContent;
}

_updateStateFromElement(element) {
let { state } = this;
state.section = this._createSectionFromElement(element);
state.markups = this._markupsFromElement(element);
state.text = '';
}

_closeCurrentSection() {
let { sections, state } = this;

if (!state.section) {
return;
}

// close a trailing text node if it exists
if (state.text.length) {
let marker = this.builder.createMarker(state.text, state.markups);
state.section.markers.append(marker);
}

sections.push(state.section);
state.section = null;
}

isSectionElement(element) {
return element.nodeType === ELEMENT_NODE &&
VALID_MARKUP_SECTION_TAGNAMES.indexOf(normalizeTagName(element.tagName)) !== -1;
}

markupsFromElement(element) {
_markupsFromElement(element) {
let { builder } = this;
let markups = [];
if (isTextNode(element)) {
Expand Down Expand Up @@ -135,7 +181,8 @@ export default class SectionParser {
return markups;
}

_createMarkerFromState(state) {
_createMarker() {
let { state } = this;
let marker = this.builder.createMarker(state.text, state.markups);
state.section.markers.append(marker);
state.text = '';
Expand All @@ -156,18 +203,18 @@ export default class SectionParser {
return tagName;
}

inferSectionTagNameFromElement(/* element */) {
_inferSectionTagNameFromElement(/* element */) {
return DEFAULT_TAG_NAME;
}

createSectionFromElement(element) {
_createSectionFromElement(element) {
let { builder } = this;

let inferredTagName = false;
let tagName = this._sectionTagNameFromElement(element);
if (!tagName) {
inferredTagName = true;
tagName = this.inferSectionTagNameFromElement(element);
tagName = this._inferSectionTagNameFromElement(element);
}
let section = builder.createMarkupSection(tagName);

Expand Down
4 changes: 2 additions & 2 deletions src/js/utils/paste-utils.js
Expand Up @@ -21,7 +21,7 @@ export function setClipboardCopyData(copyEvent, editor) {
clipboardData.setData('text/html', html);
}

export function parsePostFromPaste(pasteEvent, builder) {
export function parsePostFromPaste(pasteEvent, builder, cardParsers=[]) {
let mobiledoc, post;
const mobiledocRegex = new RegExp(/data\-mobiledoc='(.*?)'>/);

Expand All @@ -32,7 +32,7 @@ export function parsePostFromPaste(pasteEvent, builder) {
mobiledoc = JSON.parse(mobiledocString);
post = new MobiledocParser(builder).parse(mobiledoc);
} else {
post = new HTMLParser(builder).parse(html);
post = new HTMLParser(builder, {cardParsers}).parse(html);
}

return post;
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/google-docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 39 additions & 3 deletions tests/unit/parsers/dom-google-docs-test.js
Expand Up @@ -3,12 +3,13 @@ import PostNodeBuilder from 'content-kit-editor/models/post-node-builder';
import Helpers from '../../test-helpers';
import GoogleDocs from '../../fixtures/google-docs';
import { forEach } from 'content-kit-editor/utils/array-utils';
import { CARD_TYPE } from 'content-kit-editor/models/types';

const {module, test} = Helpers;

function parseHTML(html) {
function parseHTML(html, options={}) {
let builder = new PostNodeBuilder();
return new HTMLParser(builder).parse(html);
return new HTMLParser(builder, options).parse(html);
}

module('Unit: Parser: HTMLParser Google Docs');
Expand All @@ -22,6 +23,16 @@ function equalToExpected(assert, rawHTML, expectedHTML) {
raw.sections.forEach((section, sectionIndex) => {
let expectedSection = expected.sections.objectAt(sectionIndex);

if (section.type === CARD_TYPE) {
assert.equal(section.name, expectedSection.name,
`card section at index ${sectionIndex} has equal name`);

assert.deepEqual(section.payload, expectedSection.payload,
`card section at index ${sectionIndex} has equal payload`);

return;
}

assert.equal(section.markers.length, expectedSection.markers.length,
`section at index ${sectionIndex} has equal marker length`);
assert.equal(section.text, expectedSection.text,
Expand Down Expand Up @@ -52,7 +63,6 @@ function equalToExpected(assert, rawHTML, expectedHTML) {
assert.equal(expectedMarkup.getAttribute(key),
markup.getAttribute(key),
`equal attribute value for ${key}`);

});
});
});
Expand All @@ -65,3 +75,29 @@ Object.keys(GoogleDocs).forEach(key => {
equalToExpected(assert, example.raw, example.expected);
});
});

test('img in span can use a cardParser to turn img into image-card', function(assert) {
let example = GoogleDocs['img in span'];
let options = {
cardParsers: [{
parse(element, builder) {
if (element.tagName === 'IMG') {
let payload = {url: element.src};
return builder.createCardSection('image-card', payload);
}
}
}]
};
let parsed = parseHTML(example.raw, options);

let sections = parsed.sections.toArray();
let found = false, payload;
for (let i=0; i < sections.length; i++) {
if (sections[i].name === 'image-card') {
found = true;
payload = sections[i].payload;
}
}
assert.ok(found, 'found image-card');
assert.ok(payload.url, 'has url in payload');
});

0 comments on commit 1c880f3

Please sign in to comment.