@astrojs/markdoc 0.4.3 → 0.5.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -93,7 +93,7 @@ const { Content } = await entry.render();
93
93
 
94
94
  📚 See the [Astro Content Collection docs][astro-content-collections] for more information.
95
95
 
96
- ## Configuration
96
+ ## Markdoc config
97
97
 
98
98
  `@astrojs/markdoc` offers configuration options to use all of Markdoc's features and connect UI components to your content.
99
99
 
@@ -278,7 +278,7 @@ export default defineMarkdocConfig({
278
278
 
279
279
  ### Use client-side UI components
280
280
 
281
- Tags and nodes are restricted to `.astro` files. To embed client-side UI components in Markdoc, [use a wrapper `.astro` component that renders a framework component](/en/core-concepts/framework-components/#nesting-framework-components) with your desired `client:` directive.
281
+ Tags and nodes are restricted to `.astro` files. To embed client-side UI components in Markdoc, [use a wrapper `.astro` component that renders a framework component](https://docs.astro.build/en/core-concepts/framework-components/#nesting-framework-components) with your desired `client:` directive.
282
282
 
283
283
  This example wraps a React `Aside.tsx` component with a `ClientAside.astro` component:
284
284
 
@@ -401,6 +401,32 @@ const { Content } = await entry.render();
401
401
 
402
402
  This can now be accessed as `$frontmatter` in your Markdoc.
403
403
 
404
+ ## Integration config options
405
+
406
+ The Astro Markdoc integration handles configuring Markdoc options and capabilities that are not available through the `markdoc.config.js` file.
407
+
408
+ ### `allowHTML`
409
+
410
+ Enables writing HTML markup alongside Markdoc tags and nodes.
411
+
412
+ By default, Markdoc will not recognize HTML markup as semantic content.
413
+
414
+ 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.
415
+
416
+ ```js {7} "allowHTML: true"
417
+ // astro.config.mjs
418
+ import { defineConfig } from 'astro/config';
419
+ import markdoc from '@astrojs/markdoc';
420
+
421
+ export default defineConfig({
422
+ // ...
423
+ integrations: [markdoc({ allowHTML: true })],
424
+ });
425
+ ```
426
+
427
+ > **Warning**
428
+ > 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.
429
+
404
430
  ## Examples
405
431
 
406
432
  - 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.
@@ -1,6 +1,8 @@
1
1
  import type { AstroConfig, ContentEntryType } from 'astro';
2
2
  import type { MarkdocConfigResult } from './load-config.js';
3
- export declare function getContentEntryType({ markdocConfigResult, astroConfig, }: {
3
+ import type { MarkdocIntegrationOptions } from './options.js';
4
+ export declare function getContentEntryType({ markdocConfigResult, astroConfig, options, }: {
4
5
  astroConfig: AstroConfig;
5
6
  markdocConfigResult?: MarkdocConfigResult;
7
+ options?: MarkdocIntegrationOptions;
6
8
  }): Promise<ContentEntryType>;
@@ -1,43 +1,49 @@
1
1
  import Markdoc from "@markdoc/markdoc";
2
+ import { emitESMImage } from "astro/assets/utils";
2
3
  import matter from "gray-matter";
3
4
  import fs from "node:fs";
4
- import { fileURLToPath } from "node:url";
5
- import { isComponentConfig, isValidUrl, MarkdocError, prependForwardSlash } from "./utils.js";
6
- import { emitESMImage } from "astro/assets/utils";
7
5
  import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { htmlTokenTransform } from "./html/transform/html-token-transform.js";
8
8
  import { setupConfig } from "./runtime.js";
9
+ import { getMarkdocTokenizer } from "./tokenizer.js";
10
+ import { MarkdocError, isComponentConfig, isValidUrl, prependForwardSlash } from "./utils.js";
9
11
  async function getContentEntryType({
10
12
  markdocConfigResult,
11
- astroConfig
13
+ astroConfig,
14
+ options
12
15
  }) {
13
16
  return {
14
17
  extensions: [".mdoc"],
15
18
  getEntryInfo,
16
19
  handlePropagation: true,
17
20
  async getRenderModule({ contents, fileUrl, viteId }) {
18
- var _a, _b;
19
21
  const entry = getEntryInfo({ contents, fileUrl });
20
- const tokens = markdocTokenizer.tokenize(entry.body);
22
+ const tokenizer = getMarkdocTokenizer(options);
23
+ let tokens = tokenizer.tokenize(entry.body);
24
+ if (options?.allowHTML) {
25
+ tokens = htmlTokenTransform(tokenizer, tokens);
26
+ }
21
27
  const ast = Markdoc.parse(tokens);
22
28
  const usedTags = getUsedTags(ast);
23
- const userMarkdocConfig = (markdocConfigResult == null ? void 0 : markdocConfigResult.config) ?? {};
24
- const markdocConfigUrl = markdocConfigResult == null ? void 0 : markdocConfigResult.fileUrl;
29
+ const userMarkdocConfig = markdocConfigResult?.config ?? {};
30
+ const markdocConfigUrl = markdocConfigResult?.fileUrl;
25
31
  let componentConfigByTagMap = {};
26
32
  for (const tag of usedTags) {
27
- const render = (_b = (_a = userMarkdocConfig.tags) == null ? void 0 : _a[tag]) == null ? void 0 : _b.render;
33
+ const render = userMarkdocConfig.tags?.[tag]?.render;
28
34
  if (isComponentConfig(render)) {
29
35
  componentConfigByTagMap[tag] = render;
30
36
  }
31
37
  }
32
38
  let componentConfigByNodeMap = {};
33
39
  for (const [nodeType, schema] of Object.entries(userMarkdocConfig.nodes ?? {})) {
34
- const render = schema == null ? void 0 : schema.render;
40
+ const render = schema?.render;
35
41
  if (isComponentConfig(render)) {
36
42
  componentConfigByNodeMap[nodeType] = render;
37
43
  }
38
44
  }
39
45
  const pluginContext = this;
40
- const markdocConfig = await setupConfig(userMarkdocConfig);
46
+ const markdocConfig = await setupConfig(userMarkdocConfig, options);
41
47
  const filePath = fileURLToPath(fileUrl);
42
48
  const validationErrors = Markdoc.validate(
43
49
  ast,
@@ -67,18 +73,17 @@ async function getContentEntryType({
67
73
  }
68
74
  });
69
75
  }
70
- if (astroConfig.experimental.assets) {
71
- await emitOptimizedImages(ast.children, {
72
- astroConfig,
73
- pluginContext,
74
- filePath
75
- });
76
- }
76
+ await emitOptimizedImages(ast.children, {
77
+ astroConfig,
78
+ pluginContext,
79
+ filePath
80
+ });
77
81
  const res = `import { Renderer } from '@astrojs/markdoc/components';
78
82
  import { createGetHeadings, createContentComponent } from '@astrojs/markdoc/runtime';
79
- ${markdocConfigUrl ? `import markdocConfig from ${JSON.stringify(markdocConfigUrl.pathname)};` : "const markdocConfig = {};"}${astroConfig.experimental.assets ? `
80
- import { experimentalAssetsConfig } from '@astrojs/markdoc/experimental-assets-config';
81
- markdocConfig.nodes = { ...experimentalAssetsConfig.nodes, ...markdocConfig.nodes };` : ""}
83
+ ${markdocConfigUrl ? `import markdocConfig from ${JSON.stringify(markdocConfigUrl.pathname)};` : "const markdocConfig = {};"}
84
+
85
+ import { assetsConfig } from '@astrojs/markdoc/runtime-assets-config';
86
+ markdocConfig.nodes = { ...assetsConfig.nodes, ...markdocConfig.nodes };
82
87
 
83
88
  ${getStringifiedImports(componentConfigByTagMap, "Tag", astroConfig.root)}
84
89
  ${getStringifiedImports(componentConfigByNodeMap, "Node", astroConfig.root)}
@@ -86,16 +91,19 @@ ${getStringifiedImports(componentConfigByNodeMap, "Node", astroConfig.root)}
86
91
  const tagComponentMap = ${getStringifiedMap(componentConfigByTagMap, "Tag")};
87
92
  const nodeComponentMap = ${getStringifiedMap(componentConfigByNodeMap, "Node")};
88
93
 
94
+ const options = ${JSON.stringify(options)};
95
+
89
96
  const stringifiedAst = ${JSON.stringify(
90
97
  /* Double stringify to encode *as* stringified JSON */
91
98
  JSON.stringify(ast)
92
99
  )};
93
100
 
94
- export const getHeadings = createGetHeadings(stringifiedAst, markdocConfig);
101
+ export const getHeadings = createGetHeadings(stringifiedAst, markdocConfig, options);
95
102
  export const Content = createContentComponent(
96
103
  Renderer,
97
104
  stringifiedAst,
98
105
  markdocConfig,
106
+ options,
99
107
  tagComponentMap,
100
108
  nodeComponentMap,
101
109
  )`;
@@ -107,11 +115,6 @@ export const Content = createContentComponent(
107
115
  )
108
116
  };
109
117
  }
110
- const markdocTokenizer = new Markdoc.Tokenizer({
111
- // Strip <!-- comments --> from rendered output
112
- // Without this, they're rendered as strings!
113
- allowComments: true
114
- });
115
118
  function getUsedTags(markdocAst) {
116
119
  const tags = /* @__PURE__ */ new Set();
117
120
  const validationErrors = Markdoc.validate(markdocAst);
@@ -136,12 +139,11 @@ async function emitOptimizedImages(nodeChildren, ctx) {
136
139
  for (const node of nodeChildren) {
137
140
  if (node.type === "image" && typeof node.attributes.src === "string" && shouldOptimizeImage(node.attributes.src)) {
138
141
  const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath);
139
- if ((resolved == null ? void 0 : resolved.id) && fs.existsSync(new URL(prependForwardSlash(resolved.id), "file://"))) {
142
+ if (resolved?.id && fs.existsSync(new URL(prependForwardSlash(resolved.id), "file://"))) {
140
143
  const src = await emitESMImage(
141
144
  resolved.id,
142
145
  ctx.pluginContext.meta.watchMode,
143
- ctx.pluginContext.emitFile,
144
- { config: ctx.astroConfig }
146
+ ctx.pluginContext.emitFile
145
147
  );
146
148
  node.attributes.__optimizedSrc = src;
147
149
  } else {
@@ -1,5 +1,5 @@
1
- import { unescapeHTML } from "astro/runtime/server/index.js";
2
1
  import { runHighlighterWithAstro } from "@astrojs/prism/dist/highlighter";
2
+ import { unescapeHTML } from "astro/runtime/server/index.js";
3
3
  import { Markdoc } from "../config.js";
4
4
  function prism() {
5
5
  return {
@@ -1,22 +1,6 @@
1
- import { unescapeHTML } from "astro/runtime/server/index.js";
2
1
  import Markdoc from "@markdoc/markdoc";
2
+ import { unescapeHTML } from "astro/runtime/server/index.js";
3
3
  import { getHighlighter } from "shiki";
4
- const compatThemes = {
5
- "material-darker": "material-theme-darker",
6
- "material-default": "material-theme",
7
- "material-lighter": "material-theme-lighter",
8
- "material-ocean": "material-theme-ocean",
9
- "material-palenight": "material-theme-palenight"
10
- };
11
- const normalizeTheme = (theme) => {
12
- if (typeof theme === "string") {
13
- return compatThemes[theme] || theme;
14
- } else if (compatThemes[theme.name]) {
15
- return { ...theme, name: compatThemes[theme.name] };
16
- } else {
17
- return theme;
18
- }
19
- };
20
4
  const ASTRO_COLOR_REPLACEMENTS = {
21
5
  "#000001": "var(--astro-code-color-text)",
22
6
  "#000002": "var(--astro-code-color-background)",
@@ -39,7 +23,6 @@ async function shiki({
39
23
  theme = "github-dark",
40
24
  wrap = false
41
25
  } = {}) {
42
- theme = normalizeTheme(theme);
43
26
  const cacheID = typeof theme === "string" ? theme : theme.name;
44
27
  if (!highlighterCache.has(cacheID)) {
45
28
  highlighterCache.set(
@@ -1,4 +1,5 @@
1
- import Markdoc from "@markdoc/markdoc";
1
+ import Markdoc, {
2
+ } from "@markdoc/markdoc";
2
3
  import Slugger from "github-slugger";
3
4
  import { getTextContent } from "./runtime.js";
4
5
  import { MarkdocError } from "./utils.js";
@@ -19,16 +20,15 @@ const heading = {
19
20
  level: { type: Number, required: true, default: 1 }
20
21
  },
21
22
  transform(node, config) {
22
- var _a, _b, _c;
23
23
  const { level, ...attributes } = node.transformAttributes(config);
24
24
  const children = node.transformChildren(config);
25
- if (!((_a = config.ctx) == null ? void 0 : _a.headingSlugger)) {
25
+ if (!config.ctx?.headingSlugger) {
26
26
  throw new MarkdocError({
27
27
  message: "Unexpected problem adding heading IDs to Markdoc file. Did you modify the `ctx.headingSlugger` property in your Markdoc config?"
28
28
  });
29
29
  }
30
30
  const slug = getSlug(attributes, children, config.ctx.headingSlugger);
31
- const render = ((_c = (_b = config.nodes) == null ? void 0 : _b.heading) == null ? void 0 : _c.render) ?? `h${level}`;
31
+ const render = config.nodes?.heading?.render ?? `h${level}`;
32
32
  const tagProps = (
33
33
  // For components, pass down `level` as a prop,
34
34
  // alongside `__collectHeading` for our `headings` collector.
@@ -0,0 +1 @@
1
+ export declare function parseInlineCSSToReactLikeObject(css: string | undefined | null): React.CSSProperties | undefined;
@@ -0,0 +1,21 @@
1
+ import { styleToObject } from "./style-to-object.js";
2
+ function parseInlineCSSToReactLikeObject(css) {
3
+ if (typeof css === "string") {
4
+ const cssObject = {};
5
+ styleToObject(css, (originalCssDirective, value) => {
6
+ const reactCssDirective = convertCssDirectiveNameToReactCamelCase(originalCssDirective);
7
+ cssObject[reactCssDirective] = value;
8
+ });
9
+ return cssObject;
10
+ }
11
+ return void 0;
12
+ }
13
+ function convertCssDirectiveNameToReactCamelCase(original) {
14
+ const replaced = original.replace(/-([a-z0-9])/gi, (_match, char) => {
15
+ return char.toUpperCase();
16
+ });
17
+ return replaced;
18
+ }
19
+ export {
20
+ parseInlineCSSToReactLikeObject
21
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @param {String} style
3
+ * @param {Object} [options]
4
+ * @return {Object[]}
5
+ * @throws {TypeError}
6
+ * @throws {Error}
7
+ */
8
+ export declare function parseInlineStyles(style: any, options: any): any[];
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @license MIT
3
+ *
4
+ * (The MIT License)
5
+ *
6
+ * Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ *
10
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
11
+ *
12
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13
+ */
14
+ const COMMENT_REGEX = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g;
15
+ const NEWLINE_REGEX = /\n/g;
16
+ const WHITESPACE_REGEX = /^\s*/;
17
+ const PROPERTY_REGEX = /^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/;
18
+ const COLON_REGEX = /^:\s*/;
19
+ const VALUE_REGEX = /^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};])+)/;
20
+ const SEMICOLON_REGEX = /^[;\s]*/;
21
+ const TRIM_REGEX = /^\s+|\s+$/g;
22
+ const NEWLINE = "\n";
23
+ const FORWARD_SLASH = "/";
24
+ const ASTERISK = "*";
25
+ const EMPTY_STRING = "";
26
+ const TYPE_COMMENT = "comment";
27
+ const TYPE_DECLARATION = "declaration";
28
+ function parseInlineStyles(style, options) {
29
+ if (typeof style !== "string") {
30
+ throw new TypeError("First argument must be a string");
31
+ }
32
+ if (!style)
33
+ return [];
34
+ options = options || {};
35
+ let lineno = 1;
36
+ let column = 1;
37
+ function updatePosition(str) {
38
+ let lines = str.match(NEWLINE_REGEX);
39
+ if (lines)
40
+ lineno += lines.length;
41
+ let i = str.lastIndexOf(NEWLINE);
42
+ column = ~i ? str.length - i : column + str.length;
43
+ }
44
+ function position() {
45
+ let start = { line: lineno, column };
46
+ return function(node) {
47
+ node.position = new Position(start);
48
+ whitespace();
49
+ return node;
50
+ };
51
+ }
52
+ function Position(start) {
53
+ this.start = start;
54
+ this.end = { line: lineno, column };
55
+ this.source = options.source;
56
+ }
57
+ Position.prototype.content = style;
58
+ const errorsList = [];
59
+ function error(msg) {
60
+ const err = new Error(options.source + ":" + lineno + ":" + column + ": " + msg);
61
+ err.reason = msg;
62
+ err.filename = options.source;
63
+ err.line = lineno;
64
+ err.column = column;
65
+ err.source = style;
66
+ if (options.silent) {
67
+ errorsList.push(err);
68
+ } else {
69
+ throw err;
70
+ }
71
+ }
72
+ function match(re) {
73
+ const m = re.exec(style);
74
+ if (!m)
75
+ return;
76
+ const str = m[0];
77
+ updatePosition(str);
78
+ style = style.slice(str.length);
79
+ return m;
80
+ }
81
+ function whitespace() {
82
+ match(WHITESPACE_REGEX);
83
+ }
84
+ function comments(rules) {
85
+ let c;
86
+ rules = rules || [];
87
+ while (c = comment()) {
88
+ if (c !== false) {
89
+ rules.push(c);
90
+ }
91
+ }
92
+ return rules;
93
+ }
94
+ function comment() {
95
+ const pos = position();
96
+ if (FORWARD_SLASH != style.charAt(0) || ASTERISK != style.charAt(1))
97
+ return;
98
+ let i = 2;
99
+ while (EMPTY_STRING != style.charAt(i) && (ASTERISK != style.charAt(i) || FORWARD_SLASH != style.charAt(i + 1))) {
100
+ ++i;
101
+ }
102
+ i += 2;
103
+ if (EMPTY_STRING === style.charAt(i - 1)) {
104
+ return error("End of comment missing");
105
+ }
106
+ const str = style.slice(2, i - 2);
107
+ column += 2;
108
+ updatePosition(str);
109
+ style = style.slice(i);
110
+ column += 2;
111
+ return pos({
112
+ type: TYPE_COMMENT,
113
+ comment: str
114
+ });
115
+ }
116
+ function declaration() {
117
+ const pos = position();
118
+ const prop = match(PROPERTY_REGEX);
119
+ if (!prop)
120
+ return;
121
+ comment();
122
+ if (!match(COLON_REGEX))
123
+ return error("property missing ':'");
124
+ const val = match(VALUE_REGEX);
125
+ const ret = pos({
126
+ type: TYPE_DECLARATION,
127
+ property: trim(prop[0].replace(COMMENT_REGEX, EMPTY_STRING)),
128
+ value: val ? trim(val[0].replace(COMMENT_REGEX, EMPTY_STRING)) : EMPTY_STRING
129
+ });
130
+ match(SEMICOLON_REGEX);
131
+ return ret;
132
+ }
133
+ function declarations() {
134
+ const decls = [];
135
+ comments(decls);
136
+ let decl;
137
+ while (decl = declaration()) {
138
+ if (decl !== false) {
139
+ decls.push(decl);
140
+ comments(decls);
141
+ }
142
+ }
143
+ return decls;
144
+ }
145
+ whitespace();
146
+ return declarations();
147
+ }
148
+ function trim(str) {
149
+ return str ? str.replace(TRIM_REGEX, EMPTY_STRING) : EMPTY_STRING;
150
+ }
151
+ export {
152
+ parseInlineStyles
153
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Parses inline style to object.
3
+ *
4
+ * @example
5
+ * // returns { 'line-height': '42' }
6
+ * styleToObject('line-height: 42;');
7
+ *
8
+ * @param {String} style - The inline style.
9
+ * @param {Function} [iterator] - The iterator function.
10
+ * @return {null|Object}
11
+ */
12
+ export declare function styleToObject(style: any, iterator: any): {} | null;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @license MIT
3
+ *
4
+ * The MIT License (MIT)
5
+ *
6
+ * Copyright (c) 2017 Menglin "Mark" Xu <mark@remarkablemark.org>
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining
9
+ * a copy of this software and associated documentation files (the
10
+ * "Software"), to deal in the Software without restriction, including
11
+ * without limitation the rights to use, copy, modify, merge, publish,
12
+ * distribute, sublicense, and/or sell copies of the Software, and to
13
+ * permit persons to whom the Software is furnished to do so, subject to
14
+ * the following conditions:
15
+ *
16
+ * The above copyright notice and this permission notice shall be
17
+ * included in all copies or substantial portions of the Software.
18
+ *
19
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
23
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
25
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
+ */
27
+ import { parseInlineStyles } from "./parse-inline-styles.js";
28
+ function styleToObject(style, iterator) {
29
+ let output = null;
30
+ if (!style || typeof style !== "string") {
31
+ return output;
32
+ }
33
+ let declaration;
34
+ let declarations = parseInlineStyles(style);
35
+ let hasIterator = typeof iterator === "function";
36
+ let property;
37
+ let value;
38
+ for (let i = 0, len = declarations.length; i < len; i++) {
39
+ declaration = declarations[i];
40
+ property = declaration.property;
41
+ value = declaration.value;
42
+ if (hasIterator) {
43
+ iterator(property, value, declaration);
44
+ } else if (value) {
45
+ output || (output = {});
46
+ output[property] = value;
47
+ }
48
+ }
49
+ return output;
50
+ }
51
+ export {
52
+ styleToObject
53
+ };
@@ -0,0 +1,2 @@
1
+ export { htmlTag } from './tagdefs/html.tag';
2
+ export { htmlTokenTransform } from './transform/html-token-transform';
@@ -0,0 +1,6 @@
1
+ import { htmlTag } from "./tagdefs/html.tag";
2
+ import { htmlTokenTransform } from "./transform/html-token-transform";
3
+ export {
4
+ htmlTag,
5
+ htmlTokenTransform
6
+ };
@@ -0,0 +1,2 @@
1
+ import type { Config, Schema } from '@markdoc/markdoc';
2
+ export declare const htmlTag: Schema<Config, never>;
@@ -0,0 +1,21 @@
1
+ import Markdoc from "@markdoc/markdoc";
2
+ import { parseInlineCSSToReactLikeObject } from "../css/parse-inline-css-to-react.js";
3
+ const htmlTag = {
4
+ attributes: {
5
+ name: { type: String, required: true },
6
+ attrs: { type: Object }
7
+ },
8
+ transform(node, config) {
9
+ const { name, attrs: unsafeAttributes } = node.attributes;
10
+ const children = node.transformChildren(config);
11
+ const { style, ...safeAttributes } = unsafeAttributes;
12
+ if (typeof style === "string") {
13
+ const styleObject = parseInlineCSSToReactLikeObject(style);
14
+ safeAttributes.style = styleObject;
15
+ }
16
+ return new Markdoc.Tag(name, safeAttributes, children);
17
+ }
18
+ };
19
+ export {
20
+ htmlTag
21
+ };
@@ -0,0 +1,3 @@
1
+ import type { Tokenizer } from '@markdoc/markdoc';
2
+ import type * as Token from 'markdown-it/lib/token';
3
+ export declare function htmlTokenTransform(tokenizer: Tokenizer, tokens: Token[]): Token[];
@@ -0,0 +1,154 @@
1
+ import { Parser } from "htmlparser2";
2
+ function htmlTokenTransform(tokenizer, tokens) {
3
+ const output = [];
4
+ let textBuffer = "";
5
+ let inCDATA = false;
6
+ const appendText = (text) => {
7
+ textBuffer += text;
8
+ };
9
+ const processTextBuffer = () => {
10
+ if (textBuffer.length > 0) {
11
+ const toks = tokenizer.tokenize(textBuffer);
12
+ if (toks.length === 3) {
13
+ const first = toks[0];
14
+ const second = toks[1];
15
+ const third = toks.at(2);
16
+ if (first.type === "paragraph_open" && second.type === "inline" && third && third.type === "paragraph_close" && Array.isArray(second.children)) {
17
+ for (const tok of second.children) {
18
+ if (tok.type === "text") {
19
+ if (tok.content.trim() == textBuffer.trim()) {
20
+ tok.content = textBuffer;
21
+ }
22
+ }
23
+ output.push(tok);
24
+ }
25
+ } else {
26
+ for (const tok of toks) {
27
+ output.push(tok);
28
+ }
29
+ }
30
+ } else {
31
+ for (const tok of toks) {
32
+ output.push(tok);
33
+ }
34
+ }
35
+ textBuffer = "";
36
+ }
37
+ };
38
+ const parser = new Parser(
39
+ {
40
+ oncdatastart() {
41
+ inCDATA = true;
42
+ },
43
+ oncdataend() {
44
+ inCDATA = false;
45
+ },
46
+ // when an HTML tag opens...
47
+ onopentag(name, attrs) {
48
+ processTextBuffer();
49
+ output.push({
50
+ type: "tag_open",
51
+ nesting: 1,
52
+ meta: {
53
+ tag: "html-tag",
54
+ attributes: [
55
+ { type: "attribute", name: "name", value: name },
56
+ { type: "attribute", name: "attrs", value: attrs }
57
+ ]
58
+ }
59
+ });
60
+ },
61
+ ontext(content) {
62
+ if (inCDATA) {
63
+ return;
64
+ }
65
+ if (typeof content === "string") {
66
+ appendText(content);
67
+ }
68
+ },
69
+ // when an HTML tag closes...
70
+ onclosetag(name) {
71
+ processTextBuffer();
72
+ output.push({
73
+ type: "tag_close",
74
+ nesting: -1,
75
+ meta: {
76
+ tag: "html-tag",
77
+ attributes: [{ type: "attribute", name: "name", value: name }]
78
+ }
79
+ });
80
+ }
81
+ },
82
+ {
83
+ decodeEntities: false,
84
+ recognizeCDATA: true,
85
+ recognizeSelfClosing: true
86
+ }
87
+ );
88
+ for (const token of tokens) {
89
+ if (token.type.startsWith("html")) {
90
+ parser.write(token.content);
91
+ continue;
92
+ }
93
+ if (token.type === "inline") {
94
+ if (token.children) {
95
+ token.children = htmlTokenTransform(tokenizer, token.children);
96
+ }
97
+ }
98
+ output.push(token);
99
+ }
100
+ processTextBuffer();
101
+ mutateAndCollapseExtraParagraphsUnderHtml(output);
102
+ return output;
103
+ }
104
+ function mutateAndCollapseExtraParagraphsUnderHtml(tokens) {
105
+ let done = false;
106
+ while (!done) {
107
+ const idx = findExtraParagraphUnderHtml(tokens);
108
+ if (typeof idx === "number") {
109
+ const actualChildTokens = tokens[idx + 2].children ?? [];
110
+ tokens.splice(idx, 5, ...actualChildTokens);
111
+ } else {
112
+ done = true;
113
+ }
114
+ }
115
+ }
116
+ function findExtraParagraphUnderHtml(tokens) {
117
+ if (tokens.length < 5) {
118
+ return null;
119
+ }
120
+ for (let i = 0; i < tokens.length; i++) {
121
+ const last = i + 4;
122
+ if (last > tokens.length - 1) {
123
+ break;
124
+ }
125
+ const slice = tokens.slice(i, last + 1);
126
+ const isMatch = isExtraParagraphPatternMatch(slice);
127
+ if (isMatch) {
128
+ return i;
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+ function isExtraParagraphPatternMatch(slice) {
134
+ const match = isHtmlTagOpen(slice[0]) && isParagraphOpen(slice[1]) && isInline(slice[2]) && isParagraphClose(slice[3]) && isHtmlTagClose(slice[4]);
135
+ return match;
136
+ }
137
+ function isHtmlTagOpen(token) {
138
+ return token.type === "tag_open" && token.meta && token.meta.tag === "html-tag";
139
+ }
140
+ function isHtmlTagClose(token) {
141
+ return token.type === "tag_close" && token.meta && token.meta.tag === "html-tag";
142
+ }
143
+ function isParagraphOpen(token) {
144
+ return token.type === "paragraph_open";
145
+ }
146
+ function isParagraphClose(token) {
147
+ return token.type === "paragraph_close";
148
+ }
149
+ function isInline(token) {
150
+ return token.type === "inline";
151
+ }
152
+ export {
153
+ htmlTokenTransform
154
+ };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import type { AstroIntegration } from 'astro';
2
- export default function markdocIntegration(legacyConfig?: any): AstroIntegration;
2
+ import type { MarkdocIntegrationOptions } from './options.js';
3
+ export default function markdocIntegration(options?: MarkdocIntegrationOptions): AstroIntegration;
package/dist/index.js CHANGED
@@ -1,18 +1,9 @@
1
- import { bold, red } from "kleur/colors";
2
1
  import { getContentEntryType } from "./content-entry-type.js";
3
2
  import {
4
- loadMarkdocConfig,
5
- SUPPORTED_MARKDOC_CONFIG_FILES
3
+ SUPPORTED_MARKDOC_CONFIG_FILES,
4
+ loadMarkdocConfig
6
5
  } from "./load-config.js";
7
- function markdocIntegration(legacyConfig) {
8
- if (legacyConfig) {
9
- console.log(
10
- `${red(
11
- bold("[Markdoc]")
12
- )} Passing Markdoc config from your \`astro.config\` is no longer supported. Configuration should be exported from a \`markdoc.config.mjs\` file. See the configuration docs for more: https://docs.astro.build/en/guides/integrations-guide/markdoc/#configuration`
13
- );
14
- process.exit(0);
15
- }
6
+ function markdocIntegration(options) {
16
7
  let markdocConfigResult;
17
8
  let astroConfig;
18
9
  return {
@@ -22,7 +13,9 @@ function markdocIntegration(legacyConfig) {
22
13
  const { updateConfig, addContentEntryType } = params;
23
14
  astroConfig = params.config;
24
15
  markdocConfigResult = await loadMarkdocConfig(astroConfig);
25
- addContentEntryType(await getContentEntryType({ markdocConfigResult, astroConfig }));
16
+ addContentEntryType(
17
+ await getContentEntryType({ markdocConfigResult, astroConfig, options })
18
+ );
26
19
  updateConfig({
27
20
  vite: {
28
21
  ssr: {
@@ -0,0 +1,3 @@
1
+ export interface MarkdocIntegrationOptions {
2
+ allowHTML?: boolean;
3
+ }
File without changes
@@ -1,2 +1,2 @@
1
1
  import type { Config as MarkdocConfig } from '@markdoc/markdoc';
2
- export declare const experimentalAssetsConfig: MarkdocConfig;
2
+ export declare const assetsConfig: MarkdocConfig;
@@ -1,6 +1,6 @@
1
1
  import Markdoc from "@markdoc/markdoc";
2
2
  import { Image } from "astro:assets";
3
- const experimentalAssetsConfig = {
3
+ const assetsConfig = {
4
4
  nodes: {
5
5
  image: {
6
6
  attributes: {
@@ -21,5 +21,5 @@ const experimentalAssetsConfig = {
21
21
  }
22
22
  };
23
23
  export {
24
- experimentalAssetsConfig
24
+ assetsConfig
25
25
  };
package/dist/runtime.d.ts CHANGED
@@ -1,19 +1,20 @@
1
1
  import type { MarkdownHeading } from '@astrojs/markdown-remark';
2
2
  import { type NodeType, type RenderableTreeNode } from '@markdoc/markdoc';
3
3
  import type { AstroInstance } from 'astro';
4
- import type { AstroMarkdocConfig } from './config.js';
4
+ import { type AstroMarkdocConfig } from './config.js';
5
+ import type { MarkdocIntegrationOptions } from './options.js';
5
6
  /**
6
7
  * Merge user config with default config and set up context (ex. heading ID slugger)
7
8
  * Called on each file's individual transform.
8
9
  * TODO: virtual module to merge configs per-build instead of per-file?
9
10
  */
10
- export declare function setupConfig(userConfig?: AstroMarkdocConfig): Promise<MergedConfig>;
11
+ export declare function setupConfig(userConfig: AstroMarkdocConfig | undefined, options: MarkdocIntegrationOptions | undefined): Promise<MergedConfig>;
11
12
  /** Used for synchronous `getHeadings()` function */
12
- export declare function setupConfigSync(userConfig?: AstroMarkdocConfig): MergedConfig;
13
+ export declare function setupConfigSync(userConfig: AstroMarkdocConfig | undefined, options: MarkdocIntegrationOptions | undefined): MergedConfig;
13
14
  type MergedConfig = Required<Omit<AstroMarkdocConfig, 'extends'>>;
14
15
  /** Merge function from `@markdoc/markdoc` internals */
15
16
  export declare function mergeConfig(configA: AstroMarkdocConfig, configB: AstroMarkdocConfig): MergedConfig;
16
- export declare function resolveComponentImports(markdocConfig: Required<Pick<AstroMarkdocConfig, 'tags' | 'nodes'>>, tagComponentMap: Record<string, AstroInstance['default']>, nodeComponentMap: Record<NodeType, AstroInstance['default']>): Required<Pick<AstroMarkdocConfig, "tags" | "nodes">>;
17
+ export declare function resolveComponentImports(markdocConfig: Required<Pick<AstroMarkdocConfig, 'tags' | 'nodes'>>, tagComponentMap: Record<string, AstroInstance['default']>, nodeComponentMap: Record<NodeType, AstroInstance['default']>): Required<Pick<AstroMarkdocConfig, "nodes" | "tags">>;
17
18
  /**
18
19
  * Get text content as a string from a Markdoc transform AST
19
20
  */
@@ -23,6 +24,6 @@ export declare function getTextContent(childNodes: RenderableTreeNode[]): string
23
24
  * for `headings` result on `render()` return value
24
25
  */
25
26
  export declare function collectHeadings(children: RenderableTreeNode[], collectedHeadings: MarkdownHeading[]): void;
26
- export declare function createGetHeadings(stringifiedAst: string, userConfig: AstroMarkdocConfig): () => MarkdownHeading[];
27
- export declare function createContentComponent(Renderer: AstroInstance['default'], stringifiedAst: string, userConfig: AstroMarkdocConfig, tagComponentMap: Record<string, AstroInstance['default']>, nodeComponentMap: Record<NodeType, AstroInstance['default']>): any;
27
+ export declare function createGetHeadings(stringifiedAst: string, userConfig: AstroMarkdocConfig, options: MarkdocIntegrationOptions | undefined): () => MarkdownHeading[];
28
+ export declare function createContentComponent(Renderer: AstroInstance['default'], stringifiedAst: string, userConfig: AstroMarkdocConfig, options: MarkdocIntegrationOptions | undefined, tagComponentMap: Record<string, AstroInstance['default']>, nodeComponentMap: Record<NodeType, AstroInstance['default']>): import("astro/runtime/server/index.js").AstroComponentFactory;
28
29
  export {};
package/dist/runtime.js CHANGED
@@ -1,7 +1,10 @@
1
- import Markdoc from "@markdoc/markdoc";
1
+ import Markdoc, {
2
+ } from "@markdoc/markdoc";
2
3
  import { createComponent, renderComponent } from "astro/runtime/server/index.js";
4
+ import {} from "./config.js";
3
5
  import { setupHeadingConfig } from "./heading-ids.js";
4
- async function setupConfig(userConfig = {}) {
6
+ import { htmlTag } from "./html/tagdefs/html.tag.js";
7
+ async function setupConfig(userConfig = {}, options) {
5
8
  let defaultConfig = setupHeadingConfig();
6
9
  if (userConfig.extends) {
7
10
  for (let extension of userConfig.extends) {
@@ -11,11 +14,19 @@ async function setupConfig(userConfig = {}) {
11
14
  defaultConfig = mergeConfig(defaultConfig, extension);
12
15
  }
13
16
  }
14
- return mergeConfig(defaultConfig, userConfig);
17
+ let merged = mergeConfig(defaultConfig, userConfig);
18
+ if (options?.allowHTML) {
19
+ merged = mergeConfig(merged, HTML_CONFIG);
20
+ }
21
+ return merged;
15
22
  }
16
- function setupConfigSync(userConfig = {}) {
23
+ function setupConfigSync(userConfig = {}, options) {
17
24
  const defaultConfig = setupHeadingConfig();
18
- return mergeConfig(defaultConfig, userConfig);
25
+ let merged = mergeConfig(defaultConfig, userConfig);
26
+ if (options?.allowHTML) {
27
+ merged = mergeConfig(merged, HTML_CONFIG);
28
+ }
29
+ return merged;
19
30
  }
20
31
  function mergeConfig(configA, configB) {
21
32
  return {
@@ -100,9 +111,9 @@ function collectHeadings(children, collectedHeadings) {
100
111
  collectHeadings(node.children, collectedHeadings);
101
112
  }
102
113
  }
103
- function createGetHeadings(stringifiedAst, userConfig) {
114
+ function createGetHeadings(stringifiedAst, userConfig, options) {
104
115
  return function getHeadings() {
105
- const config = setupConfigSync(userConfig);
116
+ const config = setupConfigSync(userConfig, options);
106
117
  const ast = Markdoc.Ast.fromJSON(stringifiedAst);
107
118
  const content = Markdoc.transform(ast, config);
108
119
  let collectedHeadings = [];
@@ -110,12 +121,12 @@ function createGetHeadings(stringifiedAst, userConfig) {
110
121
  return collectedHeadings;
111
122
  };
112
123
  }
113
- function createContentComponent(Renderer, stringifiedAst, userConfig, tagComponentMap, nodeComponentMap) {
124
+ function createContentComponent(Renderer, stringifiedAst, userConfig, options, tagComponentMap, nodeComponentMap) {
114
125
  return createComponent({
115
126
  async factory(result, props) {
116
127
  const withVariables = mergeConfig(userConfig, { variables: props });
117
128
  const config = resolveComponentImports(
118
- await setupConfig(withVariables),
129
+ await setupConfig(withVariables, options),
119
130
  tagComponentMap,
120
131
  nodeComponentMap
121
132
  );
@@ -124,6 +135,11 @@ function createContentComponent(Renderer, stringifiedAst, userConfig, tagCompone
124
135
  propagation: "self"
125
136
  });
126
137
  }
138
+ const HTML_CONFIG = {
139
+ tags: {
140
+ "html-tag": htmlTag
141
+ }
142
+ };
127
143
  export {
128
144
  collectHeadings,
129
145
  createContentComponent,
@@ -0,0 +1,3 @@
1
+ import type { Tokenizer } from '@markdoc/markdoc';
2
+ import type { MarkdocIntegrationOptions } from './options.js';
3
+ export declare function getMarkdocTokenizer(options: MarkdocIntegrationOptions | undefined): Tokenizer;
@@ -0,0 +1,24 @@
1
+ import Markdoc from "@markdoc/markdoc";
2
+ function getMarkdocTokenizer(options) {
3
+ const key = cacheKey(options);
4
+ if (!_cachedMarkdocTokenizers[key]) {
5
+ const tokenizerOptions = {
6
+ // Strip <!-- comments --> from rendered output
7
+ // Without this, they're rendered as strings!
8
+ allowComments: true
9
+ };
10
+ if (options?.allowHTML) {
11
+ tokenizerOptions.allowIndentation = true;
12
+ tokenizerOptions.html = true;
13
+ }
14
+ _cachedMarkdocTokenizers[key] = new Markdoc.Tokenizer(tokenizerOptions);
15
+ }
16
+ return _cachedMarkdocTokenizers[key];
17
+ }
18
+ let _cachedMarkdocTokenizers = {};
19
+ function cacheKey(options) {
20
+ return JSON.stringify(options);
21
+ }
22
+ export {
23
+ getMarkdocTokenizer
24
+ };
package/dist/utils.js CHANGED
@@ -1,7 +1,11 @@
1
1
  class MarkdocError extends Error {
2
+ loc;
3
+ title;
4
+ hint;
5
+ frame;
6
+ type = "MarkdocError";
2
7
  constructor(props, ...params) {
3
8
  super(...params);
4
- this.type = "MarkdocError";
5
9
  const { title = "MarkdocError", message, stack, location, hint, frame } = props;
6
10
  this.title = title;
7
11
  if (message)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astrojs/markdoc",
3
3
  "description": "Add support for Markdoc in your Astro site",
4
- "version": "0.4.3",
4
+ "version": "0.5.0-rc.1",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -34,7 +34,7 @@
34
34
  ".": "./dist/index.js",
35
35
  "./components": "./components/index.ts",
36
36
  "./runtime": "./dist/runtime.js",
37
- "./experimental-assets-config": "./dist/experimental-assets-config.js",
37
+ "./runtime-assets-config": "./dist/runtime-assets-config.js",
38
38
  "./package.json": "./package.json"
39
39
  },
40
40
  "typesVersions": {
@@ -56,35 +56,37 @@
56
56
  "template"
57
57
  ],
58
58
  "dependencies": {
59
- "@astrojs/internal-helpers": "^0.1.0",
60
- "@astrojs/prism": "^2.1.2",
61
59
  "@markdoc/markdoc": "^0.3.0",
62
- "esbuild": "^0.17.19",
60
+ "esbuild": "^0.18.16",
63
61
  "github-slugger": "^2.0.0",
64
62
  "gray-matter": "^4.0.3",
63
+ "htmlparser2": "^9.0.0",
65
64
  "kleur": "^4.1.5",
66
65
  "shiki": "^0.14.1",
67
- "zod": "^3.17.3"
66
+ "zod": "^3.17.3",
67
+ "@astrojs/internal-helpers": "0.2.0-rc.2",
68
+ "@astrojs/prism": "3.0.0-rc.1"
68
69
  },
69
70
  "peerDependencies": {
70
- "astro": "^2.8.5"
71
+ "astro": "^3.0.0-rc.5"
71
72
  },
72
73
  "devDependencies": {
73
- "@astrojs/markdown-remark": "^2.2.1",
74
74
  "@types/chai": "^4.3.5",
75
75
  "@types/html-escaper": "^3.0.0",
76
+ "@types/markdown-it": "^12.2.3",
76
77
  "@types/mocha": "^9.1.1",
77
78
  "chai": "^4.3.7",
78
79
  "devalue": "^4.3.2",
79
80
  "linkedom": "^0.14.26",
80
81
  "mocha": "^9.2.2",
81
82
  "rollup": "^3.25.1",
82
- "vite": "^4.3.9",
83
- "astro": "2.8.5",
83
+ "vite": "^4.4.6",
84
+ "@astrojs/markdown-remark": "3.0.0-rc.1",
85
+ "astro": "3.0.0-rc.5",
84
86
  "astro-scripts": "0.0.14"
85
87
  },
86
88
  "engines": {
87
- "node": ">=16.12.0"
89
+ "node": ">=18.14.1"
88
90
  },
89
91
  "scripts": {
90
92
  "build": "astro-scripts build \"src/**/*.ts\" && tsc",