Skip to content

Commit

Permalink
add web implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Skalakid committed Feb 5, 2024
1 parent 535bea7 commit f50c53c
Show file tree
Hide file tree
Showing 17 changed files with 1,867 additions and 133 deletions.
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"

SPEC CHECKSUMS:
boost: 57d2868c099736d80fcd648bf211b4431e51a558
boost: 7dcd2de282d72e344012f7d6564d024930a6a440
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: dc178b8748748c036ef9493a5d59d6d1f91a36ce
Expand Down Expand Up @@ -692,7 +692,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: 5a169b1dd1abad06bed40ab7e1aca883c657d865
React-jsinspector: 54205b269da20c51417e0fc02c4cde9f29a4bf1a
React-logger: f42d2f2bc4cbb5d19d7c0ce84b8741b1e54e88c8
react-native-live-markdown: 5722e2203fc2fab86dcb50fdaef92ca8b6c0fdec
react-native-live-markdown: 5df7530c7b987508b8634cee2c09092657e47021
React-NativeModulesApple: 9f72feb8a04020b32417f768a7e1e40eec91fef4
React-perflogger: cb433f318c6667060fc1f62e26eb58d6eb30a627
React-RCTActionSheet: 0af3f8ac067e8a1dde902810b7ad169d0a0ec31e
Expand Down
24 changes: 23 additions & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,15 @@ function getReactNativeVersion() {
return `${major}.${minor}.${patch}`;
}

function getRandomColor() {
return `#${Math.floor(Math.random() * 16777215)
.toString(16)
.padStart(6, '0')}`;
}

