Skip to content

Commit

Permalink
Merge branch 'main' into @Skalakid/web-parser-refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
Skalakid committed Aug 19, 2024
2 parents a1bf047 + f06ac2c commit 457f42b
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 73 deletions.
36 changes: 7 additions & 29 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ jobs:
if: ${{ github.actor != 'OSBotify' }}

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
ref: main
# The OS_BOTIFY_COMMIT_TOKEN is a personal access token tied to osbotify
# This is a workaround to allow pushes to a protected branch
token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }}

- name: Decrypt & Import OSBotify GPG key
run: |
Expand All @@ -36,17 +39,11 @@ jobs:
git config --global user.name OSBotify
git config --global user.email [email protected]
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '16.x'
node-version-file: '.nvmrc'
registry-url: 'https://registry.npmjs.org'

- name: Generate branch name
run: echo "BRANCH_NAME=OSBotify-bump-version-$(uuidgen)" >> $GITHUB_ENV

- name: Create branch for version-bump pull request
run: git checkout -b ${{ env.BRANCH_NAME }}

- name: Install yarn packages
run: yarn install --immutable

Expand All @@ -63,26 +60,7 @@ jobs:
run: git tag ${{ env.NEW_VERSION }}

- name: Push branch and publish tags
run: git push --set-upstream origin ${{ env.BRANCH_NAME }} && git push --tags

- name: Create pull request
run: |
gh pr create \
--title "Update version to ${{ env.NEW_VERSION }}" \
--body "Update version to ${{ env.NEW_VERSION }}"
sleep 5
env:
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}

- name: Auto-approve pull request
run: gh pr review --approve ${{ env.BRANCH_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Auto-merge pull request
run: gh pr merge --squash --delete-branch ${{ env.BRANCH_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: git push --set-upstream origin main && git push --tags

- name: Build package
run: yarn pack
Expand Down
9 changes: 8 additions & 1 deletion ios/RCTBaseTextInputView+Markdown.mm
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ - (BOOL)markdown_textOf:(NSAttributedString *)newText equals:(NSAttributedString
{
RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils];
if (markdownUtils != nil) {
return [newText isEqualToAttributedString:oldText];
// Emoji characters are automatically assigned an AppleColorEmoji NSFont and the original font is moved to NSOriginalFont
// We need to remove these attributes before comparison
NSMutableAttributedString *newTextCopy = [newText mutableCopy];
NSMutableAttributedString *oldTextCopy = [oldText mutableCopy];
[newTextCopy removeAttribute:@"NSFont" range:NSMakeRange(0, newTextCopy.length)];
[oldTextCopy removeAttribute:@"NSFont" range:NSMakeRange(0, oldTextCopy.length)];
[oldTextCopy removeAttribute:@"NSOriginalFont" range:NSMakeRange(0, oldTextCopy.length)];
return [newTextCopy isEqualToAttributedString:oldTextCopy];
}

return [self markdown_textOf:newText equals:oldText];
Expand Down
4 changes: 0 additions & 4 deletions ios/RCTMarkdownUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,6 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA
NSRange rangeForBackground = [inputString characterAtIndex:range.location] == '\n' ? NSMakeRange(range.location + 1, range.length - 1) : range;
[attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.preBackgroundColor range:rangeForBackground];
// TODO: pass background color and ranges to layout manager
} else if (type == "h1") {
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
NSRange rangeWithHashAndSpace = NSMakeRange(range.location - 2, range.length + 2); // we also need to include prepending "# "
[attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:rangeWithHashAndSpace];
}
}

Expand Down
9 changes: 8 additions & 1 deletion ios/RCTTextInputComponentView+Markdown.mm
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,14 @@ - (BOOL)markdown__textOf:(NSAttributedString *)newText equals:(NSAttributedStrin
{
RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils];
if (markdownUtils != nil) {
return [newText isEqualToAttributedString:oldText];
// Emoji characters are automatically assigned an AppleColorEmoji NSFont and the original font is moved to NSOriginalFont
// We need to remove these attributes before comparison
NSMutableAttributedString *newTextCopy = [newText mutableCopy];
NSMutableAttributedString *oldTextCopy = [oldText mutableCopy];
[newTextCopy removeAttribute:@"NSFont" range:NSMakeRange(0, newTextCopy.length)];
[oldTextCopy removeAttribute:@"NSFont" range:NSMakeRange(0, oldTextCopy.length)];
[oldTextCopy removeAttribute:@"NSOriginalFont" range:NSMakeRange(0, oldTextCopy.length)];
return [newTextCopy isEqualToAttributedString:oldTextCopy];
}

return [self markdown__textOf:newText equals:oldText];
Expand Down
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.103",
"version": "0.1.114",
"description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.",
"main": "lib/commonjs/index",
"module": "lib/module/index",
Expand Down
99 changes: 99 additions & 0 deletions parser/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ test('labeled link', () => {
{type: 'link', start: 7, length: 19},
{type: 'syntax', start: 26, length: 1},
]);

expect('[ Link ](https://example.com)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 7, length: 2},
{type: 'link', start: 9, length: 19},
{type: 'syntax', start: 28, length: 1},
]);
});

test('link with same label as href', () => {
Expand Down Expand Up @@ -177,6 +184,15 @@ describe('email with same label as address', () => {
});
});

test('email with multiline hyperlinks', () => {
expect('[test\ntest]([email protected])').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 10, length: 2},
{type: 'link', start: 12, length: 13},
{type: 'syntax', start: 25, length: 1},
]);
});

