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

Is this working with unified v10? #49

Open
factoidforrest opened this issue May 3, 2022 · 4 comments
Open

Is this working with unified v10? #49

factoidforrest opened this issue May 3, 2022 · 4 comments

Comments

@factoidforrest
Copy link

I am getting
Cannot read properties of undefined (reading 'blockTokenizers')
In the DOM when I use this plugin with react-markdown.

Given that this project seems more or less fully abandoned, is there a replacement that does something similar? Are we waiting for someone to fork this. I tried to figure out how to fix this plugin but given the very sparse documentation on how to write unified plugins, I got very confused.

@eestein
Copy link

eestein commented Jun 21, 2022

You're probably done with this by now, but for future googlers:

Remark recommends you use remark-directive instead.

Recommendation: https://github.com/remarkjs/remark/blob/main/doc/plugins.md#list-of-plugins
Remark Directive: https://github.com/remarkjs/remark-directive#use

@fmonper1
Copy link

@eestein any tips on how to reproduce the callouts in this plugin?

@eestein
Copy link

eestein commented Jul 25, 2022

@fmonper1 you have to create your own plugin and the CSS classes. I'm gonna share the one I created:

const acceptableCalloutTypes = {
    'note': {cssClass: '', iconClass: 'comment-alt-lines'},
    'tip': {cssClass: 'is-success', iconClass: 'lightbulb'},
    'info': {cssClass: 'is-info', iconClass: 'info-circle'},
    'warning': {cssClass: 'is-warning', iconClass: 'exclamation-triangle'},
    'danger': {cssClass: 'is-danger', iconClass: 'siren-on'}
};

/**
 * Plugin to generate callout blocks.
 */
function calloutsPlugin() {
    return (tree) => {
        visit(tree, (node) => {
            if (node.type === 'textDirective' || node.type === 'leafDirective' || node.type === 'containerDirective') {
                if (!Object.keys(acceptableCalloutTypes).includes(node.name)) {
                    return;
                }

                const boxInfo = acceptableCalloutTypes[node.name];

                // Adding CSS classes according to the type.
                const data = node.data || (node.data = {});
                const tagName = node.type === 'textDirective' ? 'span' : 'div';
                data.hName = tagName;
                data.hProperties = h(tagName, {class: `message ${boxInfo.cssClass}`}).properties;

                // Creating the icon.
                const icon = h('i');
                const iconData = icon.data || (icon.data = {});
                iconData.hName = 'i';
                iconData.hProperties = h('i', {class: `far fa-${boxInfo.iconClass} md-callout-icon`}).properties;

                // Creating the icon's column.
                const iconWrapper = h('div');
                const iconWrapperData = iconWrapper.data || (iconWrapper.data = {});
                iconWrapperData.hName = 'div';
                iconWrapperData.hProperties = h('div', {class: 'column is-narrow'}).properties;
                iconWrapper.children = [icon];

                // Creating the content's column.
                const contentColWrapper = h('div');
                const contentColWrapperData = contentColWrapper.data || (contentColWrapper.data = {});
                contentColWrapperData.hName = 'div';
                contentColWrapperData.hProperties = h('div', {class: 'column'}).properties;
                contentColWrapper.children = [...node.children]; // Adding markdown's content block.

                // Creating the column's wrapper.
                const columnsWrapper = h('div');
                const columnsWrapperData = columnsWrapper.data || (columnsWrapper.data = {});
                columnsWrapperData.hName = 'div';
                columnsWrapperData.hProperties = h('div', {class: 'columns'}).properties;
                columnsWrapper.children = [iconWrapper, contentColWrapper];

                // Creating the wrapper for the callout's content.
                const contentWrapper = h('div');
                const wrapperData = contentWrapper.data || (contentWrapper.data = {});
                wrapperData.hName = 'div';
                wrapperData.hProperties = h('div', {class: 'message-body'}).properties;
                contentWrapper.children = [columnsWrapper];
                node.children = [contentWrapper];
            }
        });
    };
}

And the usage in my markdown files remains the same:

:::info
Message
:::

Remember that for the styling to work you must add your CSS and follow my code's structure, if you're copying/pasting.

This is the CSS FW I used:
https://bulma.io/documentation/components/message/#message-body-only

@jrolfs
Copy link

jrolfs commented Jul 30, 2023

@eestein, thank you so much for sharing your plugin! It's working wonderfully for me and you saved me a ton of time. In the spirit of continuing to help any others that come across this, I'll also share a few tweaks I made.

  • Handling attributes (e.g: :::info{title="Some Title"})
  • A little recursive helper that allows you to work with a standard hast AST (I found all the .data metadata assignment stuff really hard to follow)
  • Typed via TypeScript JSDoc
/** @typedef {import('remark-directive')} */
/** @typedef {import('unified').Plugin<[Settings], import('mdast').Root>} Plugin */

