From 832da062cccc814fe5748e60a9c71f03290a6eff Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 13:37:27 +0000 Subject: [PATCH] Improve trailing spurious breaks + tests --- src/editor/deserialize.js | 2 +- src/editor/operations.js | 16 +++++--- test/editor/operations-test.js | 67 ++++++++++++++++++++++++++-------- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index 7ba4c3eda3..190963f357 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -250,7 +250,7 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) { } export function parsePlainTextMessage(body, partCreator, isQuotedMessage) { - const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n + const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n, collapses \r\n const parts = lines.reduce((parts, line, i) => { if (isQuotedMessage) { parts.push(partCreator.plain(QUOTE_LINE_PREFIX)); diff --git a/src/editor/operations.js b/src/editor/operations.js index 6bae60e6b8..d0115d9ca7 100644 --- a/src/editor/operations.js +++ b/src/editor/operations.js @@ -100,6 +100,10 @@ export function formatRangeAsCode(range) { replaceRangeAndExpandSelection(range, parts); } +// parts helper methods +const isBlank = part => !part.text || !/\S/.test(part.text); +const isNL = part => part.type === "newline"; + export function toggleInlineFormat(range, prefix, suffix = prefix) { const {model, parts} = range; const {partCreator} = model; @@ -113,14 +117,12 @@ export function toggleInlineFormat(range, prefix, suffix = prefix) { // - 2 newline parts in sequence // - newline part, plain(<empty or just spaces>), newline part - const isBlank = part => !part.text || !/\S/.test(part.text); - const isNL = part => part.type === "newline"; - // bump startIndex onto the first non-blank after the paragraph ending if (isBlank(parts[i - 2]) && isNL(parts[i - 1]) && !isNL(parts[i]) && !isBlank(parts[i])) { startIndex = i; } + // if at a paragraph break, store the indexes of the paragraph if (isNL(parts[i - 1]) && isNL(parts[i])) { paragraphIndexes.push([startIndex, i - 1]); startIndex = i + 1; @@ -129,9 +131,11 @@ export function toggleInlineFormat(range, prefix, suffix = prefix) { startIndex = i + 1; } } - if (startIndex < parts.length) { - // TODO don't use parts.length here to clean up any trailing cruft - paragraphIndexes.push([startIndex, parts.length]); + + const lastNonEmptyPart = parts.map(isBlank).lastIndexOf(false); + // If we have not yet included the final paragraph then add it now + if (startIndex <= lastNonEmptyPart) { + paragraphIndexes.push([startIndex, lastNonEmptyPart + 1]); } // keep track of how many things we have inserted as an offset:=0 diff --git a/test/editor/operations-test.js b/test/editor/operations-test.js index 872cc78bdb..90a9812306 100644 --- a/test/editor/operations-test.js +++ b/test/editor/operations-test.js @@ -18,6 +18,8 @@ import EditorModel from "../../src/editor/model"; import {createPartCreator, createRenderer} from "./mock"; import {toggleInlineFormat} from "../../src/editor/operations"; +const SERIALIZED_NEWLINE = {"text": "\n", "type": "newline"}; + describe('editor/operations: formatting operations', () => { describe('toggleInlineFormat', () => { it('works for words', () => { @@ -93,17 +95,54 @@ describe('editor/operations: formatting operations', () => { expect(range.parts.map(p => p.text).join("")).toBe("world,\nhow"); expect(model.serializeParts()).toEqual([ {"text": "hello world,", "type": "plain"}, - {"text": "\n", "type": "newline"}, + SERIALIZED_NEWLINE, {"text": "how are you doing?", "type": "plain"}, ]); toggleInlineFormat(range, "**"); expect(model.serializeParts()).toEqual([ {"text": "hello **world,", "type": "plain"}, - {"text": "\n", "type": "newline"}, + SERIALIZED_NEWLINE, {"text": "how** are you doing?", "type": "plain"}, ]); }); + it('works for a paragraph with spurious breaks around it in selected range', () => { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.newline(), + pc.newline(), + pc.plain("hello world,"), + pc.newline(), + pc.plain("how are you doing?"), + pc.newline(), + pc.newline(), + ], pc, renderer); + + const range = model.startRange(model.positionForOffset(0, false), model.getPositionAtEnd()); // select-all + + expect(range.parts.map(p => p.text).join("")).toBe("\n\nhello world,\nhow are you doing?\n\n"); + expect(model.serializeParts()).toEqual([ + SERIALIZED_NEWLINE, + SERIALIZED_NEWLINE, + {"text": "hello world,", "type": "plain"}, + SERIALIZED_NEWLINE, + {"text": "how are you doing?", "type": "plain"}, + SERIALIZED_NEWLINE, + SERIALIZED_NEWLINE, + ]); + toggleInlineFormat(range, "**"); + expect(model.serializeParts()).toEqual([ + SERIALIZED_NEWLINE, + SERIALIZED_NEWLINE, + {"text": "**hello world,", "type": "plain"}, + SERIALIZED_NEWLINE, + {"text": "how are you doing?**", "type": "plain"}, + SERIALIZED_NEWLINE, + SERIALIZED_NEWLINE, + ]); + }); + it('works for multiple paragraph', () => { const renderer = createRenderer(); const pc = createPartCreator(); @@ -116,36 +155,34 @@ describe('editor/operations: formatting operations', () => { pc.plain("new paragraph"), ], pc, renderer); - let range = model.startRange(model.positionForOffset(0, true), - model.getPositionAtEnd()); // select-all + let range = model.startRange(model.positionForOffset(0, true), model.getPositionAtEnd()); // select-all expect(model.serializeParts()).toEqual([ {"text": "hello world,", "type": "plain"}, - {"text": "\n", "type": "newline"}, + SERIALIZED_NEWLINE, {"text": "how are you doing?", "type": "plain"}, - {"text": "\n", "type": "newline"}, - {"text": "\n", "type": "newline"}, + SERIALIZED_NEWLINE, + SERIALIZED_NEWLINE, {"text": "new paragraph", "type": "plain"}, ]); toggleInlineFormat(range, "__"); expect(model.serializeParts()).toEqual([ {"text": "__hello world,", "type": "plain"}, - {"text": "\n", "type": "newline"}, + SERIALIZED_NEWLINE, {"text": "how are you doing?__", "type": "plain"}, - {"text": "\n", "type": "newline"}, - {"text": "\n", "type": "newline"}, + SERIALIZED_NEWLINE, + SERIALIZED_NEWLINE, {"text": "__new paragraph__", "type": "plain"}, ]); - range = model.startRange(model.positionForOffset(0, true), - model.getPositionAtEnd()); // select-all + range = model.startRange(model.positionForOffset(0, true), model.getPositionAtEnd()); // select-all console.log("RANGE", range.parts); toggleInlineFormat(range, "__"); expect(model.serializeParts()).toEqual([ {"text": "hello world,", "type": "plain"}, - {"text": "\n", "type": "newline"}, + SERIALIZED_NEWLINE, {"text": "how are you doing?", "type": "plain"}, - {"text": "\n", "type": "newline"}, - {"text": "\n", "type": "newline"}, + SERIALIZED_NEWLINE, + SERIALIZED_NEWLINE, {"text": "new paragraph", "type": "plain"}, ]); });