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

Commit 6b13988

Browse files
yaya-usmant3chguy
andauthored
Fix: "Code formatting button does not escape backticks" (#8181)
Co-authored-by: Michael Telatynski <[email protected]>
1 parent 949b3cc commit 6b13988

File tree

3 files changed

+94
-4
lines changed

3 files changed

+94
-4
lines changed

src/editor/deserialize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function escape(text: string): string {
3232

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

src/editor/operations.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
import Range from "./range";
1818
import { Part, Type } from "./parts";
1919
import { Formatting } from "../components/views/rooms/MessageComposerFormatBar";
20+
import { longestBacktickSequence } from './deserialize';
2021

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

182183
const hasBlockFormatting = (range.length > 0)
183184
&& range.text.startsWith("```")
184-
&& range.text.endsWith("```");
185+
&& range.text.endsWith("```")
186+
&& range.text.includes('\n');
185187

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

188190
if (hasBlockFormatting) {
189-
// Remove previously pushed backticks and new lines
190191
parts.shift();
191192
parts.pop();
192193
if (parts[0]?.text === "\n" && parts[parts.length - 1]?.text === "\n") {
@@ -205,7 +206,10 @@ export function formatRangeAsCode(range: Range): void {
205206
parts.push(partCreator.newline());
206207
}
207208
} else {
208-
toggleInlineFormat(range, "`");
209+
const fenceLen = longestBacktickSequence(range.text);
210+
const hasInlineFormatting = range.text.startsWith("`") && range.text.endsWith("`");
211+
//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.
212+
toggleInlineFormat(range, "`".repeat(hasInlineFormatting ? fenceLen : fenceLen + 1));
209213
return;
210214
}
211215

@@ -240,6 +244,7 @@ export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix
240244
// compute paragraph [start, end] indexes
241245
const paragraphIndexes = [];
242246
let startIndex = 0;
247+
243248
// start at i=2 because we look at i and up to two parts behind to detect paragraph breaks at their end
244249
for (let i = 2; i < parts.length; i++) {
245250
// paragraph breaks can be denoted in a multitude of ways,

test/editor/operations-test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import {
2020
toggleInlineFormat,
2121
selectRangeOfWordAtCaret,
2222
formatRange,
23+
formatRangeAsCode,
2324
} from "../../src/editor/operations";
2425
import { Formatting } from "../../src/components/views/rooms/MessageComposerFormatBar";
26+
import { longestBacktickSequence } from '../../src/editor/deserialize';
2527

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

@@ -43,6 +45,89 @@ describe('editor/operations: formatting operations', () => {
4345
expect(model.serializeParts()).toEqual([{ "text": "hello _world_!", "type": "plain" }]);
4446
});
4547

48+
describe('escape backticks', () => {
49+
it('works for escaping backticks in between texts', () => {
50+
const renderer = createRenderer();
51+
const pc = createPartCreator();
52+
const model = new EditorModel([
53+
pc.plain("hello ` world!"),
54+
], pc, renderer);
55+
56+
const range = model.startRange(model.positionForOffset(0, false),
57+
model.positionForOffset(13, false)); // hello ` world
58+
59+
expect(range.parts[0].text.trim().includes("`")).toBeTruthy();
60+
expect(longestBacktickSequence(range.parts[0].text.trim())).toBe(1);
61+
expect(model.serializeParts()).toEqual([{ "text": "hello ` world!", "type": "plain" }]);
62+
formatRangeAsCode(range);
63+
expect(model.serializeParts()).toEqual([{ "text": "``hello ` world``!", "type": "plain" }]);
64+
});
65+
66+
it('escapes longer backticks in between text', () => {
67+
const renderer = createRenderer();
68+
const pc = createPartCreator();
69+
const model = new EditorModel([
70+
pc.plain("hello```world"),
71+
], pc, renderer);
72+
73+
const range = model.startRange(model.positionForOffset(0, false),
74+
model.getPositionAtEnd()); // hello```world
75+
76+
expect(range.parts[0].text.includes("`")).toBeTruthy();
77+
expect(longestBacktickSequence(range.parts[0].text)).toBe(3);
78+
expect(model.serializeParts()).toEqual([{ "text": "hello```world", "type": "plain" }]);
79+
formatRangeAsCode(range);
80+
expect(model.serializeParts()).toEqual([{ "text": "````hello```world````", "type": "plain" }]);
81+
});
82+
83+
it('escapes non-consecutive with varying length backticks in between text', () => {
84+
const renderer = createRenderer();
85+
const pc = createPartCreator();
86+
const model = new EditorModel([
87+
pc.plain("hell```o`w`o``rld"),
88+
], pc, renderer);
89+
90+
const range = model.startRange(model.positionForOffset(0, false),
91+
model.getPositionAtEnd()); // hell```o`w`o``rld
92+
expect(range.parts[0].text.includes("`")).toBeTruthy();
93+
expect(longestBacktickSequence(range.parts[0].text)).toBe(3);
94+
expect(model.serializeParts()).toEqual([{ "text": "hell```o`w`o``rld", "type": "plain" }]);
95+
formatRangeAsCode(range);
96+
expect(model.serializeParts()).toEqual([{ "text": "````hell```o`w`o``rld````", "type": "plain" }]);
97+
});
98+
99+
it('untoggles correctly if its already formatted', () => {
100+
const renderer = createRenderer();
101+
const pc = createPartCreator();
102+
const model = new EditorModel([
103+
pc.plain("```hello``world```"),
104+
], pc, renderer);
105+
106+
const range = model.startRange(model.positionForOffset(0, false),
107+
model.getPositionAtEnd()); // hello``world
108+
expect(range.parts[0].text.includes("`")).toBeTruthy();
109+
expect(longestBacktickSequence(range.parts[0].text)).toBe(3);
110+
expect(model.serializeParts()).toEqual([{ "text": "```hello``world```", "type": "plain" }]);
111+
formatRangeAsCode(range);
112+
expect(model.serializeParts()).toEqual([{ "text": "hello``world", "type": "plain" }]);
113+
});
114+
it('untoggles correctly it contains varying length of backticks between text', () => {
115+
const renderer = createRenderer();
116+
const pc = createPartCreator();
117+
const model = new EditorModel([
118+
pc.plain("````hell```o`w`o``rld````"),
119+
], pc, renderer);
120+
121+
const range = model.startRange(model.positionForOffset(0, false),
122+
model.getPositionAtEnd()); // hell```o`w`o``rld
123+
expect(range.parts[0].text.includes("`")).toBeTruthy();
124+
expect(longestBacktickSequence(range.parts[0].text)).toBe(4);
125+
expect(model.serializeParts()).toEqual([{ "text": "````hell```o`w`o``rld````", "type": "plain" }]);
126+
formatRangeAsCode(range);
127+
expect(model.serializeParts()).toEqual([{ "text": "hell```o`w`o``rld", "type": "plain" }]);
128+
});
129+
});
130+
46131
it('works for parts of words', () => {
47132
const renderer = createRenderer();
48133
const pc = createPartCreator();

0 commit comments

Comments
 (0)