Skip to content

Commit

Permalink
Merge pull request #257 from mixonic/parser-hooks
Browse files Browse the repository at this point in the history
Document parser hooks
  • Loading branch information
mixonic committed Dec 9, 2015
2 parents f4fc3cc + f52d97e commit 01fec18
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 37 deletions.
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,47 @@ const expansion = {
};
```

### DOM Parsing hooks

A developer can override the default parsing behavior for leaf DOM nodes in
pasted HTML.

For example, when an `img` tag is pasted it may be appropriate to
fetch that image, upload it to an authoritative source, and create a specific
kind of image card with the new URL in its payload.

A demonstration of this:

```js
function imageToCardParser(node, builder, {addSection, addMarkerable, nodeFinished}) {
if (node.nodeType !== 1 || node.tagName !== 'IMG') {
return;
}
var payload = { src: node.src };
var cardSection = builder.createCardSection('my-image', payload);
addSection(cardSection);
nodeFinished();
}
var options = {
parserPlugins: [imageToCardParser]
};
var editor = new Mobiledoc.Editor(options);
var element = document.querySelector('#editor');
editor.render(element);
```

Parser hooks are called with two arguments:

* `node` - The node of DOM being parsed. This may be a text node or an element.
* `builder` - The abstract model builder.
* `env` - An object containing three callbacks to modify the abstract
* `addSection` - Close the current section and add a new one
* `addMarkerable` - Add a markerable (marker or atom) to the current section
* `nodeFinished` - Bypass all remaining parse steps for this node

Note that you *must* call `nodeFinished` to stop a DOM node from being
parsed by the next plugin or the default parser.

### Contributing

Fork the repo, write a test, make a change, open a PR.
Expand Down
4 changes: 2 additions & 2 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class Editor {
this._elementListeners = [];
this._views = [];
this.isEditable = null;
this._cardParsers = options.cardParsers || [];
this._parserPlugins = options.parserPlugins || [];

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

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

this.run(postEditor => {
let nextPosition = postEditor.insertPost(position, pastedPost);
Expand Down
60 changes: 41 additions & 19 deletions src/js/parsers/section.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
transformHTMLText
} from '../parsers/dom';

import assert from '../utils/assert';

function isListSection(section) {
return section.type === LIST_SECTION_TYPE;
}
Expand All @@ -55,7 +57,7 @@ function isListItem(section) {
export default class SectionParser {
constructor(builder, options={}) {
this.builder = builder;
this.cardParsers = options.cardParsers || [];
this.plugins = options.plugins || [];
}

parse(element) {
Expand Down Expand Up @@ -90,11 +92,49 @@ export default class SectionParser {
});
}

runPlugins(node) {
let isNodeFinished = false;
let env = {
addSection: (section) => {
this._closeCurrentSection();
this.sections.push(section);
},
addMarkerable: (marker) => {
let { state } = this;
let { section } = state;
assert(
'Markerables can only be appended to markup sections and list item sections',
section && section.isMarkerable
);
if (state.text) {
this._createMarker();
}
section.markers.append(marker);
},
nodeFinished() {
isNodeFinished = true;
}
};
for (let i=0; i<this.plugins.length; i++) {
let plugin = this.plugins[i];
plugin(node, this.builder, env);
if (isNodeFinished) {
return true;
}
}
return false;
}

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

let nodeFinished = this.runPlugins(node);
if (nodeFinished) {
return;
}

switch (node.nodeType) {
case TEXT_NODE:
this.parseTextNode(node);
Expand All @@ -107,27 +147,9 @@ export default class SectionParser {
}
}

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._createMarker();
Expand Down
4 changes: 2 additions & 2 deletions src/js/utils/paste-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function setClipboardCopyData(copyEvent, editor) {
clipboardData.setData('text/html', html);
}

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

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

return post;
Expand Down
11 changes: 5 additions & 6 deletions tests/unit/parsers/dom-google-docs-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,11 @@ Object.keys(GoogleDocs).forEach(key => {
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);
}
plugins: [function(element, builder, {addSection}) {
if (element.tagName === 'IMG') {
let payload = {url: element.src};
let cardSection = builder.createCardSection('image-card', payload);
addSection(cardSection);
}
}]
};
Expand Down
37 changes: 29 additions & 8 deletions tests/unit/parsers/section-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,21 @@ test('#parse turns a textNode into a section', (assert) => {
assert.equal(m1.value, 'I am a text node');
});

test('#parse allows passing in cardParsers that can override parsing', (assert) => {
test('#parse allows passing in parserPlugins that can override element parsing', (assert) => {
let container = buildDOM(`
<p>text 1<img src="http://placehold.it/100x100">text 2</p>
`);

let element = container.firstChild;
let cardParsers = [{
parse(element, builder) {
if (element.tagName === 'IMG') {
let payload = {url: element.src};
return builder.createCardSection('test-image', payload);
}
let plugins = [function(element, builder, {addSection}) {
if (element.tagName !== 'IMG') {
return;
}
let payload = {url: element.src};
let cardSection = builder.createCardSection('test-image', payload);
addSection(cardSection);
}];
parser = new SectionParser(builder, {cardParsers});
parser = new SectionParser(builder, {plugins});
const sections = parser.parse(element);

assert.equal(sections.length, 3, '3 sections');
Expand All @@ -130,3 +130,24 @@ test('#parse allows passing in cardParsers that can override parsing', (assert)
assert.equal(cardSection.name, 'test-image');
assert.deepEqual(cardSection.payload, {url: 'http://placehold.it/100x100'});
});

test('#parse allows passing in parserPlugins that can override text parsing', (assert) => {
let container = buildDOM(`
<p>text 1<img src="http://placehold.it/100x100">text 2</p>
`);

let element = container.firstChild;
let plugins = [function(element, builder, {addMarkerable, nodeFinished}) {
if (element.nodeType === 3) {
if (element.textContent === 'text 1') {
addMarkerable(builder.createMarker('oh my'));
}
nodeFinished();
}
}];
parser = new SectionParser(builder, {plugins});
const sections = parser.parse(element);

assert.equal(sections.length, 1, '1 section');
assert.equal(sections[0].text, 'oh my');
});

0 comments on commit 01fec18

Please sign in to comment.