From 36b868e0940db55a498229652b00c0b5d00ad033 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sun, 4 Oct 2026 20:53:20 +0300 Subject: [PATCH] feat: add figlet banners and hyphenation behind features --- Cargo.lock | 46 + README.md | 19 +- bin/Cargo.toml | 4 + core/src/config.rs | 47 +- core/src/wrap.rs | 102 +- text/Cargo.toml | 14 + text/fonts/README.md | 10 + text/fonts/small.flf | 1097 +++++++++++++++++++ text/fonts/standard.flf | 2237 +++++++++++++++++++++++++++++++++++++++ text/src/figlet.rs | 431 ++++++++ text/src/hyphen.rs | 70 ++ text/src/layout.rs | 130 ++- text/src/lib.rs | 104 ++ 13 files changed, 4282 insertions(+), 29 deletions(-) create mode 100644 text/fonts/README.md create mode 100644 text/fonts/small.flf create mode 100644 text/fonts/standard.flf create mode 100644 text/src/figlet.rs create mode 100644 text/src/hyphen.rs diff --git a/Cargo.lock b/Cargo.lock index 5bf507e..23a476b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,15 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -207,6 +216,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "fst" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" + [[package]] name = "getrandom" version = "0.4.3" @@ -230,6 +245,29 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hyphenation" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf4dd4c44ae85155502a52c48739c8a48185d1449fff1963cffee63c28a50f0" +dependencies = [ + "bincode", + "fst", + "hyphenation_commons", + "pocket-resources", + "serde", +] + +[[package]] +name = "hyphenation_commons" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5febe7a2ade5c7d98eb8b75f946c046b335324b06a14ea0998271504134c05bf" +dependencies = [ + "fst", + "serde", +] + [[package]] name = "indexmap" version = "2.14.2" @@ -284,7 +322,9 @@ dependencies = [ name = "itsybitsy-text" version = "0.1.0" dependencies = [ + "hyphenation", "itsybitsy-core", + "log", ] [[package]] @@ -367,6 +407,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "pocket-resources" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c135f38778ad324d9e9ee68690bac2c1a51f340fdf96ca13e2ab3914eb2e51d8" + [[package]] name = "portable-atomic" version = "1.15.0" diff --git a/README.md b/README.md index 9f3c9af..544d341 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ It serves. HTTP, Spartan and Nex are working, with gemtext, XHTML-MP and HTML ou | gemtext, XHTML-MP and HTML output | implemented | | HTTP, Spartan and Nex listeners, virtual hosting | implemented | | Fixed-width text output (Nex, later Gopher) | implemented | +| FIGlet banners and hyphenation, behind features | implemented | | WML and XHTML-MP decks | planned | | Gemini and Gopher | planned | @@ -62,7 +63,7 @@ max_age = 300 `[defaults]` applies to every Markdown file in the directory; a `[page."name.md"]` table overrides it for one file. `title` is per-page by nature, so setting it in `[defaults]` is an error; left unset, it is derived from the page's first level-1 heading. -The fixed-width text format reads `margin_left`, `margin_right`, `paragraph_spacing`, `h1_style` through `h6_style`, `blockquote_bars`, `list_indent`, `code_block_line_numbers` and `wrap_code_blocks`. A heading style is `underline`, `underline:`, `markers` or `plain`; by default the first three levels are underlined with `=`, `-` and `~`, and deeper ones keep their `#` markers, since underline characters run out before heading levels do. Editing the file re-renders that directory's pages on the next request, and a file that fails to parse makes them error rather than silently falling back to defaults. +The fixed-width text format reads `margin_left`, `margin_right`, `paragraph_spacing`, `h1_style` through `h6_style`, `blockquote_bars`, `list_indent`, `code_block_line_numbers` and `wrap_code_blocks`. A heading style is `underline`, `underline:`, `figlet`, `figlet:`, `markers` or `plain`; by default the first three levels are underlined with `=`, `-` and `~`, and deeper ones keep their `#` markers, since underline characters run out before heading levels do. Editing the file re-renders that directory's pages on the next request, and a file that fails to parse makes them error rather than silently falling back to defaults. Because the name begins with a dot, the same rule that keeps `.secret.md` out of the URL space keeps this file out of it too. @@ -121,6 +122,22 @@ HTTP negotiates between them from `Accept`. Only a literal media type counts as A third party adds a format by writing a crate that depends on `itsybitsy-core`, adding it to the workspace and one `#[cfg]` arm in the binary's registry, then naming its id in `formats`. Every listener's formats are checked against the registry at startup, so a format switched off by a feature is a configuration error naming the missing id rather than a server error on the first request. +## Optional features + +Two decorative capabilities for the text format are off by default, because most pages leave them alone and both cost binary size: + +| Feature | Adds | Cost | +| --- | --- | --- | +| `figlet` | `h1_style = "figlet"` banners, fonts `small` and `standard` | +60 KiB | +| `hyphenation` | `hyphenate = true`, English patterns | +127 KiB | +| `hyphenation-all` | every language the pattern crate carries | +3.0 MiB | + +```bash +cargo build --release --features "figlet hyphenation" +``` + +A banner that will not fit the line, names a font this build lacks, or is asked for by a build without `figlet` falls back to the level's underline — a heading that cannot be decorated should not be lost. An unknown font name is logged, since that is almost always a typo. Likewise `hyphenate` has no effect without the feature, and an unknown `hyphen_lang` leaves the text unhyphenated; a ragged right edge is the plain-text convention anyway. + ## URLs Links should be root-relative and extensionless (`[about](/about)`, not `about.md`), so the same link resolves identically from every protocol. diff --git a/bin/Cargo.toml b/bin/Cargo.toml index d24ebc0..e9224bb 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -19,6 +19,10 @@ nex = [] gemtext = ["dep:itsybitsy-gemtext"] wap = ["dep:itsybitsy-wap"] text = ["dep:itsybitsy-text"] +# FIGlet banners and hyphenation for the text format, both off by default. +figlet = ["text", "itsybitsy-text/figlet"] +hyphenation = ["text", "itsybitsy-text/hyphenation"] +hyphenation-all = ["text", "itsybitsy-text/hyphenation-all"] [dependencies] anyhow = "1" diff --git a/core/src/config.rs b/core/src/config.rs index 14b8916..41fe91a 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -348,6 +348,12 @@ fn apply(keys: &PageKeys, settings: &mut PageSettings) -> Result<(), Error> { if let Some(value) = keys.code_block_line_numbers { settings.code_block_line_numbers = value; } + if let Some(value) = keys.hyphenate { + settings.hyphenate = value; + } + if let Some(value) = &keys.hyphen_lang { + settings.hyphen_lang = value.clone(); + } if let Some(value) = keys.wrap_code_blocks { settings.wrap_code_blocks = value; } @@ -369,8 +375,8 @@ fn apply(keys: &PageKeys, settings: &mut PageSettings) -> Result<(), Error> { settings.heading_styles[index] = HeadingStyle::parse(value, default_rule).ok_or_else(|| { Error::config(format!( - "h{}_style = '{value}' is not one of underline, underline:, markers \ - or plain", + "h{}_style = '{value}' is not one of underline, underline:, figlet, \ + figlet:, markers or plain", index + 1 )) })?; @@ -466,6 +472,11 @@ pub struct PageKeys { /// Columns a nested list is indented by. pub list_indent: Option, pub code_block_line_numbers: Option, + /// Break long words at hyphenation points when wrapping. Needs a build with + /// hyphenation support; without one the setting has no effect. + pub hyphenate: Option, + /// Language whose hyphenation patterns to use, as `en-us`. + pub hyphen_lang: Option, /// Wrap over-long code lines. Off by default, because wrapping code changes /// what it says; on, it is better than losing the end of the line. pub wrap_code_blocks: Option, @@ -480,9 +491,20 @@ pub enum HeadingStyle { Markers, /// The text alone. Plain, + /// A FIGlet banner in the named font, or the renderer's default font when + /// unnamed. Falls back to an underline when the banner would not fit, when + /// the font is unknown, or when the build has no FIGlet support, because a + /// banner that cannot be produced should not cost the heading. + Figlet(Option), } impl HeadingStyle { + /// The built-in decoration for a heading level, used when a requested banner + /// cannot be produced. + pub fn fallback(level: u8) -> HeadingStyle { + DEFAULT_HEADING_STYLES[(level.clamp(1, 6) - 1) as usize].clone() + } + /// Parse a configured value: `underline`, `underline:`, `markers` or /// `plain`. `default_rule` is the character bare `underline` uses. fn parse(value: &str, default_rule: char) -> Option { @@ -494,6 +516,10 @@ impl HeadingStyle { } "markers" => Some(HeadingStyle::Markers), "plain" => Some(HeadingStyle::Plain), + "figlet" => { + let font = argument.trim(); + Some(HeadingStyle::Figlet((!font.is_empty()).then(|| font.to_string()))) + } _ => None, } } @@ -528,6 +554,8 @@ pub struct PageSettings { pub blockquote_bars: bool, pub list_indent: u16, pub code_block_line_numbers: bool, + pub hyphenate: bool, + pub hyphen_lang: String, pub wrap_code_blocks: bool, } @@ -547,6 +575,10 @@ impl Default for PageSettings { blockquote_bars: true, list_indent: 2, code_block_line_numbers: true, + // Off by default: it needs a pattern dictionary, and ragged right + // edges are the convention in plain text anyway. + hyphenate: false, + hyphen_lang: "en-us".to_string(), wrap_code_blocks: false, } } @@ -862,6 +894,15 @@ mod dir_config_tests { assert_eq!(other.margin_left, 4); } + #[test] + fn a_figlet_style_may_name_a_font() { + let config = + load("[defaults]\nh1_style = \"figlet\"\nh2_style = \"figlet:standard\"\n").unwrap(); + let settings = config.settings_for("x.md").unwrap(); + assert_eq!(settings.heading_styles[0], HeadingStyle::Figlet(None)); + assert_eq!(settings.heading_styles[1], HeadingStyle::Figlet(Some("standard".into()))); + } + #[test] fn heading_styles_are_configurable_per_level() { let config = @@ -884,7 +925,7 @@ mod dir_config_tests { #[test] fn an_unknown_heading_style_is_an_error_naming_the_alternatives() { - let config = load("[defaults]\nh1_style = \"figlet\"\n").unwrap(); + let config = load("[defaults]\nh1_style = \"sparkles\"\n").unwrap(); let err = config.settings_for("x.md").unwrap_err(); assert!(err.to_string().contains("underline, underline:"), "{err}"); } diff --git a/core/src/wrap.rs b/core/src/wrap.rs index 96c8724..3e597da 100644 --- a/core/src/wrap.rs +++ b/core/src/wrap.rs @@ -21,12 +21,27 @@ pub fn pad(text: &str, width: usize) -> String { out } +/// Somewhere a word may be broken, with a hyphen added. +/// +/// A hook rather than an implementation, so the pattern dictionaries a real +/// hyphenator needs stay in the crate that opts into them and out of core. +pub trait Hyphenator: Send + Sync { + /// Byte offsets inside `word` where a break is allowed, ascending. An offset + /// at or past the end of the word is ignored. + fn opportunities(&self, word: &str) -> Vec; +} + /// Break `text` into lines no wider than `width` columns. +pub fn wrap(text: &str, width: usize) -> Vec { + wrap_with(text, width, None) +} + +/// Break `text` into lines, optionally hyphenating words that do not fit. /// /// Greedy: each line takes as many words as fit. A word wider than the whole /// width is split rather than left to overflow, since the formats this serves /// have no horizontal scroll. A width of zero means do not wrap. -pub fn wrap(text: &str, width: usize) -> Vec { +pub fn wrap_with(text: &str, width: usize, hyphenator: Option<&dyn Hyphenator>) -> Vec { let text = text.trim(); if width == 0 { return if text.is_empty() { Vec::new() } else { vec![text.to_string()] }; @@ -37,7 +52,15 @@ pub fn wrap(text: &str, width: usize) -> Vec { let mut line_width = 0; for word in text.split_whitespace() { - for piece in split_to_fit(word, width) { + // A word that does not fit the rest of the line may be split at a + // hyphenation point rather than pushed whole onto the next one. + let pieces = match hyphenator { + Some(hyphenator) if line_width > 0 => { + hyphenate_to_fit(word, width, width - line_width.min(width), hyphenator) + } + _ => split_to_fit(word, width), + }; + for piece in pieces { let piece_width = display_width(&piece); // The +1 is the space that would join it to what is already there. if line_width > 0 && line_width + 1 + piece_width > width { @@ -58,6 +81,35 @@ pub fn wrap(text: &str, width: usize) -> Vec { lines } +/// Split a word at its last hyphenation point that fits `room`, adding a hyphen. +/// +/// Returns the word untouched when it already fits, when no point helps, or when +/// the remainder would not fit a whole line either. +fn hyphenate_to_fit( + word: &str, + width: usize, + room: usize, + hyphenator: &dyn Hyphenator, +) -> Vec { + // Strictly less, because a space has to join it to what is already there. + if display_width(word) < room { + return vec![word.to_string()]; + } + for offset in hyphenator.opportunities(word).into_iter().rev() { + if offset == 0 || offset >= word.len() || !word.is_char_boundary(offset) { + continue; + } + let (head, tail) = word.split_at(offset); + // The head needs room for the joining space and the hyphen it gains. + if display_width(head) + 2 <= room && display_width(tail) <= width { + let mut pieces = vec![format!("{head}-")]; + pieces.extend(split_to_fit(tail, width)); + return pieces; + } + } + split_to_fit(word, width) +} + /// Split one word into chunks that each fit `width`, on column boundaries. /// /// A word that already fits comes back whole, which is the common case. @@ -149,3 +201,49 @@ mod tests { } } } + +#[cfg(test)] +mod hyphenation_tests { + use super::*; + + /// Breaks before every `-` marker position given at construction, so the + /// hook can be tested without a pattern dictionary. + struct At(Vec); + + impl Hyphenator for At { + fn opportunities(&self, _word: &str) -> Vec { + self.0.clone() + } + } + + #[test] + fn a_word_is_broken_at_a_point_that_fits() { + // "wonderful" breaks after "won", leaving "derful" for the next line. + let at = At(vec![3, 6]); + assert_eq!(wrap_with("a wonderful day", 10, Some(&at)), vec!["a wonder-", "ful day"]); + } + + #[test] + fn a_word_that_already_fits_is_left_whole() { + let at = At(vec![3]); + assert_eq!(wrap_with("a word here", 20, Some(&at)), vec!["a word here"]); + } + + #[test] + fn no_usable_point_leaves_the_word_for_the_next_line() { + // The only point is too late to help, so the word moves down intact. + let at = At(vec![8]); + assert_eq!(wrap_with("a wonderful", 6, Some(&at)), vec!["a", "wonder", "ful"]); + } + + #[test] + fn an_offset_outside_the_word_is_ignored() { + let at = At(vec![0, 99]); + assert_eq!(wrap_with("a wonderful day", 12, Some(&at)), vec!["a wonderful", "day"]); + } + + #[test] + fn without_a_hyphenator_the_result_is_unchanged() { + assert_eq!(wrap_with("a wonderful day", 10, None), wrap("a wonderful day", 10)); + } +} diff --git a/text/Cargo.toml b/text/Cargo.toml index 215720f..ade9068 100644 --- a/text/Cargo.toml +++ b/text/Cargo.toml @@ -7,7 +7,21 @@ description = "Fixed-width plain text output for itsybitsy" license = "Apache-2.0" publish = false +[features] +default = [] +# FIGlet banners for headings that ask for one. Off by default: headings are +# underlined, and the two vendored fonts are 40 KiB of the binary. +figlet = [] +# Hyphenation when wrapping, English patterns only. Off by default: it needs an +# embedded pattern dictionary for a setting most pages leave alone. +hyphenation = ["dep:hyphenation", "hyphenation/embed_en-us"] +# Every language the hyphenation crate carries, which costs a few megabytes of +# binary; `hyphenation` alone covers the default `en-us`. +hyphenation-all = ["hyphenation", "hyphenation/embed_all"] + [dependencies] +hyphenation = { version = "0.8", default-features = false, optional = true } +log = "0.4" itsybitsy-core = { path = "../core" } [lints.rust] diff --git a/text/fonts/README.md b/text/fonts/README.md new file mode 100644 index 0000000..072d0e8 --- /dev/null +++ b/text/fonts/README.md @@ -0,0 +1,10 @@ +# Vendored FIGlet fonts + +Compiled into the binary by `include_str!` when the `figlet` feature is on, rather than loaded from disk: a renderer must not open files, which is what keeps every path check in one place. + +| File | Font | Author | +| --- | --- | --- | +| `small.flf` | Small | Glenn Chappell, 4/93, based on Standard | +| `standard.flf` | Standard | Glenn Chappell & Ian Chai, 3/93, with later contributors | + +Both carry their own permission notice in the comment header the FIGfont format reserves for it, and both are distributed with FIGlet itself. The notices are preserved verbatim in the files; the parser skips over them, so they travel with the font rather than being stripped. diff --git a/text/fonts/small.flf b/text/fonts/small.flf new file mode 100644 index 0000000..d2a2c49 --- /dev/null +++ b/text/fonts/small.flf @@ -0,0 +1,1097 @@ +flf2a$ 5 4 13 15 10 0 22415 +Small by Glenn Chappell 4/93 -- based on Standard +Includes ISO Latin-1 +figlet release 2.1 -- 12 Aug 1994 +Permission is hereby given to modify this font, as long as the +modifier's name is placed on a comment line. + +Modified by Paul Burton 12/96 to include new parameter +supported by FIGlet and FIGWin. May also be slightly modified for better use +of new full-width/kern/smush alternatives, but default output is NOT changed. + + $@ + $@ + $@ + $@ + $@@ + _ @ + | |@ + |_|@ + (_)@ + @@ + _ _ @ + ( | )@ + V V @ + $ @ + @@ + _ _ @ + _| | |_ @ + |_ . _|@ + |_ _|@ + |_|_| @@ + @ + ||_@ + (_-<@ + / _/@ + || @@ + _ __ @ + (_)/ / @ + / /_ @ + /_/(_)@ + @@ + __ @ + / _|___ @ + > _|_ _|@ + \_____| @ + @@ + _ @ + ( )@ + |/ @ + $ @ + @@ + __@ + / /@ + | | @ + | | @ + \_\@@ + __ @ + \ \ @ + | |@ + | |@ + /_/ @@ + @ + _/\_@ + > <@ + \/ @ + @@ + _ @ + _| |_ @ + |_ _|@ + |_| @ + @@ + @ + @ + _ @ + ( )@ + |/ @@ + @ + ___ @ + |___|@ + $ @ + @@ + @ + @ + _ @ + (_)@ + @@ + __@ + / /@ + / / @ + /_/ @ + @@ + __ @ + / \ @ + | () |@ + \__/ @ + @@ + _ @ + / |@ + | |@ + |_|@ + @@ + ___ @ + |_ )@ + / / @ + /___|@ + @@ + ____@ + |__ /@ + |_ \@ + |___/@ + @@ + _ _ @ + | | | @ + |_ _|@ + |_| @ + @@ + ___ @ + | __|@ + |__ \@ + |___/@ + @@ + __ @ + / / @ + / _ \@ + \___/@ + @@ + ____ @ + |__ |@ + / / @ + /_/ @ + @@ + ___ @ + ( _ )@ + / _ \@ + \___/@ + @@ + ___ @ + / _ \@ + \_, /@ + /_/ @ + @@ + _ @ + (_)@ + _ @ + (_)@ + @@ + _ @ + (_)@ + _ @ + ( )@ + |/ @@ + __@ + / /@ + < < @ + \_\@ + @@ + @ + ___ @ + |___|@ + |___|@ + @@ + __ @ + \ \ @ + > >@ + /_/ @ + @@ + ___ @ + |__ \@ + /_/@ + (_) @ + @@ + ____ @ + / __ \ @ + / / _` |@ + \ \__,_|@ + \____/ @@ + _ @ + /_\ @ + / _ \ @ + /_/ \_\@ + @@ + ___ @ + | _ )@ + | _ \@ + |___/@ + @@ + ___ @ + / __|@ + | (__ @ + \___|@ + @@ + ___ @ + | \ @ + | |) |@ + |___/ @ + @@ + ___ @ + | __|@ + | _| @ + |___|@ + @@ + ___ @ + | __|@ + | _| @ + |_| @ + @@ + ___ @ + / __|@ + | (_ |@ + \___|@ + @@ + _ _ @ + | || |@ + | __ |@ + |_||_|@ + @@ + ___ @ + |_ _|@ + | | @ + |___|@ + @@ + _ @ + _ | |@ + | || |@ + \__/ @ + @@ + _ __@ + | |/ /@ + | ' < @ + |_|\_\@ + @@ + _ @ + | | @ + | |__ @ + |____|@ + @@ + __ __ @ + | \/ |@ + | |\/| |@ + |_| |_|@ + @@ + _ _ @ + | \| |@ + | .` |@ + |_|\_|@ + @@ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @@ + ___ @ + | _ \@ + | _/@ + |_| @ + @@ + ___ @ + / _ \ @ + | (_) |@ + \__\_\@ + @@ + ___ @ + | _ \@ + | /@ + |_|_\@ + @@ + ___ @ + / __|@ + \__ \@ + |___/@ + @@ + _____ @ + |_ _|@ + | | @ + |_| @ + @@ + _ _ @ + | | | |@ + | |_| |@ + \___/ @ + @@ + __ __@ + \ \ / /@ + \ V / @ + \_/ @ + @@ + __ __@ + \ \ / /@ + \ \/\/ / @ + \_/\_/ @ + @@ + __ __@ + \ \/ /@ + > < @ + /_/\_\@ + @@ + __ __@ + \ \ / /@ + \ V / @ + |_| @ + @@ + ____@ + |_ /@ + / / @ + /___|@ + @@ + __ @ + | _|@ + | | @ + | | @ + |__|@@ + __ @ + \ \ @ + \ \ @ + \_\@ + @@ + __ @ + |_ |@ + | |@ + | |@ + |__|@@ + /\ @ + |/\|@ + $ @ + $ @ + @@ + @ + @ + @ + ___ @ + |___|@@ + _ @ + ( )@ + \|@ + $ @ + @@ + @ + __ _ @ + / _` |@ + \__,_|@ + @@ + _ @ + | |__ @ + | '_ \@ + |_.__/@ + @@ + @ + __ @ + / _|@ + \__|@ + @@ + _ @ + __| |@ + / _` |@ + \__,_|@ + @@ + @ + ___ @ + / -_)@ + \___|@ + @@ + __ @ + / _|@ + | _|@ + |_| @ + @@ + @ + __ _ @ + / _` |@ + \__, |@ + |___/ @@ + _ @ + | |_ @ + | ' \ @ + |_||_|@ + @@ + _ @ + (_)@ + | |@ + |_|@ + @@ + _ @ + (_)@ + | |@ + _/ |@ + |__/ @@ + _ @ + | |__@ + | / /@ + |_\_\@ + @@ + _ @ + | |@ + | |@ + |_|@ + @@ + @ + _ __ @ + | ' \ @ + |_|_|_|@ + @@ + @ + _ _ @ + | ' \ @ + |_||_|@ + @@ + @ + ___ @ + / _ \@ + \___/@ + @@ + @ + _ __ @ + | '_ \@ + | .__/@ + |_| @@ + @ + __ _ @ + / _` |@ + \__, |@ + |_|@@ + @ + _ _ @ + | '_|@ + |_| @ + @@ + @ + ___@ + (_-<@ + /__/@ + @@ + _ @ + | |_ @ + | _|@ + \__|@ + @@ + @ + _ _ @ + | || |@ + \_,_|@ + @@ + @ + __ __@ + \ V /@ + \_/ @ + @@ + @ + __ __ __@ + \ V V /@ + \_/\_/ @ + @@ + @ + __ __@ + \ \ /@ + /_\_\@ + @@ + @ + _ _ @ + | || |@ + \_, |@ + |__/ @@ + @ + ___@ + |_ /@ + /__|@ + @@ + __@ + / /@ + _| | @ + | | @ + \_\@@ + _ @ + | |@ + | |@ + | |@ + |_|@@ + __ @ + \ \ @ + | |_@ + | | @ + /_/ @@ + /\/|@ + |/\/ @ + $ @ + $ @ + @@ + _ _ @ + (_)(_)@ + /--\ @ + /_/\_\@ + @@ + _ _ @ + (_)(_)@ + / __ \@ + \____/@ + @@ + _ _ @ + (_) (_)@ + | |_| |@ + \___/ @ + @@ + _ _ @ + (_)(_)@ + / _` |@ + \__,_|@ + @@ + _ _ @ + (_)_(_)@ + / _ \ @ + \___/ @ + @@ + _ _ @ + (_)(_)@ + | || |@ + \_,_|@ + @@ + ___ @ + / _ \@ + | |< <@ + | ||_/@ + |_| @@ +160 NO-BREAK SPACE + $@ + $@ + $@ + $@ + $@@ +161 INVERTED EXCLAMATION MARK + _ @ + (_)@ + | |@ + |_|@ + @@ +162 CENT SIGN + @ + || @ + / _)@ + \ _)@ + || @@ +163 POUND SIGN + __ @ + _/ _\ @ + |_ _|_ @ + (_,___|@ + @@ +164 CURRENCY SIGN + /\_/\@ + \ . /@ + / _ \@ + \/ \/@ + @@ +165 YEN SIGN + __ __ @ + \ V / @ + |__ __|@ + |__ __|@ + |_| @@ +166 BROKEN BAR + _ @ + | |@ + |_|@ + | |@ + |_|@@ +167 SECTION SIGN + __ @ + / _)@ + /\ \ @ + \ \/ @ + (__/ @@ +168 DIAERESIS + _ _ @ + (_)(_)@ + $ $ @ + $ $ @ + @@ +169 COPYRIGHT SIGN + ____ @ + / __ \ @ + / / _| \@ + \ \__| /@ + \____/ @@ +170 FEMININE ORDINAL INDICATOR + __ _ @ + / _` |@ + \__,_|@ + |____|@ + @@ +171 LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + ____@ + / / /@ + < < < @ + \_\_\@ + @@ +172 NOT SIGN + ____ @ + |__ |@ + |_|@ + $ @ + @@ +173 SOFT HYPHEN + @ + __ @ + |__|@ + $ @ + @@ +174 REGISTERED SIGN + ____ @ + / __ \ @ + / | -) \@ + \ ||\\ /@ + \____/ @@ +175 MACRON + ___ @ + |___|@ + $ @ + $ @ + @@ +176 DEGREE SIGN + _ @ + /.\@ + \_/@ + $ @ + @@ +177 PLUS-MINUS SIGN + _ @ + _| |_ @ + |_ _|@ + _|_|_ @ + |_____|@@ +178 SUPERSCRIPT TWO + __ @ + |_ )@ + /__|@ + $ @ + @@ +179 SUPERSCRIPT THREE + ___@ + |_ /@ + |__)@ + $ @ + @@ +180 ACUTE ACCENT + __@ + /_/@ + $ @ + $ @ + @@ +181 MICRO SIGN + @ + _ _ @ + | || |@ + | .,_|@ + |_| @@ +182 PILCROW SIGN + ____ @ + / |@ + \_ | |@ + |_|_|@ + @@ +183 MIDDLE DOT + @ + _ @ + (_)@ + $ @ + @@ +184 CEDILLA + @ + @ + @ + _ @ + )_)@@ +185 SUPERSCRIPT ONE + _ @ + / |@ + |_|@ + $ @ + @@ +186 MASCULINE ORDINAL INDICATOR + ___ @ + / _ \@ + \___/@ + |___|@ + @@ +187 RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + ____ @ + \ \ \ @ + > > >@ + /_/_/ @ + @@ +188 VULGAR FRACTION ONE QUARTER + _ __ @ + / |/ /__ @ + |_/ /_' |@ + /_/ |_|@ + @@ +189 VULGAR FRACTION ONE HALF + _ __ @ + / |/ /_ @ + |_/ /_ )@ + /_//__|@ + @@ +190 VULGAR FRACTION THREE QUARTERS + ___ __ @ + |_ // /__ @ + |__) /_' |@ + /_/ |_|@ + @@ +191 INVERTED QUESTION MARK + _ @ + (_) @ + / /_ @ + \___|@ + @@ +192 LATIN CAPITAL LETTER A WITH GRAVE + __ @ + \_\ @ + /--\ @ + /_/\_\@ + @@ +193 LATIN CAPITAL LETTER A WITH ACUTE + __ @ + /_/ @ + /--\ @ + /_/\_\@ + @@ +194 LATIN CAPITAL LETTER A WITH CIRCUMFLEX + /\ @ + |/\| @ + /--\ @ + /_/\_\@ + @@ +195 LATIN CAPITAL LETTER A WITH TILDE + /\/|@ + |/\/ @ + /--\ @ + /_/\_\@ + @@ +196 LATIN CAPITAL LETTER A WITH DIAERESIS + _ _ @ + (_)(_)@ + /--\ @ + /_/\_\@ + @@ +197 LATIN CAPITAL LETTER A WITH RING ABOVE + __ @ + (()) @ + /--\ @ + /_/\_\@ + @@ +198 LATIN CAPITAL LETTER AE + ____ @ + /, __|@ + / _ _| @ + /_/|___|@ + @@ +199 LATIN CAPITAL LETTER C WITH CEDILLA + ___ @ + / __|@ + | (__ @ + \___|@ + )_) @@ +200 LATIN CAPITAL LETTER E WITH GRAVE + __ @ + \_\@ + | -<@ + |__<@ + @@ +201 LATIN CAPITAL LETTER E WITH ACUTE + __@ + /_/@ + | -<@ + |__<@ + @@ +202 LATIN CAPITAL LETTER E WITH CIRCUMFLEX + /\ @ + |/\|@ + | -<@ + |__<@ + @@ +203 LATIN CAPITAL LETTER E WITH DIAERESIS + _ _ @ + (_)(_)@ + | -< @ + |__< @ + @@ +204 LATIN CAPITAL LETTER I WITH GRAVE + __ @ + \_\ @ + |_ _|@ + |___|@ + @@ +205 LATIN CAPITAL LETTER I WITH ACUTE + __ @ + /_/ @ + |_ _|@ + |___|@ + @@ +206 LATIN CAPITAL LETTER I WITH CIRCUMFLEX + //\ @ + |/_\|@ + |_ _|@ + |___|@ + @@ +207 LATIN CAPITAL LETTER I WITH DIAERESIS + _ _ @ + (_)_(_)@ + |_ _| @ + |___| @ + @@ +208 LATIN CAPITAL LETTER ETH + ____ @ + | __ \ @ + |_ _|) |@ + |____/ @ + @@ +209 LATIN CAPITAL LETTER N WITH TILDE + /\/|@ + |/\/ @ + | \| |@ + |_|\_|@ + @@ +210 LATIN CAPITAL LETTER O WITH GRAVE + __ @ + \_\_ @ + / __ \@ + \____/@ + @@ +211 LATIN CAPITAL LETTER O WITH ACUTE + __ @ + _/_/ @ + / __ \@ + \____/@ + @@ +212 LATIN CAPITAL LETTER O WITH CIRCUMFLEX + /\ @ + |/\| @ + / __ \@ + \____/@ + @@ +213 LATIN CAPITAL LETTER O WITH TILDE + /\/|@ + |/\/ @ + / __ \@ + \____/@ + @@ +214 LATIN CAPITAL LETTER O WITH DIAERESIS + _ _ @ + (_)(_)@ + / __ \@ + \____/@ + @@ +215 MULTIPLICATION SIGN + @ + /\/\@ + > <@ + \/\/@ + @@ +216 LATIN CAPITAL LETTER O WITH STROKE + ____ @ + / _//\ @ + | (//) |@ + \//__/ @ + @@ +217 LATIN CAPITAL LETTER U WITH GRAVE + __ @ + _\_\_ @ + | |_| |@ + \___/ @ + @@ +218 LATIN CAPITAL LETTER U WITH ACUTE + __ @ + _/_/_ @ + | |_| |@ + \___/ @ + @@ +219 LATIN CAPITAL LETTER U WITH CIRCUMFLEX + //\ @ + |/ \| @ + | |_| |@ + \___/ @ + @@ +220 LATIN CAPITAL LETTER U WITH DIAERESIS + _ _ @ + (_) (_)@ + | |_| |@ + \___/ @ + @@ +221 LATIN CAPITAL LETTER Y WITH ACUTE + __ @ + _/_/_@ + \ V /@ + |_| @ + @@ +222 LATIN CAPITAL LETTER THORN + _ @ + | |_ @ + | -_)@ + |_| @ + @@ +223 LATIN SMALL LETTER SHARP S + ___ @ + / _ \@ + | |< <@ + | ||_/@ + |_| @@ +224 LATIN SMALL LETTER A WITH GRAVE + __ @ + \_\_ @ + / _` |@ + \__,_|@ + @@ +225 LATIN SMALL LETTER A WITH ACUTE + __ @ + _/_/ @ + / _` |@ + \__,_|@ + @@ +226 LATIN SMALL LETTER A WITH CIRCUMFLEX + /\ @ + |/\| @ + / _` |@ + \__,_|@ + @@ +227 LATIN SMALL LETTER A WITH TILDE + /\/|@ + |/\/ @ + / _` |@ + \__,_|@ + @@ +228 LATIN SMALL LETTER A WITH DIAERESIS + _ _ @ + (_)(_)@ + / _` |@ + \__,_|@ + @@ +229 LATIN SMALL LETTER A WITH RING ABOVE + __ @ + (()) @ + / _` |@ + \__,_|@ + @@ +230 LATIN SMALL LETTER AE + @ + __ ___ @ + / _` -_)@ + \__,___|@ + @@ +231 LATIN SMALL LETTER C WITH CEDILLA + @ + __ @ + / _|@ + \__|@ + )_)@@ +232 LATIN SMALL LETTER E WITH GRAVE + __ @ + \_\ @ + / -_)@ + \___|@ + @@ +233 LATIN SMALL LETTER E WITH ACUTE + __ @ + /_/ @ + / -_)@ + \___|@ + @@ +234 LATIN SMALL LETTER E WITH CIRCUMFLEX + //\ @ + |/_\|@ + / -_)@ + \___|@ + @@ +235 LATIN SMALL LETTER E WITH DIAERESIS + _ _ @ + (_)_(_)@ + / -_) @ + \___| @ + @@ +236 LATIN SMALL LETTER I WITH GRAVE + __ @ + \_\@ + | |@ + |_|@ + @@ +237 LATIN SMALL LETTER I WITH ACUTE + __@ + /_/@ + | |@ + |_|@ + @@ +238 LATIN SMALL LETTER I WITH CIRCUMFLEX + //\ @ + |/_\|@ + | | @ + |_| @ + @@ +239 LATIN SMALL LETTER I WITH DIAERESIS + _ _ @ + (_)_(_)@ + | | @ + |_| @ + @@ +240 LATIN SMALL LETTER ETH + \\/\ @ + \/\\ @ + / _` |@ + \___/ @ + @@ +241 LATIN SMALL LETTER N WITH TILDE + /\/| @ + |/\/ @ + | ' \ @ + |_||_|@ + @@ +242 LATIN SMALL LETTER O WITH GRAVE + __ @ + \_\ @ + / _ \@ + \___/@ + @@ +243 LATIN SMALL LETTER O WITH ACUTE + __ @ + /_/ @ + / _ \@ + \___/@ + @@ +244 LATIN SMALL LETTER O WITH CIRCUMFLEX + //\ @ + |/_\|@ + / _ \@ + \___/@ + @@ +245 LATIN SMALL LETTER O WITH TILDE + /\/|@ + |/\/ @ + / _ \@ + \___/@ + @@ +246 LATIN SMALL LETTER O WITH DIAERESIS + _ _ @ + (_)_(_)@ + / _ \ @ + \___/ @ + @@ +247 DIVISION SIGN + _ @ + (_) @ + |___|@ + (_) @ + @@ +248 LATIN SMALL LETTER O WITH STROKE + @ + ___ @ + / //\@ + \//_/@ + @@ +249 LATIN SMALL LETTER U WITH GRAVE + __ @ + \_\_ @ + | || |@ + \_,_|@ + @@ +250 LATIN SMALL LETTER U WITH ACUTE + __ @ + _/_/ @ + | || |@ + \_,_|@ + @@ +251 LATIN SMALL LETTER U WITH CIRCUMFLEX + /\ @ + |/\| @ + | || |@ + \_,_|@ + @@ +252 LATIN SMALL LETTER U WITH DIAERESIS + _ _ @ + (_)(_)@ + | || |@ + \_,_|@ + @@ +253 LATIN SMALL LETTER Y WITH ACUTE + __ @ + _/_/ @ + | || |@ + \_, |@ + |__/ @@ +254 LATIN SMALL LETTER THORN + _ @ + | |__ @ + | '_ \@ + | .__/@ + |_| @@ +255 LATIN SMALL LETTER Y WITH DIAERESIS + _ _ @ + (_)(_)@ + | || |@ + \_, |@ + |__/ @@ diff --git a/text/fonts/standard.flf b/text/fonts/standard.flf new file mode 100644 index 0000000..1a60c59 --- /dev/null +++ b/text/fonts/standard.flf @@ -0,0 +1,2237 @@ +flf2a$ 6 5 16 15 13 0 24463 229 +Standard by Glenn Chappell & Ian Chai 3/93 -- based on Frank's .sig +Includes ISO Latin-1 +figlet release 2.1 -- 12 Aug 1994 +Modified for figlet 2.2 by John Cowan + to add Latin-{2,3,4,5} support (Unicode U+0100-017F). +Permission is hereby given to modify this font, as long as the +modifier's name is placed on a comment line. + +Modified by Paul Burton 12/96 to include new parameter +supported by FIGlet and FIGWin. May also be slightly modified for better use +of new full-width/kern/smush alternatives, but default output is NOT changed. + +Font modified May 20, 2012 by patorjk to add the 0xCA0 character + $@ + $@ + $@ + $@ + $@ + $@@ + _ @ + | |@ + | |@ + |_|@ + (_)@ + @@ + _ _ @ + ( | )@ + V V @ + $ @ + $ @ + @@ + _ _ @ + _| || |_ @ + |_ .. _|@ + |_ _|@ + |_||_| @ + @@ + _ @ + | | @ + / __)@ + \__ \@ + ( /@ + |_| @@ + _ __@ + (_)/ /@ + / / @ + / /_ @ + /_/(_)@ + @@ + ___ @ + ( _ ) @ + / _ \/\@ + | (_> <@ + \___/\/@ + @@ + _ @ + ( )@ + |/ @ + $ @ + $ @ + @@ + __@ + / /@ + | | @ + | | @ + | | @ + \_\@@ + __ @ + \ \ @ + | |@ + | |@ + | |@ + /_/ @@ + @ + __/\__@ + \ /@ + /_ _\@ + \/ @ + @@ + @ + _ @ + _| |_ @ + |_ _|@ + |_| @ + @@ + @ + @ + @ + _ @ + ( )@ + |/ @@ + @ + @ + _____ @ + |_____|@ + $ @ + @@ + @ + @ + @ + _ @ + (_)@ + @@ + __@ + / /@ + / / @ + / / @ + /_/ @ + @@ + ___ @ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @@ + _ @ + / |@ + | |@ + | |@ + |_|@ + @@ + ____ @ + |___ \ @ + __) |@ + / __/ @ + |_____|@ + @@ + _____ @ + |___ / @ + |_ \ @ + ___) |@ + |____/ @ + @@ + _ _ @ + | || | @ + | || |_ @ + |__ _|@ + |_| @ + @@ + ____ @ + | ___| @ + |___ \ @ + ___) |@ + |____/ @ + @@ + __ @ + / /_ @ + | '_ \ @ + | (_) |@ + \___/ @ + @@ + _____ @ + |___ |@ + / / @ + / / @ + /_/ @ + @@ + ___ @ + ( _ ) @ + / _ \ @ + | (_) |@ + \___/ @ + @@ + ___ @ + / _ \ @ + | (_) |@ + \__, |@ + /_/ @ + @@ + @ + _ @ + (_)@ + _ @ + (_)@ + @@ + @ + _ @ + (_)@ + _ @ + ( )@ + |/ @@ + __@ + / /@ + / / @ + \ \ @ + \_\@ + @@ + @ + _____ @ + |_____|@ + |_____|@ + $ @ + @@ + __ @ + \ \ @ + \ \@ + / /@ + /_/ @ + @@ + ___ @ + |__ \@ + / /@ + |_| @ + (_) @ + @@ + ____ @ + / __ \ @ + / / _` |@ + | | (_| |@ + \ \__,_|@ + \____/ @@ + _ @ + / \ @ + / _ \ @ + / ___ \ @ + /_/ \_\@ + @@ + ____ @ + | __ ) @ + | _ \ @ + | |_) |@ + |____/ @ + @@ + ____ @ + / ___|@ + | | @ + | |___ @ + \____|@ + @@ + ____ @ + | _ \ @ + | | | |@ + | |_| |@ + |____/ @ + @@ + _____ @ + | ____|@ + | _| @ + | |___ @ + |_____|@ + @@ + _____ @ + | ___|@ + | |_ @ + | _| @ + |_| @ + @@ + ____ @ + / ___|@ + | | _ @ + | |_| |@ + \____|@ + @@ + _ _ @ + | | | |@ + | |_| |@ + | _ |@ + |_| |_|@ + @@ + ___ @ + |_ _|@ + | | @ + | | @ + |___|@ + @@ + _ @ + | |@ + _ | |@ + | |_| |@ + \___/ @ + @@ + _ __@ + | |/ /@ + | ' / @ + | . \ @ + |_|\_\@ + @@ + _ @ + | | @ + | | @ + | |___ @ + |_____|@ + @@ + __ __ @ + | \/ |@ + | |\/| |@ + | | | |@ + |_| |_|@ + @@ + _ _ @ + | \ | |@ + | \| |@ + | |\ |@ + |_| \_|@ + @@ + ___ @ + / _ \ @ + | | | |@ + | |_| |@ + \___/ @ + @@ + ____ @ + | _ \ @ + | |_) |@ + | __/ @ + |_| @ + @@ + ___ @ + / _ \ @ + | | | |@ + | |_| |@ + \__\_\@ + @@ + ____ @ + | _ \ @ + | |_) |@ + | _ < @ + |_| \_\@ + @@ + ____ @ + / ___| @ + \___ \ @ + ___) |@ + |____/ @ + @@ + _____ @ + |_ _|@ + | | @ + | | @ + |_| @ + @@ + _ _ @ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + @@ + __ __@ + \ \ / /@ + \ \ / / @ + \ V / @ + \_/ @ + @@ + __ __@ + \ \ / /@ + \ \ /\ / / @ + \ V V / @ + \_/\_/ @ + @@ + __ __@ + \ \/ /@ + \ / @ + / \ @ + /_/\_\@ + @@ + __ __@ + \ \ / /@ + \ V / @ + | | @ + |_| @ + @@ + _____@ + |__ /@ + / / @ + / /_ @ + /____|@ + @@ + __ @ + | _|@ + | | @ + | | @ + | | @ + |__|@@ + __ @ + \ \ @ + \ \ @ + \ \ @ + \_\@ + @@ + __ @ + |_ |@ + | |@ + | |@ + | |@ + |__|@@ + /\ @ + |/\|@ + $ @ + $ @ + $ @ + @@ + @ + @ + @ + @ + _____ @ + |_____|@@ + _ @ + ( )@ + \|@ + $ @ + $ @ + @@ + @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + @@ + _ @ + | |__ @ + | '_ \ @ + | |_) |@ + |_.__/ @ + @@ + @ + ___ @ + / __|@ + | (__ @ + \___|@ + @@ + _ @ + __| |@ + / _` |@ + | (_| |@ + \__,_|@ + @@ + @ + ___ @ + / _ \@ + | __/@ + \___|@ + @@ + __ @ + / _|@ + | |_ @ + | _|@ + |_| @ + @@ + @ + __ _ @ + / _` |@ + | (_| |@ + \__, |@ + |___/ @@ + _ @ + | |__ @ + | '_ \ @ + | | | |@ + |_| |_|@ + @@ + _ @ + (_)@ + | |@ + | |@ + |_|@ + @@ + _ @ + (_)@ + | |@ + | |@ + _/ |@ + |__/ @@ + _ @ + | | __@ + | |/ /@ + | < @ + |_|\_\@ + @@ + _ @ + | |@ + | |@ + | |@ + |_|@ + @@ + @ + _ __ ___ @ + | '_ ` _ \ @ + | | | | | |@ + |_| |_| |_|@ + @@ + @ + _ __ @ + | '_ \ @ + | | | |@ + |_| |_|@ + @@ + @ + ___ @ + / _ \ @ + | (_) |@ + \___/ @ + @@ + @ + _ __ @ + | '_ \ @ + | |_) |@ + | .__/ @ + |_| @@ + @ + __ _ @ + / _` |@ + | (_| |@ + \__, |@ + |_|@@ + @ + _ __ @ + | '__|@ + | | @ + |_| @ + @@ + @ + ___ @ + / __|@ + \__ \@ + |___/@ + @@ + _ @ + | |_ @ + | __|@ + | |_ @ + \__|@ + @@ + @ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + @@ + @ + __ __@ + \ \ / /@ + \ V / @ + \_/ @ + @@ + @ + __ __@ + \ \ /\ / /@ + \ V V / @ + \_/\_/ @ + @@ + @ + __ __@ + \ \/ /@ + > < @ + /_/\_\@ + @@ + @ + _ _ @ + | | | |@ + | |_| |@ + \__, |@ + |___/ @@ + @ + ____@ + |_ /@ + / / @ + /___|@ + @@ + __@ + / /@ + | | @ + < < @ + | | @ + \_\@@ + _ @ + | |@ + | |@ + | |@ + | |@ + |_|@@ + __ @ + \ \ @ + | | @ + > >@ + | | @ + /_/ @@ + /\/|@ + |/\/ @ + $ @ + $ @ + $ @ + @@ + _ _ @ + (_)_(_)@ + /_\ @ + / _ \ @ + /_/ \_\@ + @@ + _ _ @ + (_)_(_)@ + / _ \ @ + | |_| |@ + \___/ @ + @@ + _ _ @ + (_) (_)@ + | | | |@ + | |_| |@ + \___/ @ + @@ + _ _ @ + (_)_(_)@ + / _` |@ + | (_| |@ + \__,_|@ + @@ + _ _ @ + (_)_(_)@ + / _ \ @ + | (_) |@ + \___/ @ + @@ + _ _ @ + (_) (_)@ + | | | |@ + | |_| |@ + \__,_|@ + @@ + ___ @ + / _ \@ + | |/ /@ + | |\ \@ + | ||_/@ + |_| @@ +160 NO-BREAK SPACE + $@ + $@ + $@ + $@ + $@ + $@@ +161 INVERTED EXCLAMATION MARK + _ @ + (_)@ + | |@ + | |@ + |_|@ + @@ +162 CENT SIGN + _ @ + | | @ + / __)@ + | (__ @ + \ )@ + |_| @@ +163 POUND SIGN + ___ @ + / ,_\ @ + _| |_ @ + | |___ @ + (_,____|@ + @@ +164 CURRENCY SIGN + /\___/\@ + \ _ /@ + | (_) |@ + / ___ \@ + \/ \/@ + @@ +165 YEN SIGN + __ __ @ + \ V / @ + |__ __|@ + |__ __|@ + |_| @ + @@ +166 BROKEN BAR + _ @ + | |@ + |_|@ + _ @ + | |@ + |_|@@ +167 SECTION SIGN + __ @ + _/ _)@ + / \ \ @ + \ \\ \@ + \ \_/@ + (__/ @@ +168 DIAERESIS + _ _ @ + (_) (_)@ + $ $ @ + $ $ @ + $ $ @ + @@ +169 COPYRIGHT SIGN + _____ @ + / ___ \ @ + / / __| \ @ + | | (__ |@ + \ \___| / @ + \_____/ @@ +170 FEMININE ORDINAL INDICATOR + __ _ @ + / _` |@ + \__,_|@ + |____|@ + $ @ + @@ +171 LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + ____@ + / / /@ + / / / @ + \ \ \ @ + \_\_\@ + @@ +172 NOT SIGN + @ + _____ @ + |___ |@ + |_|@ + $ @ + @@ +173 SOFT HYPHEN + @ + @ + ____ @ + |____|@ + $ @ + @@ +174 REGISTERED SIGN + _____ @ + / ___ \ @ + / | _ \ \ @ + | | / |@ + \ |_|_\ / @ + \_____/ @@ +175 MACRON + _____ @ + |_____|@ + $ @ + $ @ + $ @ + @@ +176 DEGREE SIGN + __ @ + / \ @ + | () |@ + \__/ @ + $ @ + @@ +177 PLUS-MINUS SIGN + _ @ + _| |_ @ + |_ _|@ + _|_|_ @ + |_____|@ + @@ +178 SUPERSCRIPT TWO + ___ @ + |_ )@ + / / @ + /___|@ + $ @ + @@ +179 SUPERSCRIPT THREE + ____@ + |__ /@ + |_ \@ + |___/@ + $ @ + @@ +180 ACUTE ACCENT + __@ + /_/@ + $ @ + $ @ + $ @ + @@ +181 MICRO SIGN + @ + _ _ @ + | | | |@ + | |_| |@ + | ._,_|@ + |_| @@ +182 PILCROW SIGN + _____ @ + / |@ + | (| | |@ + \__ | |@ + |_|_|@ + @@ +183 MIDDLE DOT + @ + _ @ + (_)@ + $ @ + $ @ + @@ +184 CEDILLA + @ + @ + @ + @ + _ @ + )_)@@ +185 SUPERSCRIPT ONE + _ @ + / |@ + | |@ + |_|@ + $ @ + @@ +186 MASCULINE ORDINAL INDICATOR + ___ @ + / _ \@ + \___/@ + |___|@ + $ @ + @@ +187 RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + ____ @ + \ \ \ @ + \ \ \@ + / / /@ + /_/_/ @ + @@ +188 VULGAR FRACTION ONE QUARTER + _ __ @ + / | / / _ @ + | |/ / | | @ + |_/ /|_ _|@ + /_/ |_| @ + @@ +189 VULGAR FRACTION ONE HALF + _ __ @ + / | / /__ @ + | |/ /_ )@ + |_/ / / / @ + /_/ /___|@ + @@ +190 VULGAR FRACTION THREE QUARTERS + ____ __ @ + |__ / / / _ @ + |_ \/ / | | @ + |___/ /|_ _|@ + /_/ |_| @ + @@ +191 INVERTED QUESTION MARK + _ @ + (_) @ + | | @ + / /_ @ + \___|@ + @@ +192 LATIN CAPITAL LETTER A WITH GRAVE + __ @ + \_\ @ + /_\ @ + / _ \ @ + /_/ \_\@ + @@ +193 LATIN CAPITAL LETTER A WITH ACUTE + __ @ + /_/ @ + /_\ @ + / _ \ @ + /_/ \_\@ + @@ +194 LATIN CAPITAL LETTER A WITH CIRCUMFLEX + //\ @ + |/_\| @ + /_\ @ + / _ \ @ + /_/ \_\@ + @@ +195 LATIN CAPITAL LETTER A WITH TILDE + /\/| @ + |/\/ @ + /_\ @ + / _ \ @ + /_/ \_\@ + @@ +196 LATIN CAPITAL LETTER A WITH DIAERESIS + _ _ @ + (_)_(_)@ + /_\ @ + / _ \ @ + /_/ \_\@ + @@ +197 LATIN CAPITAL LETTER A WITH RING ABOVE + _ @ + (o) @ + /_\ @ + / _ \ @ + /_/ \_\@ + @@ +198 LATIN CAPITAL LETTER AE + ______ @ + / ____|@ + / _ _| @ + / __ |___ @ + /_/ |_____|@ + @@ +199 LATIN CAPITAL LETTER C WITH CEDILLA + ____ @ + / ___|@ + | | @ + | |___ @ + \____|@ + )_) @@ +200 LATIN CAPITAL LETTER E WITH GRAVE + __ @ + _\_\_ @ + | ____|@ + | _|_ @ + |_____|@ + @@ +201 LATIN CAPITAL LETTER E WITH ACUTE + __ @ + _/_/_ @ + | ____|@ + | _|_ @ + |_____|@ + @@ +202 LATIN CAPITAL LETTER E WITH CIRCUMFLEX + //\ @ + |/_\| @ + | ____|@ + | _|_ @ + |_____|@ + @@ +203 LATIN CAPITAL LETTER E WITH DIAERESIS + _ _ @ + (_)_(_)@ + | ____|@ + | _|_ @ + |_____|@ + @@ +204 LATIN CAPITAL LETTER I WITH GRAVE + __ @ + \_\ @ + |_ _|@ + | | @ + |___|@ + @@ +205 LATIN CAPITAL LETTER I WITH ACUTE + __ @ + /_/ @ + |_ _|@ + | | @ + |___|@ + @@ +206 LATIN CAPITAL LETTER I WITH CIRCUMFLEX + //\ @ + |/_\|@ + |_ _|@ + | | @ + |___|@ + @@ +207 LATIN CAPITAL LETTER I WITH DIAERESIS + _ _ @ + (_)_(_)@ + |_ _| @ + | | @ + |___| @ + @@ +208 LATIN CAPITAL LETTER ETH + ____ @ + | _ \ @ + _| |_| |@ + |__ __| |@ + |____/ @ + @@ +209 LATIN CAPITAL LETTER N WITH TILDE + /\/|@ + |/\/ @ + | \| |@ + | .` |@ + |_|\_|@ + @@ +210 LATIN CAPITAL LETTER O WITH GRAVE + __ @ + \_\ @ + / _ \ @ + | |_| |@ + \___/ @ + @@ +211 LATIN CAPITAL LETTER O WITH ACUTE + __ @ + /_/ @ + / _ \ @ + | |_| |@ + \___/ @ + @@ +212 LATIN CAPITAL LETTER O WITH CIRCUMFLEX + //\ @ + |/_\| @ + / _ \ @ + | |_| |@ + \___/ @ + @@ +213 LATIN CAPITAL LETTER O WITH TILDE + /\/| @ + |/\/ @ + / _ \ @ + | |_| |@ + \___/ @ + @@ +214 LATIN CAPITAL LETTER O WITH DIAERESIS + _ _ @ + (_)_(_)@ + / _ \ @ + | |_| |@ + \___/ @ + @@ +215 MULTIPLICATION SIGN + @ + @ + /\/\@ + > <@ + \/\/@ + @@ +216 LATIN CAPITAL LETTER O WITH STROKE + ____ @ + / _// @ + | |// |@ + | //| |@ + //__/ @ + @@ +217 LATIN CAPITAL LETTER U WITH GRAVE + __ @ + _\_\_ @ + | | | |@ + | |_| |@ + \___/ @ + @@ +218 LATIN CAPITAL LETTER U WITH ACUTE + __ @ + _/_/_ @ + | | | |@ + | |_| |@ + \___/ @ + @@ +219 LATIN CAPITAL LETTER U WITH CIRCUMFLEX + //\ @ + |/ \| @ + | | | |@ + | |_| |@ + \___/ @ + @@ +220 LATIN CAPITAL LETTER U WITH DIAERESIS + _ _ @ + (_) (_)@ + | | | |@ + | |_| |@ + \___/ @ + @@ +221 LATIN CAPITAL LETTER Y WITH ACUTE + __ @ + __/_/__@ + \ \ / /@ + \ V / @ + |_| @ + @@ +222 LATIN CAPITAL LETTER THORN + _ @ + | |___ @ + | __ \@ + | ___/@ + |_| @ + @@ +223 LATIN SMALL LETTER SHARP S + ___ @ + / _ \@ + | |/ /@ + | |\ \@ + | ||_/@ + |_| @@ +224 LATIN SMALL LETTER A WITH GRAVE + __ @ + \_\_ @ + / _` |@ + | (_| |@ + \__,_|@ + @@ +225 LATIN SMALL LETTER A WITH ACUTE + __ @ + /_/_ @ + / _` |@ + | (_| |@ + \__,_|@ + @@ +226 LATIN SMALL LETTER A WITH CIRCUMFLEX + //\ @ + |/_\| @ + / _` |@ + | (_| |@ + \__,_|@ + @@ +227 LATIN SMALL LETTER A WITH TILDE + /\/| @ + |/\/_ @ + / _` |@ + | (_| |@ + \__,_|@ + @@ +228 LATIN SMALL LETTER A WITH DIAERESIS + _ _ @ + (_)_(_)@ + / _` |@ + | (_| |@ + \__,_|@ + @@ +229 LATIN SMALL LETTER A WITH RING ABOVE + __ @ + (()) @ + / _ '|@ + | (_| |@ + \__,_|@ + @@ +230 LATIN SMALL LETTER AE + @ + __ ____ @ + / _` _ \@ + | (_| __/@ + \__,____|@ + @@ +231 LATIN SMALL LETTER C WITH CEDILLA + @ + ___ @ + / __|@ + | (__ @ + \___|@ + )_) @@ +232 LATIN SMALL LETTER E WITH GRAVE + __ @ + \_\ @ + / _ \@ + | __/@ + \___|@ + @@ +233 LATIN SMALL LETTER E WITH ACUTE + __ @ + /_/ @ + / _ \@ + | __/@ + \___|@ + @@ +234 LATIN SMALL LETTER E WITH CIRCUMFLEX + //\ @ + |/_\|@ + / _ \@ + | __/@ + \___|@ + @@ +235 LATIN SMALL LETTER E WITH DIAERESIS + _ _ @ + (_)_(_)@ + / _ \ @ + | __/ @ + \___| @ + @@ +236 LATIN SMALL LETTER I WITH GRAVE + __ @ + \_\@ + | |@ + | |@ + |_|@ + @@ +237 LATIN SMALL LETTER I WITH ACUTE + __@ + /_/@ + | |@ + | |@ + |_|@ + @@ +238 LATIN SMALL LETTER I WITH CIRCUMFLEX + //\ @ + |/_\|@ + | | @ + | | @ + |_| @ + @@ +239 LATIN SMALL LETTER I WITH DIAERESIS + _ _ @ + (_)_(_)@ + | | @ + | | @ + |_| @ + @@ +240 LATIN SMALL LETTER ETH + /\/\ @ + > < @ + _\/\ |@ + / __` |@ + \____/ @ + @@ +241 LATIN SMALL LETTER N WITH TILDE + /\/| @ + |/\/ @ + | '_ \ @ + | | | |@ + |_| |_|@ + @@ +242 LATIN SMALL LETTER O WITH GRAVE + __ @ + \_\ @ + / _ \ @ + | (_) |@ + \___/ @ + @@ +243 LATIN SMALL LETTER O WITH ACUTE + __ @ + /_/ @ + / _ \ @ + | (_) |@ + \___/ @ + @@ +244 LATIN SMALL LETTER O WITH CIRCUMFLEX + //\ @ + |/_\| @ + / _ \ @ + | (_) |@ + \___/ @ + @@ +245 LATIN SMALL LETTER O WITH TILDE + /\/| @ + |/\/ @ + / _ \ @ + | (_) |@ + \___/ @ + @@ +246 LATIN SMALL LETTER O WITH DIAERESIS + _ _ @ + (_)_(_)@ + / _ \ @ + | (_) |@ + \___/ @ + @@ +247 DIVISION SIGN + @ + _ @ + _(_)_ @ + |_____|@ + (_) @ + @@ +248 LATIN SMALL LETTER O WITH STROKE + @ + ____ @ + / _//\ @ + | (//) |@ + \//__/ @ + @@ +249 LATIN SMALL LETTER U WITH GRAVE + __ @ + _\_\_ @ + | | | |@ + | |_| |@ + \__,_|@ + @@ +250 LATIN SMALL LETTER U WITH ACUTE + __ @ + _/_/_ @ + | | | |@ + | |_| |@ + \__,_|@ + @@ +251 LATIN SMALL LETTER U WITH CIRCUMFLEX + //\ @ + |/ \| @ + | | | |@ + | |_| |@ + \__,_|@ + @@ +252 LATIN SMALL LETTER U WITH DIAERESIS + _ _ @ + (_) (_)@ + | | | |@ + | |_| |@ + \__,_|@ + @@ +253 LATIN SMALL LETTER Y WITH ACUTE + __ @ + _/_/_ @ + | | | |@ + | |_| |@ + \__, |@ + |___/ @@ +254 LATIN SMALL LETTER THORN + _ @ + | |__ @ + | '_ \ @ + | |_) |@ + | .__/ @ + |_| @@ +255 LATIN SMALL LETTER Y WITH DIAERESIS + _ _ @ + (_) (_)@ + | | | |@ + | |_| |@ + \__, |@ + |___/ @@ +0x0100 LATIN CAPITAL LETTER A WITH MACRON + ____ @ + /___/ @ + /_\ @ + / _ \ @ + /_/ \_\@ + @@ +0x0101 LATIN SMALL LETTER A WITH MACRON + ___ @ + /_ _/@ + / _` |@ + | (_| |@ + \__,_|@ + @@ +0x0102 LATIN CAPITAL LETTER A WITH BREVE + _ _ @ + \\_// @ + /_\ @ + / _ \ @ + /_/ \_\@ + @@ +0x0103 LATIN SMALL LETTER A WITH BREVE + \_/ @ + ___ @ + / _` |@ + | (_| |@ + \__,_|@ + @@ +0x0104 LATIN CAPITAL LETTER A WITH OGONEK + @ + _ @ + /_\ @ + / _ \ @ + /_/ \_\@ + (_(@@ +0x0105 LATIN SMALL LETTER A WITH OGONEK + @ + __ _ @ + / _` |@ + | (_| |@ + \__,_|@ + (_(@@ +0x0106 LATIN CAPITAL LETTER C WITH ACUTE + __ @ + _/_/ @ + / ___|@ + | |___ @ + \____|@ + @@ +0x0107 LATIN SMALL LETTER C WITH ACUTE + __ @ + /__/@ + / __|@ + | (__ @ + \___|@ + @@ +0x0108 LATIN CAPITAL LETTER C WITH CIRCUMFLEX + /\ @ + _//\\@ + / ___|@ + | |___ @ + \____|@ + @@ +0x0109 LATIN SMALL LETTER C WITH CIRCUMFLEX + /\ @ + /_\ @ + / __|@ + | (__ @ + \___|@ + @@ +0x010A LATIN CAPITAL LETTER C WITH DOT ABOVE + [] @ + ____ @ + / ___|@ + | |___ @ + \____|@ + @@ +0x010B LATIN SMALL LETTER C WITH DOT ABOVE + [] @ + ___ @ + / __|@ + | (__ @ + \___|@ + @@ +0x010C LATIN CAPITAL LETTER C WITH CARON + \\// @ + _\/_ @ + / ___|@ + | |___ @ + \____|@ + @@ +0x010D LATIN SMALL LETTER C WITH CARON + \\//@ + _\/ @ + / __|@ + | (__ @ + \___|@ + @@ +0x010E LATIN CAPITAL LETTER D WITH CARON + \\// @ + __\/ @ + | _ \ @ + | |_| |@ + |____/ @ + @@ +0x010F LATIN SMALL LETTER D WITH CARON + \/ _ @ + __| |@ + / _` |@ + | (_| |@ + \__,_|@ + @@ +0x0110 LATIN CAPITAL LETTER D WITH STROKE + ____ @ + |_ __ \ @ + /| |/ | |@ + /|_|/_| |@ + |_____/ @ + @@ +0x0111 LATIN SMALL LETTER D WITH STROKE + ---|@ + __| |@ + / _` |@ + | (_| |@ + \__,_|@ + @@ +0x0112 LATIN CAPITAL LETTER E WITH MACRON + ____ @ + /___/ @ + | ____|@ + | _|_ @ + |_____|@ + @@ +0x0113 LATIN SMALL LETTER E WITH MACRON + ____@ + /_ _/@ + / _ \ @ + | __/ @ + \___| @ + @@ +0x0114 LATIN CAPITAL LETTER E WITH BREVE + _ _ @ + \\_// @ + | ____|@ + | _|_ @ + |_____|@ + @@ +0x0115 LATIN SMALL LETTER E WITH BREVE + \\ //@ + -- @ + / _ \ @ + | __/ @ + \___| @ + @@ +0x0116 LATIN CAPITAL LETTER E WITH DOT ABOVE + [] @ + _____ @ + | ____|@ + | _|_ @ + |_____|@ + @@ +0x0117 LATIN SMALL LETTER E WITH DOT ABOVE + [] @ + __ @ + / _ \@ + | __/@ + \___|@ + @@ +0x0118 LATIN CAPITAL LETTER E WITH OGONEK + @ + _____ @ + | ____|@ + | _|_ @ + |_____|@ + (__(@@ +0x0119 LATIN SMALL LETTER E WITH OGONEK + @ + ___ @ + / _ \@ + | __/@ + \___|@ + (_(@@ +0x011A LATIN CAPITAL LETTER E WITH CARON + \\// @ + __\/_ @ + | ____|@ + | _|_ @ + |_____|@ + @@ +0x011B LATIN SMALL LETTER E WITH CARON + \\//@ + \/ @ + / _ \@ + | __/@ + \___|@ + @@ +0x011C LATIN CAPITAL LETTER G WITH CIRCUMFLEX + _/\_ @ + / ___|@ + | | _ @ + | |_| |@ + \____|@ + @@ +0x011D LATIN SMALL LETTER G WITH CIRCUMFLEX + /\ @ + _/_ \@ + / _` |@ + | (_| |@ + \__, |@ + |___/ @@ +0x011E LATIN CAPITAL LETTER G WITH BREVE + _\/_ @ + / ___|@ + | | _ @ + | |_| |@ + \____|@ + @@ +0x011F LATIN SMALL LETTER G WITH BREVE + \___/ @ + __ _ @ + / _` |@ + | (_| |@ + \__, |@ + |___/ @@ +0x0120 LATIN CAPITAL LETTER G WITH DOT ABOVE + _[]_ @ + / ___|@ + | | _ @ + | |_| |@ + \____|@ + @@ +0x0121 LATIN SMALL LETTER G WITH DOT ABOVE + [] @ + __ _ @ + / _` |@ + | (_| |@ + \__, |@ + |___/ @@ +0x0122 LATIN CAPITAL LETTER G WITH CEDILLA + ____ @ + / ___|@ + | | _ @ + | |_| |@ + \____|@ + )__) @@ +0x0123 LATIN SMALL LETTER G WITH CEDILLA + @ + __ _ @ + / _` |@ + | (_| |@ + \__, |@ + |_))))@@ +0x0124 LATIN CAPITAL LETTER H WITH CIRCUMFLEX + _/ \_ @ + | / \ |@ + | |_| |@ + | _ |@ + |_| |_|@ + @@ +0x0125 LATIN SMALL LETTER H WITH CIRCUMFLEX + _ /\ @ + | |//\ @ + | '_ \ @ + | | | |@ + |_| |_|@ + @@ +0x0126 LATIN CAPITAL LETTER H WITH STROKE + _ _ @ + | |=| |@ + | |_| |@ + | _ |@ + |_| |_|@ + @@ +0x0127 LATIN SMALL LETTER H WITH STROKE + _ @ + |=|__ @ + | '_ \ @ + | | | |@ + |_| |_|@ + @@ +0x0128 LATIN CAPITAL LETTER I WITH TILDE + /\//@ + |_ _|@ + | | @ + | | @ + |___|@ + @@ +0x0129 LATIN SMALL LETTER I WITH TILDE + @ + /\/@ + | |@ + | |@ + |_|@ + @@ +0x012A LATIN CAPITAL LETTER I WITH MACRON + /___/@ + |_ _|@ + | | @ + | | @ + |___|@ + @@ +0x012B LATIN SMALL LETTER I WITH MACRON + ____@ + /___/@ + | | @ + | | @ + |_| @ + @@ +0x012C LATIN CAPITAL LETTER I WITH BREVE + \__/@ + |_ _|@ + | | @ + | | @ + |___|@ + @@ +0x012D LATIN SMALL LETTER I WITH BREVE + @ + \_/@ + | |@ + | |@ + |_|@ + @@ +0x012E LATIN CAPITAL LETTER I WITH OGONEK + ___ @ + |_ _|@ + | | @ + | | @ + |___|@ + (__(@@ +0x012F LATIN SMALL LETTER I WITH OGONEK + _ @ + (_) @ + | | @ + | | @ + |_|_@ + (_(@@ +0x0130 LATIN CAPITAL LETTER I WITH DOT ABOVE + _[] @ + |_ _|@ + | | @ + | | @ + |___|@ + @@ +0x0131 LATIN SMALL LETTER DOTLESS I + @ + _ @ + | |@ + | |@ + |_|@ + @@ +0x0132 LATIN CAPITAL LIGATURE IJ + ___ _ @ + |_ _|| |@ + | | | |@ + | |_| |@ + |__|__/ @ + @@ +0x0133 LATIN SMALL LIGATURE IJ + _ _ @ + (_) (_)@ + | | | |@ + | | | |@ + |_|_/ |@ + |__/ @@ +0x0134 LATIN CAPITAL LETTER J WITH CIRCUMFLEX + /\ @ + /_\|@ + _ | | @ + | |_| | @ + \___/ @ + @@ +0x0135 LATIN SMALL LETTER J WITH CIRCUMFLEX + /\@ + /_\@ + | |@ + | |@ + _/ |@ + |__/ @@ +0x0136 LATIN CAPITAL LETTER K WITH CEDILLA + _ _ @ + | |/ / @ + | ' / @ + | . \ @ + |_|\_\ @ + )__)@@ +0x0137 LATIN SMALL LETTER K WITH CEDILLA + _ @ + | | __@ + | |/ /@ + | < @ + |_|\_\@ + )_)@@ +0x0138 LATIN SMALL LETTER KRA + @ + _ __ @ + | |/ \@ + | < @ + |_|\_\@ + @@ +0x0139 LATIN CAPITAL LETTER L WITH ACUTE + _ //@ + | | // @ + | | @ + | |___ @ + |_____|@ + @@ +0x013A LATIN SMALL LETTER L WITH ACUTE + //@ + | |@ + | |@ + | |@ + |_|@ + @@ +0x013B LATIN CAPITAL LETTER L WITH CEDILLA + _ @ + | | @ + | | @ + | |___ @ + |_____|@ + )__)@@ +0x013C LATIN SMALL LETTER L WITH CEDILLA + _ @ + | | @ + | | @ + | | @ + |_| @ + )_)@@ +0x013D LATIN CAPITAL LETTER L WITH CARON + _ \\//@ + | | \/ @ + | | @ + | |___ @ + |_____|@ + @@ +0x013E LATIN SMALL LETTER L WITH CARON + _ \\//@ + | | \/ @ + | | @ + | | @ + |_| @ + @@ +0x013F LATIN CAPITAL LETTER L WITH MIDDLE DOT + _ @ + | | @ + | | [] @ + | |___ @ + |_____|@ + @@ +0x0140 LATIN SMALL LETTER L WITH MIDDLE DOT + _ @ + | | @ + | | []@ + | | @ + |_| @ + @@ +0x0141 LATIN CAPITAL LETTER L WITH STROKE + __ @ + | // @ + |//| @ + // |__ @ + |_____|@ + @@ +0x0142 LATIN SMALL LETTER L WITH STROKE + _ @ + | |@ + |//@ + //|@ + |_|@ + @@ +0x0143 LATIN CAPITAL LETTER N WITH ACUTE + _/ /_ @ + | \ | |@ + | \| |@ + | |\ |@ + |_| \_|@ + @@ +0x0144 LATIN SMALL LETTER N WITH ACUTE + _ @ + _ /_/ @ + | '_ \ @ + | | | |@ + |_| |_|@ + @@ +0x0145 LATIN CAPITAL LETTER N WITH CEDILLA + _ _ @ + | \ | |@ + | \| |@ + | |\ |@ + |_| \_|@ + )_) @@ +0x0146 LATIN SMALL LETTER N WITH CEDILLA + @ + _ __ @ + | '_ \ @ + | | | |@ + |_| |_|@ + )_) @@ +0x0147 LATIN CAPITAL LETTER N WITH CARON + _\/ _ @ + | \ | |@ + | \| |@ + | |\ |@ + |_| \_|@ + @@ +0x0148 LATIN SMALL LETTER N WITH CARON + \\// @ + _\/_ @ + | '_ \ @ + | | | |@ + |_| |_|@ + @@ +0x0149 LATIN SMALL LETTER N PRECEDED BY APOSTROPHE + @ + _ __ @ + ( )| '_\ @ + |/| | | |@ + |_| |_|@ + @@ +0x014A LATIN CAPITAL LETTER ENG + _ _ @ + | \ | |@ + | \| |@ + | |\ |@ + |_| \ |@ + )_)@@ +0x014B LATIN SMALL LETTER ENG + _ __ @ + | '_ \ @ + | | | |@ + |_| | |@ + | |@ + |__ @@ +0x014C LATIN CAPITAL LETTER O WITH MACRON + ____ @ + /_ _/ @ + / _ \ @ + | (_) |@ + \___/ @ + @@ +0x014D LATIN SMALL LETTER O WITH MACRON + ____ @ + /_ _/ @ + / _ \ @ + | (_) |@ + \___/ @ + @@ +0x014E LATIN CAPITAL LETTER O WITH BREVE + \ / @ + _-_ @ + / _ \ @ + | |_| |@ + \___/ @ + @@ +0x014F LATIN SMALL LETTER O WITH BREVE + \ / @ + _-_ @ + / _ \ @ + | |_| |@ + \___/ @ + @@ +0x0150 LATIN CAPITAL LETTER O WITH DOUBLE ACUTE + ___ @ + /_/_/@ + / _ \ @ + | |_| |@ + \___/ @ + @@ +0x0151 LATIN SMALL LETTER O WITH DOUBLE ACUTE + ___ @ + /_/_/@ + / _ \ @ + | |_| |@ + \___/ @ + @@ +0x0152 LATIN CAPITAL LIGATURE OE + ___ ___ @ + / _ \| __|@ + | | | | | @ + | |_| | |__@ + \___/|____@ + @@ +0x0153 LATIN SMALL LIGATURE OE + @ + ___ ___ @ + / _ \ / _ \@ + | (_) | __/@ + \___/ \___|@ + @@ +0x0154 LATIN CAPITAL LETTER R WITH ACUTE + _/_/ @ + | _ \ @ + | |_) |@ + | _ < @ + |_| \_\@ + @@ +0x0155 LATIN SMALL LETTER R WITH ACUTE + __@ + _ /_/@ + | '__|@ + | | @ + |_| @ + @@ +0x0156 LATIN CAPITAL LETTER R WITH CEDILLA + ____ @ + | _ \ @ + | |_) |@ + | _ < @ + |_| \_\@ + )_) @@ +0x0157 LATIN SMALL LETTER R WITH CEDILLA + @ + _ __ @ + | '__|@ + | | @ + |_| @ + )_) @@ +0x0158 LATIN CAPITAL LETTER R WITH CARON + _\_/ @ + | _ \ @ + | |_) |@ + | _ < @ + |_| \_\@ + @@ +0x0159 LATIN SMALL LETTER R WITH CARON + \\// @ + _\/_ @ + | '__|@ + | | @ + |_| @ + @@ +0x015A LATIN CAPITAL LETTER S WITH ACUTE + _/_/ @ + / ___| @ + \___ \ @ + ___) |@ + |____/ @ + @@ +0x015B LATIN SMALL LETTER S WITH ACUTE + __@ + _/_/@ + / __|@ + \__ \@ + |___/@ + @@ +0x015C LATIN CAPITAL LETTER S WITH CIRCUMFLEX + _/\_ @ + / ___| @ + \___ \ @ + ___) |@ + |____/ @ + @@ +0x015D LATIN SMALL LETTER S WITH CIRCUMFLEX + @ + /_\_@ + / __|@ + \__ \@ + |___/@ + @@ +0x015E LATIN CAPITAL LETTER S WITH CEDILLA + ____ @ + / ___| @ + \___ \ @ + ___) |@ + |____/ @ + )__)@@ +0x015F LATIN SMALL LETTER S WITH CEDILLA + @ + ___ @ + / __|@ + \__ \@ + |___/@ + )_)@@ +0x0160 LATIN CAPITAL LETTER S WITH CARON + _\_/ @ + / ___| @ + \___ \ @ + ___) |@ + |____/ @ + @@ +0x0161 LATIN SMALL LETTER S WITH CARON + \\//@ + _\/ @ + / __|@ + \__ \@ + |___/@ + @@ +0x0162 LATIN CAPITAL LETTER T WITH CEDILLA + _____ @ + |_ _|@ + | | @ + | | @ + |_| @ + )__)@@ +0x0163 LATIN SMALL LETTER T WITH CEDILLA + _ @ + | |_ @ + | __|@ + | |_ @ + \__|@ + )_)@@ +0x0164 LATIN CAPITAL LETTER T WITH CARON + _____ @ + |_ _|@ + | | @ + | | @ + |_| @ + @@ +0x0165 LATIN SMALL LETTER T WITH CARON + \/ @ + | |_ @ + | __|@ + | |_ @ + \__|@ + @@ +0x0166 LATIN CAPITAL LETTER T WITH STROKE + _____ @ + |_ _|@ + | | @ + -|-|- @ + |_| @ + @@ +0x0167 LATIN SMALL LETTER T WITH STROKE + _ @ + | |_ @ + | __|@ + |-|_ @ + \__|@ + @@ +0x0168 LATIN CAPITAL LETTER U WITH TILDE + @ + _/\/_ @ + | | | |@ + | |_| |@ + \___/ @ + @@ +0x0169 LATIN SMALL LETTER U WITH TILDE + @ + _/\/_ @ + | | | |@ + | |_| |@ + \__,_|@ + @@ +0x016A LATIN CAPITAL LETTER U WITH MACRON + ____ @ + /__ _/@ + | | | |@ + | |_| |@ + \___/ @ + @@ +0x016B LATIN SMALL LETTER U WITH MACRON + ____ @ + / _ /@ + | | | |@ + | |_| |@ + \__,_|@ + @@ +0x016C LATIN CAPITAL LETTER U WITH BREVE + @ + \_/_ @ + | | | |@ + | |_| |@ + \____|@ + @@ +0x016D LATIN SMALL LETTER U WITH BREVE + @ + \_/_ @ + | | | |@ + | |_| |@ + \__,_|@ + @@ +0x016E LATIN CAPITAL LETTER U WITH RING ABOVE + O @ + __ _ @ + | | | |@ + | |_| |@ + \___/ @ + @@ +0x016F LATIN SMALL LETTER U WITH RING ABOVE + O @ + __ __ @ + | | | |@ + | |_| |@ + \__,_|@ + @@ +0x0170 LATIN CAPITAL LETTER U WITH DOUBLE ACUTE + -- --@ + /_//_/@ + | | | |@ + | |_| |@ + \___/ @ + @@ +0x0171 LATIN SMALL LETTER U WITH DOUBLE ACUTE + ____@ + _/_/_/@ + | | | |@ + | |_| |@ + \__,_|@ + @@ +0x0172 LATIN CAPITAL LETTER U WITH OGONEK + _ _ @ + | | | |@ + | | | |@ + | |_| |@ + \___/ @ + (__(@@ +0x0173 LATIN SMALL LETTER U WITH OGONEK + @ + _ _ @ + | | | |@ + | |_| |@ + \__,_|@ + (_(@@ +0x0174 LATIN CAPITAL LETTER W WITH CIRCUMFLEX + __ /\ __@ + \ \ //\\/ /@ + \ \ /\ / / @ + \ V V / @ + \_/\_/ @ + @@ +0x0175 LATIN SMALL LETTER W WITH CIRCUMFLEX + /\ @ + __ //\\__@ + \ \ /\ / /@ + \ V V / @ + \_/\_/ @ + @@ +0x0176 LATIN CAPITAL LETTER Y WITH CIRCUMFLEX + /\ @ + __//\\ @ + \ \ / /@ + \ V / @ + |_| @ + @@ +0x0177 LATIN SMALL LETTER Y WITH CIRCUMFLEX + /\ @ + //\\ @ + | | | |@ + | |_| |@ + \__, |@ + |___/ @@ +0x0178 LATIN CAPITAL LETTER Y WITH DIAERESIS + [] []@ + __ _@ + \ \ / /@ + \ V / @ + |_| @ + @@ +0x0179 LATIN CAPITAL LETTER Z WITH ACUTE + __/_/@ + |__ /@ + / / @ + / /_ @ + /____|@ + @@ +0x017A LATIN SMALL LETTER Z WITH ACUTE + _ @ + _/_/@ + |_ /@ + / / @ + /___|@ + @@ +0x017B LATIN CAPITAL LETTER Z WITH DOT ABOVE + __[]_@ + |__ /@ + / / @ + / /_ @ + /____|@ + @@ +0x017C LATIN SMALL LETTER Z WITH DOT ABOVE + [] @ + ____@ + |_ /@ + / / @ + /___|@ + @@ +0x017D LATIN CAPITAL LETTER Z WITH CARON + _\_/_@ + |__ /@ + / / @ + / /_ @ + /____|@ + @@ +0x017E LATIN SMALL LETTER Z WITH CARON + \\//@ + _\/_@ + |_ /@ + / / @ + /___|@ + @@ +0x017F LATIN SMALL LETTER LONG S + __ @ + / _|@ + |-| | @ + |-| | @ + |_| @ + @@ +0x02C7 CARON + \\//@ + \/ @ + $@ + $@ + $@ + $@@ +0x02D8 BREVE + \\_//@ + \_/ @ + $@ + $@ + $@ + $@@ +0x02D9 DOT ABOVE + []@ + $@ + $@ + $@ + $@ + $@@ +0x02DB OGONEK + $@ + $@ + $@ + $@ + $@ + )_) @@ +0x02DD DOUBLE ACUTE ACCENT + _ _ @ + /_/_/@ + $@ + $@ + $@ + $@@ +0xCA0 KANNADA LETTER TTHA + _____)@ + /_ ___/@ + / _ \ @ + | (_) | @ + $\___/$ @ + @@ + \ No newline at end of file diff --git a/text/src/figlet.rs b/text/src/figlet.rs new file mode 100644 index 0000000..655b0ac --- /dev/null +++ b/text/src/figlet.rs @@ -0,0 +1,431 @@ +//! FIGfont v2 rendering, for headings that ask for a banner. +//! +//! Two fonts are compiled in rather than loaded from disk, because a renderer +//! must not open files: that invariant is what keeps every path check in one +//! place. External fonts would have to be read at startup instead. +//! +//! Only horizontal layout is implemented. Vertical smushing joins stacked rows +//! and nothing here stacks: a heading is one line of text. + +use std::collections::HashMap; + +/// The default font: it fits 80 columns at heading lengths a page actually uses, +/// where `standard` is half again as wide. +pub const DEFAULT_FONT: &str = "small"; + +/// `Small by Glenn Chappell 4/93`. +const SMALL: &str = include_str!("../fonts/small.flf"); +const STANDARD: &str = include_str!("../fonts/standard.flf"); + +/// Horizontal layout, from the font header's layout mask. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Layout { + /// Characters placed side by side untouched. + FullWidth, + /// Moved together until they touch, never overlapping. + Fitting, + /// Overlapped where a rule permits. The mask selects which rules apply. + Smushing(u16), +} + +pub struct Font { + height: usize, + hardblank: char, + layout: Layout, + /// Rows per character, padded to that character's own width. + glyphs: HashMap>, +} + +impl Font { + /// A compiled-in font by name. + pub fn named(name: &str) -> Option { + let source = match name { + "small" => SMALL, + "standard" => STANDARD, + _ => return None, + }; + // A vendored font that does not parse is a bug, not bad input. + Font::parse(source).ok() + } + + pub fn names() -> &'static [&'static str] { + &["small", "standard"] + } + + fn parse(source: &str) -> Result { + let mut lines = source.lines(); + let header = lines.next().ok_or("empty font")?; + let signature = header.strip_prefix("flf2a").ok_or("not a FIGfont v2 file")?; + let hardblank = signature.chars().next().ok_or("no hardblank in header")?; + + let fields: Vec<&str> = signature[hardblank.len_utf8()..].split_whitespace().collect(); + let height: usize = fields.first().ok_or("no height")?.parse().map_err(|_| "bad height")?; + let old_layout: i32 = + fields.get(3).ok_or("no layout")?.parse().map_err(|_| "bad layout")?; + let comment_lines: usize = + fields.get(4).ok_or("no comment count")?.parse().map_err(|_| "bad comment count")?; + // `full_layout` supersedes `old_layout` when the font provides it. + let full_layout = fields.get(6).and_then(|value| value.parse::().ok()); + let layout = resolve_layout(old_layout, full_layout); + + for _ in 0..comment_lines { + lines.next().ok_or("truncated comments")?; + } + + let mut glyphs = HashMap::new(); + // The required set is ASCII 32 to 126, in order and untagged. + for code in 32u8..=126 { + let rows = read_glyph(&mut lines, height).ok_or("truncated character")?; + glyphs.insert(code as char, rows); + } + // Then the German characters FIGlet always includes, then code-tagged + // ones. Both are optional here: a missing character falls back to a + // space, which is better than refusing to render the heading. + for _ in 0..7 { + if read_glyph(&mut lines, height).is_none() { + break; + } + } + while let Some(tag) = lines.next() { + let Some(code) = parse_code_tag(tag) else { break }; + let Some(rows) = read_glyph(&mut lines, height) else { break }; + if let Some(ch) = char::from_u32(code) { + glyphs.insert(ch, rows); + } + } + + Ok(Font { height, hardblank, layout, glyphs }) + } + + /// Render `text` as a banner, one string per row. + /// + /// Rows are padded to a common width, as FIGlet emits them, so a caller that + /// wants them ragged trims for itself. + pub fn render(&self, text: &str) -> Vec { + let mut rows: Vec> = vec![Vec::new(); self.height]; + for ch in text.chars() { + let glyph = self.glyph(ch); + self.append(&mut rows, &glyph); + } + + let width = rows.iter().map(Vec::len).max().unwrap_or(0); + rows.into_iter() + .map(|mut row| { + row.resize(width, ' '); + // A hardblank holds space apart during layout and is a space in + // the result. + row.into_iter().map(|c| if c == self.hardblank { ' ' } else { c }).collect() + }) + .collect() + } + + /// The glyph for `ch`, or a blank of the font's own width. + fn glyph(&self, ch: char) -> Vec> { + match self.glyphs.get(&ch) { + Some(rows) => rows.iter().map(|row| row.chars().collect()).collect(), + None => vec![vec![' ']; self.height], + } + } + + /// Place a glyph to the right of what is already rendered. + /// + /// The target column can be negative, which drops that many leading columns + /// of the glyph. That is how the very first character loses the blank margin + /// the font stores it with, and it is why FIGlet's own output for a single + /// character is narrower than that character's stored width. + fn append(&self, rows: &mut [Vec], glyph: &[Vec]) { + let overlap = match self.layout { + Layout::FullWidth => 0, + Layout::Fitting | Layout::Smushing(_) => self.overlap(rows, glyph), + } as isize; + + for (row, incoming) in rows.iter_mut().zip(glyph) { + let start = row.len() as isize - overlap; + for (offset, ch) in incoming.iter().enumerate() { + let index = start + offset as isize; + if index < 0 { + continue; + } + let index = index as usize; + match row.get(index) { + Some(existing) => row[index] = self.smush(*existing, *ch).unwrap_or(*ch), + None => row.push(*ch), + } + } + } + } + + /// How many columns the incoming glyph may move left. + /// + /// Per row: the blank gap between the two parts, plus one more column when + /// the characters that would then collide can merge. An empty row counts its + /// gap as -1, which is what makes the first character trim rather than keep + /// its margin. The smallest result across all rows wins, so one row's + /// collision cannot let another row's characters run together. + fn overlap(&self, rows: &[Vec], glyph: &[Vec]) -> usize { + let mut limit = isize::MAX; + for (row, incoming) in rows.iter().zip(glyph) { + let trailing = trailing_blanks(row); + let leading = leading_blanks(incoming); + let gap = if row.is_empty() { -1 } else { trailing as isize }; + let mut amt = leading as isize + gap; + + let left = row.len().checked_sub(trailing + 1).and_then(|index| row.get(index)); + let right = incoming.get(leading); + let mergeable = match (left, right) { + // Nothing on one side, so the columns can close. + (None, _) | (_, None) => true, + (Some(left), Some(right)) => self.smush(*left, *right).is_some(), + }; + if mergeable { + amt += 1; + } + limit = limit.min(amt); + } + // Never move further than the arriving glyph is wide. + limit.clamp(0, glyph.iter().map(Vec::len).min().unwrap_or(0) as isize) as usize + } + + /// The character two overlapping columns combine into, or `None` if they + /// cannot overlap at all. + fn smush(&self, left: char, right: char) -> Option { + // A blank never obscures anything, under every layout mode. + if left == ' ' { + return Some(right); + } + if right == ' ' { + return Some(left); + } + + let rules = match self.layout { + // Fitting moves characters until they touch but never merges them. + Layout::FullWidth | Layout::Fitting => return None, + Layout::Smushing(rules) => rules, + }; + + // Hardblanks only merge when the font asks for it (rule 6). + if left == self.hardblank || right == self.hardblank { + return (rules & 32 != 0 && left == right).then_some(self.hardblank); + } + + // With no rule selected, FIGlet smushes universally: the later character + // wins. + if rules & 63 == 0 { + return Some(right); + } + + // Rule 1: two identical characters become one. + if rules & 1 != 0 && left == right { + return Some(left); + } + // Rule 2: an underscore yields to a border character. + if rules & 2 != 0 { + const BORDERS: &str = "|/\\[]{}()<>"; + if left == '_' && BORDERS.contains(right) { + return Some(right); + } + if right == '_' && BORDERS.contains(left) { + return Some(left); + } + } + // Rule 3: the later class in this hierarchy wins. + if rules & 4 != 0 + && let (Some(a), Some(b)) = (hierarchy(left), hierarchy(right)) + && a != b + { + return Some(if a > b { left } else { right }); + } + // Rule 4: opposite brackets become a vertical bar. + if rules & 8 != 0 { + for (a, b) in [('[', ']'), ('{', '}'), ('(', ')')] { + if (left == a && right == b) || (left == b && right == a) { + return Some('|'); + } + } + } + // Rule 5: these pairs become a wider glyph. + if rules & 16 != 0 { + match (left, right) { + ('/', '\\') => return Some('|'), + ('\\', '/') => return Some('Y'), + ('>', '<') => return Some('X'), + _ => {} + } + } + None + } +} + +/// Where a character sits in rule 3's hierarchy, or `None` if it is not in it. +fn hierarchy(ch: char) -> Option { + match ch { + '|' => Some(1), + '/' | '\\' => Some(2), + '[' | ']' => Some(3), + '{' | '}' => Some(4), + '(' | ')' => Some(5), + '<' | '>' => Some(6), + _ => None, + } +} + +/// `full_layout` when the font provides it, else the older single number. +fn resolve_layout(old_layout: i32, full_layout: Option) -> Layout { + if let Some(full) = full_layout { + // Bit 8 selects smushing over fitting; bit 7 selects fitting over full + // width. The low six bits are the smushing rules. + if full & 128 != 0 { + return Layout::Smushing(full & 63); + } + if full & 64 != 0 { + return Layout::Fitting; + } + return Layout::FullWidth; + } + match old_layout { + -1 => Layout::FullWidth, + 0 => Layout::Fitting, + rules => Layout::Smushing(rules as u16 & 63), + } +} + +/// One character's rows, taking the endmark off each. +fn read_glyph<'a>(lines: &mut impl Iterator, height: usize) -> Option> { + let mut rows = Vec::with_capacity(height); + for _ in 0..height { + let line = lines.next()?; + rows.push(strip_endmark(line)); + } + Some(rows) +} + +/// Drop the endmark: the last character of the line, doubled on the last row. +fn strip_endmark(line: &str) -> String { + let Some(endmark) = line.chars().last() else { return String::new() }; + let mut trimmed = line.trim_end_matches(endmark); + // A row that is entirely endmarks is an empty row, not a missing one. + if trimmed.is_empty() && !line.is_empty() { + trimmed = ""; + } + trimmed.to_string() +} + +/// The character code a code-tagged line names, in decimal, hex or octal. +fn parse_code_tag(line: &str) -> Option { + let token = line.split_whitespace().next()?; + let (negative, digits) = match token.strip_prefix('-') { + Some(rest) => (true, rest), + None => (false, token), + }; + let value = if let Some(hex) = digits.strip_prefix("0x").or_else(|| digits.strip_prefix("0X")) { + u32::from_str_radix(hex, 16).ok()? + } else if digits.len() > 1 && digits.starts_with('0') { + u32::from_str_radix(&digits[1..], 8).ok()? + } else { + digits.parse().ok()? + }; + // A negative code is a FIGlet convention for a deleted character. + (!negative).then_some(value) +} + +fn leading_blanks(row: &[char]) -> usize { + row.iter().take_while(|c| **c == ' ').count() +} + +fn trailing_blanks(row: &[char]) -> usize { + row.iter().rev().take_while(|c| **c == ' ').count() +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Compared against pyfiglet's own output for the same font and text, which + /// is the oracle for this module. + #[track_caller] + fn assert_banner(font: &str, text: &str, expected: &[&str]) { + let font = Font::named(font).expect("vendored font parses"); + let rows = font.render(text); + assert_eq!(rows, expected, "\ngot:\n{}", rows.join("\n")); + } + + #[test] + fn both_vendored_fonts_parse() { + for name in Font::names() { + let font = Font::named(name).unwrap_or_else(|| panic!("{name} failed to parse")); + assert!(font.height >= 5, "{name} height {}", font.height); + assert!(font.glyphs.contains_key(&'A'), "{name} has no A"); + assert!(font.glyphs.contains_key(&' '), "{name} has no space"); + } + } + + #[test] + fn renders_small_the_way_figlet_does() { + assert_banner("small", "Hi", &[" _ _ _ ", "| || (_)", "| __ | |", "|_||_|_|", " "]); + } + + #[test] + fn renders_standard_the_way_figlet_does() { + assert_banner( + "standard", + "Hi", + &[" _ _ _ ", "| | | (_)", "| |_| | |", "| _ | |", "|_| |_|_|", " "], + ); + } + + #[test] + fn smushes_overlapping_characters() { + // A and V interlock; without smushing they would sit side by side. + assert_banner( + "small", + "AV", + &[" ___ __", " /_\\ \\ / /", " / _ \\ V / ", "/_/ \\_\\_/ ", " "], + ); + } + + #[test] + fn renders_a_whole_heading() { + assert_banner( + "small", + "Heading One", + &[ + " _ _ _ _ ___ ", + "| || |___ __ _ __| (_)_ _ __ _ / _ \\ _ _ ___ ", + "| __ / -_) _` / _` | | ' \\/ _` | | (_) | ' \\/ -_)", + "|_||_\\___\\__,_\\__,_|_|_||_\\__, | \\___/|_||_\\___|", + " |___/ ", + ], + ); + } + + #[test] + fn an_unknown_font_is_not_found_rather_than_a_panic() { + assert!(Font::named("nonexistent").is_none()); + } + + #[test] + fn an_unmapped_character_renders_as_a_blank() { + let font = Font::named("small").unwrap(); + // Rendering must not fail on a character the font lacks. + let rows = font.render("\u{1f600}"); + assert_eq!(rows.len(), font.height); + } + + #[test] + fn every_row_is_padded_to_one_width() { + let font = Font::named("standard").unwrap(); + let rows = font.render("Wide Heading"); + let width = rows[0].chars().count(); + assert!(rows.iter().all(|row| row.chars().count() == width), "{rows:#?}"); + } + + #[test] + fn code_tags_are_read_in_each_base() { + assert_eq!(parse_code_tag("233 LATIN SMALL LETTER E WITH ACUTE"), Some(233)); + assert_eq!(parse_code_tag("0x0100"), Some(0x100)); + assert_eq!(parse_code_tag("0252"), Some(0o252)); + // A negative code marks a deleted character. + assert_eq!(parse_code_tag("-1"), None); + assert_eq!(parse_code_tag(""), None); + } +} diff --git a/text/src/hyphen.rs b/text/src/hyphen.rs new file mode 100644 index 0000000..5b343a7 --- /dev/null +++ b/text/src/hyphen.rs @@ -0,0 +1,70 @@ +//! Hyphenation patterns, behind the `hyphenation` feature. +//! +//! Off by default: the pattern data is a sizeable blob, and a ragged right edge +//! is the convention in plain text anyway. The build embeds every language the +//! `hyphenation` crate ships, so `hyphen_lang` can name any of them. + +use hyphenation::{Hyphenator as _, Language, Load, Standard}; +use itsybitsy_core::wrap::Hyphenator; + +pub struct Patterns { + dictionary: Standard, +} + +impl Patterns { + /// Load the patterns for a language named as `en-us` or `en_US`. + /// + /// Returns `None` for a language the build does not carry, which leaves the + /// text unhyphenated rather than failing the page. + pub fn for_language(name: &str) -> Option { + let language = language_named(name)?; + Standard::from_embedded(language).ok().map(|dictionary| Patterns { dictionary }) + } +} + +impl Hyphenator for Patterns { + fn opportunities(&self, word: &str) -> Vec { + self.dictionary.hyphenate(word).breaks + } +} + +/// Match a configured language name against the crate's set, accepting either +/// separator and any case, so `en_US` and `en-us` both work. +fn language_named(name: &str) -> Option { + let wanted = name.trim().replace('_', "-").to_ascii_lowercase(); + Language::try_from_code(&wanted) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn loads_english_patterns() { + let patterns = Patterns::for_language("en-us").expect("en-us is embedded"); + // "hyphenation" has well-known break points; the exact set is the + // dictionary's business, but there must be some. + assert!(!patterns.opportunities("hyphenation").is_empty()); + } + + #[test] + fn accepts_either_separator_and_any_case() { + assert!(Patterns::for_language("en_US").is_some()); + assert!(Patterns::for_language("EN-US").is_some()); + } + + #[test] + fn an_unknown_language_is_not_found_rather_than_a_failure() { + assert!(Patterns::for_language("not-a-language").is_none()); + } + + #[test] + fn breaks_are_byte_offsets_inside_the_word() { + let patterns = Patterns::for_language("en-us").unwrap(); + let word = "hyphenation"; + for offset in patterns.opportunities(word) { + assert!(offset > 0 && offset < word.len(), "{offset} outside {word}"); + assert!(word.is_char_boundary(offset)); + } + } +} diff --git a/text/src/layout.rs b/text/src/layout.rs index 215d942..5a742a1 100644 --- a/text/src/layout.rs +++ b/text/src/layout.rs @@ -2,7 +2,7 @@ use itsybitsy_core::config::{HeadingStyle, PageSettings}; use itsybitsy_core::ir::{Align, Block, Inline}; -use itsybitsy_core::wrap::{display_width, pad, wrap}; +use itsybitsy_core::wrap::{Hyphenator, display_width, pad, wrap_with}; use crate::inline::flatten; @@ -19,6 +19,9 @@ pub struct Layout<'a> { /// Columns available for content, after both margins. content_width: usize, lines: Vec, + /// Loaded once per page rather than per line. `None` when hyphenation is off, + /// unsupported by this build, or the configured language is not carried. + hyphenator: Option>, /// Set by a container that has just emitted its own separating blank, so its /// first child does not emit a second one. suppress_gap: bool, @@ -31,7 +34,13 @@ impl<'a> Layout<'a> { let margins = settings.margin_left as usize + settings.margin_right as usize; // Never let margins consume the whole line; content needs somewhere to go. let content_width = total.saturating_sub(margins).max(8); - Layout { settings, content_width, lines: Vec::new(), suppress_gap: false } + Layout { + settings, + content_width, + lines: Vec::new(), + hyphenator: hyphenator(settings), + suppress_gap: false, + } } pub fn finish(mut self) -> Vec { @@ -55,25 +64,9 @@ impl<'a> Layout<'a> { match block { Block::Heading { level, inline } => { self.gap(prefix, place); - let text = flatten(inline); - let index = (*level).clamp(1, 6) as usize - 1; - match &self.settings.heading_styles[index] { - HeadingStyle::Underline(rule) => { - let wrapped = self.wrapped(&text, prefix, place); - let width = wrapped.iter().map(|l| display_width(l)).max().unwrap_or(0); - self.extend(wrapped, prefix, place); - self.push(&rule.to_string().repeat(width), prefix, place); - } - HeadingStyle::Markers => { - let marked = format!("{} {text}", "#".repeat(*level as usize)); - let wrapped = self.wrapped(&marked, prefix, place); - self.extend(wrapped, prefix, place); - } - HeadingStyle::Plain => { - let wrapped = self.wrapped(&text, prefix, place); - self.extend(wrapped, prefix, place); - } - } + let level = (*level).clamp(1, 6); + let style = self.settings.heading_styles[level as usize - 1].clone(); + self.heading(&style, level, &flatten(inline), prefix, place); } Block::Paragraph(inline) => { self.gap(prefix, place); @@ -135,6 +128,79 @@ impl<'a> Layout<'a> { } } + /// Decorate one heading. A banner that cannot be produced retries with the + /// level's built-in decoration rather than costing the heading. + fn heading( + &mut self, + style: &HeadingStyle, + level: u8, + text: &str, + prefix: &str, + place: Placement, + ) { + match style { + HeadingStyle::Underline(rule) => { + let wrapped = self.wrapped(text, prefix, place); + let width = wrapped.iter().map(|line| display_width(line)).max().unwrap_or(0); + self.extend(wrapped, prefix, place); + self.push(&rule.to_string().repeat(width), prefix, place); + } + HeadingStyle::Markers => { + let marked = format!("{} {text}", "#".repeat(level as usize)); + let wrapped = self.wrapped(&marked, prefix, place); + self.extend(wrapped, prefix, place); + } + HeadingStyle::Plain => { + let wrapped = self.wrapped(text, prefix, place); + self.extend(wrapped, prefix, place); + } + HeadingStyle::Figlet(font) => match self.banner(text, font.as_deref(), prefix, place) { + Some(rows) => self.extend(rows, prefix, place), + None => { + let fallback = HeadingStyle::fallback(level); + self.heading(&fallback, level, text, prefix, place); + } + }, + } + } + + /// A FIGlet banner, or `None` when this build has no FIGlet support, the font + /// is unknown, or the result would be wider than the line. + #[cfg(feature = "figlet")] + fn banner( + &self, + text: &str, + font: Option<&str>, + prefix: &str, + place: Placement, + ) -> Option> { + let name = font.unwrap_or(crate::figlet::DEFAULT_FONT); + let Some(font) = crate::figlet::Font::named(name) else { + // Named but unavailable is almost always a typo, so say so rather + // than quietly underlining the heading. + log::warn!( + "unknown figlet font '{name}'; available: {}", + crate::figlet::Font::names().join(", ") + ); + return None; + }; + let rows = font.render(text); + let room = self.room(prefix, place); + // A banner wider than the line is unreadable, so it is not used at all. + rows.iter().all(|row| display_width(row.trim_end()) <= room).then_some(rows) + } + + #[cfg(not(feature = "figlet"))] + fn banner( + &self, + _text: &str, + _font: Option<&str>, + _prefix: &str, + _place: Placement, + ) -> Option> { + None + } + /// One list item: the marker on the first line, continuations aligned under /// the text rather than under the marker. fn item(&mut self, blocks: &[Block], prefix: &str, marker: &str, place: Placement) { @@ -231,7 +297,8 @@ impl<'a> Layout<'a> { let body = if numbered { format!("{:0digits$} | {line}", index + 1) } else { line.clone() }; if self.settings.wrap_code_blocks { - for piece in wrap(&body, self.room(prefix, place)) { + // Code is never hyphenated: a hyphen would change what it says. + for piece in wrap_with(&body, self.room(prefix, place), None) { self.push(&piece, prefix, place); } } else { @@ -251,7 +318,7 @@ impl<'a> Layout<'a> { } fn wrapped(&self, text: &str, prefix: &str, place: Placement) -> Vec { - wrap(text, self.room(prefix, place)) + wrap_with(text, self.room(prefix, place), self.hyphenator.as_deref()) } fn extend(&mut self, lines: Vec, prefix: &str, place: Placement) { @@ -306,6 +373,23 @@ impl<'a> Layout<'a> { } } +/// The hyphenator for these settings, if one applies and this build has one. +#[cfg(feature = "hyphenation")] +fn hyphenator(settings: &PageSettings) -> Option> { + if !settings.hyphenate { + return None; + } + crate::hyphen::Patterns::for_language(&settings.hyphen_lang) + .map(|patterns| Box::new(patterns) as Box) +} + +/// Without the feature the setting has no effect, which leaves a ragged right +/// edge rather than failing the page. +#[cfg(not(feature = "hyphenation"))] +fn hyphenator(_settings: &PageSettings) -> Option> { + None +} + fn cell_aligned(cell: &str, width: usize, align: Option) -> String { let slack = width.saturating_sub(display_width(cell)); match align { diff --git a/text/src/lib.rs b/text/src/lib.rs index 74d81a8..44c7ed1 100644 --- a/text/src/lib.rs +++ b/text/src/lib.rs @@ -8,6 +8,10 @@ //! //! Unlike gemtext this wraps, because Nex and Gopher clients do not. +#[cfg(feature = "figlet")] +mod figlet; +#[cfg(feature = "hyphenation")] +mod hyphen; mod inline; mod layout; @@ -286,3 +290,103 @@ mod separation_tests { assert_eq!(render("- a\n\n1. b\n"), "- a\n\n1. b\n"); } } + +#[cfg(test)] +mod optional_feature_tests { + use itsybitsy_core::config::{HeadingStyle, PageSettings}; + use itsybitsy_core::parse; + + use super::*; + + fn render_with(settings: &PageSettings, width: u16, markdown: &str) -> String { + let doc = parse::markdown(markdown); + let ctx = RenderCtx { url: "/x", title: "T", settings, width: Some(width) }; + String::from_utf8(Text.render(&doc, &ctx).unwrap().body).unwrap() + } + + fn bare() -> PageSettings { + PageSettings { margin_left: 0, margin_right: 0, ..Default::default() } + } + + fn figlet(font: Option<&str>) -> PageSettings { + let mut settings = bare(); + settings.heading_styles[0] = HeadingStyle::Figlet(font.map(str::to_string)); + settings + } + + #[test] + #[cfg(feature = "figlet")] + fn a_figlet_heading_is_bannered_when_it_fits() { + let out = render_with(&figlet(None), 40, "# Hi\n"); + assert_eq!(out, " _ _ _\n| || (_)\n| __ | |\n|_||_|_|\n"); + } + + #[test] + #[cfg(feature = "figlet")] + fn a_named_font_is_used() { + let out = render_with(&figlet(Some("standard")), 40, "# Hi\n"); + assert!(out.starts_with(" _ _ _\n| | | (_)"), "{out:?}"); + } + + #[test] + #[cfg(feature = "figlet")] + fn a_banner_too_wide_for_the_line_falls_back_to_an_underline() { + // A banner that does not fit is unreadable, so the heading is underlined + // instead rather than wrapped into nonsense. + let out = render_with(&figlet(None), 20, "# A Very Long Heading\n"); + assert_eq!(out, "A Very Long Heading\n===================\n"); + } + + #[test] + #[cfg(feature = "figlet")] + fn an_unknown_font_falls_back_rather_than_failing_the_page() { + let out = render_with(&figlet(Some("nonexistent")), 40, "# Hi\n"); + assert_eq!(out, "Hi\n==\n"); + } + + #[test] + #[cfg(not(feature = "figlet"))] + fn without_the_feature_a_figlet_heading_is_underlined() { + let out = render_with(&figlet(None), 40, "# Hi\n"); + assert_eq!(out, "Hi\n==\n"); + } + + #[test] + #[cfg(feature = "hyphenation")] + fn hyphenation_breaks_a_word_that_would_not_fit() { + let settings = PageSettings { hyphenate: true, ..bare() }; + let out = render_with(&settings, 8, "a wonderful time\n"); + assert!(out.lines().next().unwrap().ends_with('-'), "{out:?}"); + // The word survives the break; only a hyphen is added. + assert!(out.replace("-\n", "").contains("wonderful"), "{out:?}"); + } + + #[test] + #[cfg(feature = "hyphenation")] + fn an_unknown_language_leaves_the_text_unhyphenated() { + let settings = PageSettings { hyphenate: true, hyphen_lang: "xx-yy".into(), ..bare() }; + let plain = PageSettings { hyphenate: false, ..bare() }; + let source = "a wonderful time\n"; + assert_eq!(render_with(&settings, 8, source), render_with(&plain, 8, source)); + } + + #[test] + fn hyphenation_off_leaves_a_ragged_edge() { + let settings = PageSettings { hyphenate: false, ..bare() }; + assert!(!render_with(&settings, 8, "a wonderful time\n").contains('-')); + } + + #[test] + fn code_is_never_hyphenated_even_when_wrapping_is_asked_for() { + // A hyphen inserted into code would change what it says. + let settings = PageSettings { + hyphenate: true, + wrap_code_blocks: true, + code_block_line_numbers: false, + ..bare() + }; + let long = "identifier_that_is_quite_long_indeed"; + let out = render_with(&settings, 20, &format!("```\n{long}\n```\n")); + assert!(!out.contains("- "), "{out:?}"); + } +}