From 1bfd6e20b856c30915e0333d4a4fa6144ef9aacc Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 11 Jan 2025 23:30:29 +0530 Subject: [PATCH 1/2] Stabalized the output of splitRangesOnEmojis --- src/__tests__/splitRangesOnEmojis.test.ts | 36 +++++++--- src/parseExpensiMark.ts | 6 +- src/rangeUtils.ts | 86 +++++++++++++++++++++-- 3 files changed, 111 insertions(+), 17 deletions(-) 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; } From 73386b3650fca39393888fabecc998537f32aa0b Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 11 Jan 2025 23:53:43 +0530 Subject: [PATCH 2/2] Fixed another edge case --- src/__tests__/splitRangesOnEmojis.test.ts | 30 +++++++++++++++++++++++ src/rangeUtils.ts | 6 +++++ 2 files changed, 36 insertions(+) diff --git a/src/__tests__/splitRangesOnEmojis.test.ts b/src/__tests__/splitRangesOnEmojis.test.ts index 452dcc0a..7d743fef 100644 --- a/src/__tests__/splitRangesOnEmojis.test.ts +++ b/src/__tests__/splitRangesOnEmojis.test.ts @@ -152,6 +152,36 @@ describe('multiple overlaps', () => { ]); }); + test('splitting on one type back to back', () => { + let markdownRanges: MarkdownRange[] = [ + {type: 'italic', start: 0, length: 20}, + {type: 'strikethrough', start: 2, length: 12}, + {type: 'emoji', start: 3, length: 1}, + {type: 'emoji', start: 8, length: 2}, + {type: 'strikethrough', start: 16, length: 5}, + {type: 'emoji', start: 17, length: 1}, + {type: 'emoji', start: 19, length: 1}, + {type: 'strikethrough', start: 22, length: 5}, + ]; + + markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough'); + + expect(markdownRanges).toEqual([ + {type: 'italic', start: 0, length: 20}, + {type: 'strikethrough', start: 2, length: 1}, + {type: 'emoji', start: 3, length: 1}, + {type: 'strikethrough', start: 4, length: 4}, + {type: 'emoji', start: 8, length: 2}, + {type: 'strikethrough', start: 10, length: 4}, + {type: 'strikethrough', start: 16, length: 1}, + {type: 'emoji', start: 17, length: 1}, + {type: 'strikethrough', start: 18, length: 1}, + {type: 'emoji', start: 19, length: 1}, + {type: 'strikethrough', start: 20, length: 1}, + {type: 'strikethrough', start: 22, length: 5}, + ]); + }); + test('splitting on two types', () => { let markdownRanges: MarkdownRange[] = [ {type: 'italic', start: 0, length: 20}, diff --git a/src/rangeUtils.ts b/src/rangeUtils.ts index 1615d6f6..aa6b0978 100644 --- a/src/rangeUtils.ts +++ b/src/rangeUtils.ts @@ -154,6 +154,12 @@ function splitRangesOnEmojis(ranges: MarkdownRange[], type: MarkdownType): Markd if (newRange.length > 0) { if (firstTimeEntry) { + while (!queue.isEmpty() && compareRanges(newRange, queue.peek()) >= 0) { + const dequeuedRange = queue.dequeue(); + if (dequeuedRange) { + newRanges.push(dequeuedRange); + } + } newRanges.push(newRange); } else { queue.enqueue(newRange);