test('inline code', () => {
expect('Hello `world`!').toBeParsedAs([
{type: 'syntax', start: 6, length: 1},
Expand Down Expand Up @@ -541,3 +557,86 @@ describe('report mentions', () => {
expect('reported #report-name!').toBeParsedAs([{type: 'mention-report', start: 9, length: 12}]);
});
});

describe('inline video', () => {
test('with alt text', () => {
expect('![test](https://example.com/video.mp4)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 6, length: 1},
{type: 'syntax', start: 7, length: 1},
{type: 'link', start: 8, length: 29},
{type: 'syntax', start: 37, length: 1},
]);
});

test('without alt text', () => {
expect('![](https://example.com/video.mp4)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 2, length: 1},
{type: 'syntax', start: 3, length: 1},
{type: 'link', start: 4, length: 29},
{type: 'syntax', start: 33, length: 1},
]);
});

test('with same alt text as src', () => {
expect('![https://example.com/video.mp4](https://example.com/video.mp4)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 31, length: 1},
{type: 'syntax', start: 32, length: 1},
{type: 'link', start: 33, length: 29},
{type: 'syntax', start: 62, length: 1},
]);
});

test('with alt text containing markdown', () => {
expect('![# fake-heading *bold* _italic_ ~strike~ [:-)]](https://example.com/video.mp4)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 47, length: 1},
{type: 'syntax', start: 48, length: 1},
{type: 'link', start: 49, length: 29},
{type: 'syntax', start: 78, length: 1},
]);
});

test('trying to pass additional attributes', () => {
expect('![test](https://example.com/video.mp4 "title" class="video")').toBeParsedAs([{type: 'link', start: 8, length: 29}]);
});

test('trying to inject additional attributes', () => {
expect('![test" onerror="alert(\'xss\')](https://example.com/video.mp4)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 29, length: 1},
{type: 'syntax', start: 30, length: 1},
{type: 'link', start: 31, length: 29},
{type: 'syntax', start: 60, length: 1},
]);
});

test('inline code in alt', () => {
expect('![`code`](https://example.com/video.mp4)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 8, length: 1},
{type: 'syntax', start: 9, length: 1},
{type: 'link', start: 10, length: 29},
{type: 'syntax', start: 39, length: 1},
]);
});

