Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,8 @@
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["solid"]
"plugins": ["solid"],
"rules": {
"no-unused-vars": "off"
}
}
58 changes: 58 additions & 0 deletions PR_DRAFT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Title

Align `solid-markdown` with `react-markdown` 10.1.0 and add Solid-native async rendering

# Summary

This update moves `solid-markdown` from an older compatibility-focused API to an upstream-aligned `react-markdown` 10.1.0 style API, adapted for Solid. The rendering pipeline now follows the upstream `remark -> rehype -> JSX runtime` model, the public options match current upstream concepts, and async unified plugins are supported both on the server and on the client.

# What changed

- Replaced the custom renderer/filter pipeline with an upstream-style processor flow and `hast-util-to-jsx-runtime`.
- Aligned the public API around `Markdown`, `MarkdownAsync`, `MarkdownResource`, `remarkRehypeOptions`, and `urlTransform`.
- Simplified custom component typing so overrides receive normal Solid intrinsic props plus `node`.
- Removed legacy wrapper-element behavior and made removed pre-v9 props fail explicitly at runtime instead of lingering as soft compatibility.
- Added a Solid-native client async path through `MarkdownResource`.
- Expanded SSR and client coverage to include rendering parity, URL safety, removed-prop runtime errors, plugin/property passthrough, and async lifecycle behavior.

# Why this is valuable

- It makes the package easier to understand for anyone already using modern `react-markdown`.
- It removes older behavior that was diverging from upstream semantics and complicating maintenance.
- It gives Solid users a clear story for async plugins on both the server and the client.
- It tightens the package surface area and types instead of preserving multiple generations of API compatibility.
- It raises confidence in the port with a much broader regression suite across SSR and browser rendering.

# Compatibility and migration

This change is intentionally breaking in the same places where upstream has already moved on:

- `SolidMarkdown` is removed in favor of the default `Markdown` export.
- Wrapper props such as `class`/`className` are removed; callers should wrap the component themselves.
- Legacy props such as `source`, `plugins`, `renderers`, `allowNode`, `allowedTypes`, `disallowedTypes`, `transformLinkUri`, and `transformImageUri` now throw.
- `urlTransform` replaces the older link/image transform props.
- `renderingStrategy` remains temporarily for the sync component only, but it is deprecated and intended for removal in the next major release.

# Verification

Verified locally with:

```bash
pnpm lint
pnpm test
pnpm build
```

Current test coverage includes:

- SSR rendering coverage
- Browser rendering coverage
- URL sanitization and transform behavior
- Removed-prop runtime error coverage
- Async plugin behavior for both `MarkdownAsync` and `MarkdownResource`

# Notes for review

- The goal here is not to preserve every historical `solid-markdown` API quirk. The goal is to make this package a clean Solid port of modern `react-markdown`.
- The only intentional short-term compatibility holdout is the deprecated `renderingStrategy` prop on `Markdown`.
- The README and demo docs were updated to describe the new API and the current verification story so users do not have to infer behavior from source alone.
219 changes: 197 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,25 @@

# `solid-markdown`

Render markdown as solid components.
Render markdown to Solid components.

The implementation is 90% shamelessly copied from https://github.com/remarkjs/react-markdown.
`solid-markdown` now tracks the `react-markdown` 10.x API closely and keeps the rendering pipeline upstream-aligned while adapting the JSX output to Solid.

Changes include:
## Why this update matters

- Replacing React specific component creation with SolidJS components
- Porting the implementation from javascript with JSDoc types to typescript

Please check the original repo for in-depth details on how to use.
This package is now a real Solid port of modern `react-markdown`, not a compatibility wrapper around older behavior. The public API matches upstream concepts such as `components`, `remarkRehypeOptions`, `urlTransform`, and async plugin support, while Solid gets one extra client-side helper: `MarkdownResource`.

## Installation

```bash
npm install solid-markdown
pnpm add solid-markdown
```


