mirror of
https://github.com/kbenestad/mdcms.git
synced 2026-06-18 15:24:32 +00:00
v0.3.6 — Phase 4 callout tags (+ phases 1–3)
v0.3.6 — Phase 4 callout tags (+ phases 1–3)
This commit is contained in:
commit
ca8deba23f
6 changed files with 448 additions and 56 deletions
|
|
@ -43,3 +43,13 @@ theme: theme.yml # presentational config — edit theme.yml to custo
|
||||||
# ──────────────────────────────────
|
# ──────────────────────────────────
|
||||||
# search: true
|
# search: true
|
||||||
# default-theme: system # light | dark | system
|
# 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.
|
||||||
|
|
|
||||||
139
app/index.html
139
app/index.html
|
|
@ -862,6 +862,32 @@ body {
|
||||||
}
|
}
|
||||||
.post-load-more:hover { background: var(--nav-hover-bg); }
|
.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 {
|
@media print {
|
||||||
.sidebar, .topbar, .scroll-top, .hamburger,
|
.sidebar, .topbar, .scroll-top, .hamburger,
|
||||||
.mobile-header, .theme-toggle, .search-container { display: none !important; }
|
.mobile-header, .theme-toggle, .search-container { display: none !important; }
|
||||||
|
|
@ -893,6 +919,7 @@ body {
|
||||||
let searchIndex = [];
|
let searchIndex = [];
|
||||||
let fuseInstance = null;
|
let fuseInstance = null;
|
||||||
let currentPage = null;
|
let currentPage = null;
|
||||||
|
let themeConfig = {};
|
||||||
|
|
||||||
// Category state (phase 3)
|
// Category state (phase 3)
|
||||||
let categoriesUse = false;
|
let categoriesUse = false;
|
||||||
|
|
@ -929,7 +956,8 @@ body {
|
||||||
const svg = getIcon(name);
|
const svg = getIcon(name);
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.className = 'mdcms-icon' + (className ? ' ' + className : '');
|
span.className = 'mdcms-icon' + (className ? ' ' + className : '');
|
||||||
span.innerHTML = svg || '<img src="" alt="" style="width:1em;height:1em;">';
|
const filename = normaliseIconName(name);
|
||||||
|
span.innerHTML = svg || '<img src="assets/icons/' + filename + '" alt="[missing: ' + filename + ']" style="width:1em;height:1em;display:inline-block;">';
|
||||||
return span;
|
return span;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1367,8 +1395,11 @@ body {
|
||||||
} else {
|
} else {
|
||||||
codeText = code; codeLang = lang;
|
codeText = code; codeLang = lang;
|
||||||
}
|
}
|
||||||
if (codeLang === 'mdcms') {
|
// Match both ```mdcms (type in content) and ```mdcms callout-info (type in fence)
|
||||||
const tag = parseMdcmsTag(codeText);
|
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, '&').replace(/"/g, '"');
|
const encoded = JSON.stringify(tag).replace(/&/g, '&').replace(/"/g, '"');
|
||||||
return '<div class="mdcms-tag" data-config="' + encoded + '"></div>';
|
return '<div class="mdcms-tag" data-config="' + encoded + '"></div>';
|
||||||
}
|
}
|
||||||
|
|
@ -1487,11 +1518,18 @@ function fmtDatetime(dtStr) {
|
||||||
var lines = text.trim().split('\n');
|
var lines = text.trim().split('\n');
|
||||||
var tagName = lines[0].trim();
|
var tagName = lines[0].trim();
|
||||||
var options = {};
|
var options = {};
|
||||||
|
var bodyStart = lines.length;
|
||||||
for (var i = 1; i < lines.length; i++) {
|
for (var i = 1; i < lines.length; i++) {
|
||||||
var m = lines[i].match(/^\s*([a-z\-]+)\s*:\s*(.+)$/i);
|
var m = lines[i].match(/^\s*([a-z\-]+)\s*:\s*(.*)$/i);
|
||||||
if (m) options[m[1].toLowerCase()] = m[2].trim();
|
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) {
|
function parsePostTagName(name) {
|
||||||
|
|
@ -1769,11 +1807,97 @@ function fmtDatetime(dtStr) {
|
||||||
renderYear();
|
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.style.color = primaryColour;
|
||||||
|
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() {
|
function hydrateMdcmsTags() {
|
||||||
document.querySelectorAll('.mdcms-tag').forEach(function(tagEl) {
|
document.querySelectorAll('.mdcms-tag').forEach(function(tagEl) {
|
||||||
try {
|
try {
|
||||||
var cfg = JSON.parse(tagEl.getAttribute('data-config'));
|
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) {
|
} catch (e) {
|
||||||
tagEl.textContent = 'Error rendering tag.';
|
tagEl.textContent = 'Error rendering tag.';
|
||||||
}
|
}
|
||||||
|
|
@ -2508,7 +2632,6 @@ function fmtDatetime(dtStr) {
|
||||||
if (link) link.href = `assets/images/${config.logo}`;
|
if (link) link.href = `assets/images/${config.logo}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let themeConfig = {};
|
|
||||||
if (config.theme) {
|
if (config.theme) {
|
||||||
try {
|
try {
|
||||||
const themeResp = await fetch(config.theme);
|
const themeResp = await fetch(config.theme);
|
||||||
|
|
|
||||||
|
|
@ -3,65 +3,101 @@ title: Home
|
||||||
sort: 100
|
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
|
- Item one
|
||||||
|
- Item two
|
||||||
## Reverse chronological (newest first)
|
|
||||||
|
|
||||||
```mdcms
|
|
||||||
posts-created-reversechronological
|
|
||||||
limit: 3
|
|
||||||
paginate: no
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Chronological (oldest first)
|
```mdcms callout-warning
|
||||||
|
title: Warning
|
||||||
```mdcms
|
Something needs your attention. This is a **warning** callout.
|
||||||
posts-created-chronological
|
|
||||||
limit: all
|
|
||||||
paginate: none
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## By year (reverse chrono)
|
```mdcms callout-success
|
||||||
|
title: Success
|
||||||
```mdcms
|
The operation completed successfully. This is a **success** callout.
|
||||||
posts-created-reversechronological-byyear
|
|
||||||
limit: all
|
|
||||||
defaultyear: current
|
|
||||||
selectyear: yes
|
|
||||||
paginate: none
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## By year+month (chrono)
|
```mdcms callout-error
|
||||||
|
title: Error
|
||||||
```mdcms
|
Something went wrong. This is an **error** callout.
|
||||||
posts-created-chronological-byyearmonth
|
|
||||||
limit: all
|
|
||||||
defaultyear: 2024
|
|
||||||
selectyear: yes
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Last 30 days
|
---
|
||||||
|
|
||||||
```mdcms
|
## No title
|
||||||
posts-created-reversechronological-lastmonth
|
|
||||||
limit: all
|
```mdcms callout-info
|
||||||
paginate: none
|
No title key here. The title row should not appear at all — just the body.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Paginated (2 per page)
|
---
|
||||||
|
|
||||||
```mdcms
|
## Markdown body
|
||||||
posts-created-reversechronological
|
|
||||||
limit: 2
|
```mdcms callout-warning
|
||||||
paginate: yes
|
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)
|
||||||
|
|
||||||
|
The callout below uses `message: aitranslation` to pull its title and body from the `callouts:` block in `config.yml`. The type (`warning`) also comes from the config entry, not the tag name.
|
||||||
|
|
||||||
|
```mdcms callout-info
|
||||||
|
message: aitranslation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## message: overrides inline content
|
||||||
|
|
||||||
|
When `message:` is present, any inline `title:` or body text is ignored. A warning should appear in the browser console.
|
||||||
|
|
||||||
|
```mdcms callout-info
|
||||||
|
message: aitranslation
|
||||||
|
title: This title should be ignored
|
||||||
|
This body text should also be ignored. Check the console for a warning.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Missing icon
|
||||||
|
|
||||||
|
This callout uses a non-existent icon name. A broken image should appear where the icon would be.
|
||||||
|
|
||||||
|
```mdcms callout-info
|
||||||
|
title: Custom icon that does not exist
|
||||||
|
icon: nonexistent_icon
|
||||||
|
The icon to the left of this title should show as a broken image.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Toggle dark mode and check all four callout types still look correct.
|
||||||
|
|
|
||||||
6
mdcms.py
6
mdcms.py
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
#
|
#
|
||||||
# mdcms v0.3.5 — CLI companion
|
# mdcms v0.3.6 — CLI companion
|
||||||
#
|
#
|
||||||
# Copyright 2026 Kristian Benestad
|
# Copyright 2026 Kristian Benestad
|
||||||
# Apache License, Version 2.0 — https://www.apache.org/licenses/LICENSE-2.0
|
# Apache License, Version 2.0 — https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
@ -21,8 +21,8 @@ import certifi
|
||||||
import click
|
import click
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
CLI_VERSION = "0.3.5"
|
CLI_VERSION = "0.3.6"
|
||||||
CLI_RELEASE_DATE = "16 May 2026"
|
CLI_RELEASE_DATE = "17 May 2026"
|
||||||
MIN_SUPPORTED_VERSION = "0.3"
|
MIN_SUPPORTED_VERSION = "0.3"
|
||||||
|
|
||||||
MARKER_RE = re.compile(r"mdcms v(\d+\.\d+)", re.IGNORECASE)
|
MARKER_RE = re.compile(r"mdcms v(\d+\.\d+)", re.IGNORECASE)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mdcms"
|
name = "mdcms"
|
||||||
version = "0.3.5"
|
version = "0.3.6"
|
||||||
description = "MD-CMS — Markdown-based CMS companion CLI"
|
description = "MD-CMS — Markdown-based CMS companion CLI"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "Apache-2.0" }
|
license = { text = "Apache-2.0" }
|
||||||
|
|
|
||||||
223
test_phase.py
Normal file
223
test_phase.py
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MD-CMS v0.4 phase test runner.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 test_phase.py [phase]
|
||||||
|
|
||||||
|
phase: 1-7 (default: run all phases sequentially)
|
||||||
|
|
||||||
|
Each phase fetches the corresponding branch, checks out the renderer and
|
||||||
|
content files for that phase, starts a local HTTP server, and opens the
|
||||||
|
browser. Press Enter when done to continue to the next phase.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import http.server
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import webbrowser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PORT = 8800
|
||||||
|
|
||||||
|
PHASES = {
|
||||||
|
1: ("main", "theme.yml and colour system"),
|
||||||
|
2: ("v0.4_phase2", "Icon system — local SVGs, no Google Fonts"),
|
||||||
|
3: ("v0.4_phase3", "Asset validation in mdcms build"),
|
||||||
|
4: ("claude/debug-api-errors-gd730", "Callout tags"),
|
||||||
|
5: ("v0.4_phase5", "Table of contents tag"),
|
||||||
|
6: ("v0.4_phase6", "Offline / fetch-deps"),
|
||||||
|
7: ("v0.4_phase7", "PWA — service worker and manifest"),
|
||||||
|
}
|
||||||
|
|
||||||
|
VERIFY = {
|
||||||
|
1: [
|
||||||
|
"Existing site renders correctly with theme.yml present",
|
||||||
|
"Missing theme: key falls back gracefully to hardcoded defaults",
|
||||||
|
"Accent colour and dark/light mode colours apply from theme.yml",
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
"All UI icons render correctly from local SVG files (no Google Icons font)",
|
||||||
|
"Theme toggle, search, hamburger, nav arrows all show icons",
|
||||||
|
"Broken image displays for a missing icon (test by renaming one SVG)",
|
||||||
|
"Icon name normalisation: 'arrow-right' and 'Arrow Right' both resolve",
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
"Run: python3 mdcms.py build --path app/ (NOT the installed mdcms command)",
|
||||||
|
"Warning printed for assets/images/missing-photo.png (referenced in pages/about.md)",
|
||||||
|
"No warning for assets/images/logo.png (file exists)",
|
||||||
|
"Build continues and completes after warnings",
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
"── Basic types ─────────────────────────────────────────────────",
|
||||||
|
"callout-info, callout-warning, callout-success, callout-error all render",
|
||||||
|
"Each has: coloured left border + low-opacity background in the right colour",
|
||||||
|
"── Title row ───────────────────────────────────────────────────",
|
||||||
|
"Title row shows: icon (inlined SVG) + bold title text in the accent colour",
|
||||||
|
"Title text is correct for each type: Information / Warning / Success / Error",
|
||||||
|
"── No-title callout ────────────────────────────────────────────",
|
||||||
|
"Callout with no title key: no title row rendered, just the body",
|
||||||
|
"── Markdown body ───────────────────────────────────────────────",
|
||||||
|
"Body renders full markdown: bold, italic, lists, inline code, links",
|
||||||
|
"── Custom icon override ────────────────────────────────────────",
|
||||||
|
"icon: warning on a callout-info renders the warning SVG, not the info SVG",
|
||||||
|
"── Config-defined message: key ─────────────────────────────────",
|
||||||
|
"message: aitranslation resolves title 'PLEASE NOTE:' and body from config.yml",
|
||||||
|
"── Dark mode ───────────────────────────────────────────────────",
|
||||||
|
"Toggle dark mode: all four callout types still look correct",
|
||||||
|
"Colours adapt (border and background tint remain visible on dark background)",
|
||||||
|
],
|
||||||
|
5: [
|
||||||
|
"TOC tag renders a section-grouped page list",
|
||||||
|
"Only pages visible for active category are listed",
|
||||||
|
"Draft pages are excluded",
|
||||||
|
"TOC page itself is excluded from the listing",
|
||||||
|
"Section headings and sort order are correct",
|
||||||
|
],
|
||||||
|
6: [
|
||||||
|
"mdcms fetch-deps downloads JS/CSS to assets/required/vendors/",
|
||||||
|
"Patched index.html makes no external network requests",
|
||||||
|
"Fonts load correctly from local paths",
|
||||||
|
"Site loads fully offline after fetch-deps",
|
||||||
|
],
|
||||||
|
7: [
|
||||||
|
"service-worker.js and manifest.json generated when pwa: yes",
|
||||||
|
"Full site accessible offline after one online visit",
|
||||||
|
"Cache updates correctly on new build deployment",
|
||||||
|
"Offline message displays when cache evicted",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Files checked out from the phase branch on every phase switch.
|
||||||
|
# Renderer + presentational config. Content files (home.md, config.yml) are
|
||||||
|
# added per phase in EXTRA_FILES so the right test content is loaded.
|
||||||
|
RENDERER_FILES = [
|
||||||
|
"app/index.html",
|
||||||
|
"app/theme.yml",
|
||||||
|
"app/assets/icons",
|
||||||
|
"mdcms.py",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Per-phase extra files to check out from the phase branch.
|
||||||
|
# Use this for content or config files that differ between phases.
|
||||||
|
EXTRA_FILES = {
|
||||||
|
4: [
|
||||||
|
"app/config.yml", # has callouts: block for message: key test
|
||||||
|
"app/pages/home.md", # has Phase 4 callout test cases
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def checkout_phase(phase: int) -> bool:
|
||||||
|
branch, _ = PHASES[phase]
|
||||||
|
repo_root = Path(__file__).parent
|
||||||
|
|
||||||
|
print(f"\n Fetching branch {branch} from origin...")
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "fetch", "origin", branch],
|
||||||
|
cwd=repo_root,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f" ERROR fetching: {result.stderr.strip()}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
files = RENDERER_FILES + EXTRA_FILES.get(phase, [])
|
||||||
|
print(f" Checking out {len(files)} file groups from origin/{branch}...")
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "checkout", f"origin/{branch}", "--"] + files,
|
||||||
|
cwd=repo_root,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f" ERROR checking out files: {result.stderr.strip()}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(" Rebuilding nav.yml...")
|
||||||
|
result = subprocess.run(
|
||||||
|
["python3", "mdcms.py", "build", "--path", "app/"],
|
||||||
|
cwd=repo_root,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f" WARNING: mdcms build failed: {result.stderr.strip()}")
|
||||||
|
else:
|
||||||
|
out = result.stdout.strip()
|
||||||
|
if out:
|
||||||
|
print(f" {out}")
|
||||||
|
|
||||||
|
print(" Ready.\n")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def serve(app_dir: Path):
|
||||||
|
handler = functools.partial(
|
||||||
|
http.server.SimpleHTTPRequestHandler,
|
||||||
|
directory=str(app_dir),
|
||||||
|
)
|
||||||
|
handler.log_message = lambda *a: None
|
||||||
|
with http.server.HTTPServer(("", PORT), handler) as httpd:
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
def run_phase(phase: int):
|
||||||
|
branch, description = PHASES[phase]
|
||||||
|
|
||||||
|
print("\n" + "=" * 62)
|
||||||
|
print(f" Phase {phase}: {description}")
|
||||||
|
print(f" Branch: {branch}")
|
||||||
|
print("=" * 62)
|
||||||
|
|
||||||
|
repo_root = Path(__file__).parent
|
||||||
|
app_dir = repo_root / "app"
|
||||||
|
|
||||||
|
if phase > 1:
|
||||||
|
if not checkout_phase(phase):
|
||||||
|
print(" Checkout failed — skipping this phase.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f" Serving app/ at http://localhost:{PORT}\n")
|
||||||
|
print(" Checklist:")
|
||||||
|
for item in VERIFY[phase]:
|
||||||
|
if item.startswith("──"):
|
||||||
|
print(f"\n {item}")
|
||||||
|
else:
|
||||||
|
print(f" [ ] {item}")
|
||||||
|
|
||||||
|
t = threading.Thread(target=serve, args=(app_dir,), daemon=True)
|
||||||
|
t.start()
|
||||||
|
time.sleep(0.3)
|
||||||
|
webbrowser.open(f"http://localhost:{PORT}")
|
||||||
|
|
||||||
|
print("\n Press Enter when done (Ctrl+C to abort)...")
|
||||||
|
try:
|
||||||
|
input()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n Aborted.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) == 2:
|
||||||
|
try:
|
||||||
|
phase = int(sys.argv[1])
|
||||||
|
except ValueError:
|
||||||
|
print("Usage: python3 test_phase.py [1-7]")
|
||||||
|
sys.exit(1)
|
||||||
|
if phase not in PHASES:
|
||||||
|
print(f"Phase must be 1-7, got {phase}")
|
||||||
|
sys.exit(1)
|
||||||
|
run_phase(phase)
|
||||||
|
else:
|
||||||
|
for phase in sorted(PHASES):
|
||||||
|
run_phase(phase)
|
||||||
|
print(f"\n Phase {phase} done.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in a new issue