Skip to content

Commit 69fcd72

Browse files
authored
fix(core): smarter underscore and exclamation mark escaping when serializing to markdown (#1045)
1 parent 65b57bc commit 69fcd72

2 files changed

Lines changed: 35 additions & 2 deletions

File tree

packages/editor/src/core/markdown/Markdown.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {Parser} from '../types/parser';
1111
import type {SerializerNodeToken} from '../types/serializer';
1212

1313
import {MarkdownParser} from './MarkdownParser';
14-
import {MarkdownSerializer} from './MarkdownSerializer';
14+
import {MarkdownSerializer, MarkdownSerializerState} from './MarkdownSerializer';
1515

1616
const {schema} = builder;
1717
schema.nodes['hard_break'].spec.isBreak = true;
@@ -214,4 +214,28 @@ describe('markdown', () => {
214214
),
215215
);
216216
});
217+
218+
it('escapes exclamation mark before image syntax', () => {
219+
same('hello !\\[alt\\](path/to/image)', doc(p('hello ![alt](path/to/image)')));
220+
});
221+
222+
it('escapes exclamation mark before non-escaped bracket in text()', () => {
223+
// Directly tests the fix: when text() is called with escape=false
224+
// and content starts with [, a preceding ! must become \!
225+
const state = new MarkdownSerializerState({}, {}, {});
226+
state.out = 'hello!';
227+
state.text('[link](url)', false);
228+
expect(state.out).toBe('hello\\![link](url)');
229+
});
230+
231+
it('does not escape underscore between word characters', () => {
232+
same('foo_bar', doc(p('foo_bar')));
233+
same('a_b_c', doc(p('a_b_c')));
234+
});
235+
236+
it('escapes underscore at word boundaries', () => {
237+
same('\\_leading', doc(p('_leading')));
238+
same('trailing\\_', doc(p('trailing_')));
239+
same('space \\_ space', doc(p('space _ space')));
240+
});
217241
});

packages/editor/src/core/markdown/MarkdownSerializer.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ export class MarkdownSerializerState {
228228
for (let i = 0; i < lines.length; i++) {
229229
const startOfLine = this.atBlank() || this.closed;
230230
this.write();
231+
// Escape ! before [ to prevent being parsed as image syntax
232+
if (escape === false && lines[i][0] === '[' && /(^|[^\\])!$/.test(this.out))
233+
this.out = this.out.slice(0, this.out.length - 1) + '\\!';
231234
let text = lines[i];
232235
if (escape !== false && this.options.escape !== false) text = this.esc(text, startOfLine as any)
233236
if (this.escapeWhitespace) text = this.escWhitespace(text);
@@ -380,7 +383,7 @@ export class MarkdownSerializerState {
380383
// have special meaning only at the start of the line.
381384
esc(str: string, startOfLine = false) {
382385
// eslint-disable-next-line no-useless-escape
383-
const defaultEsc = /[`\^+*\\\|~\[\]\{\}<>\$_]/g;
386+
const defaultEsc = /[`\^+*\\\|~\[\]\{\}<>\$]/g;
384387
const extraChars = this.escapeCharacters?.length ? this.escapeCharacters.map(c => '\\' + c).join('') : '';
385388
const escRegexp = this.options?.commonEscape ||
386389
// Compose the escape regexp from default, options, and extra characters
@@ -389,6 +392,12 @@ export class MarkdownSerializerState {
389392
const startOfLineEscRegexp = this.options?.startOfLineEscape || /^[:#\-*+>]/;
390393

391394
str = str.replace(escRegexp, '\\$&');
395+
// Smart underscore: don't escape _ between word characters (e.g. foo_bar)
396+
str = str.replace(/_/g, (m, i) =>
397+
i > 0 && i + 1 < str.length && /\w/.test(str[i - 1]) && /\w/.test(str[i + 1])
398+
? m
399+
: '\\' + m
400+
);
392401
if (startOfLine) str = str.replace(startOfLineEscRegexp, '\\$&').replace(/^(\s*\d+)\./, '$1\\.');
393402
return str;
394403
}

0 commit comments

Comments
 (0)