Skip to content

Commit

Permalink
Merge branch 'main' into @tomekzaw/worklets
Browse files Browse the repository at this point in the history
  • Loading branch information
tomekzaw committed Aug 30, 2024
2 parents 221fa36 + 750f2d9 commit eed5984
Show file tree
Hide file tree
Showing 29 changed files with 1,210 additions and 761 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module.exports = {
root: true,
rules: {
'rulesdir/prefer-underscore-method': 'off',
'rulesdir/prefer-import-module-contents': 'off',
'react/jsx-props-no-spreading': 'off',
'react/require-default-props': 'off',
'react/jsx-filename-extension': ['error', { extensions: ['.tsx', '.jsx'] }],
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,6 @@ android/keystores/debug.keystore

# generated by bob
lib/

# react-native-live-markdown
.build_complete
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,10 @@ The style object can be passed to multiple `MarkdownTextInput` components using
## Parsing logic

`MarkdownTextInput` behavior can be customized via `parser` property. Parser is a function that accepts a plaintext string and returns an array of `Range` objects:
`MarkdownTextInput` behavior can be customized via `parser` property. Parser is a function that accepts a plaintext string and returns an array of `MarkdownRange` objects:

```ts
interface Range {
interface MarkdownRange {
type: MarkdownType;
start: number;
length: number;
Expand Down Expand Up @@ -172,10 +172,10 @@ Currently, `react-native-live-markdown` supports only [ExpensiMark](https://gith

`MarkdownTextInput` inherits all props of React Native's `TextInput` component as well as introduces the following properties:

| Prop | Type | Default | Note |
| --------------- | ---------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parser` | `(value: string) => Range[]` | `undefined` | A function that parses the current value and returns an array of ranges. |
| `markdownStyle` | `MarkdownStyle` | `undefined` | Adds custom styling to Markdown text. The provided value is merged with default style object. See [Styling](https://github.com/expensify/react-native-live-markdown/blob/main/README.md#styling) for more information. |
| Prop | Type | Default | Note |
| --------------- | ------------------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parser` | `(value: string) => MarkdownRange[]` | `undefined` | A function that parses the current value and returns an array of ranges. |
| `markdownStyle` | `MarkdownStyle` | `undefined` | Adds custom styling to Markdown text. The provided value is merged with default style object. See [Styling](https://github.com/expensify/react-native-live-markdown/blob/main/README.md#styling) for more information. |

## Compatibility

Expand Down
12 changes: 6 additions & 6 deletions WebExample/__tests__/input.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {test, expect} from '@playwright/test';
import * as TEST_CONST from '../../example/src/testConstants';
import {checkCursorPosition, setupInput} from './utils';
import {getCursorPosition, getElementValue, setupInput} from './utils';

test.beforeEach(async ({page}) => {
await page.goto(TEST_CONST.LOCAL_URL, {waitUntil: 'load'});
Expand All @@ -12,8 +12,8 @@ test.describe('typing', () => {

await inputLocator.focus();
await inputLocator.pressSequentially(TEST_CONST.EXAMPLE_CONTENT);
const value = await inputLocator.innerText();
expect(value).toEqual(TEST_CONST.EXAMPLE_CONTENT);

expect(await getElementValue(inputLocator)).toEqual(TEST_CONST.EXAMPLE_CONTENT);
});

test('fast type cursor position', async ({page}) => {
Expand All @@ -23,10 +23,10 @@ test.describe('typing', () => {

await inputLocator.pressSequentially(EXAMPLE_LONG_CONTENT);

expect(await inputLocator.innerText()).toBe(EXAMPLE_LONG_CONTENT);
expect(await getElementValue(inputLocator)).toBe(EXAMPLE_LONG_CONTENT);

const cursorPosition = await page.evaluate(checkCursorPosition);
const cursorPosition = await getCursorPosition(inputLocator);

expect(cursorPosition).toBe(EXAMPLE_LONG_CONTENT.length);
expect(cursorPosition.end).toBe(EXAMPLE_LONG_CONTENT.length);
});
});
36 changes: 19 additions & 17 deletions WebExample/__tests__/textManipulation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {test, expect} from '@playwright/test';
import type {Locator, Page} from '@playwright/test';
import * as TEST_CONST from '../../example/src/testConstants';
import {checkCursorPosition, setupInput, getElementStyle, pressCmd} from './utils';
import {getCursorPosition, setupInput, getElementStyle, pressCmd, getElementValue} from './utils';

const pasteContent = async ({text, page, inputLocator}: {text: string; page: Page; inputLocator: Locator}) => {
await page.evaluate(async (pasteText) => navigator.clipboard.writeText(pasteText), text);
Expand Down Expand Up @@ -43,7 +43,7 @@ test.describe('paste content', () => {
const newText = '*bold*';
await pasteContent({text: newText, page, inputLocator});

expect(await inputLocator.innerText()).toBe(newText);
expect(await getElementValue(inputLocator)).toBe(newText);
});

test('paste undo', async ({page, browserName}) => {
Expand All @@ -61,10 +61,9 @@ test.describe('paste content', () => {
await page.evaluate(async (pasteText) => navigator.clipboard.writeText(pasteText), PASTE_TEXT_SECOND);
await pressCmd({inputLocator, command: 'v'});
await page.waitForTimeout(TEST_CONST.INPUT_HISTORY_DEBOUNCE_TIME_MS);

await pressCmd({inputLocator, command: 'z'});

expect(await inputLocator.innerText()).toBe(PASTE_TEXT_FIRST);
await page.waitForTimeout(TEST_CONST.INPUT_HISTORY_DEBOUNCE_TIME_MS);
expect(await getElementValue(inputLocator)).toBe(PASTE_TEXT_FIRST);
});

test('paste redo', async ({page}) => {
Expand All @@ -84,7 +83,7 @@ test.describe('paste content', () => {
await pressCmd({inputLocator, command: 'z'});
await pressCmd({inputLocator, command: 'Shift+z'});

expect(await inputLocator.innerText()).toBe(`${PASTE_TEXT_FIRST}${PASTE_TEXT_SECOND}`);
expect(await getElementValue(inputLocator)).toBe(`${PASTE_TEXT_FIRST}${PASTE_TEXT_SECOND}`);
});
});

Expand All @@ -93,9 +92,9 @@ test('select all', async ({page}) => {
await inputLocator.focus();
await pressCmd({inputLocator, command: 'a'});

const cursorPosition = await page.evaluate(checkCursorPosition);
const cursorPosition = await getCursorPosition(inputLocator);

expect(cursorPosition).toBe(TEST_CONST.EXAMPLE_CONTENT.length);
expect(cursorPosition.end).toBe(TEST_CONST.EXAMPLE_CONTENT.length);
});

test('cut content changes', async ({page, browserName}) => {
Expand All @@ -107,15 +106,12 @@ test('cut content changes', async ({page, browserName}) => {

const inputLocator = await setupInput(page, 'clear');
await pasteContent({text: WRAPPED_CONTENT, page, inputLocator});
const rootHandle = await inputLocator.locator('span.root').first();

await page.evaluate(async (initialContent) => {
const filteredNode = Array.from(document.querySelectorAll('div[contenteditable="true"] > span.root span')).find((node) => {
return node.textContent?.includes(initialContent) && node.nextElementSibling && node.nextElementSibling.textContent?.includes('*');
});
await page.evaluate(async () => {
const filteredNode = Array.from(document.querySelectorAll('span[data-type="text"]'));

const startNode = filteredNode;
const endNode = filteredNode?.nextElementSibling;
const startNode = filteredNode[1];
const endNode = filteredNode[2];

if (startNode?.firstChild && endNode?.lastChild) {
const range = new Range();
Expand All @@ -126,10 +122,16 @@ test('cut content changes', async ({page, browserName}) => {
selection?.removeAllRanges();
selection?.addRange(range);
}
}, INITIAL_CONTENT);

return filteredNode;
});

await inputLocator.focus();
await pressCmd({inputLocator, command: 'x'});

expect(await rootHandle.innerHTML()).toBe(EXPECTED_CONTENT);
expect(await getElementValue(inputLocator)).toBe(EXPECTED_CONTENT);

// Ckeck if there is no markdown elements after the cut operation
const spans = await inputLocator.locator('span[data-type="text"]');
expect(await spans.count()).toBe(1);
});
35 changes: 22 additions & 13 deletions WebExample/__tests__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ const setupInput = async (page: Page, action?: 'clear' | 'reset') => {
return inputLocator;
};

const checkCursorPosition = () => {
const editableDiv = document.querySelector('div[contenteditable="true"]') as HTMLElement;
const range = window.getSelection()?.getRangeAt(0);
if (!range || !editableDiv) {
return null;
}
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(editableDiv);
preCaretRange.setEnd(range.endContainer, range.endOffset);
return preCaretRange.toString().length;
const getCursorPosition = async (elementHandle: Locator) => {
const inputSelectionHandle = await elementHandle.evaluateHandle(
(
div: HTMLInputElement & {
selection: {start: number; end: number};
},
) => div.selection,
);
const selection = await inputSelectionHandle.jsonValue();
return selection;
};

const setCursorPosition = ({startNode, endNode}: {startNode?: Element; endNode?: Element | null}) => {
Expand All @@ -43,8 +43,11 @@ const getElementStyle = async (elementHandle: Locator) => {

if (elementHandle) {
await elementHandle.waitFor({state: 'attached'});

elementStyle = await elementHandle.getAttribute('style');
// We need to get styles from the parent element because every text node is wrapped additionally with a span element
const parentElementHandle = await elementHandle.evaluateHandle((element) => {
return element.parentElement;
});
elementStyle = await parentElementHandle.asElement()?.getAttribute('style');
}
return elementStyle;
};
Expand All @@ -55,4 +58,10 @@ const pressCmd = async ({inputLocator, command}: {inputLocator: Locator; command
await inputLocator.press(`${OPERATION_MODIFIER}+${command}`);
};

export {setupInput, checkCursorPosition, setCursorPosition, getElementStyle, pressCmd};
const getElementValue = async (elementHandle: Locator) => {
const inputValueHandle = await elementHandle.evaluateHandle((div: HTMLInputElement) => div.value);
const value = await inputValueHandle.jsonValue();
return value;
};

export {setupInput, getCursorPosition, setCursorPosition, getElementStyle, pressCmd, getElementValue};
8 changes: 4 additions & 4 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1053,13 +1053,13 @@ PODS:
- React-jsi (= 0.73.4)
- React-logger (= 0.73.4)
- React-perflogger (= 0.73.4)
- RNLiveMarkdown (0.1.111):
- RNLiveMarkdown (0.1.120):
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
- RNLiveMarkdown/common (= 0.1.111)
- RNLiveMarkdown/common (= 0.1.120)
- RNReanimated/worklets
- RNLiveMarkdown/common (0.1.111):
- RNLiveMarkdown/common (0.1.120):
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
Expand Down Expand Up @@ -1298,7 +1298,7 @@ SPEC CHECKSUMS:
React-runtimescheduler: ed48e5faac6751e66ee1261c4bd01643b436f112
React-utils: 6e5ad394416482ae21831050928ae27348f83487
ReactCommon: 840a955d37b7f3358554d819446bffcf624b2522
RNLiveMarkdown: 0ab41fe829862ca9e9ef3b6604201d1de9c9e416
RNLiveMarkdown: c69f957652deeda303840cd1bbe612f203a20a9e
RNReanimated: 1353558d1fba65da0e0d0dde4146d5690f78276f
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70
Expand Down
4 changes: 2 additions & 2 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
"start": "react-native start"
},
"dependencies": {
"expensify-common": "2.0.72",
"expensify-common": "2.0.76",
"react": "18.2.0",
"react-native": "0.73.4",
"react-native-reanimated": "3.15.0-nightly-20240804-7df5fd57d"
"react-native-reanimated": "3.15.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
Expand Down
2 changes: 0 additions & 2 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import * as React from 'react';

import {Button, Platform, StyleSheet, Text, View} from 'react-native';

import {MarkdownTextInput, parseExpensiMark} from '@expensify/react-native-live-markdown';
import type {TextInput} from 'react-native';
import * as TEST_CONST from './testConstants';
Expand Down
13 changes: 8 additions & 5 deletions 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.112",
"version": "0.1.120",
"description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.",
"main": "lib/commonjs/index",
"module": "lib/module/index",
Expand Down Expand Up @@ -36,6 +36,7 @@
"lint:WebExample": "eslint WebExample --ext .js,.ts,.tsx",
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
"prepare": "bob build",
"build:watch": "nodemon --watch src --ext .ts,.tsx,.css --exec \"rm -f .build_complete && yarn prepare && yarn pack && touch .build_complete\"",
"release": "release-it"
},
"keywords": [
Expand Down Expand Up @@ -83,14 +84,15 @@
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-tsdoc": "^0.2.17",
"expensify-common": "2.0.72",
"expensify-common": "2.0.76",
"jest": "^29.6.3",
"jest-environment-jsdom": "^29.7.0",
"nodemon": "^3.1.3",
"prettier": "2.8.8",
"react": "18.2.0",
"react-native": "0.73.4",
"react-native-builder-bob": "^0.20.0",
"react-native-reanimated": "3.15.0-nightly-20240804-7df5fd57d",
"react-native-reanimated": "3.15.0",
"react-native-web": "^0.19.10",
"release-it": "^15.0.0",
"turbo": "^1.10.7",
Expand All @@ -99,10 +101,11 @@
"resolutions": {
"@types/react": "17.0.21",
"[email protected]": "patch:react-native@npm%3A0.73.4#./.yarn/patches/react-native-npm-0.73.4-297ae78b8f.patch",
"@react-native/[email protected]": "patch:@react-native/gradle-plugin@npm%3A0.73.4#./.yarn/patches/@react-native-gradle-plugin-npm-0.73.4-6535082666.patch"
"@react-native/[email protected]": "patch:@react-native/gradle-plugin@npm%3A0.73.4#./.yarn/patches/@react-native-gradle-plugin-npm-0.73.4-6535082666.patch",
"link@^2.1.1": "patch:link@npm%3A2.1.1#./.yarn/patches/link-npm-2.1.1-1c9fea076e.patch"
},
"peerDependencies": {
"expensify-common": ">=2.0.72",
"expensify-common": ">=2.0.76",
"react": "*",
"react-native": "*",
"react-native-reanimated": ">=3.11.0"
Expand Down
27 changes: 8 additions & 19 deletions src/MarkdownTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import type {WorkletRuntime} from 'react-native-reanimated';
import type {ShareableRef, WorkletFunction} from 'react-native-reanimated/lib/typescript/commonTypes';

import MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent';
import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponent';
import NativeLiveMarkdownModule from './NativeLiveMarkdownModule';
import type * as MarkdownTextInputDecoratorViewNativeComponentTypes from './MarkdownTextInputDecoratorViewNativeComponent';
import * as StyleUtils from './styleUtils';
import type * as StyleUtilsTypes from './styleUtils';
import {mergeMarkdownStyleWithDefault} from './styleUtils';
import type {PartialMarkdownStyle} from './styleUtils';
import type {MarkdownRange} from './commonTypes';

declare global {
// eslint-disable-next-line no-var
Expand Down Expand Up @@ -39,7 +40,7 @@ function initializeLiveMarkdownIfNeeded() {
initialized = true;
}

function registerParser(parser: (input: string) => Range[]): number {
function registerParser(parser: (input: string) => MarkdownRange[]): number {
initializeLiveMarkdownIfNeeded();
const shareableWorklet = makeShareableCloneRecursive(parser) as ShareableRef<WorkletFunction<[string], Range[]>>;
const parserId = global.jsi_registerMarkdownWorklet(shareableWorklet);
Expand All @@ -50,21 +51,9 @@ function unregisterParser(parserId: number) {
global.jsi_unregisterMarkdownWorklet(parserId);
}

type PartialMarkdownStyle = StyleUtilsTypes.PartialMarkdownStyle;
type MarkdownStyle = MarkdownTextInputDecoratorViewNativeComponentTypes.MarkdownStyle;

type MarkdownType = 'bold' | 'italic' | 'strikethrough' | 'emoji' | 'mention-here' | 'mention-user' | 'mention-report' | 'link' | 'code' | 'pre' | 'blockquote' | 'h1' | 'syntax';

interface Range {
type: MarkdownType;
start: number;
length: number;
depth?: number;
}

interface MarkdownTextInputProps extends TextInputProps {
markdownStyle?: PartialMarkdownStyle;
parser: (value: string) => Range[];
parser: (value: string) => MarkdownRange[];
}

function processColorsInMarkdownStyle(input: MarkdownStyle): MarkdownStyle {
Expand All @@ -85,7 +74,7 @@ function processColorsInMarkdownStyle(input: MarkdownStyle): MarkdownStyle {
}

function processMarkdownStyle(input: PartialMarkdownStyle | undefined): MarkdownStyle {
return processColorsInMarkdownStyle(StyleUtils.mergeMarkdownStyleWithDefault(input));
return processColorsInMarkdownStyle(mergeMarkdownStyleWithDefault(input));
}

const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>((props, ref) => {
Expand Down Expand Up @@ -138,6 +127,6 @@ const styles = StyleSheet.create({
},
});

export type {PartialMarkdownStyle as MarkdownStyle, MarkdownTextInputProps, Range, MarkdownType};
export type {PartialMarkdownStyle as MarkdownStyle, MarkdownTextInputProps};

export default MarkdownTextInput;
Loading

0 comments on commit eed5984

Please sign in to comment.