Outlining the technical blueprint for adding custom shortcode support (e.g., [gallery ids="1,2,3"] or [trumpet content="description"]) to the Traven WYSIWYM Markdown Editor.
Integrating custom shortcodes follows the established decoupling between editor logic (parsing) and theme aesthetics (styling).
graph TD
Source[Raw Markdown Text] -->|1. Parse| Lezer[Lezer Markdown Parser]
Lezer -->|2. Generate AST| AST[Abstract Syntax Tree Nodes]
AST -->|3. Cursor Check| Decorator[wysiwym.js Interactive Decorator]
Decorator -->|Active Cursor: Show Code| Text[Raw Text Rendering]
Decorator -->|Inactive Cursor: Hide Code| Widget[Replace Widget Injection]
Widget -->|4. Render DOM| DOM[Shortcode Preview DOM]
DOM -->|5. Apply Skin| CSS[packages/core/assets/skins/*.css]
- Detection & AST Mapping: Standard Markdown syntax trees (via
@lezer/markdown) do not recognize custom shortcodes. Traven extends the Lezer parser with grammar extensions (e.g.src/shortcode-parser.js) to parse them into first-class AST nodes.wysiwym.jsthen traverses these AST nodes to identify shortcode blocks. - State Management: It tracks if the cursor is currently inside a shortcode's range.
- Interactive Hiding: When the cursor is outside, it collapses the shortcode syntax markers using
Decoration.replace({})and mounts a CodeMirror replacementWidgetType. When the cursor enters the shortcode, the raw source string is instantly revealed for editing.
- Replace Widgets: CodeMirror
WidgetTypeclasses will represent the shortcodes visually (e.g.,GalleryShortcodeWidget,YoutubeShortcodeWidget). - Interactive DOM: These widgets return DOM nodes representing the shortcode's output. They can fetch media previews asynchronously or display interactive UI elements (like placeholder cards).
The DOM elements rendered by the widgets are assigned semantic classes (e.g., .cm-wysiwym-shortcode-widget, .cm-wysiwym-gallery-preview).
- Skin Decoupling: The CSS skins handle color palettes, border styling, transition animations, and shadow treatments:
- Neutral Skin: Renders the shortcode preview as a flat, distraction-free container with gray slate borders (
#cbd5e1) and a clean background (#f8fafc). - Colorful Skin: Renders the shortcode preview with custom brand borders, colorful icon highlights, and transition effects.
- Neutral Skin: Renders the shortcode preview as a flat, distraction-free container with gray slate borders (
Create a helper function to find shortcodes in the document state:
function findShortcodes(state) {
const shortcodes = [];
const text = state.doc.toString();
// Regex matches bracketed shortcodes: [name key="val"]
const regex = /\[([a-z_-]+)\s+([^\]]+)\]/g;
let match;
while ((match = regex.exec(text)) !== null) {
shortcodes.push({
name: match[1],
rawAttrs: match[2],
from: match.index,
to: match.index + match[0].length
});
}
return shortcodes;
}During decoration generation inside wysiwym.js:
const shortcodes = findShortcodes(state);
for (const sc of shortcodes) {
const isCursorInside = cursorHead >= sc.from && cursorHead <= sc.to;
if (!isCursorInside) {
// Inject the custom visual preview widget
collected.push({
from: sc.from,
to: sc.to,
deco: Decoration.replace({
widget: new ShortcodeWidget(sc.name, sc.rawAttrs),
block: true
})
});
}
}Implement the widget subclass:
class ShortcodeWidget extends WidgetType {
constructor(name, attrs) {
super();
this.name = name;
this.attrs = attrs;
}
toDOM() {
const container = document.createElement("div");
container.className = `cm-wysiwym-shortcode-widget cm-wysiwym-shortcode-${this.name}`;
// Add visual details (like an icon and properties tag)
container.innerHTML = `
<div class="shortcode-header">
<span class="shortcode-icon">⚡</span>
<span class="shortcode-title">${this.name.toUpperCase()} SHORTCODE</span>
</div>
<div class="shortcode-body">
<code>${this.attrs}</code>
</div>
`;
return container;
}
}To support skinning, skins should declare definitions for the following selectors:
/* Base Container for all shortcodes */
.cm-wysiwym-shortcode-widget {
border-radius: 8px;
padding: 12px 16px;
font-family: inherit;
margin: 8px 0;
}
/* Neutral Skin Definitions */
.neutral-theme-scope .cm-wysiwym-shortcode-widget {
background-color: #f8fafc;
border: 1px solid #cbd5e1;
color: #475569;
}
/* Colorful Skin Definitions */
.colorful-theme-scope .cm-wysiwym-shortcode-widget {
background-color: #fff0e8; /* Rust wash tint */
border: 1px dashed #cc4a0a; /* Rust accent dashed border */
color: #a83808;
}
---
## 4. Built-in Shortcode: Custom Image
Traven features a native, built-in custom `[image]` shortcode supporting advanced alignment, sizing, alt text, captions, and custom CSS classes:
```markdown
[image src="photo.jpg" align="right" size="medium" alt="Screen reader text" caption="Visible caption text" class="shadow-lg"]- Fully Backwards-Compatible: The custom shortcode is completely optional. Traven remains fully backwards-compatible and non-breaking for standard legacy Markdown syntax (
). Traditional Markdown image declarations parse, render, and compile exactly as they did previously. - Separation of Presentation Concerns: In fallback HTML previews and rendering (
getContentHtml()), the shortcode compiles to a clean, semantic<img>element with no inline style attributes. Layout attributes (like width, float, margins) are mapped exclusively to class selectors (.align-[alignment],.size-[size], and.traven-image-shortcode) managed in the theme CSS/skins. - Toolbar Insert Toggle: The image insertion modal contains a sliders-icon toggle to switch between Advanced mode (inserting custom
[image]shortcodes with fields for caption, classes, alignment, and size) and Legacy mode (inserting standardMarkdown). - Lezer Parser Integration: Attributes are parsed directly using a custom inline Lezer parser (
src/shortcode-parser.js) creating a structured AST node representation. This allows the editor to skip delimiter syntax boundaries cleanly during arrow navigation.