diff --git a/packages/core/src/examples/custom-font-demo.ts b/packages/core/src/examples/custom-font-demo.ts new file mode 100644 index 000000000..3efd1c8dc --- /dev/null +++ b/packages/core/src/examples/custom-font-demo.ts @@ -0,0 +1,78 @@ +#!/usr/bin/env bun +import { createCliRenderer, ASCIIFontRenderable, BoxRenderable, fonts, type FontDefinition } from ".." + +// Example of importing an external font +// Users would do: import hugeFont from "./fonts/huge.json" +// For demo, we'll use the built-in fonts and show how to use custom ones + +async function main() { + const renderer = await createCliRenderer({ + exitOnCtrlC: true, + useMouse: true, + useAlternateScreen: true, + }) + + const container = new BoxRenderable("container", { + width: "100%", + height: "100%", + borderStyle: "rounded", + borderColor: "#3498db", + padding: 2, + flexDirection: "column", + gap: 2, + }) + + // Using built-in font (default) + const title1 = new ASCIIFontRenderable("title1", { + text: "TINY FONT", + font: fonts.tiny, // Explicitly passing FontDefinition + fg: "#ffffff", + }) + + // Using different built-in font + const title2 = new ASCIIFontRenderable("title2", { + text: "BLOCK", + font: fonts.block, + fg: "#00ff00", + }) + + // Using another built-in font + const title3 = new ASCIIFontRenderable("title3", { + text: "SHADE", + font: fonts.shade, + fg: "#ff00ff", + }) + + // Using slick font + const title4 = new ASCIIFontRenderable("title4", { + text: "SLICK", + font: fonts.slick, + fg: "#ffff00", + }) + + // Example of how users would use custom fonts: + // import customFont from "./my-custom-font.json" + // const customTitle = new ASCIIFontRenderable("custom", { + // text: "CUSTOM", + // font: customFont as FontDefinition, + // fg: "#00ffff", + // }) + + container.add(title1) + container.add(title2) + container.add(title3) + container.add(title4) + + renderer.root.add(container) + renderer.start() + + // Exit on escape + renderer.on("key", (key) => { + if (key.name === "escape") { + renderer.destroy() + process.exit(0) + } + }) +} + +main().catch(console.error) \ No newline at end of file diff --git a/packages/core/src/examples/external-font-demo.ts b/packages/core/src/examples/external-font-demo.ts new file mode 100644 index 000000000..1434030b6 --- /dev/null +++ b/packages/core/src/examples/external-font-demo.ts @@ -0,0 +1,53 @@ +#!/usr/bin/env bun +import { createCliRenderer, ASCIIFontRenderable, BoxRenderable, fonts, type FontDefinition } from ".." + +// Import external font - demonstrating how users can add custom fonts +import gridFont from "./grid-font.json" + +async function main() { + const renderer = await createCliRenderer({ + exitOnCtrlC: true, + useMouse: true, + useAlternateScreen: true, + }) + + const container = new BoxRenderable("container", { + width: "100%", + height: "100%", + borderStyle: "rounded", + borderColor: "#3498db", + padding: 2, + flexDirection: "column", + gap: 2, + }) + + // Using built-in font + const title1 = new ASCIIFontRenderable("title1", { + text: "BUILT-IN", + font: fonts.block, + fg: "#00ff00", + }) + + // Using external font loaded from JSON file + const title2 = new ASCIIFontRenderable("title2", { + text: "CUSTOM", + font: gridFont as FontDefinition, + fg: "#ff00ff", + }) + + container.add(title1) + container.add(title2) + + renderer.root.add(container) + renderer.start() + + // Exit on escape + renderer.on("key", (key) => { + if (key.name === "escape") { + renderer.destroy() + process.exit(0) + } + }) +} + +main().catch(console.error) \ No newline at end of file diff --git a/packages/core/src/examples/font-validation-demo.ts b/packages/core/src/examples/font-validation-demo.ts new file mode 100644 index 000000000..34c5f1c72 --- /dev/null +++ b/packages/core/src/examples/font-validation-demo.ts @@ -0,0 +1,133 @@ +#!/usr/bin/env bun +import { createCliRenderer, ASCIIFontRenderable, BoxRenderable, type FontDefinition, validateFontDefinition } from ".." + +// Test invalid font definitions +const invalidFonts = [ + { + name: "Missing lines", + font: { + name: "invalid1", + letterspace_size: 1, + letterspace: [" "], + chars: { "A": ["A"] } + } + }, + { + name: "Wrong letterspace length", + font: { + name: "invalid2", + lines: 2, + letterspace_size: 1, + letterspace: [" "], // Should be 2 items + chars: { "A": ["▄", "█"] } + } + }, + { + name: "Character line count mismatch", + font: { + name: "invalid3", + lines: 2, + letterspace_size: 1, + letterspace: [" ", " "], + chars: { "A": ["▄"] } // Should have 2 lines + } + }, + { + name: "Invalid colors", + font: { + name: "invalid4", + lines: 1, + letterspace_size: 1, + letterspace: [" "], + colors: -1, // Must be positive + chars: { "A": ["A"] } + } + } +] + +// Valid font definition +const validFont: FontDefinition = { + name: "valid", + lines: 2, + letterspace_size: 1, + letterspace: [" ", " "], + chars: { + "T": ["▀█▀", " █ "], + "E": ["█▀▀", "██▄"], + "S": ["█▀▀", "▄▄█"], + " ": [" ", " "] + } +} + +async function main() { + console.log("Testing font validation...\n") + + // Test invalid fonts + for (const { name, font } of invalidFonts) { + try { + validateFontDefinition(font) + console.log(`❌ ${name}: Should have failed but didn't`) + } catch (error) { + console.log(`✅ ${name}: Correctly rejected - ${error.message}`) + } + } + + // Test valid font + try { + validateFontDefinition(validFont) + console.log(`✅ Valid font: Correctly accepted`) + } catch (error) { + console.log(`❌ Valid font: Should have passed but failed - ${error.message}`) + } + + console.log("\nTesting ASCIIFontRenderable with invalid font...") + + const renderer = await createCliRenderer({ + exitOnCtrlC: true, + useAlternateScreen: true, + }) + + const container = new BoxRenderable("container", { + width: "100%", + height: "100%", + padding: 2, + flexDirection: "column", + gap: 2, + }) + + // This should work - valid font + try { + const validText = new ASCIIFontRenderable("valid", { + text: "TEST", + font: validFont, + fg: "#00ff00" + }) + container.add(validText) + console.log("✅ ASCIIFontRenderable accepted valid font") + } catch (error) { + console.log(`❌ ASCIIFontRenderable rejected valid font: ${error.message}`) + } + + // This should fail - invalid font + try { + const invalidText = new ASCIIFontRenderable("invalid", { + text: "FAIL", + font: invalidFonts[0].font as FontDefinition, + fg: "#ff0000" + }) + container.add(invalidText) + console.log("❌ ASCIIFontRenderable accepted invalid font") + } catch (error) { + console.log(`✅ ASCIIFontRenderable correctly rejected invalid font`) + } + + renderer.root.add(container) + renderer.start() + + setTimeout(() => { + renderer.destroy() + process.exit(0) + }, 2000) +} + +main().catch(console.error) \ No newline at end of file diff --git a/packages/core/src/examples/grid-font.json b/packages/core/src/examples/grid-font.json new file mode 100644 index 000000000..4a3a5e6ac --- /dev/null +++ b/packages/core/src/examples/grid-font.json @@ -0,0 +1,482 @@ +{ + "name": "grid", + "version": "0.1.0", + "homepage": "https://github.com/dominikwilkowski/cfonts", + "colors": 2, + "lines": 6, + "buffer": [ + "", + "", + "", + "", + "", + "" + ], + "letterspace": [ + "", + "", + "", + "", + "", + "" + ], + "letterspace_size": 1, + "chars": { + "A": [ + "╋╋╋╋", + "┏━━┓", + "┃┏┓┃", + "┃┏┓┃", + "┗┛┗┛", + "╋╋╋╋" + ], + "B": [ + "┏┓╋╋", + "┃┗━┓", + "┃┏┓┃", + "┃┗┛┃", + "┗━━┛", + "╋╋╋╋" + ], + "C": [ + "╋╋╋╋", + "┏━━┓", + "┃┏━┛", + "┃┗━┓", + "┗━━┛", + "╋╋╋╋" + ], + "D": [ + "╋╋┏┓", + "┏━┛┃", + "┃┏┓┃", + "┃┗┛┃", + "┗━━┛", + "╋╋╋╋" + ], + "E": [ + "╋╋╋╋", + "┏━━┓", + "┃┃━┫", + "┃┃━┫", + "┗━━┛", + "╋╋╋╋" + ], + "F": [ + "┏━┓", + "┏┛┗┓", + "┗┓┏┛", + "┃┃", + "┗┛", + "╋╋╋╋" + ], + "G": [ + "╋╋╋╋", + "┏━━┓", + "┃┏┓┃", + "┃┗┛┃", + "┗━┓┃", + "┗━━┛" + ], + "H": [ + "┏┓╋╋", + "┃┗━┓", + "┃┏┓┃", + "┃┃┃┃", + "┗┛┗┛", + "╋╋╋╋" + ], + "I": [ + "┏┓", + "┗┛", + "┏┓", + "┃┃", + "┗┛", + "╋╋" + ], + "J": [ + "┏┓", + "┗┛", + "┏┓", + "┃┃", + "┏┛┃", + "┗━┛" + ], + "K": [ + "┏┓╋╋", + "┃┃┏┓", + "┃┗┛┛", + "┃┏┓┓", + "┗┛┗┛", + "╋╋╋╋" + ], + "L": [ + "┏┓", + "┃┃", + "┃┃", + "┃┗┓", + "┗━┛", + "╋╋╋" + ], + "M": [ + "╋╋╋╋", + "┏┓┏┓", + "┃┗┛┃", + "┃┃┃┃", + "┗┻┻┛", + "╋╋╋╋" + ], + "N": [ + "╋╋╋╋", + "┏━┓", + "┃┏┓┓", + "┃┃┃┃", + "┗┛┗┛", + "╋╋╋╋" + ], + "O": [ + "╋╋╋╋", + "┏━━┓", + "┃┏┓┃", + "┃┗┛┃", + "┗━━┛", + "╋╋╋╋" + ], + "P": [ + "╋╋╋╋", + "┏━━┓", + "┃┏┓┃", + "┃┗┛┃", + "┃┏━┛", + "┗┛╋╋" + ], + "Q": [ + "╋╋╋╋", + "┏━━┓", + "┃┏┓┃", + "┃┗┛┃", + "┗━┓┃", + "╋╋┗┛" + ], + "R": [ + "╋╋╋", + "┏━┓", + "┃┏┛", + "┃┃", + "┗┛", + "╋╋╋" + ], + "S": [ + "╋╋╋╋", + "┏━━┓", + "┃━━┫", + "┣━━┃", + "┗━━┛", + "╋╋╋╋" + ], + "T": [ + "┏┓", + "┏┛┗┓", + "┗┓┏┛", + "┃┗┓", + "┗━┛", + "╋╋╋╋" + ], + "U": [ + "╋╋╋╋", + "┏┓┏┓", + "┃┃┃┃", + "┃┗┛┃", + "┗━━┛", + "╋╋╋╋" + ], + "V": [ + "╋╋╋╋", + "┏┓┏┓", + "┃┗┛┃", + "┗┓┏┛", + "┗┛", + "╋╋╋╋" + ], + "W": [ + "╋╋╋╋╋╋", + "┏┓┏┓┏┓", + "┃┗┛┗┛┃", + "┗┓┏┓┏┛", + "┗┛┗┛", + "╋╋╋╋╋╋" + ], + "X": [ + "╋╋╋╋", + "┏┓┏┓", + "┗╋╋┛", + "┏╋╋┓", + "┗┛┗┛", + "╋╋╋╋" + ], + "Y": [ + "╋╋╋╋╋", + "┏┓┏┓", + "┃┗━┛┃", + "┗━┓┏┛", + "┗━━┛", + "╋╋╋╋╋" + ], + "Z": [ + "╋╋╋╋╋", + "┏━━━┓", + "┣━━┃┃", + "┃┃━━┫", + "┗━━━┛", + "╋╋╋╋╋" + ], + "0": [ + "┏━━━┓", + "┃┏━┓┃", + "┃┃┃┃┃", + "┃┃┃┃┃", + "┃┗━┛┃", + "┗━━━┛" + ], + "1": [ + "┏┓", + "┏┛┃", + "┗┓┃", + "┃┃", + "┏┛┗┓", + "┗━━┛" + ], + "2": [ + "┏━━━┓", + "┃┏━┓┃", + "┗┛┏┛┃", + "┏━┛┏┛", + "┃┗━┻┓", + "┗━━━┛" + ], + "3": [ + "┏━━━┓", + "┃┏━┓┃", + "┗┛┏┛┃", + "┏┓┗┓┃", + "┃┗━┛┃", + "┗━━━┛" + ], + "4": [ + "┏┓┏┓", + "┃┃┃┃", + "┃┗━┛┃", + "┗━━┓┃", + "╋╋╋┃┃", + "╋╋╋┗┛" + ], + "5": [ + "┏━━━┓", + "┃┏━━┛", + "┃┗━━┓", + "┗━━┓┃", + "┏━━┛┃", + "┗━━━┛" + ], + "6": [ + "┏━━━┓", + "┃┏━━┛", + "┃┗━━┓", + "┃┏━┓┃", + "┃┗━┛┃", + "┗━━━┛" + ], + "7": [ + "┏━━━┓", + "┃┏━┓┃", + "┗┛┏┛┃", + "╋╋┃┏┛", + "╋╋┃┃", + "╋╋┗┛" + ], + "8": [ + "┏━━━┓", + "┃┏━┓┃", + "┃┗━┛┃", + "┃┏━┓┃", + "┃┗━┛┃", + "┗━━━┛" + ], + "9": [ + "┏━━━┓", + "┃┏━┓┃", + "┃┗━┛┃", + "┗━━┓┃", + "┏━━┛┃", + "┗━━━┛" + ], + "!": [ + "┏┓", + "┃┃", + "┃┃", + "┗┛", + "┏┓", + "┗┛" + ], + "?": [ + "┏━━━┓", + "┃┏━┓┃", + "┗┛┏┛┃", + "╋╋┃┏┛", + "╋╋┏┓", + "╋╋┗┛" + ], + ".": [ + "╋╋", + "╋╋", + "╋╋", + "╋╋", + "┏┓", + "┗┛" + ], + "+": [ + "╋╋╋╋", + "┏┓", + "┏┛┗┓", + "┗┓┏┛", + "┗┛", + "╋╋╋╋" + ], + "-": [ + "╋╋╋╋", + "╋╋╋╋", + "┏━━┓", + "┗━━┛", + "╋╋╋╋", + "╋╋╋╋" + ], + "_": [ + "╋╋╋╋", + "╋╋╋╋", + "╋╋╋╋", + "╋╋╋╋", + "┏━━┓", + "┗━━┛" + ], + "=": [ + "╋╋╋╋╋", + "┏━━━┓", + "┗━━━┛", + "┏━━━┓", + "┗━━━┛", + "╋╋╋╋╋" + ], + "@": [ + "┏━━━━┓", + "┃┏━━┓┃", + "┃┃┏━┃┃", + "┃┃┗┛┃┃", + "┃┗━━┛┗┓", + "┗━━━━━┛" + ], + "#": [ + "┏━━━┓", + "┏┛┏━┓┗┓", + "┗┓┃┃┃┏┛", + "┏┛┃┃┃┗┓", + "┗┓┗━┛┏┛", + "┗━━━┛" + ], + "$": [ + "┏┓", + "┏┛┗┓", + "┃━━┫", + "┣━━┃", + "┗┓┏┛", + "┗┛" + ], + "%": [ + "┏┓╋╋┏━┓", + "┗┛┏┛┏┛", + "╋╋┏┛┏┛", + "┏┛┏┛╋╋", + "┏┛┏┛┏┓", + "┗━┛╋╋┗┛" + ], + "&": [ + "╋╋┏┓", + "╋╋┃┃", + "┏━┛┗┓", + "┃┏┓┏┛", + "┃┗┛┃", + "┗━━┛" + ], + "(": [ + "╋╋┏━┓", + "┏┛┏┛", + "┏┛┏┛", + "┗┓┗┓", + "┗┓┗┓", + "╋╋┗━┛" + ], + ")": [ + "┏━┓╋╋", + "┗┓┗┓", + "┗┓┗┓", + "┏┛┏┛", + "┏┛┏┛", + "┗━┛╋╋" + ], + "/": [ + "╋╋╋╋┏━┓", + "╋╋╋┏┛┏┛", + "╋╋┏┛┏┛", + "┏┛┏┛╋╋", + "┏┛┏┛╋╋╋", + "┗━┛╋╋╋╋" + ], + ":": [ + "╋╋", + "┏┓", + "┗┛", + "┏┓", + "┗┛", + "╋╋" + ], + ";": [ + "╋╋", + "┏┓", + "┗┛", + "╋╋", + "┏┓", + "┗┫" + ], + ",": [ + "╋╋", + "╋╋", + "╋╋", + "╋╋", + "┏┓", + "┗┫" + ], + "'": [ + "┏┓", + "┗┛", + "╋╋", + "╋╋", + "╋╋", + "╋╋" + ], + "\"": [ + "┏┓┏┓", + "┗┛┗┛", + "╋╋╋╋", + "╋╋╋╋", + "╋╋╋╋", + "╋╋╋╋" + ], + " ": [ + "╋╋", + "╋╋", + "╋╋", + "╋╋", + "╋╋", + "╋╋" + ] + } +} diff --git a/packages/core/src/lib/ascii.font.ts b/packages/core/src/lib/ascii.font.ts index 9d917a1ef..32ffcc698 100644 --- a/packages/core/src/lib/ascii.font.ts +++ b/packages/core/src/lib/ascii.font.ts @@ -10,11 +10,12 @@ import slick from "./fonts/slick.json" * Font definitions plugged from cfonts - https://github.com/dominikwilkowski/cfonts */ +// Export built-in fonts for convenience export const fonts = { - tiny, - block, - shade, - slick, + tiny: tiny as FontDefinition, + block: block as FontDefinition, + shade: shade as FontDefinition, + slick: slick as FontDefinition, } type FontSegment = { @@ -22,7 +23,7 @@ type FontSegment = { colorIndex: number } -type FontDefinition = { +export type FontDefinition = { name: string lines: number letterspace_size: number @@ -31,6 +32,62 @@ type FontDefinition = { chars: Record } +/** + * Validates a FontDefinition object + * @param font - Object to validate + * @returns true if valid, throws error if invalid + */ +export function validateFontDefinition(font: any): font is FontDefinition { + if (!font || typeof font !== 'object') { + throw new Error('Font definition must be an object') + } + + if (typeof font.name !== 'string') { + throw new Error('Font definition must have a "name" property of type string') + } + + if (typeof font.lines !== 'number' || font.lines < 1) { + throw new Error('Font definition must have a "lines" property with a positive number') + } + + if (typeof font.letterspace_size !== 'number' || font.letterspace_size < 0) { + throw new Error('Font definition must have a "letterspace_size" property with a non-negative number') + } + + if (!Array.isArray(font.letterspace)) { + throw new Error('Font definition must have a "letterspace" property as an array') + } + + if (font.letterspace.length !== font.lines) { + throw new Error(`Font definition letterspace array length (${font.letterspace.length}) must match lines (${font.lines})`) + } + + if (font.colors !== undefined && (typeof font.colors !== 'number' || font.colors < 1)) { + throw new Error('Font definition "colors" property must be a positive number if provided') + } + + if (!font.chars || typeof font.chars !== 'object') { + throw new Error('Font definition must have a "chars" property as an object') + } + + // Validate that each character has the correct number of lines + for (const [char, lines] of Object.entries(font.chars)) { + if (!Array.isArray(lines)) { + throw new Error(`Character "${char}" must be an array of strings`) + } + if (lines.length !== font.lines) { + throw new Error(`Character "${char}" has ${lines.length} lines but font defines ${font.lines} lines`) + } + for (let i = 0; i < lines.length; i++) { + if (typeof lines[i] !== 'string') { + throw new Error(`Character "${char}" line ${i + 1} must be a string`) + } + } + } + + return true +} + type ParsedFontDefinition = { name: string lines: number @@ -40,11 +97,11 @@ type ParsedFontDefinition = { chars: Record } -const parsedFonts: Record = {} +const parsedFonts: Map = new Map() function parseColorTags(text: string): FontSegment[] { const segments: FontSegment[] = [] - let currentIndex = 0 + let _currentIndex = 0 const colorTagRegex = /(.*?)<\/c\d+>/g let lastIndex = 0 @@ -75,34 +132,38 @@ function parseColorTags(text: string): FontSegment[] { return segments } -function getParsedFont(fontKey: keyof typeof fonts): ParsedFontDefinition { - if (!parsedFonts[fontKey]) { - const fontDef = fonts[fontKey] as FontDefinition +function getParsedFont(fontDef: FontDefinition): ParsedFontDefinition { + // Validate font definition on first use + try { + validateFontDefinition(fontDef) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error(`Invalid font definition: ${message}`) + throw error + } + + if (!parsedFonts.has(fontDef)) { const parsedChars: Record = {} for (const [char, lines] of Object.entries(fontDef.chars)) { parsedChars[char] = lines.map((line) => parseColorTags(line)) } - parsedFonts[fontKey] = { + parsedFonts.set(fontDef, { ...fontDef, colors: fontDef.colors || 1, chars: parsedChars, - } + }) } - return parsedFonts[fontKey] + return parsedFonts.get(fontDef)! } -export function measureText({ text, font = "tiny" }: { text: string; font?: keyof typeof fonts }): { +export function measureText({ text, font = fonts.tiny }: { text: string; font?: FontDefinition }): { width: number height: number } { const fontDef = getParsedFont(font) - if (!fontDef) { - console.warn(`Font '${font}' not found`) - return { width: 0, height: 0 } - } let currentX = 0 @@ -144,11 +205,8 @@ export function measureText({ text, font = "tiny" }: { text: string; font?: keyo } } -export function getCharacterPositions(text: string, font: keyof typeof fonts = "tiny"): number[] { +export function getCharacterPositions(text: string, font: FontDefinition = fonts.tiny): number[] { const fontDef = getParsedFont(font) - if (!fontDef) { - return [0] - } const positions: number[] = [0] let currentX = 0 @@ -185,7 +243,7 @@ export function getCharacterPositions(text: string, font: keyof typeof fonts = " return positions } -export function coordinateToCharacterIndex(x: number, text: string, font: keyof typeof fonts = "tiny"): number { +export function coordinateToCharacterIndex(x: number, text: string, font: FontDefinition = fonts.tiny): number { const positions = getCharacterPositions(text, font) if (x < 0) { @@ -217,24 +275,20 @@ export function renderFontToFrameBuffer( y = 0, fg = [RGBA.fromInts(255, 255, 255, 255)], bg = RGBA.fromInts(0, 0, 0, 255), - font = "tiny", + font = fonts.tiny, }: { text: string x?: number y?: number fg?: RGBA | RGBA[] bg?: RGBA - font?: keyof typeof fonts + font?: FontDefinition }, ): { width: number; height: number } { const width = buffer.getWidth() const height = buffer.getHeight() const fontDef = getParsedFont(font) - if (!fontDef) { - console.warn(`Font '${font}' not found`) - return { width: 0, height: 0 } - } const colors = Array.isArray(fg) ? fg : [fg] diff --git a/packages/core/src/lib/selection.ts b/packages/core/src/lib/selection.ts index 48e81337a..2b8c8b177 100644 --- a/packages/core/src/lib/selection.ts +++ b/packages/core/src/lib/selection.ts @@ -1,6 +1,6 @@ import { Renderable } from ".." import type { SelectionState } from "../types" -import { coordinateToCharacterIndex, fonts } from "./ascii.font" +import { coordinateToCharacterIndex, type FontDefinition, fonts } from "./ascii.font" export class Selection { private _anchor: { x: number; y: number } @@ -233,7 +233,7 @@ export class ASCIIFontSelectionHelper { private getX: () => number, private getY: () => number, private getText: () => string, - private getFont: () => keyof typeof fonts, + private getFont: () => FontDefinition, ) {} hasSelection(): boolean { diff --git a/packages/core/src/renderables/ASCIIFont.ts b/packages/core/src/renderables/ASCIIFont.ts index a85dcaedb..44f203842 100644 --- a/packages/core/src/renderables/ASCIIFont.ts +++ b/packages/core/src/renderables/ASCIIFont.ts @@ -1,13 +1,13 @@ import type { RenderableOptions } from "../Renderable" import { ASCIIFontSelectionHelper } from "../lib/selection" -import { type fonts, measureText, renderFontToFrameBuffer, getCharacterPositions } from "../lib/ascii.font" import { RGBA, parseColor } from "../lib/RGBA" -import { FrameBufferRenderable } from "./FrameBuffer" import type { SelectionState } from "../types" +import { fonts, type FontDefinition, measureText, renderFontToFrameBuffer, getCharacterPositions, validateFontDefinition } from "../lib/ascii.font" +import { FrameBufferRenderable } from "./FrameBuffer" export interface ASCIIFontOptions extends RenderableOptions { text?: string - font?: "tiny" | "block" | "shade" | "slick" + font?: FontDefinition fg?: RGBA | RGBA[] bg?: RGBA selectionBg?: string | RGBA @@ -18,7 +18,7 @@ export interface ASCIIFontOptions extends RenderableOptions { export class ASCIIFontRenderable extends FrameBufferRenderable { public selectable: boolean = true private _text: string - private _font: keyof typeof fonts + private _font: FontDefinition private _fg: RGBA[] private _bg: RGBA private _selectionBg: RGBA | undefined @@ -27,8 +27,20 @@ export class ASCIIFontRenderable extends FrameBufferRenderable { private selectionHelper: ASCIIFontSelectionHelper constructor(id: string, options: ASCIIFontOptions) { - const font = options.font || "tiny" + const font = options.font || fonts.tiny const text = options.text || "" + + // Validate font if provided + if (options.font) { + try { + validateFontDefinition(options.font) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error(`Invalid font provided to ASCIIFontRenderable: ${message}`) + throw new Error(`Invalid font: ${message}`) + } + } + const measurements = measureText({ text: text, font }) super(id, { @@ -68,11 +80,11 @@ export class ASCIIFontRenderable extends FrameBufferRenderable { this.needsUpdate() } - get font(): keyof typeof fonts { + get font(): FontDefinition { return this._font } - set font(value: keyof typeof fonts) { + set font(value: FontDefinition) { this._font = value this.updateDimensions() this.selectionHelper.reevaluateSelection(this.width, this.height) diff --git a/packages/react/examples/ascii.tsx b/packages/react/examples/ascii.tsx index ca91e879e..7bbcf5a22 100644 --- a/packages/react/examples/ascii.tsx +++ b/packages/react/examples/ascii.tsx @@ -1,10 +1,11 @@ -import { measureText } from "@opentui/core" +import { measureText, fonts } from "@opentui/core" import { render } from "@opentui/react" import { useState } from "react" export const App = () => { const text = "ASCII" - const [font, setFont] = useState<"block" | "shade" | "slick" | "tiny">("tiny") + const [fontName, setFontName] = useState<"block" | "shade" | "slick" | "tiny">("tiny") + const font = fonts[fontName] const { width, height } = measureText({ text, @@ -21,7 +22,7 @@ export const App = () => { >