Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support ansi minify #19

Merged
merged 5 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading