Add support for wysiwyg markdown preview

This commit is contained in:
gsb
2026-04-29 05:01:51 +00:00
parent 86d59877f1
commit 4237a3f6a2
10 changed files with 430 additions and 72 deletions
+104 -1
View File
@@ -64,7 +64,7 @@ function not(name, actual, sub) {
}
}
function section(n) { /* silent */ }
function section(n) { console.log(' ' + n); }
// ── 1. Inline formatting ────────────────────────────────
section('1. Inline Formatting → HTML');
@@ -497,6 +497,109 @@ has('macro: unknown block renders error', MH('@bogus(args\ncontent\n)'), 'ribbit
eq('macro: npc round-trip', mrt('@npc(Goblin King)'), '@npc(Goblin King)');
eq('macro: user round-trip', mrt('hello @user world'), 'hello @user world');
// ── 25. Preview CSS (via .ribbit-editing pseudo-elements) ───
// Preview styling is handled by CSS ::before/::after on .ribbit-editing,
// not by JS. We verify the converter output is clean HTML without syntax spans.
not('preview: no syntax spans in toHTML',
H('**bold**'), 'ribbit-syntax');
not('preview: no syntax spans in heading',
H('## Title'), 'ribbit-syntax');
// ── 26. openPattern — unclosed delimiter detection ──────
var inlineTags = hopdown.getInlineTags();
function findTag(name) {
return inlineTags.find(function(t) { return t.name === name; });
}
var boldTag = findTag('bold');
var italicTag = findTag('italic');
var codeTag = findTag('code');
var boldItalicTag = findTag('boldItalic');
eq('openPattern: bold has pattern', String(!!boldTag.openPattern), 'true');
eq('openPattern: italic has pattern', String(!!italicTag.openPattern), 'true');
eq('openPattern: code has pattern', String(!!codeTag.openPattern), 'true');
// Unclosed bold matches
eq('openPattern: unclosed ** odd count',
String((('hello **world').match(/\*\*/g) || []).length % 2 === 1), 'true');
// Closed bold — even count
eq('openPattern: closed ** even count',
String((('hello **world**').match(/\*\*/g) || []).length % 2 === 1), 'false');
// Unclosed italic
eq('openPattern: unclosed * odd count',
String((('hello *world').match(/\*/g) || []).length % 2 === 1), 'true');
// Unclosed code
eq('openPattern: unclosed ` odd count',
String((('hello `world').match(/`/g) || []).length % 2 === 1), 'true');
// ── 27. Speculative patching ────────────────────────────
function specPatch(md, cursorLine, cursorOffset) {
var lines = md.split('\n');
var sorted = inlineTags.slice().sort(function(a, b) {
return ((a).precedence || 50) - ((b).precedence || 50);
});
for (var i = 0; i < sorted.length; i++) {
var tag = sorted[i];
if (tag.openPattern && tag.delimiter) {
var before = lines[cursorLine].slice(0, cursorOffset);
var escaped = tag.delimiter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
var re = new RegExp(escaped, 'g');
var count = (before.match(re) || []).length;
if (count % 2 === 1) {
lines[cursorLine] = lines[cursorLine] + tag.delimiter;
break;
}
}
}
return hopdown.toHTML(lines.join('\n'));
}
has('speculate: unclosed bold',
specPatch('hello **world', 0, 13), '<strong>world</strong>');
has('speculate: unclosed italic',
specPatch('hello *world', 0, 12), '<em>world</em>');
has('speculate: unclosed code',
specPatch('hello `world', 0, 12), '<code>world</code>');
has('speculate: unclosed bold+italic',
specPatch('hello ***world', 0, 14), '<em><strong>world</strong></em>');
// Already closed — no double closing
eq('speculate: closed bold unchanged',
specPatch('hello **world**', 0, 15), '<p>hello <strong>world</strong></p>');
eq('speculate: closed italic unchanged',
specPatch('hello *world*', 0, 13), '<p>hello <em>world</em></p>');
// Only cursor line patched
has('speculate: multiline patches cursor only',
specPatch('normal\nhello **world', 1, 13), '<strong>world</strong>');
not('speculate: other line untouched',
specPatch('normal\nhello **world', 1, 13), '<strong>normal</strong>');
// No unclosed delimiter — no change
eq('speculate: no delimiter no-op',
specPatch('hello world', 0, 11), '<p>hello world</p>');
// ** wins over * (precedence)
has('speculate: ** wins over *',
specPatch('hello **world', 0, 13), '<strong>');
not('speculate: ** not italic',
specPatch('hello **world', 0, 13), '<em>world</em>');
// Delimiter with no content — speculation appends but nothing to format
eq('speculate: bare delimiter no content',
specPatch('hello **', 0, 8), '<p>hello <em>*</em>*</p>');
// Even count — all closed
eq('speculate: even count no-op',
specPatch('**a** **b**', 0, 11), '<p><strong>a</strong> <strong>b</strong></p>');
// Block tags need no speculation
eq('speculate: list works as-is',
H('- '), '<ul><li></li></ul>');
has('speculate: blockquote works as-is',
H('> '), '<blockquote>');
// ── Results ─────────────────────────────────────────────
const total = passed + failed;
console.log(`\n${passed}/${total} passed (${Math.round(100 * passed / total)}%) — ${failed} failed`);