Skip to content
Merged
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
16 changes: 16 additions & 0 deletions .changeset/stupid-poets-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@astrojs/markdoc': patch
---

Adds an "allowHTML" Markdoc integration option.

When enabled, all HTML in Markdoc files will be processed, including HTML elements within Markdoc tags and nodes.

Enable this feature in the `markdoc` integration configuration:

```js
// astro.config.mjs
export default defineConfig({
integrations: [markdoc({ allowHTML: true })],
});
```
32 changes: 31 additions & 1 deletion packages/integrations/markdoc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const { Content } = await entry.render();

📚 See the [Astro Content Collection docs][astro-content-collections] for more information.

## Configuration
## Markdoc config

`@astrojs/markdoc` offers configuration options to use all of Markdoc's features and connect UI components to your content.

Expand Down Expand Up @@ -401,6 +401,36 @@ const { Content } = await entry.render();

This can now be accessed as `$frontmatter` in your Markdoc.

## Integration config options

The Astro Markdoc integration handles configuring Markdoc options and capabilities that are not available through the `markdoc.config.js` file.

### allowHTML

Enables writing HTML markup alongside Markdoc tags and nodes.

By default, Markdoc will not recognize HTML markup as semantic content.

To achieve a more Markdown-like experience, where HTML elements can be included alongside your content, set `allowHTML:true` as a `markdoc` integration option. This will enable HTML parsing in Markdoc markup.


> **Warning**
> When `allowHTML` is enabled, HTML markup inside Markdoc documents will be rendered as actual HTML elements (including `<script>`), making attack vectors like XSS possible.
>
> Ensure that any HTML markup comes from trusted sources.


```js {7} "allowHTML: true"
// astro.config.mjs
import { defineConfig } from 'astro/config';
import markdoc from '@astrojs/markdoc';

export default defineConfig({
// ...
integrations: [markdoc({ allowHTML: true })],
});
```

## Examples

- The [Astro Markdoc starter template](https://github.com/withastro/astro/tree/latest/examples/with-markdoc) shows how to use Markdoc files in your Astro project.
Expand Down
2 changes: 2 additions & 0 deletions packages/integrations/markdoc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"esbuild": "^0.17.19",
"github-slugger": "^2.0.0",
"gray-matter": "^4.0.3",
"htmlparser2": "^9.0.0",
"kleur": "^4.1.5",
"shiki": "^0.14.1",
"zod": "^3.17.3"
Expand All @@ -80,6 +81,7 @@
"@astrojs/markdown-remark": "^2.2.1",
"@types/chai": "^4.3.5",
"@types/html-escaper": "^3.0.0",
"@types/markdown-it": "^12.2.3",
"@types/mocha": "^9.1.1",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
Expand Down
27 changes: 18 additions & 9 deletions packages/integrations/markdoc/src/content-entry-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,33 @@ import path from 'node:path';
import type * as rollup from 'rollup';
import type { MarkdocConfigResult } from './load-config.js';
import { setupConfig } from './runtime.js';
import { getMarkdocTokenizer } from './tokenizer.js';
import type { MarkdocIntegrationOptions } from './options.js';
import { htmlTokenTransform } from './html/transform/html-token-transform.js';

export async function getContentEntryType({
markdocConfigResult,
astroConfig,
options,
}: {
astroConfig: AstroConfig;
markdocConfigResult?: MarkdocConfigResult;
options?: MarkdocIntegrationOptions,

}): Promise<ContentEntryType> {
return {
extensions: ['.mdoc'],
getEntryInfo,
handlePropagation: true,
async getRenderModule({ contents, fileUrl, viteId }) {
const entry = getEntryInfo({ contents, fileUrl });
const tokens = markdocTokenizer.tokenize(entry.body);
const tokenizer = getMarkdocTokenizer(options);
let tokens = tokenizer.tokenize(entry.body);

if (options?.allowHTML) {
tokens = htmlTokenTransform(tokenizer, tokens);
}

const ast = Markdoc.parse(tokens);
const usedTags = getUsedTags(ast);
const userMarkdocConfig = markdocConfigResult?.config ?? {};
Expand All @@ -51,7 +63,7 @@ export async function getContentEntryType({
}

const pluginContext = this;
const markdocConfig = await setupConfig(userMarkdocConfig);
const markdocConfig = await setupConfig(userMarkdocConfig, options);

const filePath = fileURLToPath(fileUrl);

Expand Down Expand Up @@ -113,15 +125,18 @@ ${getStringifiedImports(componentConfigByNodeMap, 'Node', astroConfig.root)}
const tagComponentMap = ${getStringifiedMap(componentConfigByTagMap, 'Tag')};
const nodeComponentMap = ${getStringifiedMap(componentConfigByNodeMap, 'Node')};

const options = ${JSON.stringify(options)};

const stringifiedAst = ${JSON.stringify(
/* Double stringify to encode *as* stringified JSON */ JSON.stringify(ast)
)};

export const getHeadings = createGetHeadings(stringifiedAst, markdocConfig);
export const getHeadings = createGetHeadings(stringifiedAst, markdocConfig, options);
export const Content = createContentComponent(
Renderer,
stringifiedAst,
markdocConfig,
options,
tagComponentMap,
nodeComponentMap,
)`;
Expand All @@ -134,12 +149,6 @@ export const Content = createContentComponent(
};
}

const markdocTokenizer = new Markdoc.Tokenizer({
// Strip <!-- comments --> from rendered output
// Without this, they're rendered as strings!
allowComments: true,
});

function getUsedTags(markdocAst: Node) {
const tags = new Set<string>();
const validationErrors = Markdoc.validate(markdocAst);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

import { styleToObject } from "./style-to-object.js";

export function parseInlineCSSToReactLikeObject(css: string | undefined | null): React.CSSProperties | undefined {
if (typeof css === "string") {
const cssObject: Record<string, string> = {};
styleToObject(css, (originalCssDirective: string, value: string) => {
const reactCssDirective = convertCssDirectiveNameToReactCamelCase(originalCssDirective);
cssObject[reactCssDirective] = value;
});
return cssObject;
}

return undefined;
}

function convertCssDirectiveNameToReactCamelCase(original: string): string {
// capture group 1 is the character to capitalize, the hyphen is omitted by virtue of being outside the capture group
const replaced = original.replace(/-([a-z0-9])/ig, (_match, char) => {
return char.toUpperCase();
});
return replaced;
}
Loading