Skip to content

Commit

Permalink
feat: better built-in components, improved docs
Browse files Browse the repository at this point in the history
  • Loading branch information
natemoo-re committed Sep 25, 2022
1 parent f99b52b commit df3e783
Show file tree
Hide file tree
Showing 11 changed files with 654 additions and 80 deletions.
5 changes: 5 additions & 0 deletions .changeset/violet-geckos-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"astro-remote": minor
---

Add support for custom Heading, CodeBlock, and CodeSpan components. Improve documentation.
135 changes: 134 additions & 1 deletion packages/astro-remote/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,136 @@
# Astro Remote

Render (and customize) remote HTML or Markdown content with Astro
Render remote HTML or Markdown content in Astro with full control over the output.

Powered by [`ultrahtml`](https://github.com/natemoo-re/ultrahtml) and [`marked`](https://github.com/markedjs/marked).

## Rendering Remote Content

The most basic function of `astro-remote` is to convert a string of HTML or Markdown to HTML. Use the `Markup` and `Markdown` components depending on your input.

```astro
---
import { Markup, Markdown } from 'astro-remote';
const { html, markdown } = await fetch('http://my-site.com/api/v1/post').then(res => res.json());
---
<Markup content={html} />
<Markdown content={markdown} />
```

### Sanitization

By default, all content will be sanitized with sensible defaults (`script` blocks are dropped). This can be controlled using the [`SanitizeOptions`](https://github.com/natemoo-re/ultrahtml/blob/71e723f6093abea2584c9ea3bfecc0ce68d02d8d/src/index.ts#L251-L268) available in `ultrahtml`. Set to `false` to disable sanitization.

```astro
---
import { Markdown } from 'astro-remote';
const content = await fetch('http://my-site.com/api/v1/post').then(res => res.text());
---
<!-- Disallow inline `style` attributes, but allow HTML comments -->
<Markdown content={content} sanitize={{ dropAttributes: { "style": ["*"] }, allowComments: true }} />
```

### Customization

Both `Markup` and `Markdown` allow full control over the rendering of output. The `components` option allows you to replace a standard HTML element with a custom component.

```astro
---
import { Markdown } from 'astro-remote';
import Title from '../components/Title.astro';
const content = await fetch('http://my-site.com/api/v1/post').then(res => res.text());
---
<!-- Render <h1> as custom <Title> component -->
<Markdown content={content} components={{ h1: Title }} />
```

In addition to built-in HTML Elements, `Markdown` also supports a few custom components for convenience.

#### `<Heading />`

The `Heading` component renders all `h1` through `h6` elements. It receives the following props:

- `as`, the `h1` through `h6` tag
- `href`, a pre-generated, slugified `href`
- `text`, the text content of the children (for generating a custom slug)

```astro
---
import { Markdown } from 'astro-remote';
import Heading from '../components/Heading.astro';
const content = await fetch('http://my-site.com/api/v1/post').then(res => res.text());
---
<!-- Render all <h1> through <h6> using custom <Heading> component -->
<Markdown content={content} components={{ Heading }} />
```

A sample `Heading` component might look something like this.

```astro
---
const { as: Component, href } = Astro.props;
---
<Component><a href={href}><slot /></a></Component>
```

#### `<CodeBlock />`

The `CodeBlock` component allows you customize the rendering of code blocks. It receives the following props:

- `lang`, the language specified after the three backticks (defaults to `plaintext`)
- `code`, the raw code to be highlighted. **Be sure to escape the output!**
- `...props`, any other attributes passed to the three backticks. These should follow HTML attribute format (`name="value"`)

A sample `CodeBlock` component might look something like this.

```astro
---
const { lang, code, ...props } = Astro.props;
const highlighted = await highlight(code, { lang });
---
<pre class={`language-${lang}`}><code set:html={highlighted} /></pre>
```

#### `<CodeSpan />`

The `CodeSpan` component allows you customize the rendering of inline code spans. It receives the following props:

- `code`, the value of the code span

A sample `CodeSpan` component might look something like this.

```astro
---
const { code } = Astro.props;
---
<code set:text={code} />
```

### Custom Components in Markdown

If you'd like to allow custom components in Markdown, you can do so using a combination of the `sanitize` and `components` options. By default, sanitization removes components.

Given the following markdown source:

```markdown
# Hello world!

<MyCustomComponent a="1" b="2" c="3">It works!</MyCustomComponent>
```

```astro
---
import { Markdown } from 'astro-remote';
import MyCustomComponent from '../components/MyCustomComponent.astro';
const content = await fetch('http://my-site.com/api/v1/post').then(res => res.text());
---
<Markdown content={content} sanitize={{ allowComponents: true }} components={{ MyCustomComponent }} />
```
37 changes: 36 additions & 1 deletion packages/astro-remote/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ import { transform } from 'ultrahtml';
import { jsx as h } from 'astro/jsx-runtime';
import { renderJSX } from 'astro/runtime/server/jsx';
import { __unsafeHTML } from 'ultrahtml';
import 'he';

export function createComponentProxy(result, _components: Record<string, any>) {
declare var he: any;

export function createComponentProxy(result, _components: Record<string, any> = {}) {
const components = {};
for (const [key, value] of Object.entries(_components)) {
if (typeof value === 'string') {
components[key] = value;
} else {
components[key] = async (props, children) => {
if (key === 'CodeBlock' || key === 'CodeSpan') {
props.code = he.decode(props.code);
}
const output = await renderJSX(
result,
h(value, { ...props, 'set:html': children.value })
Expand Down Expand Up @@ -48,7 +54,36 @@ export async function markdown(
input: string,
opts: HTMLOptions = {}
): Promise<string> {
const renderer: any = {};
if (opts.components) {
if ('Heading' in opts.components) {
renderer.heading = (children: string, level: number, raw: string, slugger) => {
const slug = slugger.slug(raw);
return `<Heading as="h${level}" href="#${slug}" text="${raw}">${children}</Heading>`
}
}
if ('CodeBlock' in opts.components) {
renderer.code = (code: string, meta = '') => {
const info = meta.split(/\s+/g) ?? [];
const lang = info[0] ?? 'plaintext';
const value = he.encode(code)
return `<CodeBlock lang=${JSON.stringify(lang)} code="${value}" ${info.splice(1).join(' ')} />`
}
}
if ('CodeSpan' in opts.components) {
renderer.codespan = (code: string) => {
const value = he.encode(code)
return `<CodeSpan code="${value}">${code}</CodeSpan>`
}
}
}
marked.use({
gfm: true,
smartypants: true,
renderer
})
const content = await marked.parse(dedent(input));

return transform(content, {
sanitize: opts.sanitize,
components: opts.components,
Expand Down
3 changes: 2 additions & 1 deletion packages/astro-remote/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@
},
"license": "MIT",
"dependencies": {
"he": "^1.2.0",
"marked": "^4.0.18",
"ultrahtml": "^0.1.1"
},
"devDependencies": {
"astro": "1.0.0-rc.7"
"astro": "1.3.0"
}
}
4 changes: 2 additions & 2 deletions packages/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
"astro": "astro"
},
"devDependencies": {
"astro": "^1.0.0-rc.7"
"astro": "^1.3.0"
},
"dependencies": {
"astro-remote": "link:../astro-remote"
"astro-remote": "^0.1.0"
}
}
5 changes: 5 additions & 0 deletions packages/demo/src/components/CodeBlock.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
const { lang, code, ...props } = Astro.props;
---

<pre><code set:text={code} /></pre>
5 changes: 5 additions & 0 deletions packages/demo/src/components/CodeSpan.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
const { code } = Astro.props;
---

<code data-code set:text={code}></code>
11 changes: 11 additions & 0 deletions packages/demo/src/components/Heading.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
const { as: Component, href } = Astro.props;
---

<Component><a href={href}><slot /></a></Component>

<style>
h2 {
color: red;
}
</style>
7 changes: 0 additions & 7 deletions packages/demo/src/components/Title.astro

This file was deleted.

64 changes: 44 additions & 20 deletions packages/demo/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,26 +1,50 @@
---
import { Markdown, Markup } from 'astro-remote';
import Title from '../components/Title.astro';
import { Markdown, Markup } from "astro-remote";
import CodeSpan from "../components/CodeSpan.astro";
import CodeBlock from "../components/CodeBlock.astro";
import Heading from "../components/Heading.astro";
const example = await fetch('https://example.com/').then(res => res.text());
const readme = await fetch('https://raw.githubusercontent.com/natemoo-re/astro-remote/main/README.md').then(res => res.text());
const example = await fetch("https://example.com/").then((res) => res.text());
const readme = `
# Hello \`world\`
"Nice"
\`inline\`
\`\`\`html filename="cool"
<div>Hello world!</div>
\`\`\`
`;
// const readme = await fetch(
// "https://raw.githubusercontent.com/natemoo-re/astro-remote/main/packages/astro-remote/README.md"
// ).then((res) => res.text());
---

<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Astro</title>
</head>
<body>
<div class="markup">
<h1>Markup</h1>
<Markup content={example} sanitize={{ dropElements: ['head'], blockElements: ['html', 'body', 'div'] }} components={{ h1: Title }} />
</div>

<div class="markdown">
<h1>Markdown</h1>
<Markdown content={readme} components={{ h1: Title }} />
</div>
</body>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Astro</title>
</head>
<body>
<div class="markup">
<h1>Markup</h1>
<Markup
content={example}
sanitize={{
dropElements: ["head"],
blockElements: ["html", "body", "div"],
}}
/>
</div>

<div class="markdown">
<h1>Markdown</h1>
<Markdown
content={readme}
components={{ Heading, CodeBlock, CodeSpan }}
/>
</div>
</body>
</html>
Loading

0 comments on commit df3e783

Please sign in to comment.