2019-09-04 13:37:27 +03:00
|
|
|
/*
|
|
|
|
Copyright 2019 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.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Some common queries and transformations on the editor model
|
|
|
|
*/
|
|
|
|
|
2019-09-05 17:22:42 +03:00
|
|
|
export function replaceRangeAndExpandSelection(range, newParts) {
|
|
|
|
const {model} = range;
|
2019-09-04 13:37:27 +03:00
|
|
|
model.transform(() => {
|
|
|
|
const oldLen = range.length;
|
|
|
|
const addedLen = range.replace(newParts);
|
|
|
|
const firstOffset = range.start.asOffset(model);
|
|
|
|
const lastOffset = firstOffset.add(oldLen + addedLen);
|
|
|
|
return model.startRange(firstOffset.asPosition(model), lastOffset.asPosition(model));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-09-05 17:22:42 +03:00
|
|
|
export function replaceRangeAndMoveCaret(range, newParts) {
|
|
|
|
const {model} = range;
|
|
|
|
model.transform(() => {
|
|
|
|
const oldLen = range.length;
|
|
|
|
const addedLen = range.replace(newParts);
|
|
|
|
const firstOffset = range.start.asOffset(model);
|
|
|
|
const lastOffset = firstOffset.add(oldLen + addedLen);
|
|
|
|
return lastOffset.asPosition(model);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-09-04 13:40:03 +03:00
|
|
|
export function rangeStartsAtBeginningOfLine(range) {
|
|
|
|
const {model} = range;
|
|
|
|
const startsWithPartial = range.start.offset !== 0;
|
|
|
|
const isFirstPart = range.start.index === 0;
|
|
|
|
const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === "newline";
|
|
|
|
return !startsWithPartial && (isFirstPart || previousIsNewline);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function rangeEndsAtEndOfLine(range) {
|
|
|
|
const {model} = range;
|
|
|
|
const lastPart = model.parts[range.end.index];
|
|
|
|
const endsWithPartial = range.end.offset !== lastPart.length;
|
|
|
|
const isLastPart = range.end.index === model.parts.length - 1;
|
|
|
|
const nextIsNewline = !isLastPart && model.parts[range.end.index + 1].type === "newline";
|
|
|
|
return !endsWithPartial && (isLastPart || nextIsNewline);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function formatRangeAsQuote(range) {
|
|
|
|
const {model, parts} = range;
|
|
|
|
const {partCreator} = model;
|
|
|
|
for (let i = 0; i < parts.length; ++i) {
|
|
|
|
const part = parts[i];
|
|
|
|
if (part.type === "newline") {
|
|
|
|
parts.splice(i + 1, 0, partCreator.plain("> "));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
parts.unshift(partCreator.plain("> "));
|
|
|
|
if (!rangeStartsAtBeginningOfLine(range)) {
|
|
|
|
parts.unshift(partCreator.newline());
|
|
|
|
}
|
2019-09-04 17:01:28 +03:00
|
|
|
if (!rangeEndsAtEndOfLine(range)) {
|
2019-09-04 13:40:03 +03:00
|
|
|
parts.push(partCreator.newline());
|
|
|
|
}
|
|
|
|
|
|
|
|
parts.push(partCreator.newline());
|
2019-09-05 17:22:42 +03:00
|
|
|
replaceRangeAndExpandSelection(range, parts);
|
2019-09-04 13:40:03 +03:00
|
|
|
}
|
|
|
|
|
2019-09-04 13:40:21 +03:00
|
|
|
export function formatRangeAsCode(range) {
|
|
|
|
const {model, parts} = range;
|
|
|
|
const {partCreator} = model;
|
|
|
|
const needsBlock = parts.some(p => p.type === "newline");
|
|
|
|
if (needsBlock) {
|
|
|
|
parts.unshift(partCreator.plain("```"), partCreator.newline());
|
|
|
|
if (!rangeStartsAtBeginningOfLine(range)) {
|
|
|
|
parts.unshift(partCreator.newline());
|
|
|
|
}
|
|
|
|
parts.push(
|
|
|
|
partCreator.newline(),
|
|
|
|
partCreator.plain("```"));
|
2019-09-04 17:01:28 +03:00
|
|
|
if (!rangeEndsAtEndOfLine(range)) {
|
2019-09-04 13:40:21 +03:00
|
|
|
parts.push(partCreator.newline());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
parts.unshift(partCreator.plain("`"));
|
|
|
|
parts.push(partCreator.plain("`"));
|
|
|
|
}
|
2019-09-05 17:22:42 +03:00
|
|
|
replaceRangeAndExpandSelection(range, parts);
|
2019-09-04 13:40:21 +03:00
|
|
|
}
|
|
|
|
|
2020-01-22 16:37:27 +03:00
|
|
|
// parts helper methods
|
|
|
|
const isBlank = part => !part.text || !/\S/.test(part.text);
|
|
|
|
const isNL = part => part.type === "newline";
|
|
|
|
|
2019-09-06 17:25:06 +03:00
|
|
|
export function toggleInlineFormat(range, prefix, suffix = prefix) {
|
2019-09-04 13:37:27 +03:00
|
|
|
const {model, parts} = range;
|
|
|
|
const {partCreator} = model;
|
2019-09-06 17:25:06 +03:00
|
|
|
|
2020-01-21 18:32:32 +03:00
|
|
|
// compute paragraph [start, end] indexes
|
|
|
|
const paragraphIndexes = [];
|
|
|
|
let startIndex = 0;
|
2020-01-23 14:22:22 +03:00
|
|
|
// start at i=2 because we look at i and up to two parts behind to detect paragraph breaks at their end
|
2020-01-21 18:32:32 +03:00
|
|
|
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
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2020-01-22 16:37:27 +03:00
|
|
|
// if at a paragraph break, store the indexes of the paragraph
|
2020-01-21 18:32:32 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2020-01-22 16:37:27 +03:00
|
|
|
|
|
|
|
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]);
|
2019-09-06 17:25:06 +03:00
|
|
|
}
|
2020-01-21 18:32:32 +03:00
|
|
|
|
|
|
|
// 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[base].serialize();
|
|
|
|
partWithoutPrefix.text = partWithoutPrefix.text.substr(prefix.length);
|
|
|
|
parts[base] = partCreator.deserializePart(partWithoutPrefix);
|
|
|
|
|
|
|
|
const partWithoutSuffix = parts[index - 1].serialize();
|
|
|
|
const suffixPartText = partWithoutSuffix.text;
|
|
|
|
partWithoutSuffix.text = suffixPartText.substring(0, suffixPartText.length - suffix.length);
|
|
|
|
parts[index - 1] = partCreator.deserializePart(partWithoutSuffix);
|
|
|
|
} else {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2019-09-05 17:22:42 +03:00
|
|
|
replaceRangeAndExpandSelection(range, parts);
|
2019-09-04 13:37:27 +03:00
|
|
|
}
|