From 014a4ad740a758a6d2b65871c41008fd60052693 Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Mon, 15 Jan 2024 22:52:49 +0100 Subject: [PATCH] introduce required changes to satisfy new eslint/prettier config --- .watchmanconfig | 2 +- WebExample/app.json | 52 +- WebExample/babel.config.js | 38 +- WebExample/tsconfig.json | 12 +- WebExample/webpack.config.js | 20 +- babel.config.js | 2 +- parser/__tests__/index.test.js | 501 +++++++++--------- parser/index.ts | 293 +++++----- parser/tsconfig.json | 50 +- scripts/pod-install.cjs | 58 +- src/MarkdownTextInput.tsx | 185 ++++--- src/MarkdownTextInput.web.tsx | 23 +- ...wnTextInputDecoratorViewNativeComponent.ts | 72 +-- src/index.tsx | 7 +- tsconfig.json | 56 +- turbo.json | 52 +- types/global.d.ts | 4 +- 17 files changed, 693 insertions(+), 734 deletions(-) diff --git a/.watchmanconfig b/.watchmanconfig index 9e26dfee..0967ef42 100644 --- a/.watchmanconfig +++ b/.watchmanconfig @@ -1 +1 @@ -{} \ No newline at end of file +{} diff --git a/WebExample/app.json b/WebExample/app.json index bb70e18b..f0710443 100644 --- a/WebExample/app.json +++ b/WebExample/app.json @@ -1,30 +1,28 @@ { - "expo": { - "name": "WebExample", - "slug": "WebExample", - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/icon.png", - "userInterfaceStyle": "light", - "splash": { - "image": "./assets/splash.png", - "resizeMode": "contain", - "backgroundColor": "#ffffff" - }, - "assetBundlePatterns": [ - "**/*" - ], - "ios": { - "supportsTablet": true - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/adaptive-icon.png", - "backgroundColor": "#ffffff" - } - }, - "web": { - "favicon": "./assets/favicon.png" + "expo": { + "name": "WebExample", + "slug": "WebExample", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "assetBundlePatterns": ["**/*"], + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + } + }, + "web": { + "favicon": "./assets/favicon.png" + } } - } } diff --git a/WebExample/babel.config.js b/WebExample/babel.config.js index 054ccfa5..f04848b7 100644 --- a/WebExample/babel.config.js +++ b/WebExample/babel.config.js @@ -2,25 +2,21 @@ const path = require('path'); const pak = require('../package.json'); module.exports = function (api) { - api.cache(true); - return { - presets: ['babel-preset-expo'], - plugins: [ - [ - 'module-resolver', - { - extensions: ['.tsx', '.ts', '.js', '.json'], - alias: { - 'react': path.join(__dirname, 'node_modules', 'react'), - 'react-native': path.join( - __dirname, - 'node_modules', - 'react-native-web' - ), - [pak.name]: path.join(__dirname, '..', pak.source), - }, - }, - ], - ], - }; + api.cache(true); + return { + presets: ['babel-preset-expo'], + plugins: [ + [ + 'module-resolver', + { + extensions: ['.tsx', '.ts', '.js', '.json'], + alias: { + react: path.join(__dirname, 'node_modules', 'react'), + 'react-native': path.join(__dirname, 'node_modules', 'react-native-web'), + [pak.name]: path.join(__dirname, '..', pak.source), + }, + }, + ], + ], + }; }; diff --git a/WebExample/tsconfig.json b/WebExample/tsconfig.json index 30bff963..03728016 100644 --- a/WebExample/tsconfig.json +++ b/WebExample/tsconfig.json @@ -1,8 +1,8 @@ { - "extends": ["../tsconfig.json", "expo/tsconfig.base"], - "compilerOptions": { - "strict": true - }, - "include": ["App.tsx"], - "exclude": ["node_modules"] + "extends": ["../tsconfig.json", "expo/tsconfig.base"], + "compilerOptions": { + "strict": true + }, + "include": ["App.tsx"], + "exclude": ["node_modules"] } diff --git a/WebExample/webpack.config.js b/WebExample/webpack.config.js index 59c9eeda..0f8c0102 100644 --- a/WebExample/webpack.config.js +++ b/WebExample/webpack.config.js @@ -1,14 +1,14 @@ const createExpoWebpackConfigAsync = require('@expo/webpack-config'); module.exports = async function (env, argv) { - const config = await createExpoWebpackConfigAsync( - { - ...env, - babel: { - dangerouslyAddModulePathsToTranspile: ['react-native-live-markdown'], - }, - }, - argv - ); - return config; + const config = await createExpoWebpackConfigAsync( + { + ...env, + babel: { + dangerouslyAddModulePathsToTranspile: ['react-native-live-markdown'], + }, + }, + argv, + ); + return config; }; diff --git a/babel.config.js b/babel.config.js index 482f1edd..af929b44 100644 --- a/babel.config.js +++ b/babel.config.js @@ -12,4 +12,4 @@ module.exports = (api) => { return { presets: ['module:metro-react-native-babel-preset'], }; -}; \ No newline at end of file +}; diff --git a/parser/__tests__/index.test.js b/parser/__tests__/index.test.js index aaf8784e..e6858701 100644 --- a/parser/__tests__/index.test.js +++ b/parser/__tests__/index.test.js @@ -1,342 +1,335 @@ require('../react-native-live-markdown-parser.js'); expect.extend({ - toBeParsedAs(received, expectedRanges) { - const actualRanges = global.parseExpensiMarkToRanges(received); - if (JSON.stringify(actualRanges) !== JSON.stringify(expectedRanges)) { - return { - pass: false, - message: () => - `Expected ${JSON.stringify(expectedRanges)}, got ${JSON.stringify( - actualRanges - )}`, - }; - } - return { pass: true }; - }, + toBeParsedAs(received, expectedRanges) { + const actualRanges = global.parseExpensiMarkToRanges(received); + if (JSON.stringify(actualRanges) !== JSON.stringify(expectedRanges)) { + return { + pass: false, + message: () => `Expected ${JSON.stringify(expectedRanges)}, got ${JSON.stringify(actualRanges)}`, + }; + } + return {pass: true}; + }, }); test('empty string', () => { - expect('').toBeParsedAs([]); + expect('').toBeParsedAs([]); }); test('no formatting', () => { - expect('Hello, world!').toBeParsedAs([]); + expect('Hello, world!').toBeParsedAs([]); }); test('bold', () => { - expect('Hello, *world*!').toBeParsedAs([ - ['syntax', 7, 1], - ['bold', 8, 5], - ['syntax', 13, 1], - ]); + expect('Hello, *world*!').toBeParsedAs([ + ['syntax', 7, 1], + ['bold', 8, 5], + ['syntax', 13, 1], + ]); }); test('italic', () => { - expect('Hello, _world_!').toBeParsedAs([ - ['syntax', 7, 1], - ['italic', 8, 5], - ['syntax', 13, 1], - ]); + expect('Hello, _world_!').toBeParsedAs([ + ['syntax', 7, 1], + ['italic', 8, 5], + ['syntax', 13, 1], + ]); }); test('strikethrough', () => { - expect('Hello, ~world~!').toBeParsedAs([ - ['syntax', 7, 1], - ['strikethrough', 8, 5], - ['syntax', 13, 1], - ]); + expect('Hello, ~world~!').toBeParsedAs([ + ['syntax', 7, 1], + ['strikethrough', 8, 5], + ['syntax', 13, 1], + ]); }); describe('mention', () => { - test('normal', () => { - expect('@here Hello!').toBeParsedAs([['mention', 0, 5]]); - }); + test('normal', () => { + expect('@here Hello!').toBeParsedAs([['mention', 0, 5]]); + }); - test('with additional letters', () => { - expect('@herex').toBeParsedAs([]); - }); + test('with additional letters', () => { + expect('@herex').toBeParsedAs([]); + }); - test('with punctation marks', () => { - expect('@here!').toBeParsedAs([['mention', 0, 5]]); - }); + test('with punctation marks', () => { + expect('@here!').toBeParsedAs([['mention', 0, 5]]); + }); }); describe('mention-user', () => { - test('normal', () => { - expect('@mail@mail.com Hello!').toBeParsedAs([['mention-user', 0, 14]]); - }); + test('normal', () => { + expect('@mail@mail.com Hello!').toBeParsedAs([['mention-user', 0, 14]]); + }); - test('without top-level domain', () => { - expect('@mail@mail').toBeParsedAs([]); - }); + test('without top-level domain', () => { + expect('@mail@mail').toBeParsedAs([]); + }); - test('with punctation marks', () => { - expect('@mail@mail.com!').toBeParsedAs([['mention-user', 0, 14]]); - }); + test('with punctation marks', () => { + expect('@mail@mail.com!').toBeParsedAs([['mention-user', 0, 14]]); + }); }); test('plain link', () => { - expect('https://example.com').toBeParsedAs([['link', 0, 19]]); + expect('https://example.com').toBeParsedAs([['link', 0, 19]]); }); test('labeled link', () => { - expect('[Link](https://example.com)').toBeParsedAs([ - ['syntax', 0, 1], - ['syntax', 5, 2], - ['link', 7, 19], - ['syntax', 26, 1], - ]); + expect('[Link](https://example.com)').toBeParsedAs([ + ['syntax', 0, 1], + ['syntax', 5, 2], + ['link', 7, 19], + ['syntax', 26, 1], + ]); }); test('link with same label as href', () => { - expect('[https://example.com](https://example.com)').toBeParsedAs([ - ['syntax', 0, 1], - ['syntax', 20, 2], - ['link', 22, 19], - ['syntax', 41, 1], - ]); + expect('[https://example.com](https://example.com)').toBeParsedAs([ + ['syntax', 0, 1], + ['syntax', 20, 2], + ['link', 22, 19], + ['syntax', 41, 1], + ]); }); test('no nesting links while typing', () => { - expect('[link](www.google.com').toBeParsedAs([['link', 7, 14]]); + expect('[link](www.google.com').toBeParsedAs([['link', 7, 14]]); }); test('link with query string', () => { - expect('https://example.com?name=John&age=25&city=NewYork').toBeParsedAs([ - ['link', 0, 49], - ]); + expect('https://example.com?name=John&age=25&city=NewYork').toBeParsedAs([['link', 0, 49]]); }); test('plain email', () => { - expect('someone@example.com').toBeParsedAs([['link', 0, 19]]); + expect('someone@example.com').toBeParsedAs([['link', 0, 19]]); }); test('labeled email', () => { - expect('[Email](mailto:someone@example.com)').toBeParsedAs([ - ['syntax', 0, 1], - ['syntax', 6, 2], - ['link', 8, 26], - ['syntax', 34, 1], - ]); + expect('[Email](mailto:someone@example.com)').toBeParsedAs([ + ['syntax', 0, 1], + ['syntax', 6, 2], + ['link', 8, 26], + ['syntax', 34, 1], + ]); }); describe('email with same label as address', () => { - test('label and address without "mailto:"', () => { - expect('[someone@example.com](someone@example.com)').toBeParsedAs([ - ['syntax', 0, 1], - ['syntax', 20, 2], - ['link', 22, 19], - ['syntax', 41, 1], - ]); - }); - - test('label with "mailto:"', () => { - expect('[mailto:someone@example.com](someone@example.com)').toBeParsedAs([ - ['syntax', 0, 1], - ['syntax', 27, 2], - ['link', 29, 19], - ['syntax', 48, 1], - ]); - }); - - test('address with "mailto:"', () => { - expect('[someone@example.com](mailto:someone@example.com)').toBeParsedAs([ - ['syntax', 0, 1], - ['syntax', 20, 2], - ['link', 22, 26], - ['syntax', 48, 1], - ]); - }); - - test('label and address with "mailto:"', () => { - expect( - '[mailto:someone@example.com](mailto:someone@example.com)' - ).toBeParsedAs([ - ['syntax', 0, 1], - ['syntax', 27, 2], - ['link', 29, 26], - ['syntax', 55, 1], - ]); - }); + test('label and address without "mailto:"', () => { + expect('[someone@example.com](someone@example.com)').toBeParsedAs([ + ['syntax', 0, 1], + ['syntax', 20, 2], + ['link', 22, 19], + ['syntax', 41, 1], + ]); + }); + + test('label with "mailto:"', () => { + expect('[mailto:someone@example.com](someone@example.com)').toBeParsedAs([ + ['syntax', 0, 1], + ['syntax', 27, 2], + ['link', 29, 19], + ['syntax', 48, 1], + ]); + }); + + test('address with "mailto:"', () => { + expect('[someone@example.com](mailto:someone@example.com)').toBeParsedAs([ + ['syntax', 0, 1], + ['syntax', 20, 2], + ['link', 22, 26], + ['syntax', 48, 1], + ]); + }); + + test('label and address with "mailto:"', () => { + expect('[mailto:someone@example.com](mailto:someone@example.com)').toBeParsedAs([ + ['syntax', 0, 1], + ['syntax', 27, 2], + ['link', 29, 26], + ['syntax', 55, 1], + ]); + }); }); test('inline code', () => { - expect('Hello `world`!').toBeParsedAs([ - ['syntax', 6, 1], - ['code', 7, 5], - ['syntax', 12, 1], - ]); + expect('Hello `world`!').toBeParsedAs([ + ['syntax', 6, 1], + ['code', 7, 5], + ['syntax', 12, 1], + ]); }); test('codeblock', () => { - expect('```\nHello world!\n```').toBeParsedAs([ - ['syntax', 0, 3], - ['pre', 3, 14], - ['syntax', 17, 3], - ]); + expect('```\nHello world!\n```').toBeParsedAs([ + ['syntax', 0, 3], + ['pre', 3, 14], + ['syntax', 17, 3], + ]); }); describe('quote', () => { - test('with single space', () => { - expect('> Hello world!').toBeParsedAs([ - ['syntax', 0, 1], - ['blockquote', 0, 14], - ]); - }); + test('with single space', () => { + expect('> Hello world!').toBeParsedAs([ + ['syntax', 0, 1], + ['blockquote', 0, 14], + ]); + }); - test('with multiple spaces', () => { - expect('> Hello world!').toBeParsedAs([ - ['syntax', 0, 1], - ['blockquote', 0, 19], - ]); - }); + test('with multiple spaces', () => { + expect('> Hello world!').toBeParsedAs([ + ['syntax', 0, 1], + ['blockquote', 0, 19], + ]); + }); - test('without space', () => { - expect('>Hello world!').toBeParsedAs([ - ['syntax', 0, 1], - ['blockquote', 0, 13], - ]); - }); + test('without space', () => { + expect('>Hello world!').toBeParsedAs([ + ['syntax', 0, 1], + ['blockquote', 0, 13], + ]); + }); }); test('multiple blockquotes', () => { - expect('> Hello\n> beautiful\n> world').toBeParsedAs([ - ['syntax', 0, 1], - ['blockquote', 0, 7], - ['syntax', 8, 1], - ['blockquote', 8, 11], - ['syntax', 20, 1], - ['blockquote', 20, 7], - ]); + expect('> Hello\n> beautiful\n> world').toBeParsedAs([ + ['syntax', 0, 1], + ['blockquote', 0, 7], + ['syntax', 8, 1], + ['blockquote', 8, 11], + ['syntax', 20, 1], + ['blockquote', 20, 7], + ]); }); test('separate blockquotes', () => { - expect('> Lorem ipsum\ndolor\n> sit amet').toBeParsedAs([ - ['syntax', 0, 1], - ['blockquote', 0, 13], - ['syntax', 20, 1], - ['blockquote', 20, 10], - ]); + expect('> Lorem ipsum\ndolor\n> sit amet').toBeParsedAs([ + ['syntax', 0, 1], + ['blockquote', 0, 13], + ['syntax', 20, 1], + ['blockquote', 20, 10], + ]); }); test('heading', () => { - expect('# Hello world').toBeParsedAs([ - ['syntax', 0, 2], - ['h1', 2, 11], - ]); + expect('# Hello world').toBeParsedAs([ + ['syntax', 0, 2], + ['h1', 2, 11], + ]); }); test('nested bold and italic', () => { - expect('*_Hello_*, _*world*_!').toBeParsedAs([ - ['syntax', 0, 1], - ['syntax', 1, 1], - ['bold', 1, 7], - ['italic', 2, 5], - ['syntax', 7, 1], - ['syntax', 8, 1], - ['syntax', 11, 1], - ['syntax', 12, 1], - ['italic', 12, 7], - ['bold', 13, 5], - ['syntax', 18, 1], - ['syntax', 19, 1], - ]); -}); - -describe('nested heading in blockquote', () => { - test('without spaces', () => { - expect('># Hello world').toBeParsedAs([ - ['syntax', 0, 1], - ['blockquote', 0, 14], - ['syntax', 1, 2], - ['h1', 3, 11], - ]); - }); - - test('with single space', () => { - expect('> # Hello world').toBeParsedAs([ - ['syntax', 0, 1], - ['blockquote', 0, 15], - ['syntax', 2, 2], - ['h1', 4, 11], - ]); - }); - - test('with multiple spaces after #', () => { - expect('># Hello world').toBeParsedAs([ - ['syntax', 0, 1], - ['blockquote', 0, 17], - ['syntax', 1, 2], - ['h1', 3, 14], + expect('*_Hello_*, _*world*_!').toBeParsedAs([ + ['syntax', 0, 1], + ['syntax', 1, 1], + ['bold', 1, 7], + ['italic', 2, 5], + ['syntax', 7, 1], + ['syntax', 8, 1], + ['syntax', 11, 1], + ['syntax', 12, 1], + ['italic', 12, 7], + ['bold', 13, 5], + ['syntax', 18, 1], + ['syntax', 19, 1], ]); - }); }); -describe('trailing whitespace', () => { - describe('after blockquote', () => { - test('nothing', () => { - expect('> Hello world').toBeParsedAs([ - ['syntax', 0, 1], - ['blockquote', 0, 13], - ]); - }); - - test('single space', () => { - expect('> Hello world ').toBeParsedAs([ - ['syntax', 0, 1], - ['blockquote', 0, 14], - ]); - }); - - test('newline', () => { - expect('> Hello world\n').toBeParsedAs([ - ['syntax', 0, 1], - ['blockquote', 0, 13], - ]); - }); - }); - - describe('after heading', () => { - test('nothing', () => { - expect('# Hello world').toBeParsedAs([ - ['syntax', 0, 2], - ['h1', 2, 11], - ]); +describe('nested heading in blockquote', () => { + test('without spaces', () => { + expect('># Hello world').toBeParsedAs([ + ['syntax', 0, 1], + ['blockquote', 0, 14], + ['syntax', 1, 2], + ['h1', 3, 11], + ]); }); - test('single space', () => { - expect('# Hello world ').toBeParsedAs([ - ['syntax', 0, 2], - ['h1', 2, 12], - ]); + test('with single space', () => { + expect('> # Hello world').toBeParsedAs([ + ['syntax', 0, 1], + ['blockquote', 0, 15], + ['syntax', 2, 2], + ['h1', 4, 11], + ]); }); - test('multiple spaces', () => { - expect('# Hello world ').toBeParsedAs([ - ['syntax', 0, 2], - ['h1', 2, 14], - ]); + test('with multiple spaces after #', () => { + expect('># Hello world').toBeParsedAs([ + ['syntax', 0, 1], + ['blockquote', 0, 17], + ['syntax', 1, 2], + ['h1', 3, 14], + ]); }); +}); - test('newline', () => { - expect('# Hello world\n').toBeParsedAs([ - ['syntax', 0, 2], - ['h1', 2, 11], - ]); +describe('trailing whitespace', () => { + describe('after blockquote', () => { + test('nothing', () => { + expect('> Hello world').toBeParsedAs([ + ['syntax', 0, 1], + ['blockquote', 0, 13], + ]); + }); + + test('single space', () => { + expect('> Hello world ').toBeParsedAs([ + ['syntax', 0, 1], + ['blockquote', 0, 14], + ]); + }); + + test('newline', () => { + expect('> Hello world\n').toBeParsedAs([ + ['syntax', 0, 1], + ['blockquote', 0, 13], + ]); + }); }); - test('multiple quotes', () => { - expect('> # Hello\n> # world').toBeParsedAs([ - ['syntax', 0, 1], - ['blockquote', 0, 9], - ['syntax', 2, 2], - ['h1', 4, 5], - ['syntax', 10, 1], - ['blockquote', 10, 9], - ['syntax', 12, 2], - ['h1', 14, 5], - ]); + describe('after heading', () => { + test('nothing', () => { + expect('# Hello world').toBeParsedAs([ + ['syntax', 0, 2], + ['h1', 2, 11], + ]); + }); + + test('single space', () => { + expect('# Hello world ').toBeParsedAs([ + ['syntax', 0, 2], + ['h1', 2, 12], + ]); + }); + + test('multiple spaces', () => { + expect('# Hello world ').toBeParsedAs([ + ['syntax', 0, 2], + ['h1', 2, 14], + ]); + }); + + test('newline', () => { + expect('# Hello world\n').toBeParsedAs([ + ['syntax', 0, 2], + ['h1', 2, 11], + ]); + }); + + test('multiple quotes', () => { + expect('> # Hello\n> # world').toBeParsedAs([ + ['syntax', 0, 1], + ['blockquote', 0, 9], + ['syntax', 2, 2], + ['h1', 4, 5], + ['syntax', 10, 1], + ['blockquote', 10, 9], + ['syntax', 12, 2], + ['h1', 14, 5], + ]); + }); }); - }); }); diff --git a/parser/index.ts b/parser/index.ts index 843366de..0fc192b0 100644 --- a/parser/index.ts +++ b/parser/index.ts @@ -1,187 +1,178 @@ // @ts-ignore - to review how it's implemented in ExpensiMark -import { ExpensiMark } from 'expensify-common/lib/ExpensiMark'; +import {ExpensiMark} from 'expensify-common/lib/ExpensiMark'; import _ from 'underscore'; type Range = [string, number, number]; type Token = ['TEXT' | 'HTML', string]; -type StackItem = { tag: string; children: Array }; +type StackItem = {tag: string; children: Array}; function parseMarkdownToHTML(markdown: string): string { - const parser = ExpensiMark; - const html = parser.replace(markdown, { - shouldKeepRawInput: true, - }); - return html; + const parser = ExpensiMark; + const html = parser.replace(markdown, { + shouldKeepRawInput: true, + }); + return html; } function parseHTMLToTokens(html: string): Token[] { - const tokens: Token[] = []; - let left = 0; - // eslint-disable-next-line no-constant-condition - while (true) { - const open = html.indexOf('<', left); - if (open === -1) { - if (left < html.length) { - tokens.push(['TEXT', html.substring(left)]); - } - break; - } - if (open !== left) { - tokens.push(['TEXT', html.substring(left, open)]); - } - const close = html.indexOf('>', open); - if (close === -1) { - throw new Error('Invalid HTML: no matching ">"'); + const tokens: Token[] = []; + let left = 0; + // eslint-disable-next-line no-constant-condition + while (true) { + const open = html.indexOf('<', left); + if (open === -1) { + if (left < html.length) { + tokens.push(['TEXT', html.substring(left)]); + } + break; + } + if (open !== left) { + tokens.push(['TEXT', html.substring(left, open)]); + } + const close = html.indexOf('>', open); + if (close === -1) { + throw new Error('Invalid HTML: no matching ">"'); + } + tokens.push(['HTML', html.substring(open, close + 1)]); + left = close + 1; } - tokens.push(['HTML', html.substring(open, close + 1)]); - left = close + 1; - } - return tokens; + return tokens; } function parseTokensToTree(tokens: Token[]): StackItem { - const stack: StackItem[] = [{ tag: '<>', children: [] }]; - tokens.forEach(([type, payload]) => { - if (type === 'TEXT') { - const text = _.unescape(payload); - const top = stack[stack.length - 1]; - top!.children.push(text); - } else if (type === 'HTML') { - if (payload.startsWith('', children: []}]; + tokens.forEach(([type, payload]) => { + if (type === 'TEXT') { + const text = _.unescape(payload); + const top = stack[stack.length - 1]; + top!.children.push(text); + } else if (type === 'HTML') { + if (payload.startsWith('') { + function addChildrenWithStyle(node: StackItem | string, style: string) { + const start = text.length; processChildren(node); - } else if (node.tag === '') { - appendSyntax('*'); - addChildrenWithStyle(node, 'bold'); - appendSyntax('*'); - } else if (node.tag === '') { - appendSyntax('_'); - addChildrenWithStyle(node, 'italic'); - appendSyntax('_'); - } else if (node.tag === '') { - appendSyntax('~'); - addChildrenWithStyle(node, 'strikethrough'); - appendSyntax('~'); - } else if (node.tag === '') { - appendSyntax('`'); - addChildrenWithStyle(node, 'code'); - appendSyntax('`'); - } else if (node.tag === '') { - addChildrenWithStyle(node, 'mention'); - } else if (node.tag === '') { - addChildrenWithStyle(node, 'mention-user'); - } else if (node.tag === '
') { - appendSyntax('>'); - addChildrenWithStyle(node, 'blockquote'); - // compensate for "> " at the beginning - if (ranges.length > 0) { - const curr = ranges[ranges.length - 1]; - curr![1] -= 1; - curr![2] += 1; - } - } else if (node.tag === '

') { - appendSyntax('# '); - addChildrenWithStyle(node, 'h1'); - } else if (node.tag.startsWith('') { + processChildren(node); + } else if (node.tag === '') { + appendSyntax('*'); + addChildrenWithStyle(node, 'bold'); + appendSyntax('*'); + } else if (node.tag === '') { + appendSyntax('_'); + addChildrenWithStyle(node, 'italic'); + appendSyntax('_'); + } else if (node.tag === '') { + appendSyntax('~'); + addChildrenWithStyle(node, 'strikethrough'); + appendSyntax('~'); + } else if (node.tag === '') { + appendSyntax('`'); + addChildrenWithStyle(node, 'code'); + appendSyntax('`'); + } else if (node.tag === '') { + addChildrenWithStyle(node, 'mention'); + } else if (node.tag === '') { + addChildrenWithStyle(node, 'mention-user'); + } else if (node.tag === '
') { + appendSyntax('>'); + addChildrenWithStyle(node, 'blockquote'); + // compensate for "> " at the beginning + if (ranges.length > 0) { + const curr = ranges[ranges.length - 1]; + curr![1] -= 1; + curr![2] += 1; + } + } else if (node.tag === '

') { + appendSyntax('# '); + addChildrenWithStyle(node, 'h1'); + } else if (node.tag.startsWith(' a[1] - b[1]); // sort by location to properly handle bold+italic + return ranges.sort((a, b) => a[1] - b[1]); // sort by location to properly handle bold+italic } function parseExpensiMarkToRanges(markdown: string): Range[] { - const html = parseMarkdownToHTML(markdown); - const tokens = parseHTMLToTokens(html); - const tree = parseTokensToTree(tokens); - const [text, ranges] = parseTreeToTextAndRanges(tree); - if (text !== markdown) { - // text mismatch, don't return any ranges - return []; - } - const sortedRanges = sortRanges(ranges); - return sortedRanges; + const html = parseMarkdownToHTML(markdown); + const tokens = parseHTMLToTokens(html); + const tree = parseTokensToTree(tokens); + const [text, ranges] = parseTreeToTextAndRanges(tree); + if (text !== markdown) { + // text mismatch, don't return any ranges + return []; + } + const sortedRanges = sortRanges(ranges); + return sortedRanges; } globalThis.parseExpensiMarkToRanges = parseExpensiMarkToRanges; diff --git a/parser/tsconfig.json b/parser/tsconfig.json index 7d15e71f..751c83f9 100644 --- a/parser/tsconfig.json +++ b/parser/tsconfig.json @@ -1,28 +1,28 @@ { - "compilerOptions": { - "rootDir": ".", - "paths": { - "@expensify/react-native-live-markdown": ["./src/index"] + "compilerOptions": { + "rootDir": ".", + "paths": { + "@expensify/react-native-live-markdown": ["./src/index"] + }, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "lib": ["esnext", "dom"], + "module": "esnext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "esnext", + "verbatimModuleSyntax": true }, - "allowUnreachableCode": false, - "allowUnusedLabels": false, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "lib": ["esnext", "dom"], - "module": "esnext", - "moduleResolution": "node", - "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true, - "noImplicitUseStrict": false, - "noStrictGenericChecks": false, - "noUncheckedIndexedAccess": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true, - "target": "esnext", - "verbatimModuleSyntax": true - }, - "include": ["./**/*", "../types/global.d.ts"] + "include": ["./**/*", "../types/global.d.ts"] } diff --git a/scripts/pod-install.cjs b/scripts/pod-install.cjs index 654c9229..51ea3ab8 100644 --- a/scripts/pod-install.cjs +++ b/scripts/pod-install.cjs @@ -1,40 +1,32 @@ const child_process = require('child_process'); module.exports = { - name: 'pod-install', - factory() { - return { - hooks: { - afterAllInstalled(project, options) { - if (process.env.POD_INSTALL === '0') { - return; - } + name: 'pod-install', + factory() { + return { + hooks: { + afterAllInstalled(project, options) { + if (process.env.POD_INSTALL === '0') { + return; + } - if ( - options && - (options.mode === 'update-lockfile' || - options.mode === 'skip-build') - ) { - return; - } + if (options && (options.mode === 'update-lockfile' || options.mode === 'skip-build')) { + return; + } - const result = child_process.spawnSync( - 'yarn', - ['pod-install', 'example/ios'], - { - cwd: project.cwd, - env: process.env, - stdio: 'inherit', - encoding: 'utf-8', - shell: true, - } - ); + const result = child_process.spawnSync('yarn', ['pod-install', 'example/ios'], { + cwd: project.cwd, + env: process.env, + stdio: 'inherit', + encoding: 'utf-8', + shell: true, + }); - if (result.status !== 0) { - throw new Error('Failed to run pod-install'); - } - }, - }, - }; - }, + if (result.status !== 0) { + throw new Error('Failed to run pod-install'); + } + }, + }, + }; + }, }; diff --git a/src/MarkdownTextInput.tsx b/src/MarkdownTextInput.tsx index 58d2fe7b..409853b6 100644 --- a/src/MarkdownTextInput.tsx +++ b/src/MarkdownTextInput.tsx @@ -1,123 +1,120 @@ -import { StyleSheet, TextInput, processColor } from 'react-native'; +import React from 'react'; +import {StyleSheet, TextInput, processColor} from 'react-native'; +import type {TextInputProps} from 'react-native'; -import type { MarkdownStyle } from './MarkdownTextInputDecoratorViewNativeComponent'; +import type * as MarkdownTextInputDecoractorView from './MarkdownTextInputDecoratorViewNativeComponent'; import MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent'; -import React from 'react'; -import type { TextInputProps } from 'react-native'; + +type MarkdownStyle = MarkdownTextInputDecoractorView.MarkdownStyle; function makeDefaultMarkdownStyle(): MarkdownStyle { - return { - syntax: { - color: 'gray', - }, - link: { - color: 'blue', - }, - h1: { - fontSize: 25, - }, - quote: { - borderColor: 'gray', - borderWidth: 6, - marginLeft: 6, - paddingLeft: 6, - }, - code: { - color: 'black', - backgroundColor: 'lightgray', - }, - pre: { - color: 'black', - backgroundColor: 'lightgray', - }, - mentionHere: { - backgroundColor: 'yellow', - }, - mentionUser: { - backgroundColor: 'cyan', - }, - }; + return { + syntax: { + color: 'gray', + }, + link: { + color: 'blue', + }, + h1: { + fontSize: 25, + }, + quote: { + borderColor: 'gray', + borderWidth: 6, + marginLeft: 6, + paddingLeft: 6, + }, + code: { + color: 'black', + backgroundColor: 'lightgray', + }, + pre: { + color: 'black', + backgroundColor: 'lightgray', + }, + mentionHere: { + backgroundColor: 'yellow', + }, + mentionUser: { + backgroundColor: 'cyan', + }, + }; } -export type PartialMarkdownStyle = Partial<{ - [K in keyof MarkdownStyle]: Partial; +type PartialMarkdownStyle = Partial<{ + [K in keyof MarkdownStyle]: Partial; }>; -function mergeMarkdownStyleWithDefault( - input: PartialMarkdownStyle | undefined -): MarkdownStyle { - const output = makeDefaultMarkdownStyle(); - - if (input !== undefined) { - for (const key in input) { - if (key in output) { - Object.assign( - output[key as keyof MarkdownStyle], - input[key as keyof MarkdownStyle] - ); - } +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; + return output; } function processColorsInMarkdownStyle(input: MarkdownStyle): MarkdownStyle { - const output = JSON.parse(JSON.stringify(input)); - - for (const key in output) { - const obj = output[key]; - for (const prop in obj) { - // TODO: use ReactNativeStyleAttributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes' - if (prop === 'color' || prop.endsWith('Color')) { - obj[prop] = processColor(obj[prop]); - } - } - } - - return output; + const output = JSON.parse(JSON.stringify(input)); + + Object.keys(output).forEach((key) => { + const obj = output[key]; + Object.keys(obj).forEach((prop) => { + // TODO: use ReactNativeStyleAttributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes' + if (!(prop === 'color' || prop.endsWith('Color'))) { + return; + } + obj[prop] = processColor(obj[prop]); + }); + }); + + return output; } -function processMarkdownStyle( - input: PartialMarkdownStyle | undefined -): MarkdownStyle { - return processColorsInMarkdownStyle(mergeMarkdownStyleWithDefault(input)); +function processMarkdownStyle(input: PartialMarkdownStyle | undefined): MarkdownStyle { + return processColorsInMarkdownStyle(mergeMarkdownStyleWithDefault(input)); } -export interface MarkdownTextInputProps extends TextInputProps { - markdownStyle?: PartialMarkdownStyle; +interface MarkdownTextInputProps extends TextInputProps { + markdownStyle?: PartialMarkdownStyle; } -const MarkdownTextInput = React.forwardRef( - (props, ref) => { +const MarkdownTextInput = React.forwardRef((props, ref) => { const IS_FABRIC = 'nativeFabricUIManager' in global; - const markdownStyle = React.useMemo( - () => processMarkdownStyle(props.markdownStyle), - [props.markdownStyle] - ); + const markdownStyle = React.useMemo(() => processMarkdownStyle(props.markdownStyle), [props.markdownStyle]); return ( - <> - - - + <> + + + ); - } -); +}); const styles = StyleSheet.create({ - displayNone: { - display: 'none', - }, - farAway: { - position: 'absolute', - top: 1e8, - left: 1e8, - }, + displayNone: { + display: 'none', + }, + farAway: { + position: 'absolute', + top: 1e8, + left: 1e8, + }, }); +export type {PartialMarkdownStyle as MarkdownStyle, MarkdownTextInputProps}; + export default MarkdownTextInput; diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index 8bc42fe0..2dea7438 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -1,17 +1,22 @@ -import { TextInput } from 'react-native'; +import {TextInput} from 'react-native'; import React from 'react'; -import type { TextInputProps } from 'react-native'; +import type {TextInputProps} from 'react-native'; -export interface MarkdownTextInputProps extends TextInputProps { - // nothing here +interface MarkdownTextInputProps extends TextInputProps { + // nothing here } -const MarkdownTextInput = React.forwardRef( - (props, ref) => { +const MarkdownTextInput = React.forwardRef((props, ref) => { // TODO: add web implementation here - return ; - } -); + return ( + + ); +}); export default MarkdownTextInput; + +export type {MarkdownTextInputProps}; diff --git a/src/MarkdownTextInputDecoratorViewNativeComponent.ts b/src/MarkdownTextInputDecoratorViewNativeComponent.ts index f5c3a344..96e958c4 100644 --- a/src/MarkdownTextInputDecoratorViewNativeComponent.ts +++ b/src/MarkdownTextInputDecoratorViewNativeComponent.ts @@ -1,44 +1,44 @@ -import type { ColorValue, ViewProps } from 'react-native'; +import type {ColorValue, ViewProps} from 'react-native'; -import type { Float } from 'react-native/Libraries/Types/CodegenTypes'; +import type {Float} from 'react-native/Libraries/Types/CodegenTypes'; import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; -export interface MarkdownStyle { - syntax: { - color: ColorValue; - }; - link: { - color: ColorValue; - }; - h1: { - fontSize: Float; - }; - quote: { - borderColor: ColorValue; - borderWidth: Float; - marginLeft: Float; - paddingLeft: Float; - }; - code: { - color: ColorValue; - backgroundColor: ColorValue; - }; - pre: { - color: ColorValue; - backgroundColor: ColorValue; - }; - mentionHere: { - backgroundColor: ColorValue; - }; - mentionUser: { - backgroundColor: ColorValue; - }; +interface MarkdownStyle { + syntax: { + color: ColorValue; + }; + link: { + color: ColorValue; + }; + h1: { + fontSize: Float; + }; + quote: { + borderColor: ColorValue; + borderWidth: Float; + marginLeft: Float; + paddingLeft: Float; + }; + code: { + color: ColorValue; + backgroundColor: ColorValue; + }; + pre: { + color: ColorValue; + backgroundColor: ColorValue; + }; + mentionHere: { + backgroundColor: ColorValue; + }; + mentionUser: { + backgroundColor: ColorValue; + }; } interface NativeProps extends ViewProps { - markdownStyle: MarkdownStyle; + markdownStyle: MarkdownStyle; } -export default codegenNativeComponent( - 'MarkdownTextInputDecoratorView' -); +export default codegenNativeComponent('MarkdownTextInputDecoratorView'); + +export type {MarkdownStyle}; diff --git a/src/index.tsx b/src/index.tsx index f26b5cc3..7e1e7507 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,2 @@ -export { default as MarkdownTextInput } from './MarkdownTextInput'; -export type { - MarkdownTextInputProps, - PartialMarkdownStyle as MarkdownStyle, -} from './MarkdownTextInput'; +export {default as MarkdownTextInput} from './MarkdownTextInput'; +export type {MarkdownTextInputProps, MarkdownStyle} from './MarkdownTextInput'; diff --git a/tsconfig.json b/tsconfig.json index 048afe70..07d9780a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,31 +1,31 @@ { - "compilerOptions": { - "rootDir": ".", - "paths": { - "@expensify/react-native-live-markdown": ["./src/index"] + "compilerOptions": { + "rootDir": ".", + "paths": { + "@expensify/react-native-live-markdown": ["./src/index"] + }, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "esnext", + "verbatimModuleSyntax": true, + "typeRoots": ["node_modules/@types"] }, - "allowUnreachableCode": false, - "allowUnusedLabels": false, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "jsx": "react", - "lib": ["esnext"], - "module": "esnext", - "moduleResolution": "node", - "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true, - "noImplicitUseStrict": false, - "noStrictGenericChecks": false, - "noUncheckedIndexedAccess": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true, - "target": "esnext", - "verbatimModuleSyntax": true, - "typeRoots": ["node_modules/@types"], - }, - "include": ["src/**/*", "types/global.d.ts"], - "exclude": ["**/node_modules/**/*", "parser/**/*", "**/lib/**/*", "**/__tests__/**/*", "example/src/**/*", "WebExample/**/*"], + "include": ["src/**/*", "types/global.d.ts"], + "exclude": ["**/node_modules/**/*", "parser/**/*", "**/lib/**/*", "**/__tests__/**/*", "example/src/**/*", "WebExample/**/*"] } diff --git a/turbo.json b/turbo.json index 331e2890..8c5093ec 100644 --- a/turbo.json +++ b/turbo.json @@ -1,34 +1,24 @@ { - "$schema": "https://turbo.build/schema.json", - "pipeline": { - "build:android": { - "inputs": [ - "package.json", - "android", - "!android/build", - "src/*.ts", - "src/*.tsx", - "example/package.json", - "example/android", - "!example/android/.gradle", - "!example/android/build", - "!example/android/app/build" - ], - "outputs": [] - }, - "build:ios": { - "inputs": [ - "package.json", - "*.podspec", - "ios", - "src/*.ts", - "src/*.tsx", - "example/package.json", - "example/ios", - "!example/ios/build", - "!example/ios/Pods" - ], - "outputs": [] + "$schema": "https://turbo.build/schema.json", + "pipeline": { + "build:android": { + "inputs": [ + "package.json", + "android", + "!android/build", + "src/*.ts", + "src/*.tsx", + "example/package.json", + "example/android", + "!example/android/.gradle", + "!example/android/build", + "!example/android/app/build" + ], + "outputs": [] + }, + "build:ios": { + "inputs": ["package.json", "*.podspec", "ios", "src/*.ts", "src/*.tsx", "example/package.json", "example/ios", "!example/ios/build", "!example/ios/Pods"], + "outputs": [] + } } - } } diff --git a/types/global.d.ts b/types/global.d.ts index ef4df91c..73399674 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -3,6 +3,6 @@ export {}; type Range = [string, number, number]; // style, location, length declare global { - // eslint-disable-next-line no-var - var parseExpensiMarkToRanges: (markdown: string) => Range[]; + // eslint-disable-next-line no-var + var parseExpensiMarkToRanges: (markdown: string) => Range[]; }