Skip to content

Commit

Permalink
support ansi minify (#19)
Browse files Browse the repository at this point in the history
* support ansi minify

* add to_ans test

* wasm: add to_ans

* wasm: add to_ans

* to_ans: add test
  • Loading branch information
ahaoboy authored Oct 27, 2024
1 parent 9d5d3fb commit bb3792b
Show file tree
Hide file tree
Showing 21 changed files with 410 additions and 39 deletions.
7 changes: 5 additions & 2 deletions ansi2-wasm/src-ts/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { to_svg, to_html, to_text, Theme } from "./wasm"
import { to_svg, to_html, to_text, Theme, to_ans } from "./wasm"
import { readFileSync, existsSync } from "node:fs"
import { optimize } from "svgo"
import { Mode } from "./wasm"
Expand All @@ -7,7 +7,7 @@ import * as t from "typanion"
import { Command, Option, Cli, Builtins } from "clipanion"

const isInteger = t.cascade(t.isNumber(), [t.isInteger()])
const isFormat = t.cascade(t.isEnum(["html", "svg", "text"]))
const isFormat = t.cascade(t.isEnum(["html", "svg", "text", "ans"]))
const isTheme = t.cascade(t.isEnum(["vscode", "ubuntu", "vga", "xterm"]))
const isLengthAdjust = t.cascade(t.isEnum(["spacing", "spacingAndGlyphs"]))
const isColor = t.cascade(
Expand Down Expand Up @@ -129,6 +129,9 @@ class AnsiCmd extends Command {
case "text": {
process.stdout.write(to_text(input, width))
}
case "ans": {
process.stdout.write(to_ans(input, width, compress))
}
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions ansi2-wasm/src-ts/wasm/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ export function to_html(s: string, theme: Theme, width?: number, font?: string,
* @returns {string}
*/
export function to_text(s: string, width?: number): string;
/**
* @param {string} s
* @param {number | undefined} [width]
* @param {boolean | undefined} [compress]
* @returns {string}
*/
export function to_ans(s: string, width?: number, compress?: boolean): string;
export enum Mode {
Dark = 0,
Light = 1,
Expand Down
5 changes: 5 additions & 0 deletions ansi2-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,8 @@ pub fn to_html(
pub fn to_text(s: String, width: Option<usize>) -> String {
ansi2::text::to_text(&s, width)
}

#[wasm_bindgen]
pub fn to_ans(s: String, width: Option<usize>, compress: Option<bool>) -> String {
ansi2::ans::to_ans(&s, width, compress.unwrap_or(false))
}
94 changes: 94 additions & 0 deletions ansi2/src/ans.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use crate::{color::AnsiColor, Canvas, Node};

pub fn to_ans<S: AsRef<str>>(str: S, width: Option<usize>, compress: bool) -> String {
let s = str.as_ref();
let canvas = Canvas::new(s, width);

let iter = if compress {
canvas.minify().into_iter()
} else {
canvas.pixels.into_iter()
};

let mut text: Vec<String> = Vec::new();

let mut last_node = Node::default();

for row in iter {
let mut row_str = Vec::new();
for c in row.iter() {
if !last_node.same_style(c) {
// FIXME: Find the minimum distance between two styles
row_str.push("\x1b[0m".to_string());
if c.bold {
row_str.push("\x1b[1m".to_string());
}
if c.italic {
row_str.push("\x1b[3m".to_string());
}
if c.dim {
row_str.push("\x1b[2m".to_string());
}
if c.underline {
row_str.push("\x1b[4m".to_string());
}
if c.hide {
row_str.push("\x1b[8m".to_string());
}
if c.blink {
row_str.push("\x1b[5m".to_string());
}

row_str.push(match c.color {
AnsiColor::Default => "".to_string(),
AnsiColor::Color8(color8) => format!("\x1b[{}m", color8.to_u8()),
AnsiColor::Color256(n) => format!("\x1b[38;5;{}m", n),
AnsiColor::Rgb(r, g, b) => format!("\x1b[38;2;{};{};{}m", r, g, b),
});

row_str.push(match c.bg_color {
AnsiColor::Default => "".to_string(),
AnsiColor::Color8(color8) => format!("\x1b[{}m", color8.to_u8() + 10),
AnsiColor::Color256(n) => format!("\x1b[48;5;{}m", n),
AnsiColor::Rgb(r, g, b) => format!("\x1b[48;2;{};{};{}m", r, g, b),
});
}
row_str.push(c.text.clone());
last_node = c.clone();
}
text.push(row_str.into_iter().collect());
}
text.join("\n")
}

#[cfg(test)]
mod test {
use std::path::Path;

use crate::{ans::to_ans, Canvas};

#[test]
fn test() {
let cargo_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let assets_dir = Path::new(&cargo_dir).parent().unwrap().join("assets");
let v = std::fs::read_dir(assets_dir).unwrap();
for i in v {
let p = i.unwrap().path().to_string_lossy().to_string();
if !p.ends_with(".ans") {
continue;
}
if p.ends_with(".min.ans") {
continue;
}
let s = std::fs::read_to_string(&p).unwrap();
let min = to_ans(&s, None, true);

let c1 = Canvas::new(&s, None);
let c2 = Canvas::new(&min, None);
assert_eq!(c1, c2);

let min2 = to_ans(&min, None, true);
assert_eq!(min2, min);
}
}
}
21 changes: 21 additions & 0 deletions ansi2/src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,27 @@ impl Color8 {
_ => Color8::Black,
}
}

pub fn to_u8(&self) -> u8 {
match self {
Color8::Black => 30,
Color8::Red => 31,
Color8::Green => 32,
Color8::Yellow => 33,
Color8::Blue => 34,
Color8::Magenta => 35,
Color8::Cyan => 36,
Color8::White => 37,
Color8::BrightBlack => 90,
Color8::BrightRed => 91,
Color8::BrightGreen => 92,
Color8::BrightYellow => 93,
Color8::BrightBlue => 94,
Color8::BrightMagenta => 95,
Color8::BrightCyan => 96,
Color8::BrightWhite => 97,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum AnsiColor {
Expand Down
10 changes: 10 additions & 0 deletions ansi2/src/lex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,12 @@ fn parse_sgr3(input: &str) -> IResult<&str, Token> {
let b = front.parse().unwrap_or(0);
let c = background.parse().unwrap_or(0);

if a == 38 && b == 5 {
return Ok((rem, Token::ColorForeground(AnsiColor::from_u8(c))));
}
if a == 48 && b == 5 {
return Ok((rem, Token::ColorBackground(AnsiColor::from_u8(c))));
}
Ok((
rem,
Token::List(vec![get_sgr(a), get_token_color(b), get_token_color(c)]),
Expand Down Expand Up @@ -414,6 +420,10 @@ fn parse_sgr5(input: &str) -> IResult<&str, Token> {
if ctrl == 38 && ty == 2 {
return Ok((rem, Token::ColorForeground(AnsiColor::Rgb(r, g, b))));
}

if ctrl == 48 && ty == 2 {
return Ok((rem, Token::ColorBackground(AnsiColor::Rgb(r, g, b))));
}
todo!()
}

Expand Down
24 changes: 19 additions & 5 deletions ansi2/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod ans;
pub mod color;
#[allow(clippy::too_many_arguments)]
pub mod css;
Expand All @@ -10,7 +11,7 @@ use color::AnsiColor;
use lex::{parse_ansi, Token};
use std::{collections::VecDeque, vec};

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Node {
pub bg_color: AnsiColor,
pub color: AnsiColor,
Expand All @@ -36,17 +37,15 @@ impl Node {
}
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Canvas {
pub pixels: Vec<Vec<Node>>,
pub w: usize,
pub h: usize,
}

fn set_node(v: &mut Vec<Vec<Node>>, node: Node, x: usize, y: usize) {
while y >= v.len() {
v.push(Vec::new());
}
ensure_height(v, y);

let row = &mut v[y];
while x >= row.len() {
Expand All @@ -67,6 +66,12 @@ fn set_node(v: &mut Vec<Vec<Node>>, node: Node, x: usize, y: usize) {
row[x] = node;
}

fn ensure_height(v: &mut Vec<Vec<Node>>, h: usize) {
while v.len() <= h {
v.push(Vec::new());
}
}

impl Canvas {
pub fn new<S: AsRef<str>>(str: S, max_width: Option<usize>) -> Self {
let s = str.as_ref();
Expand Down Expand Up @@ -114,7 +119,9 @@ impl Canvas {
Token::LineFeed => {
cur_y += 1;
cur_x = 0;
ensure_height(&mut pixels, cur_y);
}

Token::Char(c) => {
let node = Node {
text: c.into(),
Expand Down Expand Up @@ -182,6 +189,7 @@ impl Canvas {
Token::CursorUp(c) => cur_y = cur_y.saturating_sub(c as usize),
Token::CursorDown(c) => {
cur_y += c as usize;
ensure_height(&mut pixels, cur_y);
}
Token::CursorBack(c) => cur_x = cur_x.saturating_sub(c as usize),
Token::CursorForward(c) => {
Expand All @@ -190,6 +198,7 @@ impl Canvas {
cur_x %= max_width;
cur_y += 1;
}
ensure_height(&mut pixels, cur_y);
}
Token::Backspace => cur_x = cur_x.saturating_sub(1),
Token::Tab => {
Expand All @@ -204,22 +213,26 @@ impl Canvas {
cur_x %= max_width;
cur_y += 1;
}
ensure_height(&mut pixels, cur_y);
}

Token::CarriageReturn => cur_x = 0,

Token::CursorNextLine(n) => {
cur_y += n as usize;
cur_x = 0;
ensure_height(&mut pixels, cur_y);
}
Token::CursorPreviousLine(n) => {
cur_y = cur_y.saturating_sub(n as usize);
cur_x = 0;
ensure_height(&mut pixels, cur_y);
}
Token::CursorHorizontalAbsolute(n) => cur_x = (n - 1).max(0) as usize,
Token::CursorPosition(x, y) => {
cur_x = x as usize;
cur_y = y as usize;
ensure_height(&mut pixels, cur_y);
}
Token::SlowBlink | Token::RapidBlink => blink = true,
Token::Reverse => {
Expand Down Expand Up @@ -267,6 +280,7 @@ impl Canvas {
if i == '\n' {
cur_x = 0;
cur_y += 1;
ensure_height(&mut pixels, cur_y);
continue;
}

Expand Down
51 changes: 25 additions & 26 deletions ansi2/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use ansi2::ans::to_ans;
use ansi2::{css::Mode, theme::Theme};
use ansi2::{html::to_html, svg::to_svg, text::to_text};
use base64::prelude::BASE64_STANDARD;
Expand All @@ -11,6 +12,7 @@ enum Format {
Svg,
Html,
Text,
Ans,
}

#[derive(Parser, Debug, Clone)]
Expand Down Expand Up @@ -48,20 +50,26 @@ struct Args {

fn main() {
let args: Args = Args::parse();

let format = args.format.unwrap_or(Format::Svg);
let theme = args.theme.unwrap_or(Theme::Vscode);
let mode = args.mode.map(|m| match m {
Mode::Dark => ansi2::css::Mode::Dark,
Mode::Light => ansi2::css::Mode::Light,
});
let width = args.width;
let Args {
width,
format,
theme,
mode,
font,
compress,
light_bg,
dark_bg,
font_size,
length_adjust,
} = args;
let format = format.unwrap_or(Format::Svg);
let theme = theme.unwrap_or(Theme::Vscode);

let mut buf = Vec::new();
std::io::stdin()
.read_to_end(&mut buf)
.expect("can't read string from stdin");
let base64 = args.font.map(|font_url| {
let base64 = font.map(|font_url| {
if font_url.starts_with("http") {
return font_url;
}
Expand All @@ -84,12 +92,12 @@ fn main() {
width,
base64,
mode,
args.light_bg,
args.dark_bg,
args.font_size,
args.length_adjust,
light_bg,
dark_bg,
font_size,
length_adjust,
);
if args.compress {
if compress {
svg = osvg::osvg(
&svg,
Some(
Expand All @@ -112,18 +120,9 @@ fn main() {
}
svg
}
Format::Html => to_html(
&s,
theme,
width,
base64,
mode,
args.light_bg,
args.dark_bg,
args.font_size,
),
Format::Html => to_html(&s, theme, width, base64, mode, light_bg, dark_bg, font_size),
Format::Text => to_text(&s, width),
Format::Ans => to_ans(&s, width, compress),
};

println!("{}", output);
print!("{}", output);
}
Loading

0 comments on commit bb3792b

Please sign in to comment.