elviswolcott / remark-admonitions

Add admonitions support to Remarkable
MIT License
109 stars 19 forks source link

Is this working with unified v10? #49

Open factoidforrest opened 2 years ago

factoidforrest commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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

eestein commented 2 years ago

@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 commented 1 year ago

@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.

/** @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

:::