Generate custom remark/rehype plugins based on Fuwari patterns
Generates custom remark/rehype plugins following Fuwari's patterns for Markdown directives and components.
/plugin marketplace add Linaqruf/cc-plugins/plugin install fuwari-md@cc-plugins<type>Generate custom remark or rehype plugins following Fuwari's proven patterns.
If argument provided (e.g., /component directive), use that type.
Otherwise, ask:
What type of component do you want to create?
- Container Directive (:::name ... :::)
- Leaf Directive (::name{attrs})
- Text Directive (:name[content])
- Rehype Component (custom HTML rendering)
Ask relevant questions based on type:
For Directives:
What is the directive name? (e.g., "youtube", "tweet", "figure")
What attributes should it accept? (e.g., "id", "url", "caption")
Should it have children/content? (yes/no)
For Rehype Components:
What HTML element should it render? (e.g., "div", "figure", "iframe")
What CSS classes should be applied?
Based on Fuwari's patterns, generate:
// rehype-component-{name}.mjs
import { h } from 'hastscript';
/**
* Creates a {Name} component.
*
* Markdown syntax:
* :::{name}[Optional Title]
* Content here
* :::
*
* Or with attributes:
* :::{name}{attr1="value1" attr2="value2"}
* Content here
* :::
*/
export function {Name}Component(properties, children) {
if (!Array.isArray(children) || children.length === 0) {
return h('div', { class: 'hidden' }, 'Invalid {name} directive');
}
let title = null;
if (properties?.['has-directive-label']) {
title = children[0];
children = children.slice(1);
title.tagName = 'div';
}
return h('div', { class: '{name}-container' }, [
title && h('div', { class: '{name}-title' }, title),
h('div', { class: '{name}-content' }, children),
]);
}
// rehype-component-{name}.mjs
import { h } from 'hastscript';
/**
* Creates a {Name} component.
*
* Markdown syntax:
* ::{name}{attr="value"}
*/
export function {Name}Component(properties, children) {
if (Array.isArray(children) && children.length !== 0) {
return h('div', { class: 'hidden' }, '{name} must be a leaf directive');
}
const { attr1, attr2 } = properties;
if (!attr1) {
return h('div', { class: 'hidden' }, 'Missing required attribute: attr1');
}
return h('div', { class: '{name}-wrapper' }, [
// Render your component here
h('span', { class: '{name}-content' }, `Value: ${attr1}`),
]);
}
// rehype-component-{name}.mjs
import { h } from 'hastscript';
/**
* Creates a {Name} inline component.
*
* Markdown syntax:
* :{name}[inline content]
*/
export function {Name}Component(properties, children) {
return h('span', { class: '{name}' }, children);
}
// In your config (astro.config.mjs, etc.):
import { {Name}Component } from './plugins/rehype-component-{name}.mjs';
// Add to rehypePlugins:
[
rehypeComponents,
{
components: {
// ... existing components
{name}: {Name}Component,
},
},
],
.{name}-container {
/* Container styling */
border: 1px solid var(--border-color, #e1e4e8);
border-radius: 0.5rem;
padding: 1rem;
margin: 1rem 0;
}
.{name}-title {
font-weight: 600;
margin-bottom: 0.5rem;
}
.{name}-content {
/* Content styling */
}
// rehype-component-youtube.mjs
import { h } from 'hastscript';
export function YoutubeComponent(properties, children) {
if (Array.isArray(children) && children.length !== 0) {
return h('div', { class: 'hidden' }, 'youtube must be leaf directive');
}
const { id, title = 'YouTube Video' } = properties;
if (!id) {
return h('div', { class: 'hidden' }, 'Missing video ID');
}
return h('div', { class: 'youtube-embed' }, [
h('iframe', {
src: `https://www.youtube.com/embed/${id}`,
title,
frameborder: '0',
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
allowfullscreen: true,
loading: 'lazy',
}),
]);
}
Usage: ::youtube{id="dQw4w9WgXcQ" title="Rick Astley"}
// rehype-component-tweet.mjs
import { h } from 'hastscript';
export function TweetComponent(properties, children) {
const { id, user } = properties;
if (!id || !user) {
return h('div', { class: 'hidden' }, 'Tweet requires id and user');
}
const tweetUrl = `https://twitter.com/${user}/status/${id}`;
const cardUuid = `TW${Math.random().toString(36).slice(-6)}`;
return h('div', { class: 'tweet-embed', id: cardUuid }, [
h('blockquote', { class: 'twitter-tweet' }, [
h('a', { href: tweetUrl }, 'Loading tweet...'),
]),
h('script', {
async: true,
src: 'https://platform.twitter.com/widgets.js',
}),
]);
}
Usage: ::tweet{user="astaborsk" id="1234567890"}
// rehype-component-figure.mjs
import { h } from 'hastscript';
export function FigureComponent(properties, children) {
const { src, alt = '', caption } = properties;
if (!src) {
return h('div', { class: 'hidden' }, 'Figure requires src attribute');
}
return h('figure', { class: 'md-figure' }, [
h('img', { src, alt, loading: 'lazy' }),
caption && h('figcaption', caption),
]);
}
Usage: ::figure{src="/image.jpg" alt="Description" caption="Figure 1: My image"}
After gathering requirements:
If the user specifies an unrecognized component type:
List valid types:
directive or container - Container directive (:::name ... :::)leaf - Leaf directive (::name{attrs})inline or text - Text directive (:name[content])rehype - Custom rehype componentAsk which type they meant
If the directive name conflicts with existing directives:
note, tip, github are reserved)If the user doesn't provide enough details:
Before creating a new plugin file:
rehype-component-{name}.mjs already existsAfter generating the component:
hastscript is installed (required for all components)