diff --git a/src/__tests__/splitRangesOnEmojis.test.ts b/src/__tests__/splitRangesOnEmojis.test.ts index a9eb11c3..452dcc0a 100644 --- a/src/__tests__/splitRangesOnEmojis.test.ts +++ b/src/__tests__/splitRangesOnEmojis.test.ts @@ -1,10 +1,6 @@ import type {MarkdownRange} from '../commonTypes'; import {splitRangesOnEmojis} from '../rangeUtils'; -const sortRanges = (ranges: MarkdownRange[]) => { - return ranges.sort((a, b) => a.start - b.start); -}; - test('no overlap', () => { const markdownRanges: MarkdownRange[] = [ {type: 'strikethrough', start: 0, length: 10}, @@ -28,6 +24,32 @@ test('overlap different type', () => { expect(splittedRanges).toEqual(markdownRanges); }); +test('overlap with bold and emoji type', () => { + const markdownRanges: MarkdownRange[] = [ + {type: 'syntax', start: 0, length: 1}, + {type: 'italic', start: 1, length: 4}, + {type: 'syntax', start: 1, length: 1}, + {type: 'bold', start: 2, length: 2}, + {type: 'emoji', start: 2, length: 2}, + {type: 'syntax', start: 4, length: 1}, + {type: 'syntax', start: 5, length: 1}, + ]; + + const expectedResult = [ + {type: 'syntax', start: 0, length: 1}, + {type: 'italic', start: 1, length: 1}, + {type: 'syntax', start: 1, length: 1}, + {type: 'bold', start: 2, length: 2}, + {type: 'emoji', start: 2, length: 2}, + {type: 'italic', start: 4, length: 1}, + {type: 'syntax', start: 4, length: 1}, + {type: 'syntax', start: 5, length: 1}, + ]; + + const splittedRanges = splitRangesOnEmojis(markdownRanges, 'italic'); + expect(splittedRanges).toEqual(expectedResult); +}); + describe('single overlap', () => { test('emoji at the beginning', () => { let markdownRanges: MarkdownRange[] = [ @@ -36,7 +58,6 @@ describe('single overlap', () => { ]; markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); - sortRanges(markdownRanges); expect(markdownRanges).toEqual([ {type: 'emoji', start: 0, length: 2}, @@ -51,7 +72,6 @@ describe('single overlap', () => { ]; markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); - sortRanges(markdownRanges); expect(markdownRanges).toEqual([ {type: 'strikethrough', start: 0, length: 3}, @@ -67,7 +87,6 @@ describe('single overlap', () => { ]; markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); - sortRanges(markdownRanges); expect(markdownRanges).toEqual([ {type: 'strikethrough', start: 0, length: 8}, @@ -83,7 +102,6 @@ describe('single overlap', () => { ]; markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); - sortRanges(markdownRanges); expect(markdownRanges).toEqual([ {type: 'strikethrough', start: 0, length: 3}, @@ -122,7 +140,6 @@ describe('multiple overlaps', () => { ]; markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); - sortRanges(markdownRanges); expect(markdownRanges).toEqual([ {type: 'italic', start: 0, length: 20}, @@ -146,7 +163,6 @@ describe('multiple overlaps', () => { markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); markdownRanges = splitRangesOnEmojis(markdownRanges, 'italic'); - sortRanges(markdownRanges); expect(markdownRanges).toEqual([ {type: 'italic', start: 0, length: 3}, diff --git a/src/parseExpensiMark.ts b/src/parseExpensiMark.ts index 37216d11..0025227d 100644 --- a/src/parseExpensiMark.ts +++ b/src/parseExpensiMark.ts @@ -253,11 +253,11 @@ function parseExpensiMark(markdown: string): MarkdownRange[] { return []; } - let splittedRanges = splitRangesOnEmojis(ranges, 'italic'); + let splittedRanges = sortRanges(ranges); + splittedRanges = splitRangesOnEmojis(ranges, 'italic'); splittedRanges = splitRangesOnEmojis(splittedRanges, 'strikethrough'); - const sortedRanges = sortRanges(splittedRanges); - const groupedRanges = groupRanges(sortedRanges); + const groupedRanges = groupRanges(splittedRanges); return groupedRanges; } diff --git a/src/rangeUtils.ts b/src/rangeUtils.ts index f47c4aaa..1615d6f6 100644 --- a/src/rangeUtils.ts +++ b/src/rangeUtils.ts @@ -2,6 +2,44 @@ import type {MarkdownRange, MarkdownType} from './commonTypes'; +class MarkdownRangeQueue { + items: Record; + + frontIndex: number; + + backIndex: number; + + constructor() { + this.items = {}; + this.frontIndex = 0; + this.backIndex = 0; + } + + enqueue(item: MarkdownRange) { + this.items[this.backIndex] = item; + this.backIndex += 1; + } + + dequeue() { + const item = this.items[this.frontIndex]; + delete this.items[this.frontIndex]; + this.frontIndex += 1; + return item; + } + + peek() { + return this.items[this.frontIndex]; + } + + isEmpty() { + return this.frontIndex === this.backIndex; + } + + get printQueue() { + return this.items; + } +} + // getTagPriority returns a priority for a tag, higher priority means the tag should be processed first function getTagPriority(tag: string) { switch (tag) { @@ -55,9 +93,20 @@ function ungroupRanges(ranges: MarkdownRange[]): MarkdownRange[] { return ungroupedRanges; } +function compareRanges(a: MarkdownRange | undefined, b: MarkdownRange | undefined) { + if (!a) { + return -1; + } + if (!b) { + return 1; + } + return a.start - b.start || b.length - a.length || getTagPriority(b.type) - getTagPriority(a.type) || 0; +} + function splitRangesOnEmojis(ranges: MarkdownRange[], type: MarkdownType): MarkdownRange[] { const emojiRanges: MarkdownRange[] = ranges.filter((range) => range.type === 'emoji'); const newRanges: MarkdownRange[] = []; + const queue = new MarkdownRangeQueue(); let i = 0; let j = 0; @@ -68,9 +117,17 @@ function splitRangesOnEmojis(ranges: MarkdownRange[], type: MarkdownType): Markd } if (currentRange.type !== type) { - newRanges.push(currentRange); - i++; + if (queue.isEmpty() || compareRanges(currentRange, queue.peek()) < 0) { + newRanges.push(currentRange); + i++; + } else { + const newRange = queue.dequeue(); + if (newRange) { + newRanges.push(newRange); + } + } } else { + let firstTimeEntry = true; // Iterate through all emoji ranges before the end of the current range, splitting the current range at each intersection. while (j < emojiRanges.length) { const emojiRange = emojiRanges[j]; @@ -96,18 +153,39 @@ function splitRangesOnEmojis(ranges: MarkdownRange[], type: MarkdownType): Markd currentRange.length = currentEnd - emojiEnd; if (newRange.length > 0) { - newRanges.push(newRange); + if (firstTimeEntry) { + newRanges.push(newRange); + } else { + queue.enqueue(newRange); + } } + firstTimeEntry = false; } j++; } if (currentRange.length > 0) { - newRanges.push(currentRange); + if (firstTimeEntry) { + while (!queue.isEmpty() && compareRanges(currentRange, queue.peek()) >= 0) { + const newRange = queue.dequeue(); + if (newRange) { + newRanges.push(newRange); + } + } + newRanges.push(currentRange); + } else { + queue.enqueue(currentRange); + } } i++; } } + while (!queue.isEmpty()) { + const newRange = queue.dequeue(); + if (newRange) { + newRanges.push(newRange); + } + } return newRanges; }