Skip to content

Commit

Permalink
Merge branch 'main' into @Skalakid/fix-emoji-formatting-v2
Browse files Browse the repository at this point in the history
  • Loading branch information
Skalakid committed Jan 7, 2025
2 parents 711745b + e9c2893 commit d1a0677
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 62 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@expensify/react-native-live-markdown",
"version": "0.1.211",
"version": "0.1.213",
"description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.",
"main": "lib/commonjs/index",
"module": "lib/module/index",
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/parseExpensiMark.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ test('no formatting', () => {
expect('Hello, world!').toBeParsedAs([]);
});

describe('parsing error', () => {
expect(`> [exa\nmple.com](https://example.com)`).toBeParsedAs([]);
});

test('bold', () => {
expect('Hello, *world*!').toBeParsedAs([
{type: 'syntax', start: 7, length: 1},
Expand Down
43 changes: 1 addition & 42 deletions src/parseExpensiMark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {unescapeText} from 'expensify-common/dist/utils';
import {decode} from 'html-entities';
import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/commonTypes';
import type {MarkdownType, MarkdownRange} from './commonTypes';
import {splitRangesOnEmojis} from './rangeUtils';
import {groupRanges, sortRanges, splitRangesOnEmojis} from './rangeUtils';

function isWeb() {
return Platform.OS === 'web';
Expand Down Expand Up @@ -234,47 +234,6 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, MarkdownRange[]] {
return [text, ranges];
}

// getTagPriority returns a priority for a tag, higher priority means the tag should be processed first
function getTagPriority(tag: string) {
switch (tag) {
case 'syntax': // syntax has the lowest priority so other styles can be applied to it
return -1;
case 'blockquote':
return 2;
case 'h1':
return 1;
default:
return 0;
}
}

function sortRanges(ranges: MarkdownRange[]) {
// sort ranges by start position, then by length, then by tag hierarchy
return ranges.sort((a, b) => a.start - b.start || b.length - a.length || getTagPriority(b.type) - getTagPriority(a.type) || 0);
}

function groupRanges(ranges: MarkdownRange[]) {
const lastVisibleRangeIndex: {[key in MarkdownType]?: number} = {};

return ranges.reduce((acc, range) => {
const start = range.start;
const end = range.start + range.length;

const rangeWithSameStyleIndex = lastVisibleRangeIndex[range.type];
const sameStyleRange = rangeWithSameStyleIndex !== undefined ? acc[rangeWithSameStyleIndex] : undefined;

if (sameStyleRange && sameStyleRange.start <= start && sameStyleRange.start + sameStyleRange.length >= end && range.length > 1) {
// increment depth of overlapping range
sameStyleRange.depth = (sameStyleRange.depth || 1) + 1;
} else {
lastVisibleRangeIndex[range.type] = acc.length;
acc.push(range);
}

return acc;
}, [] as MarkdownRange[]);
}

function parseExpensiMark(markdown: string): MarkdownRange[] {
if (markdown.length > MAX_PARSABLE_LENGTH) {
return [];
Expand Down
56 changes: 54 additions & 2 deletions src/rangeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,59 @@

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

// getTagPriority returns a priority for a tag, higher priority means the tag should be processed first
function getTagPriority(tag: string) {
switch (tag) {
case 'blockquote':
return 2;
case 'h1':
return 1;
default:
return 0;
}
}

function sortRanges(ranges: MarkdownRange[]) {
// sort ranges by start position, then by length, then by tag hierarchy
return ranges.sort((a, b) => a.start - b.start || b.length - a.length || getTagPriority(b.type) - getTagPriority(a.type) || 0);
}

function groupRanges(ranges: MarkdownRange[]) {
const lastVisibleRangeIndex: {[key in MarkdownType]?: number} = {};

return ranges.reduce((acc, range) => {
const start = range.start;
const end = range.start + range.length;

const rangeWithSameStyleIndex = lastVisibleRangeIndex[range.type];
const sameStyleRange = rangeWithSameStyleIndex !== undefined ? acc[rangeWithSameStyleIndex] : undefined;

if (sameStyleRange && sameStyleRange.start <= start && sameStyleRange.start + sameStyleRange.length >= end && range.length > 1) {
// increment depth of overlapping range
sameStyleRange.depth = (sameStyleRange.depth || 1) + 1;
} else {
lastVisibleRangeIndex[range.type] = acc.length;
acc.push(range);
}

return acc;
}, [] as MarkdownRange[]);
}

function ungroupRanges(ranges: MarkdownRange[]): MarkdownRange[] {
const ungroupedRanges: MarkdownRange[] = [];
ranges.forEach((range) => {
if (!range.depth) {
ungroupedRanges.push(range);
}
const {depth, ...rangeWithoutDepth} = range;
Array.from({length: depth!}).forEach(() => {
ungroupedRanges.push(rangeWithoutDepth);
});
});
return ungroupedRanges;
}

function splitRangesOnEmojis(ranges: MarkdownRange[], type: MarkdownType): MarkdownRange[] {
const emojiRanges: MarkdownRange[] = ranges.filter((range) => range.type === 'emoji');
const newRanges: MarkdownRange[] = [];
Expand Down Expand Up @@ -58,5 +111,4 @@ function splitRangesOnEmojis(ranges: MarkdownRange[], type: MarkdownType): Markd
return newRanges;
}

// eslint-disable-next-line import/prefer-default-export
export {splitRangesOnEmojis};
export {sortRanges, groupRanges, ungroupRanges, splitRangesOnEmojis};
19 changes: 4 additions & 15 deletions src/web/utils/parserUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition} from './cu
import {addStyleToBlock, extendBlockStructure, getFirstBlockMarkdownRange, isBlockMarkdownType} from './blockUtils';
import type {InlineImagesInputProps, MarkdownRange} from '../../commonTypes';
import {getAnimationCurrentTimes, updateAnimationsTime} from './animationUtils';
import {sortRanges, ungroupRanges} from '../../rangeUtils';

type Paragraph = {
text: string;
Expand All @@ -14,20 +15,6 @@ type Paragraph = {
markdownRanges: MarkdownRange[];
};

function ungroupRanges(ranges: MarkdownRange[]): MarkdownRange[] {
const ungroupedRanges: MarkdownRange[] = [];
ranges.forEach((range) => {
if (!range.depth) {
ungroupedRanges.push(range);
}
const {depth, ...rangeWithoutDepth} = range;
Array.from({length: depth!}).forEach(() => {
ungroupedRanges.push(rangeWithoutDepth);
});
});
return ungroupedRanges;
}

function splitTextIntoLines(text: string): Paragraph[] {
let lineStartIndex = 0;
const lines: Paragraph[] = text.split('\n').map((line) => {
Expand Down Expand Up @@ -167,7 +154,9 @@ function parseRangesToHTMLNodes(
return {dom: rootElement, tree: rootNode};
}

const markdownRanges = ungroupRanges(ranges);
// Sort all ranges by start position, length, and by tag hierarchy so the styles and text are applied in correct order
const sortedRanges = sortRanges(ranges);
const markdownRanges = ungroupRanges(sortedRanges);
lines = mergeLinesWithMultilineTags(lines, markdownRanges);

let lastRangeEndIndex = 0;
Expand Down

0 comments on commit d1a0677

Please sign in to comment.