Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Fix: "Code formatting button does not escape backticks" #8181

Merged
merged 8 commits into from
Apr 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/editor/deserialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function escape(text: string): string {

// Finds the length of the longest backtick sequence in the given text, used for
// escaping backticks in code blocks
function longestBacktickSequence(text: string): number {
export function longestBacktickSequence(text: string): number {
let length = 0;
let currentLength = 0;

Expand Down
11 changes: 8 additions & 3 deletions src/editor/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
import Range from "./range";
import { Part, Type } from "./parts";
import { Formatting } from "../components/views/rooms/MessageComposerFormatBar";
import { longestBacktickSequence } from './deserialize';

/**
* Some common queries and transformations on the editor model
Expand Down Expand Up @@ -181,12 +182,12 @@ export function formatRangeAsCode(range: Range): void {

const hasBlockFormatting = (range.length > 0)
&& range.text.startsWith("```")
&& range.text.endsWith("```");
&& range.text.endsWith("```")
&& range.text.includes('\n');

const needsBlockFormatting = parts.some(p => p.type === Type.Newline);

if (hasBlockFormatting) {
// Remove previously pushed backticks and new lines
parts.shift();
parts.pop();
if (parts[0]?.text === "\n" && parts[parts.length - 1]?.text === "\n") {
Expand All @@ -205,7 +206,10 @@ export function formatRangeAsCode(range: Range): void {
parts.push(partCreator.newline());
}
} 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));
t3chguy marked this conversation as resolved.
Show resolved Hide resolved
return;
}

Expand Down Expand Up @@ -240,6 +244,7 @@ export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix
// compute paragraph [start, end] indexes
const paragraphIndexes = [];
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
for (let i = 2; i < parts.length; i++) {
// paragraph breaks can be denoted in a multitude of ways,
Expand Down
85 changes: 85 additions & 0 deletions test/editor/operations-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import {
toggleInlineFormat,
selectRangeOfWordAtCaret,
formatRange,
formatRangeAsCode,
} from "../../src/editor/operations";
import { Formatting } from "../../src/components/views/rooms/MessageComposerFormatBar";
import { longestBacktickSequence } from '../../src/editor/deserialize';

const SERIALIZED_NEWLINE = { "text": "\n", "type": "newline" };

Expand All @@ -43,6 +45,89 @@ describe('editor/operations: formatting operations', () => {
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', () => {
const renderer = createRenderer();
const pc = createPartCreator();
Expand Down