test('blockquote in alt', () => {
expect('![```test```](https://example.com/video.mp4)').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 12, length: 1},
{type: 'syntax', start: 13, length: 1},
{type: 'link', start: 14, length: 29},
{type: 'syntax', start: 43, length: 1},
]);
});
});
17 changes: 16 additions & 1 deletion parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, Range[]] {
} else if (node.tag === '<h1>') {
appendSyntax('# ');
addChildrenWithStyle(node, 'h1');
} else if (node.tag === '<br />') {
text += '\n';
} else if (node.tag.startsWith('<pre')) {
appendSyntax('```');
const content = node.children.join('').replaceAll('&#32;', ' ');
Expand Down Expand Up @@ -189,6 +191,20 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, Range[]] {
appendSyntax('(');
addChildrenWithStyle(linkString, 'link');
appendSyntax(')');
} else if (node.tag.startsWith('<video data-expensify-source="')) {
const src = node.tag.match(/data-expensify-source="([^"]*)"/)![1]!; // always present
const rawLink = node.tag.match(/data-raw-href="([^"]*)"/);
const hasAlt = node.tag.match(/data-link-variant="([^"]*)"/)![1] === 'labeled';
const linkString = rawLink ? unescapeText(rawLink[1]!) : src;
appendSyntax('!');
if (hasAlt) {
appendSyntax('[');
node.children.forEach((child) => processChildren(child));
appendSyntax(']');
}
appendSyntax('(');
addChildrenWithStyle(linkString, 'link');
appendSyntax(')');
} else {
throw new Error(`[react-native-live-markdown] Error in function parseTreeToTextAndRanges: Unknown tag '${node.tag}'. This tag is not supported in this function's logic.`);
}
Expand Down Expand Up @@ -254,7 +270,6 @@ function parseExpensiMarkToRanges(markdown: string): Range[] {
const groupedRanges = groupRanges(sortedRanges);
return groupedRanges;
} catch (error) {
console.error(error);
// returning an empty array in case of error
return [];
}
Expand Down
2 changes: 1 addition & 1 deletion parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@
"typescript": "^5.3.3"
},
"dependencies": {
"expensify-common": "2.0.35"
"expensify-common": "2.0.72"
}
}
41 changes: 21 additions & 20 deletions parser/react-native-live-markdown-parser.js

Large diffs are not rendered by default.

16 changes: 13 additions & 3 deletions src/MarkdownTextInput.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import React, {useEffect, useRef, useCallback, useMemo, useLayoutEffect} from 'r
import type {CSSProperties, MutableRefObject, ReactEventHandler, FocusEventHandler, MouseEvent, KeyboardEvent, SyntheticEvent, ClipboardEventHandler} from 'react';
import {StyleSheet} from 'react-native';
import {updateInputStructure} from './web/utils/parserUtils';
import BrowserUtils from './web/utils/browserUtils';
import InputHistory from './web/InputHistory';
import type {TreeNode} from './web/utils/treeUtils';
import {getCurrentCursorPosition, removeSelection, setCursorPosition} from './web/utils/cursorUtils';
Expand Down Expand Up @@ -97,6 +96,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
autoFocus = false,
onContentSizeChange,
id,
inputMode,
},
ref,
) => {
Expand Down Expand Up @@ -295,7 +295,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
const prevSelection = contentSelection.current ?? {start: 0, end: 0};
const newCursorPosition = Math.max(Math.max(contentSelection.current.end, 0) + (parsedText.length - previousText.length), 0);

if (compositionRef.current && !BrowserUtils.isMobile) {
if (compositionRef.current) {
divRef.current.value = parsedText;
compositionRef.current = false;
contentSelection.current.end = newCursorPosition;
Expand Down Expand Up @@ -346,7 +346,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
if (inputType === 'deleteContentBackward') {
// When the user does a backspace delete he expects the content before the cursor to be removed.
// For this the start value needs to be adjusted (its as if the selection was before the text that we want to delete)
start -= before;
start = Math.max(start - before, 0);
}

event.nativeEvent.count = count;
Expand Down Expand Up @@ -552,6 +552,14 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
compositionRef.current = true;
}, []);

const endComposition = useCallback(
(e) => {
compositionRef.current = false;
handleOnChangeText(e);
},
[handleOnChangeText],
);

const setRef = (currentRef: HTMLDivElement | null) => {
const r = currentRef;
if (r) {
Expand Down Expand Up @@ -651,6 +659,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
className={className}
onKeyDown={handleKeyPress}
onCompositionStart={startComposition}
onCompositionEnd={endComposition}
onKeyUp={updateSelection}
onInput={handleOnChangeText}
onClick={handleClick}
Expand All @@ -662,6 +671,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
placeholder={heightSafePlaceholder}
spellCheck={spellCheck}
dir={dir}
inputMode={inputMode}
/>
);
},
Expand Down
Loading

0 comments on commit 457f42b

Please sign in to comment.