Fix: "Code formatting button does not escape backticks" (#8181)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
949b3cc650
commit
6b13988eaa
3 changed files with 94 additions and 4 deletions
|
@ -32,7 +32,7 @@ function escape(text: string): string {
|
||||||
|
|
||||||
// Finds the length of the longest backtick sequence in the given text, used for
|
// Finds the length of the longest backtick sequence in the given text, used for
|
||||||
// escaping backticks in code blocks
|
// escaping backticks in code blocks
|
||||||
function longestBacktickSequence(text: string): number {
|
export function longestBacktickSequence(text: string): number {
|
||||||
let length = 0;
|
let length = 0;
|
||||||
let currentLength = 0;
|
let currentLength = 0;
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import Range from "./range";
|
import Range from "./range";
|
||||||
import { Part, Type } from "./parts";
|
import { Part, Type } from "./parts";
|
||||||
import { Formatting } from "../components/views/rooms/MessageComposerFormatBar";
|
import { Formatting } from "../components/views/rooms/MessageComposerFormatBar";
|
||||||
|
import { longestBacktickSequence } from './deserialize';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some common queries and transformations on the editor model
|
* Some common queries and transformations on the editor model
|
||||||
|
@ -181,12 +182,12 @@ export function formatRangeAsCode(range: Range): void {
|
||||||
|
|
||||||
const hasBlockFormatting = (range.length > 0)
|
const hasBlockFormatting = (range.length > 0)
|
||||||
&& range.text.startsWith("```")
|
&& range.text.startsWith("```")
|
||||||
&& range.text.endsWith("```");
|
&& range.text.endsWith("```")
|
||||||
|
&& range.text.includes('\n');
|
||||||
|
|
||||||
const needsBlockFormatting = parts.some(p => p.type === Type.Newline);
|
const needsBlockFormatting = parts.some(p => p.type === Type.Newline);
|
||||||
|
|
||||||
if (hasBlockFormatting) {
|
if (hasBlockFormatting) {
|
||||||
// Remove previously pushed backticks and new lines
|
|
||||||
parts.shift();
|
parts.shift();
|
||||||
parts.pop();
|
parts.pop();
|
||||||
if (parts[0]?.text === "\n" && parts[parts.length - 1]?.text === "\n") {
|
if (parts[0]?.text === "\n" && parts[parts.length - 1]?.text === "\n") {
|
||||||
|
@ -205,7 +206,10 @@ export function formatRangeAsCode(range: Range): void {
|
||||||
parts.push(partCreator.newline());
|
parts.push(partCreator.newline());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toggleInlineFormat(range, "`");
|
const fenceLen = longestBacktickSequence(range.text);
|
||||||
|
const hasInlineFormatting = range.text.startsWith("`") && range.text.endsWith("`");
|
||||||
|
//if it's already formatted untoggle based on fenceLen which returns the max. num of backtick within a text else increase the fence backticks with a factor of 1.
|
||||||
|
toggleInlineFormat(range, "`".repeat(hasInlineFormatting ? fenceLen : fenceLen + 1));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,6 +244,7 @@ export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix
|
||||||
// compute paragraph [start, end] indexes
|
// compute paragraph [start, end] indexes
|
||||||
const paragraphIndexes = [];
|
const paragraphIndexes = [];
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
|
|
||||||
// start at i=2 because we look at i and up to two parts behind to detect paragraph breaks at their end
|
// start at i=2 because we look at i and up to two parts behind to detect paragraph breaks at their end
|
||||||
for (let i = 2; i < parts.length; i++) {
|
for (let i = 2; i < parts.length; i++) {
|
||||||
// paragraph breaks can be denoted in a multitude of ways,
|
// paragraph breaks can be denoted in a multitude of ways,
|
||||||
|
|
|
@ -20,8 +20,10 @@ import {
|
||||||
toggleInlineFormat,
|
toggleInlineFormat,
|
||||||
selectRangeOfWordAtCaret,
|
selectRangeOfWordAtCaret,
|
||||||
formatRange,
|
formatRange,
|
||||||
|
formatRangeAsCode,
|
||||||
} from "../../src/editor/operations";
|
} from "../../src/editor/operations";
|
||||||
import { Formatting } from "../../src/components/views/rooms/MessageComposerFormatBar";
|
import { Formatting } from "../../src/components/views/rooms/MessageComposerFormatBar";
|
||||||
|
import { longestBacktickSequence } from '../../src/editor/deserialize';
|
||||||
|
|
||||||
const SERIALIZED_NEWLINE = { "text": "\n", "type": "newline" };
|
const SERIALIZED_NEWLINE = { "text": "\n", "type": "newline" };
|
||||||
|
|
||||||
|
@ -43,6 +45,89 @@ describe('editor/operations: formatting operations', () => {
|
||||||
expect(model.serializeParts()).toEqual([{ "text": "hello _world_!", "type": "plain" }]);
|
expect(model.serializeParts()).toEqual([{ "text": "hello _world_!", "type": "plain" }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('escape backticks', () => {
|
||||||
|
it('works for escaping backticks in between texts', () => {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("hello ` world!"),
|
||||||
|
], pc, renderer);
|
||||||
|
|
||||||
|
const range = model.startRange(model.positionForOffset(0, false),
|
||||||
|
model.positionForOffset(13, false)); // hello ` world
|
||||||
|
|
||||||
|
expect(range.parts[0].text.trim().includes("`")).toBeTruthy();
|
||||||
|
expect(longestBacktickSequence(range.parts[0].text.trim())).toBe(1);
|
||||||
|
expect(model.serializeParts()).toEqual([{ "text": "hello ` world!", "type": "plain" }]);
|
||||||
|
formatRangeAsCode(range);
|
||||||
|
expect(model.serializeParts()).toEqual([{ "text": "``hello ` world``!", "type": "plain" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('escapes longer backticks in between text', () => {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("hello```world"),
|
||||||
|
], pc, renderer);
|
||||||
|
|
||||||
|
const range = model.startRange(model.positionForOffset(0, false),
|
||||||
|
model.getPositionAtEnd()); // hello```world
|
||||||
|
|
||||||
|
expect(range.parts[0].text.includes("`")).toBeTruthy();
|
||||||
|
expect(longestBacktickSequence(range.parts[0].text)).toBe(3);
|
||||||
|
expect(model.serializeParts()).toEqual([{ "text": "hello```world", "type": "plain" }]);
|
||||||
|
formatRangeAsCode(range);
|
||||||
|
expect(model.serializeParts()).toEqual([{ "text": "````hello```world````", "type": "plain" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('escapes non-consecutive with varying length backticks in between text', () => {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("hell```o`w`o``rld"),
|
||||||
|
], pc, renderer);
|
||||||
|
|
||||||
|
const range = model.startRange(model.positionForOffset(0, false),
|
||||||
|
model.getPositionAtEnd()); // hell```o`w`o``rld
|
||||||
|
expect(range.parts[0].text.includes("`")).toBeTruthy();
|
||||||
|
expect(longestBacktickSequence(range.parts[0].text)).toBe(3);
|
||||||
|
expect(model.serializeParts()).toEqual([{ "text": "hell```o`w`o``rld", "type": "plain" }]);
|
||||||
|
formatRangeAsCode(range);
|
||||||
|
expect(model.serializeParts()).toEqual([{ "text": "````hell```o`w`o``rld````", "type": "plain" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('untoggles correctly if its already formatted', () => {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("```hello``world```"),
|
||||||
|
], pc, renderer);
|
||||||
|
|
||||||
|
const range = model.startRange(model.positionForOffset(0, false),
|
||||||
|
model.getPositionAtEnd()); // hello``world
|
||||||
|
expect(range.parts[0].text.includes("`")).toBeTruthy();
|
||||||
|
expect(longestBacktickSequence(range.parts[0].text)).toBe(3);
|
||||||
|
expect(model.serializeParts()).toEqual([{ "text": "```hello``world```", "type": "plain" }]);
|
||||||
|
formatRangeAsCode(range);
|
||||||
|
expect(model.serializeParts()).toEqual([{ "text": "hello``world", "type": "plain" }]);
|
||||||
|
});
|
||||||
|
it('untoggles correctly it contains varying length of backticks between text', () => {
|
||||||
|
const renderer = createRenderer();
|
||||||
|
const pc = createPartCreator();
|
||||||
|
const model = new EditorModel([
|
||||||
|
pc.plain("````hell```o`w`o``rld````"),
|
||||||
|
], pc, renderer);
|
||||||
|
|
||||||
|
const range = model.startRange(model.positionForOffset(0, false),
|
||||||
|
model.getPositionAtEnd()); // hell```o`w`o``rld
|
||||||
|
expect(range.parts[0].text.includes("`")).toBeTruthy();
|
||||||
|
expect(longestBacktickSequence(range.parts[0].text)).toBe(4);
|
||||||
|
expect(model.serializeParts()).toEqual([{ "text": "````hell```o`w`o``rld````", "type": "plain" }]);
|
||||||
|
formatRangeAsCode(range);
|
||||||
|
expect(model.serializeParts()).toEqual([{ "text": "hell```o`w`o``rld", "type": "plain" }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('works for parts of words', () => {
|
it('works for parts of words', () => {
|
||||||
const renderer = createRenderer();
|
const renderer = createRenderer();
|
||||||
const pc = createPartCreator();
|
const pc = createPartCreator();
|
||||||
|
|
Loading…
Reference in a new issue