## Usage

```jsx
import { SolidMarkdown } from "solid-markdown";
```tsx
import Markdown from "solid-markdown";
import remarkGfm from "remark-gfm";

const markdown = `
# This is a title
Expand All @@ -33,25 +31,202 @@ const markdown = `
- a
- list
`;
const App = () => {
return <SolidMarkdown children={markdown} />;

export default function App() {
return <Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>;
}
```

## API reference

| Export | Type | Purpose |
| --- | --- | --- |
| `Markdown` | component | Synchronous markdown renderer. |
| `MarkdownAsync` | function | Async/server renderer for async unified plugins. |
| `MarkdownResource` | component | Solid client wrapper for async plugin pipelines. |
| `defaultUrlTransform` | function | Default URL sanitizer used for links and images. |
| `AllowElement` | type | Per-element allow/deny callback. |
| `Components` | type | Custom tag-to-component overrides. |
| `ExtraProps` | type | Extra props passed to custom components (`node`). |
| `Options` | type | Shared renderer options. |
| `MarkdownResourceOptions` | type | `Options` plus `fallback`. |
| `UrlTransform` | type | URL rewrite/sanitization hook. |

### `Markdown`

Synchronous markdown renderer.

```tsx
import Markdown from "solid-markdown";
import remarkGfm from "remark-gfm";

<Markdown remarkPlugins={[remarkGfm]}>{value()}</Markdown>;
```

### `MarkdownAsync`

Async/server helper for async unified plugins.

```tsx
import { MarkdownAsync } from "solid-markdown";
import rehypeStarryNight from "rehype-starry-night";

const content = await MarkdownAsync({
children: "```js\nconsole.log(3.14)\n```",
rehypePlugins: [rehypeStarryNight],
});

return <div class="preview">{content}</div>;
```

### `MarkdownResource`

Solid-native client wrapper for async plugins.

```tsx
import { MarkdownResource } from "solid-markdown";
import rehypeStarryNight from "rehype-starry-night";

<MarkdownResource
children={value()}
fallback={<p>Rendering…</p>}
rehypePlugins={[rehypeStarryNight]}
/>;
```

### `defaultUrlTransform`

By default, unsafe protocols such as `javascript:` are removed while standard URLs, fragments, and paths are preserved.

```tsx
import Markdown, { defaultUrlTransform } from "solid-markdown";

<Markdown
urlTransform={(url, key, node) => {
const safe = defaultUrlTransform(url);
if (!safe) return safe;
return key === "href" && node.tagName === "a" ? `/out?url=${encodeURIComponent(safe)}` : safe;
}}
>
{"[OpenAI](https://openai.com)"}
</Markdown>;
```

## Options

Supported options match upstream `react-markdown` 10.x semantics:

| Option | Purpose |
| --- | --- |
| `allowElement` | Decide per HAST element whether it should render. |
| `allowedElements` | Allowlist tag names. |
| `children` | Markdown source string. `null` and `undefined` render nothing. |
| `components` | Override specific HTML tags with Solid components or tag names. |
| `disallowedElements` | Blocklist tag names. |
| `rehypePlugins` | Rehype plugins applied after markdown is converted to HAST. |
| `remarkPlugins` | Remark plugins applied while parsing markdown. |
| `remarkRehypeOptions` | Extra `remark-rehype` options merged with the safe defaults used by upstream. |
| `skipHtml` | Ignore raw HTML in the markdown source. |
| `unwrapDisallowed` | Keep children of removed nodes instead of dropping the whole subtree. |
| `urlTransform` | Rewrite or sanitize link and image URLs. |

## Components

Custom components receive normal Solid intrinsic props plus `node`.

```tsx
import Markdown, { type Components } from "solid-markdown";

const components: Components = {
code(props) {
return <code data-tag={props.node?.tagName}>{props.children}</code>;
},
};

<Markdown components={components}>{"`example`"}</Markdown>;
```

