From 30eed0c12126c6cf8b15a0cb9a570b3a805d3bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Mon, 6 Oct 2025 11:32:59 +0200 Subject: [PATCH 1/3] Fix applying styles for inline code syntaxes --- src/rangeUtils.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/rangeUtils.ts b/src/rangeUtils.ts index 2297071e..028db0dc 100644 --- a/src/rangeUtils.ts +++ b/src/rangeUtils.ts @@ -2,6 +2,10 @@ import type {MarkdownRange, MarkdownType} from './commonTypes'; +type ExtendedMarkdownRange = MarkdownRange & { + syntaxType?: 'opening' | 'closing'; +}; + // getTagPriority returns a priority for a tag, higher priority means the tag should be processed first function getTagPriority(tag: string) { switch (tag) { @@ -56,20 +60,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[]): ExtendedMarkdownRange[] { let closingSyntaxPosition: number | null = null; return ranges.filter((range, index) => { const nextRange = ranges[index + 1]; + const currentRange = range as ExtendedMarkdownRange; 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; } @@ -83,7 +89,7 @@ function getRangesToExcludeFormatting(ranges: MarkdownRange[]) { * @param baseMarkdownType - The base markdown type to exclude formatting from (e.g., 'italic'). * @param rangesToExclude - The array of MarkdownRange objects representing the ranges to exclude from formatting. */ -function excludeRangeTypesFromFormatting(ranges: MarkdownRange[], baseMarkdownType: MarkdownType, rangesToExclude: MarkdownRange[]): MarkdownRange[] { +function excludeRangeTypesFromFormatting(ranges: MarkdownRange[], baseMarkdownType: MarkdownType, rangesToExclude: ExtendedMarkdownRange[]): MarkdownRange[] { const newRanges: MarkdownRange[] = []; let i = 0; @@ -115,11 +121,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 length so opening syntax 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 current range to start before closing syntax + currentRange.length = currentEnd - excludeRangeEnd + (excludeRange.syntaxType === 'closing' ? 1 : 0); if (newRange.length > 0) { newRanges.push(newRange); @@ -138,3 +144,4 @@ function excludeRangeTypesFromFormatting(ranges: MarkdownRange[], baseMarkdownTy } export {sortRanges, groupRanges, ungroupRanges, excludeRangeTypesFromFormatting, getRangesToExcludeFormatting}; +export type {ExtendedMarkdownRange}; From d66a8d91ccfac5e3c7c6a5a07d21c4818ab0accc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Mon, 6 Oct 2025 11:34:04 +0200 Subject: [PATCH 2/3] Add nested code parsing tests --- src/__tests__/parseExpensiMark.test.ts | 99 ++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 5 deletions(-) diff --git a/src/__tests__/parseExpensiMark.test.ts b/src/__tests__/parseExpensiMark.test.ts index 868cf42a..49aa399a 100644 --- a/src/__tests__/parseExpensiMark.test.ts +++ b/src/__tests__/parseExpensiMark.test.ts @@ -1,10 +1,11 @@ import {expect} from '@jest/globals'; import type {MarkdownRange} from '../commonTypes'; import parseExpensiMark from '../parseExpensiMark'; +import type {ExtendedMarkdownRange} from '../rangeUtils'; declare module 'expect' { interface Matchers { - toBeParsedAs(expectedRanges: MarkdownRange[]): R; + toBeParsedAs(expectedRanges: ExtendedMarkdownRange[]): R; } } @@ -222,9 +223,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 +586,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 +679,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}, + ]); + }); +}); From 0169cdcbcb4c9fddea7b6254aa5b27e7308589a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Mon, 6 Oct 2025 17:03:57 +0200 Subject: [PATCH 3/3] Add review changes --- src/__tests__/parseExpensiMark.test.ts | 3 +-- src/commonTypes.ts | 1 + src/rangeUtils.ts | 15 +++++---------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/__tests__/parseExpensiMark.test.ts b/src/__tests__/parseExpensiMark.test.ts index 49aa399a..3c689a8d 100644 --- a/src/__tests__/parseExpensiMark.test.ts +++ b/src/__tests__/parseExpensiMark.test.ts @@ -1,11 +1,10 @@ import {expect} from '@jest/globals'; import type {MarkdownRange} from '../commonTypes'; import parseExpensiMark from '../parseExpensiMark'; -import type {ExtendedMarkdownRange} from '../rangeUtils'; declare module 'expect' { interface Matchers { - toBeParsedAs(expectedRanges: ExtendedMarkdownRange[]): R; + toBeParsedAs(expectedRanges: MarkdownRange[]): R; } } 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 028db0dc..27997487 100644 --- a/src/rangeUtils.ts +++ b/src/rangeUtils.ts @@ -2,10 +2,6 @@ import type {MarkdownRange, MarkdownType} from './commonTypes'; -type ExtendedMarkdownRange = MarkdownRange & { - syntaxType?: 'opening' | 'closing'; -}; - // getTagPriority returns a priority for a tag, higher priority means the tag should be processed first function getTagPriority(tag: string) { switch (tag) { @@ -64,11 +60,11 @@ function ungroupRanges(ranges: MarkdownRange[]): MarkdownRange[] { * 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[]): ExtendedMarkdownRange[] { +function getRangesToExcludeFormatting(ranges: MarkdownRange[]): MarkdownRange[] { let closingSyntaxPosition: number | null = null; return ranges.filter((range, index) => { const nextRange = ranges[index + 1]; - const currentRange = range as ExtendedMarkdownRange; + const currentRange = range; if (nextRange && nextRange.type === 'code' && range.type === 'syntax') { currentRange.syntaxType = 'opening'; closingSyntaxPosition = nextRange.start + nextRange.length; @@ -89,7 +85,7 @@ function getRangesToExcludeFormatting(ranges: MarkdownRange[]): ExtendedMarkdown * @param baseMarkdownType - The base markdown type to exclude formatting from (e.g., 'italic'). * @param rangesToExclude - The array of MarkdownRange objects representing the ranges to exclude from formatting. */ -function excludeRangeTypesFromFormatting(ranges: MarkdownRange[], baseMarkdownType: MarkdownType, rangesToExclude: ExtendedMarkdownRange[]): MarkdownRange[] { +function excludeRangeTypesFromFormatting(ranges: MarkdownRange[], baseMarkdownType: MarkdownType, rangesToExclude: MarkdownRange[]): MarkdownRange[] { const newRanges: MarkdownRange[] = []; let i = 0; @@ -121,10 +117,10 @@ function excludeRangeTypesFromFormatting(ranges: MarkdownRange[], baseMarkdownTy const newRange: MarkdownRange = { type: currentRange.type, start: currentStart, - length: excludeRangeStart - currentStart + (excludeRange.syntaxType === 'opening' ? 1 : 0), // Adjust length so opening syntax ends after the opening syntax + 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 + (excludeRange.syntaxType === 'closing' ? -1 : 0); // Adjust current range to start before closing syntax + 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) { @@ -144,4 +140,3 @@ function excludeRangeTypesFromFormatting(ranges: MarkdownRange[], baseMarkdownTy } export {sortRanges, groupRanges, ungroupRanges, excludeRangeTypesFromFormatting, getRangesToExcludeFormatting}; -export type {ExtendedMarkdownRange};