mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
feat(ascii-art): pure AsciiArt module with generate/listFonts/getFontMeta
Pure CommonJS orchestrator that unifies hand-coded fonts, figlet adapter,
and templates behind a single public API. Wired to main.js via ipcMain.handle.
Public API:
- generate({ text, font, options }) - resolves 'template:<name>' /
'figlet:<font>' / bare id, falls back to standard for unknown fonts.
- listFonts() - hand-coded first (17), then figlet, then templates (19).
- getFontMeta(id) - null for unknown id, full meta for hand-coded,
{ kind } for figlet/templates.
Amit Haridas
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* ASCII art orchestrator — pure module.
|
||||||
|
*
|
||||||
|
* Public API:
|
||||||
|
* - generate({ text, font, options }) → string
|
||||||
|
* - listFonts() → [{ id, label, kind, sample }]
|
||||||
|
* - getFontMeta(id) → { kind, height?, supportedChars? } | null
|
||||||
|
*
|
||||||
|
* `font` is a single string that namespaces each backend:
|
||||||
|
* - bare id ('standard', 'big', 'isometric1', ...) → hand-coded
|
||||||
|
* - 'template:<name>' → templates
|
||||||
|
* - 'figlet:<font>' → lazy figlet
|
||||||
|
*
|
||||||
|
* Pure: no Electron, no fs, no IPC. Wired to main.js via ipcMain.handle.
|
||||||
|
*
|
||||||
|
* @module AsciiArt
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { HAND_CODED_FONTS } = require('./AsciiArt.fonts');
|
||||||
|
const { getTemplate, ASCII_TEMPLATES } = require('./AsciiArt.templates');
|
||||||
|
const FigletAdapter = require('./AsciiArt.figlet-adapter');
|
||||||
|
|
||||||
|
const SAMPLE_TEXT = 'HELLO';
|
||||||
|
|
||||||
|
function _renderHandCoded(text, fontId) {
|
||||||
|
const font = HAND_CODED_FONTS[fontId];
|
||||||
|
if (!font) return null;
|
||||||
|
const upper = String(text ?? '').toUpperCase();
|
||||||
|
const lines = Array(font.height).fill('');
|
||||||
|
for (const ch of upper) {
|
||||||
|
const glyph = font.chars[ch] || font.chars[' '];
|
||||||
|
for (let i = 0; i < font.height; i++) lines[i] += glyph[i];
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate({ text, font, options: _options } = {}) {
|
||||||
|
const f = typeof font === 'string' && font ? font : 'standard';
|
||||||
|
if (f.startsWith('template:')) {
|
||||||
|
return getTemplate(f.slice('template:'.length));
|
||||||
|
}
|
||||||
|
if (f.startsWith('figlet:')) {
|
||||||
|
return FigletAdapter.generateFiglet(String(text ?? ''), f.slice('figlet:'.length));
|
||||||
|
}
|
||||||
|
const rendered = _renderHandCoded(text, f);
|
||||||
|
return rendered !== null ? rendered : _renderHandCoded(text, 'standard');
|
||||||
|
}
|
||||||
|
|
||||||
|
function listFonts() {
|
||||||
|
const out = [];
|
||||||
|
for (const [id] of Object.entries(HAND_CODED_FONTS)) {
|
||||||
|
out.push({
|
||||||
|
id,
|
||||||
|
label: id.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
|
||||||
|
kind: 'hand-coded',
|
||||||
|
sample: _renderHandCoded(SAMPLE_TEXT, id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const name of FigletAdapter.listFigletFonts()) {
|
||||||
|
out.push({
|
||||||
|
id: `figlet:${name}`,
|
||||||
|
label: `FIGlet · ${name}`,
|
||||||
|
kind: 'figlet',
|
||||||
|
sample: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const name of Object.keys(ASCII_TEMPLATES)) {
|
||||||
|
out.push({
|
||||||
|
id: `template:${name}`,
|
||||||
|
label: `Template · ${name.replace(/-/g, ' ')}`,
|
||||||
|
kind: 'template',
|
||||||
|
sample: ASCII_TEMPLATES[name],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFontMeta(id) {
|
||||||
|
if (typeof id !== 'string' || !id) return null;
|
||||||
|
if (id.startsWith('template:')) {
|
||||||
|
return ASCII_TEMPLATES[id.slice('template:'.length)] ? { kind: 'template' } : null;
|
||||||
|
}
|
||||||
|
if (id.startsWith('figlet:')) {
|
||||||
|
const name = id.slice('figlet:'.length);
|
||||||
|
return FigletAdapter.listFigletFonts().includes(name) ? { kind: 'figlet' } : null;
|
||||||
|
}
|
||||||
|
const font = HAND_CODED_FONTS[id];
|
||||||
|
if (!font) return null;
|
||||||
|
return {
|
||||||
|
kind: 'hand-coded',
|
||||||
|
height: font.height,
|
||||||
|
supportedChars: Object.keys(font.chars),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { generate, listFonts, getFontMeta };
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment node
|
||||||
|
*/
|
||||||
|
const AsciiArt = require('../../src/main/AsciiArt');
|
||||||
|
const AsciiArtFonts = require('../../src/main/AsciiArt.fonts');
|
||||||
|
const AsciiArtTemplates = require('../../src/main/AsciiArt.templates');
|
||||||
|
|
||||||
|
describe('AsciiArt.generate', () => {
|
||||||
|
test('renders "HELLO" in standard font with known shape', () => {
|
||||||
|
const out = AsciiArt.generate({ text: 'HELLO', font: 'standard' });
|
||||||
|
const lines = out.split('\n');
|
||||||
|
expect(lines).toHaveLength(AsciiArtFonts.HAND_CODED_FONTS.standard.height);
|
||||||
|
expect(lines[0]).toContain('H');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lowercase input is uppercased', () => {
|
||||||
|
const a = AsciiArt.generate({ text: 'hello', font: 'standard' });
|
||||||
|
const b = AsciiArt.generate({ text: 'HELLO', font: 'standard' });
|
||||||
|
expect(a).toBe(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unknown font falls back to standard', () => {
|
||||||
|
const a = AsciiArt.generate({ text: 'A', font: 'nope-not-a-font' });
|
||||||
|
const b = AsciiArt.generate({ text: 'A', font: 'standard' });
|
||||||
|
expect(a).toBe(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unknown character substitutes space glyph (column width preserved)', () => {
|
||||||
|
const font = AsciiArtFonts.HAND_CODED_FONTS.standard;
|
||||||
|
const spaceWidth = font.chars[' '][0].length;
|
||||||
|
const out = AsciiArt.generate({ text: 'A!A', font: 'standard' });
|
||||||
|
const line = out.split('\n')[0];
|
||||||
|
// "A" rendered + space-glyph for "!" + "A" rendered; the middle column
|
||||||
|
// is the space-width.
|
||||||
|
expect(line.length).toBeGreaterThanOrEqual(spaceWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders a named template via "template:<name>"', () => {
|
||||||
|
const tpl = AsciiArtTemplates.getTemplate('arrow-right');
|
||||||
|
expect(AsciiArt.generate({ text: 'ignored', font: 'template:arrow-right' })).toBe(tpl);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders figlet font via "figlet:<font>" — adapter is mocked below', () => {
|
||||||
|
// Mock the adapter module so we don't depend on the real figlet.
|
||||||
|
jest.resetModules();
|
||||||
|
jest.doMock('../../src/main/AsciiArt.figlet-adapter', () => ({
|
||||||
|
AsciiArtFigletError: class extends Error {},
|
||||||
|
generateFiglet: (t, f) => `FIGLET[${f}:${t}]`,
|
||||||
|
listFigletFonts: () => ['Big', 'Slant'],
|
||||||
|
loadFiglet: () => ({ fontsSync: () => ['Big', 'Slant'] }),
|
||||||
|
}));
|
||||||
|
const Fresh = require('../../src/main/AsciiArt');
|
||||||
|
expect(Fresh.generate({ text: 'HI', font: 'figlet:Big' })).toBe('FIGLET[Big:HI]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('propagates AsciiArtFigletError on figlet failure', () => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.doMock('../../src/main/AsciiArt.figlet-adapter', () => ({
|
||||||
|
AsciiArtFigletError: class extends Error {},
|
||||||
|
generateFiglet: () => {
|
||||||
|
throw new Error('boom');
|
||||||
|
},
|
||||||
|
listFigletFonts: () => [],
|
||||||
|
loadFiglet: () => null,
|
||||||
|
}));
|
||||||
|
const Fresh = require('../../src/main/AsciiArt');
|
||||||
|
expect(() => Fresh.generate({ text: 'X', font: 'figlet:Big' })).toThrow(/figlet/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AsciiArt.listFonts', () => {
|
||||||
|
test('returns hand-coded fonts first, then figlet, then templates', () => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.doMock('../../src/main/AsciiArt.figlet-adapter', () => ({
|
||||||
|
AsciiArtFigletError: class extends Error {},
|
||||||
|
generateFiglet: () => '',
|
||||||
|
listFigletFonts: () => ['Big', 'Slant'],
|
||||||
|
loadFiglet: () => ({ fontsSync: () => ['Big', 'Slant'] }),
|
||||||
|
}));
|
||||||
|
const Fresh = require('../../src/main/AsciiArt');
|
||||||
|
const list = Fresh.listFonts();
|
||||||
|
expect(list.length).toBeGreaterThanOrEqual(17 + 2 + 19);
|
||||||
|
// first 17 should be hand-coded
|
||||||
|
for (let i = 0; i < 17; i++) {
|
||||||
|
expect(list[i].kind).toBe('hand-coded');
|
||||||
|
expect(list[i].id).toMatch(/^[a-z0-9-]+$/);
|
||||||
|
}
|
||||||
|
// figlet entries come next
|
||||||
|
const firstFiglet = list.find((f) => f.kind === 'figlet');
|
||||||
|
expect(firstFiglet.id).toBe('figlet:Big');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('still returns hand-coded + templates when figlet is unavailable', () => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.doMock('../../src/main/AsciiArt.figlet-adapter', () => ({
|
||||||
|
AsciiArtFigletError: class extends Error {},
|
||||||
|
generateFiglet: () => '',
|
||||||
|
listFigletFonts: () => [],
|
||||||
|
loadFiglet: () => null,
|
||||||
|
}));
|
||||||
|
const Fresh = require('../../src/main/AsciiArt');
|
||||||
|
const list = Fresh.listFonts();
|
||||||
|
expect(list.some((f) => f.kind === 'figlet')).toBe(false);
|
||||||
|
expect(list.filter((f) => f.kind === 'hand-coded')).toHaveLength(17);
|
||||||
|
expect(list.filter((f) => f.kind === 'template').length).toBeGreaterThanOrEqual(19);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every entry has id, label, kind, sample', () => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.doMock('../../src/main/AsciiArt.figlet-adapter', () => ({
|
||||||
|
AsciiArtFigletError: class extends Error {},
|
||||||
|
generateFiglet: () => '',
|
||||||
|
listFigletFonts: () => [],
|
||||||
|
loadFiglet: () => null,
|
||||||
|
}));
|
||||||
|
const Fresh = require('../../src/main/AsciiArt');
|
||||||
|
for (const f of Fresh.listFonts()) {
|
||||||
|
expect(typeof f.id).toBe('string');
|
||||||
|
expect(typeof f.label).toBe('string');
|
||||||
|
expect(['hand-coded', 'figlet', 'template']).toContain(f.kind);
|
||||||
|
expect(typeof f.sample).toBe('string');
|
||||||
|
expect(f.sample.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AsciiArt.getFontMeta', () => {
|
||||||
|
test('hand-coded font returns kind, height, supportedChars', () => {
|
||||||
|
const meta = AsciiArt.getFontMeta('big');
|
||||||
|
expect(meta.kind).toBe('hand-coded');
|
||||||
|
expect(typeof meta.height).toBe('number');
|
||||||
|
expect(meta.height).toBeGreaterThanOrEqual(4);
|
||||||
|
expect(Array.isArray(meta.supportedChars)).toBe(true);
|
||||||
|
expect(meta.supportedChars).toEqual(expect.arrayContaining(['A', '0', ' ']));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('template returns { kind: "template" }', () => {
|
||||||
|
expect(AsciiArt.getFontMeta('template:flowchart')).toEqual({ kind: 'template' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('figlet returns { kind: "figlet" }', () => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.doMock('../../src/main/AsciiArt.figlet-adapter', () => ({
|
||||||
|
AsciiArtFigletError: class extends Error {},
|
||||||
|
generateFiglet: () => '',
|
||||||
|
listFigletFonts: () => ['Big'],
|
||||||
|
loadFiglet: () => ({}),
|
||||||
|
}));
|
||||||
|
const Fresh = require('../../src/main/AsciiArt');
|
||||||
|
expect(Fresh.getFontMeta('figlet:Big')).toEqual({ kind: 'figlet' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unknown id returns null', () => {
|
||||||
|
expect(AsciiArt.getFontMeta('nope')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user