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 = () => {
>