canvas feature

This commit is contained in:
th committed 2026-04-10 06:35:30 +02:00
1 parent 0d494bf507
commit dd2a995f6c
5 files changed
+603

No files matched your search

+11
View File
@@ -499,6 +499,16 @@ body.detail-image-fs-open { overflow: hidden; }
#editor-gutter > div { padding-right: 2px; box-sizing: border-box; }
.gutter-active { color: var(--t-accent) !important; font-weight: 700; background: var(--t-raised); }
/* ASCII Canvas */
#ascii-canvas-overlay { background: rgba(0,0,0,0.5); backdrop-filter: blur(2px); }
.ascii-grid { white-space: pre; user-select: none; -webkit-user-select: none; display: inline-block; width: max-content; }
.ascii-cursor { background: var(--t-accent); color: var(--t-bg); }
.ascii-char-btn { font-family: var(--t-font-mono, monospace); font-size: 13px; width: 22px; height: 22px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--t-border); border-radius: 3px; background: var(--t-bg); color: var(--t-text); cursor: pointer; transition: all 0.1s; padding: 0; line-height: 1; }
.ascii-char-btn:hover { background: var(--t-raised); color: var(--t-bright); }
.ascii-char-btn.active { background: var(--t-accent); color: var(--t-bg); border-color: var(--t-accent); }
.ascii-char-group { display: inline-flex; gap: 2px; margin-right: 6px; }
#ascii-canvas-toolbar .toolbar-btn.active { background: var(--t-accent); color: var(--t-bg); }
/* Search */
#search { backdrop-filter: blur(4px); }
#search-input { caret-color: var(--t-accent); }
@@ -553,6 +563,7 @@ body.detail-image-fs-open { overflow: hidden; }
.prose code { background: var(--t-bg); border: 1px solid var(--t-border); padding: 0.15em 0.4em; border-radius: 3px; font-size: 0.88em; }
.prose pre { background: var(--t-bg); border: 1px solid var(--t-border); padding: 0.8em 1em; overflow-x: auto; margin: 0.8em 0; border-radius: 3px; position: relative; }
.prose pre code { background: none; border: none; padding: 0; font-size: 0.85em; }
.prose pre.ascii-art { line-height: 1.35; letter-spacing: 0; white-space: pre; font-size: 0.85em; }
.prose pre .code-copy {
position: absolute; top: 4px; right: 4px;
background: var(--t-surface); border: 1px solid var(--t-border);
+47
View File
@@ -1826,6 +1826,7 @@ if (str_starts_with($route, '/api/')) {
<button data-md="hr" title="Divider" class="toolbar-btn"><i class="ph ph-minus text-sm"></i></button>
<div class="w-px h-4 bg-t-border mx-1"></div>
<button data-md="extract" title="Extract selection to new card" class="toolbar-btn"><i class="ph ph-scissors text-sm"></i></button>
<button data-md="asciiart" title="ASCII Canvas" class="toolbar-btn"><i class="ph ph-grid-four text-sm"></i></button>
<div class="flex-1"></div>
<button id="toggle-lines" class="toolbar-btn flex items-center gap-1.5 px-2" title="line numbers"><i class="ph ph-hash text-sm"></i></button>
<button id="toggle-preview" class="toolbar-btn flex items-center gap-1.5 px-2"><i class="ph ph-eye text-sm"></i><span class="font-pixel text-[9px]">preview</span></button>
@@ -1842,6 +1843,52 @@ if (str_starts_with($route, '/api/')) {
</div>
</div>
<!-- ASCII Canvas Overlay -->
<div class="overlay fixed inset-0" id="ascii-canvas-overlay" style="z-index:80;">
<div class="ascii-canvas-modal bg-t-surface border border-t-border rounded-lg shadow-xl flex flex-col" style="width:90vw;max-width:960px;height:85vh;max-height:700px;margin:auto;position:absolute;inset:0;">
<!-- Toolbar -->
<div class="flex items-center gap-1 px-3 py-2 border-b border-t-border shrink-0 flex-wrap" id="ascii-canvas-toolbar">
<button data-tool="brush" class="toolbar-btn active" title="Brush (B)"><i class="ph ph-pencil-simple text-sm"></i></button>
<button data-tool="eraser" class="toolbar-btn" title="Eraser (E)"><i class="ph ph-eraser text-sm"></i></button>
<button data-tool="line" class="toolbar-btn" title="Line (L)"><i class="ph ph-line-segment text-sm"></i></button>
<button data-tool="rect" class="toolbar-btn" title="Rectangle (R)"><i class="ph ph-rectangle text-sm"></i></button>
<button data-tool="circle" class="toolbar-btn" title="Ellipse (O)"><i class="ph ph-circle text-sm"></i></button>
<button data-tool="fill" class="toolbar-btn" title="Fill (F)"><i class="ph ph-paint-bucket text-sm"></i></button>
<button data-tool="text" class="toolbar-btn" title="Text (T)"><i class="ph ph-text-t text-sm"></i></button>
<div class="w-px h-4 bg-t-border mx-1"></div>
<label class="flex items-center gap-1 text-t-muted text-xs">
<span>Char:</span>
<input id="ascii-char-input" type="text" value="#" maxlength="1" class="w-6 text-center bg-t-bg border border-t-border rounded text-t-text text-sm font-mono px-0 py-0">
</label>
<div class="w-px h-4 bg-t-border mx-1"></div>
<label class="flex items-center gap-1 text-t-muted text-xs">
<span>W:</span>
<input id="ascii-canvas-width" type="number" value="40" min="5" max="200" class="w-12 bg-t-bg border border-t-border rounded text-t-text text-xs text-center px-1 py-0">
</label>
<label class="flex items-center gap-1 text-t-muted text-xs">
<span>H:</span>
<input id="ascii-canvas-height" type="number" value="20" min="3" max="100" class="w-12 bg-t-bg border border-t-border rounded text-t-text text-xs text-center px-1 py-0">
</label>
<button id="ascii-canvas-resize" class="toolbar-btn text-xs" title="Resize"><i class="ph ph-arrows-out text-sm"></i></button>
<div class="w-px h-4 bg-t-border mx-1"></div>
<button id="ascii-canvas-clear" class="toolbar-btn text-xs" title="Clear"><i class="ph ph-trash text-sm"></i></button>
<div class="flex-1"></div>
<span id="ascii-canvas-coords" class="text-t-dim text-xs font-mono">1:1</span>
</div>
<!-- Char palette -->
<div id="ascii-char-palette" class="flex items-center gap-0.5 px-3 py-1.5 border-b border-t-border shrink-0 flex-wrap overflow-x-auto"></div>
<!-- Grid -->
<div class="flex-1 min-h-0 overflow-auto bg-t-bg p-2">
<pre id="ascii-canvas-grid" class="ascii-grid font-mono text-sm text-t-text select-none cursor-crosshair" style="line-height:1.35;letter-spacing:0;margin:0;"></pre>
</div>
<!-- Footer -->
<div class="flex items-center justify-end gap-2 px-3 py-2 border-t border-t-border shrink-0">
<button id="ascii-canvas-cancel" class="toolbar-btn text-xs px-3 py-1">Cancel</button>
<button id="ascii-canvas-ok" class="bg-t-accent text-t-bg text-xs font-bold px-4 py-1 rounded hover:opacity-90 cursor-pointer">Insert</button>
</div>
</div>
</div>
<script type="module" src="js/app.js?v=<?= time() ?>"></script>
</body>
</html>
+509
View File
@@ -0,0 +1,509 @@
// ── ASCII Art Canvas ──
import { $ } from './util.js';
const overlay = $('ascii-canvas-overlay');
const grid = $('ascii-canvas-grid');
const toolbar = $('ascii-canvas-toolbar');
const charInput = $('ascii-char-input');
const widthInput = $('ascii-canvas-width');
const heightInput = $('ascii-canvas-height');
const btnOk = $('ascii-canvas-ok');
const btnCancel = $('ascii-canvas-cancel');
const btnClear = $('ascii-canvas-clear');
const btnResize = $('ascii-canvas-resize');
const coordsDisplay = $('ascii-canvas-coords');
let data = []; // 2D array [row][col]
let cols = 40;
let rows = 20;
let tool = 'brush'; // brush | eraser | line | rect | circle | fill | text
let char = '#';
let drawing = false;
let startCell = null; // {r, c} for shape tools
let previewCells = []; // cells to highlight during drag
let onInsert = null; // callback(text)
let cellW = 0, cellH = 0;
let gridRect = null;
// ── Grid init ──
function initGrid(w, h) {
cols = w; rows = h;
data = [];
for (let r = 0; r < rows; r++) {
data.push(new Array(cols).fill(' '));
}
render();
}
function render() {
const lines = data.map(row => row.join(''));
grid.textContent = lines.join('\n');
measureCells();
}
function measureCells() {
if (!grid.firstChild) return;
// Use getBoundingClientRect on the grid (inline-block = content-sized)
gridRect = grid.getBoundingClientRect();
cellW = gridRect.width / cols;
cellH = gridRect.height / rows;
}
function cellFromEvent(e) {
// Always re-measure to account for scroll/layout changes
gridRect = grid.getBoundingClientRect();
if (!gridRect || !cellW || !cellH) return null;
const x = e.clientX - gridRect.left;
const y = e.clientY - gridRect.top;
const c = Math.floor(x / cellW);
const r = Math.floor(y / cellH);
if (r < 0 || r >= rows || c < 0 || c >= cols) return null;
return { r, c };
}
// ── Drawing algorithms ──
function setCell(r, c, ch) {
if (r >= 0 && r < rows && c >= 0 && c < cols) {
data[r][c] = ch;
}
}
function bresenhamLine(r0, c0, r1, c1) {
const cells = [];
let dr = Math.abs(r1 - r0), dc = Math.abs(c1 - c0);
let sr = r0 < r1 ? 1 : -1, sc = c0 < c1 ? 1 : -1;
let err = dc - dr;
let r = r0, c = c0;
while (true) {
cells.push({ r, c });
if (r === r1 && c === c1) break;
const e2 = 2 * err;
if (e2 > -dr) { err -= dr; c += sc; }
if (e2 < dc) { err += dc; r += sr; }
}
return cells;
}
function rectCells(r0, c0, r1, c1) {
const cells = [];
const rMin = Math.min(r0, r1), rMax = Math.max(r0, r1);
const cMin = Math.min(c0, c1), cMax = Math.max(c0, c1);
for (let c = cMin; c <= cMax; c++) { cells.push({ r: rMin, c }); cells.push({ r: rMax, c }); }
for (let r = rMin + 1; r < rMax; r++) { cells.push({ r, c: cMin }); cells.push({ r, c: cMax }); }
return cells;
}
function circleCells(r0, c0, r1, c1) {
const cells = [];
const cr = (r0 + r1) / 2, cc = (c0 + c1) / 2;
const rx = Math.abs(c1 - c0) / 2, ry = Math.abs(r1 - r0) / 2;
if (rx < 1 && ry < 1) { cells.push({ r: Math.round(cr), c: Math.round(cc) }); return cells; }
// Parametric ellipse
const steps = Math.max(60, Math.round(Math.max(rx, ry) * 4));
const seen = new Set();
for (let i = 0; i <= steps; i++) {
const a = (2 * Math.PI * i) / steps;
const r = Math.round(cr + ry * Math.sin(a));
const c = Math.round(cc + rx * Math.cos(a));
const key = r + ',' + c;
if (!seen.has(key)) { seen.add(key); cells.push({ r, c }); }
}
return cells;
}
function floodFill(r, c, ch) {
const target = data[r]?.[c];
if (target === undefined || target === ch) return;
const stack = [{ r, c }];
while (stack.length) {
const { r: cr, c: cc } = stack.pop();
if (cr < 0 || cr >= rows || cc < 0 || cc >= cols) continue;
if (data[cr][cc] !== target) continue;
data[cr][cc] = ch;
stack.push({ r: cr - 1, c: cc }, { r: cr + 1, c: cc }, { r: cr, c: cc - 1 }, { r: cr, c: cc + 1 });
}
}
// ── Rendering with preview overlay ──
function renderWithPreview(cells, ch) {
const preview = new Map();
for (const { r, c } of cells) {
if (r >= 0 && r < rows && c >= 0 && c < cols) {
preview.set(r + ',' + c, ch);
}
}
const lines = [];
for (let r = 0; r < rows; r++) {
let line = '';
for (let c = 0; c < cols; c++) {
const key = r + ',' + c;
line += preview.has(key) ? preview.get(key) : data[r][c];
}
lines.push(line);
}
grid.textContent = lines.join('\n');
}
// ── Mouse handlers ──
function onMouseDown(e) {
if (e.button !== 0) return;
e.preventDefault();
measureCells();
const cell = cellFromEvent(e);
if (!cell) return;
drawing = true;
startCell = cell;
if (tool === 'brush') {
setCell(cell.r, cell.c, char);
render();
} else if (tool === 'eraser') {
setCell(cell.r, cell.c, ' ');
render();
} else if (tool === 'fill') {
floodFill(cell.r, cell.c, char);
render();
drawing = false;
} else if (tool === 'text') {
// Start typing at this position
startTextMode(cell.r, cell.c);
drawing = false;
}
updateCoords(cell);
}
function onMouseMove(e) {
const cell = cellFromEvent(e);
if (cell) updateCoords(cell);
if (!drawing || !cell) return;
if (tool === 'brush') {
// Draw line from last position for smooth strokes
if (startCell) {
const cells = bresenhamLine(startCell.r, startCell.c, cell.r, cell.c);
for (const { r, c } of cells) setCell(r, c, char);
startCell = cell;
}
render();
} else if (tool === 'eraser') {
if (startCell) {
const cells = bresenhamLine(startCell.r, startCell.c, cell.r, cell.c);
for (const { r, c } of cells) setCell(r, c, ' ');
startCell = cell;
}
render();
} else if (tool === 'line') {
const cells = bresenhamLine(startCell.r, startCell.c, cell.r, cell.c);
renderWithPreview(cells, char);
} else if (tool === 'rect') {
const cells = rectCells(startCell.r, startCell.c, cell.r, cell.c);
renderWithPreview(cells, char);
} else if (tool === 'circle') {
const cells = circleCells(startCell.r, startCell.c, cell.r, cell.c);
renderWithPreview(cells, char);
}
}
function onMouseUp(e) {
if (!drawing) return;
const cell = cellFromEvent(e);
if (!cell) { drawing = false; return; }
if (tool === 'line') {
for (const p of bresenhamLine(startCell.r, startCell.c, cell.r, cell.c)) setCell(p.r, p.c, char);
} else if (tool === 'rect') {
for (const p of rectCells(startCell.r, startCell.c, cell.r, cell.c)) setCell(p.r, p.c, char);
} else if (tool === 'circle') {
for (const p of circleCells(startCell.r, startCell.c, cell.r, cell.c)) setCell(p.r, p.c, char);
}
drawing = false;
render();
}
// ── Text mode ──
let textCursor = null;
function startTextMode(r, c) {
textCursor = { r, c };
renderTextCursor();
}
function renderTextCursor() {
if (!textCursor) { render(); return; }
// Show cursor as inverted cell
const lines = [];
for (let r = 0; r < rows; r++) {
let line = '';
for (let c = 0; c < cols; c++) {
line += data[r][c];
}
lines.push(line);
}
// Build HTML with cursor highlight
let html = '';
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const ch = data[r][c] === ' ' ? '\u00a0' : data[r][c];
if (r === textCursor.r && c === textCursor.c) {
html += `<span class="ascii-cursor">${ch}</span>`;
} else {
html += escChar(data[r][c]);
}
}
if (r < rows - 1) html += '\n';
}
grid.innerHTML = html;
}
function escChar(ch) {
if (ch === '<') return '&lt;';
if (ch === '>') return '&gt;';
if (ch === '&') return '&amp;';
if (ch === ' ') return ' ';
return ch;
}
function handleTextKey(e) {
if (!textCursor) return false;
if (e.key === 'Escape') {
textCursor = null;
render();
return true;
}
if (e.key === 'ArrowLeft') { textCursor.c = Math.max(0, textCursor.c - 1); renderTextCursor(); return true; }
if (e.key === 'ArrowRight') { textCursor.c = Math.min(cols - 1, textCursor.c + 1); renderTextCursor(); return true; }
if (e.key === 'ArrowUp') { textCursor.r = Math.max(0, textCursor.r - 1); renderTextCursor(); return true; }
if (e.key === 'ArrowDown') { textCursor.r = Math.min(rows - 1, textCursor.r + 1); renderTextCursor(); return true; }
if (e.key === 'Backspace') {
textCursor.c = Math.max(0, textCursor.c - 1);
setCell(textCursor.r, textCursor.c, ' ');
renderTextCursor();
return true;
}
if (e.key === 'Enter') {
textCursor.r = Math.min(rows - 1, textCursor.r + 1);
textCursor.c = 0;
renderTextCursor();
return true;
}
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
setCell(textCursor.r, textCursor.c, e.key);
textCursor.c = Math.min(cols - 1, textCursor.c + 1);
renderTextCursor();
return true;
}
return false;
}
// ── Coords display ──
function updateCoords(cell) {
if (coordsDisplay) coordsDisplay.textContent = `${cell.c + 1}:${cell.r + 1}`;
}
// ── Tool selection ──
function selectTool(t) {
tool = t;
textCursor = null;
render();
toolbar.querySelectorAll('[data-tool]').forEach(b => {
b.classList.toggle('active', b.dataset.tool === t);
});
}
// ── Character palette ──
const CHAR_SETS = {
basic: ['#', '*', '.', '+', '-', '|', '/', '\\', '=', '_', '~', ':', '^', '@', 'o', 'x'],
box: ['─', '│', '┌', '┐', '└', '┘', '├', '┤', '┬', '┴', '┼'],
boxDouble: ['═', '║', '╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬'],
blocks: ['█', '▓', '▒', '░', '▄', '▀', '▌', '▐'],
arrows: ['←', '→', '↑', '↓', '↔', '↕', '►', '◄', '▲', '▼'],
};
function buildPalette() {
const palette = $('ascii-char-palette');
if (!palette) return;
palette.innerHTML = '';
for (const [group, chars] of Object.entries(CHAR_SETS)) {
const grp = document.createElement('div');
grp.className = 'ascii-char-group';
for (const ch of chars) {
const btn = document.createElement('button');
btn.className = 'ascii-char-btn';
btn.textContent = ch;
btn.title = ch;
btn.addEventListener('click', () => {
char = ch;
charInput.value = ch;
palette.querySelectorAll('.ascii-char-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
if (ch === char) btn.classList.add('active');
grp.appendChild(btn);
}
palette.appendChild(grp);
}
}
// ── Export ──
function exportText() {
// Trim trailing spaces per line, remove trailing empty lines
const lines = data.map(row => row.join('').replace(/\s+$/, ''));
while (lines.length > 1 && !lines[lines.length - 1]) lines.pop();
return lines.join('\n');
}
// ── Open / Close ──
export function openCanvas(callback, existingText) {
onInsert = callback;
if (existingText) {
// Parse existing canvas content
const lines = existingText.split('\n');
rows = Math.max(lines.length, 5);
cols = Math.max(...lines.map(l => l.length), 20);
heightInput.value = rows;
widthInput.value = cols;
data = [];
for (let r = 0; r < rows; r++) {
const row = [];
const line = lines[r] || '';
for (let c = 0; c < cols; c++) {
row.push(c < line.length ? line[c] : ' ');
}
data.push(row);
}
} else {
// Auto-fit to available grid container space
overlay.classList.add('open');
const container = grid.parentElement;
requestAnimationFrame(() => {
const style = getComputedStyle(grid);
// Measure one character by rendering a single char
grid.textContent = 'M';
const charW = grid.getBoundingClientRect().width;
const charH = grid.getBoundingClientRect().height;
grid.textContent = '';
if (charW > 0 && charH > 0) {
const pad = parseFloat(getComputedStyle(container).paddingLeft) * 2;
cols = Math.max(20, Math.floor((container.clientWidth - pad) / charW));
rows = Math.max(5, Math.floor((container.clientHeight - pad) / charH));
} else {
cols = 80;
rows = 30;
}
widthInput.value = cols;
heightInput.value = rows;
initGrid(cols, rows);
buildPalette();
selectTool('brush');
measureCells();
});
return;
}
buildPalette();
selectTool('brush');
overlay.classList.add('open');
render();
requestAnimationFrame(() => measureCells());
}
function closeCanvas() {
overlay.classList.remove('open');
textCursor = null;
drawing = false;
onInsert = null;
}
// ── Event binding ──
function init() {
if (!overlay) return;
grid.addEventListener('mousedown', onMouseDown);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
// Keyboard for text tool + tool shortcuts
overlay.addEventListener('keydown', e => {
if (textCursor && handleTextKey(e)) { e.preventDefault(); e.stopPropagation(); return; }
if (e.key === 'Escape') { closeCanvas(); e.stopPropagation(); return; }
if (e.ctrlKey || e.metaKey) return;
const shortcuts = { b: 'brush', e: 'eraser', l: 'line', r: 'rect', o: 'circle', f: 'fill', t: 'text' };
if (shortcuts[e.key]) { selectTool(shortcuts[e.key]); e.preventDefault(); }
});
// Tool buttons
toolbar.addEventListener('click', e => {
const btn = e.target.closest('[data-tool]');
if (btn) selectTool(btn.dataset.tool);
});
// Char input
charInput.addEventListener('input', () => {
if (charInput.value.length > 0) {
char = charInput.value[charInput.value.length - 1];
charInput.value = char;
// Update palette highlight
const palette = $('ascii-char-palette');
palette?.querySelectorAll('.ascii-char-btn').forEach(b => {
b.classList.toggle('active', b.textContent === char);
});
}
});
// OK button
btnOk.addEventListener('click', () => {
const text = exportText();
if (onInsert && text.trim()) onInsert(text);
closeCanvas();
});
// Cancel
btnCancel.addEventListener('click', closeCanvas);
// Clear
btnClear.addEventListener('click', () => {
initGrid(cols, rows);
});
// Resize
btnResize.addEventListener('click', () => {
const newW = parseInt(widthInput.value) || cols;
const newH = parseInt(heightInput.value) || rows;
// Preserve existing content
const oldData = data;
const oldRows = rows, oldCols = cols;
cols = Math.max(5, Math.min(200, newW));
rows = Math.max(3, Math.min(100, newH));
widthInput.value = cols;
heightInput.value = rows;
data = [];
for (let r = 0; r < rows; r++) {
const row = [];
for (let c = 0; c < cols; c++) {
row.push(r < oldRows && c < oldCols ? oldData[r][c] : ' ');
}
data.push(row);
}
render();
});
// Close on overlay background click
overlay.addEventListener('click', e => {
if (e.target === overlay) closeCanvas();
});
}
init();
export { closeCanvas };
+32
View File
@@ -4,6 +4,7 @@ import { $, esc, api, escRe, getCsrf, fuzzyMatch } from './util.js';
import { state, getChildren, CARD_COLORS } from './state.js';
import { renderMd, addCodeCopyButtons, formatTodoItems } from './markdown.js';
import { pushCardEdit, snapshotCard } from './undo.js';
import { openCanvas } from './canvas.js';
const modal = $('modal');
const body = $('editor-body');
@@ -489,6 +490,37 @@ $('md-toolbar')?.addEventListener('click', e => {
updatePreview();
},
hr: () => insertMd('\n---\n'),
asciiart: () => {
// Check if cursor is inside an existing ```ascii block to edit it
const val = body.value;
const pos = body.selectionStart;
const before = val.substring(0, pos);
const after = val.substring(pos);
const blockStart = before.lastIndexOf('```ascii\n');
const blockEndInBefore = blockStart >= 0 ? before.indexOf('\n```', blockStart + 9) : -1;
let existingText = null, replaceStart = 0, replaceEnd = 0;
if (blockStart >= 0 && blockEndInBefore < 0) {
// Cursor is inside a ```ascii block
const contentStart = blockStart + 9; // after "```ascii\n"
const endMark = val.indexOf('\n```', contentStart);
if (endMark >= 0) {
existingText = val.substring(contentStart, endMark);
replaceStart = blockStart;
replaceEnd = endMark + 4;
}
}
openCanvas(text => {
const block = '```ascii\n' + text + '\n```';
if (existingText !== null) {
body.setRangeText(block, replaceStart, replaceEnd, 'end');
} else {
const s = body.selectionStart;
const prefix = s > 0 && val[s - 1] !== '\n' ? '\n' : '';
body.setRangeText(prefix + block + '\n', s, body.selectionEnd, 'end');
}
updatePreview();
}, existingText);
},
};
cmds[btn.dataset.md]?.();
});
+4
View File
@@ -80,6 +80,10 @@ export function renderMd(src) {
});
processed = processed
// ASCII canvas block: ```ascii
.replace(/```ascii\s*\n([\s\S]*?)(?:```|$)/g, (_, content) => {
return `<pre class="ascii-art">${esc(content.replace(/\s+$/, ''))}</pre>`;
})
// Gallery fenced block: ```gallery
.replace(/```gallery\s*\n([\s\S]*?)(?:```|$)/g, (_, content) => {
const imgs = [];