From 2cb1642dbd30bc737d91f61d327233325c30f8a5 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: 2025年10月22日 13:30:40 +0200 Subject: [PATCH 1/3] chore: add preview of markdown lib fork for testing --- examples/SampleApp/yarn.lock | 12 + package/package.json | 4 +- .../MessageSimple/utils/markdown/index.tsx | 65 + .../MessageSimple/utils/markdown/rules.ts | 1078 +++++++++++++++++ .../MessageSimple/utils/markdown/styles.ts | 177 +++ .../MessageSimple/utils/renderText.tsx | 17 +- .../messagesContext/MessagesContext.tsx | 2 +- package/yarn.lock | 41 +- 8 files changed, 1357 insertions(+), 39 deletions(-) create mode 100644 package/src/components/Message/MessageSimple/utils/markdown/index.tsx create mode 100644 package/src/components/Message/MessageSimple/utils/markdown/rules.ts create mode 100644 package/src/components/Message/MessageSimple/utils/markdown/styles.ts diff --git a/examples/SampleApp/yarn.lock b/examples/SampleApp/yarn.lock index b95479fc13..2ea2cb15fe 100644 --- a/examples/SampleApp/yarn.lock +++ b/examples/SampleApp/yarn.lock @@ -2041,6 +2041,18 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@khanacademy/perseus-utils@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@khanacademy/perseus-utils/-/perseus-utils-2.1.0.tgz#a405f5e740e73a1345f2b172ff34260b6d87f57e" + integrity sha512-QgN9qW1hnoCGkErSVwCJ5SmWAqlcgXA3TvtMjsyv8LiYbAS4/ZMQOHIuq5P7u0t1i6tp02w7Qjbz/COsxyHg/A== + +"@khanacademy/simple-markdown@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@khanacademy/simple-markdown/-/simple-markdown-2.1.0.tgz#7fcbbcb4d0eda7d0dfb09c2ae35f0b9eb1e07dcf" + integrity sha512-U9yemDLqP3ehAhUdBhzVfZe9h10wWbkrIkfFf2ejSO/cUSFZwjt+KRfZtZK6q2HY+RkNYUjALDpwJu7LkrY1gw== + dependencies: + "@khanacademy/perseus-utils" "2.1.0" + "@napi-rs/wasm-runtime@^0.2.11": version "0.2.11" resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz#192c1610e1625048089ab4e35bc0649ce478500e" diff --git a/package/package.json b/package/package.json index a892d446d7..888b46af8e 100644 --- a/package/package.json +++ b/package/package.json @@ -68,6 +68,7 @@ }, "dependencies": { "@gorhom/bottom-sheet": "^5.1.8", + "@khanacademy/simple-markdown": "^2.1.0", "@ungap/structured-clone": "^1.3.0", "dayjs": "1.11.13", "emoji-regex": "^10.4.0", @@ -77,7 +78,6 @@ "lodash-es": "4.17.21", "mime-types": "^2.1.35", "path": "0.12.7", - "react-native-markdown-package": "1.8.2", "react-native-url-polyfill": "^2.0.0", "stream-chat": "^9.23.0", "use-sync-external-store": "^1.5.0" @@ -112,11 +112,11 @@ "@babel/core": "^7.27.4", "@babel/runtime": "^7.27.6", "@op-engineering/op-sqlite": "^14.0.3", - "@shopify/flash-list": "^2.1.0", "@react-native-community/eslint-config": "3.2.0", "@react-native-community/eslint-plugin": "1.3.0", "@react-native-community/netinfo": "^11.4.1", "@react-native/babel-preset": "0.79.3", + "@shopify/flash-list": "^2.1.0", "@testing-library/jest-native": "^5.4.3", "@testing-library/react-native": "13.2.0", "@types/better-sqlite3": "^7.6.13", diff --git a/package/src/components/Message/MessageSimple/utils/markdown/index.tsx b/package/src/components/Message/MessageSimple/utils/markdown/index.tsx new file mode 100644 index 0000000000..c4cc0f692d --- /dev/null +++ b/package/src/components/Message/MessageSimple/utils/markdown/index.tsx @@ -0,0 +1,65 @@ +import React, { PropsWithChildren, useMemo } from 'react'; +import { View } from 'react-native'; + +import SimpleMarkdown, { + OutputRules, + ParserRules, + ReactOutputRule, +} from '@khanacademy/simple-markdown'; +import { isArray, isEqual, merge } from 'lodash'; + +import { getLocalRules } from './rules'; +import styles from './styles'; + +import { MarkdownStyle } from '../../../../../contexts'; + +type DefaultRules = typeof SimpleMarkdown.defaultRules; + +export type MarkdownRules = Partial; + +export type MarkdownProps = { + onLink: (url: string) => Promise; + rules: MarkdownRules; + styles: MarkdownStyle; +}; + +export type MarkdownOptions = Partial>; + +const Markdown = (props: PropsWithChildren) => { + const { onLink, rules: rulesProp, styles: stylesProp, children } = props; + + const mergedStyles = useMemo(() => merge({}, styles, stylesProp), [stylesProp]); + const localRules = useMemo( + () => + merge( + {}, + SimpleMarkdown.defaultRules, + { ...SimpleMarkdown.defaultRules }, + getLocalRules(mergedStyles, { onLink }), + rulesProp, + ) as unknown as ParserRules, + [mergedStyles, onLink, rulesProp], + ); + + const parser = useMemo(() => SimpleMarkdown.parserFor(localRules), [localRules]); + const renderer = useMemo( + () => SimpleMarkdown.outputFor(localRules as unknown as OutputRules, 'react'), + [localRules], + ); + + const childText = useMemo(() => (isArray(children) ? children.join('') : children), [children]); + + const toRender = useMemo(() => { + const blockSource = `${childText ?? ''}\n\n`; + return parser(blockSource, { inline: false }); + }, [childText, parser]); + + const tree = useMemo(() => renderer(toRender), [renderer, toRender]); + + return {tree}; +}; + +const areEqual = (prevProps: PropsWithChildren, nextProps: PropsWithChildren) => + isEqual(prevProps.children, nextProps.children); + +export default React.memo(Markdown, areEqual); diff --git a/package/src/components/Message/MessageSimple/utils/markdown/rules.ts b/package/src/components/Message/MessageSimple/utils/markdown/rules.ts new file mode 100644 index 0000000000..6e6f7e9905 --- /dev/null +++ b/package/src/components/Message/MessageSimple/utils/markdown/rules.ts @@ -0,0 +1,1078 @@ +// import React from 'react'; +// +// import { Text, View } from 'react-native'; +// +// import SimpleMarkdown from '@khanacademy/simple-markdown'; +// import { head, includes, map, noop, size, some } from 'lodash'; +// +// import { MarkdownStyle } from '../../../../../contexts'; +// +// import { MarkdownOptions } from './index'; +// +// export const getLocalRules = (styles: MarkdownStyle, opts: MarkdownOptions = {}) => { +// const LINK_INSIDE = '(?:\\[[^\\]]*\\]|[^\\]]|\\](?=[^\\[]*\\]))*'; +// const LINK_HREF_AND_TITLE = '\\s*?(?:\\s+[\'"]([\\s\\S]*?)[\'"])?\\s*'; +// const pressHandler = function (target) { +// if (opts.onLink) { +// opts.onLink(target).catch(function (error) { +// console.log('There has been a problem with this action. ' + error.message); +// throw error; +// }); +// } +// }; +// const parseInline = function (parse, content, state) { +// const isCurrentlyInline = state.inline || false; +// state.inline = true; +// const result = parse(content, state); +// state.inline = isCurrentlyInline; +// return result; +// }; +// const parseCaptureInline = function (capture, parse, state) { +// return { +// content: parseInline(parse, capture[2], state), +// }; +// }; +// return { +// autolink: { +// react(node, output, { ...state }) { +// state.withinText = true; +// const _pressHandler = () => { +// pressHandler(node.target); +// }; +// return React.createElement( +// Text, +// { +// key: state.key, +// style: styles.autolink, +// onPress: _pressHandler, +// }, +// output(node.content, state), +// ); +// }, +// }, +// blockQuote: { +// react(node, output, { ...state }) { +// state.withinQuote = true; +// +// const img = React.createElement(View, { +// key: state.key - state.key, +// style: [styles.blockQuoteSectionBar, styles.blockQuoteBar], +// }); +// +// const blockQuote = React.createElement( +// Text, +// { +// key: state.key, +// style: styles.blockQuoteText, +// }, +// output(node.content, state), +// ); +// +// return React.createElement( +// View, +// { +// key: state.key, +// style: [styles.blockQuoteSection, styles.blockQuoteText], +// }, +// [img, blockQuote], +// ); +// }, +// }, +// br: { +// react(node, output, { ...state }) { +// return React.createElement( +// Text, +// { +// key: state.key, +// style: styles.br, +// }, +// '\n\n', +// ); +// }, +// }, +// codeBlock: { +// react(node, output, { ...state }) { +// state.withinText = true; +// return React.createElement( +// Text, +// { +// key: state.key, +// style: styles.codeBlock, +// }, +// node.content, +// ); +// }, +// }, +// del: { +// react(node, output, { ...state }) { +// state.withinText = true; +// return React.createElement( +// Text, +// { +// key: state.key, +// style: styles.del, +// }, +// output(node.content, state), +// ); +// }, +// }, +// em: { +// react(node, output, { ...state }) { +// state.withinText = true; +// state.style = { +// ...(state.style || {}), +// ...styles.em, +// }; +// return React.createElement( +// Text, +// { +// key: state.key, +// style: styles.em, +// }, +// output(node.content, state), +// ); +// }, +// }, +// heading: { +// match: SimpleMarkdown.blockRegex(/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n *)+/), +// react(node, output, { ...state }) { +// // const newState = {...state}; +// state.withinText = true; +// state.withinHeading = true; +// +// state.style = { +// ...(state.style || {}), +// ...styles[`heading${node.level}`], +// }; +// +// const ret = React.createElement( +// Text, +// { +// key: state.key, +// style: state.style, +// }, +// output(node.content, state), +// ); +// return ret; +// }, +// }, +// hr: { +// react(node, output, { ...state }) { +// return React.createElement(View, { key: state.key, style: styles.hr }); +// }, +// }, +// image: { +// match: () => null, +// }, +// inlineCode: { +// parse: parseCaptureInline, +// react(node, output, { ...state }) { +// state.withinText = true; +// return React.createElement( +// Text, +// { +// key: state.key, +// style: styles.inlineCode, +// }, +// output(node.content, state), +// ); +// }, +// }, +// link: { +// match: SimpleMarkdown.inlineRegex( +// new RegExp('^\\[(' + LINK_INSIDE + ')\\]\\(' + LINK_HREF_AND_TITLE + '\\)'), +// ), +// react(node, output, { ...state }) { +// state.withinLink = true; +// const _pressHandler = () => { +// pressHandler(node.target); +// }; +// const link = React.createElement( +// Text, +// { +// key: state.key, +// style: styles.autolink, +// onPress: _pressHandler, +// }, +// output(node.content, state), +// ); +// state.withinLink = false; +// return link; +// }, +// }, +// list: { +// react(node, output, { ...state }) { +// let numberIndex = 1; +// const items = map(node.items, function (item, i) { +// let bullet; +// state.withinList = false; +// +// if (node.ordered) { +// bullet = React.createElement( +// Text, +// { key: 0, style: [styles.text, styles.listItemNumber] }, +// numberIndex + '. ', +// ); +// } else { +// bullet = React.createElement( +// Text, +// { key: 0, style: [styles.text, styles.listItemBullet] }, +// '\u2022 ', +// ); +// } +// +// if (item.length> 1) { +// if (item[1].type == 'list') { +// state.withinList = true; +// } +// } +// +// const content = output(item, state); +// let listItem; +// if ( +// includes(['text', 'paragraph', 'strong'], (head(item) || {}).type) && +// state.withinList == false +// ) { +// state.withinList = true; +// listItem = React.createElement( +// Text, +// { +// style: [styles.listItemText, { marginBottom: 0 }], +// key: 1, +// }, +// content, +// ); +// } else { +// listItem = React.createElement( +// View, +// { +// style: styles.listItemText, +// key: 1, +// }, +// content, +// ); +// } +// state.withinList = false; +// numberIndex++; +// +// return React.createElement( +// View, +// { +// key: i, +// style: styles.listRow, +// }, +// [bullet, listItem], +// ); +// }); +// +// return React.createElement(View, { key: state.key, style: styles.list }, items); +// }, +// }, +// sublist: { +// react(node, output, { ...state }) { +// const items = map(node.items, function (item, i) { +// let bullet; +// if (node.ordered) { +// bullet = React.createElement( +// Text, +// { key: 0, style: [styles.text, styles.listItemNumber] }, +// i + 1 + '. ', +// ); +// } else { +// bullet = React.createElement( +// Text, +// { key: 0, style: [styles.text, styles.listItemBullet] }, +// '\u2022 ', +// ); +// } +// +// const content = output(item, state); +// let listItem; +// state.withinList = true; +// if (includes(['text', 'paragraph', 'strong'], (head(item) || {}).type)) { +// listItem = React.createElement( +// Text, +// { +// style: styles.listItemText, +// key: 1, +// }, +// content, +// ); +// } else { +// listItem = React.createElement( +// View, +// { +// style: styles.listItem, +// key: 1, +// }, +// content, +// ); +// } +// state.withinList = false; +// return React.createElement( +// View, +// { +// key: i, +// style: styles.listRow, +// }, +// [bullet, listItem], +// ); +// }); +// +// return React.createElement(View, { key: state.key, style: styles.sublist }, items); +// }, +// }, +// mailto: { +// react(node, output, { ...state }) { +// state.withinText = true; +// return React.createElement( +// Text, +// { +// key: state.key, +// style: styles.mailto, +// onPress: noop, +// }, +// output(node.content, state), +// ); +// }, +// }, +// newline: { +// react(node, output, { ...state }) { +// return React.createElement( +// Text, +// { +// key: state.key, +// style: styles.newline, +// }, +// '\n', +// ); +// }, +// }, +// paragraph: { +// react(node, output, { ...state }) { +// let paragraphStyle = styles.paragraph; +// // Allow image to drop in next line within the paragraph +// if (some(node.content, { type: 'image' })) { +// state.withinParagraphWithImage = true; +// const paragraph = React.createElement( +// View, +// { +// key: state.key, +// style: styles.paragraphWithImage, +// }, +// output(node.content, state), +// ); +// state.withinParagraphWithImage = false; +// return paragraph; +// } else if (size(node.content) < 3 && some(node.content, { type: 'strong' })) { +// // align to center for Strong only content +// // require a check of content array size below 3, +// // as parse will include additional space as `text` +// paragraphStyle = styles.paragraphCenter; +// } +// if (state.withinList) { +// paragraphStyle = [paragraphStyle, styles.noMargin]; +// } +// return React.createElement( +// Text, +// { +// key: state.key, +// style: paragraphStyle, +// }, +// output(node.content, state), +// ); +// }, +// }, +// strong: { +// react(node, output, { ...state }) { +// state.withinText = true; +// state.style = { +// ...(state.style || {}), +// ...styles.strong, +// }; +// return React.createElement( +// Text, +// { +// key: state.key, +// style: state.style, +// }, +// output(node.content, state), +// ); +// }, +// }, +// table: { +// react(node, output, { ...state }) { +// const headers = map(node.header, function (content, i) { +// return React.createElement( +// Text, +// { +// key: i, +// style: styles.tableHeaderCell, +// }, +// output(content, state), +// ); +// }); +// +// const header = React.createElement(View, { key: -1, style: styles.tableHeader }, headers); +// +// const rows = map(node.cells, function (row, r) { +// const cells = map(row, function (content, c) { +// return React.createElement( +// View, +// { +// key: c, +// style: styles.tableRowCell, +// }, +// output(content, state), +// ); +// }); +// const rowStyles = [styles.tableRow]; +// if (node.cells.length - 1 == r) { +// rowStyles.push(styles.tableRowLast); +// } +// return React.createElement(View, { key: r, style: rowStyles }, cells); +// }); +// +// return React.createElement(View, { key: state.key, style: styles.table }, [header, rows]); +// }, +// }, +// text: { +// react(node, output, { ...state }) { +// let textStyle = { +// ...styles.text, +// ...(state.style || {}), +// }; +// +// if (state.withinLink) { +// textStyle = [styles.text, styles.autolink]; +// } +// +// if (state.withinQuote) { +// textStyle = [styles.text, styles.blockQuoteText]; +// } +// +// return React.createElement( +// Text, +// { +// key: state.key, +// style: textStyle, +// }, +// node.content, +// ); +// }, +// }, +// u: { +// // u will to the same as strong, to avoid the View nested inside text problem +// react(node, output, { ...state }) { +// state.withinText = true; +// state.style = { +// ...(state.style || {}), +// ...styles.u, +// }; +// return React.createElement( +// Text, +// { +// key: state.key, +// style: styles.strong, +// }, +// output(node.content, state), +// ); +// }, +// }, +// url: { +// react(node, output, { ...state }) { +// state.withinText = true; +// const _pressHandler = () => { +// pressHandler(node.target); +// }; +// return React.createElement( +// Text, +// { +// key: state.key, +// style: styles.autolink, +// onPress: _pressHandler, +// }, +// output(node.content, state), +// ); +// }, +// }, +// }; +// }; + +import React from 'react'; +import { Text, TextStyle, View, ViewStyle } from 'react-native'; + +import SimpleMarkdown, { + MatchFunction, + Output, + OutputRules, + ParseFunction, + Parser, + ReactOutputRule, + SingleASTNode, + State, +} from '@khanacademy/simple-markdown'; +import { head, includes, map, noop, size, some } from 'lodash'; + +import { MarkdownStyle } from '../../../../../contexts'; + +import { MarkdownOptions } from './index'; + +type MarkdownStyleProp = TextStyle | ViewStyle; + +type MarkdownState = State & { + key: number | string; + inline?: boolean; + withinText?: boolean; + withinQuote?: boolean; + withinHeading?: boolean; + withinLink?: boolean; + withinList?: boolean; + withinParagraphWithImage?: boolean; + style: MarkdownStyleProp; +}; + +type NodeWithContent = SingleASTNode & { content: SingleASTNode[] }; +type NodeWithStringContent = SingleASTNode & { content: string }; +type HeadingNode = SingleASTNode & { level: number; content: SingleASTNode[] }; +type ListNode = SingleASTNode & { + ordered: boolean; + items: SingleASTNode[] | SingleASTNode[][]; +}; +type TableNode = SingleASTNode & { + header: SingleASTNode[]; + cells: SingleASTNode[][]; +}; +type TargetNode = SingleASTNode & { target: string }; + +// Allow dynamic heading style access like styles["heading1"] +type HeadingStyles = Record; + +export const getLocalRules = ( + styles: MarkdownStyle, + opts: MarkdownOptions = {}, +): OutputRules => { + const LINK_INSIDE = '(?:\\[[^\\]]*\\]|[^\\]]|\\](?=[^\\[]*\\]))*'; + const LINK_HREF_AND_TITLE = '\\s*?(?:\\s+[\'"]([\\s\\S]*?)[\'"])?\\s*'; + + const pressHandler = (target: string) => { + if (opts.onLink) { + // user-supplied handler may be async; we keep your behavior + Promise.resolve(opts.onLink(target)).catch((error: unknown) => { + const msg = + error && typeof error === 'object' && 'toString' in error + ? String(error) + : 'Unknown error'; + + console.log('There has been a problem with this action. ' + msg); + throw error; + }); + } + }; + + const parseInline = function ( + parse: Parser, + content: string, + state: MarkdownState, + ): SingleASTNode[] { + const isCurrentlyInline = state.inline || false; + state.inline = true; + const result = parse(content, state); + state.inline = isCurrentlyInline; + return result; + }; + + const parseCaptureInline: ParseFunction = (capture, parse, state) => { + return { + content: parseInline( + parse, + // capture[2] is the inner content for inline code + String((capture as unknown as RegExpMatchArray)[2] ?? ''), + state as MarkdownState, + ), + type: 'inlineCode', // type is ignored by your renderer; safe to include + } as unknown as SingleASTNode; + }; + + return { + autolink: { + react(node: SingleASTNode, output: Output, state: MarkdownState) { + state.withinText = true; + const n = node as NodeWithContent & TargetNode; + const onPress = () => pressHandler(n.target); + return React.createElement( + Text, + { + key: state.key, + onPress, + style: styles.autolink, + }, + output(n.content, state), + ); + }, + }, + blockQuote: { + react(node: SingleASTNode, output: Output, state: MarkdownState) { + state.withinQuote = true; + const n = node as NodeWithContent; + + const img = React.createElement(View, { + key: Number(state.key) - Number(state.key), + style: [styles.blockQuoteSectionBar, styles.blockQuoteBar], + }); + + const blockQuote = React.createElement( + Text, + { + key: state.key, + style: styles.blockQuoteText, + }, + output(n.content, state), + ); + + return React.createElement( + View, + { + key: state.key, + style: [styles.blockQuoteSection, styles.blockQuoteText], + }, + [img, blockQuote], + ); + }, + }, + br: { + react(_node: SingleASTNode, _output: Output, state: MarkdownState) { + return React.createElement( + Text, + { + key: state.key, + style: styles.br, + }, + '\n\n', + ); + }, + }, + codeBlock: { + react(node: SingleASTNode, _output: Output, state: MarkdownState) { + state.withinText = true; + const n = node as NodeWithStringContent; + return React.createElement( + Text, + { + key: state.key, + style: styles.codeBlock, + }, + n.content, + ); + }, + }, + del: { + react(node: SingleASTNode, output: Output, state: MarkdownState) { + state.withinText = true; + const n = node as NodeWithContent; + return React.createElement( + Text, + { + key: state.key, + style: styles.del, + }, + output(n.content, state), + ); + }, + }, + em: { + react(node: SingleASTNode, output: Output, state: MarkdownState) { + state.withinText = true; + state.style = { + ...(state.style || {}), + ...styles.em, + }; + const n = node as NodeWithContent; + return React.createElement( + Text, + { + key: state.key, + style: styles.em, + }, + output(n.content, state), + ); + }, + }, + heading: { + match: SimpleMarkdown.blockRegex(/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n *)+/) as MatchFunction, + react(node: SingleASTNode, output: Output, state: MarkdownState) { + state.withinText = true; + state.withinHeading = true; + + const n = node as HeadingNode; + const dynHeadingStyle = (styles as unknown as HeadingStyles)[`heading${n.level}`]; + + state.style = { + ...(state.style || {}), + ...dynHeadingStyle, + }; + + const ret = React.createElement( + Text, + { + key: state.key, + style: state.style, + }, + output(n.content, state), + ); + return ret; + }, + }, + hr: { + react(_node: SingleASTNode, _output: Output, state: MarkdownState) { + return React.createElement(View, { key: state.key, style: styles.hr }); + }, + }, + image: { + // You intentionally disable parsing images; keep the shape + match: (() => null) as unknown as MatchFunction, + }, + inlineCode: { + parse: parseCaptureInline, + react(node: SingleASTNode, output: Output, state: MarkdownState) { + state.withinText = true; + const n = node as NodeWithContent; + return React.createElement( + Text, + { + key: state.key, + style: styles.inlineCode, + }, + output(n.content, state), + ); + }, + }, + link: { + match: SimpleMarkdown.inlineRegex( + new RegExp('^\\[(' + LINK_INSIDE + ')\\]\\(' + LINK_HREF_AND_TITLE + '\\)'), + ) as MatchFunction, + react(node: SingleASTNode, output: Output, state: MarkdownState) { + state.withinLink = true; + const n = node as NodeWithContent & TargetNode; + const onPress = () => pressHandler(n.target); + const link = React.createElement( + Text, + { + key: state.key, + onPress, + style: styles.autolink, + }, + output(n.content, state), + ); + state.withinLink = false; + return link; + }, + }, + list: { + react(node: SingleASTNode, output: Output, state: MarkdownState) { + let numberIndex = 1; + const n = node as ListNode; + + const items = map(n.items as SingleASTNode[][], (item, i) => { + let bullet: React.ReactNode; + state.withinList = false; + + if (n.ordered) { + bullet = React.createElement( + Text, + { key: 0, style: [styles.text, styles.listItemNumber] }, + numberIndex + '. ', + ); + } else { + bullet = React.createElement( + Text, + { key: 0, style: [styles.text, styles.listItemBullet] }, + '\u2022 ', + ); + } + + if ((item as SingleASTNode[]).length> 1) { + if ((item as SingleASTNode[])[1].type === 'list') { + state.withinList = true; + } + } + + const content = output(item as unknown as SingleASTNode[], state); + + let listItem: React.ReactNode; + if ( + includes(['text', 'paragraph', 'strong'], (head(item) || {}).type) && + state.withinList === false + ) { + state.withinList = true; + listItem = React.createElement( + Text, + { + key: 1, + style: [styles.listItemText, { marginBottom: 0 }], + }, + content, + ); + } else { + listItem = React.createElement( + View, + { + key: 1, + style: styles.listItemText, + }, + content, + ); + } + state.withinList = false; + numberIndex++; + + return React.createElement( + View, + { + key: i, + style: styles.listRow, + }, + [bullet, listItem], + ); + }); + + return React.createElement(View, { key: state.key, style: styles.list }, items); + }, + }, + mailto: { + react(node: SingleASTNode, output: Output, state: MarkdownState) { + state.withinText = true; + const n = node as NodeWithContent; + return React.createElement( + Text, + { + key: state.key, + onPress: noop, + style: styles.autolink, + }, + output(n.content, state), + ); + }, + }, + newline: { + react(_node: SingleASTNode, _output: Output, state: MarkdownState) { + return React.createElement( + Text, + { + key: state.key, + style: styles.newline, + }, + '\n', + ); + }, + }, + paragraph: { + react(node: SingleASTNode, output: Output, state: MarkdownState) { + const n = node as NodeWithContent; + let paragraphStyle: TextStyle | (TextStyle | undefined)[] | undefined = styles.paragraph; + + // Allow image to drop in next line within the paragraph + if (some(n.content, { type: 'image' })) { + state.withinParagraphWithImage = true; + const paragraph = React.createElement( + View, + { + key: state.key, + style: styles.paragraphWithImage, + }, + output(n.content, state), + ); + state.withinParagraphWithImage = false; + return paragraph; + } else if (size(n.content) < 3 && some(n.content, { type: 'strong' })) { + // center for Strong-only content + paragraphStyle = styles.paragraphCenter; + } + if (state.withinList) { + paragraphStyle = [paragraphStyle, styles.noMargin]; + } + return React.createElement( + Text, + { + key: state.key, + style: paragraphStyle, + }, + output(n.content, state), + ); + }, + }, + strong: { + react(node: SingleASTNode, output: Output, state: MarkdownState) { + state.withinText = true; + state.style = { + ...(state.style || {}), + ...styles.strong, + }; + const n = node as NodeWithContent; + return React.createElement( + Text, + { + key: state.key, + style: state.style, + }, + output(n.content, state), + ); + }, + }, + sublist: { + react(node: SingleASTNode, output: Output, state: MarkdownState) { + const n = node as ListNode; + + const items = map(n.items as SingleASTNode[][], (item, i) => { + let bullet: React.ReactNode; + if (n.ordered) { + bullet = React.createElement( + Text, + { key: 0, style: [styles.text, styles.listItemNumber] }, + i + 1 + '. ', + ); + } else { + bullet = React.createElement( + Text, + { key: 0, style: [styles.text, styles.listItemBullet] }, + '\u2022 ', + ); + } + + const content = output(item as unknown as SingleASTNode[], state); + let listItem: React.ReactNode; + state.withinList = true; + if (includes(['text', 'paragraph', 'strong'], (head(item) || {}).type)) { + listItem = React.createElement( + Text, + { + key: 1, + style: styles.listItemText, + }, + content, + ); + } else { + listItem = React.createElement( + View, + { + key: 1, + style: styles.listItem, + }, + content, + ); + } + state.withinList = false; + return React.createElement( + View, + { + key: i, + style: styles.listRow, + }, + [bullet, listItem], + ); + }); + + return React.createElement(View, { key: state.key, style: styles.sublist }, items); + }, + }, + table: { + react(node: SingleASTNode, output: Output, state: MarkdownState) { + const n = node as TableNode; + + const headers = map(n.header, (content, i) => + React.createElement( + Text, + { + key: i, + style: styles.tableHeaderCell, + }, + output(content, state), + ), + ); + + const header = React.createElement(View, { key: -1, style: styles.tableHeader }, headers); + + const rows = map(n.cells, (row, r) => { + const cells = map(row, (content, c) => + React.createElement( + View, + { + key: c, + style: styles.tableRowCell, + }, + output(content, state), + ), + ); + const rowStyles: (TextStyle | ViewStyle | undefined)[] = [styles.tableRow]; + if (n.cells.length - 1 === r) { + rowStyles.push(styles.tableRowLast); + } + return React.createElement(View, { key: r, style: rowStyles }, cells); + }); + + return React.createElement(View, { key: state.key, style: styles.table }, [header, rows]); + }, + }, + text: { + react(node: SingleASTNode, _output: Output, state: MarkdownState) { + const n = node as NodeWithStringContent; + let textStyle: TextStyle | (TextStyle | ViewStyle | undefined)[] = { + ...styles.text, + ...(state.style || {}), + }; + + if (state.withinLink) { + textStyle = [styles.text, styles.autolink]; + } + + if (state.withinQuote) { + textStyle = [styles.text, styles.blockQuoteText]; + } + + return React.createElement( + Text, + { + key: state.key, + style: textStyle, + }, + n.content, + ); + }, + }, + u: { + // u will do the same as strong, to avoid the View nested inside text problem + react(node: SingleASTNode, output: Output, state: MarkdownState) { + state.withinText = true; + state.style = { + ...(state.style || {}), + ...styles.u, + }; + const n = node as NodeWithContent; + return React.createElement( + Text, + { + key: state.key, + style: styles.strong, + }, + output(n.content, state), + ); + }, + }, + url: { + react(node: SingleASTNode, output: Output, state: MarkdownState) { + state.withinText = true; + const n = node as NodeWithContent & TargetNode; + const onPress = () => pressHandler(n.target); + return React.createElement( + Text, + { + key: state.key, + onPress, + style: styles.autolink, + }, + output(n.content, state), + ); + }, + }, + } as unknown as OutputRules; +}; diff --git a/package/src/components/Message/MessageSimple/utils/markdown/styles.ts b/package/src/components/Message/MessageSimple/utils/markdown/styles.ts new file mode 100644 index 0000000000..a7da1aa678 --- /dev/null +++ b/package/src/components/Message/MessageSimple/utils/markdown/styles.ts @@ -0,0 +1,177 @@ +import { Dimensions, Platform, StyleSheet } from 'react-native'; + +export default StyleSheet.create({ + autolink: { + color: 'blue', + }, + bgImage: { + bottom: 0, + flex: 1, + left: 0, + position: 'absolute', + right: 0, + top: 0, + }, + bgImageView: { + flex: 1, + overflow: 'hidden', + }, + blockQuoteSection: { + flexDirection: 'row', + }, + blockQuoteSectionBar: { + backgroundColor: '#DDDDDD', + height: null, + marginRight: 15, + width: 3, + }, + blockQuoteText: { + color: 'grey', + }, + codeBlock: { + backgroundColor: '#DDDDDD', + fontFamily: Platform.OS === 'ios' ? 'Courier' : 'Monospace', + fontWeight: '500', + }, + del: { + textDecorationLine: 'line-through', + textDecorationStyle: 'solid', + }, + em: { + fontStyle: 'italic', + }, + heading: { + fontWeight: '200', + }, + heading1: { + fontSize: 32, + }, + heading2: { + fontSize: 24, + }, + heading3: { + fontSize: 18, + }, + heading4: { + fontSize: 16, + }, + heading5: { + fontSize: 13, + }, + heading6: { + fontSize: 11, + }, + hr: { + backgroundColor: '#cccccc', + height: 1, + }, + image: { + alignSelf: 'center', + height: 200, + resizeMode: 'contain', + width: Dimensions.get('window').width - 30, + }, + imageBox: { + flex: 1, + resizeMode: 'cover', + }, + inlineCode: { + backgroundColor: '#eeeeee', + borderColor: '#dddddd', + borderRadius: 3, + borderWidth: 1, + fontFamily: Platform.OS === 'ios' ? 'Courier' : 'Monospace', + fontWeight: 'bold', + }, + list: {}, + listItem: { + flexDirection: 'row', + }, + listItemBullet: { + fontSize: 20, + lineHeight: 20, + }, + listItemNumber: { + fontWeight: 'normal', + }, + listItemText: { + flex: 1, + }, + listRow: { + flexDirection: 'row', + }, + noMargin: { + marginBottom: 0, + marginTop: 0, + }, + paragraph: { + alignItems: 'flex-start', + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'flex-start', + marginBottom: 10, + marginTop: 10, + }, + paragraphCenter: { + alignItems: 'flex-start', + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + marginBottom: 10, + marginTop: 10, + textAlign: 'center', + }, + paragraphWithImage: { + alignItems: 'flex-start', + flex: 1, + justifyContent: 'flex-start', + marginBottom: 10, + marginTop: 10, + }, + strong: { + fontWeight: 'bold', + }, + sublist: { + paddingLeft: 20, + width: Dimensions.get('window').width - 60, + }, + table: { + borderColor: '#222222', + borderRadius: 3, + borderWidth: 1, + }, + tableHeader: { + backgroundColor: '#222222', + flexDirection: 'row', + justifyContent: 'space-around', + }, + tableHeaderCell: { + color: '#ffffff', + fontWeight: 'bold', + padding: 5, + }, + tableRow: { + borderColor: '#222222', + flexDirection: 'row', + justifyContent: 'space-around', + }, + tableRowCell: { + padding: 5, + }, + tableRowLast: { + borderColor: 'transparent', + }, + text: { + color: '#222222', + }, + textRow: { + flexDirection: 'row', + }, + u: { + borderBottomWidth: 1, + borderColor: '#222222', + }, + view: { + alignSelf: 'stretch', + }, +}); diff --git a/package/src/components/Message/MessageSimple/utils/renderText.tsx b/package/src/components/Message/MessageSimple/utils/renderText.tsx index 7ec1599f50..afc7a60221 100644 --- a/package/src/components/Message/MessageSimple/utils/renderText.tsx +++ b/package/src/components/Message/MessageSimple/utils/renderText.tsx @@ -10,25 +10,24 @@ import { } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -// @ts-expect-error -import Markdown from 'react-native-markdown-package'; + import Animated, { clamp, scrollTo, useAnimatedRef, useSharedValue } from 'react-native-reanimated'; -import { - DefaultRules, - defaultRules, +import SimpleMarkdown, { MatchFunction, NodeOutput, Output, ParseFunction, - parseInline, SingleASTNode, State, -} from 'simple-markdown'; +} from '@khanacademy/simple-markdown'; + +const { defaultRules, parseInline } = SimpleMarkdown; import type { LocalMessage, UserResponse } from 'stream-chat'; import { generateMarkdownText } from './generateMarkdownText'; +import Markdown, { MarkdownRules } from './markdown'; import type { MessageContextValue } from '../../../../contexts/messageContext/MessageContext'; import type { Colors, MarkdownStyle } from '../../../../contexts/themeContext/utils/theme'; @@ -155,8 +154,6 @@ const mentionsParseFunction: ParseFunction = (capture, parse, state) => ({ content: parseInline(parse, capture[0], state), }); -export type MarkdownRules = Partial; - export type RenderTextParams = Partial< Pick > & { @@ -450,7 +447,7 @@ export const renderText = (params: RenderTextParams) => { }-${JSON.stringify(colors)}`} onLink={onLink} rules={{ - ...customRules, + ...(customRules as unknown as MarkdownRules), ...markdownRules, }} styles={styles} diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx index f919028760..61b89e13ca 100644 --- a/package/src/contexts/messagesContext/MessagesContext.tsx +++ b/package/src/contexts/messagesContext/MessagesContext.tsx @@ -45,7 +45,7 @@ import type { MessageTextProps } from '../../components/Message/MessageSimple/Me import { MessageTimestampProps } from '../../components/Message/MessageSimple/MessageTimestamp'; import { ReactionListBottomProps } from '../../components/Message/MessageSimple/ReactionList/ReactionListBottom'; import type { ReactionListTopProps } from '../../components/Message/MessageSimple/ReactionList/ReactionListTop'; -import type { MarkdownRules } from '../../components/Message/MessageSimple/utils/renderText'; +import type { MarkdownRules } from '../../components/Message/MessageSimple/utils/markdown'; import type { MessageActionsParams } from '../../components/Message/utils/messageActions'; import type { DateHeaderProps } from '../../components/MessageList/DateHeader'; import type { InlineDateSeparatorProps } from '../../components/MessageList/InlineDateSeparator'; diff --git a/package/yarn.lock b/package/yarn.lock index 0e0cfcab86..7916a756cd 100644 --- a/package/yarn.lock +++ b/package/yarn.lock @@ -1889,6 +1889,18 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@khanacademy/perseus-utils@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@khanacademy/perseus-utils/-/perseus-utils-2.1.0.tgz#a405f5e740e73a1345f2b172ff34260b6d87f57e" + integrity sha512-QgN9qW1hnoCGkErSVwCJ5SmWAqlcgXA3TvtMjsyv8LiYbAS4/ZMQOHIuq5P7u0t1i6tp02w7Qjbz/COsxyHg/A== + +"@khanacademy/simple-markdown@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@khanacademy/simple-markdown/-/simple-markdown-2.1.0.tgz#7fcbbcb4d0eda7d0dfb09c2ae35f0b9eb1e07dcf" + integrity sha512-U9yemDLqP3ehAhUdBhzVfZe9h10wWbkrIkfFf2ejSO/cUSFZwjt+KRfZtZK6q2HY+RkNYUjALDpwJu7LkrY1gw== + dependencies: + "@khanacademy/perseus-utils" "2.1.0" + "@napi-rs/wasm-runtime@^0.2.11": version "0.2.11" resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz#192c1610e1625048089ab4e35bc0649ce478500e" @@ -2340,7 +2352,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.0.0", "@types/react@^19.1.0": +"@types/react@*", "@types/react@^19.1.0": version "19.1.8" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.8.tgz#ff8395f2afb764597265ced15f8dddb0720ae1c3" integrity sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g== @@ -6414,7 +6426,7 @@ lodash.throttle@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== -lodash@^4.17.15, lodash@^4.17.21: +lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -7568,7 +7580,7 @@ prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.5.10, prop-types@^15.8.1: +prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -7702,22 +7714,6 @@ react-native-is-edge-to-edge@1.1.7: resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.7.tgz#28947688f9fafd584e73a4f935ea9603bd9b1939" integrity sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w== -react-native-lightbox@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/react-native-lightbox/-/react-native-lightbox-0.7.0.tgz#e52b4d7fcc141f59d7b23f0180de535e35b20ec9" - integrity sha512-HS3T4WlCd0Gb3us2d6Jse5m6KjNhngnKm35Wapq30WtQa9s+/VMmtuktbGPGaWtswcDyOj6qByeJBw9W80iPCA== - dependencies: - prop-types "^15.5.10" - -react-native-markdown-package@1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/react-native-markdown-package/-/react-native-markdown-package-1.8.2.tgz#19db1047e174077f9b9f80303938c775b1c223c0" - integrity sha512-F3z/p0XfY6Nu9NlXQx1pYcPdz7Y37NRcAKTN+yb9nwRi8BW75mdc3uaBrM13PDVUlL0hbfTL7FuoAdSbsyB5vg== - dependencies: - lodash "^4.17.15" - react-native-lightbox "^0.7.0" - simple-markdown "^0.7.1" - react-native-monorepo-config@^0.1.8: version "0.1.9" resolved "https://registry.yarnpkg.com/react-native-monorepo-config/-/react-native-monorepo-config-0.1.9.tgz#1caacc259a2b4ccf5162c14c6f5f457ae9adec40" @@ -8258,13 +8254,6 @@ simple-get@^4.0.0: once "^1.3.1" simple-concat "^1.0.0" -simple-markdown@^0.7.1: - version "0.7.3" - resolved "https://registry.yarnpkg.com/simple-markdown/-/simple-markdown-0.7.3.tgz#e32150b2ec6f8287197d09869fd928747a9c5640" - integrity sha512-uGXIc13NGpqfPeFJIt/7SHHxd6HekEJYtsdoCM06mEBPL9fQH/pSD7LRM6PZ7CKchpSvxKL4tvwMamqAaNDAyg== - dependencies: - "@types/react" ">=16.0.0" - sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" From ee5d6785d50304788b037c25f7ff66102f250e65 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: 2025年10月22日 13:47:58 +0200 Subject: [PATCH 2/3] fix: remove testing merge --- .../components/Message/MessageSimple/utils/markdown/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/package/src/components/Message/MessageSimple/utils/markdown/index.tsx b/package/src/components/Message/MessageSimple/utils/markdown/index.tsx index c4cc0f692d..e91351fbcb 100644 --- a/package/src/components/Message/MessageSimple/utils/markdown/index.tsx +++ b/package/src/components/Message/MessageSimple/utils/markdown/index.tsx @@ -34,7 +34,6 @@ const Markdown = (props: PropsWithChildren) => { merge( {}, SimpleMarkdown.defaultRules, - { ...SimpleMarkdown.defaultRules }, getLocalRules(mergedStyles, { onLink }), rulesProp, ) as unknown as ParserRules, From 6b07adb610f9e3e35c2bcd7ef0c3cd315ee757d3 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj Date: 2025年10月22日 15:16:24 +0200 Subject: [PATCH 3/3] fix: markdown parsing issues --- .../MessageSimple/utils/markdown/rules.ts | 551 +----------------- 1 file changed, 22 insertions(+), 529 deletions(-) diff --git a/package/src/components/Message/MessageSimple/utils/markdown/rules.ts b/package/src/components/Message/MessageSimple/utils/markdown/rules.ts index 6e6f7e9905..012c57df7e 100644 --- a/package/src/components/Message/MessageSimple/utils/markdown/rules.ts +++ b/package/src/components/Message/MessageSimple/utils/markdown/rules.ts @@ -1,504 +1,3 @@ -// import React from 'react'; -// -// import { Text, View } from 'react-native'; -// -// import SimpleMarkdown from '@khanacademy/simple-markdown'; -// import { head, includes, map, noop, size, some } from 'lodash'; -// -// import { MarkdownStyle } from '../../../../../contexts'; -// -// import { MarkdownOptions } from './index'; -// -// export const getLocalRules = (styles: MarkdownStyle, opts: MarkdownOptions = {}) => { -// const LINK_INSIDE = '(?:\\[[^\\]]*\\]|[^\\]]|\\](?=[^\\[]*\\]))*'; -// const LINK_HREF_AND_TITLE = '\\s*?(?:\\s+[\'"]([\\s\\S]*?)[\'"])?\\s*'; -// const pressHandler = function (target) { -// if (opts.onLink) { -// opts.onLink(target).catch(function (error) { -// console.log('There has been a problem with this action. ' + error.message); -// throw error; -// }); -// } -// }; -// const parseInline = function (parse, content, state) { -// const isCurrentlyInline = state.inline || false; -// state.inline = true; -// const result = parse(content, state); -// state.inline = isCurrentlyInline; -// return result; -// }; -// const parseCaptureInline = function (capture, parse, state) { -// return { -// content: parseInline(parse, capture[2], state), -// }; -// }; -// return { -// autolink: { -// react(node, output, { ...state }) { -// state.withinText = true; -// const _pressHandler = () => { -// pressHandler(node.target); -// }; -// return React.createElement( -// Text, -// { -// key: state.key, -// style: styles.autolink, -// onPress: _pressHandler, -// }, -// output(node.content, state), -// ); -// }, -// }, -// blockQuote: { -// react(node, output, { ...state }) { -// state.withinQuote = true; -// -// const img = React.createElement(View, { -// key: state.key - state.key, -// style: [styles.blockQuoteSectionBar, styles.blockQuoteBar], -// }); -// -// const blockQuote = React.createElement( -// Text, -// { -// key: state.key, -// style: styles.blockQuoteText, -// }, -// output(node.content, state), -// ); -// -// return React.createElement( -// View, -// { -// key: state.key, -// style: [styles.blockQuoteSection, styles.blockQuoteText], -// }, -// [img, blockQuote], -// ); -// }, -// }, -// br: { -// react(node, output, { ...state }) { -// return React.createElement( -// Text, -// { -// key: state.key, -// style: styles.br, -// }, -// '\n\n', -// ); -// }, -// }, -// codeBlock: { -// react(node, output, { ...state }) { -// state.withinText = true; -// return React.createElement( -// Text, -// { -// key: state.key, -// style: styles.codeBlock, -// }, -// node.content, -// ); -// }, -// }, -// del: { -// react(node, output, { ...state }) { -// state.withinText = true; -// return React.createElement( -// Text, -// { -// key: state.key, -// style: styles.del, -// }, -// output(node.content, state), -// ); -// }, -// }, -// em: { -// react(node, output, { ...state }) { -// state.withinText = true; -// state.style = { -// ...(state.style || {}), -// ...styles.em, -// }; -// return React.createElement( -// Text, -// { -// key: state.key, -// style: styles.em, -// }, -// output(node.content, state), -// ); -// }, -// }, -// heading: { -// match: SimpleMarkdown.blockRegex(/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n *)+/), -// react(node, output, { ...state }) { -// // const newState = {...state}; -// state.withinText = true; -// state.withinHeading = true; -// -// state.style = { -// ...(state.style || {}), -// ...styles[`heading${node.level}`], -// }; -// -// const ret = React.createElement( -// Text, -// { -// key: state.key, -// style: state.style, -// }, -// output(node.content, state), -// ); -// return ret; -// }, -// }, -// hr: { -// react(node, output, { ...state }) { -// return React.createElement(View, { key: state.key, style: styles.hr }); -// }, -// }, -// image: { -// match: () => null, -// }, -// inlineCode: { -// parse: parseCaptureInline, -// react(node, output, { ...state }) { -// state.withinText = true; -// return React.createElement( -// Text, -// { -// key: state.key, -// style: styles.inlineCode, -// }, -// output(node.content, state), -// ); -// }, -// }, -// link: { -// match: SimpleMarkdown.inlineRegex( -// new RegExp('^\\[(' + LINK_INSIDE + ')\\]\\(' + LINK_HREF_AND_TITLE + '\\)'), -// ), -// react(node, output, { ...state }) { -// state.withinLink = true; -// const _pressHandler = () => { -// pressHandler(node.target); -// }; -// const link = React.createElement( -// Text, -// { -// key: state.key, -// style: styles.autolink, -// onPress: _pressHandler, -// }, -// output(node.content, state), -// ); -// state.withinLink = false; -// return link; -// }, -// }, -// list: { -// react(node, output, { ...state }) { -// let numberIndex = 1; -// const items = map(node.items, function (item, i) { -// let bullet; -// state.withinList = false; -// -// if (node.ordered) { -// bullet = React.createElement( -// Text, -// { key: 0, style: [styles.text, styles.listItemNumber] }, -// numberIndex + '. ', -// ); -// } else { -// bullet = React.createElement( -// Text, -// { key: 0, style: [styles.text, styles.listItemBullet] }, -// '\u2022 ', -// ); -// } -// -// if (item.length> 1) { -// if (item[1].type == 'list') { -// state.withinList = true; -// } -// } -// -// const content = output(item, state); -// let listItem; -// if ( -// includes(['text', 'paragraph', 'strong'], (head(item) || {}).type) && -// state.withinList == false -// ) { -// state.withinList = true; -// listItem = React.createElement( -// Text, -// { -// style: [styles.listItemText, { marginBottom: 0 }], -// key: 1, -// }, -// content, -// ); -// } else { -// listItem = React.createElement( -// View, -// { -// style: styles.listItemText, -// key: 1, -// }, -// content, -// ); -// } -// state.withinList = false; -// numberIndex++; -// -// return React.createElement( -// View, -// { -// key: i, -// style: styles.listRow, -// }, -// [bullet, listItem], -// ); -// }); -// -// return React.createElement(View, { key: state.key, style: styles.list }, items); -// }, -// }, -// sublist: { -// react(node, output, { ...state }) { -// const items = map(node.items, function (item, i) { -// let bullet; -// if (node.ordered) { -// bullet = React.createElement( -// Text, -// { key: 0, style: [styles.text, styles.listItemNumber] }, -// i + 1 + '. ', -// ); -// } else { -// bullet = React.createElement( -// Text, -// { key: 0, style: [styles.text, styles.listItemBullet] }, -// '\u2022 ', -// ); -// } -// -// const content = output(item, state); -// let listItem; -// state.withinList = true; -// if (includes(['text', 'paragraph', 'strong'], (head(item) || {}).type)) { -// listItem = React.createElement( -// Text, -// { -// style: styles.listItemText, -// key: 1, -// }, -// content, -// ); -// } else { -// listItem = React.createElement( -// View, -// { -// style: styles.listItem, -// key: 1, -// }, -// content, -// ); -// } -// state.withinList = false; -// return React.createElement( -// View, -// { -// key: i, -// style: styles.listRow, -// }, -// [bullet, listItem], -// ); -// }); -// -// return React.createElement(View, { key: state.key, style: styles.sublist }, items); -// }, -// }, -// mailto: { -// react(node, output, { ...state }) { -// state.withinText = true; -// return React.createElement( -// Text, -// { -// key: state.key, -// style: styles.mailto, -// onPress: noop, -// }, -// output(node.content, state), -// ); -// }, -// }, -// newline: { -// react(node, output, { ...state }) { -// return React.createElement( -// Text, -// { -// key: state.key, -// style: styles.newline, -// }, -// '\n', -// ); -// }, -// }, -// paragraph: { -// react(node, output, { ...state }) { -// let paragraphStyle = styles.paragraph; -// // Allow image to drop in next line within the paragraph -// if (some(node.content, { type: 'image' })) { -// state.withinParagraphWithImage = true; -// const paragraph = React.createElement( -// View, -// { -// key: state.key, -// style: styles.paragraphWithImage, -// }, -// output(node.content, state), -// ); -// state.withinParagraphWithImage = false; -// return paragraph; -// } else if (size(node.content) < 3 && some(node.content, { type: 'strong' })) { -// // align to center for Strong only content -// // require a check of content array size below 3, -// // as parse will include additional space as `text` -// paragraphStyle = styles.paragraphCenter; -// } -// if (state.withinList) { -// paragraphStyle = [paragraphStyle, styles.noMargin]; -// } -// return React.createElement( -// Text, -// { -// key: state.key, -// style: paragraphStyle, -// }, -// output(node.content, state), -// ); -// }, -// }, -// strong: { -// react(node, output, { ...state }) { -// state.withinText = true; -// state.style = { -// ...(state.style || {}), -// ...styles.strong, -// }; -// return React.createElement( -// Text, -// { -// key: state.key, -// style: state.style, -// }, -// output(node.content, state), -// ); -// }, -// }, -// table: { -// react(node, output, { ...state }) { -// const headers = map(node.header, function (content, i) { -// return React.createElement( -// Text, -// { -// key: i, -// style: styles.tableHeaderCell, -// }, -// output(content, state), -// ); -// }); -// -// const header = React.createElement(View, { key: -1, style: styles.tableHeader }, headers); -// -// const rows = map(node.cells, function (row, r) { -// const cells = map(row, function (content, c) { -// return React.createElement( -// View, -// { -// key: c, -// style: styles.tableRowCell, -// }, -// output(content, state), -// ); -// }); -// const rowStyles = [styles.tableRow]; -// if (node.cells.length - 1 == r) { -// rowStyles.push(styles.tableRowLast); -// } -// return React.createElement(View, { key: r, style: rowStyles }, cells); -// }); -// -// return React.createElement(View, { key: state.key, style: styles.table }, [header, rows]); -// }, -// }, -// text: { -// react(node, output, { ...state }) { -// let textStyle = { -// ...styles.text, -// ...(state.style || {}), -// }; -// -// if (state.withinLink) { -// textStyle = [styles.text, styles.autolink]; -// } -// -// if (state.withinQuote) { -// textStyle = [styles.text, styles.blockQuoteText]; -// } -// -// return React.createElement( -// Text, -// { -// key: state.key, -// style: textStyle, -// }, -// node.content, -// ); -// }, -// }, -// u: { -// // u will to the same as strong, to avoid the View nested inside text problem -// react(node, output, { ...state }) { -// state.withinText = true; -// state.style = { -// ...(state.style || {}), -// ...styles.u, -// }; -// return React.createElement( -// Text, -// { -// key: state.key, -// style: styles.strong, -// }, -// output(node.content, state), -// ); -// }, -// }, -// url: { -// react(node, output, { ...state }) { -// state.withinText = true; -// const _pressHandler = () => { -// pressHandler(node.target); -// }; -// return React.createElement( -// Text, -// { -// key: state.key, -// style: styles.autolink, -// onPress: _pressHandler, -// }, -// output(node.content, state), -// ); -// }, -// }, -// }; -// }; - import React from 'react'; import { Text, TextStyle, View, ViewStyle } from 'react-native'; @@ -584,19 +83,13 @@ export const getLocalRules = ( const parseCaptureInline: ParseFunction = (capture, parse, state) => { return { - content: parseInline( - parse, - // capture[2] is the inner content for inline code - String((capture as unknown as RegExpMatchArray)[2] ?? ''), - state as MarkdownState, - ), - type: 'inlineCode', // type is ignored by your renderer; safe to include - } as unknown as SingleASTNode; + content: parseInline(parse, capture[2], state as MarkdownState), + }; }; return { autolink: { - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { state.withinText = true; const n = node as NodeWithContent & TargetNode; const onPress = () => pressHandler(n.target); @@ -612,7 +105,7 @@ export const getLocalRules = ( }, }, blockQuote: { - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { state.withinQuote = true; const n = node as NodeWithContent; @@ -641,7 +134,7 @@ export const getLocalRules = ( }, }, br: { - react(_node: SingleASTNode, _output: Output, state: MarkdownState) { + react(_node: SingleASTNode, _output: Output, { ...state }: MarkdownState) { return React.createElement( Text, { @@ -653,7 +146,7 @@ export const getLocalRules = ( }, }, codeBlock: { - react(node: SingleASTNode, _output: Output, state: MarkdownState) { + react(node: SingleASTNode, _output: Output, { ...state }: MarkdownState) { state.withinText = true; const n = node as NodeWithStringContent; return React.createElement( @@ -667,7 +160,7 @@ export const getLocalRules = ( }, }, del: { - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { state.withinText = true; const n = node as NodeWithContent; return React.createElement( @@ -681,7 +174,7 @@ export const getLocalRules = ( }, }, em: { - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { state.withinText = true; state.style = { ...(state.style || {}), @@ -700,7 +193,7 @@ export const getLocalRules = ( }, heading: { match: SimpleMarkdown.blockRegex(/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n *)+/) as MatchFunction, - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { state.withinText = true; state.withinHeading = true; @@ -724,7 +217,7 @@ export const getLocalRules = ( }, }, hr: { - react(_node: SingleASTNode, _output: Output, state: MarkdownState) { + react(_node: SingleASTNode, _output: Output, { ...state }: MarkdownState) { return React.createElement(View, { key: state.key, style: styles.hr }); }, }, @@ -734,7 +227,7 @@ export const getLocalRules = ( }, inlineCode: { parse: parseCaptureInline, - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { state.withinText = true; const n = node as NodeWithContent; return React.createElement( @@ -751,7 +244,7 @@ export const getLocalRules = ( match: SimpleMarkdown.inlineRegex( new RegExp('^\\[(' + LINK_INSIDE + ')\\]\\(' + LINK_HREF_AND_TITLE + '\\)'), ) as MatchFunction, - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { state.withinLink = true; const n = node as NodeWithContent & TargetNode; const onPress = () => pressHandler(n.target); @@ -769,7 +262,7 @@ export const getLocalRules = ( }, }, list: { - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { let numberIndex = 1; const n = node as ListNode; @@ -840,7 +333,7 @@ export const getLocalRules = ( }, }, mailto: { - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { state.withinText = true; const n = node as NodeWithContent; return React.createElement( @@ -855,7 +348,7 @@ export const getLocalRules = ( }, }, newline: { - react(_node: SingleASTNode, _output: Output, state: MarkdownState) { + react(_node: SingleASTNode, _output: Output, { ...state }: MarkdownState) { return React.createElement( Text, { @@ -867,7 +360,7 @@ export const getLocalRules = ( }, }, paragraph: { - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { const n = node as NodeWithContent; let paragraphStyle: TextStyle | (TextStyle | undefined)[] | undefined = styles.paragraph; @@ -902,7 +395,7 @@ export const getLocalRules = ( }, }, strong: { - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { state.withinText = true; state.style = { ...(state.style || {}), @@ -920,7 +413,7 @@ export const getLocalRules = ( }, }, sublist: { - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { const n = node as ListNode; const items = map(n.items as SingleASTNode[][], (item, i) => { @@ -976,7 +469,7 @@ export const getLocalRules = ( }, }, table: { - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { const n = node as TableNode; const headers = map(n.header, (content, i) => @@ -1014,7 +507,7 @@ export const getLocalRules = ( }, }, text: { - react(node: SingleASTNode, _output: Output, state: MarkdownState) { + react(node: SingleASTNode, _output: Output, { ...state }: MarkdownState) { const n = node as NodeWithStringContent; let textStyle: TextStyle | (TextStyle | ViewStyle | undefined)[] = { ...styles.text, @@ -1041,7 +534,7 @@ export const getLocalRules = ( }, u: { // u will do the same as strong, to avoid the View nested inside text problem - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { state.withinText = true; state.style = { ...(state.style || {}), @@ -1059,7 +552,7 @@ export const getLocalRules = ( }, }, url: { - react(node: SingleASTNode, output: Output, state: MarkdownState) { + react(node: SingleASTNode, output: Output, { ...state }: MarkdownState) { state.withinText = true; const n = node as NodeWithContent & TargetNode; const onPress = () => pressHandler(n.target);

AltStyle によって変換されたページ (->オリジナル) /