Skip to content

Commit

Permalink
Add text parser, use it for handling pasted text
Browse files Browse the repository at this point in the history
The text parser recognizes multiple sections for text separated by
line breaks and will parse lists when lines of text start with "*"
(unordered) or "<number>." (ordered).

fixes #263
  • Loading branch information
bantic committed Dec 16, 2015
1 parent e687d3d commit 4ae6803
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 20 deletions.
4 changes: 4 additions & 0 deletions src/js/parsers/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export default class HTMLParser {
this.options = options;
}

/**
* @param {String} html to parse
* @return {Post} A post abstract
*/
parse(html) {
let dom = parseHTML(html);
let parser = new DOMParser(this.builder, this.options);
Expand Down
84 changes: 84 additions & 0 deletions src/js/parsers/text.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import assert from 'mobiledoc-kit/utils/assert';
import {
MARKUP_SECTION_TYPE,
LIST_SECTION_TYPE
} from 'mobiledoc-kit/models/types';
import {
DEFAULT_TAG_NAME as DEFAULT_MARKUP_SECTION_TAG_NAME
} from 'mobiledoc-kit/models/markup-section';

const SECTION_BREAK = "\n";
const UL_LI_REGEX = /^\* (.*)$/;
const OL_LI_REGEX = /^\d.? (.*)$/;

export default class TextParser {
constructor(builder, options) {
this.builder = builder;
this.options = options;

this.post = this.builder.createPost();
this.prevSection = null;
}

/**
* @param {String} text to parse
* @return {Post} a post abstract
*/
parse(text) {
text.split(SECTION_BREAK).forEach(text => {
let section = this._parseSection(text);
this._appendSection(section);
});

return this.post;
}

_parseSection(text) {
let tagName = DEFAULT_MARKUP_SECTION_TAG_NAME,
type = MARKUP_SECTION_TYPE,
section;

if (UL_LI_REGEX.test(text)) {
tagName = 'ul';
type = LIST_SECTION_TYPE;
text = text.match(UL_LI_REGEX)[1];
} else if (OL_LI_REGEX.test(text)) {
tagName = 'ol';
type = LIST_SECTION_TYPE;
text = text.match(OL_LI_REGEX)[1];
}

let markers = [this.builder.createMarker(text)];

switch (type) {
case LIST_SECTION_TYPE:
let item = this.builder.createListItem(markers);
let list = this.builder.createListSection(tagName, [item]);
section = list;
break;
case MARKUP_SECTION_TYPE:
section = this.builder.createMarkupSection(tagName, markers);
break;
default:
assert(`Unknown type encountered ${type}`, false);
}

return section;
}

_appendSection(section) {
let isSameListSection =
section.isListSection &&
this.prevSection && this.prevSection.isListSection &&
this.prevSection.tagName === section.tagName;

if (isSameListSection) {
section.items.forEach(item => {
this.prevSection.items.append(item.clone());
});
} else {
this.post.sections.insertAfter(section, this.prevSection);
this.prevSection = section;
}
}
}
68 changes: 48 additions & 20 deletions src/js/utils/paste-utils.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
/* global JSON */
import mobiledocParsers from '../parsers/mobiledoc';
import HTMLParser from '../parsers/html';
import TextParser from '../parsers/text';
import HTMLRenderer from 'mobiledoc-html-renderer';
import TextRenderer from 'mobiledoc-text-renderer';

const MOBILEDOC_REGEX = new RegExp(/data\-mobiledoc='(.*?)'>/);
const MIME_TEXT_PLAIN = 'text/plain';
const MIME_TEXT_HTML = 'text/html';

function parsePostFromHTML(html, builder, plugins) {
let post;

if (MOBILEDOC_REGEX.test(html)) {
let mobiledocString = html.match(MOBILEDOC_REGEX)[1];
let mobiledoc = JSON.parse(mobiledocString);
post = mobiledocParsers.parse(builder, mobiledoc);
} else {
post = new HTMLParser(builder, {plugins}).parse(html);
}

return post;
}

function parsePostFromText(text, builder, plugins) {
let parser = new TextParser(builder, {plugins});
let post = parser.parse(text);
return post;
}

/**
* @param {Event} copyEvent
* @param {Editor}
* @return null
*/
export function setClipboardCopyData(copyEvent, editor) {
const { cursor, post } = editor;
const { clipboardData } = copyEvent;
Expand All @@ -12,35 +42,33 @@ export function setClipboardCopyData(copyEvent, editor) {
const mobiledoc = post.cloneRange(range);

let unknownCardHandler = () => {}; // ignore unknown cards
let {result: innerHTML } = new HTMLRenderer({unknownCardHandler})
.render(mobiledoc);
let {result: innerHTML } =
new HTMLRenderer({unknownCardHandler}).render(mobiledoc);

const html =
`<div data-mobiledoc='${JSON.stringify(mobiledoc)}'>${innerHTML}</div>`;
const {result: plain} = new TextRenderer({unknownCardHandler})
.render(mobiledoc);
const {result: plain} =
new TextRenderer({unknownCardHandler}).render(mobiledoc);

clipboardData.setData('text/plain', plain);
clipboardData.setData('text/html', html);
clipboardData.setData(MIME_TEXT_PLAIN, plain);
clipboardData.setData(MIME_TEXT_HTML, html);
}

/**
* @param {Event} pasteEvent
* @param {PostNodeBuilder} builder
* @param {Array} plugins parser plugins
* @return {Post}
*/
export function parsePostFromPaste(pasteEvent, builder, plugins=[]) {
let mobiledoc, post;
const mobiledocRegex = new RegExp(/data\-mobiledoc='(.*?)'>/);

let html = pasteEvent.clipboardData.getData('text/html');
let post;
let html = pasteEvent.clipboardData.getData(MIME_TEXT_HTML);

// Fallback to 'text/plain'
if (!html || html.length === 0) {
html = pasteEvent.clipboardData.getData('text/plain');
}

if (mobiledocRegex.test(html)) {
let mobiledocString = html.match(mobiledocRegex)[1];
mobiledoc = JSON.parse(mobiledocString);
post = mobiledocParsers.parse(builder, mobiledoc);
if (!html || html.length === 0) { // Fallback to 'text/plain'
let text = pasteEvent.clipboardData.getData(MIME_TEXT_PLAIN);
post = parsePostFromText(text);
} else {
post = new HTMLParser(builder, {plugins}).parse(html);
post = parsePostFromHTML(html, builder, plugins);
}

return post;
Expand Down
122 changes: 122 additions & 0 deletions tests/unit/parsers/text-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import TextParser from 'mobiledoc-kit/parsers/text';
import PostNodeBuilder from 'mobiledoc-kit/models/post-node-builder';
import Helpers from '../../test-helpers';

const {module, test} = Helpers;

let editorElement, builder, parser, editor;

module('Unit: Parser: TextParser', {
beforeEach() {
editorElement = $('#editor')[0];
builder = new PostNodeBuilder();
parser = new TextParser(builder);
},
afterEach() {
builder = null;
parser = null;
if (editor) {
editor.destroy();
editor = null;
}
}
});

test('#parse returns a markup section when given single line of text', (assert) => {
let text = 'some text';
let post = parser.parse(text);
let expected = Helpers.postAbstract.build(({post, markupSection, marker}) => {
return post([markupSection('p', [marker('some text')])]);
});

assert.postIsSimilar(post, expected);
});

test('#parse returns multiple markup sections when given multiple lines', (assert) => {
let text = ['first section', 'second section'].join('\n');
let post = parser.parse(text);
let expected = Helpers.postAbstract.build(({post, markupSection, marker}) => {
return post([
markupSection('p', [marker('first section')]),
markupSection('p', [marker('second section')])
]);
});

assert.postIsSimilar(post, expected);
});

test('#parse returns list section when text starts with "*"', (assert) => {
let text = '* a list item';

let post = parser.parse(text);
let expected = Helpers.postAbstract.build(({post, listSection, listItem, marker}) => {
return post([
listSection('ul', [listItem([marker('a list item')])])
]);
});

assert.postIsSimilar(post, expected);
});

test('#parse returns list section with multiple items when text starts with "*"', (assert) => {
let text = ['* first', '* second'].join('\n');

let post = parser.parse(text);
let expected = Helpers.postAbstract.build(({post, listSection, listItem, marker}) => {
return post([
listSection('ul', [
listItem([marker('first')]),
listItem([marker('second')])
])
]);
});

assert.postIsSimilar(post, expected);
});

test('#parse returns list sections separated by markup sections', (assert) => {
let text = ['* first list', 'middle section', '* second list'].join('\n');

let post = parser.parse(text);
let expected = Helpers.postAbstract.build(
({post, listSection, listItem, markupSection, marker}) => {
return post([
listSection('ul', [
listItem([marker('first list')])
]),
markupSection('p', [marker('middle section')]),
listSection('ul', [
listItem([marker('second list')])
])
]);
});

assert.postIsSimilar(post, expected);
});

test('#parse returns ordered list items', (assert) => {
let text = '1. first list';

let post = parser.parse(text);
let expected = Helpers.postAbstract.build(
({post, listSection, listItem, markupSection, marker}) => {
return post([listSection('ol', [listItem([marker('first list')])])]);
});

assert.postIsSimilar(post, expected);
});

test('#parse can have ordered and unordered lists together', (assert) => {
let text = ['1. ordered list', '* unordered list'].join('\n');

let post = parser.parse(text);
let expected = Helpers.postAbstract.build(
({post, listSection, listItem, markupSection, marker}) => {
return post([
listSection('ol', [listItem([marker('ordered list')])]),
listSection('ul', [listItem([marker('unordered list')])])
]);
});

assert.postIsSimilar(post, expected);
});

0 comments on commit 4ae6803

Please sign in to comment.