export default function App() {
const [value, setValue] = React.useState(DEFAULT_TEXT);
const [markdownStyle, setMarkdownStyle] = React.useState({});

// TODO: use MarkdownTextInput ref instead of TextInput ref
const ref = React.useRef<TextInput>(null);
Expand Down Expand Up @@ -89,6 +96,8 @@ export default function App() {
onChangeText={setValue}
style={styles.input}
ref={ref}
markdownStyle={markdownStyle}
placeholder="Type here..."
/>
{/* <Text>TextInput singleline</Text>
<TextInput
Expand Down Expand Up @@ -126,12 +135,25 @@ export default function App() {
/>
<Button
title="Reset"
onPress={() => setValue(DEFAULT_TEXT)}
onPress={() => {
setValue(DEFAULT_TEXT);
setMarkdownStyle({});
}}
/>
<Button
title="Clear"
onPress={() => setValue('')}
/>
<Button
title="Change style"
onPress={() =>
setMarkdownStyle({
link: {
color: getRandomColor(),
},
})
}
/>
</View>
);
}
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"lint:parser": "eslint parser --ext .js,.ts,.tsx",
"lint:web": "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",
"prepare": "bob build && mkdir -p lib/parser && cp parser/react-native-live-markdown-parser.js lib/parser/react-native-live-markdown-parser.js",
"release": "release-it"
},
"keywords": [
Expand Down Expand Up @@ -85,11 +85,13 @@
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-tsdoc": "^0.2.17",
"jest": "^28.1.1",
"jest-environment-jsdom": "^29.7.0",
"pod-install": "^0.1.0",
"prettier": "^2.0.5",
"react": "18.2.0",
"react-native": "0.72.9",
"react-native-builder-bob": "^0.20.0",
"react-native-web": "^0.19.10",
"release-it": "^15.0.0",
"turbo": "^1.10.7",
"typescript": "^5.3.3"
Expand All @@ -113,7 +115,8 @@
"modulePathIgnorePatterns": [
"<rootDir>/example/node_modules",
"<rootDir>/lib/"
]
],
"testEnvironment": "jsdom"
},
"commitlint": {
"extends": [
Expand Down
36 changes: 18 additions & 18 deletions parser/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,43 +173,43 @@ test('codeblock', () => {
describe('blockquote', () => {
test('with single space', () => {
expect('> Hello world!').toBeParsedAs([
['syntax', 0, 1],
['blockquote', 0, 14],
['syntax', 0, 1],
]);
});

test('with multiple spaces', () => {
expect('> Hello world!').toBeParsedAs([
['syntax', 0, 1],
['blockquote', 0, 19],
['syntax', 0, 1],
]);
});

test('without space', () => {
expect('>Hello world!').toBeParsedAs([
['syntax', 0, 1],
['blockquote', 0, 13],
['syntax', 0, 1],
]);
});
});

test('multiple blockquotes', () => {
expect('> Hello\n> beautiful\n> world').toBeParsedAs([
['syntax', 0, 1],
['blockquote', 0, 7],
['syntax', 8, 1],
['syntax', 0, 1],
['blockquote', 8, 11],
['syntax', 20, 1],
['syntax', 8, 1],
['blockquote', 20, 7],
['syntax', 20, 1],
]);
});

test('separate blockquotes', () => {
expect('> Lorem ipsum\ndolor\n> sit amet').toBeParsedAs([
['syntax', 0, 1],
['blockquote', 0, 13],
['syntax', 20, 1],
['syntax', 0, 1],
['blockquote', 20, 10],
['syntax', 20, 1],
]);
});

Expand All @@ -223,14 +223,14 @@ test('h1', () => {
test('nested bold and italic', () => {
expect('*_Hello_*, _*world*_!').toBeParsedAs([
['syntax', 0, 1],
['syntax', 1, 1],
['bold', 1, 7],
['syntax', 1, 1],
['italic', 2, 5],
['syntax', 7, 1],
['syntax', 8, 1],
['syntax', 11, 1],
['syntax', 12, 1],
['italic', 12, 7],
['syntax', 12, 1],
['bold', 13, 5],
['syntax', 18, 1],
['syntax', 19, 1],
Expand All @@ -240,26 +240,26 @@ test('nested bold and italic', () => {
describe('nested h1 in blockquote', () => {
test('without spaces', () => {
expect('># Hello world').toBeParsedAs([
['syntax', 0, 1],
['blockquote', 0, 14],
['syntax', 0, 1],
['syntax', 1, 2],
['h1', 3, 11],
]);
});

test('with single space', () => {
expect('> # Hello world').toBeParsedAs([
['syntax', 0, 1],
['blockquote', 0, 15],
['syntax', 0, 1],
['syntax', 2, 2],
['h1', 4, 11],
]);
});

test('with multiple spaces after #', () => {
expect('># Hello world').toBeParsedAs([
['syntax', 0, 1],
['blockquote', 0, 17],
['syntax', 0, 1],
['syntax', 1, 2],
['h1', 3, 14],
]);
Expand All @@ -270,22 +270,22 @@ describe('trailing whitespace', () => {
describe('after blockquote', () => {
test('nothing', () => {
expect('> Hello world').toBeParsedAs([
['syntax', 0, 1],
['blockquote', 0, 13],
['syntax', 0, 1],
]);
});

test('single space', () => {
expect('> Hello world ').toBeParsedAs([
['syntax', 0, 1],
['blockquote', 0, 14],
['syntax', 0, 1],
]);
});

test('newline', () => {
expect('> Hello world\n').toBeParsedAs([
['syntax', 0, 1],
['blockquote', 0, 13],
['syntax', 0, 1],
]);
});
});
Expand Down Expand Up @@ -321,12 +321,12 @@ describe('trailing whitespace', () => {

test('multiple blockquotes', () => {
expect('> # Hello\n> # world').toBeParsedAs([
['syntax', 0, 1],
['blockquote', 0, 9],
['syntax', 0, 1],
['syntax', 2, 2],
['h1', 4, 5],
['syntax', 10, 1],
['blockquote', 10, 9],
['syntax', 10, 1],
['syntax', 12, 2],
['h1', 14, 5],
]);
Expand Down
15 changes: 14 additions & 1 deletion parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,21 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, Range[]] {
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 'blockquote':
return 2;
case 'h1':
return 1;
default:
return 0;
}
}

function sortRanges(ranges: Range[]) {
return ranges.sort((a, b) => a[1] - b[1]); // sort by location to properly handle bold+italic
// sort ranges by start position, then by length, then by tag hierarchy
return ranges.sort((a, b) => a[1] - b[1] || b[2] - a[2] || getTagPriority(b[0]) - getTagPriority(a[0]) || 0);
}

function parseExpensiMarkToRanges(markdown: string): Range[] {
Expand Down
14 changes: 7 additions & 7 deletions parser/react-native-live-markdown-parser.js

Large diffs are not rendered by default.

78 changes: 9 additions & 69 deletions src/MarkdownTextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,16 @@
import {StyleSheet, TextInput, processColor} from 'react-native';
import React from 'react';
import {Platform, StyleSheet, TextInput, processColor} from 'react-native';
import type {TextInputProps} from 'react-native';

import type * as MarkdownTextInputDecoractorView from './MarkdownTextInputDecoratorViewNativeComponent';
import MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent';
import type * as MarkdownTextInputDecoratorViewNativeComponentTypes from './MarkdownTextInputDecoratorViewNativeComponent';
import * as StyleUtils from './styleUtils';
import type * as StyleUtilsTypes from './styleUtils';

type MarkdownStyle = MarkdownTextInputDecoractorView.MarkdownStyle;

const FONT_FAMILY_MONOSPACE = Platform.select({
ios: 'Courier',
default: 'monospace',
});

function makeDefaultMarkdownStyle(): MarkdownStyle {
return {
syntax: {
color: 'gray',
},
link: {
color: 'blue',
},
h1: {
fontSize: 25,
},
blockquote: {
borderColor: 'gray',
borderWidth: 6,
marginLeft: 6,
paddingLeft: 6,
},
code: {
fontFamily: FONT_FAMILY_MONOSPACE,
color: 'black',
backgroundColor: 'lightgray',
},
pre: {
fontFamily: FONT_FAMILY_MONOSPACE,
color: 'black',
backgroundColor: 'lightgray',
},
mentionHere: {
color: 'green',
backgroundColor: 'lime',
},
mentionUser: {
color: 'blue',
backgroundColor: 'cyan',
},
};
}
type PartialMarkdownStyle = StyleUtilsTypes.PartialMarkdownStyle;
type MarkdownStyle = MarkdownTextInputDecoratorViewNativeComponentTypes.MarkdownStyle;

type PartialMarkdownStyle = Partial<{
[K in keyof MarkdownStyle]: Partial<MarkdownStyle[K]>;
}>;

function mergeMarkdownStyleWithDefault(input: PartialMarkdownStyle | undefined): MarkdownStyle {
const output = makeDefaultMarkdownStyle();

if (input !== undefined) {
Object.keys(input).forEach((key) => {
if (!(key in output)) {
return;
}
Object.assign(output[key as keyof MarkdownStyle], input[key as keyof MarkdownStyle]);
});
}

return output;
interface MarkdownTextInputProps extends TextInputProps {
markdownStyle?: PartialMarkdownStyle;
}

function processColorsInMarkdownStyle(input: MarkdownStyle): MarkdownStyle {
Expand All @@ -87,11 +31,7 @@ function processColorsInMarkdownStyle(input: MarkdownStyle): MarkdownStyle {
}

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

interface MarkdownTextInputProps extends TextInputProps {
markdownStyle?: PartialMarkdownStyle;
return processColorsInMarkdownStyle(StyleUtils.mergeMarkdownStyleWithDefault(input));
}

const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>((props, ref) => {
Expand Down
Loading

0 comments on commit f50c53c

Please sign in to comment.