TreeNode.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import type { RenderableTreeNode } from '@markdoc/markdoc';
  2. import Markdoc from '@markdoc/markdoc';
  3. import type { AstroInstance } from 'astro';
  4. import type { HTMLString } from 'astro/runtime/server/index.js';
  5. import {
  6. createComponent,
  7. createHeadAndContent,
  8. isHTMLString,
  9. render,
  10. renderComponent,
  11. renderScriptElement,
  12. renderTemplate,
  13. renderUniqueStylesheet,
  14. unescapeHTML,
  15. } from 'astro/runtime/server/index.js';
  16. export type TreeNode =
  17. | {
  18. type: 'text';
  19. content: string | HTMLString;
  20. }
  21. | {
  22. type: 'component';
  23. component: AstroInstance['default'];
  24. collectedLinks?: string[];
  25. collectedStyles?: string[];
  26. collectedScripts?: string[];
  27. props: Record<string, any>;
  28. children: TreeNode[];
  29. }
  30. | {
  31. type: 'element';
  32. tag: string;
  33. attributes: Record<string, any>;
  34. children: TreeNode[];
  35. };
  36. export const ComponentNode = createComponent({
  37. factory(result: any, { treeNode }: { treeNode: TreeNode }) {
  38. if (treeNode.type === 'text') return render`${treeNode.content}`;
  39. const slots = {
  40. default: () =>
  41. render`${treeNode.children.map((child) =>
  42. renderComponent(result, 'ComponentNode', ComponentNode, { treeNode: child })
  43. )}`,
  44. };
  45. if (treeNode.type === 'component') {
  46. let styles = '',
  47. links = '',
  48. scripts = '';
  49. if (Array.isArray(treeNode.collectedStyles)) {
  50. styles = treeNode.collectedStyles
  51. .map((style: any) =>
  52. renderUniqueStylesheet(result, {
  53. type: 'inline',
  54. content: style,
  55. })
  56. )
  57. .join('');
  58. }
  59. if (Array.isArray(treeNode.collectedLinks)) {
  60. links = treeNode.collectedLinks
  61. .map((link: any) => {
  62. return renderUniqueStylesheet(result, {
  63. type: 'external',
  64. src: link[0] === '/' ? link : '/' + link,
  65. });
  66. })
  67. .join('');
  68. }
  69. if (Array.isArray(treeNode.collectedScripts)) {
  70. scripts = treeNode.collectedScripts
  71. .map((script: any) => renderScriptElement(script))
  72. .join('');
  73. }
  74. const head = unescapeHTML(styles + links + scripts);
  75. let headAndContent = createHeadAndContent(
  76. head,
  77. renderTemplate`${renderComponent(
  78. result,
  79. treeNode.component.name,
  80. treeNode.component,
  81. treeNode.props,
  82. slots
  83. )}`
  84. );
  85. // Let the runtime know that this component is being used.
  86. result._metadata.propagators.add({
  87. init() {
  88. return headAndContent;
  89. },
  90. });
  91. return headAndContent;
  92. }
  93. return renderComponent(result, treeNode.tag, treeNode.tag, treeNode.attributes, slots);
  94. },
  95. propagation: 'self',
  96. });
  97. export async function createTreeNode(node: RenderableTreeNode): Promise<TreeNode> {
  98. if (isHTMLString(node)) {
  99. return { type: 'text', content: node as HTMLString };
  100. } else if (typeof node === 'string' || typeof node === 'number') {
  101. return { type: 'text', content: String(node) };
  102. } else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) {
  103. return { type: 'text', content: '' };
  104. }
  105. const children = await Promise.all(node.children.map((child) => createTreeNode(child)));
  106. if (typeof node.name === 'function') {
  107. const component = node.name;
  108. const props = node.attributes;
  109. return {
  110. type: 'component',
  111. component,
  112. props,
  113. children,
  114. };
  115. } else if (isPropagatedAssetsModule(node.name)) {
  116. const { collectedStyles, collectedLinks, collectedScripts } = node.name;
  117. const component = (await node.name.getMod()).default;
  118. const props = node.attributes;
  119. return {
  120. type: 'component',
  121. component,
  122. collectedStyles,
  123. collectedLinks,
  124. collectedScripts,
  125. props,
  126. children,
  127. };
  128. } else {
  129. return {
  130. type: 'element',
  131. tag: node.name,
  132. attributes: node.attributes,
  133. children,
  134. };
  135. }
  136. }
  137. type PropagatedAssetsModule = {
  138. __astroPropagation: true;
  139. getMod: () => Promise<AstroInstance>;
  140. collectedStyles: string[];
  141. collectedLinks: string[];
  142. collectedScripts: string[];
  143. };
  144. function isPropagatedAssetsModule(module: any): module is PropagatedAssetsModule {
  145. return typeof module === 'object' && module != null && '__astroPropagation' in module;
  146. }