import { h, s } from 'hastscript';
import { visit } from 'unist-util-visit';

/**
 * @typedef {{ title?: string, size?: number }} Attributes
 * @typedef {Object} Settings
 */

const callouts = {
  note: { color: 'brandTan', icon: 'h-clipboard-list', title: 'Note' },
  tip: { color: 'success', icon: 'h-clipboard-check', title: 'Tip' },
  info: { color: 'primary', icon: 'i-info', title: 'Info' },
  warning: { color: 'warning', icon: 'i-alert-triangle', title: 'Warning' },
  danger: { color: 'danger', icon: 'i-alert-octagon', title: 'Danger' },
};

const iconSizeMap = /** @type {Record<number, string>} */ ({
  4: 'large',
  5: 'medium',
  6: 'small',
});
const spacingMap = /** @type {Record<number, string>} */ ({
  4: '300',
  5: '200',
  6: '100',
});

/**
 * Recursively walk a `hast` tree and decorate each node with the metadata
 * required for `remark-directive`
 *
 * @param {JSX.Element} node
 */
const decorateHast = node => {
  Object.assign(node.data ?? (node.data = {}), {
    hName: node.tagName,
    hProperties: node.properties,
  });

  if (node.children && Array.isArray(node.children)) {
    node.children.forEach(decorateHast);
  }
};

/**
 * Check if directive `name` is a supported callout
 *
 * @param {string} name
 * @returns {name is keyof typeof callouts}
 */
const isSupportedCallout = name => Object.keys(callouts).includes(name);

/**
 * Remark plugin to support block-quote style callouts with the same syntax
 * introduced in `remark-admonition` which is apparently no longer supported in
 * the latest version of Remark.
 *
 * @see {@link https://github.com/elviswolcott/remark-admonitions/issues/49#issuecomment-1162400177}
 * @see {@link https://github.com/remarkjs/remark-directive#examples}
 *
 * @type {Plugin}
 */
const plugin = () => tree => {
  visit(tree, node => {
    if (
      !(
        node.type === 'textDirective' ||
        node.type === 'leafDirective' ||
        node.type === 'containerDirective'
      )
    ) {
      return;
    }

    if (!isSupportedCallout(node.name)) return;

    // Grab attributes from the directive and apply defaults

    const { color, icon, title: defaultTitle } = callouts[node.name];
    const { title = defaultTitle, size = '6' } = node.attributes ?? {};

    // Next, build up all of the elements that are going to make up the
    // callout DOM structure in `hast`. These are separated out as nesting all
    // of the `hastscript` `h` and `s` calls would get a little unweildy
    // compared to something like JSX.

    // Icon -----------------

    const iconSize = iconSizeMap[size] ?? 'small';
    const svg = s(
      'svg',
      {
        xmlns: 'http://www.w3.org/2000/svg',
        class: `icon icon-${iconSize} text-${color}-700 m-0`,
      },
      [s('use', { 'xlink:href': `/icons/all.svg#${icon}` })],
    );

    // Heading --------------

    const heading = h(
      `h${size}`,
      { class: `m-0 fw-bodySemiBold text-${color}-700` },
      [{ type: 'text', value: title }],
    );

    // Title --------------

    const spacing = spacingMap[size] ?? '100';
    const titleContainer =
      // Wrapping just the title container in `.hover-bootstrap`
      // to apply the Bootstrap theme for the icon and heading
      h('span', { class: 'hover-bootstrap' }, [
        h(
          'span',
          {
            class: `d-inline-flex align-items-center gap-${spacing} mb-${spacing}`,
          },
          [svg, heading],
        ),
      ]);

    // Body --------------

    const body = h('div', { class: 'callout-body' }, [
      titleContainer,
      // Actual Markdown content for the callout
      h('div', { class: 'column' }, [...node.children]),
    ]);

    // Mutate the actual node we're visiting to attach the `hast`
    // tree we've built up with all of the elements we're inserting

    node.tagName = node.type === 'textDirective' ? 'span' : 'blockquote';
    node.properties = { class: `message`, 'data-color-scheme': color };
    node.children = [body];

    // Finally we need to walk this whole `hast` tree we've built and augment
    // each node with the metadata that `remark-directive` requires

    decorateHast(node);
  });
};

export default plugin;

Usage

:::note

The icons must always come _after_ the `<input>` element

:::
:::note{title="Anchor must accept a ref"}

As with [`trigger`](#basic-usage), if you pass a custom component inside
`anchor` ensure that it uses `forwardRef` so the popover is triggered
successfully.

:::
:::info{title="Core Concepts" size="5"}

By the end of this walkthrough, you'll have a solid understanding of:

- Contributing a bug fix
- Adding changelog information associated with your fix
- Getting your fix released using continuous integration

:::

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

4 participants