diff options
author | 2025-06-05 14:25:23 +0000 | |
---|---|---|
committer | 2025-06-05 14:25:23 +0000 | |
commit | e586d7d704d475afe3373a1de6ae20d504f79d6d (patch) | |
tree | 7e3fa24807cebd48a86bd40f866d792181191ee9 /packages/integrations/markdoc | |
download | astro-latest.tar.gz astro-latest.tar.zst astro-latest.zip |
Sync from a8e1c0a7402940e0fc5beef669522b315052df1blatest
Diffstat (limited to 'packages/integrations/markdoc')
164 files changed, 6286 insertions, 0 deletions
diff --git a/packages/integrations/markdoc/CHANGELOG.md b/packages/integrations/markdoc/CHANGELOG.md new file mode 100644 index 000000000..b00a81542 --- /dev/null +++ b/packages/integrations/markdoc/CHANGELOG.md @@ -0,0 +1,971 @@ +# @astrojs/markdoc + +## 0.15.0 + +### Minor Changes + +- [#13809](https://github.com/withastro/astro/pull/13809) [`3c3b492`](https://github.com/withastro/astro/commit/3c3b492375bd6a63f1fb6cede3685aff999be3c9) Thanks [@ascorbic](https://github.com/ascorbic)! - Increases minimum Node.js version to 18.20.8 + + Node.js 18 has now reached end-of-life and should not be used. For now, Astro will continue to support Node.js 18.20.8, which is the final LTS release of Node.js 18, as well as Node.js 20 and Node.js 22 or later. We will drop support for Node.js 18 in a future release, so we recommend upgrading to Node.js 22 as soon as possible. See Astro's [Node.js support policy](https://docs.astro.build/en/upgrade-astro/#support) for more details. + + :warning: **Important note for users of Cloudflare Pages**: The current build image for Cloudflare Pages uses Node.js 18.17.1 by default, which is no longer supported by Astro. If you are using Cloudflare Pages you should [override the default Node.js version](https://developers.cloudflare.com/pages/configuration/build-image/#override-default-versions) to Node.js 22. This does not affect users of Cloudflare Workers, which uses Node.js 22 by default. + +### Patch Changes + +- Updated dependencies [[`3c3b492`](https://github.com/withastro/astro/commit/3c3b492375bd6a63f1fb6cede3685aff999be3c9)]: + - @astrojs/prism@3.3.0 + - @astrojs/markdown-remark@6.3.2 + +## 0.14.2 + +### Patch Changes + +- [#13731](https://github.com/withastro/astro/pull/13731) [`c3e80c2`](https://github.com/withastro/astro/commit/c3e80c25b90c803e2798b752583a8e77cdad3146) Thanks [@jsparkdev](https://github.com/jsparkdev)! - update vite to latest version for fixing CVE + +## 0.14.1 + +### Patch Changes + +- [#13591](https://github.com/withastro/astro/pull/13591) [`5dd2d3f`](https://github.com/withastro/astro/commit/5dd2d3fde8a138ed611dedf39ffa5dfeeed315f8) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Removes unused code + +## 0.14.0 + +### Minor Changes + +- [#13578](https://github.com/withastro/astro/pull/13578) [`406501a`](https://github.com/withastro/astro/commit/406501aeb7f314ae5c31f31a373c270e3b9ec715) Thanks [@stramel](https://github.com/stramel)! - The SVG import feature introduced behind a flag in [v5.0.0](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md#500) is no longer experimental and is available for general use. + + This feature allows you to import SVG files directly into your Astro project as components and inline them into your HTML. + + To use this feature, import an SVG file in your Astro project, passing any common SVG attributes to the imported component. + + ```astro + --- + import Logo from './path/to/svg/file.svg'; + --- + + <Logo <Logo width={64} height={64} fill="currentColor" /> + ``` + + If you have been waiting for stabilization before using the SVG Components feature, you can now do so. + + If you were previously using this feature, please remove the experimental flag from your Astro config: + + ```diff + import { defineConfig } from 'astro' + + export default defineConfig({ + - experimental: { + - svg: true, + - } + }) + ``` + + Additionally, a few features that were available during the experimental stage were removed in a previous release. Please see [the v5.6.0 changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md#560) for details if you have not yet already updated your project code for the experimental feature accordingly. + + Please see the [SVG Components guide in docs](https://docs.astro.build/en/guides/images/#svg-components) for more about this feature. + +## 0.13.4 + +### Patch Changes + +- [#13596](https://github.com/withastro/astro/pull/13596) [`3752519`](https://github.com/withastro/astro/commit/375251966d1b28a570bff45ff0fe7e7d2fe46f72) Thanks [@jsparkdev](https://github.com/jsparkdev)! - update vite to latest version to fix CVE + +- [#13547](https://github.com/withastro/astro/pull/13547) [`360cb91`](https://github.com/withastro/astro/commit/360cb9199a4314f90825c5639ff4396760e9cfcc) Thanks [@jsparkdev](https://github.com/jsparkdev)! - Updates vite to the latest version + +## 0.13.3 + +### Patch Changes + +- [#13526](https://github.com/withastro/astro/pull/13526) [`ff9d69e`](https://github.com/withastro/astro/commit/ff9d69e3443c80059c54f6296d19f66bb068ead3) Thanks [@jsparkdev](https://github.com/jsparkdev)! - update `vite` to the latest version + +## 0.13.2 + +### Patch Changes + +- [#13505](https://github.com/withastro/astro/pull/13505) [`a98ae5b`](https://github.com/withastro/astro/commit/a98ae5b8f5c33900379012e9e253a755c0a8927e) Thanks [@ematipico](https://github.com/ematipico)! - Updates the dependency `vite` to the latest. + +## 0.13.1 + +### Patch Changes + +- Updated dependencies [[`91c9503`](https://github.com/withastro/astro/commit/91c95034e0d0bd450170623fd8aab4b56b5b1366)]: + - @astrojs/markdown-remark@6.3.1 + +## 0.13.0 + +### Minor Changes + +- [#13352](https://github.com/withastro/astro/pull/13352) [`cb886dc`](https://github.com/withastro/astro/commit/cb886dcde6c28acca286a66be46228a4d4cc52e7) Thanks [@delucis](https://github.com/delucis)! - Adds support for a new `experimental.headingIdCompat` flag + + By default, Astro removes a trailing `-` from the end of IDs it generates for headings ending with + special characters. This differs from the behavior of common Markdown processors. + + You can now disable this behavior with a new configuration flag: + + ```js + // astro.config.mjs + import { defineConfig } from 'astro/config'; + + export default defineConfig({ + experimental: { + headingIdCompat: true, + }, + }); + ``` + + This can be useful when heading IDs and anchor links need to behave consistently across your site + and other platforms such as GitHub and npm. + + If you are [using the `rehypeHeadingIds` plugin directly](https://docs.astro.build/en/guides/markdown-content/#heading-ids-and-plugins), you can also pass this new option: + + ```js + // astro.config.mjs + import { defineConfig } from 'astro/config'; + import { rehypeHeadingIds } from '@astrojs/markdown-remark'; + import { otherPluginThatReliesOnHeadingIDs } from 'some/plugin/source'; + + export default defineConfig({ + markdown: { + rehypePlugins: [ + [rehypeHeadingIds, { experimentalHeadingIdCompat: true }], + otherPluginThatReliesOnHeadingIDs, + ], + }, + }); + ``` + +### Patch Changes + +- Updated dependencies [[`cb886dc`](https://github.com/withastro/astro/commit/cb886dcde6c28acca286a66be46228a4d4cc52e7), [`a3327ff`](https://github.com/withastro/astro/commit/a3327ffbe6373228339824684eaa6f340a20a32e)]: + - @astrojs/markdown-remark@6.3.0 + +## 0.12.11 + +### Patch Changes + +- Updated dependencies [[`042d1de`](https://github.com/withastro/astro/commit/042d1de901fd9aa66157ce078b28bcd9786e1373)]: + - @astrojs/internal-helpers@0.6.1 + - @astrojs/markdown-remark@6.2.1 + +## 0.12.10 + +### Patch Changes + +- [#13323](https://github.com/withastro/astro/pull/13323) [`80926fa`](https://github.com/withastro/astro/commit/80926fadc06492fcae55f105582b9dc8279da6b3) Thanks [@ematipico](https://github.com/ematipico)! - Updates `esbuild` and `vite` to the latest to avoid false positives audits warnings caused by `esbuild`. + +- Updated dependencies [[`1e11f5e`](https://github.com/withastro/astro/commit/1e11f5e8b722b179e382f3c792cd961b2b51f61b), [`1e11f5e`](https://github.com/withastro/astro/commit/1e11f5e8b722b179e382f3c792cd961b2b51f61b)]: + - @astrojs/internal-helpers@0.6.0 + - @astrojs/markdown-remark@6.2.0 + +## 0.12.9 + +### Patch Changes + +- Updated dependencies [[`b71bd10`](https://github.com/withastro/astro/commit/b71bd10989c0070847cecb101afb8278d5ef7091)]: + - @astrojs/internal-helpers@0.5.1 + +## 0.12.8 + +### Patch Changes + +- Updated dependencies [[`5361755`](https://github.com/withastro/astro/commit/536175528dbbe75aa978d615ba2517b64bad7879), [`db252e0`](https://github.com/withastro/astro/commit/db252e0692a0addf7239bfefc0220c525d63337d)]: + - @astrojs/internal-helpers@0.5.0 + - @astrojs/markdown-remark@6.1.0 + +## 0.12.7 + +### Patch Changes + +- [#13011](https://github.com/withastro/astro/pull/13011) [`cf30880`](https://github.com/withastro/astro/commit/cf3088060d45227dcb48e041c4ed5e0081d71398) Thanks [@ascorbic](https://github.com/ascorbic)! - Upgrades Vite + +## 0.12.6 + +### Patch Changes + +- [#12361](https://github.com/withastro/astro/pull/12361) [`3d89e62`](https://github.com/withastro/astro/commit/3d89e6282235a8da45d9ddfe02bcf7ec78056941) Thanks [@LunaticMuch](https://github.com/LunaticMuch)! - Upgrades the `esbuild` version to match `vite` + +- [#12967](https://github.com/withastro/astro/pull/12967) [`0ef1613`](https://github.com/withastro/astro/commit/0ef1613ea36439a76965290053ccc3f8afb9f400) Thanks [@bluwy](https://github.com/bluwy)! - Fixes rendering components when the `nodes.document.render` Markdoc config is set to `null` + +- Updated dependencies [[`3d89e62`](https://github.com/withastro/astro/commit/3d89e6282235a8da45d9ddfe02bcf7ec78056941)]: + - @astrojs/markdown-remark@6.0.2 + +## 0.12.5 + +### Patch Changes + +- [#12930](https://github.com/withastro/astro/pull/12930) [`a20a4d7`](https://github.com/withastro/astro/commit/a20a4d7b8ffe3ae941b5c510b319ac6f9783aabe) Thanks [@bluwy](https://github.com/bluwy)! - Fixes rendering code blocks within if tags + +## 0.12.4 + +### Patch Changes + +- [#12799](https://github.com/withastro/astro/pull/12799) [`739dbfb`](https://github.com/withastro/astro/commit/739dbfba4214107cf8fc40c702834dad33eed3b0) Thanks [@ascorbic](https://github.com/ascorbic)! - Upgrades Vite to pin esbuild + +## 0.12.3 + +### Patch Changes + +- [#12694](https://github.com/withastro/astro/pull/12694) [`495f46b`](https://github.com/withastro/astro/commit/495f46bca78665732e51c629d93a68fa392b88a4) Thanks [@ematipico](https://github.com/ematipico)! - Fixes a bug where the experimental feature `experimental.svg` was incorrectly used when generating ESM images + +## 0.12.2 + +### Patch Changes + +- Updated dependencies [[`f13417b`](https://github.com/withastro/astro/commit/f13417bfbf73130c224752379e2da33084f89554), [`87231b1`](https://github.com/withastro/astro/commit/87231b1168da66bb593f681206c42fa555dfcabc), [`a71e9b9`](https://github.com/withastro/astro/commit/a71e9b93b317edc0ded49d4d50f1b7841c8cd428)]: + - @astrojs/markdown-remark@6.0.1 + +## 0.12.1 + +### Patch Changes + +- [#12594](https://github.com/withastro/astro/pull/12594) [`4f2fd0a`](https://github.com/withastro/astro/commit/4f2fd0a0d67a748af8b611b9afc7d4c789f7c8cc) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes compatibility with Astro 5 + +## 0.12.0 + +### Minor Changes + +- [#12539](https://github.com/withastro/astro/pull/12539) [`827093e`](https://github.com/withastro/astro/commit/827093e6175549771f9d93ddf3f2be4c2c60f0b7) Thanks [@bluwy](https://github.com/bluwy)! - Drops node 21 support + +### Patch Changes + +- [#11825](https://github.com/withastro/astro/pull/11825) [`560ef15`](https://github.com/withastro/astro/commit/560ef15ad23bd137b56ef1048eb2df548b99fdce) Thanks [@bluwy](https://github.com/bluwy)! - Uses latest version of `@astrojs/markdown-remark` with updated Shiki APIs + +- [#12075](https://github.com/withastro/astro/pull/12075) [`a19530e`](https://github.com/withastro/astro/commit/a19530e377b7d7afad58a33b23c0a5df1c376819) Thanks [@bluwy](https://github.com/bluwy)! - Parses frontmatter ourselves + +- [#12584](https://github.com/withastro/astro/pull/12584) [`fa07002`](https://github.com/withastro/astro/commit/fa07002352147d45da193f28fd6e02d2d42dc67a) Thanks [@ascorbic](https://github.com/ascorbic)! - Correctly renders boolean HTML attributes + +- Updated dependencies [[`3ab3b4e`](https://github.com/withastro/astro/commit/3ab3b4efbcdd2aabea5f949deedf51a5acefae59), [`5608338`](https://github.com/withastro/astro/commit/560833843c6d3ce2b6c6c473ec4ae70e744bf255), [`827093e`](https://github.com/withastro/astro/commit/827093e6175549771f9d93ddf3f2be4c2c60f0b7), [`560ef15`](https://github.com/withastro/astro/commit/560ef15ad23bd137b56ef1048eb2df548b99fdce), [`83a2a64`](https://github.com/withastro/astro/commit/83a2a648418ad30f4eb781d1c1b5f2d8a8ac846e), [`3ab3b4e`](https://github.com/withastro/astro/commit/3ab3b4efbcdd2aabea5f949deedf51a5acefae59), [`a19530e`](https://github.com/withastro/astro/commit/a19530e377b7d7afad58a33b23c0a5df1c376819), [`1dc8f5e`](https://github.com/withastro/astro/commit/1dc8f5eb7c515c89aadc85cfa0a300d4f65e8671)]: + - @astrojs/markdown-remark@6.0.0 + - @astrojs/prism@3.2.0 + - @astrojs/internal-helpers@0.4.2 + +## 0.12.0-beta.1 + +### Minor Changes + +- [#12539](https://github.com/withastro/astro/pull/12539) [`827093e`](https://github.com/withastro/astro/commit/827093e6175549771f9d93ddf3f2be4c2c60f0b7) Thanks [@bluwy](https://github.com/bluwy)! - Drops node 21 support + +### Patch Changes + +- Updated dependencies [[`827093e`](https://github.com/withastro/astro/commit/827093e6175549771f9d93ddf3f2be4c2c60f0b7)]: + - @astrojs/prism@3.2.0-beta.0 + - @astrojs/markdown-remark@6.0.0-beta.3 + +## 0.11.5-beta.1 + +### Patch Changes + +- [#12075](https://github.com/withastro/astro/pull/12075) [`a19530e`](https://github.com/withastro/astro/commit/a19530e377b7d7afad58a33b23c0a5df1c376819) Thanks [@bluwy](https://github.com/bluwy)! - Parses frontmatter ourselves + +- Updated dependencies [[`a19530e`](https://github.com/withastro/astro/commit/a19530e377b7d7afad58a33b23c0a5df1c376819)]: + - @astrojs/markdown-remark@6.0.0-beta.2 + +## 0.11.5-beta.0 + +### Patch Changes + +- Updated dependencies [[`5608338`](https://github.com/withastro/astro/commit/560833843c6d3ce2b6c6c473ec4ae70e744bf255)]: + - @astrojs/markdown-remark@6.0.0-beta.1 + +## 1.0.0-alpha.1 + +### Patch Changes + +- [#11825](https://github.com/withastro/astro/pull/11825) [`560ef15`](https://github.com/withastro/astro/commit/560ef15ad23bd137b56ef1048eb2df548b99fdce) Thanks [@bluwy](https://github.com/bluwy)! - Uses latest version of `@astrojs/markdown-remark` with updated Shiki APIs + +- Updated dependencies [[`3ab3b4e`](https://github.com/withastro/astro/commit/3ab3b4efbcdd2aabea5f949deedf51a5acefae59), [`560ef15`](https://github.com/withastro/astro/commit/560ef15ad23bd137b56ef1048eb2df548b99fdce), [`3ab3b4e`](https://github.com/withastro/astro/commit/3ab3b4efbcdd2aabea5f949deedf51a5acefae59)]: + - @astrojs/markdown-remark@6.0.0-alpha.1 + +## 1.0.0-alpha.0 + +### Patch Changes + +- Updated dependencies [[`b6fbdaa`](https://github.com/withastro/astro/commit/b6fbdaa94a9ecec706a99e1938fbf5cd028c72e0), [`89bab1e`](https://github.com/withastro/astro/commit/89bab1e70786123fbe933a9d7a1b80c9334dcc5f), [`d74617c`](https://github.com/withastro/astro/commit/d74617cbd3278feba05909ec83db2d73d57a153e), [`83a2a64`](https://github.com/withastro/astro/commit/83a2a648418ad30f4eb781d1c1b5f2d8a8ac846e), [`e90f559`](https://github.com/withastro/astro/commit/e90f5593d23043579611452a84b9e18ad2407ef9), [`2df49a6`](https://github.com/withastro/astro/commit/2df49a6fb4f6d92fe45f7429430abe63defeacd6), [`8a53517`](https://github.com/withastro/astro/commit/8a5351737d6a14fc55f1dafad8f3b04079e81af6)]: + - astro@5.0.0-alpha.0 + - @astrojs/markdown-remark@6.0.0-alpha.0 + +## 0.11.5 + +### Patch Changes + +- Updated dependencies [[`710a1a1`](https://github.com/withastro/astro/commit/710a1a11f488ff6ed3da6d3e0723b2322ccfe27b)]: + - @astrojs/markdown-remark@5.3.0 + +## 0.11.4 + +### Patch Changes + +- [#11846](https://github.com/withastro/astro/pull/11846) [`ed7bbd9`](https://github.com/withastro/astro/commit/ed7bbd990f80cacf9c5ec2a70ad7501631b92d3f) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes an issue preventing to use Astro components as Markdoc tags and nodes when configured using the `extends` property. + +## 0.11.3 + +### Patch Changes + +- Updated dependencies [[`49b5145`](https://github.com/withastro/astro/commit/49b5145158a603b9bb951bf914a6a9780c218704)]: + - @astrojs/markdown-remark@5.2.0 + +## 0.11.2 + +### Patch Changes + +- [#11450](https://github.com/withastro/astro/pull/11450) [`eb303e1`](https://github.com/withastro/astro/commit/eb303e1ad5dade7787c0d9bbb520c21292cf3950) Thanks [@schpet](https://github.com/schpet)! - Adds support for markdown-it's typographer option + +## 0.11.1 + +### Patch Changes + +- Updated dependencies [[`b6afe6a`](https://github.com/withastro/astro/commit/b6afe6a782f68f4a279463a144baaf99cb96b6dc), [`41064ce`](https://github.com/withastro/astro/commit/41064cee78c1cccd428f710a24c483aeb275fd95)]: + - @astrojs/markdown-remark@5.1.1 + - @astrojs/internal-helpers@0.4.1 + +## 0.11.0 + +### Minor Changes + +- [#10833](https://github.com/withastro/astro/pull/10833) [`8d5f3e8`](https://github.com/withastro/astro/commit/8d5f3e8656027023f9fda51c66b0213ffe16d3a5) Thanks [@renovate](https://github.com/apps/renovate)! - Updates `@markdoc/markdoc` to v0.4 + +### Patch Changes + +- [#10833](https://github.com/withastro/astro/pull/10833) [`8d5f3e8`](https://github.com/withastro/astro/commit/8d5f3e8656027023f9fda51c66b0213ffe16d3a5) Thanks [@renovate](https://github.com/apps/renovate)! - Updates `esbuild` dependency to v0.20. This should not affect projects in most cases. + +## 0.10.0 + +### Minor Changes + +- [#10689](https://github.com/withastro/astro/pull/10689) [`683d51a5eecafbbfbfed3910a3f1fbf0b3531b99`](https://github.com/withastro/astro/commit/683d51a5eecafbbfbfed3910a3f1fbf0b3531b99) Thanks [@ematipico](https://github.com/ematipico)! - Deprecate support for versions of Node.js older than `v18.17.1` for Node.js 18, older than `v20.0.3` for Node.js 20, and the complete Node.js v19 release line. + + This change is in line with Astro's [Node.js support policy](https://docs.astro.build/en/upgrade-astro/#support). + +### Patch Changes + +- Updated dependencies [[`ccafa8d230f65c9302421a0ce0a0adc5824bfd55`](https://github.com/withastro/astro/commit/ccafa8d230f65c9302421a0ce0a0adc5824bfd55), [`683d51a5eecafbbfbfed3910a3f1fbf0b3531b99`](https://github.com/withastro/astro/commit/683d51a5eecafbbfbfed3910a3f1fbf0b3531b99)]: + - @astrojs/markdown-remark@5.1.0 + - @astrojs/prism@3.1.0 + +## 0.9.5 + +### Patch Changes + +- [#10649](https://github.com/withastro/astro/pull/10649) [`90cfade88c2b9a34d8a5fe711ce329732d690409`](https://github.com/withastro/astro/commit/90cfade88c2b9a34d8a5fe711ce329732d690409) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Add automatic resolution for Markdoc partials. This allows you to render other Markdoc files inside of a given entry. Reference files using the `partial` tag with a `file` attribute for the relative file path: + + ```md + <!--src/content/blog/post.mdoc--> + + {% partial file="my-partials/_diagram.mdoc" /%} + + <!--src/content/blog/my-partials/_diagram.mdoc--> + + ## Diagram + + This partial will render inside of `post.mdoc.` + +  + ``` + +## 0.9.4 + +### Patch Changes + +- [#10632](https://github.com/withastro/astro/pull/10632) [`da2fb875fc58b65a21d37a3d29f570fa20b5219c`](https://github.com/withastro/astro/commit/da2fb875fc58b65a21d37a3d29f570fa20b5219c) Thanks [@bluwy](https://github.com/bluwy)! - Moves `@astrojs/markdown-remark` as a dependency + +- Updated dependencies [[`2cf116f80cb5e421ab5cc5eb4a654e7b78c1b8de`](https://github.com/withastro/astro/commit/2cf116f80cb5e421ab5cc5eb4a654e7b78c1b8de), [`374efcdff9625ca43309d89e3b9cfc9174351512`](https://github.com/withastro/astro/commit/374efcdff9625ca43309d89e3b9cfc9174351512)]: + - @astrojs/markdown-remark@5.0.0 + +## 0.9.3 + +### Patch Changes + +- Updated dependencies [[`20463a6c1e1271d8dc3cb0ab3419ee5c72abd218`](https://github.com/withastro/astro/commit/20463a6c1e1271d8dc3cb0ab3419ee5c72abd218)]: + - @astrojs/internal-helpers@0.4.0 + +## 0.9.2 + +### Patch Changes + +- Updated dependencies [[`1ea0a25b94125e4f6f2ac82b42f638e22d7bdffd`](https://github.com/withastro/astro/commit/1ea0a25b94125e4f6f2ac82b42f638e22d7bdffd)]: + - @astrojs/internal-helpers@0.3.0 + +## 0.9.1 + +### Patch Changes + +- [#10278](https://github.com/withastro/astro/pull/10278) [`a548a3a99c2835c19662fc38636f92b2bda26614`](https://github.com/withastro/astro/commit/a548a3a99c2835c19662fc38636f92b2bda26614) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes original images sometimes being kept / deleted when they shouldn't in both MDX and Markdoc + +## 0.9.0 + +### Minor Changes + +- [#9958](https://github.com/withastro/astro/pull/9958) [`14ce8a6ebfc9daf951d2dca54737d857c229667c`](https://github.com/withastro/astro/commit/14ce8a6ebfc9daf951d2dca54737d857c229667c) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Adds support for using a custom tag (component) for optimized images + + Starting from this version, when a tag called `image` is used, its `src` attribute will automatically be resolved if it's a local image. Astro will pass the result `ImageMetadata` object to the underlying component as the `src` prop. For non-local images (i.e. images using URLs or absolute paths), Astro will continue to pass the `src` as a string. + + ```ts + // markdoc.config.mjs + import { component, defineMarkdocConfig, nodes } from '@astrojs/markdoc/config'; + + export default defineMarkdocConfig({ + tags: { + image: { + attributes: nodes.image.attributes, + render: component('./src/components/MarkdocImage.astro'), + }, + }, + }); + ``` + + ```astro + --- + // src/components/MarkdocImage.astro + import { Image } from 'astro:assets'; + + interface Props { + src: ImageMetadata | string; + alt: string; + width: number; + height: number; + } + + const { src, alt, width, height } = Astro.props; + --- + + <Image {src} {alt} {width} {height} /> + ``` + + ```mdoc + {% image src="./astro-logo.png" alt="Astro Logo" width="100" height="100" %} + ``` + +## 0.8.3 + +### Patch Changes + +- [#9643](https://github.com/withastro/astro/pull/9643) [`e9a72d9a91a3741566866bcaab11172cb0dc7d31`](https://github.com/withastro/astro/commit/e9a72d9a91a3741566866bcaab11172cb0dc7d31) Thanks [@blackmann](https://github.com/blackmann)! - Removes unnecessary `shikiji` dependency + +## 0.8.2 + +### Patch Changes + +- [#9479](https://github.com/withastro/astro/pull/9479) [`1baf0b0d3cbd0564954c2366a7278794fad6726e`](https://github.com/withastro/astro/commit/1baf0b0d3cbd0564954c2366a7278794fad6726e) Thanks [@sarah11918](https://github.com/sarah11918)! - Updates README + +## 0.8.1 + +### Patch Changes + +- [#9452](https://github.com/withastro/astro/pull/9452) [`e83b5095f`](https://github.com/withastro/astro/commit/e83b5095f164f48ba40fc715a805fc66a3e39dcf) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Upgrades vite to latest + +## 0.8.0 + +### Minor Changes + +- [#9263](https://github.com/withastro/astro/pull/9263) [`3cbd8ea75`](https://github.com/withastro/astro/commit/3cbd8ea7534910e3beae396dcfa93ce87dcdd91f) Thanks [@bluwy](https://github.com/bluwy)! - Removes internal `propagators` handling for Astro 3 + +## 1.0.0-beta.1 + +### Minor Changes + +- [#9263](https://github.com/withastro/astro/pull/9263) [`3cbd8ea75`](https://github.com/withastro/astro/commit/3cbd8ea7534910e3beae396dcfa93ce87dcdd91f) Thanks [@bluwy](https://github.com/bluwy)! - Removes internal `propagators` handling for Astro 3 + +## 1.0.0-beta.0 + +### Patch Changes + +- Updated dependencies [[`abf601233`](https://github.com/withastro/astro/commit/abf601233f8188d118a8cb063c777478d8d9f1a3), [`6201bbe96`](https://github.com/withastro/astro/commit/6201bbe96c2a083fb201e4a43a9bd88499821a3e), [`cdabf6ef0`](https://github.com/withastro/astro/commit/cdabf6ef02be7220fd2b6bdcef924ceca089381e), [`1c48ed286`](https://github.com/withastro/astro/commit/1c48ed286538ab9e354eca4e4dcd7c6385c96721), [`37697a2c5`](https://github.com/withastro/astro/commit/37697a2c5511572dc29c0a4ea46f90c2f62be8e6), [`bd0c2e9ae`](https://github.com/withastro/astro/commit/bd0c2e9ae3389a9d3085050c1e8134ae98dff299), [`0fe3a7ed5`](https://github.com/withastro/astro/commit/0fe3a7ed5d7bb1a9fce1623e84ba14104b51223c), [`710be505c`](https://github.com/withastro/astro/commit/710be505c9ddf416e77a75343d8cae9c497d72c6), [`153a5abb9`](https://github.com/withastro/astro/commit/153a5abb905042ac68b712514dc9ec387d3e6b17)]: + - astro@4.0.0-beta.0 + +## 0.7.2 + +### Patch Changes + +- [#9083](https://github.com/withastro/astro/pull/9083) [`4537ecf0d`](https://github.com/withastro/astro/commit/4537ecf0d060f89cb8c000338a7fc5f4197a88c8) Thanks [@bluwy](https://github.com/bluwy)! - Uses new `createShikiHighlighter` API from `@astrojs/markdown-remark` to avoid code duplication + +## 0.7.1 + +### Patch Changes + +- [#8759](https://github.com/withastro/astro/pull/8759) [`01c801108`](https://github.com/withastro/astro/commit/01c801108f1f5429436e4fc930018bf96ed31f79) Thanks [@lutaok](https://github.com/lutaok)! - Fix build process on markdoc integration when root folder contains spaces + +- [#8762](https://github.com/withastro/astro/pull/8762) [`35cd810f0`](https://github.com/withastro/astro/commit/35cd810f0f988010fbb8e6d7ab205de5d816e2b2) Thanks [@evadecker](https://github.com/evadecker)! - Upgrades Zod to 3.22.4 + +## 0.7.0 + +### Minor Changes + +- [#8802](https://github.com/withastro/astro/pull/8802) [`73b8d60f8`](https://github.com/withastro/astro/commit/73b8d60f8c3eeae74035202b0ea0d4848e736c7d) Thanks [@AndyClifford](https://github.com/AndyClifford)! - Added ignoreIndentation as a markdoc integration option to enable better readability of source code. + +### Patch Changes + +- Updated dependencies [[`26b77b8fe`](https://github.com/withastro/astro/commit/26b77b8fef0e03bfc5550aecaa1f56a4fc1cd297)]: + - astro@3.3.4 + +## 0.6.0 + +### Minor Changes + +- [#8502](https://github.com/withastro/astro/pull/8502) [`c4270e476`](https://github.com/withastro/astro/commit/c4270e47681ee2453f3fea07fed7b238645fd6ea) Thanks [@bluwy](https://github.com/bluwy)! - Updates the internal `shiki` syntax highlighter to `shikiji`, an ESM-focused alternative that simplifies bundling and maintenance. + + There are no new options and no changes to how you author code blocks and syntax highlighting. + + **Potentially breaking change:** While this refactor should be transparent for most projects, the transition to `shikiji` now produces a smaller HTML markup by attaching a fallback `color` style to the `pre` or `code` element, instead of to the line `span` directly. For example: + + Before: + + ```html + <code class="astro-code" style="background-color: #24292e"> + <pre> + <span class="line" style="color: #e1e4e8">my code</span> + </pre> + </code> + ``` + + After: + + ```html + <code class="astro-code" style="background-color: #24292e; color: #e1e4e8"> + <pre> + <span class="line">my code<span> + </pre> + </code> + ``` + + This does not affect the colors as the `span` will inherit the `color` from the parent, but if you're relying on a specific HTML markup, please check your site carefully after upgrading to verify the styles. + +### Patch Changes + +- Updated dependencies [[`2993055be`](https://github.com/withastro/astro/commit/2993055bed2764c31ff4b4f55b81ab6b1ae6b401), [`c4270e476`](https://github.com/withastro/astro/commit/c4270e47681ee2453f3fea07fed7b238645fd6ea), [`bd5aa1cd3`](https://github.com/withastro/astro/commit/bd5aa1cd35ecbd2784f30dd836ff814684fee02b), [`f369fa250`](https://github.com/withastro/astro/commit/f369fa25055a3497ebaf61c88fb0e8af56c73212), [`391729686`](https://github.com/withastro/astro/commit/391729686bcc8404a7dd48c5987ee380daf3200f), [`f999365b8`](https://github.com/withastro/astro/commit/f999365b8248b8b14f3743e68a42d450d06acff3), [`b2ae9ee0c`](https://github.com/withastro/astro/commit/b2ae9ee0c42b11ffc1d3f070d1d5ac881aef84ed), [`0abff97fe`](https://github.com/withastro/astro/commit/0abff97fed3db14be3c75ff9ece3aab67c4ba783), [`3bef32f81`](https://github.com/withastro/astro/commit/3bef32f81c56bc600ca307f1bd40787e23e625a5)]: + - astro@3.3.0 + +## 0.5.2 + +### Patch Changes + +- [#8737](https://github.com/withastro/astro/pull/8737) [`6f60da805`](https://github.com/withastro/astro/commit/6f60da805e0014bc50dd07bef972e91c73560c3c) Thanks [@ematipico](https://github.com/ematipico)! - Add provenance statement when publishing the library from CI + +- Updated dependencies [[`6f60da805`](https://github.com/withastro/astro/commit/6f60da805e0014bc50dd07bef972e91c73560c3c), [`d78806dfe`](https://github.com/withastro/astro/commit/d78806dfe0301ea7ffe6c7c1f783bd415ac7cda9), [`d1c75fe15`](https://github.com/withastro/astro/commit/d1c75fe158839699c59728cf3a83888e8c72a459), [`aa265d730`](https://github.com/withastro/astro/commit/aa265d73024422967c1b1c68ad268c419c6c798f), [`78adbc443`](https://github.com/withastro/astro/commit/78adbc4433208458291e36713909762e148e1e5d), [`21e0757ea`](https://github.com/withastro/astro/commit/21e0757ea22a57d344c934045ca19db93b684436), [`357270f2a`](https://github.com/withastro/astro/commit/357270f2a3d0bf2aa634ba7e52e9d17618eff4a7)]: + - @astrojs/internal-helpers@0.2.1 + - astro@3.2.3 + +## 0.5.1 + +### Patch Changes + +- [#8710](https://github.com/withastro/astro/pull/8710) [`4c2bec681`](https://github.com/withastro/astro/commit/4c2bec681b0752e7215b8a32bd2d44bf477adac1) Thanks [@matthewp](https://github.com/matthewp)! - Fixes View transition styles being missing when component used multiple times + +- Updated dependencies [[`455af3235`](https://github.com/withastro/astro/commit/455af3235b3268852e6988accecc796f03f6d16e), [`4c2bec681`](https://github.com/withastro/astro/commit/4c2bec681b0752e7215b8a32bd2d44bf477adac1)]: + - astro@3.2.2 + +## 0.5.0 + +### Minor Changes + +- [#8188](https://github.com/withastro/astro/pull/8188) [`d0679a666`](https://github.com/withastro/astro/commit/d0679a666f37da0fca396d42b9b32bbb25d29312) Thanks [@ematipico](https://github.com/ematipico)! - Remove support for Node 16. The lowest supported version by Astro and all integrations is now v18.14.1. As a reminder, Node 16 will be deprecated on the 11th September 2023. + +- [#8179](https://github.com/withastro/astro/pull/8179) [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7) Thanks [@matthewp](https://github.com/matthewp)! - Astro 3.0 Release Candidate + +- [#8169](https://github.com/withastro/astro/pull/8169) [`e79e3779d`](https://github.com/withastro/astro/commit/e79e3779df0ad35253abcdb931d622847d9adb12) Thanks [@bluwy](https://github.com/bluwy)! - Remove pre-shiki v0.14 theme names for compatibility. Please rename to the new theme names to migrate: + + - `material-darker` -> `material-theme-darker` + - `material-default` -> `material-theme` + - `material-lighter` -> `material-theme-lighter` + - `material-ocean` -> `material-theme-ocean` + - `material-palenight` -> `material-theme-palenight` + +### Patch Changes + +- Updated dependencies [[`d0679a666`](https://github.com/withastro/astro/commit/d0679a666f37da0fca396d42b9b32bbb25d29312), [`db39206cb`](https://github.com/withastro/astro/commit/db39206cbb85b034859ac416179f141184bb2bff), [`2aa6d8ace`](https://github.com/withastro/astro/commit/2aa6d8ace398a41c2dec5473521d758816b08191), [`adf9fccfd`](https://github.com/withastro/astro/commit/adf9fccfdda107c2224558f1c2e6a77847ac0a8a), [`0c7b42dc6`](https://github.com/withastro/astro/commit/0c7b42dc6780e687e416137539f55a3a427d1d10), [`46c4c0e05`](https://github.com/withastro/astro/commit/46c4c0e053f830585b9ef229ce1c259df00a80f8), [`364d861bd`](https://github.com/withastro/astro/commit/364d861bd527b8511968e2837728148f090bedef), [`2484dc408`](https://github.com/withastro/astro/commit/2484dc4080e5cd84b9a53648a1de426d7c907be2), [`81545197a`](https://github.com/withastro/astro/commit/81545197a32fd015d763fc386c8b67e0e08b7393), [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7), [`c2c71d90c`](https://github.com/withastro/astro/commit/c2c71d90c264a2524f99e0373ab59015f23ad4b1), [`cd2d7e769`](https://github.com/withastro/astro/commit/cd2d7e76981ef9b9013453aa2629838e1e9fd422), [`80f1494cd`](https://github.com/withastro/astro/commit/80f1494cdaf72e58a420adb4f7c712d4089e1923), [`e45f30293`](https://github.com/withastro/astro/commit/e45f3029340db718b6ed7e91b5d14f5cf14cd71d), [`c0de7a7b0`](https://github.com/withastro/astro/commit/c0de7a7b0f042cd49cbea4f4ac1b2ab6f9fef644), [`65c354969`](https://github.com/withastro/astro/commit/65c354969e6fe0ef6d622e8f4c545e2f717ce8c6), [`3c3100851`](https://github.com/withastro/astro/commit/3c31008519ce68b5b1b1cb23b71fbe0a2d506882), [`34cb20021`](https://github.com/withastro/astro/commit/34cb2002161ba88df6bcb72fecfd12ed867c134b), [`a824863ab`](https://github.com/withastro/astro/commit/a824863ab1c451f4068eac54f28dd240573e1cba), [`44f7a2872`](https://github.com/withastro/astro/commit/44f7a28728c56c04ac377b6e917329f324874043), [`1048aca55`](https://github.com/withastro/astro/commit/1048aca550769415e528016e42b358ffbfd44b61), [`be6bbd2c8`](https://github.com/withastro/astro/commit/be6bbd2c86b9bf5268e765bb937dda00ff15781a), [`9e021a91c`](https://github.com/withastro/astro/commit/9e021a91c57d10809f588dd47968fc0e7f8b4d5c), [`7511a4980`](https://github.com/withastro/astro/commit/7511a4980fd36536464c317de33a5190427f430a), [`c37632a20`](https://github.com/withastro/astro/commit/c37632a20d06164fb97a4c2fc48df6d960398832), [`acf652fc1`](https://github.com/withastro/astro/commit/acf652fc1d5db166231e87e22d0d50444f5556d8), [`42785c7b7`](https://github.com/withastro/astro/commit/42785c7b784b151e6d582570e5d74482129e8eb8), [`8450379db`](https://github.com/withastro/astro/commit/8450379db854fb1eaa9f38f21d65db240bc616cd), [`dbc97b121`](https://github.com/withastro/astro/commit/dbc97b121f42583728f1cdfdbf14575fda943f5b), [`7d2f311d4`](https://github.com/withastro/astro/commit/7d2f311d428e3d1c8c13b9bf2a708d6435713fc2), [`2540feedb`](https://github.com/withastro/astro/commit/2540feedb06785d5a20eecc3668849f147d778d4), [`ea7ff5177`](https://github.com/withastro/astro/commit/ea7ff5177dbcd7b2508cb1eef1b22b8ee1f47079), [`68efd4a8b`](https://github.com/withastro/astro/commit/68efd4a8b29f248397667801465b3152dc98e9a7), [`7bd1b86f8`](https://github.com/withastro/astro/commit/7bd1b86f85c06fdde0a1ed9146d01bac69990671), [`036388f66`](https://github.com/withastro/astro/commit/036388f66dab68ad54b895ed86f9176958dd83c8), [`519a1c4e8`](https://github.com/withastro/astro/commit/519a1c4e8407c7abcb8d879b67a9f4b960652cae), [`1f58a7a1b`](https://github.com/withastro/astro/commit/1f58a7a1bea6888868b689dac94801d554319b02), [`2ae9d37f0`](https://github.com/withastro/astro/commit/2ae9d37f0a9cb21ab288d3c30aecb6d84db87788), [`a8f35777e`](https://github.com/withastro/astro/commit/a8f35777e7e322068a4e2f520c2c9e43ade19e58), [`70f34f5a3`](https://github.com/withastro/astro/commit/70f34f5a355f42526ee9e5355f3de8e510002ea2), [`5208a3c8f`](https://github.com/withastro/astro/commit/5208a3c8fefcec7694857fb344af351f4631fc34), [`84af8ed9d`](https://github.com/withastro/astro/commit/84af8ed9d1e6401c6ebc9c60fe8cddb44d5044b0), [`f003e7364`](https://github.com/withastro/astro/commit/f003e7364317cafdb8589913b26b28e928dd07c9), [`ffc9e2d3d`](https://github.com/withastro/astro/commit/ffc9e2d3de46049bf3d82140ef018f524fb03187), [`732111cdc`](https://github.com/withastro/astro/commit/732111cdce441639db31f40f621df48442d00969), [`0f637c71e`](https://github.com/withastro/astro/commit/0f637c71e511cb4c51712128d217a26c8eee4d40), [`33b8910cf`](https://github.com/withastro/astro/commit/33b8910cfdce5713891c50a84a0a8fe926311710), [`8a5b0c1f3`](https://github.com/withastro/astro/commit/8a5b0c1f3a4be6bb62db66ec70144109ff5b4c59), [`148e61d24`](https://github.com/withastro/astro/commit/148e61d2492456811f8a3c8daaab1c3429a2ffdc), [`e79e3779d`](https://github.com/withastro/astro/commit/e79e3779df0ad35253abcdb931d622847d9adb12), [`632579dc2`](https://github.com/withastro/astro/commit/632579dc2094cc342929261c89e689f0dd358284), [`3674584e0`](https://github.com/withastro/astro/commit/3674584e02b161a698b429ceb66723918fdc56ac), [`1db4e92c1`](https://github.com/withastro/astro/commit/1db4e92c12ed73681217f5cefd39f2f47542f961), [`e7f872e91`](https://github.com/withastro/astro/commit/e7f872e91e852b901cf221a5151077dec64305bf), [`16f09dfff`](https://github.com/withastro/astro/commit/16f09dfff7722fda99dd0412e3006a7a39c80829), [`4477bb41c`](https://github.com/withastro/astro/commit/4477bb41c8ed688785c545731ef5b184b629f4e5), [`55c10d1d5`](https://github.com/withastro/astro/commit/55c10d1d564e805efc3c1a7c48e0d9a1cdf0c7ed), [`3e834293d`](https://github.com/withastro/astro/commit/3e834293d47ab2761a7aa013916e8371871efb7f), [`96beb883a`](https://github.com/withastro/astro/commit/96beb883ad87f8bbf5b2f57e14a743763d2a6f58), [`997a0db8a`](https://github.com/withastro/astro/commit/997a0db8a4e3851edd69384cf5eadbb969e1d547), [`80f1494cd`](https://github.com/withastro/astro/commit/80f1494cdaf72e58a420adb4f7c712d4089e1923), [`0f0625504`](https://github.com/withastro/astro/commit/0f0625504145f18cba7dc6cf20291cb2abddc5a9), [`e1ae56e72`](https://github.com/withastro/astro/commit/e1ae56e724d0f83db1230359e06cd6bc26f5fa26), [`f32d093a2`](https://github.com/withastro/astro/commit/f32d093a280faafff024228c12bb438156ec34d7), [`f01eb585e`](https://github.com/withastro/astro/commit/f01eb585e7c972d940761309b1595f682b6922d2), [`b76c166bd`](https://github.com/withastro/astro/commit/b76c166bdd8e28683f62806aef968d1e0c3b06d9), [`a87cbe400`](https://github.com/withastro/astro/commit/a87cbe400314341d5f72abf86ea264e6b47c091f), [`866ed4098`](https://github.com/withastro/astro/commit/866ed4098edffb052239cdb26e076cf8db61b1d9), [`767eb6866`](https://github.com/withastro/astro/commit/767eb68666eb777965baa0d6ade20bbafecf95bf), [`32669cd47`](https://github.com/withastro/astro/commit/32669cd47555e9c7433c3998a2b6e624dfb2d8e9)]: + - @astrojs/prism@3.0.0 + - astro@3.0.0 + - @astrojs/internal-helpers@0.2.0 + +## 0.5.0-rc.1 + +### Minor Changes + +- [#8179](https://github.com/withastro/astro/pull/8179) [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7) Thanks [@matthewp](https://github.com/matthewp)! - Astro 3.0 Release Candidate + +- [#8169](https://github.com/withastro/astro/pull/8169) [`e79e3779d`](https://github.com/withastro/astro/commit/e79e3779df0ad35253abcdb931d622847d9adb12) Thanks [@bluwy](https://github.com/bluwy)! - Remove pre-shiki v0.14 theme names for compatibility. Please rename to the new theme names to migrate: + + - `material-darker` -> `material-theme-darker` + - `material-default` -> `material-theme` + - `material-lighter` -> `material-theme-lighter` + - `material-ocean` -> `material-theme-ocean` + - `material-palenight` -> `material-theme-palenight` + +### Patch Changes + +- Updated dependencies [[`adf9fccfd`](https://github.com/withastro/astro/commit/adf9fccfdda107c2224558f1c2e6a77847ac0a8a), [`582132328`](https://github.com/withastro/astro/commit/5821323285646aee7ff9194a505f708028e4db57), [`81545197a`](https://github.com/withastro/astro/commit/81545197a32fd015d763fc386c8b67e0e08b7393), [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7), [`be6bbd2c8`](https://github.com/withastro/astro/commit/be6bbd2c86b9bf5268e765bb937dda00ff15781a), [`42785c7b7`](https://github.com/withastro/astro/commit/42785c7b784b151e6d582570e5d74482129e8eb8), [`95120efbe`](https://github.com/withastro/astro/commit/95120efbe817163663492181cbeb225849354493), [`2ae9d37f0`](https://github.com/withastro/astro/commit/2ae9d37f0a9cb21ab288d3c30aecb6d84db87788), [`f003e7364`](https://github.com/withastro/astro/commit/f003e7364317cafdb8589913b26b28e928dd07c9), [`732111cdc`](https://github.com/withastro/astro/commit/732111cdce441639db31f40f621df48442d00969), [`33b8910cf`](https://github.com/withastro/astro/commit/33b8910cfdce5713891c50a84a0a8fe926311710), [`e79e3779d`](https://github.com/withastro/astro/commit/e79e3779df0ad35253abcdb931d622847d9adb12), [`179796405`](https://github.com/withastro/astro/commit/179796405e053b559d83f84507e5a465861a029a), [`a87cbe400`](https://github.com/withastro/astro/commit/a87cbe400314341d5f72abf86ea264e6b47c091f), [`767eb6866`](https://github.com/withastro/astro/commit/767eb68666eb777965baa0d6ade20bbafecf95bf)]: + - astro@3.0.0-rc.5 + - @astrojs/prism@3.0.0-rc.1 + - @astrojs/internal-helpers@0.2.0-rc.2 + +## 1.0.0-beta.1 + +### Patch Changes + +- Updated dependencies [[`2aa6d8ace`](https://github.com/withastro/astro/commit/2aa6d8ace398a41c2dec5473521d758816b08191)]: + - @astrojs/internal-helpers@0.2.0-beta.1 + - astro@3.0.0-beta.2 + +## 1.0.0-beta.0 + +### Minor Changes + +- [`1eae2e3f7`](https://github.com/withastro/astro/commit/1eae2e3f7d693c9dfe91c8ccfbe606d32bf2fb81) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Remove support for Node 16. The lowest supported version by Astro and all integrations is now v18.14.1. As a reminder, Node 16 will be deprecated on the 11th September 2023. + +### Patch Changes + +- Updated dependencies [[`1eae2e3f7`](https://github.com/withastro/astro/commit/1eae2e3f7d693c9dfe91c8ccfbe606d32bf2fb81), [`76ddef19c`](https://github.com/withastro/astro/commit/76ddef19ccab6e5f7d3a5740cd41acf10e334b38), [`9b4f70a62`](https://github.com/withastro/astro/commit/9b4f70a629f55e461759ba46f68af7097a2e9215), [`3fdf509b2`](https://github.com/withastro/astro/commit/3fdf509b2731a9b2f972d89291e57cf78d62c769), [`2f951cd40`](https://github.com/withastro/astro/commit/2f951cd403dfcc2c3ca6aae618ae3e1409516e32), [`c022a4217`](https://github.com/withastro/astro/commit/c022a4217a805d223c1494e9eda4e48bbf810388), [`67becaa58`](https://github.com/withastro/astro/commit/67becaa580b8f787df58de66b7008b7098f1209c), [`bc37331d8`](https://github.com/withastro/astro/commit/bc37331d8154e3e95a8df9131e4e014e78a7a9e7), [`dfc2d93e3`](https://github.com/withastro/astro/commit/dfc2d93e3c645995379358fabbdfa9aab99f43d8), [`3dc1ca2fa`](https://github.com/withastro/astro/commit/3dc1ca2fac8d9965cc5085a5d09e72ed87b4281a), [`1be84dfee`](https://github.com/withastro/astro/commit/1be84dfee3ce8e6f5cc624f99aec4e980f6fde37), [`35f01df79`](https://github.com/withastro/astro/commit/35f01df797d23315f2bee2fc3fd795adb0559c58), [`3fdf509b2`](https://github.com/withastro/astro/commit/3fdf509b2731a9b2f972d89291e57cf78d62c769), [`78de801f2`](https://github.com/withastro/astro/commit/78de801f21fd4ca1653950027d953bf08614566b), [`59d6e569f`](https://github.com/withastro/astro/commit/59d6e569f63e175c97e82e94aa7974febfb76f7c), [`7723c4cc9`](https://github.com/withastro/astro/commit/7723c4cc93298c2e6530e55da7afda048f22cf81), [`fb5cd6b56`](https://github.com/withastro/astro/commit/fb5cd6b56dc27a71366ed5e1ab8bfe9b8f96bac5), [`631b9c410`](https://github.com/withastro/astro/commit/631b9c410d5d66fa384674027ba95d69ebb5063f)]: + - @astrojs/prism@3.0.0-beta.0 + - astro@3.0.0-beta.0 + - @astrojs/internal-helpers@0.2.0-beta.0 + +## 0.4.4 + +### Patch Changes + +- [#7597](https://github.com/withastro/astro/pull/7597) [`7461e82c8`](https://github.com/withastro/astro/commit/7461e82c81438df956861197536f9ceeaf63d6b3) Thanks [@alex-sherwin](https://github.com/alex-sherwin)! - 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 })], + }); + ``` + +- Updated dependencies [[`0f677c009`](https://github.com/withastro/astro/commit/0f677c009d102bc12232a966634136be58f34739), [`188eeddd4`](https://github.com/withastro/astro/commit/188eeddd47a61e04639670496924c37866180749)]: + - astro@2.9.3 + +## 0.4.3 + +### Patch Changes + +- [#7706](https://github.com/withastro/astro/pull/7706) [`4f6b5ae2b`](https://github.com/withastro/astro/commit/4f6b5ae2ba8eb162e03f25cbd600a905d434f529) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fix Markdoc integration not being able to import `emitESMImage` from Astro + +- Updated dependencies [[`72bbfac97`](https://github.com/withastro/astro/commit/72bbfac976c2965a523eea88ff0543e64d848d80), [`d401866f9`](https://github.com/withastro/astro/commit/d401866f93bfe25a50c171bc54b2b1ee0f483cc9), [`4f6b5ae2b`](https://github.com/withastro/astro/commit/4f6b5ae2ba8eb162e03f25cbd600a905d434f529), [`06c255716`](https://github.com/withastro/astro/commit/06c255716ae8e922fb9d4ffa5595cbb34146fff6)]: + - astro@2.8.5 + +## 0.4.2 + +### Patch Changes + +- [#7593](https://github.com/withastro/astro/pull/7593) [`c135633bf`](https://github.com/withastro/astro/commit/c135633bf6a84e751249920cba9009f0e394e29a) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Add a documentation link to the configuration error hint for those migration pre-v0.4.0 config to the latest version. + +- [#7599](https://github.com/withastro/astro/pull/7599) [`8df6a423c`](https://github.com/withastro/astro/commit/8df6a423c5088a68cc409b5415b09aff0c10a0f1) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix hyphens in Markdoc tag names causing build failures + +- Updated dependencies [[`904921cbe`](https://github.com/withastro/astro/commit/904921cbe44e168477c751774a2e01a6cc972a16), [`3669e2d27`](https://github.com/withastro/astro/commit/3669e2d2762bf8a4909be00ed212a6c5e847eedf), [`831dfd151`](https://github.com/withastro/astro/commit/831dfd1516c8b900ec4a0c151a40121655cdedc6)]: + - astro@2.8.1 + +## 0.4.1 + +### Patch Changes + +- [#7575](https://github.com/withastro/astro/pull/7575) [`30d04db98`](https://github.com/withastro/astro/commit/30d04db98107b40669e964c3ec4ac77dc2d65645) Thanks [@bluwy](https://github.com/bluwy)! - Handle internal access change + +- Updated dependencies [[`9e5fafa2b`](https://github.com/withastro/astro/commit/9e5fafa2b25b5128084c7072aa282642fcfbb14b), [`9e5fafa2b`](https://github.com/withastro/astro/commit/9e5fafa2b25b5128084c7072aa282642fcfbb14b), [`9e5fafa2b`](https://github.com/withastro/astro/commit/9e5fafa2b25b5128084c7072aa282642fcfbb14b), [`6e9c29579`](https://github.com/withastro/astro/commit/6e9c295799cb6524841adbcbec21ff628d8d19c8), [`9e5fafa2b`](https://github.com/withastro/astro/commit/9e5fafa2b25b5128084c7072aa282642fcfbb14b), [`9e5fafa2b`](https://github.com/withastro/astro/commit/9e5fafa2b25b5128084c7072aa282642fcfbb14b)]: + - astro@2.8.0 + +## 0.4.0 + +### Minor Changes + +- [#7468](https://github.com/withastro/astro/pull/7468) [`fb7af5511`](https://github.com/withastro/astro/commit/fb7af551148f5ca6c4f98a4e556c8948c5690919) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Updates the Markdoc config object for rendering Astro components as tags or nodes. Rather than importing components directly, Astro includes a new `component()` function to specify your component path. This unlocks using Astro components from npm packages and `.ts` files. + + ### Migration + + Update all component imports to instead import the new `component()` function and use it to render your Astro components: + + ```diff + // markdoc.config.mjs + import { + defineMarkdocConfig, + + component, + } from '@astrojs/markdoc/config'; + - import Aside from './src/components/Aside.astro'; + + export default defineMarkdocConfig({ + tags: { + aside: { + render: Aside, + + render: component('./src/components/Aside.astro'), + } + } + }); + ``` + +### Patch Changes + +- [#7467](https://github.com/withastro/astro/pull/7467) [`f6feff7a2`](https://github.com/withastro/astro/commit/f6feff7a2991fb94e11ee1b70ac606e4c053062b) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Restart the dev server whenever your markdoc config changes. + +- Updated dependencies [[`6dfd7081b`](https://github.com/withastro/astro/commit/6dfd7081b7a1532ab0fe3af8bcf079b10a5640a9), [`83016795e`](https://github.com/withastro/astro/commit/83016795e9e149bc64e2441d477cf8c65ef5a117), [`d3247851f`](https://github.com/withastro/astro/commit/d3247851f04e911c134cfedc22db17b7d61c53d9), [`a3928016c`](https://github.com/withastro/astro/commit/a3928016cc375842cf47e7a227835cd17e48a409), [`2726098bc`](https://github.com/withastro/astro/commit/2726098bc82f910edda4198b9fb94f2bfd048976), [`f4fea3b02`](https://github.com/withastro/astro/commit/f4fea3b02b0737053c7c7521a7d4dd235648918a)]: + - astro@2.7.2 + +## 0.3.3 + +### Patch Changes + +- [#7351](https://github.com/withastro/astro/pull/7351) [`a30f2f3de`](https://github.com/withastro/astro/commit/a30f2f3de440c39c88a4e0ed3f47064a6b5a54f7) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix cloudflare build errors for a bad "./config" entrypoint and "node:crypto" getting included unexpectedly. + +- [#7341](https://github.com/withastro/astro/pull/7341) [`491c2db42`](https://github.com/withastro/astro/commit/491c2db424434167327e780ad57b8f665498003d) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Improve error message for unsupported Zod transforms from the content config. + +- Updated dependencies [[`491c2db42`](https://github.com/withastro/astro/commit/491c2db424434167327e780ad57b8f665498003d), [`0a8d178c9`](https://github.com/withastro/astro/commit/0a8d178c90f033fbba40666c54bcfc58c53ac905)]: + - astro@2.6.3 + +## 0.3.2 + +### Patch Changes + +- [#7311](https://github.com/withastro/astro/pull/7311) [`a11b62ee1`](https://github.com/withastro/astro/commit/a11b62ee1f5d524b0ba942818525b623a6d6eb99) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix Markdoc type errors for `render` and `defineMarkdocConfig()` when using a TypeScript Markdoc config file. + +- [#7309](https://github.com/withastro/astro/pull/7309) [`2a4bb23b2`](https://github.com/withastro/astro/commit/2a4bb23b2f7f82b3fabdad4d64101fcc778acaa4) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix missing styles and scripts for components when using `document: { render: null }` in the Markdoc config. + +- Updated dependencies [[`8034edd9e`](https://github.com/withastro/astro/commit/8034edd9ecf805073395ba7f68f73cd5fc4d2c73)]: + - astro@2.6.1 + +## 0.3.1 + +### Patch Changes + +- [#7224](https://github.com/withastro/astro/pull/7224) [`563293c5d`](https://github.com/withastro/astro/commit/563293c5d67e2bf13b9c735581969a0341861b44) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Allow HTML comments `<!--like this-->` in Markdoc files. + +- [#7185](https://github.com/withastro/astro/pull/7185) [`339529fc8`](https://github.com/withastro/astro/commit/339529fc820bac2d514b63198ecf54a1d88c0917) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Bring back improved style and script handling across content collection files. This addresses bugs found in a previous release to `@astrojs/markdoc`. + +- Updated dependencies [[`6e27f2f6d`](https://github.com/withastro/astro/commit/6e27f2f6dbd52f980c487e875faf1b066f65cffd), [`96ae37eb0`](https://github.com/withastro/astro/commit/96ae37eb09f7406f40fba93e14b2a26ccd46640c), [`fea306936`](https://github.com/withastro/astro/commit/fea30693609cc517d8660972151f4d12a0dd4e82), [`5156c4f90`](https://github.com/withastro/astro/commit/5156c4f90e0922f62d25fa0c82bbefae39f4c2b6), [`9e7366567`](https://github.com/withastro/astro/commit/9e7366567e2b83d46a46db35e74ad508d1978039), [`339529fc8`](https://github.com/withastro/astro/commit/339529fc820bac2d514b63198ecf54a1d88c0917)]: + - astro@2.5.7 + +## 0.3.0 + +### Minor Changes + +- [#7244](https://github.com/withastro/astro/pull/7244) [`bef3a75db`](https://github.com/withastro/astro/commit/bef3a75dbc48d584daff9f7f3d5a8937b0356170) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Remove the auto-generated `$entry` variable for Markdoc entries. To access frontmatter as a variable, you can pass `entry.data` as a prop where you render your content: + + ```astro + --- + import { getEntry } from 'astro:content'; + + const entry = await getEntry('docs', 'why-markdoc'); + const { Content } = await entry.render(); + --- + + <Content frontmatter={entry.data} /> + ``` + +### Patch Changes + +- [#7187](https://github.com/withastro/astro/pull/7187) [`1efaef6be`](https://github.com/withastro/astro/commit/1efaef6be0265c68eac706623778e8ad23b33247) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Add support for syntax highlighting with Shiki. Apply to your Markdoc config using the `extends` property: + + ```js + // markdoc.config.mjs + import { defineMarkdocConfig } from '@astrojs/markdoc/config'; + import shiki from '@astrojs/markdoc/shiki'; + + export default defineMarkdocConfig({ + extends: [ + shiki({ + /** Shiki config options */ + }), + ], + }); + ``` + + Learn more in the [`@astrojs/markdoc` README.](https://docs.astro.build/en/guides/integrations-guide/markdoc/#syntax-highlighting) + +- [#7209](https://github.com/withastro/astro/pull/7209) [`16b836411`](https://github.com/withastro/astro/commit/16b836411980f18c58ca15712d92cec1b3c95670) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Add a built-in extension for syntax highlighting with Prism. Apply to your Markdoc config using the `extends` property: + + ```js + // markdoc.config.mjs + import { defineMarkdocConfig } from '@astrojs/markdoc/config'; + import prism from '@astrojs/markdoc/prism'; + + export default defineMarkdocConfig({ + extends: [prism()], + }); + ``` + + Learn more in the [`@astrojs/markdoc` README.](https://docs.astro.build/en/guides/integrations-guide/markdoc/#syntax-highlighting) + +- Updated dependencies [[`8b041bf57`](https://github.com/withastro/astro/commit/8b041bf57c76830c4070330270521e05d8e58474), [`6c7df28ab`](https://github.com/withastro/astro/commit/6c7df28ab34b756b8426443bf6976e24d4611a62), [`ee2aca80a`](https://github.com/withastro/astro/commit/ee2aca80a71afe843af943b11966fcf77f556cfb), [`7851f9258`](https://github.com/withastro/astro/commit/7851f9258fae2f54795470253df9ce4bcd5f9cb0), [`bef3a75db`](https://github.com/withastro/astro/commit/bef3a75dbc48d584daff9f7f3d5a8937b0356170), [`52af9ad18`](https://github.com/withastro/astro/commit/52af9ad18840ffa4e2996386c82cbe34d9fd076a), [`f5063d0a0`](https://github.com/withastro/astro/commit/f5063d0a01e3179da902fdc0a2b22f88cb3c95c7), [`cf621340b`](https://github.com/withastro/astro/commit/cf621340b00fda441f4ef43196c0363d09eae70c), [`2bda7fb0b`](https://github.com/withastro/astro/commit/2bda7fb0bce346f7725086980e1648e2636bbefb), [`af3c5a2e2`](https://github.com/withastro/astro/commit/af3c5a2e25bd3e7b2a3f7f08e41ee457093c8cb1), [`f2f18b440`](https://github.com/withastro/astro/commit/f2f18b44055c6334a39d6379de88fe41e518aa1e)]: + - astro@2.5.6 + +## 0.2.3 + +### Patch Changes + +- [#7178](https://github.com/withastro/astro/pull/7178) [`57e65d247`](https://github.com/withastro/astro/commit/57e65d247f67de61bcc3a585c2254feb61ed2e74) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix: revert Markdoc asset bleed changes. Production build issues were discovered that deserve a different fix. + +- Updated dependencies [[`904131aec`](https://github.com/withastro/astro/commit/904131aec3bacb2824ad60457a45772eba27b5ab), [`57e65d247`](https://github.com/withastro/astro/commit/57e65d247f67de61bcc3a585c2254feb61ed2e74)]: + - astro@2.5.5 + +## 0.2.2 + +### Patch Changes + +- [#6758](https://github.com/withastro/astro/pull/6758) [`f558a9e20`](https://github.com/withastro/astro/commit/f558a9e2056fc8f2e2d5814e74f199e398159fc4) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Improve style and script handling across content collection files. This addresses style bleed present in `@astrojs/markdoc` v0.1.0 + +- Updated dependencies [[`f558a9e20`](https://github.com/withastro/astro/commit/f558a9e2056fc8f2e2d5814e74f199e398159fc4), [`b41963b77`](https://github.com/withastro/astro/commit/b41963b775149b802eea9e12c5fe266bb9a02944)]: + - astro@2.5.3 + +## 0.2.1 + +### Patch Changes + +- [#7141](https://github.com/withastro/astro/pull/7141) [`a9e1cd7e5`](https://github.com/withastro/astro/commit/a9e1cd7e58794fe220539c2ed935c9eb96bab55a) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix inconsistent Markdoc heading IDs for documents with the same headings. + +- Updated dependencies [[`72f686a68`](https://github.com/withastro/astro/commit/72f686a68930de52f9a274c13c98acad59925b31), [`319a0a7a0`](https://github.com/withastro/astro/commit/319a0a7a0a6a950387c942b467746d590bb32fda), [`852d59a8d`](https://github.com/withastro/astro/commit/852d59a8d68e124f10852609e0f1619d5838ac76), [`530fb9ebe`](https://github.com/withastro/astro/commit/530fb9ebee77646921ec29d45d9b66484bdfb521), [`3257dd289`](https://github.com/withastro/astro/commit/3257dd28901c785a6a661211b98c5ef2cb3b9aa4)]: + - astro@2.5.1 + +## 0.2.0 + +### Minor Changes + +- [#6850](https://github.com/withastro/astro/pull/6850) [`c6d7ebefd`](https://github.com/withastro/astro/commit/c6d7ebefdd554a9ef29cfeb426ac55cab80d6473) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Content collections now support data formats including JSON and YAML. You can also create relationships, or references, between collections to pull information from one collection entry into another. Learn more on our [updated Content Collections docs](https://docs.astro.build/en/guides/content-collections/). + +- [#7095](https://github.com/withastro/astro/pull/7095) [`fb84622af`](https://github.com/withastro/astro/commit/fb84622af04f795de8d17f24192de105f70fe910) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Generate heading `id`s and populate the `headings` property for all Markdoc files + +### Patch Changes + +- [#7111](https://github.com/withastro/astro/pull/7111) [`6b4fcde37`](https://github.com/withastro/astro/commit/6b4fcde3760140733ad03a162dd0682004c106b2) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix: add `headings` to Markdoc `render()` return type. + +- [#7104](https://github.com/withastro/astro/pull/7104) [`826e02890`](https://github.com/withastro/astro/commit/826e0289005f645b902375b98d5549c6a95ccafa) Thanks [@bluwy](https://github.com/bluwy)! - Specify `"files"` field to only publish necessary files + +- Updated dependencies [[`4516d7b22`](https://github.com/withastro/astro/commit/4516d7b22c5979cde4537f196b53ae2826ba9561), [`e186ecc5e`](https://github.com/withastro/astro/commit/e186ecc5e292de8c6a2c441a2d588512c0813068), [`c6d7ebefd`](https://github.com/withastro/astro/commit/c6d7ebefdd554a9ef29cfeb426ac55cab80d6473), [`914c439bc`](https://github.com/withastro/astro/commit/914c439bccee9fec002c6d92beaa501c398e62ac), [`e9fc2c221`](https://github.com/withastro/astro/commit/e9fc2c2213036d47cd30a47a6cdad5633481a0f8), [`075eee08f`](https://github.com/withastro/astro/commit/075eee08f2e2b0baea008b97f3523f2cb937ee44), [`719002ca5`](https://github.com/withastro/astro/commit/719002ca5b128744fb4316d4a52c5dcd46a42759), [`fc52681ba`](https://github.com/withastro/astro/commit/fc52681ba2f8fe8bcd92eeedf3c6a52fd86a390e), [`fb84622af`](https://github.com/withastro/astro/commit/fb84622af04f795de8d17f24192de105f70fe910), [`cada10a46`](https://github.com/withastro/astro/commit/cada10a466f81f8edb0aa664f9cffdb6b5b8f307), [`cd410c5eb`](https://github.com/withastro/astro/commit/cd410c5eb71f825259279c27c4c39d0ad282c3f0), [`73ec6f6c1`](https://github.com/withastro/astro/commit/73ec6f6c16cadb71dafe9f664f0debde072c3173), [`410428672`](https://github.com/withastro/astro/commit/410428672ed97bba7ca0b3352c1a7ee564921462), [`763ff2d1e`](https://github.com/withastro/astro/commit/763ff2d1e44f54b899d7c65386f1b4b877c95737), [`c1669c001`](https://github.com/withastro/astro/commit/c1669c0011eecfe65a459d727848c18c189a54ca), [`3d525efc9`](https://github.com/withastro/astro/commit/3d525efc95cfb2deb5d9e04856d02965d66901c9)]: + - astro@2.5.0 + +## 0.1.3 + +### Patch Changes + +- [#7045](https://github.com/withastro/astro/pull/7045) [`3a9f72c7f`](https://github.com/withastro/astro/commit/3a9f72c7f30ed173438fd0a222a094e5997b917d) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Improve Markdoc validation errors with full message and file preview. + +- Updated dependencies [[`48395c815`](https://github.com/withastro/astro/commit/48395c81522f7527126699c4f185f7b4488a4b9a), [`630f8c8ef`](https://github.com/withastro/astro/commit/630f8c8ef68fedfa393899c13a072e50145895e8)]: + - astro@2.4.4 + +## 0.1.2 + +### Patch Changes + +- [#6932](https://github.com/withastro/astro/pull/6932) [`49514e4ce`](https://github.com/withastro/astro/commit/49514e4ce40fedb39bf7decd2c296258efbdafc7) Thanks [@bluwy](https://github.com/bluwy)! - Upgrade shiki to v0.14.1. This updates the shiki theme colors and adds the theme name to the `pre` tag, e.g. `<pre class="astro-code github-dark">`. + +- Updated dependencies [[`818252acd`](https://github.com/withastro/astro/commit/818252acda3c00499cea51ffa0f26d4c2ccd3a02), [`80e3d4d3d`](https://github.com/withastro/astro/commit/80e3d4d3d0f7719d8eae5435bba3805503057511), [`3326492b9`](https://github.com/withastro/astro/commit/3326492b94f76ed2b0154dd9b9a1a9eb883c1e31), [`cac4a321e`](https://github.com/withastro/astro/commit/cac4a321e814fb805eb0e3ced469e25261a50885), [`831b67cdb`](https://github.com/withastro/astro/commit/831b67cdb8250f93f66e3b171fab024652bf80f2), [`49514e4ce`](https://github.com/withastro/astro/commit/49514e4ce40fedb39bf7decd2c296258efbdafc7), [`0883fd487`](https://github.com/withastro/astro/commit/0883fd4875548a613df122f0b87a1ca8b7a7cf7d)]: + - astro@2.4.0 + +## 0.1.1 + +### Patch Changes + +- [#6723](https://github.com/withastro/astro/pull/6723) [`73fcc7627`](https://github.com/withastro/astro/commit/73fcc7627e27a001d3ed2f4d046999d91f1aef85) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix: when using `render: null` in your config, content is now rendered without a wrapper element. + +- Updated dependencies [[`489dd8d69`](https://github.com/withastro/astro/commit/489dd8d69cdd9d7c243cf8bec96051a914984b9c), [`a1a4f45b5`](https://github.com/withastro/astro/commit/a1a4f45b51a80215fa7598da83bd0d9c5acd20d2), [`a1108e037`](https://github.com/withastro/astro/commit/a1108e037115cdb67d03505286c7d3a4fc2a1ff5), [`8b88e4cf1`](https://github.com/withastro/astro/commit/8b88e4cf15c8bea7942b3985380164e0edf7250b), [`d54cbe413`](https://github.com/withastro/astro/commit/d54cbe41349e55f8544212ad9320705f07325920), [`4c347ab51`](https://github.com/withastro/astro/commit/4c347ab51e46f2319d614f8577fe502e3dc816e2), [`ff0430786`](https://github.com/withastro/astro/commit/ff043078630e678348ae4f4757b3015b3b862c16), [`2f2e572e9`](https://github.com/withastro/astro/commit/2f2e572e937fd25451bbc78a05d55b7caa1ca3ec), [`7116c021a`](https://github.com/withastro/astro/commit/7116c021a39eac15a6e1264dfbd11bef0f5d618a)]: + - astro@2.2.0 + +## 0.1.0 + +### Minor Changes + +- [#6653](https://github.com/withastro/astro/pull/6653) [`7c439868a`](https://github.com/withastro/astro/commit/7c439868a3bc7d466418da9af669966014f3d9fe) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Simplify Markdoc configuration with a new `markdoc.config.mjs` file. This lets you import Astro components directly to render as Markdoc tags and nodes, without the need for the previous `components` property. This new configuration also unlocks passing variables to your Markdoc from the `Content` component ([see the new docs](https://docs.astro.build/en/guides/integrations-guide/markdoc/#pass-markdoc-variables)). + + ## Migration + + Move any existing Markdoc config from your `astro.config` to a new `markdoc.config.mjs` file at the root of your project. This should be applied as a default export, with the optional `defineMarkdocConfig()` helper for autocomplete in your editor. + + This example configures an `aside` Markdoc tag. Note that components should be imported and applied to the `render` attribute _directly,_ instead of passing the name as a string: + + ```js + // markdoc.config.mjs + import { defineMarkdocConfig } from '@astrojs/markdoc/config'; + import Aside from './src/components/Aside.astro'; + + export default defineMarkdocConfig({ + tags: { + aside: { + render: Aside, + }, + }, + }); + ``` + + You should also remove the `components` prop from your `Content` components. Since components are imported into your config directly, this is no longer needed. + + ```diff + --- + - import Aside from '../components/Aside.astro'; + import { getEntryBySlug } from 'astro:content'; + + const entry = await getEntryBySlug('docs', 'why-markdoc'); + const { Content } = await entry.render(); + --- + + <Content + - components={{ Aside }} + /> + ``` + +### Patch Changes + +- Updated dependencies [[`1f783e320`](https://github.com/withastro/astro/commit/1f783e32075c20b13063599696644f5d47b75d8d), [`2e92e9aa9`](https://github.com/withastro/astro/commit/2e92e9aa976735c3ddb647152bb9c4850136e386), [`adecda7d6`](https://github.com/withastro/astro/commit/adecda7d6009793c5d20519a997e3b7afb08ad57), [`386336441`](https://github.com/withastro/astro/commit/386336441ad70017eea22db0683591126131db21), [`7c439868a`](https://github.com/withastro/astro/commit/7c439868a3bc7d466418da9af669966014f3d9fe), [`25cd3e574`](https://github.com/withastro/astro/commit/25cd3e574999c1c7294a089ad8c39df27ccdbf17), [`4bf87c64f`](https://github.com/withastro/astro/commit/4bf87c64ff7e9ca49e0f5c27e06bd49faaf60542), [`fc0ed9c53`](https://github.com/withastro/astro/commit/fc0ed9c53cd374860bbdb2503318a55ca09a2662)]: + - astro@2.1.8 + +## 0.0.5 + +### Patch Changes + +- [#6630](https://github.com/withastro/astro/pull/6630) [`cfcf2e2ff`](https://github.com/withastro/astro/commit/cfcf2e2ffdaa68ace5c84329c05b83559a29d638) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Support automatic image optimization for Markdoc images when using `experimental.assets`. You can [follow our Assets guide](https://docs.astro.build/en/guides/assets/#enabling-assets-in-your-project) to enable this feature in your project. Then, start using relative or aliased image sources in your Markdoc files for automatic optimization: + + ```md + <!--Relative paths--> + +  + + <!--Or configured aliases--> + +  + ``` + +- Updated dependencies [[`b7194103e`](https://github.com/withastro/astro/commit/b7194103e39267bf59dcd6ba00f522e424219d16), [`cfcf2e2ff`](https://github.com/withastro/astro/commit/cfcf2e2ffdaa68ace5c84329c05b83559a29d638), [`45da39a86`](https://github.com/withastro/astro/commit/45da39a8642d64eb318840b18dfc2b5ccc6561bc), [`7daef9a29`](https://github.com/withastro/astro/commit/7daef9a2993b5d457f3d243a1ebfd1dd383b3327)]: + - astro@2.1.7 + +## 0.0.4 + +### Patch Changes + +- [#6588](https://github.com/withastro/astro/pull/6588) [`f42f47dc6`](https://github.com/withastro/astro/commit/f42f47dc6a91cdb6534dab0ecbf9e8e85f00ba40) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Allow access to content collection entry information (including parsed frontmatter and the entry slug) from your Markdoc using the `$entry` variable: + + ```mdx + --- + title: Hello Markdoc! + --- + + # {% $entry.data.title %} + ``` + +- [#6607](https://github.com/withastro/astro/pull/6607) [`86273b588`](https://github.com/withastro/astro/commit/86273b5881cc61ebee11d40280b4c0aba8f4bb2e) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Fix: Update Markdoc renderer internals to remove unneeded dependencies + +- [#6622](https://github.com/withastro/astro/pull/6622) [`b37b86540`](https://github.com/withastro/astro/commit/b37b865400e77e92878d7e150244acce47e933c6) Thanks [@paulrudy](https://github.com/paulrudy)! - Fix README instructions for installing Markdoc manually. + +## 0.0.3 + +### Patch Changes + +- [#6552](https://github.com/withastro/astro/pull/6552) [`392ba3e4d`](https://github.com/withastro/astro/commit/392ba3e4d55f73ce9194bd94a2243f1aa62af079) Thanks [@bluwy](https://github.com/bluwy)! - Fix integration return type + +## 0.0.2 + +### Patch Changes + +- [#6494](https://github.com/withastro/astro/pull/6494) [`a13e9d7e3`](https://github.com/withastro/astro/commit/a13e9d7e33baccf51e7d4815f99b481ad174bc57) Thanks [@Yan-Thomas](https://github.com/Yan-Thomas)! - Consistency improvements to several package descriptions + +## 0.0.1 + +### Patch Changes + +- [#6209](https://github.com/withastro/astro/pull/6209) [`fec583909`](https://github.com/withastro/astro/commit/fec583909ab62829dc0c1600e2387979365f2b94) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Introduce the (experimental) `@astrojs/markdoc` integration. This unlocks Markdoc inside your Content Collections, bringing support for Astro and UI components in your content. This also improves Astro core internals to make Content Collections extensible to more file types in the future. + + You can install this integration using the `astro add` command: + + ``` + astro add markdoc + ``` + + [Read the `@astrojs/markdoc` documentation](https://docs.astro.build/en/guides/integrations-guide/markdoc/) for usage instructions, and browse the [new `with-markdoc` starter](https://astro.new/with-markdoc) to try for yourself. diff --git a/packages/integrations/markdoc/README.md b/packages/integrations/markdoc/README.md new file mode 100644 index 000000000..58582b5c7 --- /dev/null +++ b/packages/integrations/markdoc/README.md @@ -0,0 +1,38 @@ +# @astrojs/markdoc (experimental) 📝 + +This **[Astro integration][astro-integration]** enables the usage of [Markdoc](https://markdoc.dev/) to create components, pages, and content collection entries. + +## Documentation + +Read the [`@astrojs/markdoc` docs][docs] + +## Support + +- Get help in the [Astro Discord][discord]. Post questions in our `#support` forum, or visit our dedicated `#dev` channel to discuss current development and more! + +- Check our [Astro Integration Documentation][astro-integration] for more on integrations. + +- Submit bug reports and feature requests as [GitHub issues][issues]. + +## Contributing + +This package is maintained by Astro's Core team. You're welcome to submit an issue or PR! These links will help you get started: + +- [Contributor Manual][contributing] +- [Code of Conduct][coc] +- [Community Guide][community] + +## License + +MIT + +Copyright (c) 2023–present [Astro][astro] + +[astro]: https://astro.build/ +[docs]: https://docs.astro.build/en/guides/integrations-guide/markdoc/ +[contributing]: https://github.com/withastro/astro/blob/main/CONTRIBUTING.md +[coc]: https://github.com/withastro/.github/blob/main/CODE_OF_CONDUCT.md +[community]: https://github.com/withastro/.github/blob/main/COMMUNITY_GUIDE.md +[discord]: https://astro.build/chat/ +[issues]: https://github.com/withastro/astro/issues +[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/ diff --git a/packages/integrations/markdoc/components/Renderer.astro b/packages/integrations/markdoc/components/Renderer.astro new file mode 100644 index 000000000..270604091 --- /dev/null +++ b/packages/integrations/markdoc/components/Renderer.astro @@ -0,0 +1,23 @@ +--- +//! astro-head-inject +import type { Config, RenderableTreeNodes } from '@markdoc/markdoc'; +import Markdoc from '@markdoc/markdoc'; +import { ComponentNode, createTreeNode } from './TreeNode.js'; + +type Props = { + config: Config; + stringifiedAst: string; +}; + +const { stringifiedAst, config } = Astro.props as Props; + +const ast = Markdoc.Ast.fromJSON(stringifiedAst); +// The AST may be an array, and `transform` has overloads for arrays and non-array cases, +// However TypeScript seems to struggle to combine both overloads into a single signature. +// Also, `transform` returns a promise here but the types don't reflect that. +// @ts-expect-error +const content = (await Markdoc.transform(ast, config)) as RenderableTreeNodes; +const treeNode = await createTreeNode(content); +--- + +<ComponentNode treeNode={treeNode} /> diff --git a/packages/integrations/markdoc/components/TreeNode.ts b/packages/integrations/markdoc/components/TreeNode.ts new file mode 100644 index 000000000..f1fbb4618 --- /dev/null +++ b/packages/integrations/markdoc/components/TreeNode.ts @@ -0,0 +1,178 @@ +import type { RenderableTreeNodes } from '@markdoc/markdoc'; +import Markdoc from '@markdoc/markdoc'; +import type { AstroInstance, SSRResult } from 'astro'; +import type { HTMLString } from 'astro/runtime/server/index.js'; +import { + createComponent, + createHeadAndContent, + isHTMLString, + render, + renderComponent, + renderScriptElement, + renderTemplate, + renderUniqueStylesheet, + unescapeHTML, +} from 'astro/runtime/server/index.js'; + +type TreeNode = + // Markdoc `if` tag often returns an array of nodes in the AST, which gets translated + // here as an array of `TreeNode`s, which we'll render all without a wrapper. + | TreeNode[] + | { + type: 'text'; + content: string | HTMLString; + } + | { + type: 'component'; + component: AstroInstance['default']; + collectedLinks?: string[]; + collectedStyles?: string[]; + collectedScripts?: string[]; + props: Record<string, any>; + children: TreeNode[]; + } + | { + type: 'element'; + tag: string; + attributes: Record<string, any>; + children: TreeNode[]; + }; + +function renderTreeNodeToFactoryResult(result: SSRResult, treeNode: TreeNode) { + if (Array.isArray(treeNode)) { + return Promise.all( + treeNode.map((node) => + renderComponent(result, 'ComponentNode', ComponentNode, { treeNode: node }), + ), + ); + } + + if (treeNode.type === 'text') return render`${treeNode.content}`; + + const slots = { + default: () => + render`${treeNode.children.map((child) => + renderComponent(result, 'ComponentNode', ComponentNode, { treeNode: child }), + )}`, + }; + if (treeNode.type === 'component') { + let styles = '', + links = '', + scripts = ''; + if (Array.isArray(treeNode.collectedStyles)) { + styles = treeNode.collectedStyles + .map((style: any) => + renderUniqueStylesheet(result, { + type: 'inline', + content: style, + }), + ) + .join(''); + } + if (Array.isArray(treeNode.collectedLinks)) { + links = treeNode.collectedLinks + .map((link: any) => { + return renderUniqueStylesheet(result, { + type: 'external', + src: link[0] === '/' ? link : '/' + link, + }); + }) + .join(''); + } + if (Array.isArray(treeNode.collectedScripts)) { + scripts = treeNode.collectedScripts + .map((script: any) => renderScriptElement(script)) + .join(''); + } + + const head = unescapeHTML(styles + links + scripts); + + let headAndContent = createHeadAndContent( + head, + renderTemplate`${renderComponent( + result, + treeNode.component.name, + treeNode.component, + treeNode.props, + slots, + )}`, + ); + + // Let the runtime know that this component is being used. + // @ts-expect-error Astro only uses `init()` so specify it only (plus `_metadata` is internal) + result._metadata.propagators.add({ + init() { + return headAndContent; + }, + }); + + return headAndContent; + } + return renderComponent(result, treeNode.tag, treeNode.tag, treeNode.attributes, slots); +} + +export const ComponentNode = createComponent({ + factory(result: SSRResult, { treeNode }: { treeNode: TreeNode | TreeNode[] }) { + return renderTreeNodeToFactoryResult(result, treeNode); + }, + propagation: 'self', +}); + +export async function createTreeNode(node: RenderableTreeNodes): Promise<TreeNode> { + if (Array.isArray(node)) { + return Promise.all(node.map((child) => createTreeNode(child))); + } else if (isHTMLString(node)) { + return { type: 'text', content: node as HTMLString }; + } else if (typeof node === 'string' || typeof node === 'number') { + return { type: 'text', content: String(node) }; + } else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) { + return { type: 'text', content: '' }; + } + + const children = await Promise.all(node.children.map((child) => createTreeNode(child))); + + if (typeof node.name === 'function') { + const component = node.name; + const props = node.attributes; + + return { + type: 'component', + component, + props, + children, + }; + } else if (isPropagatedAssetsModule(node.name)) { + const { collectedStyles, collectedLinks, collectedScripts } = node.name; + const component = (await node.name.getMod()).default; + const props = node.attributes; + + return { + type: 'component', + component, + collectedStyles, + collectedLinks, + collectedScripts, + props, + children, + }; + } else { + return { + type: 'element', + tag: node.name, + attributes: node.attributes, + children, + }; + } +} + +type PropagatedAssetsModule = { + __astroPropagation: true; + getMod: () => Promise<AstroInstance>; + collectedStyles: string[]; + collectedLinks: string[]; + collectedScripts: string[]; +}; + +function isPropagatedAssetsModule(module: any): module is PropagatedAssetsModule { + return typeof module === 'object' && module != null && '__astroPropagation' in module; +} diff --git a/packages/integrations/markdoc/components/index.ts b/packages/integrations/markdoc/components/index.ts new file mode 100644 index 000000000..dc81899f1 --- /dev/null +++ b/packages/integrations/markdoc/components/index.ts @@ -0,0 +1,2 @@ +// @ts-expect-error +export { default as Renderer } from './Renderer.astro'; diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json new file mode 100644 index 000000000..3e4ad91e5 --- /dev/null +++ b/packages/integrations/markdoc/package.json @@ -0,0 +1,90 @@ +{ + "name": "@astrojs/markdoc", + "description": "Add support for Markdoc in your Astro site", + "version": "0.15.0", + "type": "module", + "types": "./dist/index.d.ts", + "author": "withastro", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/withastro/astro.git", + "directory": "packages/integrations/markdoc" + }, + "keywords": [ + "astro-integration", + "astro-component", + "markdoc" + ], + "bugs": "https://github.com/withastro/astro/issues", + "homepage": "https://docs.astro.build/en/guides/integrations-guide/markdoc/", + "exports": { + "./prism": { + "types": "./dist/extensions/prism.d.ts", + "default": "./dist/extensions/prism.js" + }, + "./shiki": { + "types": "./dist/extensions/shiki.d.ts", + "default": "./dist/extensions/shiki.js" + }, + "./config": { + "types": "./dist/config.d.ts", + "default": "./dist/config.js" + }, + ".": "./dist/index.js", + "./components": "./components/index.ts", + "./runtime": "./dist/runtime.js", + "./runtime-assets-config": "./dist/runtime-assets-config.js", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "config": [ + "./dist/config.d.ts" + ], + "prism": [ + "./dist/extensions/prism.d.ts" + ], + "shiki": [ + "./dist/extensions/shiki.d.ts" + ] + } + }, + "files": [ + "components", + "dist", + "template" + ], + "scripts": { + "build": "astro-scripts build \"src/**/*.ts\" && tsc", + "build:ci": "astro-scripts build \"src/**/*.ts\"", + "dev": "astro-scripts dev \"src/**/*.ts\"", + "test": "astro-scripts test --timeout 60000 \"test/**/*.test.js\"" + }, + "dependencies": { + "@astrojs/internal-helpers": "workspace:*", + "@astrojs/markdown-remark": "workspace:*", + "@astrojs/prism": "workspace:*", + "@markdoc/markdoc": "^0.5.1", + "esbuild": "^0.25.0", + "github-slugger": "^2.0.0", + "htmlparser2": "^10.0.0" + }, + "peerDependencies": { + "astro": "^5.0.0" + }, + "devDependencies": { + "@types/markdown-it": "^14.1.2", + "astro": "workspace:*", + "astro-scripts": "workspace:*", + "devalue": "^5.1.1", + "linkedom": "^0.18.9", + "vite": "^6.3.4" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + }, + "publishConfig": { + "provenance": true + } +} diff --git a/packages/integrations/markdoc/src/config.ts b/packages/integrations/markdoc/src/config.ts new file mode 100644 index 000000000..c62bfebab --- /dev/null +++ b/packages/integrations/markdoc/src/config.ts @@ -0,0 +1,53 @@ +import { isRelativePath } from '@astrojs/internal-helpers/path'; +import type { + Config, + ConfigType as MarkdocConfig, + MaybePromise, + NodeType, + Schema, +} from '@markdoc/markdoc'; +import _Markdoc from '@markdoc/markdoc'; +import type { AstroInstance } from 'astro'; +import { heading } from './heading-ids.js'; +import { componentConfigSymbol } from './utils.js'; + +export type Render = ComponentConfig | AstroInstance['default'] | string; +export type ComponentConfig = { + type: 'package' | 'local'; + path: string; + namedExport?: string; + [componentConfigSymbol]: true; +}; + +export type AstroMarkdocConfig<C extends Record<string, any> = Record<string, any>> = Omit< + MarkdocConfig, + 'tags' | 'nodes' +> & + Partial<{ + tags: Record<string, Schema<Config, Render>>; + nodes: Partial<Record<NodeType, Schema<Config, Render>>>; + ctx: C; + extends: MaybePromise<ResolvedAstroMarkdocConfig>[]; + }>; + +export type ResolvedAstroMarkdocConfig = Omit<AstroMarkdocConfig, 'extends'>; + +export const Markdoc = _Markdoc; +export const nodes = { ...Markdoc.nodes, heading }; + +export function defineMarkdocConfig(config: AstroMarkdocConfig): AstroMarkdocConfig { + return config; +} + +export function component(pathnameOrPkgName: string, namedExport?: string): ComponentConfig { + return { + type: isNpmPackageName(pathnameOrPkgName) ? 'package' : 'local', + path: pathnameOrPkgName, + namedExport, + [componentConfigSymbol]: true, + }; +} + +function isNpmPackageName(pathname: string) { + return !isRelativePath(pathname) && !pathname.startsWith('/'); +} diff --git a/packages/integrations/markdoc/src/content-entry-type.ts b/packages/integrations/markdoc/src/content-entry-type.ts new file mode 100644 index 000000000..5f2c5c155 --- /dev/null +++ b/packages/integrations/markdoc/src/content-entry-type.ts @@ -0,0 +1,429 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { parseFrontmatter } from '@astrojs/markdown-remark'; +import type { Config as MarkdocConfig, Node } from '@markdoc/markdoc'; +import Markdoc from '@markdoc/markdoc'; +import type { AstroConfig, ContentEntryType } from 'astro'; +import { emitESMImage } from 'astro/assets/utils'; +import type { Rollup, ErrorPayload as ViteErrorPayload } from 'vite'; +import type { ComponentConfig } from './config.js'; +import { htmlTokenTransform } from './html/transform/html-token-transform.js'; +import type { MarkdocConfigResult } from './load-config.js'; +import type { MarkdocIntegrationOptions } from './options.js'; +import { setupConfig } from './runtime.js'; +import { getMarkdocTokenizer } from './tokenizer.js'; +import { MarkdocError, isComponentConfig, isValidUrl, prependForwardSlash } from './utils.js'; + +export async function getContentEntryType({ + markdocConfigResult, + astroConfig, + options, +}: { + astroConfig: AstroConfig; + markdocConfigResult?: MarkdocConfigResult; + options?: MarkdocIntegrationOptions; +}): Promise<ContentEntryType> { + return { + extensions: ['.mdoc'], + getEntryInfo({ fileUrl, contents }) { + const parsed = safeParseFrontmatter(contents, fileURLToPath(fileUrl)); + return { + data: parsed.frontmatter, + body: parsed.content.trim(), + slug: parsed.frontmatter.slug, + rawData: parsed.rawFrontmatter, + }; + }, + handlePropagation: true, + async getRenderModule({ contents, fileUrl, viteId }) { + const parsed = safeParseFrontmatter(contents, fileURLToPath(fileUrl)); + const tokenizer = getMarkdocTokenizer(options); + let tokens = tokenizer.tokenize(parsed.content); + + if (options?.allowHTML) { + tokens = htmlTokenTransform(tokenizer, tokens); + } + + const ast = Markdoc.parse(tokens); + const userMarkdocConfig = markdocConfigResult?.config ?? {}; + const markdocConfigUrl = markdocConfigResult?.fileUrl; + const pluginContext = this; + const markdocConfig = await setupConfig( + userMarkdocConfig, + options, + astroConfig.experimental.headingIdCompat, + ); + const filePath = fileURLToPath(fileUrl); + raiseValidationErrors({ + ast, + /* Raised generics issue with Markdoc core https://github.com/markdoc/markdoc/discussions/400 */ + markdocConfig: markdocConfig as MarkdocConfig, + viteId, + astroConfig, + filePath, + }); + await resolvePartials({ + ast, + markdocConfig: markdocConfig as MarkdocConfig, + fileUrl, + allowHTML: options?.allowHTML, + tokenizer, + pluginContext, + root: astroConfig.root, + raisePartialValidationErrors: (partialAst, partialPath) => { + raiseValidationErrors({ + ast: partialAst, + markdocConfig: markdocConfig as MarkdocConfig, + viteId, + astroConfig, + filePath: partialPath, + }); + }, + }); + + const usedTags = getUsedTags(ast); + + let componentConfigByTagMap: Record<string, ComponentConfig> = {}; + // Only include component imports for tags used in the document. + // Avoids style and script bleed. + for (const tag of usedTags) { + const render = markdocConfig.tags?.[tag]?.render; + if (isComponentConfig(render)) { + componentConfigByTagMap[tag] = render; + } + } + let componentConfigByNodeMap: Record<string, ComponentConfig> = {}; + for (const [nodeType, schema] of Object.entries(markdocConfig.nodes ?? {})) { + const render = schema?.render; + if (isComponentConfig(render)) { + componentConfigByNodeMap[nodeType] = render; + } + } + + await emitOptimizedImages(ast.children, { + astroConfig, + pluginContext, + filePath, + }); + + const res = `import { Renderer } from '@astrojs/markdoc/components'; +import { createGetHeadings, createContentComponent } from '@astrojs/markdoc/runtime'; +${ + markdocConfigUrl + ? `import markdocConfig from ${JSON.stringify(fileURLToPath(markdocConfigUrl))};` + : 'const markdocConfig = {};' +} + +import { assetsConfig } from '@astrojs/markdoc/runtime-assets-config'; +markdocConfig.nodes = { ...assetsConfig.nodes, ...markdocConfig.nodes }; + +${getStringifiedImports(componentConfigByTagMap, 'Tag', astroConfig.root)} +${getStringifiedImports(componentConfigByNodeMap, 'Node', astroConfig.root)} +const experimentalHeadingIdCompat = ${JSON.stringify(astroConfig.experimental.headingIdCompat || false)} + +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, options, experimentalHeadingIdCompat); +export const Content = createContentComponent( + Renderer, + stringifiedAst, + markdocConfig, + options, + tagComponentMap, + nodeComponentMap, + experimentalHeadingIdCompat, +)`; + return { code: res }; + }, + contentModuleTypes: await fs.promises.readFile( + new URL('../template/content-module-types.d.ts', import.meta.url), + 'utf-8', + ), + }; +} + +/** + * Recursively resolve partial tags to their content. + * Note: Mutates the `ast` object directly. + */ +async function resolvePartials({ + ast, + fileUrl, + root, + tokenizer, + allowHTML, + markdocConfig, + pluginContext, + raisePartialValidationErrors, +}: { + ast: Node; + fileUrl: URL; + root: URL; + tokenizer: any; + allowHTML?: boolean; + markdocConfig: MarkdocConfig; + pluginContext: Rollup.PluginContext; + raisePartialValidationErrors: (ast: Node, filePath: string) => void; +}) { + const relativePartialPath = path.relative(fileURLToPath(root), fileURLToPath(fileUrl)); + for (const node of ast.walk()) { + if (node.type === 'tag' && node.tag === 'partial') { + const { file } = node.attributes; + if (!file) { + throw new MarkdocError({ + // Should be caught by Markdoc validation step. + message: `(Uncaught error) Partial tag requires a 'file' attribute`, + }); + } + + if (markdocConfig.partials?.[file]) continue; + + let partialPath: string; + let partialContents: string; + try { + const resolved = await pluginContext.resolve(file, fileURLToPath(fileUrl)); + let partialId = resolved?.id; + if (!partialId) { + const attemptResolveAsRelative = await pluginContext.resolve( + './' + file, + fileURLToPath(fileUrl), + ); + if (!attemptResolveAsRelative?.id) throw new Error(); + partialId = attemptResolveAsRelative.id; + } + + partialPath = fileURLToPath(new URL(prependForwardSlash(partialId), 'file://')); + partialContents = await fs.promises.readFile(partialPath, 'utf-8'); + } catch { + throw new MarkdocError({ + message: [ + `**${String(relativePartialPath)}** contains invalid content:`, + `Could not read partial file \`${file}\`. Does the file exist?`, + ].join('\n'), + }); + } + if (pluginContext.meta.watchMode) pluginContext.addWatchFile(partialPath); + let partialTokens = tokenizer.tokenize(partialContents); + if (allowHTML) { + partialTokens = htmlTokenTransform(tokenizer, partialTokens); + } + const partialAst = Markdoc.parse(partialTokens); + raisePartialValidationErrors(partialAst, partialPath); + await resolvePartials({ + ast: partialAst, + root, + fileUrl: pathToFileURL(partialPath), + tokenizer, + allowHTML, + markdocConfig, + pluginContext, + raisePartialValidationErrors, + }); + + Object.assign(node, partialAst); + } + } +} + +function raiseValidationErrors({ + ast, + markdocConfig, + viteId, + astroConfig, + filePath, +}: { + ast: Node; + markdocConfig: MarkdocConfig; + viteId: string; + astroConfig: AstroConfig; + filePath: string; +}) { + const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => { + return ( + (e.error.level === 'error' || e.error.level === 'critical') && + // Ignore `variable-undefined` errors. + // Variables can be configured at runtime, + // so we cannot validate them at build time. + e.error.id !== 'variable-undefined' && + // Ignore missing partial errors. + // We will resolve these in `resolvePartials`. + !(e.error.id === 'attribute-value-invalid' && /^Partial .+ not found/.test(e.error.message)) + ); + }); + + if (validationErrors.length) { + const rootRelativePath = path.relative(fileURLToPath(astroConfig.root), filePath); + throw new MarkdocError({ + message: [ + `**${String(rootRelativePath)}** contains invalid content:`, + ...validationErrors.map((e) => `- ${e.error.message}`), + ].join('\n'), + location: { + // Error overlay does not support multi-line or ranges. + // Just point to the first line. + line: validationErrors[0].lines[0], + file: viteId, + }, + }); + } +} + +function getUsedTags(markdocAst: Node) { + const tags = new Set<string>(); + const validationErrors = Markdoc.validate(markdocAst); + // Hack: run the validator with an empty config and look for 'tag-undefined'. + // This is our signal that a tag is being used! + for (const { error } of validationErrors) { + if (error.id === 'tag-undefined') { + const [, tagName] = /Undefined tag: '(.*)'/.exec(error.message) ?? []; + tags.add(tagName); + } + } + return tags; +} + +/** + * Emits optimized images, and appends the generated `src` to each AST node + * via the `__optimizedSrc` attribute. + */ +async function emitOptimizedImages( + nodeChildren: Node[], + ctx: { + pluginContext: Rollup.PluginContext; + filePath: string; + astroConfig: AstroConfig; + }, +) { + for (const node of nodeChildren) { + let isComponent = node.type === 'tag' && node.tag === 'image'; + // Support either a ![]() or {% image %} syntax, and handle the `src` attribute accordingly. + if ((node.type === 'image' || isComponent) && typeof node.attributes.src === 'string') { + let attributeName = isComponent ? 'src' : '__optimizedSrc'; + + // If the image isn't an URL or a link to public, try to resolve it. + if (shouldOptimizeImage(node.attributes.src)) { + // Attempt to resolve source with Vite. + // This handles relative paths and configured aliases + const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath); + + if (resolved?.id && fs.existsSync(new URL(prependForwardSlash(resolved.id), 'file://'))) { + const src = await emitESMImage( + resolved.id, + ctx.pluginContext.meta.watchMode, + // FUTURE: Remove in this in v6 + resolved.id.endsWith('.svg'), + ctx.pluginContext.emitFile, + ); + + const fsPath = resolved.id; + + if (src) { + // We cannot track images in Markdoc, Markdoc rendering always strips out the proxy. As such, we'll always + // assume that the image is referenced elsewhere, to be on safer side. + if (ctx.astroConfig.output === 'static') { + if (globalThis.astroAsset.referencedImages) + globalThis.astroAsset.referencedImages.add(fsPath); + } + + node.attributes[attributeName] = { ...src, fsPath }; + } + } else { + throw new MarkdocError({ + message: `Could not resolve image ${JSON.stringify( + node.attributes.src, + )} from ${JSON.stringify(ctx.filePath)}. Does the file exist?`, + }); + } + } else if (isComponent) { + // If the user is using the {% image %} tag, always pass the `src` attribute as `__optimizedSrc`, even if it's an external URL or absolute path. + // That way, the component can decide whether to optimize it or not. + node.attributes[attributeName] = node.attributes.src; + } + } + await emitOptimizedImages(node.children, ctx); + } +} + +function shouldOptimizeImage(src: string) { + // Optimize anything that is NOT external or an absolute path to `public/` + return !isValidUrl(src) && !src.startsWith('/'); +} + +/** + * Get stringified import statements for configured tags or nodes. + * `componentNamePrefix` is appended to the import name for namespacing. + * + * Example output: `import Tagaside from '/Users/.../src/components/Aside.astro';` + */ +function getStringifiedImports( + componentConfigMap: Record<string, ComponentConfig>, + componentNamePrefix: string, + root: URL, +) { + let stringifiedComponentImports = ''; + for (const [key, config] of Object.entries(componentConfigMap)) { + const importName = config.namedExport + ? `{ ${config.namedExport} as ${componentNamePrefix + toImportName(key)} }` + : componentNamePrefix + toImportName(key); + const resolvedPath = + config.type === 'local' ? fileURLToPath(new URL(config.path, root)) : config.path; + + stringifiedComponentImports += `import ${importName} from ${JSON.stringify(resolvedPath)};\n`; + } + return stringifiedComponentImports; +} + +function toImportName(unsafeName: string) { + // TODO: more checks that name is a safe JS variable name + return unsafeName.replace('-', '_'); +} + +/** + * Get a stringified map from tag / node name to component import name. + * This uses the same `componentNamePrefix` used by `getStringifiedImports()`. + * + * Example output: `{ aside: Tagaside, heading: Tagheading }` + */ +function getStringifiedMap( + componentConfigMap: Record<string, ComponentConfig>, + componentNamePrefix: string, +) { + let stringifiedComponentMap = '{'; + for (const key in componentConfigMap) { + stringifiedComponentMap += `${JSON.stringify(key)}: ${ + componentNamePrefix + toImportName(key) + },\n`; + } + stringifiedComponentMap += '}'; + return stringifiedComponentMap; +} + +/** + * Match YAML exception handling from Astro core errors + * @see 'astro/src/core/errors.ts' + */ +function safeParseFrontmatter(fileContents: string, filePath: string) { + try { + // empty with lines to preserve sourcemap location, but not `empty-with-spaces` + // because markdoc struggles with spaces + return parseFrontmatter(fileContents, { frontmatter: 'empty-with-lines' }); + } catch (e: any) { + if (e.name === 'YAMLException') { + const err: Error & ViteErrorPayload['err'] = e; + err.id = filePath; + err.loc = { file: e.id, line: e.mark.line + 1, column: e.mark.column }; + err.message = e.reason; + throw err; + } else { + throw e; + } + } +} diff --git a/packages/integrations/markdoc/src/extensions/prism.ts b/packages/integrations/markdoc/src/extensions/prism.ts new file mode 100644 index 000000000..721564871 --- /dev/null +++ b/packages/integrations/markdoc/src/extensions/prism.ts @@ -0,0 +1,21 @@ +import { runHighlighterWithAstro } from '@astrojs/prism/dist/highlighter'; +import { unescapeHTML } from 'astro/runtime/server/index.js'; +import { type AstroMarkdocConfig, Markdoc } from '../config.js'; + +export default function prism(): AstroMarkdocConfig { + return { + nodes: { + fence: { + attributes: Markdoc.nodes.fence.attributes!, + transform({ attributes: { language, content } }) { + const { html, classLanguage } = runHighlighterWithAstro(language, content); + + // Use `unescapeHTML` to return `HTMLString` for Astro renderer to inline as HTML + return unescapeHTML( + `<pre class="${classLanguage}"><code class="${classLanguage}">${html}</code></pre>`, + ) as any; + }, + }, + }, + }; +} diff --git a/packages/integrations/markdoc/src/extensions/shiki.ts b/packages/integrations/markdoc/src/extensions/shiki.ts new file mode 100644 index 000000000..1102242fd --- /dev/null +++ b/packages/integrations/markdoc/src/extensions/shiki.ts @@ -0,0 +1,35 @@ +import { createShikiHighlighter } from '@astrojs/markdown-remark'; +import Markdoc from '@markdoc/markdoc'; +import type { ShikiConfig } from 'astro'; +import { unescapeHTML } from 'astro/runtime/server/index.js'; +import type { AstroMarkdocConfig } from '../config.js'; + +export default async function shiki(config?: ShikiConfig): Promise<AstroMarkdocConfig> { + const highlighter = await createShikiHighlighter({ + langs: config?.langs, + theme: config?.theme, + themes: config?.themes, + }); + + return { + nodes: { + fence: { + attributes: Markdoc.nodes.fence.attributes!, + async transform({ attributes }) { + // NOTE: The `meta` from fence code, e.g. ```js {1,3-4}, isn't quite supported by Markdoc. + // Only the `js` part is parsed as `attributes.language` and the rest is ignored. This means + // some Shiki transformers may not work correctly as it relies on the `meta`. + const lang = typeof attributes.language === 'string' ? attributes.language : 'plaintext'; + const html = await highlighter.codeToHtml(attributes.content, lang, { + wrap: config?.wrap, + defaultColor: config?.defaultColor, + transformers: config?.transformers, + }); + + // Use `unescapeHTML` to return `HTMLString` for Astro renderer to inline as HTML + return unescapeHTML(html) as any; + }, + }, + }, + }; +} diff --git a/packages/integrations/markdoc/src/heading-ids.ts b/packages/integrations/markdoc/src/heading-ids.ts new file mode 100644 index 000000000..7242e0e16 --- /dev/null +++ b/packages/integrations/markdoc/src/heading-ids.ts @@ -0,0 +1,86 @@ +import Markdoc, { + type Config as MarkdocConfig, + type RenderableTreeNode, + type Schema, +} from '@markdoc/markdoc'; +import Slugger from 'github-slugger'; +import { getTextContent } from './runtime.js'; +import { MarkdocError } from './utils.js'; + +function getSlug( + attributes: Record<string, any>, + children: RenderableTreeNode[], + headingSlugger: Slugger, + experimentalHeadingIdCompat: boolean, +): string { + if (attributes.id && typeof attributes.id === 'string') { + return attributes.id; + } + const textContent = attributes.content ?? getTextContent(children); + let slug = headingSlugger.slug(textContent); + + if (!experimentalHeadingIdCompat) { + if (slug.endsWith('-')) slug = slug.slice(0, -1); + } + return slug; +} + +type HeadingIdConfig = MarkdocConfig & { + ctx: { headingSlugger: Slugger; experimentalHeadingIdCompat: boolean }; +}; + +/* + Expose standalone node for users to import in their config. + Allows users to apply a custom `render: AstroComponent` + and spread our default heading attributes. +*/ +export const heading: Schema = { + children: ['inline'], + attributes: { + id: { type: String }, + level: { type: Number, required: true, default: 1 }, + }, + transform(node, config: HeadingIdConfig) { + const { level, ...attributes } = node.transformAttributes(config); + const children = node.transformChildren(config); + + if (!config.ctx?.headingSlugger) { + throw new MarkdocError({ + message: + 'Unexpected problem adding heading IDs to Markdoc file. Did you modify the `ctx.headingSlugger` property in your Markdoc config?', + }); + } + const slug = getSlug( + attributes, + children, + config.ctx.headingSlugger, + config.ctx.experimentalHeadingIdCompat, + ); + + const render = config.nodes?.heading?.render ?? `h${level}`; + + const tagProps = + // For components, pass down `level` as a prop, + // alongside `__collectHeading` for our `headings` collector. + // Avoid accidentally rendering `level` as an HTML attribute otherwise! + typeof render === 'string' + ? { ...attributes, id: slug } + : { ...attributes, id: slug, __collectHeading: true, level }; + + return new Markdoc.Tag(render, tagProps, children); + }, +}; + +// Called internally to ensure `ctx` is generated per-file, instead of per-build. +export function setupHeadingConfig(experimentalHeadingIdCompat: boolean): HeadingIdConfig { + const headingSlugger = new Slugger(); + return { + ctx: { + headingSlugger, + experimentalHeadingIdCompat, + }, + nodes: { + heading, + }, + }; +} diff --git a/packages/integrations/markdoc/src/html/css/parse-inline-css-to-react.ts b/packages/integrations/markdoc/src/html/css/parse-inline-css-to-react.ts new file mode 100644 index 000000000..c35376641 --- /dev/null +++ b/packages/integrations/markdoc/src/html/css/parse-inline-css-to-react.ts @@ -0,0 +1,24 @@ +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-z\d])/gi, (_match, char) => { + return char.toUpperCase(); + }); + return replaced; +} diff --git a/packages/integrations/markdoc/src/html/css/parse-inline-styles.ts b/packages/integrations/markdoc/src/html/css/parse-inline-styles.ts new file mode 100644 index 000000000..fa3217c89 --- /dev/null +++ b/packages/integrations/markdoc/src/html/css/parse-inline-styles.ts @@ -0,0 +1,276 @@ +// @ts-nocheck +// https://github.com/remarkablemark/inline-style-parser + +/** + * @license MIT + * + * (The MIT License) + * + * Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca> + * + * 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: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * 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. + */ + +// http://www.w3.org/TR/CSS21/grammar.html +// https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027 +const COMMENT_REGEX = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g; + +const NEWLINE_REGEX = /\n/g; +const WHITESPACE_REGEX = /^\s*/; + +// declaration +const PROPERTY_REGEX = /^([-#/*\\\w]+(\[[\da-z_-]+\])?)\s*/; +const COLON_REGEX = /^:\s*/; +// Disable eslint as we're not sure how to improve this regex yet +// eslint-disable-next-line regexp/no-super-linear-backtracking +const VALUE_REGEX = /^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*\)|[^};])+)/; +const SEMICOLON_REGEX = /^[;\s]*/; + +// https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill +const TRIM_REGEX = /^\s+|\s+$/g; + +// strings +const NEWLINE = '\n'; +const FORWARD_SLASH = '/'; +const ASTERISK = '*'; +const EMPTY_STRING = ''; + +// types +const TYPE_COMMENT = 'comment'; +const TYPE_DECLARATION = 'declaration'; + +/** + * @param {String} style + * @param {Object} [options] + * @return {Object[]} + * @throws {TypeError} + * @throws {Error} + */ +export function parseInlineStyles(style, options) { + if (typeof style !== 'string') { + throw new TypeError('First argument must be a string'); + } + + if (!style) return []; + + options = options || {}; + + /** + * Positional. + */ + let lineno = 1; + let column = 1; + + /** + * Update lineno and column based on `str`. + * + * @param {String} str + */ + function updatePosition(str) { + let lines = str.match(NEWLINE_REGEX); + if (lines) lineno += lines.length; + let i = str.lastIndexOf(NEWLINE); + column = ~i ? str.length - i : column + str.length; + } + + /** + * Mark position and patch `node.position`. + * + * @return {Function} + */ + function position() { + let start = { line: lineno, column: column }; + return function (node) { + node.position = new Position(start); + whitespace(); + return node; + }; + } + + /** + * Store position information for a node. + * + * @constructor + * @property {Object} start + * @property {Object} end + * @property {undefined|String} source + */ + function Position(start) { + this.start = start; + this.end = { line: lineno, column: column }; + this.source = options.source; + } + + /** + * Non-enumerable source string. + */ + Position.prototype.content = style; + + const errorsList = []; + + /** + * Error `msg`. + * + * @param {String} msg + * @throws {Error} + */ + function error(msg) { + const err = new Error(options.source + ':' + lineno + ':' + column + ': ' + msg); + err.reason = msg; + err.filename = options.source; + err.line = lineno; + err.column = column; + err.source = style; + + if (options.silent) { + errorsList.push(err); + } else { + throw err; + } + } + + /** + * Match `re` and return captures. + * + * @param {RegExp} re + * @return {undefined|Array} + */ + function match(re) { + const m = re.exec(style); + if (!m) return; + const str = m[0]; + updatePosition(str); + style = style.slice(str.length); + return m; + } + + /** + * Parse whitespace. + */ + function whitespace() { + match(WHITESPACE_REGEX); + } + + /** + * Parse comments. + * + * @param {Object[]} [rules] + * @return {Object[]} + */ + function comments(rules) { + let c; + rules = rules || []; + while ((c = comment())) { + if (c !== false) { + rules.push(c); + } + } + return rules; + } + + /** + * Parse comment. + * + * @return {Object} + * @throws {Error} + */ + function comment() { + const pos = position(); + if (FORWARD_SLASH != style.charAt(0) || ASTERISK != style.charAt(1)) return; + + let i = 2; + while ( + EMPTY_STRING != style.charAt(i) && + (ASTERISK != style.charAt(i) || FORWARD_SLASH != style.charAt(i + 1)) + ) { + ++i; + } + i += 2; + + if (EMPTY_STRING === style.charAt(i - 1)) { + return error('End of comment missing'); + } + + const str = style.slice(2, i - 2); + column += 2; + updatePosition(str); + style = style.slice(i); + column += 2; + + return pos({ + type: TYPE_COMMENT, + comment: str, + }); + } + + /** + * Parse declaration. + * + * @return {Object} + * @throws {Error} + */ + function declaration() { + const pos = position(); + + // prop + const prop = match(PROPERTY_REGEX); + if (!prop) return; + comment(); + + // : + if (!match(COLON_REGEX)) return error("property missing ':'"); + + // val + const val = match(VALUE_REGEX); + + const ret = pos({ + type: TYPE_DECLARATION, + property: trim(prop[0].replace(COMMENT_REGEX, EMPTY_STRING)), + value: val ? trim(val[0].replace(COMMENT_REGEX, EMPTY_STRING)) : EMPTY_STRING, + }); + + // ; + match(SEMICOLON_REGEX); + + return ret; + } + + /** + * Parse declarations. + * + * @return {Object[]} + */ + function declarations() { + const decls = []; + + comments(decls); + + // declarations + let decl; + while ((decl = declaration())) { + if (decl !== false) { + decls.push(decl); + comments(decls); + } + } + + return decls; + } + + whitespace(); + return declarations(); +} + +/** + * Trim `str`. + * + * @param {String} str + * @return {String} + */ +function trim(str) { + return str ? str.replace(TRIM_REGEX, EMPTY_STRING) : EMPTY_STRING; +} diff --git a/packages/integrations/markdoc/src/html/css/style-to-object.ts b/packages/integrations/markdoc/src/html/css/style-to-object.ts new file mode 100644 index 000000000..084c93c93 --- /dev/null +++ b/packages/integrations/markdoc/src/html/css/style-to-object.ts @@ -0,0 +1,70 @@ +// @ts-nocheck +// https://github.com/remarkablemark/style-to-object + +/** + * @license MIT + * + * The MIT License (MIT) + * + * Copyright (c) 2017 Menglin "Mark" Xu <mark@remarkablemark.org> + * + * 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: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * 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. + */ + +import { parseInlineStyles } from './parse-inline-styles.js'; + +/** + * Parses inline style to object. + * + * @example + * // returns { 'line-height': '42' } + * styleToObject('line-height: 42;'); + * + * @param {String} style - The inline style. + * @param {Function} [iterator] - The iterator function. + * @return {null|Object} + */ +export function styleToObject(style, iterator) { + let output = null; + if (!style || typeof style !== 'string') { + return output; + } + + let declaration; + let declarations = parseInlineStyles(style); + let hasIterator = typeof iterator === 'function'; + let property; + let value; + + for (let i = 0, len = declarations.length; i < len; i++) { + declaration = declarations[i]; + property = declaration.property; + value = declaration.value; + + if (hasIterator) { + iterator(property, value, declaration); + } else if (value) { + output || (output = {}); + output[property] = value; + } + } + + return output; +} diff --git a/packages/integrations/markdoc/src/html/tagdefs/html.tag.ts b/packages/integrations/markdoc/src/html/tagdefs/html.tag.ts new file mode 100644 index 000000000..0c094227d --- /dev/null +++ b/packages/integrations/markdoc/src/html/tagdefs/html.tag.ts @@ -0,0 +1,69 @@ +import type { Config, Schema } from '@markdoc/markdoc'; +import Markdoc from '@markdoc/markdoc'; + +const booleanAttributes = new Set([ + 'allowfullscreen', + 'async', + 'autofocus', + 'autoplay', + 'checked', + 'controls', + 'default', + 'defer', + 'disabled', + 'disablepictureinpicture', + 'disableremoteplayback', + 'download', + 'formnovalidate', + 'hidden', + 'inert', + 'ismap', + 'itemscope', + 'loop', + 'multiple', + 'muted', + 'nomodule', + 'novalidate', + 'open', + 'playsinline', + 'readonly', + 'required', + 'reversed', + 'selected', +]); + +// local +import { parseInlineCSSToReactLikeObject } from '../css/parse-inline-css-to-react.js'; + +// a Markdoc tag that will render a given HTML element and its attributes, as produced by the htmlTokenTransform function +export const htmlTag: Schema<Config, never> = { + attributes: { + name: { type: String, required: true }, + attrs: { type: Object }, + }, + + transform(node, config) { + const { name, attrs: unsafeAttributes } = node.attributes; + const children = node.transformChildren(config); + + // pull out any "unsafe" attributes which need additional processing + const { style, ...safeAttributes } = unsafeAttributes as Record<string, unknown>; + + // Convert boolean attributes to boolean literals + for (const [key, value] of Object.entries(safeAttributes)) { + if (booleanAttributes.has(key)) { + // If the attribute exists, ensure its value is a boolean + safeAttributes[key] = value === '' || value === true || value === 'true'; + } + } + + // if the inline "style" attribute is present we need to parse the HTML into a react-like React.CSSProperties object + if (typeof style === 'string') { + const styleObject = parseInlineCSSToReactLikeObject(style); + safeAttributes.style = styleObject; + } + + // create a Markdoc Tag for the given HTML node with the HTML attributes and children + return new Markdoc.Tag(name, safeAttributes, children); + }, +}; diff --git a/packages/integrations/markdoc/src/html/transform/html-token-transform.ts b/packages/integrations/markdoc/src/html/transform/html-token-transform.ts new file mode 100644 index 000000000..b80595f97 --- /dev/null +++ b/packages/integrations/markdoc/src/html/transform/html-token-transform.ts @@ -0,0 +1,249 @@ +import type { Tokenizer } from '@markdoc/markdoc'; +import { Parser } from 'htmlparser2'; +// @ts-expect-error This type isn't exported +// biome-ignore lint/correctness/noUnusedImports: not correctly detected because type isn't exported +import type * as Token from 'markdown-it/lib/token'; + +export function htmlTokenTransform(tokenizer: Tokenizer, tokens: Token[]): Token[] { + const output: Token[] = []; + + // hold a lazy buffer of text and process it only when necessary + let textBuffer = ''; + + let inCDATA = false; + + const appendText = (text: string) => { + textBuffer += text; + }; + + // process the current text buffer w/ Markdoc's Tokenizer for tokens + const processTextBuffer = () => { + if (textBuffer.length > 0) { + // tokenize the text buffer to look for structural markup tokens + const toks = tokenizer.tokenize(textBuffer); + + // when we tokenize some raw text content, it's basically treated like Markdown, and will result in a paragraph wrapper, which we don't want + // in this scenario, we just want to generate a text token, but, we have to tokenize it in case there's other structural markup + if (toks.length === 3) { + const first = toks[0]; + const second = toks[1]; + const third: Token | undefined = toks.at(2); + + if ( + first.type === 'paragraph_open' && + second.type === 'inline' && + third && + third.type === 'paragraph_close' && + Array.isArray(second.children) + ) { + for (const tok of second.children as Token[]) { + // if the given token is a 'text' token and its trimmed content is the same as the pre-tokenized text buffer, use the original + // text buffer instead to preserve leading/trailing whitespace that is lost during tokenization of pure text content + if (tok.type === 'text') { + if (tok.content.trim() == textBuffer.trim()) { + tok.content = textBuffer; + } + } + output.push(tok); + } + } else { + // some other markup that happened to be 3 tokens, push tokens as-is + for (const tok of toks) { + output.push(tok); + } + } + } else { + // some other tokenized markup, push tokens as-is + for (const tok of toks) { + output.push(tok); + } + } + + // reset the current lazy text buffer + textBuffer = ''; + } + }; + + // create an incremental HTML parser that tracks HTML tag open, close and text content + const parser = new Parser( + { + oncdatastart() { + inCDATA = true; + }, + + oncdataend() { + inCDATA = false; + }, + + // when an HTML tag opens... + onopentag(name, attrs) { + // process any buffered text to be treated as text node before the currently opening HTML tag + processTextBuffer(); + + // push an 'html-tag' 'tag_open' Markdoc node instance for the currently opening HTML tag onto the resulting Token stack + output.push({ + type: 'tag_open', + nesting: 1, + meta: { + tag: 'html-tag', + attributes: [ + { type: 'attribute', name: 'name', value: name }, + { type: 'attribute', name: 'attrs', value: attrs }, + ], + }, + } as Token); + }, + + ontext(content: string | null | undefined) { + if (inCDATA) { + // ignore entirely while inside CDATA + return; + } + + // only accumulate text into the buffer if we're not under an ignored HTML element + if (typeof content === 'string') { + appendText(content); + } + }, + + // when an HTML tag closes... + onclosetag(name) { + // process any buffered text to be treated as a text node inside the currently closing HTML tag + processTextBuffer(); + + // push an 'html-tag' 'tag_close' Markdoc node instance for the currently closing HTML tag onto the resulting Token stack + output.push({ + type: 'tag_close', + nesting: -1, + meta: { + tag: 'html-tag', + attributes: [{ type: 'attribute', name: 'name', value: name }], + }, + } as Token); + }, + }, + { + decodeEntities: false, + recognizeCDATA: true, + recognizeSelfClosing: true, + }, + ); + + // for every detected token... + for (const token of tokens) { + // if it was an HTML token, write the HTML text into the HTML parser + if (token.type.startsWith('html')) { + // as the parser encounters opening/closing HTML tags, it will push Markdoc Tag nodes into the output stack + parser.write(token.content); + + // continue loop... IMPORTANT! we're throwing away the original 'html' tokens here (raw HTML strings), since the parser is inserting new ones based on the parsed HTML + continue; + } + + // process any child content for HTML + if (token.type === 'inline') { + if (token.children) { + token.children = htmlTokenTransform(tokenizer, token.children); + } + } + + // not an HTML Token, preserve it at the current stack location + output.push(token); + } + + // process any remaining buffered text + processTextBuffer(); + + // + // post-process the current levels output Token[] array to un-wind this pattern: + // + // [ + // { type: tag_open, meta.tag: html-tag }, + // { type: paragraph_open }, + // { type: inline, children [...] }, + // { type: paragraph_close }, + // { type: tag_close, meta.tag: html-tag } + // ] + // + // the paragraph_open, inline, paragraph_close triplet needs to be replaced by the children of the inline node + // + // this is extra, unwanted paragraph wrapping unfortunately introduced by markdown-it during processing w/ HTML enabled + // + + mutateAndCollapseExtraParagraphsUnderHtml(output); + + return output; +} + +function mutateAndCollapseExtraParagraphsUnderHtml(tokens: Token[]): void { + let done = false; + + while (!done) { + const idx = findExtraParagraphUnderHtml(tokens); + if (typeof idx === 'number') { + // mutate + + const actualChildTokens = tokens[idx + 2].children ?? []; + + tokens.splice(idx, 5, ...actualChildTokens); + } else { + done = true; + } + } +} + +/** + * + * @param token + * @returns + */ +function findExtraParagraphUnderHtml(tokens: Token[]): number | null { + if (tokens.length < 5) { + return null; + } + + for (let i = 0; i < tokens.length; i++) { + const last = i + 4; + if (last > tokens.length - 1) { + break; // early exit, no more possible 5-long slices to search + } + + const slice = tokens.slice(i, last + 1); + const isMatch = isExtraParagraphPatternMatch(slice); + if (isMatch) { + return i; + } + } + + return null; +} + +function isExtraParagraphPatternMatch(slice: Token[]): boolean { + const match = + isHtmlTagOpen(slice[0]) && + isParagraphOpen(slice[1]) && + isInline(slice[2]) && + isParagraphClose(slice[3]) && + isHtmlTagClose(slice[4]); + return match; +} + +function isHtmlTagOpen(token: Token): boolean { + return token.type === 'tag_open' && token.meta && token.meta.tag === 'html-tag'; +} + +function isHtmlTagClose(token: Token): boolean { + return token.type === 'tag_close' && token.meta && token.meta.tag === 'html-tag'; +} + +function isParagraphOpen(token: Token): boolean { + return token.type === 'paragraph_open'; +} + +function isParagraphClose(token: Token): boolean { + return token.type === 'paragraph_close'; +} + +function isInline(token: Token): boolean { + return token.type === 'inline'; +} diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts new file mode 100644 index 000000000..d328d4a8d --- /dev/null +++ b/packages/integrations/markdoc/src/index.ts @@ -0,0 +1,49 @@ +import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro'; +import { getContentEntryType } from './content-entry-type.js'; +import { + type MarkdocConfigResult, + SUPPORTED_MARKDOC_CONFIG_FILES, + loadMarkdocConfig, +} from './load-config.js'; +import type { MarkdocIntegrationOptions } from './options.js'; + +type SetupHookParams = HookParameters<'astro:config:setup'> & { + // `contentEntryType` is not a public API + // Add type defs here + addContentEntryType: (contentEntryType: ContentEntryType) => void; +}; + +export default function markdocIntegration(options?: MarkdocIntegrationOptions): AstroIntegration { + let markdocConfigResult: MarkdocConfigResult | undefined; + let astroConfig: AstroConfig; + return { + name: '@astrojs/markdoc', + hooks: { + 'astro:config:setup': async (params) => { + const { updateConfig, addContentEntryType } = params as SetupHookParams; + astroConfig = params.config; + + markdocConfigResult = await loadMarkdocConfig(astroConfig); + + addContentEntryType( + await getContentEntryType({ markdocConfigResult, astroConfig, options }), + ); + + updateConfig({ + vite: { + ssr: { + external: ['@astrojs/markdoc/prism', '@astrojs/markdoc/shiki'], + }, + }, + }); + }, + 'astro:server:setup': async ({ server }) => { + server.watcher.on('all', (_event, entry) => { + if (SUPPORTED_MARKDOC_CONFIG_FILES.some((f) => entry.endsWith(f))) { + server.restart(); + } + }); + }, + }, + }; +} diff --git a/packages/integrations/markdoc/src/load-config.ts b/packages/integrations/markdoc/src/load-config.ts new file mode 100644 index 000000000..ce181e604 --- /dev/null +++ b/packages/integrations/markdoc/src/load-config.ts @@ -0,0 +1,118 @@ +import * as fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import type { AstroConfig } from 'astro'; +import { build as esbuild } from 'esbuild'; +import type { AstroMarkdocConfig } from './config.js'; +import { MarkdocError } from './utils.js'; + +export const SUPPORTED_MARKDOC_CONFIG_FILES = [ + 'markdoc.config.js', + 'markdoc.config.mjs', + 'markdoc.config.mts', + 'markdoc.config.ts', +]; + +export type MarkdocConfigResult = { + config: AstroMarkdocConfig; + fileUrl: URL; +}; + +export async function loadMarkdocConfig( + astroConfig: Pick<AstroConfig, 'root'>, +): Promise<MarkdocConfigResult | undefined> { + let markdocConfigUrl: URL | undefined; + for (const filename of SUPPORTED_MARKDOC_CONFIG_FILES) { + const filePath = new URL(filename, astroConfig.root); + if (!fs.existsSync(filePath)) continue; + + markdocConfigUrl = filePath; + break; + } + if (!markdocConfigUrl) return; + + const { code } = await bundleConfigFile({ + markdocConfigUrl, + astroConfig, + }); + const config: AstroMarkdocConfig = await loadConfigFromBundledFile(astroConfig.root, code); + + return { + config, + fileUrl: markdocConfigUrl, + }; +} + +/** + * Bundle config file to support `.ts` files. + * Simplified fork from Vite's `bundleConfigFile` function: + * @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L961 + */ +async function bundleConfigFile({ + markdocConfigUrl, + astroConfig, +}: { + markdocConfigUrl: URL; + astroConfig: Pick<AstroConfig, 'root'>; +}): Promise<{ code: string; dependencies: string[] }> { + let markdocError: MarkdocError | undefined; + + const result = await esbuild({ + absWorkingDir: fileURLToPath(astroConfig.root), + entryPoints: [fileURLToPath(markdocConfigUrl)], + outfile: 'out.js', + write: false, + target: ['node16'], + platform: 'node', + packages: 'external', + bundle: true, + format: 'esm', + sourcemap: 'inline', + metafile: true, + plugins: [ + { + name: 'stub-astro-imports', + setup(build) { + build.onResolve({ filter: /.*\.astro$/ }, () => { + // Avoid throwing within esbuild. + // This swallows the `hint` and blows up the stacktrace. + markdocError = new MarkdocError({ + message: '`.astro` files are no longer supported in the Markdoc config.', + hint: 'Use the `component()` utility to specify a component path instead. See https://docs.astro.build/en/guides/integrations-guide/markdoc/', + }); + return { + // Stub with an unused default export. + path: 'data:text/javascript,export default true', + external: true, + }; + }); + }, + }, + ], + }); + if (markdocError) throw markdocError; + const { text } = result.outputFiles[0]; + return { + code: text, + dependencies: result.metafile ? Object.keys(result.metafile.inputs) : [], + }; +} + +/** + * Forked from Vite config loader, replacing CJS-based path concat + * with ESM only + * @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L1074 + */ +async function loadConfigFromBundledFile(root: URL, code: string): Promise<AstroMarkdocConfig> { + // Write it to disk, load it with native Node ESM, then delete the file. + const tmpFileUrl = new URL(`markdoc.config.timestamp-${Date.now()}.mjs`, root); + fs.writeFileSync(tmpFileUrl, code); + try { + return (await import(tmpFileUrl.pathname)).default; + } finally { + try { + fs.unlinkSync(tmpFileUrl); + } catch { + // already removed if this function is called twice simultaneously + } + } +} diff --git a/packages/integrations/markdoc/src/options.ts b/packages/integrations/markdoc/src/options.ts new file mode 100644 index 000000000..abaeb5a96 --- /dev/null +++ b/packages/integrations/markdoc/src/options.ts @@ -0,0 +1,5 @@ +export interface MarkdocIntegrationOptions { + allowHTML?: boolean; + ignoreIndentation?: boolean; + typographer?: boolean; +} diff --git a/packages/integrations/markdoc/src/runtime-assets-config.ts b/packages/integrations/markdoc/src/runtime-assets-config.ts new file mode 100644 index 000000000..0211c1381 --- /dev/null +++ b/packages/integrations/markdoc/src/runtime-assets-config.ts @@ -0,0 +1,26 @@ +//@ts-expect-error Cannot find module 'astro:assets' or its corresponding type declarations. +import { Image } from 'astro:assets'; +import type { Config as MarkdocConfig } from '@markdoc/markdoc'; +import Markdoc from '@markdoc/markdoc'; + +export const assetsConfig: MarkdocConfig = { + nodes: { + image: { + attributes: { + ...Markdoc.nodes.image.attributes, + __optimizedSrc: { type: 'Object' }, + }, + transform(node, config) { + const attributes = node.transformAttributes(config); + const children = node.transformChildren(config); + + if (node.type === 'image' && '__optimizedSrc' in node.attributes) { + const { __optimizedSrc, ...rest } = node.attributes; + return new Markdoc.Tag(Image, { ...rest, src: __optimizedSrc }, children); + } else { + return new Markdoc.Tag('img', attributes, children); + } + }, + }, + }, +}; diff --git a/packages/integrations/markdoc/src/runtime.ts b/packages/integrations/markdoc/src/runtime.ts new file mode 100644 index 000000000..44c232b79 --- /dev/null +++ b/packages/integrations/markdoc/src/runtime.ts @@ -0,0 +1,217 @@ +import type { MarkdownHeading } from '@astrojs/markdown-remark'; +import Markdoc, { + type ConfigType, + type Node, + type NodeType, + type RenderableTreeNode, +} from '@markdoc/markdoc'; +import type { AstroInstance } from 'astro'; +import { createComponent, renderComponent } from 'astro/runtime/server/index.js'; +import type { AstroMarkdocConfig } from './config.js'; +import { setupHeadingConfig } from './heading-ids.js'; +import { htmlTag } from './html/tagdefs/html.tag.js'; +import type { MarkdocIntegrationOptions } from './options.js'; +/** + * Merge user config with default config and set up context (ex. heading ID slugger) + * Called on each file's individual transform. + * TODO: virtual module to merge configs per-build instead of per-file? + */ +export async function setupConfig( + userConfig: AstroMarkdocConfig = {}, + options: MarkdocIntegrationOptions | undefined, + experimentalHeadingIdCompat: boolean, +): Promise<MergedConfig> { + let defaultConfig: AstroMarkdocConfig = setupHeadingConfig(experimentalHeadingIdCompat); + + if (userConfig.extends) { + for (let extension of userConfig.extends) { + if (extension instanceof Promise) { + extension = await extension; + } + + defaultConfig = mergeConfig(defaultConfig, extension); + } + } + + let merged = mergeConfig(defaultConfig, userConfig); + + if (options?.allowHTML) { + merged = mergeConfig(merged, HTML_CONFIG); + } + + return merged; +} + +/** Used for synchronous `getHeadings()` function */ +export function setupConfigSync( + userConfig: AstroMarkdocConfig = {}, + options: MarkdocIntegrationOptions | undefined, + experimentalHeadingIdCompat: boolean, +): MergedConfig { + const defaultConfig: AstroMarkdocConfig = setupHeadingConfig(experimentalHeadingIdCompat); + + let merged = mergeConfig(defaultConfig, userConfig); + + if (options?.allowHTML) { + merged = mergeConfig(merged, HTML_CONFIG); + } + + return merged; +} + +type MergedConfig = Required<Omit<AstroMarkdocConfig, 'extends'>>; + +/** Merge function from `@markdoc/markdoc` internals */ +export function mergeConfig( + configA: AstroMarkdocConfig, + configB: AstroMarkdocConfig, +): MergedConfig { + return { + ...configA, + ...configB, + ctx: { + ...configA.ctx, + ...configB.ctx, + }, + tags: { + ...configA.tags, + ...configB.tags, + }, + nodes: { + ...configA.nodes, + ...configB.nodes, + }, + functions: { + ...configA.functions, + ...configB.functions, + }, + variables: { + ...configA.variables, + ...configB.variables, + }, + partials: { + ...configA.partials, + ...configB.partials, + }, + validation: { + ...configA.validation, + ...configB.validation, + }, + }; +} + +export function resolveComponentImports( + markdocConfig: Required<Pick<AstroMarkdocConfig, 'tags' | 'nodes'>>, + tagComponentMap: Record<string, AstroInstance['default']>, + nodeComponentMap: Record<NodeType, AstroInstance['default']>, +) { + for (const [tag, render] of Object.entries(tagComponentMap)) { + const config = markdocConfig.tags[tag]; + if (config) config.render = render; + } + for (const [node, render] of Object.entries(nodeComponentMap)) { + const config = markdocConfig.nodes[node as NodeType]; + if (config) config.render = render; + } + return markdocConfig; +} + +/** + * Get text content as a string from a Markdoc transform AST + */ +export function getTextContent(childNodes: RenderableTreeNode[]): string { + let text = ''; + for (const node of childNodes) { + if (typeof node === 'string' || typeof node === 'number') { + text += node; + } else if (typeof node === 'object' && Markdoc.Tag.isTag(node)) { + text += getTextContent(node.children); + } + } + return text; +} + +const headingLevels = [1, 2, 3, 4, 5, 6] as const; + +/** + * Collect headings from Markdoc transform AST + * for `headings` result on `render()` return value + */ +export function collectHeadings( + children: RenderableTreeNode[], + collectedHeadings: MarkdownHeading[], +) { + for (const node of children) { + if (typeof node !== 'object' || !Markdoc.Tag.isTag(node)) continue; + + if (node.attributes.__collectHeading === true && typeof node.attributes.level === 'number') { + collectedHeadings.push({ + slug: node.attributes.id, + depth: node.attributes.level, + text: getTextContent(node.children), + }); + continue; + } + + for (const level of headingLevels) { + if (node.name === 'h' + level) { + collectedHeadings.push({ + slug: node.attributes.id, + depth: level, + text: getTextContent(node.children), + }); + } + } + collectHeadings(node.children, collectedHeadings); + } +} + +export function createGetHeadings( + stringifiedAst: string, + userConfig: AstroMarkdocConfig, + options: MarkdocIntegrationOptions | undefined, + experimentalHeadingIdCompat: boolean, +) { + return function getHeadings() { + /* Yes, we are transforming twice (once from `getHeadings()` and again from <Content /> in case of variables). + TODO: propose new `render()` API to allow Markdoc variable passing to `render()` itself, + instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */ + const config = setupConfigSync(userConfig, options, experimentalHeadingIdCompat); + const ast = Markdoc.Ast.fromJSON(stringifiedAst); + const content = Markdoc.transform(ast as Node, config as ConfigType); + let collectedHeadings: MarkdownHeading[] = []; + collectHeadings(Array.isArray(content) ? content : [content], collectedHeadings); + return collectedHeadings; + }; +} + +export function createContentComponent( + Renderer: AstroInstance['default'], + stringifiedAst: string, + userConfig: AstroMarkdocConfig, + options: MarkdocIntegrationOptions | undefined, + tagComponentMap: Record<string, AstroInstance['default']>, + nodeComponentMap: Record<NodeType, AstroInstance['default']>, + experimentalHeadingIdCompat: boolean, +) { + return createComponent({ + async factory(result: any, props: Record<string, any>) { + const withVariables = mergeConfig(userConfig, { variables: props }); + const config = resolveComponentImports( + await setupConfig(withVariables, options, experimentalHeadingIdCompat), + tagComponentMap, + nodeComponentMap, + ); + + return renderComponent(result, Renderer.name, Renderer, { stringifiedAst, config }, {}); + }, + propagation: 'self', + } as any); +} + +// statically define a partial MarkdocConfig which registers the required "html-tag" Markdoc tag when the "allowHTML" feature is enabled +const HTML_CONFIG: AstroMarkdocConfig = { + tags: { + 'html-tag': htmlTag, + }, +}; diff --git a/packages/integrations/markdoc/src/tokenizer.ts b/packages/integrations/markdoc/src/tokenizer.ts new file mode 100644 index 000000000..1f5b1de28 --- /dev/null +++ b/packages/integrations/markdoc/src/tokenizer.ts @@ -0,0 +1,44 @@ +import type { Tokenizer } from '@markdoc/markdoc'; +import Markdoc from '@markdoc/markdoc'; +import type { MarkdocIntegrationOptions } from './options.js'; + +type TokenizerOptions = ConstructorParameters<typeof Tokenizer>[0]; + +export function getMarkdocTokenizer(options: MarkdocIntegrationOptions | undefined): Tokenizer { + const key = cacheKey(options); + + if (!_cachedMarkdocTokenizers[key]) { + const tokenizerOptions: TokenizerOptions = { + // Strip <!-- comments --> from rendered output + // Without this, they're rendered as strings! + allowComments: true, + }; + + if (options?.allowHTML) { + // allow indentation for Markdoc tags that are interleaved inside HTML block elements + tokenizerOptions.allowIndentation = true; + // enable HTML token detection in markdown-it + tokenizerOptions.html = true; + } + if (options?.ignoreIndentation) { + // allow indentation so nested Markdoc tags can be formatted for better readability + tokenizerOptions.allowIndentation = true; + } + if (options?.typographer) { + // enable typographer to convert straight quotes to curly quotes, etc. + tokenizerOptions.typographer = options.typographer; + } + + _cachedMarkdocTokenizers[key] = new Markdoc.Tokenizer(tokenizerOptions); + } + + return _cachedMarkdocTokenizers[key]; +} + +// create this on-demand when needed since it relies on the runtime MarkdocIntegrationOptions and may change during +// the life of module in certain scenarios (unit tests, etc.) +let _cachedMarkdocTokenizers: Record<string, Tokenizer> = {}; + +function cacheKey(options: MarkdocIntegrationOptions | undefined): string { + return JSON.stringify(options); +} diff --git a/packages/integrations/markdoc/src/utils.ts b/packages/integrations/markdoc/src/utils.ts new file mode 100644 index 000000000..dc9c9023f --- /dev/null +++ b/packages/integrations/markdoc/src/utils.ts @@ -0,0 +1,68 @@ +import type { ComponentConfig } from './config.js'; + +/** + * Matches AstroError object with types like error codes stubbed out + * @see 'astro/src/core/errors/errors.ts' + */ +export class MarkdocError extends Error { + public loc: ErrorLocation | undefined; + public title: string | undefined; + public hint: string | undefined; + public frame: string | undefined; + + type = 'MarkdocError'; + + constructor(props: ErrorProperties, ...params: any) { + super(...params); + + const { title = 'MarkdocError', message, stack, location, hint, frame } = props; + + this.title = title; + if (message) this.message = message; + // Only set this if we actually have a stack passed, otherwise uses Error's + this.stack = stack ? stack : this.stack; + this.loc = location; + this.hint = hint; + this.frame = frame; + } +} + +interface ErrorLocation { + file?: string; + line?: number; + column?: number; +} + +interface ErrorProperties { + code?: number; + title?: string; + name?: string; + message?: string; + location?: ErrorLocation; + hint?: string; + stack?: string; + frame?: string; +} + +/** + * @see 'astro/src/core/path.ts' + */ +export function prependForwardSlash(str: string) { + return str[0] === '/' ? str : '/' + str; +} + +export function isValidUrl(str: string): boolean { + try { + new URL(str); + return true; + } catch { + return false; + } +} + +/** Identifier for components imports passed as `tags` or `nodes` configuration. */ +export const componentConfigSymbol = Symbol.for('@astrojs/markdoc/component-config'); + +export function isComponentConfig(value: unknown): value is ComponentConfig { + return typeof value === 'object' && value !== null && componentConfigSymbol in value; +} diff --git a/packages/integrations/markdoc/template/content-module-types.d.ts b/packages/integrations/markdoc/template/content-module-types.d.ts new file mode 100644 index 000000000..193f4c6f2 --- /dev/null +++ b/packages/integrations/markdoc/template/content-module-types.d.ts @@ -0,0 +1,8 @@ +declare module 'astro:content' { + interface Render { + '.mdoc': Promise<{ + Content(props: Record<string, any>): import('astro').MarkdownInstance<{}>['Content']; + headings: import('astro').MarkdownHeading[]; + }>; + } +} diff --git a/packages/integrations/markdoc/test/content-collections.test.js b/packages/integrations/markdoc/test/content-collections.test.js new file mode 100644 index 000000000..48e97d45d --- /dev/null +++ b/packages/integrations/markdoc/test/content-collections.test.js @@ -0,0 +1,120 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { parse as parseDevalue } from 'devalue'; +import { fixLineEndings, loadFixture } from '../../../astro/test/test-utils.js'; +import markdoc from '../dist/index.js'; + +function formatPost(post) { + return { + ...post, + body: fixLineEndings(post.body), + }; +} + +const root = new URL('./fixtures/content-collections/', import.meta.url); + +const sortById = (a, b) => a.id.localeCompare(b.id); + +describe('Markdoc - Content Collections', () => { + let baseFixture; + + before(async () => { + baseFixture = await loadFixture({ + root, + integrations: [markdoc()], + }); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await baseFixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('loads entry', async () => { + const res = await baseFixture.fetch('/entry.json'); + const post = parseDevalue(await res.text()); + assert.deepEqual(formatPost(post), post1Entry); + }); + + it('loads collection', async () => { + const res = await baseFixture.fetch('/collection.json'); + const posts = parseDevalue(await res.text()); + assert.notEqual(posts, null); + + assert.deepEqual( + posts.sort(sortById).map((post) => formatPost(post)), + [post1Entry, post2Entry, post3Entry], + ); + }); + }); + + describe('build', () => { + before(async () => { + await baseFixture.build(); + }); + + it('loads entry', async () => { + const res = await baseFixture.readFile('/entry.json'); + const post = parseDevalue(res); + assert.deepEqual(formatPost(post), post1Entry); + }); + + it('loads collection', async () => { + const res = await baseFixture.readFile('/collection.json'); + const posts = parseDevalue(res); + assert.notEqual(posts, null); + assert.deepEqual( + posts.sort(sortById).map((post) => formatPost(post)), + [post1Entry, post2Entry, post3Entry], + ); + }); + }); +}); + +const post1Entry = { + id: 'post-1.mdoc', + slug: 'post-1', + collection: 'blog', + data: { + schemaWorks: true, + title: 'Post 1', + }, + body: '## Post 1\n\nThis is the contents of post 1.', + deferredRender: true, + filePath: 'src/content/blog/post-1.mdoc', + digest: '5d5bd98d949e2b9a', +}; + +const post2Entry = { + id: 'post-2.mdoc', + slug: 'post-2', + collection: 'blog', + data: { + schemaWorks: true, + title: 'Post 2', + }, + body: '## Post 2\n\nThis is the contents of post 2.', + deferredRender: true, + filePath: 'src/content/blog/post-2.mdoc', + digest: '595af4b93a4af072', +}; + +const post3Entry = { + id: 'post-3.mdoc', + slug: 'post-3', + collection: 'blog', + data: { + schemaWorks: true, + title: 'Post 3', + }, + body: '## Post 3\n\nThis is the contents of post 3.', + deferredRender: true, + filePath: 'src/content/blog/post-3.mdoc', + digest: 'ef589606e542247e', +}; diff --git a/packages/integrations/markdoc/test/fixtures/content-collections/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/content-collections/astro.config.mjs new file mode 100644 index 000000000..1bd8ba93f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/content-collections/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/content-collections/package.json b/packages/integrations/markdoc/test/fixtures/content-collections/package.json new file mode 100644 index 000000000..370b87957 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/content-collections/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-content-collections", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-1.mdoc b/packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-1.mdoc new file mode 100644 index 000000000..06c900963 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-1.mdoc @@ -0,0 +1,7 @@ +--- +title: Post 1 +--- + +## Post 1 + +This is the contents of post 1. diff --git a/packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-2.mdoc b/packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-2.mdoc new file mode 100644 index 000000000..cf4dc162f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-2.mdoc @@ -0,0 +1,7 @@ +--- +title: Post 2 +--- + +## Post 2 + +This is the contents of post 2. diff --git a/packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-3.mdoc b/packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-3.mdoc new file mode 100644 index 000000000..6c601eb65 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/content-collections/src/content/blog/post-3.mdoc @@ -0,0 +1,7 @@ +--- +title: Post 3 +--- + +## Post 3 + +This is the contents of post 3. diff --git a/packages/integrations/markdoc/test/fixtures/content-collections/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/content-collections/src/content/config.ts new file mode 100644 index 000000000..3b411201a --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/content-collections/src/content/config.ts @@ -0,0 +1,12 @@ +import { defineCollection, z } from 'astro:content'; + +const blog = defineCollection({ + schema: z.object({ + title: z.string(), + }).transform(data => ({ + ...data, + schemaWorks: true, + })) +}); + +export const collections = { blog }; diff --git a/packages/integrations/markdoc/test/fixtures/content-collections/src/pages/collection.json.js b/packages/integrations/markdoc/test/fixtures/content-collections/src/pages/collection.json.js new file mode 100644 index 000000000..cb3c84652 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/content-collections/src/pages/collection.json.js @@ -0,0 +1,8 @@ +import { getCollection } from 'astro:content'; +import { stringify } from 'devalue'; +import { stripAllRenderFn } from '../../utils.js'; + +export async function GET() { + const posts = await getCollection('blog'); + return new Response(stringify(stripAllRenderFn(posts))); +} diff --git a/packages/integrations/markdoc/test/fixtures/content-collections/src/pages/entry.json.js b/packages/integrations/markdoc/test/fixtures/content-collections/src/pages/entry.json.js new file mode 100644 index 000000000..53dd17013 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/content-collections/src/pages/entry.json.js @@ -0,0 +1,8 @@ +import { getEntryBySlug } from 'astro:content'; +import { stringify } from 'devalue'; +import { stripRenderFn } from '../../utils.js'; + +export async function GET() { + const post = await getEntryBySlug('blog', 'post-1'); + return new Response(stringify(stripRenderFn(post))); +} diff --git a/packages/integrations/markdoc/test/fixtures/content-collections/utils.js b/packages/integrations/markdoc/test/fixtures/content-collections/utils.js new file mode 100644 index 000000000..3a6244327 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/content-collections/utils.js @@ -0,0 +1,8 @@ +export function stripRenderFn(entryWithRender) { + const { render, ...entry } = entryWithRender; + return entry; +} + +export function stripAllRenderFn(collection = []) { + return collection.map(stripRenderFn); +} diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/headings-custom/astro.config.mjs new file mode 100644 index 000000000..1bd8ba93f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/markdoc.config.mjs b/packages/integrations/markdoc/test/fixtures/headings-custom/markdoc.config.mjs new file mode 100644 index 000000000..cbd03d728 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/markdoc.config.mjs @@ -0,0 +1,10 @@ +import { component, defineMarkdocConfig, nodes } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({ + nodes: { + heading: { + ...nodes.heading, + render: component('./src/components/Heading.astro'), + } + } +}); diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/package.json b/packages/integrations/markdoc/test/fixtures/headings-custom/package.json new file mode 100644 index 000000000..67a974912 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/headings-custom", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/src/components/Heading.astro b/packages/integrations/markdoc/test/fixtures/headings-custom/src/components/Heading.astro new file mode 100644 index 000000000..ec6fa8305 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/src/components/Heading.astro @@ -0,0 +1,14 @@ +--- +type Props = { + level: number; + id: string; +}; + +const { level, id }: Props = Astro.props; + +const Tag = `h${level}`; +--- + +<Tag data-custom-heading {id}> + <slot /> +</Tag> diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/config.ts new file mode 100644 index 000000000..a142ace11 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; + +const docs = defineCollection({}); + +export const collections = { + docs, +}; diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings-stale-cache-check.mdoc b/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings-stale-cache-check.mdoc new file mode 100644 index 000000000..75cd52884 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings-stale-cache-check.mdoc @@ -0,0 +1,13 @@ +Our heading ID generator can have a stale cache for duplicates. Let's check for those! + +# Level 1 heading + +## Level **2 heading** + +### Level _3 heading_ + +#### Level [4 heading](/with-a-link) + +##### Level 5 heading with override {% #id-override %} + +###### Level 6 heading diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings.mdoc b/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings.mdoc new file mode 100644 index 000000000..3eb66580a --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings.mdoc @@ -0,0 +1,11 @@ +# Level 1 heading + +## Level **2 heading** + +### Level _3 heading_ + +#### Level [4 heading](/with-a-link) + +##### Level 5 heading with override {% #id-override %} + +###### Level 6 heading diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/src/pages/[slug].astro b/packages/integrations/markdoc/test/fixtures/headings-custom/src/pages/[slug].astro new file mode 100644 index 000000000..90b021e95 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings-custom/src/pages/[slug].astro @@ -0,0 +1,34 @@ +--- +import { CollectionEntry, getCollection } from "astro:content"; + +export async function getStaticPaths() { + const docs = await getCollection('docs'); + return docs.map(doc => ({ params: { slug: doc.slug }, props: doc })); +} + +type Props = CollectionEntry<'docs'>; + +const { Content, headings } = await Astro.props.render(); +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Content</title> +</head> +<body> + <nav data-toc> + <ul> + {headings.map(heading => ( + <li> + <a href={`#${heading.slug}`} data-depth={heading.depth}>{heading.text}</a> + </li> + ))} + </ul> + </nav> + <Content /> +</body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/headings/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/headings/astro.config.mjs new file mode 100644 index 000000000..1bd8ba93f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/headings/markdoc.config.mjs b/packages/integrations/markdoc/test/fixtures/headings/markdoc.config.mjs new file mode 100644 index 000000000..a5863ec12 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings/markdoc.config.mjs @@ -0,0 +1,3 @@ +import { defineMarkdocConfig } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({}); diff --git a/packages/integrations/markdoc/test/fixtures/headings/package.json b/packages/integrations/markdoc/test/fixtures/headings/package.json new file mode 100644 index 000000000..1daaae400 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/headings", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/headings/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/headings/src/content/config.ts new file mode 100644 index 000000000..a142ace11 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings/src/content/config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; + +const docs = defineCollection({}); + +export const collections = { + docs, +}; diff --git a/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings-stale-cache-check.mdoc b/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings-stale-cache-check.mdoc new file mode 100644 index 000000000..75cd52884 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings-stale-cache-check.mdoc @@ -0,0 +1,13 @@ +Our heading ID generator can have a stale cache for duplicates. Let's check for those! + +# Level 1 heading + +## Level **2 heading** + +### Level _3 heading_ + +#### Level [4 heading](/with-a-link) + +##### Level 5 heading with override {% #id-override %} + +###### Level 6 heading diff --git a/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings-with-special-characters.mdoc b/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings-with-special-characters.mdoc new file mode 100644 index 000000000..2d1801014 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings-with-special-characters.mdoc @@ -0,0 +1,3 @@ +## `<Picture />` + +### « Sacrebleu ! » diff --git a/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings.mdoc b/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings.mdoc new file mode 100644 index 000000000..3eb66580a --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings.mdoc @@ -0,0 +1,11 @@ +# Level 1 heading + +## Level **2 heading** + +### Level _3 heading_ + +#### Level [4 heading](/with-a-link) + +##### Level 5 heading with override {% #id-override %} + +###### Level 6 heading diff --git a/packages/integrations/markdoc/test/fixtures/headings/src/pages/[slug].astro b/packages/integrations/markdoc/test/fixtures/headings/src/pages/[slug].astro new file mode 100644 index 000000000..90b021e95 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/headings/src/pages/[slug].astro @@ -0,0 +1,34 @@ +--- +import { CollectionEntry, getCollection } from "astro:content"; + +export async function getStaticPaths() { + const docs = await getCollection('docs'); + return docs.map(doc => ({ params: { slug: doc.slug }, props: doc })); +} + +type Props = CollectionEntry<'docs'>; + +const { Content, headings } = await Astro.props.render(); +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Content</title> +</head> +<body> + <nav data-toc> + <ul> + {headings.map(heading => ( + <li> + <a href={`#${heading.slug}`} data-depth={heading.depth}>{heading.text}</a> + </li> + ))} + </ul> + </nav> + <Content /> +</body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/image-assets/astro.config.mjs new file mode 100644 index 000000000..6be0918b8 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/astro.config.mjs @@ -0,0 +1,11 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; +import { testImageService } from '../../../../../astro/test/test-image-service.js'; + +// https://astro.build/config +export default defineConfig({ + image: { + service: testImageService(), + }, + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/markdoc.config.mjs b/packages/integrations/markdoc/test/fixtures/image-assets/markdoc.config.mjs new file mode 100644 index 000000000..04fe75244 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/markdoc.config.mjs @@ -0,0 +1,10 @@ +import { component, defineMarkdocConfig, nodes } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({ + tags: { + image: { + attributes: nodes.image.attributes, + render: component('./src/components/Image.astro'), + }, + }, +}); diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/package.json b/packages/integrations/markdoc/test/fixtures/image-assets/package.json new file mode 100644 index 000000000..30df52c2f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/image-assets", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/src/assets/alias/cityscape.jpg b/packages/integrations/markdoc/test/fixtures/image-assets/src/assets/alias/cityscape.jpg Binary files differnew file mode 100644 index 000000000..3fe6e5b64 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/src/assets/alias/cityscape.jpg diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/src/assets/relative/oar.jpg b/packages/integrations/markdoc/test/fixtures/image-assets/src/assets/relative/oar.jpg Binary files differnew file mode 100644 index 000000000..a63298b38 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/src/assets/relative/oar.jpg diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/src/components/Image.astro b/packages/integrations/markdoc/test/fixtures/image-assets/src/components/Image.astro new file mode 100644 index 000000000..e572c04d7 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/src/components/Image.astro @@ -0,0 +1,22 @@ +--- +import { Image } from 'astro:assets'; +// src/components/MyImage.astro +import type { ImageMetadata } from 'astro'; +type Props = { + src: string | ImageMetadata; + alt: string; +}; +const { src, alt } = Astro.props; +--- +{ + typeof src === 'string' ? ( + <img class="custom-styles" src={src} alt={alt} /> + ) : ( + <Image class="custom-styles" {src} {alt} /> + ) +} +<style> + .custom-styles { + border: 1px solid red; + } +</style> diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/image-assets/src/content/config.ts new file mode 100644 index 000000000..a142ace11 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/src/content/config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; + +const docs = defineCollection({}); + +export const collections = { + docs, +}; diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/src/content/docs/intro.mdoc b/packages/integrations/markdoc/test/fixtures/image-assets/src/content/docs/intro.mdoc new file mode 100644 index 000000000..f5ba3950f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/src/content/docs/intro.mdoc @@ -0,0 +1,9 @@ +# Image assets + + {% #public %} + + {% #relative %} + + {% #alias %} + +{% image src="../../assets/relative/oar.jpg" alt="oar" /%} {% #component %} diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/image-assets/src/pages/index.astro new file mode 100644 index 000000000..51810b4a8 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { getEntryBySlug } from 'astro:content'; + +const intro = await getEntryBySlug('docs', 'intro'); +const { Content } = await intro.render(); +--- + +<html lang="en"> + <head> + <meta charset="utf-8" /> + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> + <meta name="viewport" content="width=device-width" /> + <meta name="generator" content={Astro.generator} /> + <title>Astro</title> + </head> + <body> + <Content /> + </body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/src/public/favicon.svg b/packages/integrations/markdoc/test/fixtures/image-assets/src/public/favicon.svg new file mode 100644 index 000000000..f157bd1c5 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/src/public/favicon.svg @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128"> + <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" /> + <style> + path { fill: #000; } + @media (prefers-color-scheme: dark) { + path { fill: #FFF; } + } + </style> +</svg> diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/tsconfig.json b/packages/integrations/markdoc/test/fixtures/image-assets/tsconfig.json new file mode 100644 index 000000000..c193287fc --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "astro/tsconfigs/base", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "~/assets/*": ["src/assets/*"] + }, + }, + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +} diff --git a/packages/integrations/markdoc/test/fixtures/propagated-assets/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/propagated-assets/astro.config.mjs new file mode 100644 index 000000000..1bd8ba93f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/propagated-assets/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/propagated-assets/markdoc.config.mjs b/packages/integrations/markdoc/test/fixtures/propagated-assets/markdoc.config.mjs new file mode 100644 index 000000000..cd63c33be --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/propagated-assets/markdoc.config.mjs @@ -0,0 +1,16 @@ +import { component, defineMarkdocConfig } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({ + tags: { + aside: { + render: component('./src/components/Aside.astro'), + attributes: { + type: { type: String }, + title: { type: String }, + } + }, + logHello: { + render: component('./src/components/LogHello.astro'), + } + }, +}) diff --git a/packages/integrations/markdoc/test/fixtures/propagated-assets/package.json b/packages/integrations/markdoc/test/fixtures/propagated-assets/package.json new file mode 100644 index 000000000..3b51e158d --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/propagated-assets/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-propagated-assets", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/propagated-assets/src/components/Aside.astro b/packages/integrations/markdoc/test/fixtures/propagated-assets/src/components/Aside.astro new file mode 100644 index 000000000..be15e8c0a --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/propagated-assets/src/components/Aside.astro @@ -0,0 +1,116 @@ +--- +// Inspired by the `Aside` component from docs.astro.build +// https://github.com/withastro/starlight/blob/main/packages/starlight/integrations/asides.ts + +interface Props { + type?: 'note' | 'tip' | 'caution' | 'danger'; + title?: string; +} + +const labelByType = { + note: 'Note', + tip: 'Tip', + caution: 'Caution', + danger: 'Danger', +}; +const { type = 'note' } = Astro.props as Props; +const title = Astro.props.title ?? labelByType[type] ?? ''; + +// SVG icon paths based on GitHub Octicons +const icons: Record<NonNullable<Props['type']>, { viewBox: string; d: string }> = { + note: { + viewBox: '0 0 18 18', + d: 'M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75zM3.5 6.25a.75.75 0 01.75-.75h7a.75.75 0 010 1.5h-7a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h4a.75.75 0 000-1.5h-4z', + }, + tip: { + viewBox: '0 0 18 18', + d: 'M14 0a8.8 8.8 0 0 0-6 2.6l-.5.4-.9 1H3.3a1.8 1.8 0 0 0-1.5.8L.1 7.6a.8.8 0 0 0 .4 1.1l3.1 1 .2.1 2.4 2.4.1.2 1 3a.8.8 0 0 0 1 .5l2.9-1.7a1.8 1.8 0 0 0 .8-1.5V9.5l1-1 .4-.4A8.8 8.8 0 0 0 16 2v-.1A1.8 1.8 0 0 0 14.2 0h-.1zm-3.5 10.6-.3.2L8 12.3l.5 1.8 2-1.2a.3.3 0 0 0 .1-.2v-2zM3.7 8.1l1.5-2.3.2-.3h-2a.3.3 0 0 0-.3.1l-1.2 2 1.8.5zm5.2-4.5a7.3 7.3 0 0 1 5.2-2.1h.1a.3.3 0 0 1 .3.3v.1a7.3 7.3 0 0 1-2.1 5.2l-.5.4a15.2 15.2 0 0 1-2.5 2L7.1 11 5 9l1.5-2.3a15.3 15.3 0 0 1 2-2.5l.4-.5zM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-8.4 9.6a1.5 1.5 0 1 0-2.2-2.2 7 7 0 0 0-1.1 3 .2.2 0 0 0 .3.3c.6 0 2.2-.4 3-1.1z', + }, + caution: { + viewBox: '-1 1 18 18', + d: 'M8.9 1.5C8.7 1.2 8.4 1 8 1s-.7.2-.9.5l-7 12a1 1 0 0 0 0 1c.2.3.6.5 1 .5H15c.4 0 .7-.2.9-.5a1 1 0 0 0 0-1l-7-12zM9 13H7v-2h2v2zm0-3H7V6h2v4z', + }, + danger: { + viewBox: '0 1 14 17', + d: 'M5 .3c.9 2.2.5 3.4-.5 4.3C3.5 5.6 2 6.5 1 8c-1.5 2-1.7 6.5 3.5 7.7-2.2-1.2-2.6-4.5-.3-6.6-.6 2 .6 3.3 2 2.8 1.4-.4 2.3.6 2.2 1.7 0 .8-.3 1.4-1 1.8A5.6 5.6 0 0 0 12 10c0-2.9-2.5-3.3-1.3-5.7-1.5.2-2 1.2-1.8 2.8 0 1-1 1.8-2 1.3-.6-.4-.6-1.2 0-1.8C8.2 5.3 8.7 2.5 5 .3z', + }, +}; +const { viewBox, d } = icons[type]; +--- + +<aside class={`content ${type}`} aria-label={title}> + <p class="title" aria-hidden="true"> + <span class="icon"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox={viewBox} width={16} height={16}> + <path fill-rule="evenodd" d={d}></path> + </svg> + </span> + {title} + </p> + <section> + <slot /> + </section> +</aside> + +<style> + aside { + --color-base-purple: 269, 79%; + --color-base-teal: 180, 80%; + --color-base-red: 351, 100%; + --color-base-yellow: 41, 100%; + + --aside-color-base: var(--color-base-purple); + --aside-color-lightness: 54%; + --aside-accent-color: hsl(var(--aside-color-base), var(--aside-color-lightness)); + --aside-text-lightness: 20%; + --aside-text-accent-color: hsl(var(--aside-color-base), var(--aside-text-lightness)); + + border-inline-start: 4px solid var(--aside-accent-color); + padding: 1rem; + background-color: hsla(var(--aside-color-base), var(--aside-color-lightness), 0.1); + /* Indicates the aside boundaries for forced colors users, transparent is changed to a solid color */ + outline: 1px solid transparent; + } + + .title { + line-height: 1; + margin-bottom: 0.5rem; + font-size: 0.9rem; + letter-spacing: 0.05em; + font-weight: bold; + text-transform: uppercase; + color: var(--aside-text-accent-color); + } + + .icon svg { + width: 1.5em; + height: 1.5em; + vertical-align: middle; + fill: currentcolor; + } + + aside :global(a), + aside :global(a > code:not([class*='language'])) { + color: var(--aside-text-accent-color); + } + + aside :global(pre) { + margin-left: 0; + margin-right: 0; + } + + .tip { + --aside-color-lightness: 42%; + --aside-color-base: var(--color-base-teal); + } + + .caution { + --aside-color-lightness: 59%; + --aside-color-base: var(--color-base-yellow); + } + + .danger { + --aside-color-lightness: 54%; + --aside-color-base: var(--color-base-red); + } +</style> diff --git a/packages/integrations/markdoc/test/fixtures/propagated-assets/src/components/LogHello.astro b/packages/integrations/markdoc/test/fixtures/propagated-assets/src/components/LogHello.astro new file mode 100644 index 000000000..6d994378b --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/propagated-assets/src/components/LogHello.astro @@ -0,0 +1,5 @@ +<p>I'm gonna log hello</p> + +<script> + console.log('hello'); +</script> diff --git a/packages/integrations/markdoc/test/fixtures/propagated-assets/src/content/blog/scripts.mdoc b/packages/integrations/markdoc/test/fixtures/propagated-assets/src/content/blog/scripts.mdoc new file mode 100644 index 000000000..808cffaca --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/propagated-assets/src/content/blog/scripts.mdoc @@ -0,0 +1,5 @@ +--- +title: Scripts +--- + +{% logHello /%} diff --git a/packages/integrations/markdoc/test/fixtures/propagated-assets/src/content/blog/styles.mdoc b/packages/integrations/markdoc/test/fixtures/propagated-assets/src/content/blog/styles.mdoc new file mode 100644 index 000000000..20960e4cf --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/propagated-assets/src/content/blog/styles.mdoc @@ -0,0 +1,9 @@ +--- +title: Styles +--- + +{% aside type="tip" %} + +## Example + +{% /aside %} diff --git a/packages/integrations/markdoc/test/fixtures/propagated-assets/src/pages/[slug].astro b/packages/integrations/markdoc/test/fixtures/propagated-assets/src/pages/[slug].astro new file mode 100644 index 000000000..baee15375 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/propagated-assets/src/pages/[slug].astro @@ -0,0 +1,26 @@ +--- +import { getCollection } from 'astro:content'; + +export async function getStaticPaths() { + const posts = await getCollection('blog'); + return posts.map((post) => ({ + params: { slug: post.slug }, + props: post, + })); +} + +const { Content } = await Astro.props.render(); +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>{Astro.props.data.title}</title> +</head> +<body> + <Content /> +</body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/render with-space/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/render with-space/astro.config.mjs new file mode 100644 index 000000000..1bd8ba93f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render with-space/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/render with-space/package.json b/packages/integrations/markdoc/test/fixtures/render with-space/package.json new file mode 100644 index 000000000..daae65443 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render with-space/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-render-with-space", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/render with-space/src/content/blog/simple.mdoc b/packages/integrations/markdoc/test/fixtures/render with-space/src/content/blog/simple.mdoc new file mode 100644 index 000000000..2dbe492fe --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render with-space/src/content/blog/simple.mdoc @@ -0,0 +1,7 @@ +--- +title: Simple post with root folder containing a space +--- + +## Simple post with root folder containing a space + +This is a simple Markdoc post with root folder containing a space. diff --git a/packages/integrations/markdoc/test/fixtures/render with-space/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/render with-space/src/content/config.ts new file mode 100644 index 000000000..629486e48 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render with-space/src/content/config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; + +const blog = defineCollection({}); + +export const collections = { + blog, +}; diff --git a/packages/integrations/markdoc/test/fixtures/render with-space/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/render with-space/src/pages/index.astro new file mode 100644 index 000000000..940eef154 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render with-space/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { getEntryBySlug } from "astro:content"; + +const post = await getEntryBySlug('blog', 'simple'); +const { Content } = await post.render(); +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Content</title> +</head> +<body> + <Content /> +</body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/render-html/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/render-html/astro.config.mjs new file mode 100644 index 000000000..0872080fe --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc({ allowHTML: true })], +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-html/markdoc.config.ts b/packages/integrations/markdoc/test/fixtures/render-html/markdoc.config.ts new file mode 100644 index 000000000..bbc529f5a --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/markdoc.config.ts @@ -0,0 +1,19 @@ +import { component, defineMarkdocConfig } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({ + tags: { + aside: { + render: component('./src/components/Aside.astro'), + attributes: { + type: { type: String }, + title: { type: String }, + }, + }, + mark: { + render: component('./src/components/Mark.astro'), + attributes: { + color: { type: String }, + }, + }, + }, +}) diff --git a/packages/integrations/markdoc/test/fixtures/render-html/package.json b/packages/integrations/markdoc/test/fixtures/render-html/package.json new file mode 100644 index 000000000..4a0c67bfe --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-render-html", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/render-html/src/components/Aside.astro b/packages/integrations/markdoc/test/fixtures/render-html/src/components/Aside.astro new file mode 100644 index 000000000..ed7077dbd --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/src/components/Aside.astro @@ -0,0 +1,116 @@ +--- +// Inspired by the `Aside` component from docs.astro.build +// https://github.com/withastro/starlight/blob/main/packages/starlight/integrations/asides.ts + +export interface Props { + type?: 'note' | 'tip' | 'caution' | 'danger'; + title?: string; +} + +const labelByType = { + note: 'Note', + tip: 'Tip', + caution: 'Caution', + danger: 'Danger', +}; +const { type = 'note' } = Astro.props as Props; +const title = Astro.props.title ?? labelByType[type] ?? ''; + +// SVG icon paths based on GitHub Octicons +const icons: Record<NonNullable<Props['type']>, { viewBox: string; d: string }> = { + note: { + viewBox: '0 0 18 18', + d: 'M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75zM3.5 6.25a.75.75 0 01.75-.75h7a.75.75 0 010 1.5h-7a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h4a.75.75 0 000-1.5h-4z', + }, + tip: { + viewBox: '0 0 18 18', + d: 'M14 0a8.8 8.8 0 0 0-6 2.6l-.5.4-.9 1H3.3a1.8 1.8 0 0 0-1.5.8L.1 7.6a.8.8 0 0 0 .4 1.1l3.1 1 .2.1 2.4 2.4.1.2 1 3a.8.8 0 0 0 1 .5l2.9-1.7a1.8 1.8 0 0 0 .8-1.5V9.5l1-1 .4-.4A8.8 8.8 0 0 0 16 2v-.1A1.8 1.8 0 0 0 14.2 0h-.1zm-3.5 10.6-.3.2L8 12.3l.5 1.8 2-1.2a.3.3 0 0 0 .1-.2v-2zM3.7 8.1l1.5-2.3.2-.3h-2a.3.3 0 0 0-.3.1l-1.2 2 1.8.5zm5.2-4.5a7.3 7.3 0 0 1 5.2-2.1h.1a.3.3 0 0 1 .3.3v.1a7.3 7.3 0 0 1-2.1 5.2l-.5.4a15.2 15.2 0 0 1-2.5 2L7.1 11 5 9l1.5-2.3a15.3 15.3 0 0 1 2-2.5l.4-.5zM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-8.4 9.6a1.5 1.5 0 1 0-2.2-2.2 7 7 0 0 0-1.1 3 .2.2 0 0 0 .3.3c.6 0 2.2-.4 3-1.1z', + }, + caution: { + viewBox: '-1 1 18 18', + d: 'M8.9 1.5C8.7 1.2 8.4 1 8 1s-.7.2-.9.5l-7 12a1 1 0 0 0 0 1c.2.3.6.5 1 .5H15c.4 0 .7-.2.9-.5a1 1 0 0 0 0-1l-7-12zM9 13H7v-2h2v2zm0-3H7V6h2v4z', + }, + danger: { + viewBox: '0 1 14 17', + d: 'M5 .3c.9 2.2.5 3.4-.5 4.3C3.5 5.6 2 6.5 1 8c-1.5 2-1.7 6.5 3.5 7.7-2.2-1.2-2.6-4.5-.3-6.6-.6 2 .6 3.3 2 2.8 1.4-.4 2.3.6 2.2 1.7 0 .8-.3 1.4-1 1.8A5.6 5.6 0 0 0 12 10c0-2.9-2.5-3.3-1.3-5.7-1.5.2-2 1.2-1.8 2.8 0 1-1 1.8-2 1.3-.6-.4-.6-1.2 0-1.8C8.2 5.3 8.7 2.5 5 .3z', + }, +}; +const { viewBox, d } = icons[type]; +--- + +<aside class={`content ${type}`} aria-label={title}> + <p class="title" aria-hidden="true"> + <span class="icon"> + <svg xmlns="http://www.w3.org/2000/svg" viewBox={viewBox} width={16} height={16}> + <path fill-rule="evenodd" d={d}></path> + </svg> + </span> + {title} + </p> + <section> + <slot /> + </section> +</aside> + +<style> + aside { + --color-base-purple: 269, 79%; + --color-base-teal: 180, 80%; + --color-base-red: 351, 100%; + --color-base-yellow: 41, 100%; + + --aside-color-base: var(--color-base-purple); + --aside-color-lightness: 54%; + --aside-accent-color: hsl(var(--aside-color-base), var(--aside-color-lightness)); + --aside-text-lightness: 20%; + --aside-text-accent-color: hsl(var(--aside-color-base), var(--aside-text-lightness)); + + border-inline-start: 4px solid var(--aside-accent-color); + padding: 1rem; + background-color: hsla(var(--aside-color-base), var(--aside-color-lightness), 0.1); + /* Indicates the aside boundaries for forced colors users, transparent is changed to a solid color */ + outline: 1px solid transparent; + } + + .title { + line-height: 1; + margin-bottom: 0.5rem; + font-size: 0.9rem; + letter-spacing: 0.05em; + font-weight: bold; + text-transform: uppercase; + color: var(--aside-text-accent-color); + } + + .icon svg { + width: 1.5em; + height: 1.5em; + vertical-align: middle; + fill: currentcolor; + } + + aside :global(a), + aside :global(a > code:not([class*='language'])) { + color: var(--aside-text-accent-color); + } + + aside :global(pre) { + margin-left: 0; + margin-right: 0; + } + + .tip { + --aside-color-lightness: 42%; + --aside-color-base: var(--color-base-teal); + } + + .caution { + --aside-color-lightness: 59%; + --aside-color-base: var(--color-base-yellow); + } + + .danger { + --aside-color-lightness: 54%; + --aside-color-base: var(--color-base-red); + } +</style> diff --git a/packages/integrations/markdoc/test/fixtures/render-html/src/components/Mark.astro b/packages/integrations/markdoc/test/fixtures/render-html/src/components/Mark.astro new file mode 100644 index 000000000..7d0b6c9fb --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/src/components/Mark.astro @@ -0,0 +1,11 @@ +--- + +export interface Props { + color?: 'hotpink' | 'blue'; +} + +const { color } = Astro.props; + +--- + +<span style={`color: ${color}`} class="mark"><slot /></span> diff --git a/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/_partial.mdoc b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/_partial.mdoc new file mode 100644 index 000000000..f8774e911 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/_partial.mdoc @@ -0,0 +1,5 @@ +## HTML in a partial + +<ul> + <li id="partial">List item</li> +</ul> diff --git a/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/components.mdoc b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/components.mdoc new file mode 100644 index 000000000..55890ce09 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/components.mdoc @@ -0,0 +1,47 @@ +--- +title: Welcome to Markdoc 👋 +--- + +This is a {% mark color="hotpink" %}inline mark{% /mark %} in regular Markdown markup. + +<p id="p1">This is a {% mark color="hotpink" %}inline mark{% /mark %} under some HTML</p> + +<div id="div1"> + <p id="div1-p1">This is a {% mark color="hotpink" %}inline mark{% /mark %} under some HTML</p> + <p id="div1-p2">This is a <span id="div1-p2-span1">{% mark color="hotpink" %}inline mark{% /mark %}</span> under some HTML</p> +</div> + +{% aside title="Aside One" type="tip" %} + +I'm a Markdown paragraph inside an top-level aside tag + +## I'm an H2 via Markdown markup + +<h2 id="h-two">I'm an H2 via HTML markup</h2> + +**Markdown bold** vs <strong>HTML bold</strong> + +RENDERED + +{% if $revealSecret %} +NOT RENDERED +{% /if %} + +{% if $revealSecret %}NOT RENDERED{% /if %} + +{% /aside %} + +<section id="section1"> + <div id="div1"> +{% aside title="Nested un-indented Aside" type="tip" %} +regular Markdown markup +<p id="p4">nested {% mark color="hotpink" %}inline mark{% /mark %} content</p> +{% /aside %} + </div> + <div id="div2"> + {% aside title="Nested indented Aside 💀" type="tip" %} + regular Markdown markup + <p id="p5">nested {% mark color="hotpink" %}inline<span id="inception-span"> mark</span>{% /mark %} content</p> + {% /aside %} + </div> +</section> diff --git a/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/nested-html.mdoc b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/nested-html.mdoc new file mode 100644 index 000000000..161d128bf --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/nested-html.mdoc @@ -0,0 +1,18 @@ +--- +title: Simple post +--- +<p id="p1">before <span class="inner-class" id="inner1" style="color: hotpink;">inner</span> after</p> +<p id="p2"> + before + <span class="inner-class" id="inner1" style="color: hotpink;">inner</span> + after +</p> +<div id="div-l1"> + <div id="div-l2-1"> + <p id="p3">before <span class="inner-class" id="inner1" style="color: hotpink;">inner</span> after</p> + </div> + <div id="div-l2-2"> + <p id="p4">before <span class="inner-class" id="inner1" style="color: hotpink;">inner</span> after</p> + <p id="p5">before <span class="inner-class" id="inner1" style="color: hotpink;">inner</span> after</p> + </div> +</div>
\ No newline at end of file diff --git a/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/randomly-cased-html-attributes.mdoc b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/randomly-cased-html-attributes.mdoc new file mode 100644 index 000000000..18c62bd59 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/randomly-cased-html-attributes.mdoc @@ -0,0 +1,20 @@ +--- +title: Simple post +--- + +<table> + <thead> + <tr> + <th>one</th> + <th>two</th> + <th>three</th> + </tr> + </thead> + <tbody> + <tr><td id="td1" rowspan="2" colspan="3">three wide and two tall</td></tr> + <tr><td id="td2" ROWSPAN="2" COLSPAN="3">three wide and two tall</td></tr> + <tr><td id="td3" rowSpan="2" colSpan="3">three wide and two tall</td></tr> + <tr><td id="td4" RowSpan="2" ColSpan="3">three wide and two tall</td></tr> + </tr> + </tbody> +</table> diff --git a/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/simple.mdoc b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/simple.mdoc new file mode 100644 index 000000000..30b9b7f8f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/simple.mdoc @@ -0,0 +1,15 @@ +--- +title: Simple post +--- + +## Simple <span class="inside-h2" style="color: fuscia">post</span> header + +This is a simple Markdoc <span class="post-class" style="color: hotpink;">post</span>. + +<p>This is a paragraph!</p> + +<p>This is a <span class="inside-p">span</span> inside a paragraph!</p> + +<video + src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExejZnbmN6ODlxOWk2djVmcnFkMjIwbmFnZGFtZ2J4aG52dzVvbjJlaCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/H1vJZzQAiILUUq0FUL/giphy.mp4" + autoplay muted></video>
\ No newline at end of file diff --git a/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/with-partial.mdoc b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/with-partial.mdoc new file mode 100644 index 000000000..c42d3cd70 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/src/content/blog/with-partial.mdoc @@ -0,0 +1,5 @@ +--- +title: With Partial +--- + +{% partial file="./_partial.mdoc" /%} diff --git a/packages/integrations/markdoc/test/fixtures/render-html/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/render-html/src/content/config.ts new file mode 100644 index 000000000..629486e48 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/src/content/config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; + +const blog = defineCollection({}); + +export const collections = { + blog, +}; diff --git a/packages/integrations/markdoc/test/fixtures/render-html/src/pages/[slug].astro b/packages/integrations/markdoc/test/fixtures/render-html/src/pages/[slug].astro new file mode 100644 index 000000000..bea51d3b5 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-html/src/pages/[slug].astro @@ -0,0 +1,29 @@ +--- +import { getCollection, getEntryBySlug } from "astro:content"; + +const { slug } = Astro.params; + +const post = await getEntryBySlug('blog', slug); +const { Content } = await post.render(); + +export async function getStaticPaths() { + const blogEntries = await getCollection('blog'); + return blogEntries.map(entry => ({ + params: { slug: entry.slug }, props: { entry }, + })); +} + +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Content</title> +</head> +<body> + <Content /> +</body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/render-null/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/render-null/astro.config.mjs new file mode 100644 index 000000000..1bd8ba93f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-null/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-null/markdoc.config.mjs b/packages/integrations/markdoc/test/fixtures/render-null/markdoc.config.mjs new file mode 100644 index 000000000..5db65fddd --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-null/markdoc.config.mjs @@ -0,0 +1,15 @@ +import { defineMarkdocConfig, nodes, component } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({ + nodes: { + document: { + ...nodes.document, + render: null, + }, + }, + tags: { + 'div-wrapper': { + render: component('./src/components/DivWrapper.astro'), + }, + }, +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-null/package.json b/packages/integrations/markdoc/test/fixtures/render-null/package.json new file mode 100644 index 000000000..e9529b693 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-null/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-render-null", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/render-null/src/components/DivWrapper.astro b/packages/integrations/markdoc/test/fixtures/render-null/src/components/DivWrapper.astro new file mode 100644 index 000000000..942a11945 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-null/src/components/DivWrapper.astro @@ -0,0 +1 @@ +<div class="div-wrapper"><slot /></div> diff --git a/packages/integrations/markdoc/test/fixtures/render-null/src/content/blog/render-null.mdoc b/packages/integrations/markdoc/test/fixtures/render-null/src/content/blog/render-null.mdoc new file mode 100644 index 000000000..f85ebebd1 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-null/src/content/blog/render-null.mdoc @@ -0,0 +1,13 @@ +--- +title: Post with render null +--- + +## Post with render null + +This should render the contents inside a fragment! + +{% div-wrapper %} + +I'm inside a div wrapper + +{% /div-wrapper %} diff --git a/packages/integrations/markdoc/test/fixtures/render-null/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/render-null/src/content/config.ts new file mode 100644 index 000000000..629486e48 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-null/src/content/config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; + +const blog = defineCollection({}); + +export const collections = { + blog, +}; diff --git a/packages/integrations/markdoc/test/fixtures/render-null/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/render-null/src/pages/index.astro new file mode 100644 index 000000000..ed8417c5b --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-null/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { getEntryBySlug } from "astro:content"; + +const post = await getEntryBySlug('blog', 'render-null'); +const { Content } = await post.render(); +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Content</title> +</head> +<body> + <Content /> +</body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/render-partials/astro.config.mjs new file mode 100644 index 000000000..1bd8ba93f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/markdoc.config.ts b/packages/integrations/markdoc/test/fixtures/render-partials/markdoc.config.ts new file mode 100644 index 000000000..c9762aed5 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/markdoc.config.ts @@ -0,0 +1,7 @@ +import { Markdoc, defineMarkdocConfig } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({ + partials: { + configured: Markdoc.parse('# Configured partial {% #configured %}'), + }, +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/package.json b/packages/integrations/markdoc/test/fixtures/render-partials/package.json new file mode 100644 index 000000000..021e1c2d9 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-render-partials", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/_partial.mdoc b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/_partial.mdoc new file mode 100644 index 000000000..4ace9a9d3 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/_partial.mdoc @@ -0,0 +1,3 @@ +## Partial {% #top %} + +{% partial file="../nested/_partial.mdoc" /%} diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/with-partials.mdoc b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/with-partials.mdoc new file mode 100644 index 000000000..2d9a87110 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/blog/with-partials.mdoc @@ -0,0 +1,7 @@ +--- +title: Post with partials +--- + +{% partial file="_partial.mdoc" /%} + +{% partial file="configured" /%} diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/config.ts new file mode 100644 index 000000000..629486e48 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; + +const blog = defineCollection({}); + +export const collections = { + blog, +}; diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/src/content/nested/_partial.mdoc b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/nested/_partial.mdoc new file mode 100644 index 000000000..4193609bf --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/src/content/nested/_partial.mdoc @@ -0,0 +1 @@ +## Nested partial {% #nested %} diff --git a/packages/integrations/markdoc/test/fixtures/render-partials/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/render-partials/src/pages/index.astro new file mode 100644 index 000000000..e9549f314 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-partials/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { getEntryBySlug } from 'astro:content'; + +const post = await getEntryBySlug('blog', 'with-partials'); +const { Content } = await post.render(); +--- + +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Content</title> + </head> + <body> + <Content /> + </body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/render-simple/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/render-simple/astro.config.mjs new file mode 100644 index 000000000..1bd8ba93f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-simple/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-simple/package.json b/packages/integrations/markdoc/test/fixtures/render-simple/package.json new file mode 100644 index 000000000..9354cdc58 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-simple/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-render-simple", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/render-simple/src/content/blog/simple.mdoc b/packages/integrations/markdoc/test/fixtures/render-simple/src/content/blog/simple.mdoc new file mode 100644 index 000000000..557f7b8e5 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-simple/src/content/blog/simple.mdoc @@ -0,0 +1,7 @@ +--- +title: Simple post +--- + +## Simple post + +This is a simple Markdoc post. diff --git a/packages/integrations/markdoc/test/fixtures/render-simple/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/render-simple/src/content/config.ts new file mode 100644 index 000000000..629486e48 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-simple/src/content/config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; + +const blog = defineCollection({}); + +export const collections = { + blog, +}; diff --git a/packages/integrations/markdoc/test/fixtures/render-simple/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/render-simple/src/pages/index.astro new file mode 100644 index 000000000..940eef154 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-simple/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { getEntryBySlug } from "astro:content"; + +const post = await getEntryBySlug('blog', 'simple'); +const { Content } = await post.render(); +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Content</title> +</head> +<body> + <Content /> +</body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/render-typographer/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/render-typographer/astro.config.mjs new file mode 100644 index 000000000..408e036c7 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-typographer/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc({ typographer: true })], +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-typographer/package.json b/packages/integrations/markdoc/test/fixtures/render-typographer/package.json new file mode 100644 index 000000000..02fd6788f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-typographer/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-render-typographer", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/render-typographer/src/content/blog/typographer.mdoc b/packages/integrations/markdoc/test/fixtures/render-typographer/src/content/blog/typographer.mdoc new file mode 100644 index 000000000..2180e7a47 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-typographer/src/content/blog/typographer.mdoc @@ -0,0 +1,7 @@ +--- +title: Typographer +--- + +## Typographer's post + +This is a post to test the "typographer" option. diff --git a/packages/integrations/markdoc/test/fixtures/render-typographer/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/render-typographer/src/content/config.ts new file mode 100644 index 000000000..629486e48 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-typographer/src/content/config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; + +const blog = defineCollection({}); + +export const collections = { + blog, +}; diff --git a/packages/integrations/markdoc/test/fixtures/render-typographer/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/render-typographer/src/pages/index.astro new file mode 100644 index 000000000..88fc531fa --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-typographer/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { getEntryBySlug } from "astro:content"; + +const post = await getEntryBySlug('blog', 'typographer'); +const { Content } = await post.render(); +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Content</title> +</head> +<body> + <Content /> +</body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/render-with-components/astro.config.mjs new file mode 100644 index 000000000..3023497aa --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/astro.config.mjs @@ -0,0 +1,8 @@ +import markdoc from '@astrojs/markdoc'; +import preact from '@astrojs/preact'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc(), preact()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/markdoc.config.ts b/packages/integrations/markdoc/test/fixtures/render-with-components/markdoc.config.ts new file mode 100644 index 000000000..6093ec593 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/markdoc.config.ts @@ -0,0 +1,32 @@ +import { Markdoc, component, defineMarkdocConfig } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({ + nodes: { + fence: { + render: component('./src/components/Code.astro'), + attributes: { + language: { type: String }, + content: { type: String }, + }, + }, + }, + tags: { + 'marquee-element': { + render: component('./src/components/CustomMarquee.astro'), + attributes: { + direction: { + type: String, + default: 'left', + matches: ['left', 'right', 'up', 'down'], + errorLevel: 'critical', + }, + }, + }, + counter: { + render: component('./src/components/CounterWrapper.astro'), + }, + 'deeply-nested': { + render: component('./src/components/DeeplyNested.astro'), + }, + }, +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/package.json b/packages/integrations/markdoc/test/fixtures/render-with-components/package.json new file mode 100644 index 000000000..c8cadbc37 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/package.json @@ -0,0 +1,11 @@ +{ + "name": "@test/markdoc-render-with-components", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "@astrojs/preact": "workspace:*", + "astro": "workspace:*", + "preact": "^10.26.5" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/Code.astro b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/Code.astro new file mode 100644 index 000000000..18bf1399f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/Code.astro @@ -0,0 +1,12 @@ +--- +import { Code } from 'astro/components'; + +type Props = { + content: string; + language: string; +} + +const { content, language } = Astro.props as Props; +--- + +<Code lang={language} code={content} /> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/Counter.tsx b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/Counter.tsx new file mode 100644 index 000000000..f1e239718 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/Counter.tsx @@ -0,0 +1,10 @@ +import { useState } from 'preact/hooks'; + +export default function Counter() { + const [count, setCount] = useState(1); + return ( + <button id="counter" onClick={() => setCount(count + 1)}> + {count} + </button> + ); +} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/CounterWrapper.astro b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/CounterWrapper.astro new file mode 100644 index 000000000..e45ac6438 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/CounterWrapper.astro @@ -0,0 +1,5 @@ +--- +import Counter from './Counter'; +--- + +<Counter client:load /> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/CustomMarquee.astro b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/CustomMarquee.astro new file mode 100644 index 000000000..3108b9973 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/CustomMarquee.astro @@ -0,0 +1 @@ +<marquee data-custom-marquee {...Astro.props}><slot /></marquee> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/DeeplyNested.astro b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/DeeplyNested.astro new file mode 100644 index 000000000..eb23f675a --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/components/DeeplyNested.astro @@ -0,0 +1,5 @@ +--- + +--- + +<p id="deeply-nested">Deeply nested partial</p> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/_nested.mdoc b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/_nested.mdoc new file mode 100644 index 000000000..68f529280 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/_nested.mdoc @@ -0,0 +1,3 @@ +Render components from a deeply nested partial: + +{% deeply-nested /%} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/blog/_counter.mdoc b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/blog/_counter.mdoc new file mode 100644 index 000000000..4a015695c --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/blog/_counter.mdoc @@ -0,0 +1,7 @@ +# Hello from a partial! + +Render a component from a partial: + +{% counter /%} + +{% partial file="../_nested.mdoc" /%} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/blog/with-components.mdoc b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/blog/with-components.mdoc new file mode 100644 index 000000000..0d1ec835c --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/blog/with-components.mdoc @@ -0,0 +1,28 @@ +--- +title: Post with components +--- + +## Post with components + +This uses a custom marquee component with a shortcode: + +{% marquee-element direction="right" %} +I'm a marquee too! +{% /marquee-element %} + +{% partial file="_counter.mdoc" /%} + +And a code component for code blocks: + +```js +const isRenderedWithShiki = true; +``` + +{% if equals("true", "true") %} +Inside truthy + +```js +const isRenderedWithShikiInside = true; +``` + +{% /if %} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/config.ts new file mode 100644 index 000000000..629486e48 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/content/config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; + +const blog = defineCollection({}); + +export const collections = { + blog, +}; diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/render-with-components/src/pages/index.astro new file mode 100644 index 000000000..52239acce --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { getEntryBySlug } from "astro:content"; + +const post = await getEntryBySlug('blog', 'with-components'); +const { Content } = await post.render(); +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Content</title> +</head> +<body> + <Content /> +</body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-components/tsconfig.json b/packages/integrations/markdoc/test/fixtures/render-with-components/tsconfig.json new file mode 100644 index 000000000..f993eddf6 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-components/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "astro/tsconfigs/base", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact" + }, + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +}
\ No newline at end of file diff --git a/packages/integrations/markdoc/test/fixtures/render-with-config/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/render-with-config/astro.config.mjs new file mode 100644 index 000000000..1bd8ba93f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-config/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-with-config/markdoc.config.ts b/packages/integrations/markdoc/test/fixtures/render-with-config/markdoc.config.ts new file mode 100644 index 000000000..c43ee93a3 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-config/markdoc.config.ts @@ -0,0 +1,15 @@ +import { defineMarkdocConfig } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({ + variables: { + countries: ['ES', 'JP'], + }, + functions: { + includes: { + transform(parameters) { + const [array, value] = Object.values(parameters); + return Array.isArray(array) ? array.includes(value) : false; + }, + }, + }, +}) diff --git a/packages/integrations/markdoc/test/fixtures/render-with-config/package.json b/packages/integrations/markdoc/test/fixtures/render-with-config/package.json new file mode 100644 index 000000000..d4751388c --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-config/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-render-with-config", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-config/src/content/blog/with-config.mdoc b/packages/integrations/markdoc/test/fixtures/render-with-config/src/content/blog/with-config.mdoc new file mode 100644 index 000000000..5376404ea --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-config/src/content/blog/with-config.mdoc @@ -0,0 +1,13 @@ +--- +title: Post with config +--- + +## Post with config + +{% if includes($countries, "EN") %} Hello {% /if %} +{% if includes($countries, "ES") %} Hola {% /if %} +{% if includes($countries, "JP") %} Konnichiwa {% /if %} + +## Runtime variables + +{% $runtimeVariable %} {% #runtime-variable %} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-config/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/render-with-config/src/content/config.ts new file mode 100644 index 000000000..629486e48 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-config/src/content/config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; + +const blog = defineCollection({}); + +export const collections = { + blog, +}; diff --git a/packages/integrations/markdoc/test/fixtures/render-with-config/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/render-with-config/src/pages/index.astro new file mode 100644 index 000000000..616d5ec0a --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-config/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { getEntryBySlug } from "astro:content"; + +const post = await getEntryBySlug('blog', 'with-config'); +const { Content } = await post.render(); +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Content</title> +</head> +<body> + <Content runtimeVariable="working!" /> +</body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-extends-components/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/astro.config.mjs new file mode 100644 index 000000000..1bd8ba93f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-with-extends-components/markdoc.config.ts b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/markdoc.config.ts new file mode 100644 index 000000000..8daba3746 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/markdoc.config.ts @@ -0,0 +1,31 @@ +import { component, defineMarkdocConfig } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({ + extends: [preset()], +}); + +function preset() { + return { + nodes: { + fence: { + render: component('./src/components/Code.astro'), + attributes: { + language: { type: String }, + content: { type: String }, + }, + }, + }, + tags: { + 'marquee-element': { + render: component('./src/components/CustomMarquee.astro'), + attributes: { + direction: { + type: String, + default: 'left', + matches: ['left', 'right', 'up', 'down'], + }, + }, + }, + }, + } +} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-extends-components/package.json b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/package.json new file mode 100644 index 000000000..962a2b8a7 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-render-with-extends-components", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/components/Code.astro b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/components/Code.astro new file mode 100644 index 000000000..18bf1399f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/components/Code.astro @@ -0,0 +1,12 @@ +--- +import { Code } from 'astro/components'; + +type Props = { + content: string; + language: string; +} + +const { content, language } = Astro.props as Props; +--- + +<Code lang={language} code={content} /> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/components/CustomMarquee.astro b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/components/CustomMarquee.astro new file mode 100644 index 000000000..3108b9973 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/components/CustomMarquee.astro @@ -0,0 +1 @@ +<marquee data-custom-marquee {...Astro.props}><slot /></marquee> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/content/blog/with-components.mdoc b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/content/blog/with-components.mdoc new file mode 100644 index 000000000..8ff944b06 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/content/blog/with-components.mdoc @@ -0,0 +1,26 @@ +--- +title: Post with components +--- + +## Post with components + +This uses a custom marquee component with a shortcode: + +{% marquee-element direction="right" %} +I'm a marquee too! +{% /marquee-element %} + +And a code component for code blocks: + +```js +const isRenderedWithShiki = true; +``` + +{% if equals("true", "true") %} +Inside truthy + +```js +const isRenderedWithShikiInside = true; +``` + +{% /if %} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/content/config.ts new file mode 100644 index 000000000..629486e48 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/content/config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; + +const blog = defineCollection({}); + +export const collections = { + blog, +}; diff --git a/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/pages/index.astro new file mode 100644 index 000000000..52239acce --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-extends-components/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { getEntryBySlug } from "astro:content"; + +const post = await getEntryBySlug('blog', 'with-components'); +const { Content } = await post.render(); +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Content</title> +</head> +<body> + <Content /> +</body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-indented-components/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/astro.config.mjs new file mode 100644 index 000000000..e690d7757 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc({ ignoreIndentation: true })], +}); diff --git a/packages/integrations/markdoc/test/fixtures/render-with-indented-components/markdoc.config.ts b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/markdoc.config.ts new file mode 100644 index 000000000..2016327a8 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/markdoc.config.ts @@ -0,0 +1,26 @@ +import { component, defineMarkdocConfig } from '@astrojs/markdoc/config'; + +export default defineMarkdocConfig({ + nodes: { + fence: { + render: component('./src/components/Code.astro'), + attributes: { + language: { type: String }, + content: { type: String }, + }, + }, + }, + tags: { + 'marquee-element': { + render: component('./src/components/CustomMarquee.astro'), + attributes: { + direction: { + type: String, + default: 'left', + matches: ['left', 'right', 'up', 'down'], + errorLevel: 'critical', + }, + }, + }, + }, +}) diff --git a/packages/integrations/markdoc/test/fixtures/render-with-indented-components/package.json b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/package.json new file mode 100644 index 000000000..3c1d0a7b5 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-render-with-indented-components", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/components/Code.astro b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/components/Code.astro new file mode 100644 index 000000000..18bf1399f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/components/Code.astro @@ -0,0 +1,12 @@ +--- +import { Code } from 'astro/components'; + +type Props = { + content: string; + language: string; +} + +const { content, language } = Astro.props as Props; +--- + +<Code lang={language} code={content} /> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/components/CustomMarquee.astro b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/components/CustomMarquee.astro new file mode 100644 index 000000000..3108b9973 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/components/CustomMarquee.astro @@ -0,0 +1 @@ +<marquee data-custom-marquee {...Astro.props}><slot /></marquee> diff --git a/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/content/blog/with-indented-components.mdoc b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/content/blog/with-indented-components.mdoc new file mode 100644 index 000000000..a52b35fc4 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/content/blog/with-indented-components.mdoc @@ -0,0 +1,24 @@ +--- +title: Post with indented components +--- + +## Post with indented components + +This uses a custom marquee component with a shortcode: + +{% marquee-element direction="right" %} + I'm a marquee too! + + {% marquee-element direction="right" %} + I'm an indented marquee! + + ### I am an h3! + {% /marquee-element %} + + And a nested code block: + + ```js + const isRenderedWithShiki = true; + ``` +{% /marquee-element %} + diff --git a/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/content/config.ts new file mode 100644 index 000000000..629486e48 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/content/config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; + +const blog = defineCollection({}); + +export const collections = { + blog, +}; diff --git a/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/pages/index.astro new file mode 100644 index 000000000..0ae7ed4c9 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/render-with-indented-components/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { getEntryBySlug } from "astro:content"; + +const post = await getEntryBySlug('blog', 'with-indented-components'); +const { Content } = await post.render(); +--- + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Content</title> +</head> +<body> + <Content /> +</body> +</html> diff --git a/packages/integrations/markdoc/test/fixtures/variables/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/variables/astro.config.mjs new file mode 100644 index 000000000..1bd8ba93f --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/variables/astro.config.mjs @@ -0,0 +1,7 @@ +import markdoc from '@astrojs/markdoc'; +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/variables/package.json b/packages/integrations/markdoc/test/fixtures/variables/package.json new file mode 100644 index 000000000..0ac7a3c82 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/variables/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/markdoc-variables", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/variables/src/content/blog/entry.mdoc b/packages/integrations/markdoc/test/fixtures/variables/src/content/blog/entry.mdoc new file mode 100644 index 000000000..151d5a81d --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/variables/src/content/blog/entry.mdoc @@ -0,0 +1,9 @@ +--- +title: Test entry +--- + +# {% $entry.data.title %} + +- id: {% $entry.id %} {% #id %} +- slug: {% $entry.slug %} {% #slug %} +- collection: {% $entry.collection %} {% #collection %} diff --git a/packages/integrations/markdoc/test/fixtures/variables/src/content/config.ts b/packages/integrations/markdoc/test/fixtures/variables/src/content/config.ts new file mode 100644 index 000000000..ff473d4af --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/variables/src/content/config.ts @@ -0,0 +1,9 @@ +import { defineCollection, z } from 'astro:content'; + +const blog = defineCollection({ + schema: z.object({ + title: z.string().transform(v => 'Processed by schema: ' + v), + }), +}); + +export const collections = { blog } diff --git a/packages/integrations/markdoc/test/fixtures/variables/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/variables/src/pages/index.astro new file mode 100644 index 000000000..a2766faf0 --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/variables/src/pages/index.astro @@ -0,0 +1,19 @@ +--- +import { getEntryBySlug } from 'astro:content'; + +const entry = await getEntryBySlug('blog', 'entry'); +const { Content } = await entry.render(); +--- + +<html lang="en"> + <head> + <meta charset="utf-8" /> + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> + <meta name="viewport" content="width=device-width" /> + <meta name="generator" content={Astro.generator} /> + <title>Astro</title> + </head> + <body> + <Content {entry} /> + </body> +</html> diff --git a/packages/integrations/markdoc/test/headings.test.js b/packages/integrations/markdoc/test/headings.test.js new file mode 100644 index 000000000..1a6061aa6 --- /dev/null +++ b/packages/integrations/markdoc/test/headings.test.js @@ -0,0 +1,264 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { parseHTML } from 'linkedom'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +async function getFixture(name) { + return await loadFixture({ + root: new URL(`./fixtures/${name}/`, import.meta.url), + }); +} + +describe('experimental.headingIdCompat', () => { + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: new URL(`./fixtures/headings/`, import.meta.url), + experimental: { headingIdCompat: true }, + }); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('applies IDs to headings containing special characters', async () => { + const res = await fixture.fetch('/headings-with-special-characters'); + const html = await res.text(); + const { document } = parseHTML(html); + + assert.equal(document.querySelector('h2')?.id, 'picture-'); + assert.equal(document.querySelector('h3')?.id, '-sacrebleu--'); + }); + }); +}); + +describe('Markdoc - Headings', () => { + let fixture; + + before(async () => { + fixture = await getFixture('headings'); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('applies IDs to headings', async () => { + const res = await fixture.fetch('/headings'); + const html = await res.text(); + const { document } = parseHTML(html); + + idTest(document); + }); + + it('applies IDs to headings containing special characters', async () => { + const res = await fixture.fetch('/headings-with-special-characters'); + const html = await res.text(); + const { document } = parseHTML(html); + + assert.equal(document.querySelector('h2')?.id, 'picture'); + assert.equal(document.querySelector('h3')?.id, '-sacrebleu-'); + }); + + it('generates the same IDs for other documents with the same headings', async () => { + const res = await fixture.fetch('/headings-stale-cache-check'); + const html = await res.text(); + const { document } = parseHTML(html); + + idTest(document); + }); + + it('generates a TOC with correct info', async () => { + const res = await fixture.fetch('/headings'); + const html = await res.text(); + const { document } = parseHTML(html); + + tocTest(document); + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + it('applies IDs to headings', async () => { + const html = await fixture.readFile('/headings/index.html'); + const { document } = parseHTML(html); + + idTest(document); + }); + + it('generates the same IDs for other documents with the same headings', async () => { + const html = await fixture.readFile('/headings-stale-cache-check/index.html'); + const { document } = parseHTML(html); + + idTest(document); + }); + + it('generates a TOC with correct info', async () => { + const html = await fixture.readFile('/headings/index.html'); + const { document } = parseHTML(html); + + tocTest(document); + }); + }); +}); + +describe('Markdoc - Headings with custom Astro renderer', () => { + let fixture; + + before(async () => { + fixture = await getFixture('headings-custom'); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('applies IDs to headings', async () => { + const res = await fixture.fetch('/headings'); + const html = await res.text(); + const { document } = parseHTML(html); + + idTest(document); + }); + + it('generates the same IDs for other documents with the same headings', async () => { + const res = await fixture.fetch('/headings-stale-cache-check'); + const html = await res.text(); + const { document } = parseHTML(html); + + idTest(document); + }); + + it('generates a TOC with correct info', async () => { + const res = await fixture.fetch('/headings'); + const html = await res.text(); + const { document } = parseHTML(html); + + tocTest(document); + }); + + it('renders Astro component for each heading', async () => { + const res = await fixture.fetch('/headings'); + const html = await res.text(); + const { document } = parseHTML(html); + + astroComponentTest(document); + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + it('applies IDs to headings', async () => { + const html = await fixture.readFile('/headings/index.html'); + const { document } = parseHTML(html); + + idTest(document); + }); + + it('generates the same IDs for other documents with the same headings', async () => { + const html = await fixture.readFile('/headings-stale-cache-check/index.html'); + const { document } = parseHTML(html); + + idTest(document); + }); + + it('generates a TOC with correct info', async () => { + const html = await fixture.readFile('/headings/index.html'); + const { document } = parseHTML(html); + + tocTest(document); + }); + + it('renders Astro component for each heading', async () => { + const html = await fixture.readFile('/headings/index.html'); + const { document } = parseHTML(html); + + astroComponentTest(document); + }); + }); +}); + +const depthToHeadingMap = { + 1: { + slug: 'level-1-heading', + text: 'Level 1 heading', + }, + 2: { + slug: 'level-2-heading', + text: 'Level 2 heading', + }, + 3: { + slug: 'level-3-heading', + text: 'Level 3 heading', + }, + 4: { + slug: 'level-4-heading', + text: 'Level 4 heading', + }, + 5: { + slug: 'id-override', + text: 'Level 5 heading with override', + }, + 6: { + slug: 'level-6-heading', + text: 'Level 6 heading', + }, +}; + +/** @param {Document} document */ +function idTest(document) { + for (const [depth, info] of Object.entries(depthToHeadingMap)) { + assert.equal(document.querySelector(`h${depth}`)?.getAttribute('id'), info.slug); + } +} + +/** @param {Document} document */ +function tocTest(document) { + const toc = document.querySelector('[data-toc] > ul'); + assert.equal(toc.children.length, Object.keys(depthToHeadingMap).length); + + for (const [depth, info] of Object.entries(depthToHeadingMap)) { + const linkEl = toc.querySelector(`a[href="#${info.slug}"]`); + assert.ok(linkEl); + assert.equal(linkEl.getAttribute('data-depth'), depth); + assert.equal(linkEl.textContent.trim(), info.text); + } +} + +/** @param {Document} document */ +function astroComponentTest(document) { + const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); + + for (const heading of headings) { + assert.equal(heading.hasAttribute('data-custom-heading'), true); + } +} diff --git a/packages/integrations/markdoc/test/image-assets.test.js b/packages/integrations/markdoc/test/image-assets.test.js new file mode 100644 index 000000000..793bf1be6 --- /dev/null +++ b/packages/integrations/markdoc/test/image-assets.test.js @@ -0,0 +1,93 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { parseHTML } from 'linkedom'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +const root = new URL('./fixtures/image-assets/', import.meta.url); + +describe('Markdoc - Image assets', () => { + let baseFixture; + + before(async () => { + baseFixture = await loadFixture({ + root, + }); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await baseFixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('uses public/ image paths unchanged', async () => { + const res = await baseFixture.fetch('/'); + const html = await res.text(); + const { document } = parseHTML(html); + assert.equal(document.querySelector('#public > img')?.src, '/favicon.svg'); + }); + + it('transforms relative image paths to optimized path', async () => { + const res = await baseFixture.fetch('/'); + const html = await res.text(); + const { document } = parseHTML(html); + assert.match( + document.querySelector('#relative > img')?.src, + /\/_image\?href=.*%2Fsrc%2Fassets%2Frelative%2Foar.jpg%3ForigWidth%3D420%26origHeight%3D630%26origFormat%3Djpg&w=420&h=630&f=webp/, + ); + }); + + it('transforms aliased image paths to optimized path', async () => { + const res = await baseFixture.fetch('/'); + const html = await res.text(); + const { document } = parseHTML(html); + assert.match( + document.querySelector('#alias > img')?.src, + /\/_image\?href=.*%2Fsrc%2Fassets%2Falias%2Fcityscape.jpg%3ForigWidth%3D420%26origHeight%3D280%26origFormat%3Djpg&w=420&h=280&f=webp/, + ); + }); + + it('passes images inside image tags to configured image component', async () => { + const res = await baseFixture.fetch('/'); + const html = await res.text(); + const { document } = parseHTML(html); + assert.equal(document.querySelector('#component > img')?.className, 'custom-styles'); + }); + }); + + describe('build', () => { + before(async () => { + await baseFixture.build(); + }); + + it('uses public/ image paths unchanged', async () => { + const html = await baseFixture.readFile('/index.html'); + const { document } = parseHTML(html); + assert.equal(document.querySelector('#public > img')?.src, '/favicon.svg'); + }); + + it('transforms relative image paths to optimized path', async () => { + const html = await baseFixture.readFile('/index.html'); + const { document } = parseHTML(html); + assert.match(document.querySelector('#relative > img')?.src, /^\/_astro\/oar.*\.webp$/); + }); + + it('transforms aliased image paths to optimized path', async () => { + const html = await baseFixture.readFile('/index.html'); + const { document } = parseHTML(html); + assert.match(document.querySelector('#alias > img')?.src, /^\/_astro\/cityscape.*\.webp$/); + }); + + it('passes images inside image tags to configured image component', async () => { + const html = await baseFixture.readFile('/index.html'); + const { document } = parseHTML(html); + assert.equal(document.querySelector('#component > img')?.className, 'custom-styles'); + assert.match(document.querySelector('#component > img')?.src, /^\/_astro\/oar.*\.webp$/); + }); + }); +}); diff --git a/packages/integrations/markdoc/test/propagated-assets.test.js b/packages/integrations/markdoc/test/propagated-assets.test.js new file mode 100644 index 000000000..a0768448f --- /dev/null +++ b/packages/integrations/markdoc/test/propagated-assets.test.js @@ -0,0 +1,69 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { parseHTML } from 'linkedom'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +describe('Markdoc - propagated assets', () => { + let fixture; + let devServer; + before(async () => { + fixture = await loadFixture({ + root: new URL('./fixtures/propagated-assets/', import.meta.url), + // test suite was authored when inlineStylesheets defaulted to never + build: { inlineStylesheets: 'never' }, + }); + }); + + const modes = ['dev', 'prod']; + + for (const mode of modes) { + describe(mode, () => { + /** @type {Document} */ + let stylesDocument; + /** @type {Document} */ + let scriptsDocument; + + before(async () => { + if (mode === 'prod') { + await fixture.build(); + stylesDocument = parseHTML(await fixture.readFile('/styles/index.html')).document; + scriptsDocument = parseHTML(await fixture.readFile('/scripts/index.html')).document; + } else if (mode === 'dev') { + devServer = await fixture.startDevServer(); + const styleRes = await fixture.fetch('/styles'); + const scriptRes = await fixture.fetch('/scripts'); + stylesDocument = parseHTML(await styleRes.text()).document; + scriptsDocument = parseHTML(await scriptRes.text()).document; + } + }); + + after(async () => { + if (mode === 'dev') devServer?.stop(); + }); + + it('Bundles styles', async () => { + let styleContents; + if (mode === 'dev') { + const styles = stylesDocument.querySelectorAll('style'); + assert.equal(styles.length, 1); + styleContents = styles[0].textContent; + } else { + const links = stylesDocument.querySelectorAll('link[rel="stylesheet"]'); + assert.equal(links.length, 1); + styleContents = await fixture.readFile(links[0].href); + } + assert.equal(styleContents.includes('--color-base-purple: 269, 79%;'), true); + }); + + it('[fails] Does not bleed styles to other page', async () => { + if (mode === 'dev') { + const styles = scriptsDocument.querySelectorAll('style'); + assert.equal(styles.length, 0); + } else { + const links = scriptsDocument.querySelectorAll('link[rel="stylesheet"]'); + assert.equal(links.length, 0); + } + }); + }); + } +}); diff --git a/packages/integrations/markdoc/test/render-components.test.js b/packages/integrations/markdoc/test/render-components.test.js new file mode 100644 index 000000000..e8ddec909 --- /dev/null +++ b/packages/integrations/markdoc/test/render-components.test.js @@ -0,0 +1,94 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { parseHTML } from 'linkedom'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +const root = new URL('./fixtures/render-with-components/', import.meta.url); + +describe('Markdoc - render components', () => { + let fixture; + + before(async () => { + fixture = await loadFixture({ + root, + }); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('renders content - with components', async () => { + const res = await fixture.fetch('/'); + const html = await res.text(); + + renderComponentsChecks(html); + }); + + it('renders content - with components inside partials', async () => { + const res = await fixture.fetch('/'); + const html = await res.text(); + + renderComponentsInsidePartialsChecks(html); + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + it('renders content - with components', async () => { + const html = await fixture.readFile('/index.html'); + + renderComponentsChecks(html); + }); + + it('renders content - with components inside partials', async () => { + const html = await fixture.readFile('/index.html'); + + renderComponentsInsidePartialsChecks(html); + }); + }); +}); + +/** @param {string} html */ +function renderComponentsChecks(html) { + const { document } = parseHTML(html); + const h2 = document.querySelector('h2'); + assert.equal(h2.textContent, 'Post with components'); + + // Renders custom shortcode component + const marquee = document.querySelector('marquee'); + assert.notEqual(marquee, null); + assert.equal(marquee.hasAttribute('data-custom-marquee'), true); + + // Renders Astro Code component + const pre = document.querySelector('pre'); + assert.notEqual(pre, null); + assert.equal(pre.className, 'astro-code github-dark'); + + // Renders 2nd Astro Code component inside if tag + const pre2 = document.querySelectorAll('pre')[1]; + assert.notEqual(pre2, null); + assert.equal(pre2.className, 'astro-code github-dark'); +} + +/** @param {string} html */ +function renderComponentsInsidePartialsChecks(html) { + const { document } = parseHTML(html); + // renders Counter.tsx + const button = document.querySelector('#counter'); + assert.equal(button.textContent, '1'); + + // renders DeeplyNested.astro + const deeplyNested = document.querySelector('#deeply-nested'); + assert.equal(deeplyNested.textContent, 'Deeply nested partial'); +} diff --git a/packages/integrations/markdoc/test/render-extends-components.test.js b/packages/integrations/markdoc/test/render-extends-components.test.js new file mode 100644 index 000000000..f5f1454c8 --- /dev/null +++ b/packages/integrations/markdoc/test/render-extends-components.test.js @@ -0,0 +1,69 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { parseHTML } from 'linkedom'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +const root = new URL('./fixtures/render-with-extends-components/', import.meta.url); + +describe('Markdoc - render components defined in `extends`', () => { + let fixture; + + before(async () => { + fixture = await loadFixture({ + root, + }); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('renders content - with components', async () => { + const res = await fixture.fetch('/'); + const html = await res.text(); + + renderComponentsChecks(html); + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + it('renders content - with components', async () => { + const html = await fixture.readFile('/index.html'); + + renderComponentsChecks(html); + }); + }); +}); + +/** @param {string} html */ +function renderComponentsChecks(html) { + const { document } = parseHTML(html); + const h2 = document.querySelector('h2'); + assert.equal(h2.textContent, 'Post with components'); + + // Renders custom shortcode component + const marquee = document.querySelector('marquee'); + assert.notEqual(marquee, null); + assert.equal(marquee.hasAttribute('data-custom-marquee'), true); + + // Renders Astro Code component + const pre = document.querySelector('pre'); + assert.notEqual(pre, null); + assert.equal(pre.className, 'astro-code github-dark'); + + // Renders 2nd Astro Code component inside if tag + const pre2 = document.querySelectorAll('pre')[1]; + assert.notEqual(pre2, null); + assert.equal(pre2.className, 'astro-code github-dark'); +} diff --git a/packages/integrations/markdoc/test/render-html.test.js b/packages/integrations/markdoc/test/render-html.test.js new file mode 100644 index 000000000..5bf7fe5ce --- /dev/null +++ b/packages/integrations/markdoc/test/render-html.test.js @@ -0,0 +1,316 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { parseHTML } from 'linkedom'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +async function getFixture(name) { + return await loadFixture({ + root: new URL(`./fixtures/${name}/`, import.meta.url), + }); +} + +describe('Markdoc - render html', () => { + let fixture; + + before(async () => { + fixture = await getFixture('render-html'); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('renders content - simple', async () => { + const res = await fixture.fetch('/simple'); + const html = await res.text(); + + renderSimpleChecks(html); + }); + + it('renders content - nested-html', async () => { + const res = await fixture.fetch('/nested-html'); + const html = await res.text(); + + renderNestedHTMLChecks(html); + }); + + it('renders content - components interleaved with html', async () => { + const res = await fixture.fetch('/components'); + const html = await res.text(); + + renderComponentsHTMLChecks(html); + }); + + it('renders content - randomly cased html attributes', async () => { + const res = await fixture.fetch('/randomly-cased-html-attributes'); + const html = await res.text(); + + renderRandomlyCasedHTMLAttributesChecks(html); + }); + + it('renders content - html within partials', async () => { + const res = await fixture.fetch('/with-partial'); + const html = await res.text(); + + renderHTMLWithinPartialChecks(html); + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + it('renders content - simple', async () => { + const html = await fixture.readFile('/simple/index.html'); + + renderSimpleChecks(html); + }); + + it('renders content - nested-html', async () => { + const html = await fixture.readFile('/nested-html/index.html'); + + renderNestedHTMLChecks(html); + }); + + it('renders content - components interleaved with html', async () => { + const html = await fixture.readFile('/components/index.html'); + + renderComponentsHTMLChecks(html); + }); + + it('renders content - randomly cased html attributes', async () => { + const html = await fixture.readFile('/randomly-cased-html-attributes/index.html'); + + renderRandomlyCasedHTMLAttributesChecks(html); + }); + + it('renders content - html within partials', async () => { + const html = await fixture.readFile('/with-partial/index.html'); + + renderHTMLWithinPartialChecks(html); + }); + }); +}); + +/** @param {string} html */ +function renderSimpleChecks(html) { + const { document } = parseHTML(html); + + const h2 = document.querySelector('h2'); + assert.equal(h2.textContent, 'Simple post header'); + + const spanInsideH2 = document.querySelector('h2 > span'); + assert.equal(spanInsideH2.textContent, 'post'); + assert.equal(spanInsideH2.className, 'inside-h2'); + assert.equal(spanInsideH2.style.color, 'fuscia'); + + const p1 = document.querySelector('article > p:nth-of-type(1)'); + assert.equal(p1.children.length, 1); + assert.equal(p1.textContent, 'This is a simple Markdoc post.'); + + const p2 = document.querySelector('article > p:nth-of-type(2)'); + assert.equal(p2.children.length, 0); + assert.equal(p2.textContent, 'This is a paragraph!'); + + const p3 = document.querySelector('article > p:nth-of-type(3)'); + assert.equal(p3.children.length, 1); + assert.equal(p3.textContent, 'This is a span inside a paragraph!'); + + const video = document.querySelector('video'); + assert.ok(video, 'A video element should exist'); + assert.ok(video.hasAttribute('autoplay'), 'The video element should have the autoplay attribute'); + assert.ok(video.hasAttribute('muted'), 'The video element should have the muted attribute'); +} + +/** @param {string} html */ +function renderNestedHTMLChecks(html) { + const { document } = parseHTML(html); + + const p1 = document.querySelector('p:nth-of-type(1)'); + assert.equal(p1.id, 'p1'); + assert.equal(p1.textContent, 'before inner after'); + assert.equal(p1.children.length, 1); + + const p1Span1 = p1.querySelector('span'); + assert.equal(p1Span1.textContent, 'inner'); + assert.equal(p1Span1.id, 'inner1'); + assert.equal(p1Span1.className, 'inner-class'); + assert.equal(p1Span1.style.color, 'hotpink'); + + const p2 = document.querySelector('p:nth-of-type(2)'); + assert.equal(p2.id, 'p2'); + assert.equal(p2.textContent, '\n before\n inner\n after\n'); + assert.equal(p2.children.length, 1); + + const divL1 = document.querySelector('div:nth-of-type(1)'); + assert.equal(divL1.id, 'div-l1'); + assert.equal(divL1.children.length, 2); + + const divL2_1 = divL1.querySelector('div:nth-of-type(1)'); + assert.equal(divL2_1.id, 'div-l2-1'); + assert.equal(divL2_1.children.length, 1); + + const p3 = divL2_1.querySelector('p:nth-of-type(1)'); + assert.equal(p3.id, 'p3'); + assert.equal(p3.textContent, 'before inner after'); + assert.equal(p3.children.length, 1); + + const divL2_2 = divL1.querySelector('div:nth-of-type(2)'); + assert.equal(divL2_2.id, 'div-l2-2'); + assert.equal(divL2_2.children.length, 2); + + const p4 = divL2_2.querySelector('p:nth-of-type(1)'); + assert.equal(p4.id, 'p4'); + assert.equal(p4.textContent, 'before inner after'); + assert.equal(p4.children.length, 1); + + const p5 = divL2_2.querySelector('p:nth-of-type(2)'); + assert.equal(p5.id, 'p5'); + assert.equal(p5.textContent, 'before inner after'); + assert.equal(p5.children.length, 1); +} + +/** + * + * @param {string} html */ +function renderRandomlyCasedHTMLAttributesChecks(html) { + const { document } = parseHTML(html); + + const td1 = document.querySelector('#td1'); + const td2 = document.querySelector('#td1'); + const td3 = document.querySelector('#td1'); + const td4 = document.querySelector('#td1'); + + // all four <td>'s which had randomly cased variants of colspan/rowspan should all be rendered lowercased at this point + + assert.equal(td1.getAttribute('colspan'), '3'); + assert.equal(td1.getAttribute('rowspan'), '2'); + + assert.equal(td2.getAttribute('colspan'), '3'); + assert.equal(td2.getAttribute('rowspan'), '2'); + + assert.equal(td3.getAttribute('colspan'), '3'); + assert.equal(td3.getAttribute('rowspan'), '2'); + + assert.equal(td4.getAttribute('colspan'), '3'); + assert.equal(td4.getAttribute('rowspan'), '2'); +} + +/** + * @param {string} html + */ +function renderHTMLWithinPartialChecks(html) { + const { document } = parseHTML(html); + + const li = document.querySelector('ul > li#partial'); + assert.equal(li.textContent, 'List item'); +} + +/** + * Asserts that the rendered HTML tags with interleaved Markdoc tags (both block and inline) rendered in the expected nested graph of elements + * + * @param {string} html */ +function renderComponentsHTMLChecks(html) { + const { document } = parseHTML(html); + + const naturalP1 = document.querySelector('article > p:nth-of-type(1)'); + assert.equal(naturalP1.textContent, 'This is a inline mark in regular Markdown markup.'); + assert.equal(naturalP1.children.length, 1); + + const p1 = document.querySelector('article > p:nth-of-type(2)'); + assert.equal(p1.id, 'p1'); + assert.equal(p1.textContent, 'This is a inline mark under some HTML'); + assert.equal(p1.children.length, 1); + assertInlineMark(p1.children[0]); + + const div1p1 = document.querySelector('article > #div1 > p:nth-of-type(1)'); + assert.equal(div1p1.id, 'div1-p1'); + assert.equal(div1p1.textContent, 'This is a inline mark under some HTML'); + assert.equal(div1p1.children.length, 1); + assertInlineMark(div1p1.children[0]); + + const div1p2 = document.querySelector('article > #div1 > p:nth-of-type(2)'); + assert.equal(div1p2.id, 'div1-p2'); + assert.equal(div1p2.textContent, 'This is a inline mark under some HTML'); + assert.equal(div1p2.children.length, 1); + + const div1p2span1 = div1p2.querySelector('span'); + assert.equal(div1p2span1.id, 'div1-p2-span1'); + assert.equal(div1p2span1.textContent, 'inline mark'); + assert.equal(div1p2span1.children.length, 1); + assertInlineMark(div1p2span1.children[0]); + + const aside1 = document.querySelector('article > aside:nth-of-type(1)'); + const aside1Title = aside1.querySelector('p.title'); + assert.equal(aside1Title.textContent.trim(), 'Aside One'); + const aside1Section = aside1.querySelector('section'); + const aside1SectionP1 = aside1Section.querySelector('p:nth-of-type(1)'); + assert.equal( + aside1SectionP1.textContent, + "I'm a Markdown paragraph inside an top-level aside tag", + ); + const aside1H2_1 = aside1Section.querySelector('h2:nth-of-type(1)'); + assert.equal(aside1H2_1.id, 'im-an-h2-via-markdown-markup'); // automatic slug + assert.equal(aside1H2_1.textContent, "I'm an H2 via Markdown markup"); + const aside1H2_2 = aside1Section.querySelector('h2:nth-of-type(2)'); + assert.equal(aside1H2_2.id, 'h-two'); + assert.equal(aside1H2_2.textContent, "I'm an H2 via HTML markup"); + const aside1SectionP2 = aside1Section.querySelector('p:nth-of-type(2)'); + assert.equal(aside1SectionP2.textContent, 'Markdown bold vs HTML bold'); + assert.equal(aside1SectionP2.children.length, 2); + const aside1SectionP2Strong1 = aside1SectionP2.querySelector('strong:nth-of-type(1)'); + assert.equal(aside1SectionP2Strong1.textContent, 'Markdown bold'); + const aside1SectionP2Strong2 = aside1SectionP2.querySelector('strong:nth-of-type(2)'); + assert.equal(aside1SectionP2Strong2.textContent, 'HTML bold'); + + const article = document.querySelector('article'); + assert.equal(article.textContent.includes('RENDERED'), true); + assert.notEqual(article.textContent.includes('NOT RENDERED'), true); + + const section1 = document.querySelector('article > #section1'); + const section1div1 = section1.querySelector('#div1'); + const section1Aside1 = section1div1.querySelector('aside:nth-of-type(1)'); + const section1Aside1Title = section1Aside1.querySelector('p.title'); + assert.equal(section1Aside1Title.textContent.trim(), 'Nested un-indented Aside'); + const section1Aside1Section = section1Aside1.querySelector('section'); + const section1Aside1SectionP1 = section1Aside1Section.querySelector('p:nth-of-type(1)'); + assert.equal(section1Aside1SectionP1.textContent, 'regular Markdown markup'); + const section1Aside1SectionP4 = section1Aside1Section.querySelector('p:nth-of-type(2)'); + assert.equal(section1Aside1SectionP4.textContent, 'nested inline mark content'); + assert.equal(section1Aside1SectionP4.children.length, 1); + assertInlineMark(section1Aside1SectionP4.children[0]); + + const section1div2 = section1.querySelector('#div2'); + const section1Aside2 = section1div2.querySelector('aside:nth-of-type(1)'); + const section1Aside2Title = section1Aside2.querySelector('p.title'); + assert.equal(section1Aside2Title.textContent.trim(), 'Nested indented Aside 💀'); + const section1Aside2Section = section1Aside2.querySelector('section'); + const section1Aside2SectionP1 = section1Aside2Section.querySelector('p:nth-of-type(1)'); + assert.equal(section1Aside2SectionP1.textContent, 'regular Markdown markup'); + const section1Aside1SectionP5 = section1Aside2Section.querySelector('p:nth-of-type(2)'); + assert.equal(section1Aside1SectionP5.id, 'p5'); + assert.equal(section1Aside1SectionP5.children.length, 1); + const section1Aside1SectionP5Span1 = section1Aside1SectionP5.children[0]; + assert.equal(section1Aside1SectionP5Span1.textContent, 'inline mark'); + assert.equal(section1Aside1SectionP5Span1.children.length, 1); + const section1Aside1SectionP5Span1Span1 = section1Aside1SectionP5Span1.children[0]; + assert.equal(section1Aside1SectionP5Span1Span1.textContent, ' mark'); +} + +/** @param {HTMLElement | null | undefined} el */ + +function assertInlineMark(el) { + assert.ok(el); + assert.equal(el.children.length, 0); + assert.equal(el.textContent, 'inline mark'); + assert.equal(el.className, 'mark'); + assert.equal(el.style.color, 'hotpink'); +} diff --git a/packages/integrations/markdoc/test/render-indented-components.test.js b/packages/integrations/markdoc/test/render-indented-components.test.js new file mode 100644 index 000000000..ac47e72f9 --- /dev/null +++ b/packages/integrations/markdoc/test/render-indented-components.test.js @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { parseHTML } from 'linkedom'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +const root = new URL('./fixtures/render-with-indented-components/', import.meta.url); + +describe('Markdoc - render indented components', () => { + let fixture; + + before(async () => { + fixture = await loadFixture({ + root, + }); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('renders content - with indented components', async () => { + const res = await fixture.fetch('/'); + const html = await res.text(); + + renderIndentedComponentsChecks(html); + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + it('renders content - with indented components', async () => { + const html = await fixture.readFile('/index.html'); + + renderIndentedComponentsChecks(html); + }); + }); +}); + +/** @param {string} html */ +function renderIndentedComponentsChecks(html) { + const { document } = parseHTML(html); + const h2 = document.querySelector('h2'); + assert.equal(h2.textContent, 'Post with indented components'); + + // Renders custom shortcode components + const marquees = document.querySelectorAll('marquee'); + assert.equal(marquees.length, 2); + + // Renders h3 + const h3 = document.querySelector('h3'); + assert.equal(h3.textContent, 'I am an h3!'); + + // Renders Astro Code component + const pre = document.querySelector('pre'); + assert.notEqual(pre, null); + assert.equal(pre.className, 'astro-code github-dark'); +} diff --git a/packages/integrations/markdoc/test/render.test.js b/packages/integrations/markdoc/test/render.test.js new file mode 100644 index 000000000..4c9293288 --- /dev/null +++ b/packages/integrations/markdoc/test/render.test.js @@ -0,0 +1,199 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { parseHTML } from 'linkedom'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +async function getFixture(name) { + return await loadFixture({ + root: new URL(`./fixtures/${name}/`, import.meta.url), + }); +} + +describe('Markdoc - render', () => { + describe('dev', () => { + it('renders content - simple', async () => { + const fixture = await getFixture('render-simple'); + const server = await fixture.startDevServer(); + + const res = await fixture.fetch('/'); + const html = await res.text(); + + renderSimpleChecks(html); + + await server.stop(); + }); + + it('renders content - with partials', async () => { + const fixture = await getFixture('render-partials'); + const server = await fixture.startDevServer(); + + const res = await fixture.fetch('/'); + const html = await res.text(); + + renderPartialsChecks(html); + + await server.stop(); + }); + + it('renders content - with config', async () => { + const fixture = await getFixture('render-with-config'); + const server = await fixture.startDevServer(); + + const res = await fixture.fetch('/'); + const html = await res.text(); + + renderConfigChecks(html); + + await server.stop(); + }); + + it('renders content - with `render: null` in document', async () => { + const fixture = await getFixture('render-null'); + const server = await fixture.startDevServer(); + + const res = await fixture.fetch('/'); + const html = await res.text(); + + renderNullChecks(html); + + await server.stop(); + }); + + it('renders content - with root folder containing space', async () => { + const fixture = await getFixture('render with-space'); + const server = await fixture.startDevServer(); + + const res = await fixture.fetch('/'); + const html = await res.text(); + + renderWithRootFolderContainingSpace(html); + + await server.stop(); + }); + }); + + describe('build', () => { + it('renders content - simple', async () => { + const fixture = await getFixture('render-simple'); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + + renderSimpleChecks(html); + }); + + it('renders content - with partials', async () => { + const fixture = await getFixture('render-partials'); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + + renderPartialsChecks(html); + }); + + it('renders content - with config', async () => { + const fixture = await getFixture('render-with-config'); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + + renderConfigChecks(html); + }); + + it('renders content - with `render: null` in document', async () => { + const fixture = await getFixture('render-null'); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + + renderNullChecks(html); + }); + + it('renders content - with root folder containing space', async () => { + const fixture = await getFixture('render with-space'); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + + renderWithRootFolderContainingSpace(html); + }); + + it('renders content - with typographer option', async () => { + const fixture = await getFixture('render-typographer'); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + + renderTypographerChecks(html); + }); + }); +}); + +/** + * @param {string} html + */ +function renderNullChecks(html) { + const { document } = parseHTML(html); + const h2 = document.querySelector('h2'); + assert.equal(h2.textContent, 'Post with render null'); + assert.equal(h2.parentElement?.tagName, 'BODY'); + const divWrapper = document.querySelector('.div-wrapper'); + assert.equal(divWrapper.textContent, "I'm inside a div wrapper"); +} + +/** @param {string} html */ +function renderPartialsChecks(html) { + const { document } = parseHTML(html); + const top = document.querySelector('#top'); + assert.ok(top); + const nested = document.querySelector('#nested'); + assert.ok(nested); + const configured = document.querySelector('#configured'); + assert.ok(configured); +} + +/** @param {string} html */ +function renderConfigChecks(html) { + const { document } = parseHTML(html); + const h2 = document.querySelector('h2'); + assert.equal(h2.textContent, 'Post with config'); + const textContent = html; + + assert.notEqual(textContent.includes('Hello'), true); + assert.equal(textContent.includes('Hola'), true); + assert.equal(textContent.includes('Konnichiwa'), true); + + const runtimeVariable = document.querySelector('#runtime-variable'); + assert.equal(runtimeVariable?.textContent?.trim(), 'working!'); +} + +/** @param {string} html */ +function renderSimpleChecks(html) { + const { document } = parseHTML(html); + const h2 = document.querySelector('h2'); + assert.equal(h2.textContent, 'Simple post'); + const p = document.querySelector('p'); + assert.equal(p.textContent, 'This is a simple Markdoc post.'); +} + +/** @param {string} html */ +function renderWithRootFolderContainingSpace(html) { + const { document } = parseHTML(html); + const h2 = document.querySelector('h2'); + assert.equal(h2.textContent, 'Simple post with root folder containing a space'); + const p = document.querySelector('p'); + assert.equal(p.textContent, 'This is a simple Markdoc post with root folder containing a space.'); +} + +/** + * @param {string} html + */ +function renderTypographerChecks(html) { + const { document } = parseHTML(html); + + const h2 = document.querySelector('h2'); + assert.equal(h2.textContent, 'Typographer’s post'); + + const p = document.querySelector('p'); + assert.equal(p.textContent, 'This is a post to test the “typographer” option.'); +} diff --git a/packages/integrations/markdoc/test/syntax-highlighting.test.js b/packages/integrations/markdoc/test/syntax-highlighting.test.js new file mode 100644 index 000000000..6ea841ae1 --- /dev/null +++ b/packages/integrations/markdoc/test/syntax-highlighting.test.js @@ -0,0 +1,136 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import Markdoc from '@markdoc/markdoc'; +import { isHTMLString } from 'astro/runtime/server/index.js'; +import { parseHTML } from 'linkedom'; +import prism from '../dist/extensions/prism.js'; +import shiki from '../dist/extensions/shiki.js'; +import { setupConfig } from '../dist/runtime.js'; + +const entry = ` +\`\`\`ts +const highlighting = true; +\`\`\` + +\`\`\`css +.highlighting { + color: red; +} +\`\`\` +`; + +describe('Markdoc - syntax highlighting', () => { + describe('shiki', () => { + it('transforms with defaults', async () => { + const ast = Markdoc.parse(entry); + const content = await Markdoc.transform(ast, await getConfigExtendingShiki()); + + assert.equal(content.children.length, 2); + for (const codeBlock of content.children) { + assert.equal(isHTMLString(codeBlock), true); + + const pre = parsePreTag(codeBlock); + assert.equal(pre.classList.contains('astro-code'), true); + assert.equal(pre.classList.contains('github-dark'), true); + } + }); + it('transforms with `theme` property', async () => { + const ast = Markdoc.parse(entry); + const content = await Markdoc.transform( + ast, + await getConfigExtendingShiki({ + theme: 'dracula', + }), + ); + assert.equal(content.children.length, 2); + for (const codeBlock of content.children) { + assert.equal(isHTMLString(codeBlock), true); + + const pre = parsePreTag(codeBlock); + assert.equal(pre.classList.contains('astro-code'), true); + assert.equal(pre.classList.contains('dracula'), true); + } + }); + it('transforms with `wrap` property', async () => { + const ast = Markdoc.parse(entry); + const content = await Markdoc.transform( + ast, + await getConfigExtendingShiki({ + wrap: true, + }), + ); + assert.equal(content.children.length, 2); + for (const codeBlock of content.children) { + assert.equal(isHTMLString(codeBlock), true); + + const pre = parsePreTag(codeBlock); + assert.equal(pre.getAttribute('style').includes('white-space: pre-wrap'), true); + assert.equal(pre.getAttribute('style').includes('word-wrap: break-word'), true); + } + }); + it('transform within if tags', async () => { + const ast = Markdoc.parse(` +{% if equals("true", "true") %} +Inside truthy + +\`\`\`js +const hello = "yes"; +\`\`\` + +{% /if %}`); + const content = await Markdoc.transform(ast, await getConfigExtendingShiki()); + assert.equal(content.children.length, 1); + assert.equal(content.children[0].length, 2); + const pTag = content.children[0][0]; + assert.equal(pTag.name, 'p'); + const codeBlock = content.children[0][1]; + assert.equal(isHTMLString(codeBlock), true); + const pre = parsePreTag(codeBlock); + assert.equal(pre.classList.contains('astro-code'), true); + assert.equal(pre.classList.contains('github-dark'), true); + }); + }); + + describe('prism', () => { + it('transforms', async () => { + const ast = Markdoc.parse(entry); + const config = await setupConfig({ + extends: [prism()], + }); + const content = await Markdoc.transform(ast, config); + + assert.equal(content.children.length, 2); + const [tsBlock, cssBlock] = content.children; + + assert.equal(isHTMLString(tsBlock), true); + assert.equal(isHTMLString(cssBlock), true); + + const preTs = parsePreTag(tsBlock); + assert.equal(preTs.classList.contains('language-ts'), true); + + const preCss = parsePreTag(cssBlock); + assert.equal(preCss.classList.contains('language-css'), true); + }); + }); +}); + +/** + * @param {import('astro').ShikiConfig} config + * @returns {import('../src/config.js').AstroMarkdocConfig} + */ +async function getConfigExtendingShiki(config) { + return await setupConfig({ + extends: [shiki(config)], + }); +} + +/** + * @param {string} html + * @returns {HTMLPreElement} + */ +function parsePreTag(html) { + const { document } = parseHTML(html); + const pre = document.querySelector('pre'); + assert.ok(pre); + return pre; +} diff --git a/packages/integrations/markdoc/test/variables.test.js b/packages/integrations/markdoc/test/variables.test.js new file mode 100644 index 000000000..9873afcb7 --- /dev/null +++ b/packages/integrations/markdoc/test/variables.test.js @@ -0,0 +1,55 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { parseHTML } from 'linkedom'; +import { loadFixture } from '../../../astro/test/test-utils.js'; +import markdoc from '../dist/index.js'; + +const root = new URL('./fixtures/variables/', import.meta.url); + +describe('Markdoc - Variables', () => { + let baseFixture; + + before(async () => { + baseFixture = await loadFixture({ + root, + integrations: [markdoc()], + }); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await baseFixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('has expected entry properties', async () => { + const res = await baseFixture.fetch('/'); + const html = await res.text(); + const { document } = parseHTML(html); + assert.equal(document.querySelector('h1')?.textContent, 'Processed by schema: Test entry'); + assert.equal(document.getElementById('id')?.textContent?.trim(), 'id: entry.mdoc'); + assert.equal(document.getElementById('slug')?.textContent?.trim(), 'slug: entry'); + assert.equal(document.getElementById('collection')?.textContent?.trim(), 'collection: blog'); + }); + }); + + describe('build', () => { + before(async () => { + await baseFixture.build(); + }); + + it('has expected entry properties', async () => { + const html = await baseFixture.readFile('/index.html'); + const { document } = parseHTML(html); + assert.equal(document.querySelector('h1')?.textContent, 'Processed by schema: Test entry'); + assert.equal(document.getElementById('id')?.textContent?.trim(), 'id: entry.mdoc'); + assert.equal(document.getElementById('slug')?.textContent?.trim(), 'slug: entry'); + assert.equal(document.getElementById('collection')?.textContent?.trim(), 'collection: blog'); + }); + }); +}); diff --git a/packages/integrations/markdoc/tsconfig.json b/packages/integrations/markdoc/tsconfig.json new file mode 100644 index 000000000..1504b4b6d --- /dev/null +++ b/packages/integrations/markdoc/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "outDir": "./dist" + } +} |