v0.4 Phase 4: callout tags

- Extend renderer.code to match `mdcms <type>` fence syntax (e.g. ```mdcms callout-info)
- Extend parseMdcmsTag to capture body lines after the key-value block
- Add renderCalloutTag: icon + title row, markdown body, colour CSS vars
- Add hexToRgba helper for low-opacity background colour
- Make themeConfig module-level so callout renderer can read callout defaults
- Add callout CSS: left border, title row flex layout, icon fill
- Add reusable message: key support with category-aware language resolution
- Add aitranslation callout message to config.yml for test
- Update home.md with full Phase 4 test cases

https://claude.ai/code/session_01UP8Wo2CKPNhvvTkzX48CWF
This commit is contained in:
Claude 2026-05-17 17:43:27 +00:00
parent 8a39bc31e9
commit 690965df7d
No known key found for this signature in database
3 changed files with 192 additions and 51 deletions

View file

@ -43,3 +43,13 @@ theme: theme.yml # presentational config — edit theme.yml to custo
# ──────────────────────────────────
# search: true
# default-theme: system # light | dark | system
# ──────────────────────────────────
# Reusable callout messages (optional)
# ──────────────────────────────────
callouts:
aitranslation:
type: warning
en:
title: "PLEASE NOTE:"
text: This page has been translated with artificial intelligence. It has not been reviewed by staff yet.

View file

@ -862,6 +862,32 @@ body {
}
.post-load-more:hover { background: var(--nav-hover-bg); }
/* ── Callout tags ──────────────────────────────────────── */
.mdcms-callout {
border-left: 4px solid var(--callout-primary, var(--accent));
background: var(--callout-bg, transparent);
border-radius: 0 0.4rem 0.4rem 0;
padding: 0.75rem 1rem;
margin: 1rem 0;
}
.mdcms-callout-title {
display: flex;
align-items: center;
gap: 0.45rem;
font-weight: 700;
color: var(--callout-primary, var(--accent));
margin-bottom: 0.4rem;
}
.mdcms-callout-title .mdcms-icon svg {
fill: var(--callout-primary, var(--accent));
width: 1.2em;
height: 1.2em;
display: block;
}
.mdcms-callout-body { margin: 0; }
.mdcms-callout-body > :first-child { margin-top: 0; }
.mdcms-callout-body > :last-child { margin-bottom: 0; }
@media print {
.sidebar, .topbar, .scroll-top, .hamburger,
.mobile-header, .theme-toggle, .search-container { display: none !important; }
@ -893,6 +919,7 @@ body {
let searchIndex = [];
let fuseInstance = null;
let currentPage = null;
let themeConfig = {};
// Category state (phase 3)
let categoriesUse = false;
@ -1367,8 +1394,11 @@ body {
} else {
codeText = code; codeLang = lang;
}
if (codeLang === 'mdcms') {
const tag = parseMdcmsTag(codeText);
// Match both ```mdcms (type in content) and ```mdcms callout-info (type in fence)
if (codeLang && (codeLang === 'mdcms' || codeLang.startsWith('mdcms '))) {
const fenceType = codeLang === 'mdcms' ? '' : codeLang.slice('mdcms '.length).trim();
const fullText = fenceType ? (fenceType + '\n' + (codeText || '')) : (codeText || '');
const tag = parseMdcmsTag(fullText);
const encoded = JSON.stringify(tag).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
return '<div class="mdcms-tag" data-config="' + encoded + '"></div>';
}
@ -1487,11 +1517,18 @@ function fmtDatetime(dtStr) {
var lines = text.trim().split('\n');
var tagName = lines[0].trim();
var options = {};
var bodyStart = lines.length;
for (var i = 1; i < lines.length; i++) {
var m = lines[i].match(/^\s*([a-z\-]+)\s*:\s*(.+)$/i);
if (m) options[m[1].toLowerCase()] = m[2].trim();
var m = lines[i].match(/^\s*([a-z\-]+)\s*:\s*(.*)$/i);
if (m) {
options[m[1].toLowerCase()] = m[2].trim();
} else {
bodyStart = i;
break;
}
}
return { tagName: tagName, options: options };
var body = lines.slice(bodyStart).join('\n').trim();
return { tagName: tagName, options: options, body: body };
}
function parsePostTagName(name) {
@ -1769,11 +1806,96 @@ function fmtDatetime(dtStr) {
renderYear();
}
// Callout type defaults (fallback when theme.yml has no callouts block)
const CALLOUT_DEFAULTS = {
info: { icon: 'info', colour: '#2563EB' },
warning: { icon: 'warning', colour: '#D97706' },
success: { icon: 'success', colour: '#16A34A' },
error: { icon: 'error', colour: '#DC2626' },
};
function renderCalloutTag(container, tag) {
var typeMatch = tag.tagName.match(/^callout-(info|warning|success|error)$/);
var calloutType = typeMatch ? typeMatch[1] : 'info';
var opts = tag.options;
var msgKey = opts.message || null;
var title = opts.title || null;
var iconName = opts.icon || null;
var bodyMd = tag.body || '';
// Resolve message: key — config.yml callouts block
if (msgKey) {
var msgDefs = config.callouts || {};
var msgDef = msgDefs[msgKey];
if (msgDef) {
// Override callout type from message definition
if (msgDef.type) calloutType = msgDef.type;
// Language resolution: activeCategory → defaultCategoryCode → first key
var lang = activeCategory || defaultCategoryCode;
var langEntry = (lang && msgDef[lang]) || msgDef[defaultCategoryCode];
if (!langEntry) {
var keys = Object.keys(msgDef).filter(function(k) { return k !== 'type'; });
langEntry = msgDef[keys[0]];
}
if (langEntry) {
title = langEntry.title || null;
bodyMd = langEntry.text || '';
}
if (opts.title || tag.body) {
console.warn('[mdcms] callout: message: key takes precedence; inline title/body ignored.');
}
}
}
// Get callout colours/icon from theme.yml callouts block, then fallback
var themeCallouts = (themeConfig.callouts || {})[calloutType] || {};
var fallback = CALLOUT_DEFAULTS[calloutType] || CALLOUT_DEFAULTS.info;
var primaryColour = themeCallouts['primary-colour'] || fallback.colour;
var bgColour = themeCallouts['background-colour'] || fallback.colour;
if (!iconName) iconName = themeCallouts.icon || fallback.icon;
// Build element
container.className = 'mdcms-callout mdcms-callout-' + calloutType;
container.style.setProperty('--callout-primary', primaryColour);
container.style.setProperty('--callout-bg', hexToRgba(bgColour, 0.08));
if (title) {
var titleRow = document.createElement('div');
titleRow.className = 'mdcms-callout-title';
titleRow.appendChild(iconEl(iconName));
var titleText = document.createElement('span');
titleText.textContent = title;
titleRow.appendChild(titleText);
container.appendChild(titleRow);
}
if (bodyMd) {
var bodyEl = document.createElement('div');
bodyEl.className = 'mdcms-callout-body';
bodyEl.innerHTML = marked.parse(bodyMd);
container.appendChild(bodyEl);
}
}
function hexToRgba(hex, alpha) {
var h = hex.replace('#', '');
if (h.length === 3) h = h[0]+h[0]+h[1]+h[1]+h[2]+h[2];
var r = parseInt(h.substring(0,2), 16);
var g = parseInt(h.substring(2,4), 16);
var b = parseInt(h.substring(4,6), 16);
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
}
function hydrateMdcmsTags() {
document.querySelectorAll('.mdcms-tag').forEach(function(tagEl) {
try {
var cfg = JSON.parse(tagEl.getAttribute('data-config'));
renderPostTag(tagEl, cfg);
if (/^callout-(info|warning|success|error)$/.test(cfg.tagName)) {
renderCalloutTag(tagEl, cfg);
} else {
renderPostTag(tagEl, cfg);
}
} catch (e) {
tagEl.textContent = 'Error rendering tag.';
}
@ -2508,7 +2630,6 @@ function fmtDatetime(dtStr) {
if (link) link.href = `assets/images/${config.logo}`;
}
let themeConfig = {};
if (config.theme) {
try {
const themeResp = await fetch(config.theme);

View file

@ -3,65 +3,75 @@ title: Home
sort: 100
---
# MD-CMS
# Phase 4 — Callout Tags
This is the default startpage for MD-CMS.
Check each callout below. Each should show a coloured left border, an icon, a bold title in the accent colour, and a rendered body.
## Testing MD-CMS
---
If you want to test `MD-CMS` you can grab `samplesite` from the repo and place the content in your website root. This page (`pages/home.md`) won't be replaced.
## Basic types
**Post listing tests** below contains various custom tags to display posts. There are no posts now, but if you download the `samplesite` it will fetch the posts in
```mdcms callout-info
title: Information
This is an **info** callout. Supports *italic*, `code`, and lists:
## Post listing tests
## Reverse chronological (newest first)
```mdcms
posts-created-reversechronological
limit: 3
paginate: no
- Item one
- Item two
```
## Chronological (oldest first)
```mdcms
posts-created-chronological
limit: all
paginate: none
```mdcms callout-warning
title: Warning
Something needs your attention. This is a **warning** callout.
```
## By year (reverse chrono)
```mdcms
posts-created-reversechronological-byyear
limit: all
defaultyear: current
selectyear: yes
paginate: none
```mdcms callout-success
title: Success
The operation completed successfully. This is a **success** callout.
```
## By year+month (chrono)
```mdcms
posts-created-chronological-byyearmonth
limit: all
defaultyear: 2024
selectyear: yes
```mdcms callout-error
title: Error
Something went wrong. This is an **error** callout.
```
## Last 30 days
---
```mdcms
posts-created-reversechronological-lastmonth
limit: all
paginate: none
## No title
```mdcms callout-info
No title key here. The title row should not appear at all — just the body.
```
## Paginated (2 per page)
---
```mdcms
posts-created-reversechronological
limit: 2
paginate: yes
## Markdown body
```mdcms callout-warning
title: Rich body
- List item one
- List item two
A paragraph with `inline code` and a [link](https://example.com).
```
---
## Custom icon override
```mdcms callout-info
title: Info with warning icon
icon: warning
This info callout uses the warning icon instead of the default info icon.
```
---
## Config-defined message (message: key)
```mdcms callout-warning
message: aitranslation
```
---
Toggle dark mode and check all four callout types still look correct.