diff --git a/src/__tests__/parseExpensiMark.test.ts b/src/__tests__/parseExpensiMark.test.ts index 868cf42a..3c689a8d 100644 --- a/src/__tests__/parseExpensiMark.test.ts +++ b/src/__tests__/parseExpensiMark.test.ts @@ -222,9 +222,9 @@ test('email with multiline hyperlinks', () => { test('inline code', () => { expect('Hello `world`!').toBeParsedAs([ - {type: 'syntax', start: 6, length: 1}, + {type: 'syntax', start: 6, length: 1, syntaxType: 'opening'}, {type: 'code', start: 7, length: 5}, - {type: 'syntax', start: 12, length: 1}, + {type: 'syntax', start: 12, length: 1, syntaxType: 'closing'}, ]); }); @@ -585,9 +585,9 @@ describe('report mentions', () => { test('report mention with markdown', () => { expect('reported #`report-name` should be highlighted').toBeParsedAs([ - {type: 'syntax', start: 10, length: 1}, + {type: 'syntax', start: 10, length: 1, syntaxType: 'opening'}, {type: 'code', start: 11, length: 11}, - {type: 'syntax', start: 22, length: 1}, + {type: 'syntax', start: 22, length: 1, syntaxType: 'closing'}, ]); }); @@ -678,3 +678,91 @@ describe('inline video', () => { ]); }); }); + +describe('nested inline code', () => { + test('starting with emoji', () => { + expect('_~`๐Ÿš€test`~_').toBeParsedAs([ + {type: 'syntax', start: 0, length: 1}, + {type: 'italic', start: 1, length: 2}, + {type: 'italic', start: 5, length: 4}, + {type: 'italic', start: 9, length: 2}, + {type: 'syntax', start: 1, length: 1}, + {type: 'strikethrough', start: 2, length: 1}, + {type: 'strikethrough', start: 5, length: 4}, + {type: 'strikethrough', start: 9, length: 1}, + {type: 'syntax', start: 2, length: 1, syntaxType: 'opening'}, + {type: 'code', start: 3, length: 6}, + {type: 'emoji', start: 3, length: 2}, + {type: 'syntax', start: 9, length: 1, syntaxType: 'closing'}, + {type: 'syntax', start: 10, length: 1}, + {type: 'syntax', start: 11, length: 1}, + ]); + }); + + test('emoji in the middle', () => { + expect('_~`te๐Ÿš€st`~_').toBeParsedAs([ + {type: 'syntax', start: 0, length: 1}, + {type: 'italic', start: 1, length: 2}, + {type: 'italic', start: 3, length: 2}, + {type: 'italic', start: 7, length: 2}, + {type: 'italic', start: 9, length: 2}, + {type: 'syntax', start: 1, length: 1}, + {type: 'strikethrough', start: 2, length: 1}, + {type: 'strikethrough', start: 3, length: 2}, + {type: 'strikethrough', start: 7, length: 2}, + {type: 'strikethrough', start: 9, length: 1}, + {type: 'syntax', start: 2, length: 1, syntaxType: 'opening'}, + {type: 'code', start: 3, length: 6}, + {type: 'emoji', start: 5, length: 2}, + {type: 'syntax', start: 9, length: 1, syntaxType: 'closing'}, + {type: 'syntax', start: 10, length: 1}, + {type: 'syntax', start: 11, length: 1}, + ]); + }); + + test('ending with emoji', () => { + expect('_~`test๐Ÿš€`~_').toBeParsedAs([ + {type: 'syntax', start: 0, length: 1}, + {type: 'italic', start: 1, length: 2}, + {type: 'italic', start: 3, length: 4}, + {type: 'italic', start: 9, length: 2}, + {type: 'syntax', start: 1, length: 1}, + {type: 'strikethrough', start: 2, length: 1}, + {type: 'strikethrough', start: 3, length: 4}, + {type: 'strikethrough', start: 9, length: 1}, + {type: 'syntax', start: 2, length: 1, syntaxType: 'opening'}, + {type: 'code', start: 3, length: 6}, + {type: 'emoji', start: 7, length: 2}, + {type: 'syntax', start: 9, length: 1, syntaxType: 'closing'}, + {type: 'syntax', start: 10, length: 1}, + {type: 'syntax', start: 11, length: 1}, + ]); + }); + + test('emoji inside and outside inline code', () => { + expect('_~๐Ÿš€`๐Ÿš€te๐Ÿš€st๐Ÿš€`๐Ÿš€~_').toBeParsedAs([ + {type: 'syntax', start: 0, length: 1}, + {type: 'italic', start: 1, length: 1}, + {type: 'italic', start: 4, length: 1}, + {type: 'italic', start: 7, length: 2}, + {type: 'italic', start: 11, length: 2}, + {type: 'italic', start: 15, length: 1}, + {type: 'italic', start: 18, length: 1}, + {type: 'syntax', start: 1, length: 1}, + {type: 'strikethrough', start: 4, length: 1}, + {type: 'strikethrough', start: 7, length: 2}, + {type: 'strikethrough', start: 11, length: 2}, + {type: 'strikethrough', start: 15, length: 1}, + {type: 'emoji', start: 2, length: 2}, + {type: 'syntax', start: 4, length: 1, syntaxType: 'opening'}, + {type: 'code', start: 5, length: 10}, + {type: 'emoji', start: 5, length: 2}, + {type: 'emoji', start: 9, length: 2}, + {type: 'emoji', start: 13, length: 2}, + {type: 'syntax', start: 15, length: 1, syntaxType: 'closing'}, + {type: 'emoji', start: 16, length: 2}, + {type: 'syntax', start: 18, length: 1}, + {type: 'syntax', start: 19, length: 1}, + ]); + }); +}); diff --git a/src/commonTypes.ts b/src/commonTypes.ts index db74ea38..6e08c471 100644 --- a/src/commonTypes.ts +++ b/src/commonTypes.ts @@ -21,6 +21,7 @@ interface MarkdownRange { start: number; length: number; depth?: number; + syntaxType?: 'opening' | 'closing'; } type InlineImagesInputProps = { diff --git a/src/rangeUtils.ts b/src/rangeUtils.ts index 2297071e..27997487 100644 --- a/src/rangeUtils.ts +++ b/src/rangeUtils.ts @@ -56,20 +56,22 @@ function ungroupRanges(ranges: MarkdownRange[]): MarkdownRange[] { }); return ungroupedRanges; } - /** * Creates a list of ranges that should not be formatted by certain markdown types (italic, strikethrough). * This includes emojis and syntaxes of inline code blocks. */ -function getRangesToExcludeFormatting(ranges: MarkdownRange[]) { +function getRangesToExcludeFormatting(ranges: MarkdownRange[]): MarkdownRange[] { let closingSyntaxPosition: number | null = null; return ranges.filter((range, index) => { const nextRange = ranges[index + 1]; + const currentRange = range; if (nextRange && nextRange.type === 'code' && range.type === 'syntax') { + currentRange.syntaxType = 'opening'; closingSyntaxPosition = nextRange.start + nextRange.length; return true; } if (closingSyntaxPosition !== null && range.type === 'syntax' && range.start <= closingSyntaxPosition) { + currentRange.syntaxType = 'closing'; closingSyntaxPosition = null; return true; } @@ -115,11 +117,11 @@ function excludeRangeTypesFromFormatting(ranges: MarkdownRange[], baseMarkdownTy const newRange: MarkdownRange = { type: currentRange.type, start: currentStart, - length: excludeRangeStart - currentStart, + length: excludeRangeStart - currentStart + (excludeRange.syntaxType === 'opening' ? 1 : 0), // Adjust the length so the new range from the split ends after the opening syntax ...(currentRange?.depth && {depth: currentRange?.depth}), }; - currentRange.start = excludeRangeEnd; - currentRange.length = currentEnd - excludeRangeEnd; + currentRange.start = excludeRangeEnd + (excludeRange.syntaxType === 'closing' ? -1 : 0); // Adjust the current range to start before the closing syntax + currentRange.length = currentEnd - excludeRangeEnd + (excludeRange.syntaxType === 'closing' ? 1 : 0); if (newRange.length > 0) { newRanges.push(newRange);