Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Problem with embedding inside react-markdown due to async / await #123

Open
simonv3 opened this issue Apr 20, 2023 · 3 comments
Open

Problem with embedding inside react-markdown due to async / await #123

simonv3 opened this issue Apr 20, 2023 · 3 comments

Comments

@simonv3
Copy link

simonv3 commented Apr 20, 2023

  • remark-embedder-core version: 3.0.1
  • node version: v16.19.0
  • yarn version: 1.22.19

Relevant code or config

    <ReactMarkdown
        remarkPlugins={[
          remarkGfm,
          [remarkEmbedder, { transformers: [CodeSandboxTransformer] }],
        ]}
      >
        {content}
      </ReactMarkdown>

What you did: I embedded remarkEmbedder in react-markdown.

What happened:

It fails with error:

Uncaught (in promise) Error: `runSync` finished async. Use `run` instead

Problem description:

This happens because react-markdown does not support async plugins.

Suggested solution:

For the basic functionality of remarkEmbedder it doesn't look like it's necessary to do async. Is there a way to set this up so that it's not required?

When I strip the async functionality from the code in my own local copy of index.js it works fine.

@WNemencha
Copy link

@simonv3 how did you tweak the code so it works without async?

@simonv3
Copy link
Author

simonv3 commented May 6, 2023

This is what my final file looks like:

import { visit } from "unist-util-visit";
import { fromParse5 } from "hast-util-from-parse5";
import * as parse5 from "parse5";

/**
 * NOTE: this is adapted from @remark-embedder/core. It's the same functionality
 * but the async / await has been stripped from it.
 * See ticket https://github.com/remark-embedder/core/issues/123#issue-1676994245
 */

Object.defineProperty(exports, "__esModule", {
  value: true,
});
// exports.default = void 0;

// results in an AST node of type "root" with a single "children" node of type "element"
// so we return the first (and only) child "element" node
const htmlToHast = (string) => {
  return fromParse5(parse5.parseFragment(string)).children[0];
};

const getUrlString = (url) => {
  const urlString = url.startsWith("http") ? url : `https://${url}`;

  try {
    return new URL(urlString).toString();
  } catch (error) {
    return null;
  }
};

const remarkEmbedder = ({ cache, transformers, handleHTML, handleError }) => {
  // convert the array of transformers to one with both the transformer and the config tuple
  const transformersAndConfig = transformers.map((t) =>
    Array.isArray(t)
      ? {
          config: t[1],
          transformer: t[0],
        }
      : {
          transformer: t,
        }
  );
  return (tree) => {
    // const utilVisit = import("unist-util-visit");
    // console.log("utilVisit", utilVisit);
    // const { visit } = utilVisit;
    const nodeAndURL = [];
    visit(tree, "paragraph", (paragraphNode) => {
      if (paragraphNode.children.length !== 1) {
        return;
      }

      const { children } = paragraphNode;
      const node = children[0];
      const isText = node.type === "text"; // it's a valid link if there's no title, and the value is the same as the URL

      const isValidLink =
        node.type === "link" &&
        !node.title &&
        node.children.length === 1 &&
        node.children[0].type === "text" &&
        node.children[0].value === node.url;

      if (!(isText || isValidLink)) {
        return;
      }

      const value = isText ? node.value : node.url;
      const urlString = getUrlString(value);

      if (!urlString) {
        return;
      }

      nodeAndURL.push({
        parentNode: paragraphNode,
        url: urlString,
      });
    });
    const nodesToTransform = [];

    for (const node of nodeAndURL) {
      for (const transformerAndConfig of transformersAndConfig) {
        // we need to make sure this is completed in sequence
        // because the order matters
        // eslint-disable-next-line no-await-in-loop
        if (transformerAndConfig.transformer.shouldTransform(node.url)) {
          nodesToTransform.push({ ...node, ...transformerAndConfig });
          break;
        }
      }
    }

    nodesToTransform.forEach(({ parentNode, url, transformer, config }) => {
      const errorMessageBanner = `The following error occurred while processing \`${url}\` with the remark-embedder transformer \`${transformer.name}\`:`;

      try {
        const cacheKey = `remark-embedder:${transformer.name}:${url}`;
        let html = cache == null ? void 0 : cache.get(cacheKey);

        if (!html) {
          try {
            var _html$trim, _html;

            html = transformer.getHTML(url, config);
            html =
              (_html$trim = (_html = html) == null ? void 0 : _html.trim()) !=
              null
                ? _html$trim
                : null;
            cache == null ? void 0 : cache.set(cacheKey, html); // optional handleHTML transform function

            if (handleHTML) {
              var _html$trim2, _html2;

              html = handleHTML(html, {
                url,
                transformer,
                config,
              });
              html =
                (_html$trim2 =
                  (_html2 = html) == null ? void 0 : _html2.trim()) != null
                  ? _html$trim2
                  : null;
            }
          } catch (e) {
            if (handleError) {
              var _html$trim3, _html3;

              const error = e;
              console.error(`${errorMessageBanner}\n\n${error.message}`);
              html = handleError({
                error,
                url,
                transformer,
                config,
              });
              html =
                (_html$trim3 =
                  (_html3 = html) == null ? void 0 : _html3.trim()) != null
                  ? _html$trim3
                  : null;
            } else {
              throw e;
            }
          }
        } // if nothing's returned from getHTML, then no modifications are needed

        if (!html) {
          return;
        } // convert the HTML string into an AST

        const htmlElement = htmlToHast(html); // set the parentNode.data with the necessary properties

        parentNode.data = {
          hChildren: htmlElement.children,
          hName: htmlElement.tagName,
          hProperties: htmlElement.properties,
        };
      } catch (e) {
        const error = e;
        error.message = `${errorMessageBanner}\n\n${error.message}`;
        throw error;
      }
    });
    return tree;
  };
};

export default remarkEmbedder;

@MichaelDeBoey
Copy link
Member

Once remarkjs/react-markdown#682 is merged & released, this plugin will run out of the box with react-markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants