{"id":37230525,"url":"https://github.com/ensunluer/react-native-markdown-stream","last_synced_at":"2026-01-15T03:38:45.611Z","repository":{"id":319998007,"uuid":"1077078465","full_name":"ensunluer/react-native-markdown-stream","owner":"ensunluer","description":"A lightweight React Native renderer that turns streaming Markdown (chat responses, AI completions, docs) into polished UI in real time.","archived":false,"fork":false,"pushed_at":"2026-01-10T21:41:39.000Z","size":1667,"stargazers_count":2,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-11T06:31:04.632Z","etag":null,"topics":["markdown","react-native","stream"],"latest_commit_sha":null,"homepage":"https://rnms.enesunluer.com/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ensunluer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-15T18:39:18.000Z","updated_at":"2026-01-10T21:41:40.000Z","dependencies_parsed_at":"2025-10-21T12:09:25.496Z","dependency_job_id":"f58c8455-a88c-4e9c-a7d7-22725ee75c9b","html_url":"https://github.com/ensunluer/react-native-markdown-stream","commit_stats":null,"previous_names":["ensunluer/react-native-markdown-stream"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/ensunluer/react-native-markdown-stream","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ensunluer%2Freact-native-markdown-stream","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ensunluer%2Freact-native-markdown-stream/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ensunluer%2Freact-native-markdown-stream/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ensunluer%2Freact-native-markdown-stream/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ensunluer","download_url":"https://codeload.github.com/ensunluer/react-native-markdown-stream/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ensunluer%2Freact-native-markdown-stream/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28442306,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-15T00:55:22.719Z","status":"online","status_checked_at":"2026-01-15T02:00:08.019Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["markdown","react-native","stream"],"created_at":"2026-01-15T03:38:44.988Z","updated_at":"2026-01-15T03:38:45.595Z","avatar_url":"https://github.com/ensunluer.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# react-native-markdown-stream\n\nA lightweight React Native renderer that turns streaming Markdown (chat responses, AI completions, docs) into polished UI in real time.\n\n## Highlights\n\n- **Streaming-first:** `AsyncIterable`, `ReadableStream`, generators, or manual chunk pushes all work out of the box.\n- **Reveal controls:** Animate output per chunk, word, or character with configurable delays.\n- **Theming without repainting the screen:** ship light/dark presets, or merge your own colors with a simple config object.\n- **Code \u0026 math aware:** inline/code blocks with optional copy actions, math rendering via the optional `react-native-math-view` peer.\n- **Zero native code:** works in the classic architecture and the New Architecture (Fabric/TurboModules) without extra steps.\n\n\u003cdetails\u003e\n\u003csummary\u003eContents\u003c/summary\u003e\n\n- [Installation](#installation)\n- [Metro configuration](#metro-configuration)\n- [Quick start](#quick-start)\n- [Streaming sources](#supported-stream-sources)\n- [Component props](#markdownstream-props)\n- [Customising the renderer](#customising-the-renderer)\n- [Theming](#theming)\n- [Hook API](#using-usemarkdownstream-directly)\n- [Type exports](#other-exports)\n- [Running the example](#running-the-example-app)\n- [Contributing](#contributing)\n\u003c/details\u003e\n\n## Installation\n\n```sh\n# yarn\nyarn add react-native-markdown-stream\n\n# npm\nnpm install react-native-markdown-stream\n```\n\n### Optional math support\n\nInstall the optional peer if you want LaTeX blocks to render with `react-native-math-view`:\n\n```sh\nyarn add react-native-math-view\n```\n\nWithout it installed, math blocks gracefully fall back to styled text.\n\n## Metro configuration\n\nThe library bundles modern ESM packages from the `remark/unified` ecosystem. Metro 0.72+ understands them as long as package exports are enabled.\n\nAdd or update `metro.config.js` in your app:\n\n```js\n// metro.config.js\nconst {getDefaultConfig} = require('metro-config');\n\nmodule.exports = (() =\u003e {\n  const config = getDefaultConfig(__dirname);\n  config.resolver.unstable_enablePackageExports = true;\n  return config;\n})();\n```\n\nUsing Expo or a monorepo? Mirror the example app:\n\n```js\n// example/metro.config.js\nconst path = require('path');\nconst {getDefaultConfig} = require('@expo/metro-config');\nconst {withMetroConfig} = require('react-native-monorepo-config');\n\nconst root = path.resolve(__dirname, '..');\nconst config = withMetroConfig(getDefaultConfig(__dirname), {\n  root,\n  dirname: __dirname,\n});\n\nconfig.resolver.unstable_enablePackageExports = true;\n\nmodule.exports = config;\n```\n\n## Quick start\n\n```tsx\nimport {MarkdownStream} from 'react-native-markdown-stream';\n\nconst STREAM_URL = 'https://example.com/chat-stream';\n\nexport function ChatMessage() {\n  return (\n    \u003cMarkdownStream\n      source={listenToSSE(STREAM_URL)}\n      revealMode=\"word\"\n      revealDelay={24}\n      enableCodeCopy\n      enableImageLightbox\n      theme={{\n        base: 'dark',\n        colors: {\n          linkColor: '#4ade80',\n          quoteBorderColor: '#22c55e',\n        },\n      }}\n    /\u003e\n  );\n}\n\nfunction* staticExample() {\n  yield '# Hello\\n';\n  yield 'Streaming markdown arrives chunk by chunk.\\n';\n}\n\nasync function* listenToSSE(url: string) {\n  const response = await fetch(url);\n  if (!response.body) {\n    return;\n  }\n  const reader = response.body.getReader();\n  const decoder = new TextDecoder();\n\n  while (true) {\n    const {value, done} = await reader.read();\n    if (done) break;\n    yield decoder.decode(value);\n  }\n\n  reader.releaseLock();\n}\n```\n\n### Controlled usage\n\nPass the `content` prop to render pre-computed Markdown (and still opt into streaming later if you call `onReady` controls).\n\n```tsx\n\u003cMarkdownStream content={markdownString} onReady={(controls) =\u003e controls.start()} /\u003e\n```\n\n## Supported stream sources\n\n`source` accepts any of the following:\n\n- `AsyncIterable\u003cstring\u003e` or `Iterable\u003cstring\u003e` (generators, async generators).\n- A function returning one of the above (lazy initialisation).\n- A `ReadableStream` (SSE, `fetch` with streaming responses).\n- Call `controls.appendChunk()` manually via the `onReady` callback for complete control.\n\nAll chunks (strings, `Uint8Array`, objects with `toString`) are normalised to strings before parsing.\n\n## `MarkdownStream` props\n\n| Prop | Type | Description |\n| --- | --- | --- |\n| `source` | `MarkdownStreamSource\u003cstring\u003e` | Primary stream. Optional if you control content manually. |\n| `content` | `string` | Fully rendered markdown. Bypasses streaming until you call `start`. |\n| `initialValue` | `string` | Markdown shown before the first chunk arrives. |\n| `theme` | `'light' \\| 'dark' \\| MarkdownTheme \\| MarkdownThemeConfig` | Pick a preset or merge custom colors; defaults to light theme with transparent background. |\n| `textColor` | `string` | Override the resolved theme's primary text color without redefining the whole palette. |\n| `mutedTextColor` | `string` | Override the resolved theme's muted/secondary text color. |\n| `revealMode` | `'chunk' \\| 'word' \\| 'character'` | Controls how new content animates in. |\n| `revealDelay` | `number` | Delay (ms) between reveals; ignored when `revealMode=\"chunk\"`. |\n| `autoStart` | `boolean` | Start streaming as soon as `source` exists (default `true`). |\n| `onReady` | `(controls: UseMarkdownStreamResult) =\u003e void` | Exposes stream controls (append, reset, start, stop). |\n| `onChunk` / `onEnd` / `onError` | callbacks | Tap into stream lifecycle events. |\n| `showCodeLineNumbers` | `boolean` | Adds line numbers to fenced code blocks. |\n| `enableCodeCopy` | `boolean` | Shows a copy action on code blocks (uses clipboard when available). |\n| `codeCopyLabel` | `string` | Custom label for the copy button. |\n| `onCodeCopy` | `({value, language}) =\u003e boolean \\| void` | Return `true` to mark the copy action as handled. Fires before the built-in clipboard logic. |\n| `onImagePress` | `({url, alt}) =\u003e boolean \\| void` | Intercept image taps. Return `true` to skip built-in lightbox. |\n| `enableImageLightbox` | `boolean` | Presents a modal preview when images are tapped. |\n| `onBlockLongPress` | `({node}) =\u003e void` | Receive long-press events for any block node. |\n| `blockLongPressDelay` | `number` | Milliseconds before the long-press fires (default `300`). |\n| `components` | `Partial\u003cMarkdownRendererComponents\u003e` | Override individual renderers (`codeBlock`, `inlineCode`, `mathBlock`, `image`, …). |\n\n## Customising the renderer\n\nEvery major element exposes a customization hook:\n\n- Provide `components` to override code/math/image rendering with your own component tree.\n- Style code blocks via `CodeBlock` `containerStyle`/`codeStyle` props if you bring your own.\n- Hook into `onImagePress`, `enableImageLightbox`, and `onBlockLongPress` for richer media UX.\n- Implement your own copy logic with `onCodeCopy` (e.g. analytics or custom tooltips).\n\n## Theming\n\nLight and dark themes ship by default, both with transparent container backgrounds so they blend into your layout. Override a single token or a whole palette by passing a `MarkdownTheme` or `MarkdownThemeConfig`, or use the `textColor` / `mutedTextColor` props for quick tweaks.\n\n```tsx\nimport {MarkdownStream} from 'react-native-markdown-stream';\n\n\u003cMarkdownStream\n  theme={{\n    base: 'light',\n    colors: {\n      backgroundColor: 'transparent',\n      textColor: '#0f172a',\n      linkColor: '#f97316',\n      quoteBorderColor: '#fb923c',\n    },\n  }}\n/\u003e;\n```\n\nCall `resolveTheme` if you need the concrete palette outside the component.\n\n## Handling incomplete markdown\n\nStreaming text often arrives with half-typed emphasis or unfinished links. Before parsing we run the content through a small sanitizer that auto-closes common Markdown markers and neutralises broken URLs. The implementation is adapted from [Vercel's streamdown `parse-incomplete-markdown`](https://github.com/vercel/streamdown/blob/main/packages/streamdown/lib/parse-incomplete-markdown.ts) (Apache-2.0).\n\n## Using `useMarkdownStream` directly\n\nThe hook powers the component and can run headless when you need custom rendering.\n\n```tsx\nimport {useMarkdownStream} from 'react-native-markdown-stream';\n\nexport function CustomRenderer({source}) {\n  const stream = useMarkdownStream({\n    source,\n    revealMode: 'character',\n    onChunk: (chunk) =\u003e console.log('chunk', chunk.length),\n  });\n\n  return (\n    \u003cScrollView\u003e\n      \u003cText\u003e{stream.content}\u003c/Text\u003e\n      \u003cButton title=\"Skip animation\" onPress={() =\u003e stream.setRevealMode('chunk')} /\u003e\n    \u003c/ScrollView\u003e\n  );\n}\n```\n\nThe hook returns the current `content`, the accumulated `fullContent`, status flags, and control helpers (`appendChunk`, `reset`, `start`, `stop`, `setRevealMode`, `setRevealDelay`).\n\n## Other exports\n\n```ts\nimport {\n  MarkdownRenderer,\n  parseMarkdown,\n  useMarkdownStream,\n  lightTheme,\n  darkTheme,\n  resolveTheme,\n  type MarkdownTheme,\n  type MarkdownThemeConfig,\n  type ThemeMode,\n  type MarkdownRendererComponents,\n} from 'react-native-markdown-stream';\n```\n\nUse `MarkdownRenderer` when you already have an mdast `Root`, or to wrap custom parsed content.\n\n## Running the example app\n\n```sh\nyarn install\nyarn example          # starts Expo in the example/ workspace\n\n# or target a platform directly\nyarn workspace react-native-markdown-stream-example android\nyarn workspace react-native-markdown-stream-example ios\n```\n\nThe example showcases reveal modes, code copy actions, image lightbox, and long-press handlers.\n\n## Architecture support\n\nThe package is JavaScript-only and works unchanged in both the classic bridge and the New Architecture (Fabric/TurboModules). No native modules, pods, or Gradle steps are required.\n\n## Contributing\n\n- [Development workflow](CONTRIBUTING.md#development-workflow)\n- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)\n- [Code of conduct](CODE_OF_CONDUCT.md)\n\n---\n\n© [Enes Ünlüer](https://github.com/ensunluer)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fensunluer%2Freact-native-markdown-stream","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fensunluer%2Freact-native-markdown-stream","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fensunluer%2Freact-native-markdown-stream/lists"}