@astrojs/markdoc 0.4.3 → 1.0.0-beta.0

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
 
@@ -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
2
  import matter from "gray-matter";
3
3
  import fs from "node:fs";
4
+ import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
- import { isComponentConfig, isValidUrl, MarkdocError, prependForwardSlash } from "./utils.js";
6
+ import { MarkdocError, isComponentConfig, isValidUrl, prependForwardSlash } from "./utils.js";
6
7
  import { emitESMImage } from "astro/assets/utils";
7
- import path from "node:path";
8
+ import { htmlTokenTransform } from "./html/transform/html-token-transform.js";
8
9
  import { setupConfig } from "./runtime.js";
10
+ import { getMarkdocTokenizer } from "./tokenizer.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,
@@ -86,16 +92,19 @@ ${getStringifiedImports(componentConfigByNodeMap, "Node", astroConfig.root)}
86
92
  const tagComponentMap = ${getStringifiedMap(componentConfigByTagMap, "Tag")};
87
93
  const nodeComponentMap = ${getStringifiedMap(componentConfigByNodeMap, "Node")};
88
94
 
95
+ const options = ${JSON.stringify(options)};
96
+
89
97
  const stringifiedAst = ${JSON.stringify(
90
98
  /* Double stringify to encode *as* stringified JSON */
91
99
  JSON.stringify(ast)
92
100
  )};
93
101
 
94
- export const getHeadings = createGetHeadings(stringifiedAst, markdocConfig);
102
+ export const getHeadings = createGetHeadings(stringifiedAst, markdocConfig, options);
95
103
  export const Content = createContentComponent(
96
104
  Renderer,
97
105
  stringifiedAst,
98
106
  markdocConfig,
107
+ options,
99
108
  tagComponentMap,
100
109
  nodeComponentMap,
101
110
  )`;
@@ -107,11 +116,6 @@ export const Content = createContentComponent(
107
116
  )
108
117
  };
109
118
  }
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
119
  function getUsedTags(markdocAst) {
116
120
  const tags = /* @__PURE__ */ new Set();
117
121
  const validationErrors = Markdoc.validate(markdocAst);
@@ -136,12 +140,11 @@ async function emitOptimizedImages(nodeChildren, ctx) {
136
140
  for (const node of nodeChildren) {
137
141
  if (node.type === "image" && typeof node.attributes.src === "string" && shouldOptimizeImage(node.attributes.src)) {
138
142
  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://"))) {
143
+ if (resolved?.id && fs.existsSync(new URL(prependForwardSlash(resolved.id), "file://"))) {
140
144
  const src = await emitESMImage(
141
145
  resolved.id,
142
146
  ctx.pluginContext.meta.watchMode,
143
- ctx.pluginContext.emitFile,
144
- { config: ctx.astroConfig }
147
+ ctx.pluginContext.emitFile
145
148
  );
146
149
  node.attributes.__optimizedSrc = src;
147
150
  } 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,5 +1,5 @@
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
4
  const compatThemes = {
5
5
  "material-darker": "material-theme-darker",
@@ -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
package/dist/runtime.d.ts CHANGED
@@ -2,18 +2,19 @@ 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
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,9 @@
1
- import Markdoc from "@markdoc/markdoc";
1
+ import Markdoc, {
2
+ } from "@markdoc/markdoc";
2
3
  import { createComponent, renderComponent } from "astro/runtime/server/index.js";
3
4
  import { setupHeadingConfig } from "./heading-ids.js";
4
- async function setupConfig(userConfig = {}) {
5
+ import { htmlTag } from "./html/tagdefs/html.tag.js";
6
+ async function setupConfig(userConfig = {}, options) {
5
7
  let defaultConfig = setupHeadingConfig();
6
8
  if (userConfig.extends) {
7
9
  for (let extension of userConfig.extends) {
@@ -11,11 +13,19 @@ async function setupConfig(userConfig = {}) {
11
13
  defaultConfig = mergeConfig(defaultConfig, extension);
12
14
  }
13
15
  }
14
- return mergeConfig(defaultConfig, userConfig);
16
+ let merged = mergeConfig(defaultConfig, userConfig);
17
+ if (options?.allowHTML) {
18
+ merged = mergeConfig(merged, HTML_CONFIG);
19
+ }
20
+ return merged;
15
21
  }
16
- function setupConfigSync(userConfig = {}) {
22
+ function setupConfigSync(userConfig = {}, options) {
17
23
  const defaultConfig = setupHeadingConfig();
18
- return mergeConfig(defaultConfig, userConfig);
24
+ let merged = mergeConfig(defaultConfig, userConfig);
25
+ if (options?.allowHTML) {
26
+ merged = mergeConfig(merged, HTML_CONFIG);
27
+ }
28
+ return merged;
19
29
  }
20
30
  function mergeConfig(configA, configB) {
21
31
  return {
@@ -100,9 +110,9 @@ function collectHeadings(children, collectedHeadings) {
100
110
  collectHeadings(node.children, collectedHeadings);
101
111
  }
102
112
  }
103
- function createGetHeadings(stringifiedAst, userConfig) {
113
+ function createGetHeadings(stringifiedAst, userConfig, options) {
104
114
  return function getHeadings() {
105
- const config = setupConfigSync(userConfig);
115
+ const config = setupConfigSync(userConfig, options);
106
116
  const ast = Markdoc.Ast.fromJSON(stringifiedAst);
107
117
  const content = Markdoc.transform(ast, config);
108
118
  let collectedHeadings = [];
@@ -110,12 +120,12 @@ function createGetHeadings(stringifiedAst, userConfig) {
110
120
  return collectedHeadings;
111
121
  };
112
122
  }
113
- function createContentComponent(Renderer, stringifiedAst, userConfig, tagComponentMap, nodeComponentMap) {
123
+ function createContentComponent(Renderer, stringifiedAst, userConfig, options, tagComponentMap, nodeComponentMap) {
114
124
  return createComponent({
115
125
  async factory(result, props) {
116
126
  const withVariables = mergeConfig(userConfig, { variables: props });
117
127
  const config = resolveComponentImports(
118
- await setupConfig(withVariables),
128
+ await setupConfig(withVariables, options),
119
129
  tagComponentMap,
120
130
  nodeComponentMap
121
131
  );
@@ -124,6 +134,11 @@ function createContentComponent(Renderer, stringifiedAst, userConfig, tagCompone
124
134
  propagation: "self"
125
135
  });
126
136
  }
137
+ const HTML_CONFIG = {
138
+ tags: {
139
+ "html-tag": htmlTag
140
+ }
141
+ };
127
142
  export {
128
143
  collectHeadings,
129
144
  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": "1.0.0-beta.0",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
7
7
  "author": "withastro",
@@ -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-beta.0",
68
+ "@astrojs/prism": "3.0.0-beta.0"
68
69
  },
69
70
  "peerDependencies": {
70
- "astro": "^2.8.5"
71
+ "astro": "^3.0.0-beta.0"
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-beta.0",
85
+ "astro": "3.0.0-beta.0",
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",