Skip to content

Commit

Permalink
Parse nbsp into spaces, render nbsp where needed
Browse files Browse the repository at this point in the history
* Mobiledoc should, ideally, not contain any HTML entities. Instead
  these should be converted into their property plain text pair. This
  changes the parsing code to disallow nbsp chars, instead converting
  them into regular spaces.
* The Content-Kit renderer must display spaces correctly when there are
  multiple spaces in a row. This patch ensures that at least every other
  space character is displayed as an nbsp.

Fixes #195
  • Loading branch information
mixonic committed Nov 3, 2015
1 parent c7340f2 commit 6a95af5
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 18 deletions.
10 changes: 9 additions & 1 deletion src/js/parsers/dom.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NO_BREAK_SPACE } from '../renderers/editor-dom';
import {
MARKUP_SECTION_TYPE,
LIST_SECTION_TYPE,
Expand All @@ -18,6 +19,13 @@ import Markup from 'content-kit-editor/models/markup';

const GOOGLE_DOCS_CONTAINER_ID_REGEX = /^docs\-internal\-guid/;

const NO_BREAK_SPACE_REGEX = new RegExp(NO_BREAK_SPACE, 'g');
export function transformHTMLText(textContent) {
let text = textContent;
text = text.replace(NO_BREAK_SPACE_REGEX, ' ');
return text;
}

function isGoogleDocsContainer(element) {
return !isTextNode(element) &&
normalizeTagName(element.tagName) === normalizeTagName('b') &&
Expand Down Expand Up @@ -154,7 +162,7 @@ export default class DOMParser {
let previousMarker;

walkTextNodes(element, (textNode) => {
const text = textNode.textContent;
const text = transformHTMLText(textNode.textContent);
let markups = this.collectMarkups(textNode, element);

let marker;
Expand Down
10 changes: 7 additions & 3 deletions src/js/parsers/section.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ import {
contains
} from 'content-kit-editor/utils/array-utils';

import {
transformHTMLText
} from '../parsers/dom';

function isListSection(section) {
return section.type === LIST_SECTION_TYPE;
}
Expand Down Expand Up @@ -164,8 +168,7 @@ export default class SectionParser {

// 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);
this._createMarker();
}

sections.push(state.section);
Expand Down Expand Up @@ -222,7 +225,8 @@ export default class SectionParser {

_createMarker() {
let { state } = this;
let marker = this.builder.createMarker(state.text, state.markups);
let text = transformHTMLText(state.text);
let marker = this.builder.createMarker(text, state.markups);
state.section.markers.append(marker);
state.text = '';
}
Expand Down
33 changes: 21 additions & 12 deletions src/js/renderers/editor-dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@ function createElementFromMarkup(doc, markup) {
return element;
}

// FIXME: This can be done more efficiently with a single pass
// building a correct string based on the original.
function renderHTMLText(marker) {
let text = marker.value;
// If the first marker has a leading space or the last marker has a
// trailing space, the browser will collapse the space when we position
// the cursor.
// See https://github.com/bustlelabs/content-kit-editor/issues/68
// and https://github.com/bustlelabs/content-kit-editor/issues/75
if (!marker.next && endsWith(text, SPACE)) {
text = text.substr(0, text.length - 1) + NO_BREAK_SPACE;
} else if ((!marker.prev || endsWith(marker.prev.value, SPACE)) && startsWith(text, SPACE)) {
text = NO_BREAK_SPACE + text.substr(1);
}
text = text.replace(/ ( )/g, () => {
return ' '+NO_BREAK_SPACE;
});
return text;
}

// ascends from element upward, returning the last parent node that is not
// parentElement
function penultimateParentOf(element, parentElement) {
Expand Down Expand Up @@ -79,18 +99,7 @@ function getNextMarkerElement(renderNode) {
}

function renderMarker(marker, element, previousRenderNode) {
let text = marker.value;

// If the first marker has a leading space or the last marker has a
// trailing space, the browser will collapse the space when we position
// the cursor.
// See https://github.com/bustlelabs/content-kit-editor/issues/68
// and https://github.com/bustlelabs/content-kit-editor/issues/75
if (!marker.next && endsWith(text, SPACE)) {
text = text.substr(0, text.length - 1) + NO_BREAK_SPACE;
} else if (!marker.prev && startsWith(text, SPACE)) {
text = NO_BREAK_SPACE + text.substr(1);
}
let text = renderHTMLText(marker);

let textNode = document.createTextNode(text);
let currentElement = textNode;
Expand Down
27 changes: 27 additions & 0 deletions tests/acceptance/editor-selections-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ const { test, module } = Helpers;

let fixture, editor, editorElement;

const mobileDocWithSection = {
version: MOBILEDOC_VERSION,
sections: [
[],
[
[1, "P", [
[[], 0, "one trick pony"]
]]
]
]
};

const mobileDocWith2Sections = {
version: MOBILEDOC_VERSION,
sections: [
Expand Down Expand Up @@ -252,6 +264,21 @@ test('keystroke of printable character while text is selected deletes the text',
'updates the section');
});

test('selecting text bounded by space and typing replaces it', (assert) => {
editor = new Editor({mobiledoc: mobileDocWithSection});
editor.render(editorElement);

Helpers.dom.selectText('trick', editorElement);
Helpers.dom.insertText(editor, 'X');

assert.hasElement('#editor p:contains(one X pony)',
'new text present');

Helpers.dom.insertText(editor, 'Y');
assert.hasElement('#editor p:contains(one XY pony)',
'cursor positioned correctly');
});

test('selecting all text across sections and hitting enter deletes and moves cursor to empty section', (assert) => {
editor = new Editor({mobiledoc: mobileDocWith2Sections});
editor.render(editorElement);
Expand Down
39 changes: 37 additions & 2 deletions tests/unit/parsers/dom-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import DOMParser from 'content-kit-editor/parsers/dom';
import PostNodeBuilder from 'content-kit-editor/models/post-node-builder';
import Helpers from '../../test-helpers';
import { Editor } from 'content-kit-editor';
import { NO_BREAK_SPACE } from 'content-kit-editor/renderers/editor-dom';

const {module, test} = Helpers;

Expand Down Expand Up @@ -50,8 +51,17 @@ test('#parse can parse multiple elements', (assert) => {
assert.equal(s2.markers.head.value, 'some other text');
});

test('#parse can parse spaces and breaking spaces', (assert) => {
let element = buildDOM("<p>some &nbsp;text &nbsp;&nbsp;for &nbsp; &nbsp;you</p>");

const post = parser.parse(element);
const s1 = post.sections.head;
assert.equal(s1.markers.length, 1, 's1 has 1 marker');
assert.equal(s1.markers.head.value, 'some text for you', 'has text');
});

test('editor#reparse catches changes to section', (assert) => {
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) =>
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) =>
post([
markupSection('p', [marker('the marker')])
])
Expand All @@ -75,8 +85,33 @@ test('editor#reparse catches changes to section', (assert) => {
assert.equal(section.text, 'the NEW marker');
});

test('editor#reparse parses spaces and breaking spaces', (assert) => {
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) =>
post([
markupSection('p', [marker('the marker')])
])
);
editor = new Editor({mobiledoc});
const editorElement = $('<div id="editor"></div>')[0];
$('#qunit-fixture').append(editorElement);
editor.render(editorElement);

assert.hasElement('#editor p:contains(the marker)', 'precond - rendered correctly');

const p = $('#editor p:eq(0)')[0];
p.childNodes[0].textContent = `some ${NO_BREAK_SPACE}text ${NO_BREAK_SPACE}${NO_BREAK_SPACE}for ${NO_BREAK_SPACE} ${NO_BREAK_SPACE}you`;

// In Firefox, changing the text content changes the selection, so re-set it
Helpers.dom.moveCursorTo(p.childNodes[0]);

editor.reparse();

const section = editor.post.sections.head;
assert.equal(section.text, 'some text for you');
});

test('editor#reparse catches changes to list section', (assert) => {
const mobiledoc = Helpers.mobiledoc.build(({post, listSection, listItem, marker}) =>
const mobiledoc = Helpers.mobiledoc.build(({post, listSection, listItem, marker}) =>
post([
listSection('ul', [
listItem([marker('the list item')])
Expand Down
15 changes: 15 additions & 0 deletions tests/unit/renderers/editor-dom-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import PostNodeBuilder from 'content-kit-editor/models/post-node-builder';
import Renderer from 'content-kit-editor/renderers/editor-dom';
import RenderTree from 'content-kit-editor/models/render-tree';
import Helpers from '../../test-helpers';
import { NO_BREAK_SPACE } from 'content-kit-editor/renderers/editor-dom';
const { module, test } = Helpers;

const DATA_URL = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=";
Expand Down Expand Up @@ -589,6 +590,20 @@ test('renders markup section "pull-quote" as <div class="pull-quote"></div>', (a
assert.equal(renderTree.rootElement.innerHTML, expectedDOM.outerHTML);
});

test('renders a bunch of spaces with nbsp', (assert) => {
const post = Helpers.postAbstract.build(({post, markupSection, marker}) => {
return post([markupSection('p', [marker('a b c d ')])]);
});
const renderTree = new RenderTree(post);
render(renderTree);

const expectedDOM = Helpers.dom.build(t => {
return t('p', {}, [t.text(`a b ${NO_BREAK_SPACE}c ${NO_BREAK_SPACE} ${NO_BREAK_SPACE}d${NO_BREAK_SPACE}`)]);
});

assert.equal(renderTree.rootElement.innerHTML, expectedDOM.outerHTML);
});

/*
test("It renders a renderTree with rendered dirty section", (assert) => {
/*
Expand Down

0 comments on commit 6a95af5

Please sign in to comment.