Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stabilised the output of splitRangesOnEmojis #597

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 56 additions & 10 deletions src/__tests__/splitRangesOnEmojis.test.ts
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -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[] = [
Expand All @@ -36,7 +58,6 @@ describe('single overlap', () => {
];

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
sortRanges(markdownRanges);

expect(markdownRanges).toEqual([
{type: 'emoji', start: 0, length: 2},
Expand All @@ -51,7 +72,6 @@ describe('single overlap', () => {
];

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
sortRanges(markdownRanges);

expect(markdownRanges).toEqual([
{type: 'strikethrough', start: 0, length: 3},
Expand All @@ -67,7 +87,6 @@ describe('single overlap', () => {
];

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
sortRanges(markdownRanges);

expect(markdownRanges).toEqual([
{type: 'strikethrough', start: 0, length: 8},
Expand All @@ -83,7 +102,6 @@ describe('single overlap', () => {
];

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
sortRanges(markdownRanges);

expect(markdownRanges).toEqual([
{type: 'strikethrough', start: 0, length: 3},
Expand Down Expand Up @@ -122,7 +140,6 @@ describe('multiple overlaps', () => {
];

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
sortRanges(markdownRanges);

expect(markdownRanges).toEqual([
{type: 'italic', start: 0, length: 20},
Expand All @@ -135,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},
Expand All @@ -146,7 +193,6 @@ describe('multiple overlaps', () => {

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
markdownRanges = splitRangesOnEmojis(markdownRanges, 'italic');
sortRanges(markdownRanges);

expect(markdownRanges).toEqual([
{type: 'italic', start: 0, length: 3},
Expand Down
6 changes: 3 additions & 3 deletions src/parseExpensiMark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
92 changes: 88 additions & 4 deletions src/rangeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,44 @@

import type {MarkdownRange, MarkdownType} from './commonTypes';

class MarkdownRangeQueue {
items: Record<number, MarkdownRange>;

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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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];
Expand All @@ -96,18 +153,45 @@ function splitRangesOnEmojis(ranges: MarkdownRange[], type: MarkdownType): Markd
currentRange.length = currentEnd - emojiEnd;

if (newRange.length > 0) {
newRanges.push(newRange);
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);
}
}
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;
}

Expand Down
Loading