Initial attempt to make toggleInlineFormat paragraph-aware

pull/21833/head
Michael Telatynski 2020-01-21 15:32:32 +00:00
parent adec308529
commit b2aba6db35
5 changed files with 221 additions and 27 deletions

View File

@ -250,7 +250,7 @@ function parseHtmlMessage(html, partCreator, isQuotedMessage) {
}
export function parsePlainTextMessage(body, partCreator, isQuotedMessage) {
const lines = body.split("\n");
const lines = body.split(/\r\n|\r|\n/g); // split on any new-line combination not just \n
const parts = lines.reduce((parts, line, i) => {
if (isQuotedMessage) {
parts.push(partCreator.plain(QUOTE_LINE_PREFIX));

View File

@ -104,23 +104,63 @@ export function toggleInlineFormat(range, prefix, suffix = prefix) {
const {model, parts} = range;
const {partCreator} = model;
const isFormatted = parts.length &&
parts[0].text.startsWith(prefix) &&
parts[parts.length - 1].text.endsWith(suffix);
// compute paragraph [start, end] indexes
const paragraphIndexes = [];
let startIndex = 0;
// let seenNewlines = 0;
for (let i = 2; i < parts.length; i++) {
// paragraph breaks can be denoted in a multitude of ways,
// - 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 (isNL(parts[i - 1]) && isNL(parts[i])) {
paragraphIndexes.push([startIndex, i - 1]);
startIndex = i + 1;
} else if (isNL(parts[i - 2]) && isBlank(parts[i - 1]) && isNL(parts[i])) {
paragraphIndexes.push([startIndex, i - 2]);
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]);
}
// keep track of how many things we have inserted as an offset:=0
let offset = 0;
paragraphIndexes.forEach(([startIndex, endIndex]) => {
// for each paragraph apply the same rule
const base = startIndex + offset;
const index = endIndex + offset;
const isFormatted = (index - base > 0) &&
parts[base].text.startsWith(prefix) &&
parts[index - 1].text.endsWith(suffix);
if (isFormatted) {
// remove prefix and suffix
const partWithoutPrefix = parts[0].serialize();
const partWithoutPrefix = parts[base].serialize();
partWithoutPrefix.text = partWithoutPrefix.text.substr(prefix.length);
parts[0] = partCreator.deserializePart(partWithoutPrefix);
parts[base] = partCreator.deserializePart(partWithoutPrefix);
const partWithoutSuffix = parts[parts.length - 1].serialize();
const partWithoutSuffix = parts[index - 1].serialize();
const suffixPartText = partWithoutSuffix.text;
partWithoutSuffix.text = suffixPartText.substring(0, suffixPartText.length - suffix.length);
parts[parts.length - 1] = partCreator.deserializePart(partWithoutSuffix);
parts[index - 1] = partCreator.deserializePart(partWithoutSuffix);
} else {
parts.unshift(partCreator.plain(prefix));
parts.push(partCreator.plain(suffix));
parts.splice(index, 0, partCreator.plain(suffix)); // splice in the later one first to not change offset
parts.splice(base, 0, partCreator.plain(prefix));
offset += 2; // offset index to account for the two items we just spliced in
}
});
replaceRangeAndExpandSelection(range, parts);
}

View File

@ -67,3 +67,13 @@ export function createPartCreator(completions = []) {
};
return new PartCreator(new MockRoom(), new MockClient(), autoCompleteCreator);
}
export function createRenderer() {
const render = (c) => {
render.caret = c;
render.count += 1;
};
render.count = 0;
render.caret = null;
return render;
}

View File

@ -0,0 +1,154 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {getLineAndNodePosition} from "../../src/editor/caret";
import EditorModel from "../../src/editor/model";
import {createPartCreator, createRenderer} from "./mock";
import {toggleInlineFormat} from "../../src/editor/operations";
describe('editor/operations: formatting operations', () => {
describe('toggleInlineFormat', () => {
it('works for words', () => {
const renderer = createRenderer();
const pc = createPartCreator();
const model = new EditorModel([
pc.plain("hello world!"),
], pc, renderer);
const range = model.startRange(model.positionForOffset(6, false),
model.positionForOffset(11, false)); // around "world"
expect(range.parts[0].text).toBe("world");
expect(model.serializeParts()).toEqual([{"text": "hello world!", "type": "plain"}]);
toggleInlineFormat(range, "_");
expect(model.serializeParts()).toEqual([{"text": "hello _world_!", "type": "plain"}]);
});
it('works for parts of words', () => {
const renderer = createRenderer();
const pc = createPartCreator();
const model = new EditorModel([
pc.plain("hello world!"),
], pc, renderer);
const range = model.startRange(model.positionForOffset(7, false),
model.positionForOffset(10, false)); // around "orl"
expect(range.parts[0].text).toBe("orl");
expect(model.serializeParts()).toEqual([{"text": "hello world!", "type": "plain"}]);
toggleInlineFormat(range, "*");
expect(model.serializeParts()).toEqual([{"text": "hello w*orl*d!", "type": "plain"}]);
});
it('works for around pills', () => {
const renderer = createRenderer();
const pc = createPartCreator();
const model = new EditorModel([
pc.plain("hello there "),
pc.atRoomPill("@room"),
pc.plain(", how are you doing?"),
], pc, renderer);
const range = model.startRange(model.positionForOffset(6, false),
model.positionForOffset(30, false)); // around "there @room, how are you"
expect(range.parts.map(p => p.text).join("")).toBe("there @room, how are you");
expect(model.serializeParts()).toEqual([
{"text": "hello there ", "type": "plain"},
{"text": "@room", "type": "at-room-pill"},
{"text": ", how are you doing?", "type": "plain"},
]);
toggleInlineFormat(range, "_");
expect(model.serializeParts()).toEqual([
{"text": "hello _there ", "type": "plain"},
{"text": "@room", "type": "at-room-pill"},
{"text": ", how are you_ doing?", "type": "plain"},
]);
});
it('works for a paragraph', () => {
const renderer = createRenderer();
const pc = createPartCreator();
const model = new EditorModel([
pc.plain("hello world,"),
pc.newline(),
pc.plain("how are you doing?"),
], pc, renderer);
const range = model.startRange(model.positionForOffset(6, false),
model.positionForOffset(16, false)); // around "world,\nhow"
expect(range.parts.map(p => p.text).join("")).toBe("world,\nhow");
expect(model.serializeParts()).toEqual([
{"text": "hello world,", "type": "plain"},
{"text": "\n", "type": "newline"},
{"text": "how are you doing?", "type": "plain"},
]);
toggleInlineFormat(range, "**");
expect(model.serializeParts()).toEqual([
{"text": "hello **world,", "type": "plain"},
{"text": "\n", "type": "newline"},
{"text": "how** are you doing?", "type": "plain"},
]);
});
it('works for multiple paragraph', () => {
const renderer = createRenderer();
const pc = createPartCreator();
const model = new EditorModel([
pc.plain("hello world,"),
pc.newline(),
pc.plain("how are you doing?"),
pc.newline(),
pc.newline(),
pc.plain("new paragraph"),
], pc, renderer);
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"},
{"text": "how are you doing?", "type": "plain"},
{"text": "\n", "type": "newline"},
{"text": "\n", "type": "newline"},
{"text": "new paragraph", "type": "plain"},
]);
toggleInlineFormat(range, "__");
expect(model.serializeParts()).toEqual([
{"text": "__hello world,", "type": "plain"},
{"text": "\n", "type": "newline"},
{"text": "how are you doing?__", "type": "plain"},
{"text": "\n", "type": "newline"},
{"text": "\n", "type": "newline"},
{"text": "__new paragraph__", "type": "plain"},
]);
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"},
{"text": "how are you doing?", "type": "plain"},
{"text": "\n", "type": "newline"},
{"text": "\n", "type": "newline"},
{"text": "new paragraph", "type": "plain"},
]);
});
});
});

View File

@ -15,17 +15,7 @@ limitations under the License.
*/
import EditorModel from "../../src/editor/model";
import {createPartCreator} from "./mock";
function createRenderer() {
const render = (c) => {
render.caret = c;
render.count += 1;
};
render.count = 0;
render.caret = null;
return render;
}
import {createPartCreator, createRenderer} from "./mock";
const pillChannel = "#riot-dev:matrix.org";