r4ai / remark-callout

A remark plugin to add obsidian style callouts to markdown
https://r4ai.github.io/remark-callout/
MIT License
3 stars 1 forks source link

How to add icons in callout #112

Closed eikowagenknecht closed 1 month ago

eikowagenknecht commented 1 month ago

In Obsidian, a foldable callout looks like this:

<div data-callout-metadata="" data-callout-fold="+" data-callout="quote" data-tag-name="div" class="el-div">
    <div data-callout-metadata="" data-callout-fold="+" data-callout="quote" class="callout is-collapsible" dir="auto">
        <div class="callout-title" dir="auto">
            <div class="callout-icon" dir="auto">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-quote">
                    <path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"/>
                    <path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"/>
                </svg>
            </div>
            <div class="callout-title-inner" dir="auto">Foldable callout with custom title</div>
            <div class="callout-fold" dir="auto">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-chevron-down">
                    <path d="m6 9 6 6 6-6"/>
                </svg>
            </div>
        </div>
        <div class="callout-content" dir="auto" style="">
            <p dir="auto">Example callout with a custom title that is also foldable</p>
        </div>
    </div>
</div>

I want to achieve a very similar look and behaviour. The closest I can get it with your plugin is:

<div class="callout" data-callout="quote" open="">
<div class="callout-title">Foldable callout, extended with custom title</div>
<div class="callout-content">
<p>Example callout with a custom title that is also foldable</p>
</div>
</div>

Is it possible to add inline SVG icons like Obsidian does (without resorting to MDX and Astro components)?

eikowagenknecht commented 1 month ago

It would be nice, if there was a possibility to add custom nodes before / after the content in the title to achieve something like this (example from Obsidian with an icon and the dropdown icon for the collapsible element):

<div class="callout-title">
  <div class="callout-icon"><svg>...<svg></div>
  <div class="callout-title-inner">This is the callout title</div>
  <div class="callout-fold"><svg>...<svg></div>
</div>
r4ai commented 1 month ago

Hi @eikowagenknecht! Thanks for the feedback! I'm sorry it took me a while to respond to you.

I agree, it would be useful to be able to add icons as HTML elements as well. It might be good to add options.icon and options.foldIcon to the options to add the icon element as hast or HTML string.

I would like to implement this tomorrow if possible.

r4ai commented 1 month ago

For your information, although not an HTML element, you can also use CSS Pseudo-elements to display icons. This is the method used to display icons in the Playground.

https://stackoverflow.com/questions/19255296/is-there-a-way-to-use-svg-as-content-in-a-pseudo-element-before-or-after

eikowagenknecht commented 1 month ago

Thank you. I am aware that adding CSS Pseudo-elements can be used as a workaround, but for styling or attaching event handlers, real elements are preferable :-)

r4ai commented 1 month ago

I am currently developing a PR to add the feature to allow icons to be embedded as HTML string or hast, so I think it will be ready soon. #128

eikowagenknecht commented 1 month ago

Thank you, works great!

In case anyone stumbles upon this wanting the "exact Obsidian experience", here's my code using the exact same icons obsidian uses:

function getCalloutIcon(type) {
  switch (type) {
    case "abstract":
    case "summary":
    case "tldr":
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-clipboard-list"><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><path d="M12 11h4"></path><path d="M12 16h4"></path><path d="M8 11h.01"></path><path d="M8 16h.01"></path></svg>';
    case "info":
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-info"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg>';
    case "todo":
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-check-circle-2"><circle cx="12" cy="12" r="10"></circle><path d="m9 12 2 2 4-4"></path></svg>';
    case "tip":
    case "hint":
    case "important":
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-flame"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"></path></svg>';
    case "success":
    case "check":
    case "done":
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-check"><path d="M20 6 9 17l-5-5"></path></svg>';
    case "question":
    case "help":
    case "faq":
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><path d="M12 17h.01"></path></svg>';
    case "warning":
    case "caution":
    case "attention":
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-alert-triangle"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>';
    case "failure":
    case "fail":
    case "missing":
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-x"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>';
    case "danger":
    case "error":
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>';
    case "bug":
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-bug"><path d="m8 2 1.88 1.88"></path><path d="M14.12 3.88 16 2"></path><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"></path><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"></path><path d="M12 20v-9"></path><path d="M6.53 9C4.6 8.8 3 7.1 3 5"></path><path d="M6 13H2"></path><path d="M3 21c0-2.1 1.7-3.9 3.8-4"></path><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"></path><path d="M22 13h-4"></path><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"></path></svg>';
    case "example":
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-list"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>';
    case "quote":
    case "cite":
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-quote"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"></path><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path></svg>';
    case "note":
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-pencil"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path><path d="m15 5 4 4"></path></svg>';
    default: // Note icon as fallback as well
      return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-pencil"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"></path><path d="m15 5 4 4"></path></svg>';
  }
}
[
  remarkCallout,
  {
    root: (callout) => ({
      tagName: "blockquote",
      properties: {
        // dataCalloutMetadata: true,
        dataCalloutFold: callout.isFoldable
          ? callout.defaultFolded
            ? "-"
            : "+"
          : false,
        dataCallout: callout.type,
        dataCalloutDefaultFolded:
          callout.defaultFolded == null
            ? undefined
            : String(callout.defaultFolded),
        class: callout.isFoldable
          ? callout.defaultFolded
            ? "not-prose callout is-collapsible is-collapsed"
            : "not-prose callout is-collapsible"
          : "not-prose callout",
      },
    }),
    title: (callout) => ({
      tagName: "div",
      properties: {
        dataCalloutTitle: true,
        class: "callout-title flex flex-row gap-2 items-center",
      },
    }),
    body: () => ({
      tagName: "div",
      properties: {
        dataCalloutTitle: true,
        class: "callout-content",
      },
    }),
    icon: (callout) => ({
      tagName: "div",
      properties: {
        className: "callout-icon",
      },
      children: getCalloutIcon(callout.type),
    }),
    foldIcon: (callout) =>
      callout.isFoldable
        ? {
            tagName: "div",
            properties: {
              className: "callout-fold-icon",
            },
            children:
              '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"></path></svg>',
          }
        : undefined,
  },
]