Skip to content

Commit

Permalink
Inline images feature (#462)
Browse files Browse the repository at this point in the history
Co-authored-by: Bartosz Grajdek <[email protected]>
  • Loading branch information
Skalakid and BartoszGrajdek authored Sep 13, 2024
1 parent 0f8f21c commit e4bb45b
Show file tree
Hide file tree
Showing 18 changed files with 445 additions and 35 deletions.
2 changes: 1 addition & 1 deletion WebExample/__tests__/styles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ test.describe('markdown content styling', () => {

test('blockquote', async ({page, browserName}) => {
const blockquoteStyle =
'border-color: gray; border-width: 6px; margin-left: 6px; padding-left: 6px; border-left-style: solid; display: inline-block; max-width: 100%; box-sizing: border-box;';
'border-color: gray; border-width: 6px; margin-left: 6px; padding-left: 6px; border-left-style: solid; display: inline-block; max-width: 100%; box-sizing: border-box; overflow-wrap: anywhere;';

// Firefox border properties are serialized slightly differently
const browserStyle = browserName === 'firefox' ? blockquoteStyle.replace('border-left-style: solid', 'border-left: 6px solid gray') : blockquoteStyle;
Expand Down
1 change: 1 addition & 0 deletions example/src/testConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const EXAMPLE_CONTENT = [
'@here',
'@[email protected]',
'#mention-report',
'![demo image](https://picsum.photos/id/1069/200/300)',
].join('\n');

const INPUT_ID = 'MarkdownInput_Example';
Expand Down
12 changes: 12 additions & 0 deletions parser/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ describe('trailing whitespace', () => {
describe('inline image', () => {
test('with alt text', () => {
expect('![test](https://example.com/image.png)').toBeParsedAs([
{type: 'inline-image', start: 0, length: 38},
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 6, length: 1},
Expand All @@ -412,6 +413,7 @@ describe('inline image', () => {

test('without alt text', () => {
expect('![](https://example.com/image.png)').toBeParsedAs([
{type: 'inline-image', start: 0, length: 34},
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 2, length: 1},
Expand All @@ -423,6 +425,7 @@ describe('inline image', () => {

test('with same alt text as src', () => {
expect('![https://example.com/image.png](https://example.com/image.png)').toBeParsedAs([
{type: 'inline-image', start: 0, length: 63},
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 31, length: 1},
Expand All @@ -434,12 +437,14 @@ describe('inline image', () => {

test('text containing images', () => {
expect('An image of a banana: ![banana](https://example.com/banana.png) an image of a developer: ![dev](https://example.com/developer.png)').toBeParsedAs([
{type: 'inline-image', start: 22, length: 41},
{type: 'syntax', start: 22, length: 1},
{type: 'syntax', start: 23, length: 1},
{type: 'syntax', start: 30, length: 1},
{type: 'syntax', start: 31, length: 1},
{type: 'link', start: 32, length: 30},
{type: 'syntax', start: 62, length: 1},
{type: 'inline-image', start: 89, length: 41},
{type: 'syntax', start: 89, length: 1},
{type: 'syntax', start: 90, length: 1},
{type: 'syntax', start: 94, length: 1},
Expand All @@ -451,6 +456,7 @@ describe('inline image', () => {

test('with alt text containing markdown', () => {
expect('![# fake-heading *bold* _italic_ ~strike~ [:-)]](https://example.com/image.png)').toBeParsedAs([
{type: 'inline-image', start: 0, length: 79},
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 47, length: 1},
Expand All @@ -462,6 +468,7 @@ describe('inline image', () => {

test('text containing image and autolink', () => {
expect('An image of a banana: ![banana](https://example.com/banana.png) an autolink: example.com').toBeParsedAs([
{type: 'inline-image', start: 22, length: 41},
{type: 'syntax', start: 22, length: 1},
{type: 'syntax', start: 23, length: 1},
{type: 'syntax', start: 30, length: 1},
Expand All @@ -482,6 +489,7 @@ describe('inline image', () => {

test('trying to inject additional attributes', () => {
expect('![test" onerror="alert(\'xss\')](https://example.com/image.png)').toBeParsedAs([
{type: 'inline-image', start: 0, length: 61},
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 29, length: 1},
Expand All @@ -493,6 +501,7 @@ describe('inline image', () => {

test('inline code in alt', () => {
expect('![`code`](https://example.com/image.png)').toBeParsedAs([
{type: 'inline-image', start: 0, length: 40},
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 8, length: 1},
Expand All @@ -504,6 +513,7 @@ describe('inline image', () => {

test('blockquote in alt', () => {
expect('![```test```](https://example.com/image.png)').toBeParsedAs([
{type: 'inline-image', start: 0, length: 44},
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'syntax', start: 12, length: 1},
Expand All @@ -515,6 +525,7 @@ describe('inline image', () => {

test('image without alt text', () => {
expect('!(https://example.com/image.png)').toBeParsedAs([
{type: 'inline-image', start: 0, length: 32},
{type: 'syntax', start: 0, length: 1},
{type: 'syntax', start: 1, length: 1},
{type: 'link', start: 2, length: 29},
Expand All @@ -526,6 +537,7 @@ describe('inline image', () => {
expect('# ![](example.com)').toBeParsedAs([
{type: 'syntax', start: 0, length: 2},
{type: 'h1', start: 2, length: 16},
{type: 'inline-image', start: 2, length: 16},
{type: 'syntax', start: 2, length: 1},
{type: 'syntax', start: 3, length: 1},
{type: 'syntax', start: 4, length: 1},
Expand Down
20 changes: 19 additions & 1 deletion parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,21 @@
import ExpensiMark from 'expensify-common/dist/ExpensiMark';
import {unescapeText} from './utils';

type MarkdownType = 'bold' | 'italic' | 'strikethrough' | 'emoji' | 'mention-here' | 'mention-user' | 'mention-report' | 'link' | 'code' | 'pre' | 'blockquote' | 'h1' | 'syntax';
type MarkdownType =
| 'bold'
| 'italic'
| 'strikethrough'
| 'emoji'
| 'mention-here'
| 'mention-user'
| 'mention-report'
| 'link'
| 'code'
| 'pre'
| 'blockquote'
| 'h1'
| 'syntax'
| 'inline-image';
type MarkdownRange = {
type: MarkdownType;
start: number;
Expand Down Expand Up @@ -182,6 +196,10 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, MarkdownRange[]] {
const rawLink = node.tag.match(/data-raw-href="([^"]*)"/);
const linkString = rawLink ? unescapeText(rawLink[1]!) : src;

const start = text.length;
const length = 3 + (hasAlt ? 2 + unescapeText(alt?.[1] || '').length : 0) + linkString.length;
ranges.push({type: 'inline-image', start, length});

appendSyntax('!');
if (hasAlt) {
appendSyntax('[');
Expand Down
30 changes: 15 additions & 15 deletions parser/react-native-live-markdown-parser.js

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions src/MarkdownTextInput.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
if (text === null) {
return {text: divRef.current.value, cursorPosition: null};
}
const parsedText = updateInputStructure(target, text, cursorPosition, customMarkdownStyles, false, shouldForceDOMUpdate);
const parsedText = updateInputStructure(target, text, cursorPosition, multiline, customMarkdownStyles, false, shouldForceDOMUpdate);
divRef.current.value = parsedText.text;

if (history.current && shouldAddToHistory) {
Expand All @@ -158,7 +158,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(

return parsedText;
},
[],
[multiline],
);

const processedMarkdownStyle = useMemo(() => {
Expand Down Expand Up @@ -287,7 +287,9 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(

updateTextColor(divRef.current, e.target.textContent ?? '');
const previousText = divRef.current.value;
const parsedText = normalizeValue(inputType === 'pasteText' ? pasteContent.current || '' : parseInnerHTMLToText(e.target as MarkdownTextInputElement));
const parsedText = normalizeValue(
inputType === 'pasteText' ? pasteContent.current || '' : parseInnerHTMLToText(e.target as MarkdownTextInputElement, inputType, contentSelection.current.start),
);

if (pasteContent.current) {
pasteContent.current = null;
Expand Down
21 changes: 21 additions & 0 deletions src/MarkdownTextInputDecoratorViewNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ interface MarkdownStyle {
color: ColorValue;
backgroundColor: ColorValue;
};
inlineImage: {
maxWidth: Float;
maxHeight: Float;
marginTop: Float;
marginBottom: Float;
};
loadingIndicatorContainer?: {
backgroundColor?: ColorValue;
borderWidth?: Float;
borderColor?: ColorValue;
borderRadius?: Float;
width?: Float;
height?: Float;
};
loadingIndicator?: {
primaryColor?: ColorValue;
secondaryColor?: ColorValue;
width?: Float;
height?: Float;
borderWidth?: Float;
};
}

interface NativeProps extends ViewProps {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/webParser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const toBeParsedAsHTML = function (actual: string, expectedHTML: string) {
let expected = expectedHTML;
const markdownRanges = global.parseExpensiMarkToRanges(actual);

const actualDOM = parseRangesToHTMLNodes(actual, markdownRanges, {}, true).dom;
const actualDOM = parseRangesToHTMLNodes(actual, markdownRanges, true, {}, true).dom;
const actualHTML = actualDOM.innerHTML;

if (actualHTML === expected) {
Expand Down
16 changes: 15 additions & 1 deletion src/commonTypes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
type MarkdownType = 'bold' | 'italic' | 'strikethrough' | 'emoji' | 'mention-here' | 'mention-user' | 'mention-report' | 'link' | 'code' | 'pre' | 'blockquote' | 'h1' | 'syntax';
type MarkdownType =
| 'bold'
| 'italic'
| 'strikethrough'
| 'emoji'
| 'mention-here'
| 'mention-user'
| 'mention-report'
| 'link'
| 'code'
| 'pre'
| 'blockquote'
| 'h1'
| 'syntax'
| 'inline-image';

interface MarkdownRange {
type: MarkdownType;
Expand Down
22 changes: 20 additions & 2 deletions src/styleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ function makeDefaultMarkdownStyle(): MarkdownStyle {
color: 'red',
backgroundColor: 'pink',
},
inlineImage: {
maxWidth: 150,
maxHeight: 150,
marginTop: 5,
marginBottom: 0,
},
loadingIndicator: {
primaryColor: 'gray',
secondaryColor: 'lightgray',
},
};
}

Expand All @@ -65,13 +75,21 @@ function mergeMarkdownStyleWithDefault(input: PartialMarkdownStyle | undefined):
if (!(key in output)) {
return;
}
Object.assign(output[key as keyof MarkdownStyle], input[key as keyof MarkdownStyle]);

const outputValue = output[key as keyof MarkdownStyle];
if (outputValue) {
Object.assign(outputValue, input[key as keyof MarkdownStyle]);
}
});
}

return output;
}

function parseStringWithUnitToNumber(value: string | null): number {
return value ? parseInt(value.replace('px', ''), 10) : 0;
}

export type {PartialMarkdownStyle};

export {mergeMarkdownStyleWithDefault};
export {mergeMarkdownStyleWithDefault, parseStringWithUnitToNumber};
17 changes: 17 additions & 0 deletions src/web/MarkdownTextInput.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,20 @@
display: block; /* For Firefox */
content: attr(placeholder);
}

@keyframes react-native-live-markdown-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

.react-native-live-markdown-input-multiline [contenteditable='false'] {
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
Loading

0 comments on commit e4bb45b

Please sign in to comment.