feat: Add macro support

New: macros.ts with MacroDef, parseBlockMacro, matchInlineMacro,
buildMacroTags, processInlineMacros.

Macro syntax:
  @user                     — bare, no args
  @user()                   — empty parens, same as bare
  @npc(Goblin King)         — self-closing with args
  @style(box center         — block: no closing paren on first line
  Content here.             — content on subsequent lines
  )                         — closing paren on its own line

Unknown macro names now render as an error:
  <span class="ribbit-error">Unknown macro: @bogus</span>

The verbatim keyword causes the contents to render as literals and also
preserves line breaks.
This commit is contained in:
gsb
2026-04-29 03:03:58 +00:00
parent df49ce7545
commit 86d59877f1
5 changed files with 382 additions and 5 deletions
+8 -4
View File
@@ -6,6 +6,7 @@ import { HopDown } from './hopdown';
import { defaultTheme } from './default-theme';
import { ThemeManager } from './theme-manager';
import { RibbitEmitter, type RibbitEventMap } from './events';
import { buildMacroTags, type MacroDef } from './macros';
import type { RibbitTheme } from './types';
export interface RibbitSettings {
@@ -15,6 +16,7 @@ export interface RibbitSettings {
currentTheme?: string;
themes?: RibbitTheme[];
themesPath?: string;
macros?: MacroDef[];
on?: Partial<RibbitEventMap>;
}
@@ -71,12 +73,14 @@ export class Ribbit {
converter: HopDown;
themesPath: string;
private emitter: RibbitEmitter;
private macros: MacroDef[];
constructor(settings: RibbitSettings) {
this.api = settings.api || null;
this.element = document.getElementById(settings.editorId || 'ribbit')!;
this.themesPath = settings.themesPath || './themes';
this.emitter = new RibbitEmitter();
this.macros = settings.macros || [];
this.states = {
VIEW: 'view',
};
@@ -89,8 +93,8 @@ export class Ribbit {
this.themes = new ThemeManager(defaultTheme, this.themesPath, (theme, previous) => {
this.theme = theme;
this.converter = theme.tags
? new HopDown({ tags: theme.tags })
: new HopDown();
? new HopDown({ tags: theme.tags, macros: this.macros })
: new HopDown({ macros: this.macros });
this.cachedHTML = null;
this.emitter.emit('themeChange', {
current: theme,
@@ -110,8 +114,8 @@ export class Ribbit {
this.themes.set(activeName);
this.theme = this.themes.current();
this.converter = this.theme.tags
? new HopDown({ tags: this.theme.tags })
: new HopDown();
? new HopDown({ tags: this.theme.tags, macros: this.macros })
: new HopDown({ macros: this.macros });
(settings.plugins || []).forEach(plugin => {
this.enabledPlugins[plugin.name] = new plugin({