mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
fix(ascii-art): wire Box/Templates mode + correct standalone <link href>; v4.9.2
- src/ascii-generator.html line 7: ../fonts.css -> fonts.css (src/-relative; fixes silent stylesheet miss — same class as the v4.9.0->v4.9.1 script-tag fix) - src/renderer/ascii-controller.js: wire all 3 mode tabs, 18 template buttons, and the 3 box form fields. setMode() toggles .active on tabs and matching .mode-section. Templates route through the orchestrator's template:<id> font namespace. Box mode renders text with single/double/ rounded/bold/ascii borders via a pure renderBox() helper. All 11 T9 behaviours preserved. - tests/ascii-controller.test.js: 6 tests covering stylesheet path, pure box renderer, mode-tab switching, and template button wiring. - package.json 4.9.1 -> 4.9.2; README + UPDATES updated. Amit Haridas
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ASCII Art Generator - MarkdownConverter</title>
|
||||
<link rel="stylesheet" href="../fonts.css" />
|
||||
<link rel="stylesheet" href="fonts.css" />
|
||||
<style>
|
||||
:root {
|
||||
--ci-dark-gray: #464646;
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
* Insert / Copy / Save actions. Last-used font is restored from
|
||||
* electron-store (via main) and saved on every change.
|
||||
*
|
||||
* Wires three mode tabs (Text Banner / Box-Frame / Templates) plus the
|
||||
* 18 template buttons and the Box form fields (#box-text, #box-style,
|
||||
* #box-padding). Templates route through `api.generate({ font: 'template:<id>' })`
|
||||
* so the orchestrator owns template content.
|
||||
*
|
||||
* No bundler, no framework — pure DOM + window.electronAPI bridge.
|
||||
*/
|
||||
(function () {
|
||||
@@ -23,12 +28,26 @@
|
||||
btnSave: document.getElementById('btn-save'),
|
||||
btnGenerate: document.getElementById('btn-generate'),
|
||||
warning: document.getElementById('ascii-warning'),
|
||||
modeTabs: document.querySelectorAll('.mode-tab[data-mode]'),
|
||||
modeSections: document.querySelectorAll('.mode-section'),
|
||||
templateButtons: document.querySelectorAll('.template-btn[data-template]'),
|
||||
boxText: document.getElementById('box-text'),
|
||||
boxStyle: document.getElementById('box-style'),
|
||||
boxPadding: document.getElementById('box-padding'),
|
||||
};
|
||||
|
||||
// Section ID for each mode. Keep in sync with ascii-generator.html IDs.
|
||||
const MODE_SECTION_ID = {
|
||||
text: 'text-mode',
|
||||
box: 'box-mode',
|
||||
templates: 'templates-mode',
|
||||
};
|
||||
|
||||
let _fonts = [];
|
||||
let _currentFont = 'standard';
|
||||
let _previewText = '';
|
||||
let _searchDebounce = null;
|
||||
let _currentMode = 'text';
|
||||
|
||||
function showWarning(msg) {
|
||||
if (!els.warning) return;
|
||||
@@ -45,8 +64,124 @@
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the active mode tab and the visible mode section.
|
||||
* Toggles the `.active` class on tabs and on the matching `.mode-section`.
|
||||
* @param {'text'|'box'|'templates'} mode
|
||||
*/
|
||||
function setMode(mode) {
|
||||
if (!MODE_SECTION_ID[mode]) return;
|
||||
_currentMode = mode;
|
||||
|
||||
// Toggle tab buttons.
|
||||
els.modeTabs.forEach((tab) => {
|
||||
if (tab.dataset.mode === mode) tab.classList.add('active');
|
||||
else tab.classList.remove('active');
|
||||
});
|
||||
|
||||
// Toggle mode sections.
|
||||
els.modeSections.forEach((sec) => {
|
||||
if (sec.id === MODE_SECTION_ID[mode]) sec.classList.add('active');
|
||||
else sec.classList.remove('active');
|
||||
});
|
||||
|
||||
// Each mode has its own preview pipeline; trigger a refresh.
|
||||
refreshPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a simple box border around the user's text using the chosen style.
|
||||
* Pure function — exported on window for tests via ASCIIBoxRenderer.
|
||||
* @param {string} text
|
||||
* @param {'single'|'double'|'rounded'|'bold'|'ascii'} style
|
||||
* @param {number} padding 0..10 spaces on each side
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderBox(text, style, padding) {
|
||||
const lines = String(text ?? '').split('\n');
|
||||
const pad = Math.max(0, Math.min(10, Number(padding) || 0));
|
||||
const padded = lines.map((l) => ' '.repeat(pad) + l + ' '.repeat(pad));
|
||||
const width = padded.reduce((w, l) => Math.max(w, visibleLength(l)), 0);
|
||||
|
||||
let tl, tr, bl, br, h, v;
|
||||
switch (style) {
|
||||
case 'double':
|
||||
tl = '╔';
|
||||
tr = '╗';
|
||||
bl = '╚';
|
||||
br = '╝';
|
||||
h = '═';
|
||||
v = '║';
|
||||
break;
|
||||
case 'rounded':
|
||||
tl = '╭';
|
||||
tr = '╮';
|
||||
bl = '╰';
|
||||
br = '╯';
|
||||
h = '─';
|
||||
v = '│';
|
||||
break;
|
||||
case 'bold':
|
||||
tl = '┏';
|
||||
tr = '┓';
|
||||
bl = '┗';
|
||||
br = '┛';
|
||||
h = '━';
|
||||
v = '┃';
|
||||
break;
|
||||
case 'ascii':
|
||||
tl = '+';
|
||||
tr = '+';
|
||||
bl = '+';
|
||||
br = '+';
|
||||
h = '-';
|
||||
v = '|';
|
||||
break;
|
||||
case 'single':
|
||||
default:
|
||||
tl = '┌';
|
||||
tr = '┐';
|
||||
bl = '└';
|
||||
br = '┘';
|
||||
h = '─';
|
||||
v = '│';
|
||||
break;
|
||||
}
|
||||
|
||||
const top = tl + h.repeat(width + 2) + tr;
|
||||
const bot = bl + h.repeat(width + 2) + br;
|
||||
const body = padded.map((l) => v + ' ' + l.padEnd(width, ' ') + ' ' + v).join('\n');
|
||||
return [top, body, bot].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Count printable character width, ignoring box-drawing characters and
|
||||
* spaces (which all occupy a single monospace column).
|
||||
*/
|
||||
function visibleLength(s) {
|
||||
// Box-drawing chars and most printable ASCII occupy one column in our
|
||||
// monospace font. This is a deliberate simplification — emoji or wide
|
||||
// CJK would need a more sophisticated width table.
|
||||
return Array.from(s).length;
|
||||
}
|
||||
|
||||
async function refreshPreview() {
|
||||
if (!api) return;
|
||||
if (_currentMode === 'templates') {
|
||||
// Template mode is button-driven; no live preview here.
|
||||
return;
|
||||
}
|
||||
if (_currentMode === 'box') {
|
||||
const text = els.boxText ? els.boxText.value : '';
|
||||
const style = els.boxStyle ? els.boxStyle.value : 'single';
|
||||
const padding = els.boxPadding ? els.boxPadding.value : 2;
|
||||
_previewText = text;
|
||||
const out = renderBox(text, style, padding);
|
||||
els.preview.textContent = out;
|
||||
showWarning('');
|
||||
return;
|
||||
}
|
||||
// text mode (default)
|
||||
const text = els.textInput ? els.textInput.value : 'HELLO';
|
||||
_previewText = text;
|
||||
try {
|
||||
@@ -93,6 +228,26 @@
|
||||
renderFontList('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a template by id: fetch its content via the orchestrator's
|
||||
* template:<id> font namespace and update the preview.
|
||||
*/
|
||||
async function renderTemplate(templateId) {
|
||||
if (!api) return;
|
||||
try {
|
||||
const out = await api.generate({ text: '', font: `template:${templateId}` });
|
||||
els.preview.textContent = out;
|
||||
showWarning('');
|
||||
// Mark the clicked button as active.
|
||||
els.templateButtons.forEach((btn) => {
|
||||
if (btn.dataset.template === templateId) btn.classList.add('active');
|
||||
else btn.classList.remove('active');
|
||||
});
|
||||
} catch (err) {
|
||||
showWarning(`Template render failed: ${err.message || err}`);
|
||||
}
|
||||
}
|
||||
|
||||
function wireEvents() {
|
||||
if (els.textInput) {
|
||||
els.textInput.addEventListener('input', debounce(refreshPreview, 200));
|
||||
@@ -151,6 +306,39 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- mode-tab switching (Text Banner / Box-Frame / Templates) ----
|
||||
els.modeTabs.forEach((tab) => {
|
||||
tab.addEventListener('click', () => {
|
||||
const mode = tab.dataset.mode;
|
||||
if (mode) setMode(mode);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Box form: any change re-renders the box preview ----
|
||||
if (els.boxText) {
|
||||
els.boxText.addEventListener('input', () => {
|
||||
if (_currentMode === 'box') refreshPreview();
|
||||
});
|
||||
}
|
||||
if (els.boxStyle) {
|
||||
els.boxStyle.addEventListener('change', () => {
|
||||
if (_currentMode === 'box') refreshPreview();
|
||||
});
|
||||
}
|
||||
if (els.boxPadding) {
|
||||
els.boxPadding.addEventListener('input', () => {
|
||||
if (_currentMode === 'box') refreshPreview();
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Template buttons: render the chosen template into the preview ----
|
||||
els.templateButtons.forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = btn.dataset.template;
|
||||
if (id) renderTemplate(id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
@@ -159,7 +347,10 @@
|
||||
await refreshPreview();
|
||||
}
|
||||
|
||||
// Expose pure helpers for tests; do not pollute window in production beyond
|
||||
// what the bootstrap entry-point needs.
|
||||
window.ASCIIController = { bootstrap };
|
||||
window.ASCIIBoxRenderer = { renderBox };
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bootstrap);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user