diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..b012da2c
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,27 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
+{
+ "name": "Node.js & TypeScript",
+ // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
+ "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
+ "customizations": {
+ "vscode": {
+ "extensions": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
+ }
+ }
+
+ // Features to add to the dev container. More info: https://containers.dev/features.
+ // "features": {},
+
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
+ // "forwardPorts": [],
+
+ // Use 'postCreateCommand' to run commands after the container is created.
+ // "postCreateCommand": "yarn install",
+
+ // Configure tool-specific properties.
+ // "customizations": {},
+
+ // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
+ // "remoteUser": "root"
+}
diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index d46623ba..00000000
--- a/.eslintrc.js
+++ /dev/null
@@ -1,9 +0,0 @@
-module.exports = {
- root: true,
- extends: '@react-native-community',
- settings: {
- react: {
- version: require('./package.json').peerDependencies.react,
- },
- }
-};
diff --git a/.gitignore b/.gitignore
index e3e176f1..68b7c6f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,5 @@ package-lock.json
yarn-error.log
yarn.lock
.eslintcache
+/dist
+tsconfig.tsbuildinfo
\ No newline at end of file
diff --git a/.npmignore b/.npmignore
index 5a3dd218..f34c5929 100644
--- a/.npmignore
+++ b/.npmignore
@@ -3,3 +3,4 @@
/bin
/docs
/doc
+/dist
\ No newline at end of file
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 00000000..4b0d4ad5
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "bracketSpacing": false,
+ "singleQuote": false,
+ "trailingComma": "all"
+}
diff --git a/.prettierrc.js b/.prettierrc.js
deleted file mode 100644
index 5c4de1a4..00000000
--- a/.prettierrc.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
- bracketSpacing: false,
- jsxBracketSameLine: true,
- singleQuote: true,
- trailingComma: 'all',
-};
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..1e9031c3
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "typescript.tsserver.experimental.enableProjectDiagnostics": true
+}
diff --git a/README.md b/README.md
index dd58d859..ef107361 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,14 @@
-# React Native Markdown Display [](https://badge.fury.io/js/react-native-markdown-display) [](https://snyk.io/test/github/iamacup/react-native-markdown-display)
+# React Native Markdown Display
-**This is a fork of [iamacup/react-native-markdown-display](https://github.com/iamacup/react-native-markdown-display) that increases the depended upon versions of react to 18 and react-native to v0.68. This makes it compatible with Expo SDK 46**
+It a 100% compatible CommonMark renderer, a react-native markdown renderer done right. This is **not** a web-view markdown renderer but a renderer that uses native components for all its elements. These components can be overwritten and styled as needed.
+
+## Credit
+
+- [mientjan/react-native-markdown-renderer](https://github.com/mientjan/react-native-markdown-renderer): Original library
+- [iamacup/react-native-markdown-display](https://github.com/iamacup/react-native-markdown-display): Fork
+- [jonasmerlin/react-native-markdown-display](https://github.com/jonasmerlin/react-native-markdown-display): Second Fork
+- [@jthoward64](https://github.com/jthoward64): Typescript port
-It a 100% compatible CommonMark renderer, a react-native markdown renderer done right. This is __not__ a web-view markdown renderer but a renderer that uses native components for all its elements. These components can be overwritten and styled as needed.
### Compatibility with react-native-markdown-renderer
@@ -10,14 +16,8 @@ This is intended to be a replacement for react-native-markdown-renderer, with a
### Install
-#### Yarn
-```npm
-yarn add react-native-markdown-display
-```
-
-#### NPM
-```npm
-npm install -S react-native-markdown-display
+```sh
+npm install @ukdanceblue/react-native-markdown-display
```
### Get Started
@@ -26,7 +26,7 @@ npm install -S react-native-markdown-display
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';
-import Markdown from 'react-native-markdown-display';
+import Markdown from '@ukdanceblue/react-native-markdown-display';
const copy = `# h1 Heading 8-)
@@ -56,32 +56,29 @@ const App: () => React$Node = () => {
export default App;
```
-
### Props and Functions
The `` object takes the following common props:
-| Property | Default | Required | Description
-| --- | --- | --- | ---
-| `children` | N/A | `true` | The markdown string to render, or the [pre-processed tree](#pre-processing)
-| `style` | [source](https://github.com/iamacup/react-native-markdown-display/blob/master/src/lib/styles.js) | `false` | An object to override the styling for the various rules, [see style section below](#rules-and-styles) for more info
-| `mergeStyle` | `true` | `false` | If true, when a style is supplied, the individual items are merged with the default styles instead of overwriting them
-| `rules` | [source](https://github.com/iamacup/react-native-markdown-display/blob/master/src/lib/renderRules.js) | `false` | An object of rules that specify how to render each markdown item, [see rules section below](#rules) for more info
-| `onLinkPress` | `import { Linking } from 'react-native';` and `Linking.openURL(url);` | `false` | A handler function to change click behaviour, [see handling links section below](#handling-links) for more info
-| `debugPrintTree` | `false` | `false` | Will print the AST tree to the console to help you see what the markdown is being translated to
-
+| Property | Default | Required | Description |
+| ---------------- | ----------------------------------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
+| `children` | N/A | `true` | The markdown string to render, or the [pre-processed tree](#pre-processing) |
+| `style` | [source](https://github.com/iamacup/react-native-markdown-display/blob/master/src/lib/styles.js) | `false` | An object to override the styling for the various rules, [see style section below](#rules-and-styles) for more info |
+| `mergeStyle` | `true` | `false` | If true, when a style is supplied, the individual items are merged with the default styles instead of overwriting them |
+| `rules` | [source](https://github.com/iamacup/react-native-markdown-display/blob/master/src/lib/renderRules.js) | `false` | An object of rules that specify how to render each markdown item, [see rules section below](#rules) for more info |
+| `onLinkPress` | `import { Linking } from 'react-native';` and `Linking.openURL(url);` | `false` | A handler function to change click behaviour, [see handling links section below](#handling-links) for more info |
+| `debugPrintTree` | `false` | `false` | Will print the AST tree to the console to help you see what the markdown is being translated to |
And some additional, less used options:
-| Property | Default | Required | Description
-| --- | --- | --- | ---
-| `renderer` | `instanceOf(AstRenderer)` | `false` | Used to specify a custom renderer, you can not use the rules or styles props with a custom renderer.
-| `markdownit` | `instanceOf(MarkdownIt)` | `false` | A custom markdownit instance with your configuration, default is `MarkdownIt({typographer: true})`
-| `maxTopLevelChildren` | `null` | `false` | If defined as a number will only render out first `n` many top level children, then will try to render out `topLevelMaxExceededItem`
-| `topLevelMaxExceededItem` | `...` | `false` | Will render when `maxTopLevelChildren` is hit. Make sure to give it a key!
-| `allowedImageHandlers` | `['data:image/png;base64', 'data:image/gif;base64', 'data:image/jpeg;base64', 'https://', 'http://']` | `false` | Any image that does not start with one of these will have the `defaultImageHandler` value prepended to it (unless `defaultImageHandler` is null in which case it won't try to render anything)
-| `defaultImageHandler` | `http://` | `false` | Will be prepended to an image url if it does not start with something in the `allowedImageHandlers` array, if this is set to null, it won't try to recover but will just not render anything instead.
-
+| Property | Default | Required | Description |
+| ------------------------- | ----------------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `renderer` | `instanceOf(AstRenderer)` | `false` | Used to specify a custom renderer, you can not use the rules or styles props with a custom renderer. |
+| `markdownit` | `instanceOf(MarkdownIt)` | `false` | A custom markdownit instance with your configuration, default is `MarkdownIt({typographer: true})` |
+| `maxTopLevelChildren` | `null` | `false` | If defined as a number will only render out first `n` many top level children, then will try to render out `topLevelMaxExceededItem` |
+| `topLevelMaxExceededItem` | `...` | `false` | Will render when `maxTopLevelChildren` is hit. Make sure to give it a key! |
+| `allowedImageHandlers` | `['data:image/png;base64', 'data:image/gif;base64', 'data:image/jpeg;base64', 'https://', 'http://']` | `false` | Any image that does not start with one of these will have the `defaultImageHandler` value prepended to it (unless `defaultImageHandler` is null in which case it won't try to render anything) |
+| `defaultImageHandler` | `http://` | `false` | Will be prepended to an image url if it does not start with something in the `allowedImageHandlers` array, if this is set to null, it won't try to recover but will just not render anything instead. |
# Syntax Support
@@ -97,14 +94,13 @@ And some additional, less used options:
###### h6 Heading
```
-| iOS | Android
-| --- | ---
-|
|
+| iOS | Android |
+| ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
+|
|
|
-
Horizontal Rules
@@ -119,16 +115,13 @@ And some additional, less used options:
Some text below
```
-| iOS | Android
-| --- | ---
-|
|
-
+| iOS | Android |
+| ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
+|
|
|
-
-
Emphasis
@@ -144,14 +137,13 @@ And some additional, less used options:
~~Strikethrough~~
```
-| iOS | Android
-| --- | ---
-|
|
+| iOS | Android |
+| ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
+|
|
|
-
Blockquotes
@@ -161,14 +153,13 @@ And some additional, less used options:
> > > ...or with spaces between arrows.
```
-| iOS | Android
-| --- | ---
-|
|
+| iOS | Android |
+| ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
+|
|
|
-
Lists
@@ -195,48 +186,47 @@ And some additional, less used options:
58. bar
```
-| iOS | Android
-| --- | ---
-|
|
+| iOS | Android |
+| ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
+|
|
|
-
Code
```
Inline \`code\`
- Indented code
+Indented code
// Some comments
line 1 of code
line 2 of code
line 3 of code
+Block code "fences"
- Block code "fences"
+\`\`\`
+Sample text here...
+\`\`\`
- \`\`\`
- Sample text here...
- \`\`\`
+Syntax highlighting
- Syntax highlighting
+\`\`\` js
+var foo = function (bar) {
+return bar++;
+};
- \`\`\` js
- var foo = function (bar) {
- return bar++;
- };
+console.log(foo(5));
+\`\`\`
- console.log(foo(5));
- \`\`\`
```
| iOS | Android
| --- | ---
-|
|
+|
|
@@ -246,24 +236,26 @@ And some additional, less used options:
```
- | Option | Description |
- | ------ | ----------- |
- | data | path to data files to supply the data that will be passed into templates. |
- | engine | engine to be used for processing templates. Handlebars is the default. |
- | ext | extension to be used for dest files. |
- Right aligned columns
+| Option | Description |
+| ------ | ------------------------------------------------------------------------- |
+| data | path to data files to supply the data that will be passed into templates. |
+| engine | engine to be used for processing templates. Handlebars is the default. |
+| ext | extension to be used for dest files. |
+
+Right aligned columns
+
+| Option | Description |
+| -----: | ------------------------------------------------------------------------: |
+| data | path to data files to supply the data that will be passed into templates. |
+| engine | engine to be used for processing templates. Handlebars is the default. |
+| ext | extension to be used for dest files. |
- | Option | Description |
- | ------:| -----------:|
- | data | path to data files to supply the data that will be passed into templates. |
- | engine | engine to be used for processing templates. Handlebars is the default. |
- | ext | extension to be used for dest files. |
```
| iOS | Android
| --- | ---
-|
|
+|
|
@@ -272,16 +264,18 @@ And some additional, less used options:
```
- [link text](https://www.google.com)
- [link with title](https://www.google.com "title text!")
+[link text](https://www.google.com)
+
+[link with title](https://www.google.com "title text!")
+
+Autoconverted link https://www.google.com (enable linkify to see)
- Autoconverted link https://www.google.com (enable linkify to see)
```
| iOS | Android
| --- | ---
-|
|
+|
|
@@ -290,21 +284,23 @@ And some additional, less used options:
```
- 
- 
- Like links, Images also have a footnote style syntax
+
+
- ![Alt text][id]
+Like links, Images also have a footnote style syntax
- With a reference later in the document defining the URL location:
+![Alt text][id]
+
+With a reference later in the document defining the URL location:
+
+[id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat"
- [id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat"
```
| iOS | Android
| --- | ---
-|
|
+|
|
@@ -314,20 +310,22 @@ And some additional, less used options:
```
- Enable typographer option to see result.
- (c) (C) (r) (R) (tm) (TM) (p) (P) +-
+Enable typographer option to see result.
- test.. test... test..... test?..... test!....
+(c) (C) (r) (R) (tm) (TM) (p) (P) +-
- !!!!!! ???? ,, -- ---
+test.. test... test..... test?..... test!....
- "Smartypants, double quotes" and 'single quotes'
-```
+!!!!!! ???? ,, -- ---
+
+"Smartypants, double quotes" and 'single quotes'
+
+````
| iOS | Android
| --- | ---
-|
|
+|
|
@@ -351,7 +349,7 @@ import { SafeAreaView, ScrollView, StatusBar } from 'react-native';
import Markdown, { MarkdownIt } from 'react-native-markdown-display';
import blockEmbedPlugin from 'markdown-it-block-embed';
-const markdownItInstance =
+const markdownItInstance =
MarkdownIt({typographer: true})
.use(blockEmbedPlugin, {
containerClassName: "video-embed"
@@ -386,7 +384,7 @@ const App: () => React$Node = () => {
export default App;
-```
+````
In the console, we will see the following rendered tree:
@@ -401,15 +399,13 @@ body
With the following error message:
```
-Warning, unknown render rule encountered: video. 'unknown' render rule used (by default, returns null - nothing rendered)
+Warning, unknown render rule encountered: video. 'unknown' render rule used (by default, returns null - nothing rendered)
```
-
#### Step 2
We need to create the **render rules** and **styles** to handle this new **'video'** component
-
```jsx
import React from 'react';
import { SafeAreaView, ScrollView, StatusBar } from 'react-native';
@@ -417,7 +413,7 @@ import { SafeAreaView, ScrollView, StatusBar } from 'react-native';
import Markdown, { MarkdownIt } from 'react-native-markdown-display';
import blockEmbedPlugin from 'markdown-it-block-embed';
-const markdownItInstance =
+const markdownItInstance =
MarkdownIt({typographer: true})
.use(blockEmbedPlugin, {
containerClassName: "video-embed"
@@ -455,7 +451,7 @@ const App: () => React$Node = () => {
Return a video component instead of this text component!
);
}
-
+
}}
>
{copy}
@@ -501,14 +497,15 @@ And all of the video properties needed to render something meaningful are on the
You can do some additional debugging of what the markdown instance is spitting out like this:
```jsx
-import Markdown, { MarkdownIt } from 'react-native-markdown-display';
-import blockEmbedPlugin from 'markdown-it-block-embed';
+import Markdown, {MarkdownIt} from "react-native-markdown-display";
+import blockEmbedPlugin from "markdown-it-block-embed";
-const markdownItInstance =
- MarkdownIt({typographer: true})
- .use(blockEmbedPlugin, {
- containerClassName: "video-embed"
- });
+const markdownItInstance = MarkdownIt({typographer: true}).use(
+ blockEmbedPlugin,
+ {
+ containerClassName: "video-embed",
+ },
+);
const copy = `
# Some header
@@ -523,7 +520,6 @@ console.log(astTree);
//this contains the html that would be generated - not used by react-native-markdown-display but useful for reference
const html = markdownItInstance.render(copy);
console.log(html);
-
```
The above code will output something like this:
@@ -549,11 +545,9 @@ html:
```
-
-
All Markdown for Testing
@@ -712,7 +706,6 @@ Typographic Replacements
-
# Rules and Styles
### How to style stuff
@@ -723,11 +716,10 @@ Think of the implementation like applying styles in CSS. changes to the `body` e
**Be careful when styling 'text':** the text rule is not applied to all rendered text, most notably list bullet points. If you want to, for instance, color all text, change the `body` style.
-
Example
-
+
```jsx
import React from 'react';
@@ -783,7 +775,7 @@ export default App;
-### Styles
+### Styles
Styles are used to override how certain rules are styled. The existing implementation is [here](https://github.com/iamacup/react-native-markdown-display/blob/master/src/lib/styles.js)
@@ -925,46 +917,45 @@ export default App;
-
### All rules and their associated styles:
-| Render Rule | Style(s) |
-| ------ | ----------- |
-| `body` | `body` |
-| `heading1` | `heading1` |
-| `heading2` | `heading2` |
-| `heading3` | `heading3` |
-| `heading4` | `heading4` |
-| `heading5` | `heading5` |
-| `heading6` | `heading6` |
-| `hr` | `hr` |
-| `strong` | `strong` |
-| `em` | `em` |
-| `s` | `s` |
-| `blockquote` | `blockquote` |
-| `bullet_list` | `bullet_list` |
-| `ordered_list` | `ordered_list` |
-| `list_item` | `list_item` - This is a special case that contains a set of pseudo classes that don't align to the render rule: `ordered_list_icon`, `ordered_list_content`, `bullet_list_icon`, `bullet_list_content` |
-| `code_inline` | `code_inline` |
-| `code_block` | `code_block` |
-| `fence` | `fence` |
-| `table` | `table` |
-| `thead` | `thead` |
-| `tbody` | `tbody` |
-| `th` | `th` |
-| `tr` | `tr` |
-| `td` | `td` |
-| `link` | `link` |
-| `blocklink` | `blocklink` |
-| `image` | `image` |
-| `text` | `text` |
-| `textgroup` | `textgroup` |
-| `paragraph` | `paragraph` |
-| `hardbreak` | `hardbreak` |
-| `softbreak` | `softbreak` |
-| `pre` | `pre` |
-| `inline` | `inline` |
-| `span` | `span` |
+| Render Rule | Style(s) |
+| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `body` | `body` |
+| `heading1` | `heading1` |
+| `heading2` | `heading2` |
+| `heading3` | `heading3` |
+| `heading4` | `heading4` |
+| `heading5` | `heading5` |
+| `heading6` | `heading6` |
+| `hr` | `hr` |
+| `strong` | `strong` |
+| `em` | `em` |
+| `s` | `s` |
+| `blockquote` | `blockquote` |
+| `bullet_list` | `bullet_list` |
+| `ordered_list` | `ordered_list` |
+| `list_item` | `list_item` - This is a special case that contains a set of pseudo classes that don't align to the render rule: `ordered_list_icon`, `ordered_list_content`, `bullet_list_icon`, `bullet_list_content` |
+| `code_inline` | `code_inline` |
+| `code_block` | `code_block` |
+| `fence` | `fence` |
+| `table` | `table` |
+| `thead` | `thead` |
+| `tbody` | `tbody` |
+| `th` | `th` |
+| `tr` | `tr` |
+| `td` | `td` |
+| `link` | `link` |
+| `blocklink` | `blocklink` |
+| `image` | `image` |
+| `text` | `text` |
+| `textgroup` | `textgroup` |
+| `paragraph` | `paragraph` |
+| `hardbreak` | `hardbreak` |
+| `softbreak` | `softbreak` |
+| `pre` | `pre` |
+| `inline` | `inline` |
+| `span` | `span` |
# Handling Links
@@ -988,7 +979,7 @@ const onLinkPress = (url) => {
// some custom logic
return false;
}
-
+
// return true to open with `Linking.openURL
// return false to handle it yourself
return true
@@ -1071,7 +1062,6 @@ export default App;
-
# Disabling Specific Types of Markdown
You can dissable any type of markdown you want, which is very useful in a mobile environment, by passing the markdownit property like below. Note that for convenience we also export the `MarkdownIt` instance so you don't have to include it as a project dependency directly just to remove some types of markdown.
@@ -1117,7 +1107,6 @@ export default App;
A full list of things you can turn off is [here](https://github.com/markdown-it/markdown-it/blob/master/lib/presets/commonmark.js)
-
### Pre Processing
It is possible to need to pre-process the data outside of this library ([related discussion here](https://github.com/iamacup/react-native-markdown-display/issues/79)). As a result, you can pass an AST tree directly as the children like this:
@@ -1160,7 +1149,6 @@ const App: () => React$Node = () => {
export default App;
```
-
### Other Notes
This is a fork of [react-native-markdown-renderer](https://github.com/mientjan/react-native-markdown-renderer), a library that unfortunately has not been updated for some time so i took all of the outstanding pull requests from that library and tested + merged as necessary.
diff --git a/demo/.gitignore b/demo/.gitignore
new file mode 100644
index 00000000..05647d55
--- /dev/null
+++ b/demo/.gitignore
@@ -0,0 +1,35 @@
+# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
+
+# dependencies
+node_modules/
+
+# Expo
+.expo/
+dist/
+web-build/
+
+# Native
+*.orig.*
+*.jks
+*.p8
+*.p12
+*.key
+*.mobileprovision
+
+# Metro
+.metro-health-check*
+
+# debug
+npm-debug.*
+yarn-debug.*
+yarn-error.*
+
+# macOS
+.DS_Store
+*.pem
+
+# local env files
+.env*.local
+
+# typescript
+*.tsbuildinfo
diff --git a/demo/App.tsx b/demo/App.tsx
new file mode 100644
index 00000000..d304476b
--- /dev/null
+++ b/demo/App.tsx
@@ -0,0 +1,55 @@
+import { StatusBar } from "expo-status-bar";
+import { Text, View } from "react-native";
+
+import Markdown from "@ukdanceblue/react-native-markdown-display";
+import { useState } from "react";
+import Picker from "react-native-picker-select";
+import * as Samples from "./sampleFiles";
+
+const sampleList = Object.entries(Samples);
+
+export default function App() {
+ const [selectedSample, setSelectedSample] = useState(sampleList[0][1]);
+
+ return (
+
+
+ ({label: key, value}))}
+ onValueChange={(value: string) => {
+ setSelectedSample(value);
+ }}
+ />
+
+
+ {selectedSample}
+
+
+ {selectedSample}
+
+
+
+ );
+}
diff --git a/demo/app.json b/demo/app.json
new file mode 100644
index 00000000..bc8f04e8
--- /dev/null
+++ b/demo/app.json
@@ -0,0 +1,27 @@
+{
+ "expo": {
+ "name": "demo",
+ "slug": "demo",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "userInterfaceStyle": "light",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "ios": {
+ "supportsTablet": true
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#ffffff"
+ }
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/demo/assets/adaptive-icon.png b/demo/assets/adaptive-icon.png
new file mode 100644
index 00000000..03d6f6b6
Binary files /dev/null and b/demo/assets/adaptive-icon.png differ
diff --git a/demo/assets/favicon.png b/demo/assets/favicon.png
new file mode 100644
index 00000000..e75f697b
Binary files /dev/null and b/demo/assets/favicon.png differ
diff --git a/demo/assets/icon.png b/demo/assets/icon.png
new file mode 100644
index 00000000..a0b1526f
Binary files /dev/null and b/demo/assets/icon.png differ
diff --git a/demo/assets/splash.png b/demo/assets/splash.png
new file mode 100644
index 00000000..0e89705a
Binary files /dev/null and b/demo/assets/splash.png differ
diff --git a/demo/babel.config.js b/demo/babel.config.js
new file mode 100644
index 00000000..2900afe9
--- /dev/null
+++ b/demo/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = function(api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ };
+};
diff --git a/demo/package.json b/demo/package.json
new file mode 100644
index 00000000..bdca7ce5
--- /dev/null
+++ b/demo/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "demo",
+ "version": "1.0.0",
+ "main": "expo/AppEntry.js",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "web": "expo start --web"
+ },
+ "dependencies": {
+ "@expo/metro-runtime": "~3.2.3",
+ "@react-native-picker/picker": "2.7.5",
+ "@ukdanceblue/react-native-markdown-display": "^0.1.3",
+ "expo": "~51.0.28",
+ "expo-status-bar": "~1.12.1",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-native": "0.74.5",
+ "react-native-picker-select": "^9.3.1",
+ "react-native-web": "~0.19.10"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.20.0",
+ "@types/react": "~18.2.45",
+ "typescript": "^5.1.3"
+ },
+ "private": true
+}
diff --git a/demo/sampleFiles.ts b/demo/sampleFiles.ts
new file mode 100644
index 00000000..a0272bab
--- /dev/null
+++ b/demo/sampleFiles.ts
@@ -0,0 +1,246 @@
+export const markdownItDemo = `---
+__Advertisement :)__
+
+- __[pica](https://nodeca.github.io/pica/demo/)__ - high quality and fast image
+ resize in browser.
+- __[babelfish](https://github.com/nodeca/babelfish/)__ - developer friendly
+ i18n with plurals support and easy syntax.
+
+You will like those projects!
+
+---
+
+# h1 Heading 8-)
+## h2 Heading
+### h3 Heading
+#### h4 Heading
+##### h5 Heading
+###### h6 Heading
+
+
+## Horizontal Rules
+
+___
+
+---
+
+***
+
+
+## Typographic replacements
+
+Enable typographer option to see result.
+
+(c) (C) (r) (R) (tm) (TM) (p) (P) +-
+
+test.. test... test..... test?..... test!....
+
+!!!!!! ???? ,, -- ---
+
+"Smartypants, double quotes" and 'single quotes'
+
+
+## Emphasis
+
+**This is bold text**
+
+__This is bold text__
+
+*This is italic text*
+
+_This is italic text_
+
+~~Strikethrough~~
+
+
+## Blockquotes
+
+
+> Blockquotes can also be nested...
+>> ...by using additional greater-than signs right next to each other...
+> > > ...or with spaces between arrows.
+
+
+## Lists
+
+Unordered
+
++ Create a list by starting a line with \`+\`, \`-\`, or \`*\`
++ Sub-lists are made by indenting 2 spaces:
+ - Marker character change forces new list start:
+ * Ac tristique libero volutpat at
+ + Facilisis in pretium nisl aliquet
+ - Nulla volutpat aliquam velit
++ Very easy!
+
+Ordered
+
+1. Lorem ipsum dolor sit amet
+2. Consectetur adipiscing elit
+3. Integer molestie lorem at massa
+
+
+1. You can use sequential numbers...
+1. ...or keep all the numbers as \`1.\`
+
+Start numbering with offset:
+
+57. foo
+1. bar
+
+
+## Code
+
+Inline \`code\`
+
+Indented code
+
+ // Some comments
+ line 1 of code
+ line 2 of code
+ line 3 of code
+
+
+Block code "fences"
+
+\`\`\`
+Sample text here...
+\`\`\`
+
+Syntax highlighting
+
+\`\`\` js
+var foo = function (bar) {
+ return bar++;
+};
+
+console.log(foo(5));
+\`\`\`
+
+## Tables
+
+| Option | Description |
+| ------ | ----------- |
+| data | path to data files to supply the data that will be passed into templates. |
+| engine | engine to be used for processing templates. Handlebars is the default. |
+| ext | extension to be used for dest files. |
+
+Right aligned columns
+
+| Option | Description |
+| ------:| -----------:|
+| data | path to data files to supply the data that will be passed into templates. |
+| engine | engine to be used for processing templates. Handlebars is the default. |
+| ext | extension to be used for dest files. |
+
+
+## Links
+
+[link text](http://dev.nodeca.com)
+
+[link with title](http://nodeca.github.io/pica/demo/ "title text!")
+
+Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
+
+
+## Images
+
+
+
+
+Like links, Images also have a footnote style syntax
+
+![Alt text][id]
+
+With a reference later in the document defining the URL location:
+
+[id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat"
+
+
+## Plugins
+
+The killer feature of \`markdown-it\` is very effective support of
+[syntax plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin).
+
+
+### [Emojies](https://github.com/markdown-it/markdown-it-emoji)
+
+> Classic markup: :wink: :cry: :laughing: :yum:
+>
+> Shortcuts (emoticons): :-) :-( 8-) ;)
+
+see [how to change output](https://github.com/markdown-it/markdown-it-emoji#change-output) with twemoji.
+
+
+### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup)
+
+- 19^th^
+- H~2~O
+
+
+### [\\](https://github.com/markdown-it/markdown-it-ins)
+
+++Inserted text++
+
+
+### [\\](https://github.com/markdown-it/markdown-it-mark)
+
+==Marked text==
+
+
+### [Footnotes](https://github.com/markdown-it/markdown-it-footnote)
+
+Footnote 1 link[^first].
+
+Footnote 2 link[^second].
+
+Inline footnote^[Text of inline footnote] definition.
+
+Duplicated footnote reference[^second].
+
+[^first]: Footnote **can have markup**
+
+ and multiple paragraphs.
+
+[^second]: Footnote text.
+
+
+### [Definition lists](https://github.com/markdown-it/markdown-it-deflist)
+
+Term 1
+
+: Definition 1
+with lazy continuation.
+
+Term 2 with *inline markup*
+
+: Definition 2
+
+ { some code, part of Definition 2 }
+
+ Third paragraph of definition 2.
+
+_Compact style:_
+
+Term 1
+ ~ Definition 1
+
+Term 2
+ ~ Definition 2a
+ ~ Definition 2b
+
+
+### [Abbreviations](https://github.com/markdown-it/markdown-it-abbr)
+
+This is HTML abbreviation example.
+
+It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on.
+
+*[HTML]: Hyper Text Markup Language
+
+### [Custom containers](https://github.com/markdown-it/markdown-it-container)
+
+::: warning
+*here be dragons*
+:::
+`
\ No newline at end of file
diff --git a/demo/tsconfig.json b/demo/tsconfig.json
new file mode 100644
index 00000000..b9567f60
--- /dev/null
+++ b/demo/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "expo/tsconfig.base",
+ "compilerOptions": {
+ "strict": true
+ }
+}
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 00000000..bc2f9e54
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,28 @@
+import eslint from "@eslint/js";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+ eslint.configs.recommended,
+ ...tseslint.configs.strictTypeChecked,
+ ...tseslint.configs.stylisticTypeChecked,
+ {
+ languageOptions: {
+ parserOptions: {
+ projectService: {
+ allowDefaultProject: ["eslint.config.mjs"],
+ },
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ },
+ {
+ rules: {
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/no-unused-vars": ["error", {argsIgnorePattern: "^_"}],
+ },
+ },
+ {
+ ignores: ["dist/"],
+ },
+);
diff --git a/package.json b/package.json
index 455dcb34..537319d3 100644
--- a/package.json
+++ b/package.json
@@ -1,19 +1,7 @@
{
- "name": "@jonasmerlin/react-native-markdown-display",
- "version": "1.0.2",
+ "name": "@ukdanceblue/react-native-markdown-display",
+ "version": "0.1.6",
"description": "Markdown renderer for react-native, with CommonMark spec support + adds syntax extensions & sugar (URL autolinking, typographer), originally created by Mient-jan Stelling as react-native-markdown-renderer",
- "main": "src/index.js",
- "types": "src/index.d.ts",
- "scripts": {
- "lint": "eslint --fix --cache ./src"
- },
- "pre-commit": [
- "lint"
- ],
- "repository": {
- "type": "git",
- "url": "git+https://github.com/jonasmerlin/react-native-markdown-display.git"
- },
"keywords": [
"react",
"react-native",
@@ -22,35 +10,49 @@
"commonmark",
"markdown-it"
],
- "author": "Mient-jan Stelling and Tom Pickard + others from the community",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ukdanceblue/react-native-markdown-display.git"
+ },
+ "private": false,
"license": "MIT",
- "bugs": {
- "url": "https://github.com/jonasmerlin/react-native-markdown-display/issues"
+ "author": "Mient-jan Stelling and Tom Pickard + others from the community, ported to TypeScript by Tag Howard",
+ "type": "module",
+ "exports": {
+ ".": {
+ "react-native": "./dist/index.js",
+ "default": "./dist/index.js",
+ "types": "./dist/index.d.ts"
+ }
},
- "homepage": "https://github.com/jonasmerlin/react-native-markdown-display/",
- "dependencies": {
- "css-to-react-native": "^3.0.0",
- "markdown-it": "^13.0.1",
- "prop-types": "^15.8.1",
- "react-native-fit-image": "^1.5.5"
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "scripts": {
+ "build": "tsc",
+ "check": "npm run lint && npm run format && npm run build -- --noEmit",
+ "format": "prettier . --check",
+ "format-fix": "prettier . --write",
+ "lint": "eslint .",
+ "lint-fix": "eslint . --fix",
+ "watch": "tsc -w"
},
- "peerDependencies": {
- "react": "^16.2.0 || ^18.0.0",
- "react-native": ">=0.50.4"
+ "dependencies": {
+ "css-to-react-native": "^3.2.0",
+ "markdown-it": "^14.1.0"
},
"devDependencies": {
- "@types/markdown-it": "^12.2.3",
- "@types/react-native": ">=0.67.7",
- "@babel/core": "^7.17.10",
- "@babel/runtime": "^7.17.9",
- "@react-native-community/eslint-config": "^3.0.2",
- "@typescript-eslint/parser": "^5.23.0",
- "eslint": "^8.15.0",
- "json-schema": "^0.4.0",
- "pre-commit": "1.2.2",
- "typescript": "^4.6.4"
+ "@eslint/js": "^9.13.0",
+ "@types/eslint__js": "^8.42.3",
+ "@types/markdown-it": "^14.1.2",
+ "@types/react": "^18.0.0",
+ "eslint": "^9.13.0",
+ "prettier": "^3.3.3",
+ "react-native": "^0.76.0",
+ "typescript": "^5.6.3",
+ "typescript-eslint": "^8.12.0"
},
- "directories": {
- "doc": "doc"
+ "peerDependencies": {
+ "react": ">=18.0.0",
+ "react-native": ">=0.74.0"
}
}
diff --git a/src/index.d.ts b/src/index.d.ts
deleted file mode 100644
index ae8e8e01..00000000
--- a/src/index.d.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-// tslint:disable:max-classes-per-file
-import MarkdownIt from 'markdown-it';
-import Token from 'markdown-it/lib/token';
-import {ComponentType, ReactNode} from 'react';
-import {StyleSheet, View} from 'react-native';
-
-export function getUniqueID(): string;
-export function openUrl(url: string): void;
-
-export function hasParents(parents: any[], type: string): boolean;
-
-export type RenderFunction = (
- node: ASTNode,
- children: ReactNode[],
- parentNodes: ASTNode[],
- styles: any,
- styleObj?: any,
- // must have this so that we can have fixed overrides with more arguments
- ...args: any
-) => ReactNode;
-
-export type RenderLinkFunction = (
- node: ASTNode,
- children: ReactNode[],
- parentNodes: ASTNode[],
- styles: any,
- onLinkPress?: (url: string) => boolean,
-) => ReactNode;
-
-export type RenderImageFunction = (
- node: ASTNode,
- children: ReactNode[],
- parentNodes: ASTNode[],
- styles: any,
- allowedImageHandlers: string[],
- defaultImageHandler: string,
-) => ReactNode;
-
-export interface RenderRules {
- [name: string]: RenderFunction | undefined;
- link?: RenderLinkFunction;
- blocklink?: RenderLinkFunction;
- image?: RenderImageFunction;
-}
-
-export const renderRules: RenderRules;
-
-export interface MarkdownParser {
- parse: (value: string, options: any) => Token[];
-}
-
-export interface ASTNode {
- type: string;
- sourceType: string; // original source token name
- key: string;
- content: string;
- markup: string;
- tokenIndex: number;
- index: number;
- attributes: Record;
- children: ASTNode[];
-}
-
-export class AstRenderer {
- constructor(renderRules: RenderRules, style?: any);
- getRenderFunction(type: string): RenderFunction;
- renderNode(node: any, parentNodes: ReadonlyArray): ReactNode;
- render(nodes: ReadonlyArray): View;
-}
-
-export function parser(
- source: string,
- renderer: (node: ASTNode) => View,
- parser: MarkdownParser,
-): any;
-
-export function stringToTokens(
- source: string,
- markdownIt: MarkdownParser,
-): Token[];
-
-export function tokensToAST(tokens: ReadonlyArray): ASTNode[];
-
-export interface MarkdownProps {
- children?: ReactNode;
- rules?: RenderRules;
- style?: StyleSheet.NamedStyles;
- renderer?: AstRenderer;
- markdownit?: MarkdownIt;
- mergeStyle?: boolean;
- debugPrintTree?: boolean;
- onLinkPress?: (url: string) => boolean;
-}
-
-type MarkdownStatic = ComponentType;
-export const Markdown: MarkdownStatic;
-export type Markdown = MarkdownStatic;
-export {MarkdownIt};
-export default Markdown;
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index 59afec7c..00000000
--- a/src/index.js
+++ /dev/null
@@ -1,231 +0,0 @@
-/**
- * Base Markdown component
- * @author Mient-jan Stelling + contributors
- */
-
-import React, {useMemo} from 'react';
-import {Text, StyleSheet} from 'react-native';
-import PropTypes from 'prop-types';
-import parser from './lib/parser';
-import getUniqueID from './lib/util/getUniqueID';
-import hasParents from './lib/util/hasParents';
-import openUrl from './lib/util/openUrl';
-import tokensToAST from './lib/util/tokensToAST';
-import renderRules from './lib/renderRules';
-import AstRenderer from './lib/AstRenderer';
-import MarkdownIt from 'markdown-it';
-import removeTextStyleProps from './lib/util/removeTextStyleProps';
-import {styles} from './lib/styles';
-import {stringToTokens} from './lib/util/stringToTokens';
-import FitImage from 'react-native-fit-image';
-import textStyleProps from './lib/data/textStyleProps';
-
-export {
- getUniqueID,
- openUrl,
- hasParents,
- renderRules,
- AstRenderer,
- parser,
- stringToTokens,
- tokensToAST,
- MarkdownIt,
- styles,
- removeTextStyleProps,
- FitImage,
- textStyleProps,
-};
-
-// we use StyleSheet.flatten here to make sure we have an object, in case someone
-// passes in a StyleSheet.create result to the style prop
-const getStyle = (mergeStyle, style) => {
- let useStyles = {};
-
- if (mergeStyle === true && style !== null) {
- // make sure we get anything user defuned
- Object.keys(style).forEach((value) => {
- useStyles[value] = {
- ...StyleSheet.flatten(style[value]),
- };
- });
-
- // combine any existing styles
- Object.keys(styles).forEach((value) => {
- useStyles[value] = {
- ...styles[value],
- ...StyleSheet.flatten(style[value]),
- };
- });
- } else {
- useStyles = {
- ...styles,
- };
-
- if (style !== null) {
- Object.keys(style).forEach((value) => {
- useStyles[value] = {
- ...StyleSheet.flatten(style[value]),
- };
- });
- }
- }
-
- Object.keys(useStyles).forEach((value) => {
- useStyles['_VIEW_SAFE_' + value] = removeTextStyleProps(useStyles[value]);
- });
-
- return StyleSheet.create(useStyles);
-};
-
-const getRenderer = (
- renderer,
- rules,
- style,
- mergeStyle,
- onLinkPress,
- maxTopLevelChildren,
- topLevelMaxExceededItem,
- allowedImageHandlers,
- defaultImageHandler,
- debugPrintTree,
-) => {
- if (renderer && rules) {
- console.warn(
- 'react-native-markdown-display you are using renderer and rules at the same time. This is not possible, props.rules is ignored',
- );
- }
-
- if (renderer && style) {
- console.warn(
- 'react-native-markdown-display you are using renderer and style at the same time. This is not possible, props.style is ignored',
- );
- }
-
- // these checks are here to prevent extra overhead.
- if (renderer) {
- if (!(typeof renderer === 'function') || renderer instanceof AstRenderer) {
- return renderer;
- } else {
- throw new Error(
- 'Provided renderer is not compatible with function or AstRenderer. please change',
- );
- }
- } else {
- let useStyles = getStyle(mergeStyle, style);
-
- return new AstRenderer(
- {
- ...renderRules,
- ...(rules || {}),
- },
- useStyles,
- onLinkPress,
- maxTopLevelChildren,
- topLevelMaxExceededItem,
- allowedImageHandlers,
- defaultImageHandler,
- debugPrintTree,
- );
- }
-};
-
-const Markdown = React.memo(
- ({
- children,
- renderer = null,
- rules = null,
- style = null,
- mergeStyle = true,
- markdownit = MarkdownIt({
- typographer: true,
- }),
- onLinkPress,
- maxTopLevelChildren = null,
- topLevelMaxExceededItem = ...,
- allowedImageHandlers = [
- 'data:image/png;base64',
- 'data:image/gif;base64',
- 'data:image/jpeg;base64',
- 'https://',
- 'http://',
- ],
- defaultImageHandler = 'https://',
- debugPrintTree = false,
- }) => {
- const momoizedRenderer = useMemo(
- () =>
- getRenderer(
- renderer,
- rules,
- style,
- mergeStyle,
- onLinkPress,
- maxTopLevelChildren,
- topLevelMaxExceededItem,
- allowedImageHandlers,
- defaultImageHandler,
- debugPrintTree,
- ),
- [
- maxTopLevelChildren,
- onLinkPress,
- renderer,
- rules,
- style,
- mergeStyle,
- topLevelMaxExceededItem,
- allowedImageHandlers,
- defaultImageHandler,
- debugPrintTree,
- ],
- );
-
- const momoizedParser = useMemo(() => markdownit, [markdownit]);
-
- return parser(children, momoizedRenderer.render, momoizedParser);
- },
-);
-
-Markdown.propTypes = {
- children: PropTypes.oneOfType([PropTypes.node, PropTypes.array]).isRequired,
- renderer: PropTypes.oneOfType([
- PropTypes.func,
- PropTypes.instanceOf(AstRenderer),
- ]),
- onLinkPress: PropTypes.func,
- maxTopLevelChildren: PropTypes.number,
- topLevelMaxExceededItem: PropTypes.any,
- rules: (props, propName, componentName) => {
- let invalidProps = [];
- const prop = props[propName];
-
- if (!prop) {
- return;
- }
-
- if (typeof prop === 'object') {
- invalidProps = Object.keys(prop).filter(
- (key) => typeof prop[key] !== 'function',
- );
- }
-
- if (typeof prop !== 'object') {
- return new Error(
- `Invalid prop \`${propName}\` supplied to \`${componentName}\`. Must be of shape {[index:string]:function} `,
- );
- } else if (invalidProps.length > 0) {
- return new Error(
- `Invalid prop \`${propName}\` supplied to \`${componentName}\`. These ` +
- `props are not of type function \`${invalidProps.join(', ')}\` `,
- );
- }
- },
- markdownit: PropTypes.instanceOf(MarkdownIt),
- style: PropTypes.any,
- mergeStyle: PropTypes.bool,
- allowedImageHandlers: PropTypes.arrayOf(PropTypes.string),
- defaultImageHandler: PropTypes.string,
- debugPrintTree: PropTypes.bool,
-};
-
-export default Markdown;
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 00000000..06e8a008
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,216 @@
+import MarkdownIt from "markdown-it";
+import type { ReactNode } from "react";
+import React, { useMemo } from "react";
+import type { TextStyle, ViewStyle } from "react-native";
+import { StyleSheet, Text } from "react-native";
+
+import AstRenderer from "./lib/AstRenderer";
+import parser from "./lib/parser";
+import type { RenderRules } from "./lib/renderRules";
+import renderRules from "./lib/renderRules";
+import { styles } from "./lib/styles";
+import type { ASTNode } from "./lib/types";
+import removeTextStyleProps from "./lib/util/removeTextStyleProps";
+
+function getStyle(
+ mergeStyle: boolean,
+ style: StyleSheet.NamedStyles | undefined,
+): ReturnType {
+ let useStyles: Record = {};
+
+ if (mergeStyle && style != null) {
+ Object.keys(style).forEach((value) => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ useStyles[value] = {
+ // @ts-expect-error this is fine
+ ...StyleSheet.flatten(style[value]),
+ };
+ });
+
+ Object.keys(styles).forEach((value) => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ useStyles[value] = {
+ // @ts-expect-error this is fine
+ ...styles[value],
+ // @ts-expect-error this is fine
+ ...StyleSheet.flatten(style[value]),
+ };
+ });
+ } else {
+ // @ts-expect-error this is fine
+ useStyles = {
+ ...styles,
+ };
+
+ if (style != null) {
+ Object.keys(style).forEach((value) => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ useStyles[value] = {
+ // @ts-expect-error this is fine
+ ...StyleSheet.flatten(style[value]),
+ };
+ });
+ }
+ }
+
+ Object.keys(useStyles).forEach((value) => {
+ useStyles[`_VIEW_SAFE_${value}`] = removeTextStyleProps(useStyles[value]);
+ });
+
+ return StyleSheet.create(useStyles);
+}
+
+const getRenderer = (
+ renderer: AstRenderer | undefined,
+ rules: RenderRules | undefined,
+ style: StyleSheet.NamedStyles | undefined,
+ mergeStyle: boolean,
+ onLinkPress: ((url: string) => boolean) | undefined,
+ maxTopLevelChildren: number | null,
+ topLevelMaxExceededItem: React.ReactNode,
+ allowedImageHandlers: string[],
+ defaultImageHandler: string,
+ debugPrintTree: boolean,
+): AstRenderer => {
+ if (renderer && rules) {
+ console.warn(
+ "react-native-markdown-display you are using renderer and rules at the same time. This is not possible, props.rules is ignored",
+ );
+ }
+
+ if (renderer && style) {
+ console.warn(
+ "react-native-markdown-display you are using renderer and style at the same time. This is not possible, props.style is ignored",
+ );
+ }
+
+ if (renderer) {
+ if (
+ !(typeof renderer === "function") ||
+ (renderer as unknown) instanceof AstRenderer
+ ) {
+ return renderer;
+ } else {
+ throw new TypeError(
+ "Provided renderer is not compatible with function or AstRenderer. please change",
+ );
+ }
+ } else {
+ const useStyles = getStyle(mergeStyle, style);
+
+ return new AstRenderer(
+ {
+ ...renderRules,
+ ...(rules ?? {}),
+ },
+ useStyles,
+ onLinkPress,
+ maxTopLevelChildren,
+ topLevelMaxExceededItem,
+ allowedImageHandlers,
+ defaultImageHandler,
+ debugPrintTree,
+ );
+ }
+};
+
+export interface MarkdownProps {
+ children: string;
+ rules?: RenderRules;
+ style?: StyleSheet.NamedStyles;
+ renderer?: AstRenderer;
+ markdownit?: MarkdownIt;
+ mergeStyle?: boolean;
+ debugPrintTree?: boolean;
+ onLinkPress?: (url: string) => boolean;
+}
+
+const Markdown: React.FC<
+ MarkdownProps & {
+ maxTopLevelChildren?: number | null;
+ topLevelMaxExceededItem?: React.ReactNode;
+ allowedImageHandlers?: string[];
+ defaultImageHandler?: string;
+ }
+> = React.memo(
+ ({
+ children,
+ renderer = undefined,
+ rules = undefined,
+ style = undefined,
+ mergeStyle = true,
+ markdownit = MarkdownIt({
+ typographer: true,
+ }),
+ onLinkPress,
+ maxTopLevelChildren = null,
+ topLevelMaxExceededItem = ...,
+ allowedImageHandlers = [
+ "data:image/png;base64",
+ "data:image/gif;base64",
+ "data:image/jpeg;base64",
+ "https://",
+ "http://",
+ ],
+ defaultImageHandler = "https://",
+ debugPrintTree = false,
+ }: MarkdownProps & {
+ maxTopLevelChildren?: number | null;
+ topLevelMaxExceededItem?: React.ReactNode;
+ allowedImageHandlers?: string[];
+ defaultImageHandler?: string;
+ }) => {
+ const momoizedRenderer = useMemo(
+ () =>
+ getRenderer(
+ renderer,
+ rules,
+ style,
+ mergeStyle,
+ onLinkPress,
+ maxTopLevelChildren,
+ topLevelMaxExceededItem,
+ allowedImageHandlers,
+ defaultImageHandler,
+ debugPrintTree,
+ ),
+ [
+ maxTopLevelChildren,
+ onLinkPress,
+ renderer,
+ rules,
+ style,
+ mergeStyle,
+ topLevelMaxExceededItem,
+ allowedImageHandlers,
+ defaultImageHandler,
+ debugPrintTree,
+ ],
+ );
+
+ const memoizedParser = useMemo(() => markdownit, [markdownit]);
+
+ return parser(
+ children,
+ (nodes: readonly ASTNode[]): ReactNode => momoizedRenderer.render(nodes),
+ memoizedParser,
+ );
+ },
+);
+
+export default Markdown;
+
+export { default as MarkdownIt } from "markdown-it";
+export { default as AstRenderer } from "./lib/AstRenderer";
+export { default as textStyleProps } from "./lib/data/textStyleProps";
+export { default as parser } from "./lib/parser";
+export { default as renderRules } from "./lib/renderRules";
+export { styles } from "./lib/styles";
+export type { ASTNode } from "./lib/types";
+export { default as getUniqueID } from "./lib/util/getUniqueID";
+export { default as hasParents } from "./lib/util/hasParents";
+export { default as openUrl } from "./lib/util/openUrl";
+export { default as removeTextStyleProps } from "./lib/util/removeTextStyleProps";
+export { stringToTokens } from "./lib/util/stringToTokens";
+export { default as tokensToAST } from "./lib/util/tokensToAST";
+
diff --git a/src/lib/AstRenderer.js b/src/lib/AstRenderer.js
deleted file mode 100644
index 608e9455..00000000
--- a/src/lib/AstRenderer.js
+++ /dev/null
@@ -1,186 +0,0 @@
-import {StyleSheet} from 'react-native';
-
-import getUniqueID from './util/getUniqueID';
-import convertAdditionalStyles from './util/convertAdditionalStyles';
-
-import textStyleProps from './data/textStyleProps';
-
-export default class AstRenderer {
- /**
- *
- * @param {Object.} renderRules
- * @param {any} style
- */
- constructor(
- renderRules,
- style,
- onLinkPress,
- maxTopLevelChildren,
- topLevelMaxExceededItem,
- allowedImageHandlers,
- defaultImageHandler,
- debugPrintTree,
- ) {
- this._renderRules = renderRules;
- this._style = style;
- this._onLinkPress = onLinkPress;
- this._maxTopLevelChildren = maxTopLevelChildren;
- this._topLevelMaxExceededItem = topLevelMaxExceededItem;
- this._allowedImageHandlers = allowedImageHandlers;
- this._defaultImageHandler = defaultImageHandler;
- this._debugPrintTree = debugPrintTree;
- }
-
- /**
- *
- * @param {string} type
- * @return {string}
- */
- getRenderFunction = (type) => {
- const renderFunction = this._renderRules[type];
-
- if (!renderFunction) {
- console.warn(
- `Warning, unknown render rule encountered: ${type}. 'unknown' render rule used (by default, returns null - nothing rendered)`,
- );
- return this._renderRules.unknown;
- }
-
- return renderFunction;
- };
-
- /**
- *
- * @param node
- * @param parentNodes
- * @return {*}
- */
- renderNode = (node, parentNodes, isRoot = false) => {
- const renderFunction = this.getRenderFunction(node.type);
- const parents = [...parentNodes];
-
- if (this._debugPrintTree === true) {
- let str = '';
-
- for (let a = 0; a < parents.length; a++) {
- str = str + '-';
- }
-
- console.log(`${str}${node.type}`);
- }
-
- parents.unshift(node);
-
- // calculate the children first
- let children = node.children.map((value) => {
- return this.renderNode(value, parents);
- });
-
- // render any special types of nodes that have different renderRule function signatures
-
- if (node.type === 'link' || node.type === 'blocklink') {
- return renderFunction(
- node,
- children,
- parentNodes,
- this._style,
- this._onLinkPress,
- );
- }
-
- if (node.type === 'image') {
- return renderFunction(
- node,
- children,
- parentNodes,
- this._style,
- this._allowedImageHandlers,
- this._defaultImageHandler,
- );
- }
-
- // We are at the bottom of some tree - grab all the parent styles
- // this effectively grabs the styles from parents and
- // applies them in order of priority parent (least) to child (most)
- // to allow styling global, then lower down things individually
-
- // we have to handle list_item seperately here because they have some child
- // pseudo classes that need the additional style props from parents passed down to them
- if (children.length === 0 || node.type === 'list_item') {
- const styleObj = {};
-
- for (let a = parentNodes.length - 1; a > -1; a--) {
- // grab and additional attributes specified by markdown-it
- let refStyle = {};
-
- if (
- parentNodes[a].attributes &&
- parentNodes[a].attributes.style &&
- typeof parentNodes[a].attributes.style === 'string'
- ) {
- refStyle = convertAdditionalStyles(parentNodes[a].attributes.style);
- }
-
- // combine in specific styles for the object
- if (this._style[parentNodes[a].type]) {
- refStyle = {
- ...refStyle,
- ...StyleSheet.flatten(this._style[parentNodes[a].type]),
- };
-
- // workaround for list_items and their content cascading down the tree
- if (parentNodes[a].type === 'list_item') {
- let contentStyle = {};
-
- if (parentNodes[a + 1].type === 'bullet_list') {
- contentStyle = this._style.bullet_list_content;
- } else if (parentNodes[a + 1].type === 'ordered_list') {
- contentStyle = this._style.ordered_list_content;
- }
-
- refStyle = {
- ...refStyle,
- ...StyleSheet.flatten(contentStyle),
- };
- }
- }
-
- // then work out if any of them are text styles that should be used in the end.
- const arr = Object.keys(refStyle);
-
- for (let b = 0; b < arr.length; b++) {
- if (textStyleProps.includes(arr[b])) {
- styleObj[arr[b]] = refStyle[arr[b]];
- }
- }
- }
-
- return renderFunction(node, children, parentNodes, this._style, styleObj);
- }
-
- // cull top level children
-
- if (
- isRoot === true &&
- this._maxTopLevelChildren &&
- children.length > this._maxTopLevelChildren
- ) {
- children = children.slice(0, this._maxTopLevelChildren);
- children.push(this._topLevelMaxExceededItem);
- }
-
- // render anythign else that has a normal signature
-
- return renderFunction(node, children, parentNodes, this._style);
- };
-
- /**
- *
- * @param nodes
- * @return {*}
- */
- render = (nodes) => {
- const root = {type: 'body', key: getUniqueID(), children: nodes};
- return this.renderNode(root, [], true);
- };
-}
diff --git a/src/lib/AstRenderer.ts b/src/lib/AstRenderer.ts
new file mode 100644
index 00000000..003cc62e
--- /dev/null
+++ b/src/lib/AstRenderer.ts
@@ -0,0 +1,173 @@
+import {StyleSheet, ViewStyle} from "react-native";
+
+import convertAdditionalStyles from "./util/convertAdditionalStyles";
+import getUniqueID from "./util/getUniqueID";
+
+import {ReactNode} from "react";
+import textStyleProps from "./data/textStyleProps";
+import {RenderRules} from "./renderRules";
+import {ASTNode} from "./types";
+
+export default class AstRenderer {
+ constructor(
+ private renderRules: RenderRules,
+ private style: Record,
+ private onLinkPress: ((url: string) => boolean) | undefined,
+ private maxTopLevelChildren: number | null,
+ private topLevelMaxExceededItem: React.ReactNode,
+ private allowedImageHandlers: string[],
+ private defaultImageHandler: string,
+ private debugPrintTree: boolean,
+ ) {}
+
+ getRenderFunction(type: R): RenderRules[R] {
+ const renderFunction = this.renderRules[type];
+
+ if (!renderFunction as unknown) {
+ console.warn(
+ `Warning, unknown render rule encountered: ${type}. 'unknown' render rule used (by default, returns null - nothing rendered)`,
+ );
+ return this.renderRules.unknown;
+ }
+
+ return renderFunction;
+ }
+
+ renderNode(node: ASTNode, parentNodes: ASTNode[], isRoot = false): ReactNode {
+ const parents = [...parentNodes];
+
+ if (this.debugPrintTree) {
+ let str = "";
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ for (const _ of parents) {
+ str = str + "-";
+ }
+
+ console.log(`${str}${node.type}`);
+ }
+
+ parents.unshift(node);
+
+ // calculate the children first
+ let children = node.children.map((value) => {
+ return this.renderNode(value, parents);
+ });
+
+ // render any special types of nodes that have different renderRule function signatures
+
+ if (node.type === "link" || node.type === "blocklink") {
+ return this.getRenderFunction(node.type)(
+ node,
+ children,
+ parentNodes,
+ this.style,
+ this.onLinkPress,
+ );
+ }
+
+ if (node.type === "image") {
+ return this.getRenderFunction(node.type)(
+ node,
+ children,
+ parentNodes,
+ this.style,
+ this.allowedImageHandlers,
+ this.defaultImageHandler,
+ );
+ }
+
+ // We are at the bottom of some tree - grab all the parent styles
+ // this effectively grabs the styles from parents and
+ // applies them in order of priority parent (least) to child (most)
+ // to allow styling global, then lower down things individually
+
+ // we have to handle list_item seperately here because they have some child
+ // pseudo classes that need the additional style props from parents passed down to them
+ if (children.length === 0 || node.type === "list_item") {
+ const styleObj = {};
+
+ for (let a = parentNodes.length - 1; a > -1; a--) {
+ // grab and additional attributes specified by markdown-it
+ let refStyle = {};
+
+ if (
+ parentNodes[a].attributes?.style &&
+ typeof parentNodes[a].attributes?.style === "string"
+ ) {
+ refStyle = convertAdditionalStyles(
+ String(parentNodes[a].attributes?.style),
+ );
+ }
+
+ // combine in specific styles for the object
+ if (this.style[parentNodes[a].type] as unknown) {
+ refStyle = {
+ ...refStyle,
+ ...StyleSheet.flatten(this.style[parentNodes[a].type]),
+ };
+
+ // workaround for list_items and their content cascading down the tree
+ if (parentNodes[a].type === "list_item") {
+ let contentStyle = {};
+
+ if (parentNodes[a + 1].type === "bullet_list") {
+ contentStyle = this.style.bullet_list_content;
+ } else if (parentNodes[a + 1].type === "ordered_list") {
+ contentStyle = this.style.ordered_list_content;
+ }
+
+ refStyle = {
+ ...refStyle,
+ ...StyleSheet.flatten(contentStyle),
+ };
+ }
+ }
+
+ // then work out if any of them are text styles that should be used in the end.
+ const arr = Object.keys(refStyle) as (keyof ViewStyle)[];
+
+ for (const key of arr) {
+ if (textStyleProps.includes(key)) {
+ // @ts-expect-error this is fine
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ styleObj[key] = refStyle[key];
+ }
+ }
+ }
+
+ return this.getRenderFunction(node.type)(
+ node,
+ children,
+ parentNodes,
+ this.style,
+ styleObj,
+ );
+ }
+
+ // cull top level children
+
+ if (
+ isRoot &&
+ this.maxTopLevelChildren &&
+ children.length > this.maxTopLevelChildren
+ ) {
+ children = children.slice(0, this.maxTopLevelChildren);
+ children.push(this.topLevelMaxExceededItem);
+ }
+
+ // render anythign else that has a normal signature
+
+ return this.getRenderFunction(node.type)(
+ node,
+ children,
+ parentNodes,
+ this.style,
+ );
+ }
+
+ render(nodes: readonly ASTNode[]): ReactNode {
+ const root: ASTNode = {type: "body", key: getUniqueID(), children: nodes};
+ return this.renderNode(root, [], true);
+ }
+}
diff --git a/src/lib/data/textStyleProps.js b/src/lib/data/textStyleProps.js
deleted file mode 100644
index c48139d0..00000000
--- a/src/lib/data/textStyleProps.js
+++ /dev/null
@@ -1,21 +0,0 @@
-export default [
- 'textShadowOffset',
- 'color',
- 'fontSize',
- 'fontStyle',
- 'fontWeight',
- 'lineHeight',
- 'textAlign',
- 'textDecorationLine',
- 'textShadowColor',
- 'fontFamily',
- 'textShadowRadius',
- 'includeFontPadding',
- 'textAlignVertical',
- 'fontVariant',
- 'letterSpacing',
- 'textDecorationColor',
- 'textDecorationStyle',
- 'textTransform',
- 'writingDirection',
-];
diff --git a/src/lib/data/textStyleProps.ts b/src/lib/data/textStyleProps.ts
new file mode 100644
index 00000000..a9350870
--- /dev/null
+++ b/src/lib/data/textStyleProps.ts
@@ -0,0 +1,21 @@
+export default [
+ "textShadowOffset",
+ "color",
+ "fontSize",
+ "fontStyle",
+ "fontWeight",
+ "lineHeight",
+ "textAlign",
+ "textDecorationLine",
+ "textShadowColor",
+ "fontFamily",
+ "textShadowRadius",
+ "includeFontPadding",
+ "textAlignVertical",
+ "fontVariant",
+ "letterSpacing",
+ "textDecorationColor",
+ "textDecorationStyle",
+ "textTransform",
+ "writingDirection",
+];
diff --git a/src/lib/parser.js b/src/lib/parser.js
deleted file mode 100644
index 4e897933..00000000
--- a/src/lib/parser.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import tokensToAST from './util/tokensToAST';
-import {stringToTokens} from './util/stringToTokens';
-import {cleanupTokens} from './util/cleanupTokens';
-import groupTextTokens from './util/groupTextTokens';
-import omitListItemParagraph from './util/omitListItemParagraph';
-
-/**
- *
- * @param {string} source
- * @param {function} [renderer]
- * @param {AstRenderer} [markdownIt]
- * @return {View}
- */
-export default function parser(source, renderer, markdownIt) {
- if (Array.isArray(source)) {
- return renderer(source);
- }
-
- let tokens = stringToTokens(source, markdownIt);
- tokens = cleanupTokens(tokens);
- tokens = groupTextTokens(tokens);
- tokens = omitListItemParagraph(tokens);
-
- const astTree = tokensToAST(tokens);
-
- return renderer(astTree);
-}
diff --git a/src/lib/parser.ts b/src/lib/parser.ts
new file mode 100644
index 00000000..b5cce86b
--- /dev/null
+++ b/src/lib/parser.ts
@@ -0,0 +1,27 @@
+import MarkdownIt, { Token } from "markdown-it";
+import { ReactNode } from "react";
+import { ASTNode } from "./types";
+import { cleanupTokens } from "./util/cleanupTokens";
+import groupTextTokens from "./util/groupTextTokens";
+import omitListItemParagraph from "./util/omitListItemParagraph";
+import { stringToTokens } from "./util/stringToTokens";
+import tokensToAST from "./util/tokensToAST";
+
+export default function parser(
+ source: string,
+ renderer: (source: ASTNode[]) => ReactNode,
+ markdownIt: MarkdownIt,
+): ReactNode {
+ if (Array.isArray(source)) {
+ return renderer(source);
+ }
+
+ let tokens: (Token)[] = stringToTokens(source, markdownIt);
+ tokens = cleanupTokens(tokens);
+ tokens = groupTextTokens(tokens);
+ tokens = omitListItemParagraph(tokens);
+
+ const astTree = tokensToAST(tokens);
+
+ return renderer(astTree);
+}
diff --git a/src/lib/renderRules.js b/src/lib/renderRules.js
deleted file mode 100644
index 6f2ed8d4..00000000
--- a/src/lib/renderRules.js
+++ /dev/null
@@ -1,347 +0,0 @@
-import React from 'react';
-import {
- Text,
- TouchableWithoutFeedback,
- View,
- Platform,
- StyleSheet,
-} from 'react-native';
-import FitImage from 'react-native-fit-image';
-
-import openUrl from './util/openUrl';
-import hasParents from './util/hasParents';
-
-import textStyleProps from './data/textStyleProps';
-
-const renderRules = {
- // when unknown elements are introduced, so it wont break
- unknown: (node, children, parent, styles) => null,
-
- // The main container
- body: (node, children, parent, styles) => (
-
- {children}
-
- ),
-
- // Headings
- heading1: (node, children, parent, styles) => (
-
- {children}
-
- ),
- heading2: (node, children, parent, styles) => (
-
- {children}
-
- ),
- heading3: (node, children, parent, styles) => (
-
- {children}
-
- ),
- heading4: (node, children, parent, styles) => (
-
- {children}
-
- ),
- heading5: (node, children, parent, styles) => (
-
- {children}
-
- ),
- heading6: (node, children, parent, styles) => (
-
- {children}
-
- ),
-
- // Horizontal Rule
- hr: (node, children, parent, styles) => (
-
- ),
-
- // Emphasis
- strong: (node, children, parent, styles) => (
-
- {children}
-
- ),
- em: (node, children, parent, styles) => (
-
- {children}
-
- ),
- s: (node, children, parent, styles) => (
-
- {children}
-
- ),
-
- // Blockquotes
- blockquote: (node, children, parent, styles) => (
-
- {children}
-
- ),
-
- // Lists
- bullet_list: (node, children, parent, styles) => (
-
- {children}
-
- ),
- ordered_list: (node, children, parent, styles) => (
-
- {children}
-
- ),
- // this is a unique and quite annoying render rule because it has
- // child items that can be styled (the list icon and the list content)
- // outside of the AST tree so there are some work arounds in the
- // AST renderer specifically to get the styling right here
- list_item: (node, children, parent, styles, inheritedStyles = {}) => {
- // we need to grab any text specific stuff here that is applied on the list_item style
- // and apply it onto bullet_list_icon. the AST renderer has some workaround code to make
- // the content classes apply correctly to the child AST tree items as well
- // as code that forces the creation of the inheritedStyles object for list_items
- const refStyle = {
- ...inheritedStyles,
- ...StyleSheet.flatten(styles.list_item),
- };
-
- const arr = Object.keys(refStyle);
-
- const modifiedInheritedStylesObj = {};
-
- for (let b = 0; b < arr.length; b++) {
- if (textStyleProps.includes(arr[b])) {
- modifiedInheritedStylesObj[arr[b]] = refStyle[arr[b]];
- }
- }
-
- if (hasParents(parent, 'bullet_list')) {
- return (
-
-
- {Platform.select({
- android: '\u2022',
- ios: '\u00B7',
- default: '\u2022',
- })}
-
- {children}
-
- );
- }
-
- if (hasParents(parent, 'ordered_list')) {
- const orderedListIndex = parent.findIndex(
- (el) => el.type === 'ordered_list',
- );
-
- const orderedList = parent[orderedListIndex];
- let listItemNumber;
-
- if (orderedList.attributes && orderedList.attributes.start) {
- listItemNumber = orderedList.attributes.start + node.index;
- } else {
- listItemNumber = node.index + 1;
- }
-
- return (
-
-
- {listItemNumber}
- {node.markup}
-
- {children}
-
- );
- }
-
- // we should not need this, but just in case
- return (
-
- {children}
-
- );
- },
-
- // Code
- code_inline: (node, children, parent, styles, inheritedStyles = {}) => (
-
- {node.content}
-
- ),
- code_block: (node, children, parent, styles, inheritedStyles = {}) => {
- // we trim new lines off the end of code blocks because the parser sends an extra one.
- let {content} = node;
-
- if (
- typeof node.content === 'string' &&
- node.content.charAt(node.content.length - 1) === '\n'
- ) {
- content = node.content.substring(0, node.content.length - 1);
- }
-
- return (
-
- {content}
-
- );
- },
- fence: (node, children, parent, styles, inheritedStyles = {}) => {
- // we trim new lines off the end of code blocks because the parser sends an extra one.
- let {content} = node;
-
- if (
- typeof node.content === 'string' &&
- node.content.charAt(node.content.length - 1) === '\n'
- ) {
- content = node.content.substring(0, node.content.length - 1);
- }
-
- return (
-
- {content}
-
- );
- },
-
- // Tables
- table: (node, children, parent, styles) => (
-
- {children}
-
- ),
- thead: (node, children, parent, styles) => (
-
- {children}
-
- ),
- tbody: (node, children, parent, styles) => (
-
- {children}
-
- ),
- th: (node, children, parent, styles) => (
-
- {children}
-
- ),
- tr: (node, children, parent, styles) => (
-
- {children}
-
- ),
- td: (node, children, parent, styles) => (
-
- {children}
-
- ),
-
- // Links
- link: (node, children, parent, styles, onLinkPress) => (
- openUrl(node.attributes.href, onLinkPress)}>
- {children}
-
- ),
- blocklink: (node, children, parent, styles, onLinkPress) => (
- openUrl(node.attributes.href, onLinkPress)}
- style={styles.blocklink}>
- {children}
-
- ),
-
- // Images
- image: (
- node,
- children,
- parent,
- styles,
- allowedImageHandlers,
- defaultImageHandler,
- ) => {
- const {src, alt} = node.attributes;
-
- // we check that the source starts with at least one of the elements in allowedImageHandlers
- const show =
- allowedImageHandlers.filter((value) => {
- return src.toLowerCase().startsWith(value.toLowerCase());
- }).length > 0;
-
- if (show === false && defaultImageHandler === null) {
- return null;
- }
-
- const imageProps = {
- indicator: true,
- key: node.key,
- style: styles._VIEW_SAFE_image,
- source: {
- uri: show === true ? src : `${defaultImageHandler}${src}`,
- },
- };
-
- if (alt) {
- imageProps.accessible = true;
- imageProps.accessibilityLabel = alt;
- }
-
- return ;
- },
-
- // Text Output
- text: (node, children, parent, styles, inheritedStyles = {}) => (
-
- {node.content}
-
- ),
- textgroup: (node, children, parent, styles) => (
-
- {children}
-
- ),
- paragraph: (node, children, parent, styles) => (
-
- {children}
-
- ),
- hardbreak: (node, children, parent, styles) => (
-
- {'\n'}
-
- ),
- softbreak: (node, children, parent, styles) => (
-
- {'\n'}
-
- ),
-
- // Believe these are never used but retained for completeness
- pre: (node, children, parent, styles) => (
-
- {children}
-
- ),
- inline: (node, children, parent, styles) => (
-
- {children}
-
- ),
- span: (node, children, parent, styles) => (
-
- {children}
-
- ),
-};
-
-export default renderRules;
diff --git a/src/lib/renderRules.tsx b/src/lib/renderRules.tsx
new file mode 100644
index 00000000..fbb64e4a
--- /dev/null
+++ b/src/lib/renderRules.tsx
@@ -0,0 +1,455 @@
+import type { ReactNode } from "react";
+import type { ViewStyle } from "react-native";
+import {
+ Image,
+ Platform,
+ StyleSheet,
+ Text,
+ TouchableWithoutFeedback,
+ View,
+} from "react-native";
+
+import textStyleProps from "./data/textStyleProps";
+import type { ASTNode } from "./types";
+import hasParents from "./util/hasParents";
+import openUrl from "./util/openUrl";
+
+export type RenderFunction = (
+ node: ASTNode,
+ children: ReactNode[],
+ parentNodes: ASTNode[],
+ styles: Record,
+) => ReactNode;
+
+export type InheritingRenderFunction = (
+ node: ASTNode,
+ children: ReactNode[],
+ parentNodes: ASTNode[],
+ styles: Record,
+ inheritedStyles?: Record,
+) => ReactNode;
+
+export type RenderLinkFunction = (
+ node: ASTNode,
+ children: ReactNode[],
+ parentNodes: ASTNode[],
+ styles: Record,
+ onLinkPress?: (url: string) => boolean,
+) => ReactNode;
+
+export type RenderImageFunction = (
+ node: ASTNode,
+ children: ReactNode[],
+ parentNodes: ASTNode[],
+ styles: Record,
+ allowedImageHandlers: string[],
+ defaultImageHandler: string | null,
+) => ReactNode;
+
+export type SomeRenderFunction =
+ | RenderFunction
+ | InheritingRenderFunction
+ | RenderLinkFunction
+ | RenderImageFunction;
+
+export interface RenderRules {
+ unknown: RenderFunction;
+ body: RenderFunction;
+ heading1: RenderFunction;
+ heading2: RenderFunction;
+ heading3: RenderFunction;
+ heading4: RenderFunction;
+ heading5: RenderFunction;
+ heading6: RenderFunction;
+ hr: RenderFunction;
+ strong: RenderFunction;
+ em: RenderFunction;
+ s: RenderFunction;
+ blockquote: RenderFunction;
+ bullet_list: RenderFunction;
+ ordered_list: RenderFunction;
+ list_item: InheritingRenderFunction;
+ code_inline: InheritingRenderFunction;
+ code_block: InheritingRenderFunction;
+ fence: InheritingRenderFunction;
+ table: RenderFunction;
+ thead: RenderFunction;
+ tbody: RenderFunction;
+ th: RenderFunction;
+ tr: RenderFunction;
+ td: RenderFunction;
+ link: RenderLinkFunction;
+ blocklink: RenderLinkFunction;
+ image: RenderImageFunction;
+ text: InheritingRenderFunction;
+ textgroup: RenderFunction;
+ paragraph: RenderFunction;
+ hardbreak: RenderFunction;
+ softbreak: RenderFunction;
+ pre: RenderFunction;
+ inline: RenderFunction;
+ span: RenderFunction;
+}
+
+const renderRules: RenderRules = {
+ // when unknown elements are introduced, so it wont break
+ unknown: () => null,
+
+ // The main container
+ body: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+
+ // Headings
+ heading1: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ heading2: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ heading3: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ heading4: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ heading5: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ heading6: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+
+ // Horizontal Rule
+ hr: (node, _children, _parent, styles) => (
+
+ ),
+
+ // Emphasis
+ strong: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ em: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ s: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+
+ // Blockquotes
+ blockquote: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+
+ // Lists
+ bullet_list: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ ordered_list: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ // this is a unique and quite annoying render rule because it has
+ // child items that can be styled (the list icon and the list content)
+ // outside of the AST tree so there are some work arounds in the
+ // AST renderer specifically to get the styling right here
+ list_item: (node, children, parent, styles, inheritedStyles = {}) => {
+ // we need to grab any text specific stuff here that is applied on the list_item style
+ // and apply it onto bullet_list_icon. the AST renderer has some workaround code to make
+ // the content classes apply correctly to the child AST tree items as well
+ // as code that forces the creation of the inheritedStyles object for list_items
+ const refStyle = {
+ ...inheritedStyles,
+ ...StyleSheet.flatten(styles.list_item),
+ };
+
+ const arr = Object.keys(refStyle);
+
+ const modifiedInheritedStylesObj: Record = {};
+
+ for (const key of arr) {
+ if (textStyleProps.includes(key)) {
+ // @ts-expect-error - this is fine
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ modifiedInheritedStylesObj[key] = refStyle[key];
+ }
+ }
+
+ if (hasParents(parent, "bullet_list")) {
+ return (
+
+
+ {Platform.select({
+ android: "\u2022",
+ ios: "\u00B7",
+ default: "\u2022",
+ })}
+
+ {children}
+
+ );
+ }
+
+ if (hasParents(parent, "ordered_list")) {
+ const orderedListIndex = parent.findIndex(
+ (el) => el.type === "ordered_list",
+ );
+
+ const orderedList = parent[orderedListIndex];
+ let listItemNumber;
+
+ // @ts-expect-error - this is fine
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, prefer-const
+ listItemNumber = orderedList.attributes.start
+ ? // @ts-expect-error - this is fine
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
+ orderedList.attributes.start + node.index
+ : (node.index ?? 0) + 1;
+
+ return (
+
+
+ {listItemNumber}
+ {node.markup}
+
+ {children}
+
+ );
+ }
+
+ // we should not need this, but just in case
+ return (
+
+ {children}
+
+ );
+ },
+
+ // Code
+ code_inline: (node, _children, _parent, styles, inheritedStyles = {}) => (
+
+ {node.content}
+
+ ),
+ code_block: (
+ {content, key},
+ _children,
+ _parent,
+ styles,
+ inheritedStyles = {},
+ ) => {
+ // we trim new lines off the end of code blocks because the parser sends an extra one.
+
+ if (typeof content === "string" && content.endsWith("\n")) {
+ content = content.substring(0, content.length - 1);
+ }
+
+ return (
+
+ {content}
+
+ );
+ },
+ fence: ({content, key}, _children, _parent, styles, inheritedStyles = {}) => {
+ // we trim new lines off the end of code blocks because the parser sends an extra one.
+
+ if (typeof content === "string" && content.endsWith("\n")) {
+ content = content.substring(0, content.length - 1);
+ }
+
+ return (
+
+ {content}
+
+ );
+ },
+
+ // Tables
+ table: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ thead: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ tbody: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ th: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ tr: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ td: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+
+ // Links
+ link: (node, children, _parent, styles, onLinkPress) => (
+ {
+ openUrl(String(node.attributes?.href), onLinkPress);
+ })
+ }
+ >
+ {children}
+
+ ),
+ blocklink: (node, children, _parent, styles, onLinkPress) => (
+ {
+ openUrl(String(node.attributes?.href), onLinkPress);
+ })
+ }
+ style={styles.blocklink}
+ >
+ {children}
+
+ ),
+
+ // Images
+ image: (
+ node,
+ _children,
+ _parent,
+ styles,
+ allowedImageHandlers,
+ defaultImageHandler,
+ ) => {
+ if (!node.attributes) {
+ return null;
+ }
+
+ const {src, alt} = node.attributes;
+
+ if (typeof src !== "string") {
+ return null;
+ }
+
+ // we check that the source starts with at least one of the elements in allowedImageHandlers
+ const show = allowedImageHandlers.some((value) => {
+ return src.toLowerCase().startsWith(value.toLowerCase());
+ });
+
+ if (!show && defaultImageHandler === null) {
+ return null;
+ }
+
+ const imageProps = {
+ indicator: true,
+ key: node.key,
+ style: styles._VIEW_SAFE_image,
+ source: {
+ uri: show ? src : `${String(defaultImageHandler)}${src}`,
+ },
+ };
+
+ if (alt) {
+ // @ts-expect-error - this is fine
+ imageProps.accessible = true;
+ // @ts-expect-error - this is fine
+ imageProps.accessibilityLabel = alt;
+ }
+
+ if (imageProps.style.overflow === "scroll") {
+ throw new Error(
+ "Image style property 'overflow' is set to 'scroll'. This is not supported",
+ );
+ }
+ // @ts-expect-error - this is fine
+ return ;
+ },
+
+ // Text Output
+ text: (node, _children, _parent, styles, inheritedStyles = {}) => (
+
+ {node.content}
+
+ ),
+ textgroup: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ paragraph: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ hardbreak: (node, _children, _parent, styles) => (
+
+ {"\n"}
+
+ ),
+ softbreak: (node, _children, _parent, styles) => (
+
+ {"\n"}
+
+ ),
+
+ // Believe these are never used but retained for completeness
+ pre: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ inline: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+ span: (node, children, _parent, styles) => (
+
+ {children}
+
+ ),
+};
+
+export default renderRules;
diff --git a/src/lib/styles.js b/src/lib/styles.ts
similarity index 63%
rename from src/lib/styles.js
rename to src/lib/styles.ts
index e1b5e39e..1590643b 100644
--- a/src/lib/styles.js
+++ b/src/lib/styles.ts
@@ -1,4 +1,4 @@
-import {Platform} from 'react-native';
+import {Platform} from "react-native";
// this is converted to a stylesheet internally at run time with StyleSheet.create(
export const styles = {
@@ -7,51 +7,51 @@ export const styles = {
// Headings
heading1: {
- flexDirection: 'row',
+ flexDirection: "row",
fontSize: 32,
},
heading2: {
- flexDirection: 'row',
+ flexDirection: "row",
fontSize: 24,
},
heading3: {
- flexDirection: 'row',
+ flexDirection: "row",
fontSize: 18,
},
heading4: {
- flexDirection: 'row',
+ flexDirection: "row",
fontSize: 16,
},
heading5: {
- flexDirection: 'row',
+ flexDirection: "row",
fontSize: 13,
},
heading6: {
- flexDirection: 'row',
+ flexDirection: "row",
fontSize: 11,
},
// Horizontal Rule
hr: {
- backgroundColor: '#000000',
+ backgroundColor: "#000000",
height: 1,
},
// Emphasis
strong: {
- fontWeight: 'bold',
+ fontWeight: "bold",
},
em: {
- fontStyle: 'italic',
+ fontStyle: "italic",
},
s: {
- textDecorationLine: 'line-through',
+ textDecorationLine: "line-through",
},
// Blockquotes
blockquote: {
- backgroundColor: '#F5F5F5',
- borderColor: '#CCC',
+ backgroundColor: "#F5F5F5",
+ borderColor: "#CCC",
borderLeftWidth: 4,
marginLeft: 5,
paddingHorizontal: 5,
@@ -61,8 +61,8 @@ export const styles = {
bullet_list: {},
ordered_list: {},
list_item: {
- flexDirection: 'row',
- justifyContent: 'flex-start',
+ flexDirection: "row",
+ justifyContent: "flex-start",
},
// @pseudo class, does not have a unique render rule
bullet_list_icon: {
@@ -86,46 +86,46 @@ export const styles = {
// Code
code_inline: {
borderWidth: 1,
- borderColor: '#CCCCCC',
- backgroundColor: '#f5f5f5',
+ borderColor: "#CCCCCC",
+ backgroundColor: "#f5f5f5",
padding: 10,
borderRadius: 4,
...Platform.select({
- ['ios']: {
- fontFamily: 'Courier',
+ ["ios"]: {
+ fontFamily: "Courier",
},
- ['android']: {
- fontFamily: 'monospace',
+ ["android"]: {
+ fontFamily: "monospace",
},
}),
},
code_block: {
borderWidth: 1,
- borderColor: '#CCCCCC',
- backgroundColor: '#f5f5f5',
+ borderColor: "#CCCCCC",
+ backgroundColor: "#f5f5f5",
padding: 10,
borderRadius: 4,
...Platform.select({
- ['ios']: {
- fontFamily: 'Courier',
+ ["ios"]: {
+ fontFamily: "Courier",
},
- ['android']: {
- fontFamily: 'monospace',
+ ["android"]: {
+ fontFamily: "monospace",
},
}),
},
fence: {
borderWidth: 1,
- borderColor: '#CCCCCC',
- backgroundColor: '#f5f5f5',
+ borderColor: "#CCCCCC",
+ backgroundColor: "#f5f5f5",
padding: 10,
borderRadius: 4,
...Platform.select({
- ['ios']: {
- fontFamily: 'Courier',
+ ["ios"]: {
+ fontFamily: "Courier",
},
- ['android']: {
- fontFamily: 'monospace',
+ ["android"]: {
+ fontFamily: "monospace",
},
}),
},
@@ -133,7 +133,7 @@ export const styles = {
// Tables
table: {
borderWidth: 1,
- borderColor: '#000000',
+ borderColor: "#000000",
borderRadius: 3,
},
thead: {},
@@ -144,8 +144,8 @@ export const styles = {
},
tr: {
borderBottomWidth: 1,
- borderColor: '#000000',
- flexDirection: 'row',
+ borderColor: "#000000",
+ flexDirection: "row",
},
td: {
flex: 1,
@@ -154,11 +154,11 @@ export const styles = {
// Links
link: {
- textDecorationLine: 'underline',
+ textDecorationLine: "underline",
},
blocklink: {
flex: 1,
- borderColor: '#000000',
+ borderColor: "#000000",
borderBottomWidth: 1,
},
@@ -173,14 +173,14 @@ export const styles = {
paragraph: {
marginTop: 10,
marginBottom: 10,
- flexWrap: 'wrap',
- flexDirection: 'row',
- alignItems: 'flex-start',
- justifyContent: 'flex-start',
- width: '100%',
+ flexWrap: "wrap",
+ flexDirection: "row",
+ alignItems: "flex-start",
+ justifyContent: "flex-start",
+ width: "100%",
},
hardbreak: {
- width: '100%',
+ width: "100%",
height: 1,
},
softbreak: {},
diff --git a/src/lib/types.ts b/src/lib/types.ts
new file mode 100644
index 00000000..d5cd4d07
--- /dev/null
+++ b/src/lib/types.ts
@@ -0,0 +1,16 @@
+import type {RenderRules} from "./renderRules";
+
+export interface ASTNode {
+ type: T;
+ sourceType?: string; // original source token name
+ sourceInfo?: string;
+ sourceMeta?: unknown;
+ block?: boolean;
+ key: string;
+ content?: string;
+ markup?: string;
+ tokenIndex?: number;
+ index?: number;
+ attributes?: Record;
+ children: readonly ASTNode[];
+}
diff --git a/src/lib/util/Token.js b/src/lib/util/Token.js
deleted file mode 100644
index 9a7c18c5..00000000
--- a/src/lib/util/Token.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export default class Token {
- constructor(type, nesting = 0, children = null, block = false) {
- this.type = type;
- this.nesting = nesting;
- this.children = children;
- this.block = block;
- }
-}
diff --git a/src/lib/util/cleanupTokens.js b/src/lib/util/cleanupTokens.ts
similarity index 50%
rename from src/lib/util/cleanupTokens.js
rename to src/lib/util/cleanupTokens.ts
index 621b761d..e4c03dff 100644
--- a/src/lib/util/cleanupTokens.js
+++ b/src/lib/util/cleanupTokens.ts
@@ -1,22 +1,26 @@
-import getTokenTypeByToken from './getTokenTypeByToken';
-import flattenInlineTokens from './flattenInlineTokens';
-import renderInlineAsText from './renderInlineAsText';
-
-export function cleanupTokens(tokens) {
+import { Token } from "markdown-it";
+import flattenInlineTokens from "./flattenInlineTokens";
+import getTokenTypeByToken from "./getTokenTypeByToken";
+import renderInlineAsText from "./renderInlineAsText";
+
+export function cleanupTokens(
+ tokens: (Token)[],
+): (Token)[] {
tokens = flattenInlineTokens(tokens);
tokens.forEach((token) => {
token.type = getTokenTypeByToken(token);
// set image and hardbreak to block elements
- if (token.type === 'image' || token.type === 'hardbreak') {
+ if (token.type === "image" || token.type === "hardbreak") {
token.block = true;
}
// Set img alt text
- if (token.type === 'image') {
- token.attrs[token.attrIndex('alt')][1] = renderInlineAsText(
- token.children,
- );
+ if (token.type === "image") {
+ if (token.attrs)
+ token.attrs[token.attrIndex("alt")][1] = renderInlineAsText(
+ token.children ?? [],
+ );
}
});
@@ -24,26 +28,26 @@ export function cleanupTokens(tokens) {
* changing a link token to a blocklink to fix issue where link tokens with
* nested non text tokens breaks component
*/
- const stack = [];
- tokens = tokens.reduce((acc, token, index) => {
- if (token.type === 'link' && token.nesting === 1) {
+ const stack: (Token)[] = [];
+ tokens = tokens.reduce<(Token)[]>((acc, token) => {
+ if (token.type === "link" && token.nesting === 1) {
stack.push(token);
} else if (
stack.length > 0 &&
- token.type === 'link' &&
+ token.type === "link" &&
token.nesting === -1
) {
if (stack.some((stackToken) => stackToken.block)) {
- stack[0].type = 'blocklink';
+ stack[0].type = "blocklink";
stack[0].block = true;
- token.type = 'blocklink';
+ token.type = "blocklink";
token.block = true;
}
stack.push(token);
while (stack.length) {
- acc.push(stack.shift());
+ acc.push(stack.shift()!);
}
} else if (stack.length > 0) {
stack.push(token);
diff --git a/src/lib/util/convertAdditionalStyles.js b/src/lib/util/convertAdditionalStyles.js
deleted file mode 100644
index 3cf4e40e..00000000
--- a/src/lib/util/convertAdditionalStyles.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import cssToReactNative from 'css-to-react-native';
-
-export default function convertAdditionalStyles(style) {
- const rules = style.split(';');
-
- const tuples = rules
- .map((rule) => {
- let [key, value] = rule.split(':');
-
- if (key && value) {
- key = key.trim();
- value = value.trim();
- return [key, value];
- } else {
- return null;
- }
- })
- .filter((x) => {
- return x != null;
- });
-
- const conv = cssToReactNative(tuples);
-
- return conv;
-}
diff --git a/src/lib/util/convertAdditionalStyles.ts b/src/lib/util/convertAdditionalStyles.ts
new file mode 100644
index 00000000..19d38b0e
--- /dev/null
+++ b/src/lib/util/convertAdditionalStyles.ts
@@ -0,0 +1,25 @@
+import cssToReactNative, {Style, StyleTuple} from "css-to-react-native";
+
+export default function convertAdditionalStyles(style: string): Style {
+ const rules = style.split(";");
+
+ const tuples: StyleTuple[] = rules
+ .map((rule): StyleTuple | null => {
+ let [key, value] = rule.split(":");
+
+ if (key && value) {
+ key = key.trim();
+ value = value.trim();
+ return [key, value];
+ } else {
+ return null;
+ }
+ })
+ .filter((x) => {
+ return x != null;
+ });
+
+ const conv = cssToReactNative(tuples);
+
+ return conv;
+}
diff --git a/src/lib/util/flattenInlineTokens.js b/src/lib/util/flattenInlineTokens.js
deleted file mode 100644
index 4fe3b605..00000000
--- a/src/lib/util/flattenInlineTokens.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export default function flattenTokens(tokens) {
- return tokens.reduce((acc, curr) => {
- if (curr.type === 'inline' && curr.children && curr.children.length > 0) {
- const children = flattenTokens(curr.children);
- while (children.length) {
- acc.push(children.shift());
- }
- } else {
- acc.push(curr);
- }
-
- return acc;
- }, []);
-}
diff --git a/src/lib/util/flattenInlineTokens.ts b/src/lib/util/flattenInlineTokens.ts
new file mode 100644
index 00000000..35561f37
--- /dev/null
+++ b/src/lib/util/flattenInlineTokens.ts
@@ -0,0 +1,18 @@
+import { Token } from "markdown-it";
+
+export default function flattenTokens(
+ tokens: (Token)[],
+): (Token )[] {
+ return tokens.reduce((acc, curr) => {
+ if (curr.type === "inline" && curr.children && curr.children.length > 0) {
+ const children = flattenTokens(curr.children);
+ while (children.length) {
+ acc.push(children.shift()!);
+ }
+ } else {
+ acc.push(curr);
+ }
+
+ return acc;
+ }, []);
+}
diff --git a/src/lib/util/getTokenTypeByToken.js b/src/lib/util/getTokenTypeByToken.ts
similarity index 52%
rename from src/lib/util/getTokenTypeByToken.js
rename to src/lib/util/getTokenTypeByToken.ts
index c4955105..03f3a2c8 100644
--- a/src/lib/util/getTokenTypeByToken.js
+++ b/src/lib/util/getTokenTypeByToken.ts
@@ -1,3 +1,6 @@
+import { Token } from "markdown-it";
+import { RenderRules } from "../renderRules";
+
const regSelectOpenClose = /_open|_close/g;
/**
@@ -23,16 +26,20 @@ const regSelectOpenClose = /_open|_close/g;
* @param token
* @return {String}
*/
-export default function getTokenTypeByToken(token) {
- let cleanedType = 'unknown';
+export default function getTokenTypeByToken(
+ token: Token,
+): keyof RenderRules {
+ let cleanedType: keyof RenderRules | "heading" = "unknown";
if (token.type) {
- cleanedType = token.type.replace(regSelectOpenClose, '');
+ cleanedType = token.type.replace(regSelectOpenClose, "") as
+ | keyof RenderRules
+ | "heading";
}
switch (cleanedType) {
- case 'heading': {
- cleanedType = `${cleanedType}${token.tag.substr(1)}`;
+ case "heading": {
+ cleanedType = `${cleanedType}${token.tag.substring(1) as "1" | "2" | "3" | "4" | "5" | "6"}`;
break;
}
default: {
diff --git a/src/lib/util/getUniqueID.js b/src/lib/util/getUniqueID.ts
similarity index 100%
rename from src/lib/util/getUniqueID.js
rename to src/lib/util/getUniqueID.ts
diff --git a/src/lib/util/groupTextTokens.js b/src/lib/util/groupTextTokens.js
deleted file mode 100644
index fb38e3d1..00000000
--- a/src/lib/util/groupTextTokens.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import Token from './Token';
-
-export default function groupTextTokens(tokens) {
- const result = [];
-
- let hasGroup = false;
-
- tokens.forEach((token, index) => {
- if (!token.block && !hasGroup) {
- hasGroup = true;
- result.push(new Token('textgroup', 1));
- result.push(token);
- } else if (!token.block && hasGroup) {
- result.push(token);
- } else if (token.block && hasGroup) {
- hasGroup = false;
- result.push(new Token('textgroup', -1));
- result.push(token);
- } else {
- result.push(token);
- }
- });
-
- return result;
-}
diff --git a/src/lib/util/groupTextTokens.ts b/src/lib/util/groupTextTokens.ts
new file mode 100644
index 00000000..8c6fee32
--- /dev/null
+++ b/src/lib/util/groupTextTokens.ts
@@ -0,0 +1,29 @@
+import MarkdownIt, { type Token } from "markdown-it";
+
+const {core: {State: {prototype: {Token}}}} = new MarkdownIt();
+
+export default function groupTextTokens(
+ tokens: Token[],
+): (Token)[] {
+ const result: Token[] = [];
+
+ let hasGroup = false;
+
+ tokens.forEach((token) => {
+ if (!token.block && !hasGroup) {
+ hasGroup = true;
+ result.push(new Token("textgroup", "", 1));
+ result.push(token);
+ } else if (!token.block && hasGroup) {
+ result.push(token);
+ } else if (token.block && hasGroup) {
+ hasGroup = false;
+ result.push(new Token("textgroup", "", -1));
+ result.push(token);
+ } else {
+ result.push(token);
+ }
+ });
+
+ return result;
+}
diff --git a/src/lib/util/hasParents.js b/src/lib/util/hasParents.js
deleted file mode 100644
index 57287540..00000000
--- a/src/lib/util/hasParents.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- *
- * @param {Array} parents
- * @param {string} type
- * @return {boolean}
- */
-export default function hasParents(parents, type) {
- return parents.findIndex((el) => el.type === type) > -1;
-}
diff --git a/src/lib/util/hasParents.ts b/src/lib/util/hasParents.ts
new file mode 100644
index 00000000..53bf84c1
--- /dev/null
+++ b/src/lib/util/hasParents.ts
@@ -0,0 +1,5 @@
+import {ASTNode} from "../types";
+
+export default function hasParents(parents: ASTNode[], type: string): boolean {
+ return parents.findIndex((el) => el.type === type) > -1;
+}
diff --git a/src/lib/util/omitListItemParagraph.js b/src/lib/util/omitListItemParagraph.ts
similarity index 71%
rename from src/lib/util/omitListItemParagraph.js
rename to src/lib/util/omitListItemParagraph.ts
index 8efc08ae..239f982a 100644
--- a/src/lib/util/omitListItemParagraph.js
+++ b/src/lib/util/omitListItemParagraph.ts
@@ -1,6 +1,10 @@
-export default function omitListItemParagraph(tokens) {
+import { Token } from "markdown-it";
+
+export default function omitListItemParagraph(
+ tokens: Token[],
+): Token[] {
// used to ensure that we remove the correct ending paragraph token
- let depth = null;
+ let depth: number | null = null;
return tokens.filter((token, index) => {
// update depth if we've already removed a starting paragraph token
if (depth !== null) {
@@ -8,13 +12,13 @@ export default function omitListItemParagraph(tokens) {
}
// check for a list_item token followed by paragraph token (to remove)
- if (token.type === 'list_item' && token.nesting === 1 && depth === null) {
+ if (token.type === "list_item" && token.nesting === 1 && depth === null) {
const next = index + 1 in tokens ? tokens[index + 1] : null;
- if (next && next.type === 'paragraph' && next.nesting === 1) {
+ if (next && next.type === "paragraph" && next.nesting === 1) {
depth = 0;
return true;
}
- } else if (token.type === 'paragraph') {
+ } else if (token.type === "paragraph") {
if (token.nesting === 1 && depth === 1) {
// remove the paragraph token immediately after the list_item token
return false;
diff --git a/src/lib/util/openUrl.js b/src/lib/util/openUrl.js
deleted file mode 100644
index 1c1bf198..00000000
--- a/src/lib/util/openUrl.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import {Linking} from 'react-native';
-
-export default function openUrl(url, customCallback) {
- if (customCallback) {
- const result = customCallback(url);
- if (url && result && typeof result === 'boolean') {
- Linking.openURL(url);
- }
- } else if (url) {
- Linking.openURL(url);
- }
-}
diff --git a/src/lib/util/openUrl.ts b/src/lib/util/openUrl.ts
new file mode 100644
index 00000000..06be37cb
--- /dev/null
+++ b/src/lib/util/openUrl.ts
@@ -0,0 +1,19 @@
+import {Linking} from "react-native";
+
+export default function openUrl(
+ url: string,
+ customCallback?: (url: string) => boolean,
+): void {
+ if (customCallback) {
+ const result = customCallback(url);
+ if (url && result && typeof result === "boolean") {
+ Linking.openURL(url).catch((err: unknown) => {
+ console.warn(err);
+ });
+ }
+ } else if (url) {
+ Linking.openURL(url).catch((err: unknown) => {
+ console.warn(err);
+ });
+ }
+}
diff --git a/src/lib/util/removeTextStyleProps.js b/src/lib/util/removeTextStyleProps.js
deleted file mode 100644
index 773fec86..00000000
--- a/src/lib/util/removeTextStyleProps.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import textStyleProps from '../data/textStyleProps';
-
-export default function removeTextStyleProps(style) {
- const intersection = textStyleProps.filter((value) =>
- Object.keys(style).includes(value),
- );
-
- const obj = {...style};
-
- intersection.forEach((value) => {
- delete obj[value];
- });
-
- return obj;
-}
diff --git a/src/lib/util/removeTextStyleProps.ts b/src/lib/util/removeTextStyleProps.ts
new file mode 100644
index 00000000..bb44078f
--- /dev/null
+++ b/src/lib/util/removeTextStyleProps.ts
@@ -0,0 +1,18 @@
+import {ViewStyle} from "react-native";
+import textStyleProps from "../data/textStyleProps";
+
+export default function removeTextStyleProps(style: ViewStyle): ViewStyle {
+ const intersection = textStyleProps.filter((value) =>
+ Object.keys(style).includes(value),
+ );
+
+ const obj = {...style};
+
+ intersection.forEach((value) => {
+ // @ts-expect-error this is fine
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
+ delete obj[value];
+ });
+
+ return obj;
+}
diff --git a/src/lib/util/renderInlineAsText.js b/src/lib/util/renderInlineAsText.js
deleted file mode 100644
index 274efae9..00000000
--- a/src/lib/util/renderInlineAsText.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export default function renderInlineAsText(tokens) {
- var result = '';
-
- for (var i = 0, len = tokens.length; i < len; i++) {
- if (tokens[i].type === 'text') {
- result += tokens[i].content;
- } else if (tokens[i].type === 'image') {
- result += renderInlineAsText(tokens[i].children);
- }
- }
-
- return result;
-}
diff --git a/src/lib/util/renderInlineAsText.ts b/src/lib/util/renderInlineAsText.ts
new file mode 100644
index 00000000..5bf26105
--- /dev/null
+++ b/src/lib/util/renderInlineAsText.ts
@@ -0,0 +1,15 @@
+import {Token} from "markdown-it";
+
+export default function renderInlineAsText(tokens: Token[]): string {
+ let result = "";
+
+ for (let i = 0, len = tokens.length; i < len; i++) {
+ if (tokens[i].type === "text") {
+ result += tokens[i].content;
+ } else if (tokens[i].type === "image") {
+ result += renderInlineAsText(tokens[i].children!);
+ }
+ }
+
+ return result;
+}
diff --git a/src/lib/util/splitTextNonTextNodes.js b/src/lib/util/splitTextNonTextNodes.js
deleted file mode 100644
index e8111695..00000000
--- a/src/lib/util/splitTextNonTextNodes.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export default function splitTextNonTextNodes(children) {
- return children.reduce(
- (acc, curr) => {
- if (curr.type.displayName === 'Text') {
- acc.textNodes.push(curr);
- } else {
- acc.nonTextNodes.push(curr);
- }
-
- return acc;
- },
- {textNodes: [], nonTextNodes: []},
- );
-}
diff --git a/src/lib/util/stringToTokens.js b/src/lib/util/stringToTokens.js
deleted file mode 100644
index 6ac77f37..00000000
--- a/src/lib/util/stringToTokens.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export function stringToTokens(source, markdownIt) {
- let result = [];
- try {
- result = markdownIt.parse(source, {});
- } catch (err) {
- console.warn(err);
- }
-
- return result;
-}
diff --git a/src/lib/util/stringToTokens.ts b/src/lib/util/stringToTokens.ts
new file mode 100644
index 00000000..ceb48e70
--- /dev/null
+++ b/src/lib/util/stringToTokens.ts
@@ -0,0 +1,15 @@
+import MarkdownIt, {Token} from "markdown-it";
+
+export function stringToTokens(
+ source: string,
+ markdownIt: MarkdownIt,
+): Token[] {
+ let result: Token[] = [];
+ try {
+ result = markdownIt.parse(source, {});
+ } catch (err) {
+ console.warn(err);
+ }
+
+ return result;
+}
diff --git a/src/lib/util/tokensToAST.js b/src/lib/util/tokensToAST.ts
similarity index 57%
rename from src/lib/util/tokensToAST.js
rename to src/lib/util/tokensToAST.ts
index b0ed265c..2c736ade 100644
--- a/src/lib/util/tokensToAST.js
+++ b/src/lib/util/tokensToAST.ts
@@ -1,13 +1,9 @@
-import getUniqueID from './getUniqueID';
-import getTokenTypeByToken from './getTokenTypeByToken';
+import { Token } from "markdown-it";
+import { ASTNode } from "../types";
+import getTokenTypeByToken from "./getTokenTypeByToken";
+import getUniqueID from "./getUniqueID";
-/**
- *
- * @param {{type: string, tag:string, content: string, children: *, attrs: Array, meta, info, block: boolean}} token
- * @param {number} tokenIndex
- * @return {{type: string, content, tokenIndex: *, index: number, attributes: {}, children: *}}
- */
-function createNode(token, tokenIndex) {
+function createNode(token: Token, tokenIndex: number): ASTNode {
const type = getTokenTypeByToken(token);
const content = token.content;
@@ -24,26 +20,21 @@ function createNode(token, tokenIndex) {
type,
sourceType: token.type,
sourceInfo: token.info,
- sourceMeta: token.meta,
+ sourceMeta: token.meta as unknown,
block: token.block,
markup: token.markup,
- key: getUniqueID() + '_' + type,
+ key: getUniqueID() + "_" + type,
content,
tokenIndex,
index: 0,
attributes,
- children: tokensToAST(token.children),
+ children: token.children ? tokensToAST(token.children) : [],
};
}
-/**
- *
- * @param {Array<{type: string, tag:string, content: string, children: *, attrs: Array}>}tokens
- * @return {Array}
- */
-export default function tokensToAST(tokens) {
- let stack = [];
- let children = [];
+export default function tokensToAST(tokens?: (Token)[]): ASTNode[] {
+ const stack = [];
+ let children: ASTNode[] = [];
if (!tokens || tokens.length === 0) {
return [];
@@ -55,9 +46,9 @@ export default function tokensToAST(tokens) {
if (
!(
- astNode.type === 'text' &&
+ astNode.type === "text" &&
astNode.children.length === 0 &&
- astNode.content === ''
+ astNode.content === ""
)
) {
astNode.index = children.length;
@@ -65,9 +56,12 @@ export default function tokensToAST(tokens) {
if (token.nesting === 1) {
children.push(astNode);
stack.push(children);
+ // @ts-expect-error read-only is not a concern
children = astNode.children;
} else if (token.nesting === -1) {
+ // @ts-expect-error we know it's defined
children = stack.pop();
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if (token.nesting === 0) {
children.push(astNode);
}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..910bffee
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,101 @@
+{
+ "compilerOptions": {
+ "noEmit": false,
+ /* Visit https://aka.ms/tsconfig to read more about this file */
+ /* Projects */
+ "incremental": true /* Save .tsbuildinfo files to allow for incremental compilation of projects. */,
+ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
+ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
+ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
+ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
+ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
+ /* Language and Environment */
+ "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
+ "lib": [
+ "ES2021",
+ "ES2022.Error"
+ ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
+ "jsx": "react-native" /* Specify what JSX code is generated. */,
+ "experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
+ "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */,
+ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
+ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
+ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
+ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
+ /* Modules */
+ "module": "CommonJS" /* Specify what module code is generated. */,
+ "rootDir": "./src" /* Specify the root folder within your source files. */,
+ "moduleResolution": "Node", /* Specify how TypeScript looks up a file from a given module specifier. */
+ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
+ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
+ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
+ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
+ // "resolveJsonModule": true, /* Enable importing .json files. */
+ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
+ /* JavaScript Support */
+ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
+ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+ /* Emit */
+ "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
+ "declarationMap": true /* Create sourcemaps for d.ts files. */,
+ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
+ "sourceMap": true /* Create source map files for emitted JavaScript files. */,
+ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+ "outDir": "./dist" /* Specify an output folder for all emitted files. */,
+ // "removeComments": true, /* Disable emitting comments. */
+ // "noEmit": true, /* Disable emitting files from a compilation. */
+ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
+ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
+ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
+ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+ // "newLine": "crlf", /* Set the newline character for emitting files. */
+ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
+ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
+ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
+ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
+ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
+ /* Interop Constraints */
+ "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
+ "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
+ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
+ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
+ /* Type Checking */
+ "strict": true /* Enable all strict type-checking options. */,
+ "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
+ "strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */,
+ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+ "strictPropertyInitialization": true /* Check for class properties that are declared but not set in the constructor. */,
+ "noImplicitThis": true /* Enable error reporting when 'this' is given the type 'any'. */,
+ "useUnknownInCatchVariables": true /* Default catch clause variables as 'unknown' instead of 'any'. */,
+ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
+ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
+ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
+ // "exactOptionalPropertyTypes": true /* Interpret optional property types as written, rather than adding 'undefined'. */,
+ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
+ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
+ "noUncheckedIndexedAccess": false /* Add 'undefined' to a type when accessed using an index. */,
+ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
+ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
+ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
+ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
+ /* Completeness */
+ "skipDefaultLibCheck": false /* Skip type checking .d.ts files that are included with TypeScript. */,
+ // "skipLibCheck": false /* Skip type checking all .d.ts files. */
+ "types": []
+ },
+ "include": ["./src/**/*"]
+}