## Migration

### Default import

Before:

```tsx
import { SolidMarkdown } from "solid-markdown";

<SolidMarkdown children={markdown} />;
```

After:

```tsx
import Markdown from "solid-markdown";

<Markdown>{markdown}</Markdown>;
```

## Rendering strategy
There's an extra option you can pass to the markdown component: `renderingStrategy: "memo" | "reconcile"`.
### Wrapper ownership

The default value is `"memo"`, which means that the markdown parser will generate a new full AST tree each time (inside a `useMemo`), and use that.
As a consequence, the full DOM will be re-rendered, even the markdown nodes that haven't changed. (Should be fine 90% of the time).
Before:

Using `reconcile` will switch the strategy to using a solid store with the `reconcile` function (https://docs.solidjs.com/reference/store-utilities/reconcile). This will diff the previous and next markdown ASTs and only trigger re-renders for the parts that have changed.
This will help with cases like streaming partial content and updating the markdown gradually (see https://github.com/andi23rosca/solid-markdown/issues/32).
```tsx
<Markdown class="markdown-body">{markdown}</Markdown>;
```

After:

```tsx
<div class="markdown-body">
<Markdown>{markdown}</Markdown>
</div>
```

### URL transforms

Before:

```tsx
<Markdown transformLinkUri={(href) => href} transformImageUri={(src) => src}>
{markdown}
</Markdown>;
```

After:

```tsx
<SolidMarkdown renderingStrategy="reconcile" children={markdown} />;
<Markdown
urlTransform={(url, key) => {
if (key === "href") return url;
if (key === "src") return url;
return url;
}}
>
{markdown}
</Markdown>;
```

### Removed and deprecated behavior

- `SolidMarkdown` is gone. Use the default export instead.
- Wrapper props such as `class` and `className` are gone. Wrap `Markdown` in your own element.
- Legacy pre-v9 props now throw at runtime instead of being silently accepted. This includes deprecated names such as `source`, `plugins`, `renderers`, `allowNode`, `allowedTypes`, `disallowedTypes`, `transformLinkUri`, `transformImageUri`, `linkTarget`, and the old source-position props.
- `urlTransform` replaces `transformLinkUri` and `transformImageUri`.
- `renderingStrategy="memo" | "reconcile"` still exists temporarily on `Markdown`, but it is deprecated and will be removed in the next major release.

## Testing and status

Local verification for this port currently runs through:

```bash
pnpm lint
pnpm test
pnpm build
```

## TODO
Current status:

- [ ] Port unit tests from from original library
- Sync rendering is supported through `Markdown`.
- Async unified plugins are supported on the server through `MarkdownAsync`.
- Async unified plugins are supported on the client through `MarkdownResource`.
- SSR and DOM behavior are covered by the package test suite.
37 changes: 17 additions & 20 deletions dev/README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
# SolidStart
# Dev playground

Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
This app is the local playground for the synchronous `Markdown` API. It renders markdown with `remark-gfm`, lets you edit the source live, and exercises custom component overrides against the package source in this repo.

## Creating a project
## What it covers

```bash
# create a new project in the current directory
npm init solid@latest

# create a new project in my-app
npm init solid@latest my-app
```
- Default `Markdown` import
- `remarkPlugins={[remarkGfm]}`
- Live markdown editing
- Custom component mapping for headings

## Developing
## Running it

Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:

```bash
npm run dev

# or start the server and open the app in a new browser tab
npm run dev -- --open
cd dev
pnpm install
pnpm dev
```

## Building
The demo imports the local package source directly, so it is useful for checking API ergonomics while working on the library.

Solid apps are built with _presets_, which optimise your project for deployment to different environments.

By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
## Building

## This project was created with the [Solid CLI](https://github.com/solidjs-community/solid-cli)
```bash
cd dev
pnpm build
```
Loading