Introduction
When working with Markdown in Astro, you’ll often need to transform or create HTML elements programmatically. This is where hastscript comes in - a powerful utility that allows you to build HTML Abstract Syntax Trees (HAST) using a JSX-like syntax.
In this guide, we’ll explore how hastscript is used in Astro projects, particularly in custom remark and rehype plugins.
What is hastscript?
hastscript (Hypertext Abstract Syntax Tree Script) is a utility that creates HAST nodes - the same structure used by rehype and remark plugins to represent HTML. Think of it as a way to write HTML in JavaScript without using strings.
Why Use hastscript?
- Type-safe: Creates proper HAST nodes with correct structure
- Clean syntax: Similar to JSX but in pure JavaScript
- Integration: Works seamlessly with remark/rehype ecosystem
- Maintainable: Easier to modify than string concatenation
Installation
npm install hastscriptBasic Syntax
The basic syntax of hastscript is:
h(tagName, properties, children)Where:
tagName: HTML element name (string)properties: Object containing attributes (optional)children: Array of child nodes or text content (optional)
Simple Examples
Creating a Div Element
import { h } from 'hastscript';
// Simple divconst node = h('div', 'Hello World');
// Result (HAST):// {// type: 'element',// tagName: 'div',// properties: {},// children: [{ type: 'text', value: 'Hello World' }]// }Adding Classes and Attributes
// Div with classconst divWithClass = h('div', { className: 'container' }, 'Content');
// Multiple classesconst multiClass = h('div', { className: ['card', 'shadow-lg'] }, 'Card content');
// With ID and data attributesconst withAttrs = h('div', { id: 'main', className: 'wrapper', dataTheme: 'dark'}, 'Content');Creating Links
// Simple linkconst link = h('a', { href: 'https://example.com' }, 'Click here');
// Link with target and relconst externalLink = h('a', { href: 'https://example.com', target: '_blank', rel: 'noopener noreferrer'}, 'External link');Nested Elements
You can create complex nested structures by passing arrays of child elements:
import { h } from 'hastscript';
const card = h('div', { className: 'card' }, [ h('h2', { className: 'card-title' }, 'Card Title'), h('p', { className: 'card-description' }, 'This is a description'), h('button', { className: 'btn' }, 'Click Me')]);Real-World Usage in Astro
Custom Remark Plugin Example
Here’s how hastscript is used in a custom remark plugin to create custom components:
import { h } from 'hastscript';import { visit } from 'unist-util-visit';
export function remarkCustomButton() { return (tree) => { visit(tree, 'text', (node, index, parent) => { const match = node.value.match(/\[button:(.+?)\]\((.+?)\)/);
if (match) { const [, text, href] = match;
// Create button element using hastscript const buttonNode = h('a', { href: href, className: ['btn', 'btn-primary'],8 collapsed lines
role: 'button' }, text);
parent.children[index] = buttonNode; } }); };}Creating Alert/Admonition Boxes
function createAdmonition(type, title, content) { const iconMap = { note: '📝', tip: '💡', warning: '⚠️', danger: '🚨' };
return h('div', { className: ['admonition', `admonition-${type}`], dataType: type }, [ h('div', { className: 'admonition-title' }, [ h('span', { className: 'admonition-icon' }, iconMap[type]), h('strong', title)7 collapsed lines
]), h('div', { className: 'admonition-content' }, content) ]);}
// Usageconst noteBox = createAdmonition('note', 'Important Note', 'This is a note');Working with Custom Elements
Creating Code Blocks with Syntax Highlighting
function createCodeBlock(code, language) { return h('div', { className: 'code-wrapper' }, [ h('div', { className: 'code-header' }, [ h('span', { className: 'language-badge' }, language), h('button', { className: 'copy-button', dataCode: code }, 'Copy') ]), h('pre', [ h('code', { className: `language-${language}` }, code) ]) ]);}Creating Cards with Images
function createImageCard(imgSrc, title, description, linkUrl) { return h('div', { className: 'image-card' }, [ h('img', { src: imgSrc, alt: title, className: 'card-image', loading: 'lazy' }), h('div', { className: 'card-body' }, [ h('h3', { className: 'card-title' }, title), h('p', { className: 'card-description' }, description), h('a', { href: linkUrl, className: 'card-link' }, 'Read more →')3 collapsed lines
]) ]);}hastscript in Astro Plugin Example
Here’s a complete example of a remark plugin using hastscript in an Astro project:
import { h } from 'hastscript';import { visit } from 'unist-util-visit';
export function remarkGithubCard() { return (tree) => { visit(tree, 'text', (node, index, parent) => { // Match pattern: [github:username/repo] const match = node.value.match(/\[github:([^/]+)\/([^\]]+)\]/);
if (match) { const [, username, repo] = match; const repoUrl = `https://github.com/${username}/${repo}`;
// Create GitHub card using hastscript28 collapsed lines
const githubCard = h('div', { className: 'github-card' }, [ h('div', { className: 'github-card-header' }, [ h('svg', { className: 'github-icon', viewBox: '0 0 16 16', width: '16', height: '16' }, [ h('path', { fill: 'currentColor', d: 'M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z' }) ]), h('span', { className: 'github-repo-name' }, `${username}/${repo}`) ]), h('a', { href: repoUrl, target: '_blank', rel: 'noopener noreferrer', className: 'github-link' }, 'View on GitHub →') ]);
parent.children[index] = githubCard; } }); };}Using the Plugin in Astro Config
import { defineConfig } from 'astro/config';import { remarkGithubCard } from './src/plugins/remark-github-card.js';
export default defineConfig({ markdown: { remarkPlugins: [ remarkGithubCard ] }});Common Patterns
Creating Containers with Multiple Children
// Creating a tab containerconst tabs = h('div', { className: 'tabs' }, [ h('div', { className: 'tab-headers' }, [ h('button', { className: 'tab-button active', dataTab: 'tab1' }, 'Tab 1'), h('button', { className: 'tab-button', dataTab: 'tab2' }, 'Tab 2'), h('button', { className: 'tab-button', dataTab: 'tab3' }, 'Tab 3') ]), h('div', { className: 'tab-content' }, [ h('div', { className: 'tab-pane active', id: 'tab1' }, 'Content 1'), h('div', { className: 'tab-pane', id: 'tab2' }, 'Content 2'), h('div', { className: 'tab-pane', id: 'tab3' }, 'Content 3') ])]);Conditional Attributes
function createButton(text, options = {}) { const { href, disabled, variant = 'primary' } = options;
const props = { className: ['btn', `btn-${variant}`], ...(disabled && { disabled: true }), ...(href && { href }) };
return h(href ? 'a' : 'button', props, text);}
// Usageconst linkButton = createButton('Click', { href: '/page', variant: 'secondary' });const disabledButton = createButton('Disabled', { disabled: true });hastscript Shortcuts
Using Selectors (CSS-like syntax)
// Instead of:h('div', { className: 'container' })
// You can write:h('div.container')
// Multiple classes:h('div.card.shadow-lg')
// With ID:h('div#main.container')
// Combination:h('section#about.section.dark-theme')Tips and Best Practices
1. Keep Components Reusable
// Good - reusable functionfunction createHighlight(text, color) { return h('span', { className: 'highlight', style: `background-color: ${color}` }, text);}
// Bad - hardcoded valuesconst highlight = h('span', { className: 'highlight', style: 'background-color: yellow'}, 'hardcoded text');2. Type Safety with TypeScript
import { h } from 'hastscript';import type { Element } from 'hast';
function createAlert(message: string, type: 'info' | 'warning' | 'error'): Element { return h('div', { className: ['alert', `alert-${type}`], role: 'alert' }, message);}3. Handle Text Safely
// Safe text handlingfunction createParagraph(text) { return h('p', { className: 'paragraph' }, typeof text === 'string' ? text : String(text) );}4. Compose Complex Structures
function createArticle({ title, author, date, content }) { return h('article', { className: 'blog-post' }, [ createHeader(title, author, date), createContent(content), createFooter() ]);}
function createHeader(title, author, date) { return h('header', { className: 'post-header' }, [ h('h1', title), h('div', { className: 'post-meta' }, [ h('span', { className: 'author' }, author), h('time', { dateTime: date }, formatDate(date)) ])2 collapsed lines
]);}Debugging hastscript Nodes
To see what HAST structure is created:
import { h } from 'hastscript';import { toHtml } from 'hast-util-to-html';
const node = h('div.container', [ h('h1', 'Hello'), h('p', 'World')]);
// Convert to HTML string for debuggingconsole.log(toHtml(node));// Output: <div class="container"><h1>Hello</h1><p>World</p></div>
// Inspect HAST structureconsole.log(JSON.stringify(node, null, 2));Common Use Cases in Astro
1. Custom Markdown Syntax
Create custom syntax that transforms into HTML components
2. Plugin Development
Build remark/rehype plugins for content transformation
3. Component Wrapping
Wrap existing content with custom containers
4. Dynamic Content Generation
Generate HTML structures based on frontmatter or external data
5. Accessibility Enhancements
Add ARIA attributes and semantic HTML programmatically
Resources
Conclusion
hastscript is an essential tool when working with Markdown processing in Astro. It provides a clean, type-safe way to create HTML structures programmatically, making your plugins more maintainable and reliable.
By mastering hastscript, you can:
- Create custom Markdown extensions
- Build reusable content components
- Transform content dynamically
- Maintain clean, readable code
Start experimenting with hastscript in your next Astro project, and you’ll discover how powerful it can be for content transformation!
Have questions or suggestions? Feel free to reach out or leave a comment below. Happy coding! 🚀