Fix: "Code formatting button does not escape backticks" (#8181)
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>pull/28788/head^2
							parent
							
								
									949b3cc650
								
							
						
					
					
						commit
						6b13988eaa
					
				|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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") { | ||||
|  | @ -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)); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|  | @ -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,
 | ||||
|  |  | |||
|  | @ -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" }; | ||||
| 
 | ||||
|  | @ -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(); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Yaya Usman
						Yaya Usman