diff --git a/.changeset/curvy-files-impress.md b/.changeset/curvy-files-impress.md new file mode 100644 index 0000000..f3dc0a5 --- /dev/null +++ b/.changeset/curvy-files-impress.md @@ -0,0 +1,5 @@ +--- +'@opentf/cli-styles': minor +--- + +Fixed dim issues in popular terminals & added full support for underline with colors. diff --git a/README.md b/README.md index 0822d8d..d7a6809 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,17 @@ > Style your CLI text using [ANSI](https://en.wikipedia.org/wiki/ANSI_escape_code) escape sequences. -## Try it online at [https://node-repl.pages.dev](https://node-repl.pages.dev/) +
+ +**Try it online at [https://node-repl.pages.dev](https://node-repl.pages.dev/)** + +
+ +--- -### [@opentf/std](https://js-std.pages.dev/) - An Extensive JavaScript Standard Library, please review and give feedback. +🚀 [@opentf/std](https://js-std.pages.dev/) - An Extensive JavaScript Standard Library. Please review and give feedback. + +--- ## Features @@ -74,6 +82,10 @@ yarn add @opentf/cli-styles pnpm add @opentf/cli-styles ``` +```shell +bun add @opentf/cli-styles +``` + ## Syntax ```ts @@ -85,7 +97,8 @@ style(str: string, options?: { color: boolean }): string; ```ts import { style } from '@opentf/cli-styles'; -style('$key[.key...]{Text}'); +const out = style('$key[.key...]{Text}'); +console.log(out); ``` ## Examples @@ -157,10 +170,24 @@ style( Underlined texts ```ts -style('Highlighted fruits: $und{Apple}, cat, $und{Banana}'); +style('$und{Straight underline}'); +style('$und{Straight $nou{(No underline here)} underline}'); +style('$dbu{Double underline}'); +style('$cru{Curly underline}'); +style('$dou{Dotted underline}'); +style('$dau{Dashed underline}'); +style('$und.ug{Straight underline colored}'); +style('$dbu.uy{Double underline colored}'); +style('$cru.ur{Curly underline colored}'); +style('$dou.ub{Dotted underline colored}'); +style('$dau.uo{Dashed underline colored}'); +style('$und.urgb(100,200,255){Straight underline RGB colored}'); +style('$und.ub{Straight $ruc{underline reset} colored}'); ``` -![](assets/underline.png) +![Underline Demo](assets/Underline-Demo.png) + +\*Output from VS Code. Strikethrough text @@ -224,7 +251,7 @@ DISK: $${getColor(disk)}{${disk}%} Escape characters: -Use double back slashes to escape a character in a string. +Use double back slashes to escape a character in the string. ```ts style( @@ -244,46 +271,60 @@ style('$g.bol{SALE! -} $blk.r.bol{50% OFFER}'); ## Color Keys -| Key | Description | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| r | Red - rgb(255,65,54) | -| g | Green - rgb(46,204,64) | -| b | Blue - rgb(0,116,217) | -| o | Orange - rgb(255,133,27) | -| y | Yellow - rgb(255,220,0) | -| w | White - rgb(255,255,255) | -| m | Magenta - rgb(255,105,193) | -| bl | Black - rgb(17,17,17) | -| gr | Grey - rgb(170,170,170) | -| navy | Navy - rgb(0,31,63) | -| aqua | Aqua - rgb(127,219,255) | -| teal | Teal - rgb(57,204,204) | -| purple | Purple - rgb(177,13,201) | -| fuchsia | Fuchsia - rgb(240,18,190) | -| maroon | Maroon - rgb(133,20,75) | -| olive | Olive - rgb(61,153,112) | -| lime | Lime - rgb(1,255,112) | -| silver | Silver - rgb(221,221,221) | -| rgb(red, green, blue) | The RGB colors, Eg: rgb(255,0,0) for red color | -| hex(#------) | The 6-digit Hex colors, Eg: hex(#00ff00) for green color | -| bg\* | The background colors can be applied with prefix `bg` to any color keys.
Eg:
bgw for white bg
bgrgb(0,0,0) for black bg
bghex(#0000FF) for blue bg | +| Key | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| r | Red - rgb(255,65,54) | +| g | Green - rgb(46,204,64) | +| b | Blue - rgb(0,116,217) | +| o | Orange - rgb(255,133,27) | +| y | Yellow - rgb(255,220,0) | +| w | White - rgb(255,255,255) | +| m | Magenta - rgb(255,105,193) | +| c | Cyan - rgb(154, 236, 254) | +| n | Navy - rgb(0,31,63) | +| a | Aqua - rgb(127,219,255) | +| t | Teal - rgb(57,204,204) | +| p | Purple - rgb(177,13,201) | +| f | Fuchsia - rgb(240,18,190) | +| s | Silver - rgb(221,221,221) | +| ma | Maroon - rgb(133,20,75) | +| ol | Olive - rgb(61,153,112) | +| li | Lime - rgb(1,255,112) | +| bl | Black - rgb(17,17,17) | +| gr | Grey - rgb(170,170,170) | +| pi | Pink - rgb(255, 191, 203) | +| rgb(red, green, blue) | The RGB foreground colors, Eg: rgb(255,0,0) for red color. | +| urgb(red, green, blue) | The RGB underlined colors, Eg: urgb(255, 255, 0) for yellow underlined color. | +| hex(#------) | The 6-digit Hex colors, Eg: hex(#00ff00) for green color. | +| bg\* | The background colors can be applied with prefix `bg` to any color keys.
Eg:
bgw for white bg
bgrgb(0,0,0) for black bg
bghex(#0000FF) for blue bg | +| u\* | The underline colors. | ## Modifier Keys -| Key | Description | -| --- | ------------------------------------- | -| res | Reset all attributes to normal | -| nor | Normal intensity Neither bold nor dim | -| bol | Bold or increased intensity text | -| dim | Dimmed or decreased intensity text | -| ita | Italic text | -| und | Underlined text | -| inv | Swap foreground and background colors | -| str | Strikethrough text | -| hid | Hidden text | -| dun | Double underlined text | -| ovl | Overlined text | -| blk | Blinking text | +| Key | Description | +| --- | -------------------------------------- | +| res | Reset all attributes to normal. | +| nor | Normal intensity Neither bold nor dim. | +| bol | Bold or increased intensity text. | +| dim | Dimmed or decreased intensity text. | +| ita | Italic text. | +| inv | Swap foreground and background colors. | +| noi | Not inversed. | +| str | Strikethrough text. | +| nos | Not Strikethrough text. | +| hid | Hidden text. | +| vis | Visible text. | +| und | Straight underline text. | +| nou | No Underlined text. | +| dbu | Doubly Underlined text. | +| cru | Curly Underlined text. | +| dou | Dotted Underlined text. | +| dau | Dashed Underlined text. | +| ruc | Reset Underlined text color. | +| ovl | Overlined text. | +| noo | Not Overlined text. | +| blk | Blinking text. | +| nob | No Blinking text. | ## Color Overrides @@ -299,20 +340,21 @@ You can force enable colors/styles by setting `FORCE_COLOR=3` in enviroment vari ## Modifier Keys Supported Terminals -| Key | Supported Terminals | -| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| res | ✅ Gnome Terminal
✅ Konsole
✅ xfce4-terminal
✅ MATE Terminal
✅ iTerm2
✅ VS Code
✅ Tabby
✅ Hyper
✅ Alacritty
✅ Contour
✅ kitty
✅ WezTerm
✅ Windows Terminal | -| nor | ✅ Gnome Terminal
✅ Konsole
✅ xfce4-terminal
✅ MATE Terminal
✅ VS Code
✅ Tabby
✅ Hyper
✅ Contour
✅ kitty
✅ WezTerm
✅ Windows Terminal
✅ iTerm2
✅ Alacritty | -| bol | ✅ Gnome Terminal
✅ Konsole
✅ xfce4-terminal
✅ MATE Terminal
✅ VS Code
✅ Tabby
✅ Hyper
✅ Contour
✅ kitty
✅ WezTerm
✅ Windows Terminal
✅ iTerm2
✅ Alacritty | -| dim | ✅ Gnome Terminal
✅ Konsole
✅ xfce4-terminal
✅ MATE Terminal
✅ VS Code
✅ Tabby
✅ Hyper
✅ Contour
✅ kitty
✅ WezTerm
✅ Windows Terminal
✅ iTerm2
✅ Alacritty | -| ita | ✅ Gnome Terminal
✅ Konsole
✅ xfce4-terminal
✅ MATE Terminal
✅ VS Code
✅ Tabby
✅ Hyper
✅ Contour
❌ kitty
✅ WezTerm
✅ Windows Terminal
✅ iTerm2
✅ Alacritty | -| und | ✅ Gnome Terminal
✅ Konsole
✅ xfce4-terminal
✅ MATE Terminal
✅ VS Code
✅ Tabby
✅ Hyper
✅ Contour
✅ kitty
✅ WezTerm
✅ Windows Terminal
✅ iTerm2
✅ Alacritty | -| inv | ✅ Gnome Terminal
✅ Konsole
✅ xfce4-terminal
✅ MATE Terminal
✅ VS Code
✅ Tabby
✅ Hyper
✅ Contour
✅ kitty
✅ WezTerm
✅ Windows Terminal
✅ iTerm2
✅ Alacritty | -| str | ✅ Gnome Terminal
✅ Konsole
✅ xfce4-terminal
✅ MATE Terminal
✅ VS Code
✅ Tabby
✅ Hyper
✅ Contour
✅ kitty
✅ WezTerm
✅ Windows Terminal
✅ iTerm2
✅ Alacritty | -| hid | ✅ Gnome Terminal
✅ Konsole
✅ xfce4-terminal
✅ MATE Terminal
✅ VS Code
✅ Tabby
✅ Hyper
✅ Contour
❌ kitty
✅ WezTerm
✅ Windows Terminal
❌ iTerm2
✅ Alacritty | -| dun | ✅ Gnome Terminal
❌ Konsole
✅ xfce4-terminal
✅ MATE Terminal
✅ VS Code
✅ Tabby
❌ Hyper
✅ Contour
✅ kitty
✅ WezTerm
✅ Windows Terminal
❌ iTerm2
❌ Alacritty | -| ovl | ✅ Gnome Terminal
✅ Konsole
✅ xfce4-terminal
✅ MATE Terminal
✅ VS Code
❌ Tabby
❌ Hyper
✅ Contour
❌ kitty
✅ WezTerm
✅ Windows Terminal
❌ iTerm2
❌ Alacritty | -| blk | ✅ Gnome Terminal
✅ Konsole
✅ xfce4-terminal
✅ MATE Terminal
❌ VS Code
❌ Tabby
❌ Hyper
✅ Contour
❌ kitty
✅ WezTerm
✅ Windows Terminal
❌ iTerm2
❌ Alacritty | +| Supported Terminals | res | nor | bol | dim | ita | inv | str | hid | ovl | blk | und | dbu | +| ------------------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Gnome Terminal | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Konsole | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | +| xfce4-terminal | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| MATE Terminal | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| iTerm2 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | +| VS Code | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | +| Tabby | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | +| Hyper | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | +| Alacritty | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | +| Contour | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| kitty | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | +| WezTerm | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Windows Terminal | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ## References @@ -332,7 +374,7 @@ https://developer.chrome.com/docs/devtools/console/format-style/ - [@opentf/std](https://www.npmjs.com/package/@opentf/std) - A collection of JavaScript utility functions. -- [@opentf/cli-pbar](https://www.npmjs.com/package/@opentf/cli-pbar) - CLI progress bar. +- [@opentf/cli-pbar](https://www.npmjs.com/package/@opentf/cli-pbar) - The CLI progress bar. ## License diff --git a/__tests__/cases.spec.js b/__tests__/cases.spec.js index 9fcd705..4d76990 100644 --- a/__tests__/cases.spec.js +++ b/__tests__/cases.spec.js @@ -34,8 +34,4 @@ describe('cases', () => { it('renders $ signs inbetween styles', () => { expect(style('$$r{\\$}$')).toEqual('$\x1B[38;2;255;65;54m$\x1B[0m$'); }); - - it('renders dimmed green unicode symbol', () => { - expect(style('$g.dim{➜}')).toEqual('\x1B[38;2;46;204;64m\x1B[2m➜\x1B[0m'); - }); }); diff --git a/__tests__/colors.spec.js b/__tests__/colors.spec.js index a1eacf3..1e44215 100644 --- a/__tests__/colors.spec.js +++ b/__tests__/colors.spec.js @@ -27,6 +27,14 @@ describe('colors', () => { ); }); + it('renders pink fg color', () => { + expect(style('$pi{Pink}')).toEqual('\x1B[38;2;255;191;203mPink\x1B[0m'); + }); + + it('renders cyan fg color', () => { + expect(style('$c{Cyan}')).toEqual('\x1B[38;2;154;236;254mCyan\x1B[0m'); + }); + it('renders orange fg color', () => { expect(style('$o{ORANGE}')).toEqual('\x1B[38;2;255;133;27mORANGE\x1B[0m'); }); @@ -48,47 +56,39 @@ describe('colors', () => { }); it('renders navy fg color', () => { - expect(style('$navy{NAVY}')).toEqual('\x1B[38;2;0;31;63mNAVY\x1B[0m'); + expect(style('$n{NAVY}')).toEqual('\x1B[38;2;0;31;63mNAVY\x1B[0m'); }); it('renders aqua fg color', () => { - expect(style('$aqua{AQUA}')).toEqual('\x1B[38;2;127;219;255mAQUA\x1B[0m'); + expect(style('$a{AQUA}')).toEqual('\x1B[38;2;127;219;255mAQUA\x1B[0m'); }); it('renders teal fg color', () => { - expect(style('$teal{TEAL}')).toEqual('\x1B[38;2;57;204;204mTEAL\x1B[0m'); + expect(style('$t{TEAL}')).toEqual('\x1B[38;2;57;204;204mTEAL\x1B[0m'); }); it('renders purple fg color', () => { - expect(style('$purple{PURPLE}')).toEqual( - '\x1B[38;2;177;13;201mPURPLE\x1B[0m' - ); + expect(style('$p{PURPLE}')).toEqual('\x1B[38;2;177;13;201mPURPLE\x1B[0m'); }); it('renders fuchsia fg color', () => { - expect(style('$fuchsia{FUCHSIA}')).toEqual( - '\x1B[38;2;240;18;190mFUCHSIA\x1B[0m' - ); + expect(style('$f{FUCHSIA}')).toEqual('\x1B[38;2;240;18;190mFUCHSIA\x1B[0m'); }); it('renders maroon fg color', () => { - expect(style('$maroon{MAROON}')).toEqual( - '\x1B[38;2;133;20;75mMAROON\x1B[0m' - ); + expect(style('$ma{MAROON}')).toEqual('\x1B[38;2;133;20;75mMAROON\x1B[0m'); }); it('renders olive fg color', () => { - expect(style('$olive{OLIVE}')).toEqual('\x1B[38;2;61;153;112mOLIVE\x1B[0m'); + expect(style('$ol{OLIVE}')).toEqual('\x1B[38;2;61;153;112mOLIVE\x1B[0m'); }); it('renders lime fg color', () => { - expect(style('$lime{LIME}')).toEqual('\x1B[38;2;1;255;112mLIME\x1B[0m'); + expect(style('$li{LIME}')).toEqual('\x1B[38;2;1;255;112mLIME\x1B[0m'); }); it('renders silver fg color', () => { - expect(style('$silver{SILVER}')).toEqual( - '\x1B[38;2;221;221;221mSILVER\x1B[0m' - ); + expect(style('$s{SILVER}')).toEqual('\x1B[38;2;221;221;221mSILVER\x1B[0m'); }); it('renders bg red color', () => { @@ -124,47 +124,41 @@ describe('colors', () => { }); it('renders bg navy color', () => { - expect(style('$bgnavy{NAVY}')).toEqual('\x1B[48;2;0;31;63mNAVY\x1B[0m'); + expect(style('$bgn{NAVY}')).toEqual('\x1B[48;2;0;31;63mNAVY\x1B[0m'); }); it('renders bg aqua color', () => { - expect(style('$bgaqua{AQUA}')).toEqual('\x1B[48;2;127;219;255mAQUA\x1B[0m'); + expect(style('$bga{AQUA}')).toEqual('\x1B[48;2;127;219;255mAQUA\x1B[0m'); }); it('renders bg teal color', () => { - expect(style('$bgteal{TEAL}')).toEqual('\x1B[48;2;57;204;204mTEAL\x1B[0m'); + expect(style('$bgt{TEAL}')).toEqual('\x1B[48;2;57;204;204mTEAL\x1B[0m'); }); it('renders bg purple color', () => { - expect(style('$bgpurple{PURPLE}')).toEqual( - '\x1B[48;2;177;13;201mPURPLE\x1B[0m' - ); + expect(style('$bgp{PURPLE}')).toEqual('\x1B[48;2;177;13;201mPURPLE\x1B[0m'); }); it('renders bg fuchsia color', () => { - expect(style('$bgfuchsia{FUCHSIA}')).toEqual( + expect(style('$bgf{FUCHSIA}')).toEqual( '\x1B[48;2;240;18;190mFUCHSIA\x1B[0m' ); }); it('renders bg maroon color', () => { - expect(style('$bgmaroon{MAROON}')).toEqual( - '\x1B[48;2;133;20;75mMAROON\x1B[0m' - ); + expect(style('$bgma{MAROON}')).toEqual('\x1B[48;2;133;20;75mMAROON\x1B[0m'); }); it('renders bg olive color', () => { - expect(style('$bgolive{OLIVE}')).toEqual( - '\x1B[48;2;61;153;112mOLIVE\x1B[0m' - ); + expect(style('$bgol{OLIVE}')).toEqual('\x1B[48;2;61;153;112mOLIVE\x1B[0m'); }); it('renders bg lime color', () => { - expect(style('$bglime{LIME}')).toEqual('\x1B[48;2;1;255;112mLIME\x1B[0m'); + expect(style('$bgli{LIME}')).toEqual('\x1B[48;2;1;255;112mLIME\x1B[0m'); }); it('renders bg silver color', () => { - expect(style('$bgsilver{SILVER}')).toEqual( + expect(style('$bgs{SILVER}')).toEqual( '\x1B[48;2;221;221;221mSILVER\x1B[0m' ); }); @@ -237,4 +231,16 @@ describe('colors', () => { style('This is will be normal $r{RED} string', { color: false }) ).toEqual('This is will be normal RED string'); }); + + it('renders default fg color within red color', () => { + expect(style('$r{Straight $dfg{sometext} underline}')).toEqual( + '\u001B[38;2;255;65;54mStraight \u001B[0m\u001B[39msometext\u001B[0m\u001B[38;2;255;65;54m underline\u001B[0m' + ); + }); + + it('renders default bg color within yellow bg color text', () => { + expect(style('$bgy{Straight $dbg{some text} underline}')).toEqual( + '\u001B[48;2;255;220;0mStraight \u001B[0m\u001B[49msome text\u001B[0m\u001B[48;2;255;220;0m underline\u001B[0m' + ); + }); }); diff --git a/__tests__/dim.spec.js b/__tests__/dim.spec.js new file mode 100644 index 0000000..9a63639 --- /dev/null +++ b/__tests__/dim.spec.js @@ -0,0 +1,53 @@ +import { style } from '../src/index'; + +beforeAll(() => { + process.env.FORCE_COLOR = 3; +}); + +afterAll(() => { + delete process.env.FORCE_COLOR; +}); + +describe('Dim', () => { + it('renders dim text', () => { + expect(style('$dim{text}')).toEqual('\x1B[2mtext\x1B[0m'); + }); + + it('renders dim text with color', () => { + expect(style('$dim.r{text}')).toEqual( + '\u001B[38;2;127;32;27mtext\u001B[0m' + ); + }); + + it('renders normal text inside dim', () => { + expect(style('$dim{Some $nor{long} text}')).toEqual( + '\u001B[2mSome \u001B[0m\u001B[2m\u001B[22mlong\u001B[0m\u001B[2m text\u001B[0m' + ); + }); + + it('renders bold color text inside dim', () => { + expect(style('$dim{Some $bol.y{long} text}')).toEqual( + '\u001B[2mSome \u001B[0m\u001B[1m\u001B[38;2;127;110;0mlong\u001B[0m\u001B[2m text\u001B[0m' + ); + }); + + it('renders nested dim text with color', () => { + expect(style('$dim.r{Some $g{green} text}')).toEqual( + '\u001B[38;2;127;32;27mSome \u001B[0m\u001B[38;2;23;102;32mgreen\u001B[0m\u001B[38;2;127;32;27m text\u001B[0m' + ); + }); + + it('renders normal text nested within dim text with color', () => { + expect( + style('$dim.r{Some $g{green text $nor{inside} some text} text}') + ).toEqual( + '\u001B[38;2;127;32;27mSome \u001B[0m\u001B[38;2;23;102;32mgreen text \u001B[0m\u001B[38;2;46;204;64m\u001B[22minside\u001B[0m\u001B[38;2;23;102;32m some text\u001B[0m\u001B[38;2;127;32;27m text\u001B[0m' + ); + }); + + it('renders rgb colored dim text within normal text', () => { + expect(style('Normal $rgb(255, 150, 0).dim{dim text} text')).toEqual( + 'Normal \u001B[38;2;127;75;0mdim text\u001B[0m text' + ); + }); +}); diff --git a/__tests__/modifiers.spec.js b/__tests__/modifiers.spec.js index 670a098..2c4e008 100644 --- a/__tests__/modifiers.spec.js +++ b/__tests__/modifiers.spec.js @@ -11,13 +11,13 @@ afterAll(() => { describe('Modifiers', () => { it('renders default style', () => { expect(style('$r{RED $res{Normal} RED}')).toEqual( - '\x1B[38;2;255;65;54mRED \x1B[0m\x1B[38;2;255;65;54m\x1B[0mNormal\x1B[0m\x1B[38;2;255;65;54m RED\x1B[0m' + '\x1B[38;2;255;65;54mRED \x1B[0m\x1B[0mNormal\x1B[0m\x1B[38;2;255;65;54m RED\x1B[0m' ); }); it('renders normal style', () => { expect(style('$bol{BOLD $nor{NORMAL} BOLD}')).toEqual( - '\x1B[1mBOLD \x1B[0m\x1B[1m\x1B[22mNORMAL\x1B[0m\x1B[1m BOLD\x1B[0m' + '\x1B[1mBOLD \x1B[0m\x1B[22mNORMAL\x1B[0m\x1B[1m BOLD\x1B[0m' ); }); @@ -25,39 +25,49 @@ describe('Modifiers', () => { expect(style('$blk{SALE}')).toEqual('\x1B[5mSALE\x1B[0m'); }); - it('renders double underline text', () => { - expect(style('$dun{text}')).toEqual('\x1B[21mtext\x1B[0m'); + it('renders no blinking text', () => { + expect(style('$nob{Offer sale}')).toEqual('\u001B[25mOffer sale\u001B[0m'); }); it('renders hidden text', () => { expect(style('$hid{text}')).toEqual('\x1B[8mtext\x1B[0m'); }); + it('renders visible text inside hidden text', () => { + expect(style('Email: $hid{user12345$vis{@example.com}}')).toEqual( + 'Email: \u001B[8muser12345\u001B[0m\u001B[28m@example.com\u001B[0m\u001B[8m\u001B[0m' + ); + }); + it('renders overline text', () => { expect(style('$ovl{text}')).toEqual('\x1B[53mtext\x1B[0m'); }); - it('renders bold text', () => { - expect(style('$bol{text}')).toEqual('\x1B[1mtext\x1B[0m'); + it('renders no overline text', () => { + expect(style('$noo{text}')).toEqual('\x1B[55mtext\x1B[0m'); }); - it('renders dim text', () => { - expect(style('$dim{text}')).toEqual('\x1B[2mtext\x1B[0m'); + it('renders bold text', () => { + expect(style('$bol{text}')).toEqual('\x1B[1mtext\x1B[0m'); }); it('renders italic text', () => { expect(style('$ita{text}')).toEqual('\x1B[3mtext\x1B[0m'); }); - it('renders underline text', () => { - expect(style('$und{text}')).toEqual('\x1B[4mtext\x1B[0m'); + it('renders inversed text', () => { + expect(style('$inv{text}')).toEqual('\x1B[7mtext\x1B[0m'); }); - it('renders inv text', () => { - expect(style('$inv{text}')).toEqual('\x1B[7mtext\x1B[0m'); + it('renders not inversed text', () => { + expect(style('$noi{text}')).toEqual('\x1B[27mtext\x1B[0m'); }); it('renders strikethrough text', () => { expect(style('$str{text}')).toEqual('\x1B[9mtext\x1B[0m'); }); + + it('renders not strikethrough text', () => { + expect(style('$nos{text}')).toEqual('\x1B[29mtext\x1B[0m'); + }); }); diff --git a/__tests__/underline.spec.js b/__tests__/underline.spec.js new file mode 100644 index 0000000..1ad03d2 --- /dev/null +++ b/__tests__/underline.spec.js @@ -0,0 +1,93 @@ +import { style } from '../src/index'; + +beforeAll(() => { + process.env.FORCE_COLOR = 3; +}); + +afterAll(() => { + delete process.env.FORCE_COLOR; +}); + +describe('Underline', () => { + it('renders straight underline text', () => { + expect(style('$und{Straight Underline}')).toEqual( + '\x1B[4mStraight Underline\x1B[0m' + ); + }); + + it('renders straight underline text with no underline', () => { + expect( + style('$und{Straight $nou{(No underline here)} underline}') + ).toEqual( + '\u001B[4mStraight \u001B[0m\u001B[24m(No underline here)\u001B[0m\u001B[4m underline\u001B[0m' + ); + }); + + it('renders doubly underline text', () => { + expect(style('$dbu{Double underline}')).toEqual( + '\u001B[21mDouble underline\u001B[0m' + ); + }); + + it('renders curly underline text', () => { + expect(style('$cru{Curly underline}')).toEqual( + '\u001B[4:3mCurly underline\u001B[0m' + ); + }); + + it('renders dotted underline text', () => { + expect(style('$dou{Dotted underline}')).toEqual( + '\u001B[4:4mDotted underline\u001B[0m' + ); + }); + + it('renders dashed underline text', () => { + expect(style('$dau{Dashed underline}')).toEqual( + '\u001B[4:5mDashed underline\u001B[0m' + ); + }); + + it('renders straight colored underline text', () => { + expect(style('$und.ug{Straight underline colored}')).toEqual( + '\u001B[4m\u001B[58:2:0:46:204:64mStraight underline colored\u001B[0m' + ); + }); + + it('renders doubly colored underline text', () => { + expect(style('$dbu.uy{Double underline colored}')).toEqual( + '\u001B[21m\u001B[58:2:0:255:220:0mDouble underline colored\u001B[0m' + ); + }); + + it('renders curly colored underline text', () => { + expect(style('$cru.ur{Curly underline colored}')).toEqual( + '\u001B[4:3m\u001B[58:2:0:255:65:54mCurly underline colored\u001B[0m' + ); + }); + + it('renders dotted colored underline text', () => { + expect(style('$dou.ub{Dotted underline colored}')).toEqual( + '\u001B[4:4m\u001B[58:2:0:0:116:217mDotted underline colored\u001B[0m' + ); + }); + + it('renders dashed colored underline text', () => { + expect(style('$dau.uo{Dashed underline colored}')).toEqual( + '\u001B[4:5m\u001B[58:2:0:255:133:27mDashed underline colored\u001B[0m' + ); + }); + + it('renders staight rgb colored', () => { + expect( + style('$und.urgb(100,200,255){Straight underline $ug{RGB} colored}') + ).toEqual( + '\u001B[4m\u001B[58:2:0:100;200;255mStraight underline \u001B[0m\u001B[4m\u001B[58:2:0:46:204:64mRGB\u001B[0m\u001B[4m\u001B[58:2:0:100;200;255m colored\u001B[0m' + ); + }); + + it('renders default colored underline between blue colored text', () => { + expect(style('$und.ub{Straight $ruc{underline reset} colored}')).toEqual( + '\u001B[4m\u001B[58:2:0:0:116:217mStraight \u001B[0m\u001B[4m\u001B[59munderline reset\u001B[0m\u001B[4m\u001B[58:2:0:0:116:217m colored\u001B[0m' + ); + }); +}); diff --git a/assets/Underline-Demo.png b/assets/Underline-Demo.png new file mode 100644 index 0000000..e2e8c80 Binary files /dev/null and b/assets/Underline-Demo.png differ diff --git a/assets/underline.png b/assets/underline.png deleted file mode 100644 index fbe4cb2..0000000 Binary files a/assets/underline.png and /dev/null differ diff --git a/demo.js b/demo.js index 50508c1..8d23f5b 100644 --- a/demo.js +++ b/demo.js @@ -6,9 +6,9 @@ function highlight(code) { let html = hljs.highlight(code, { language: 'js', }).value; - html = html.replaceAll('', '$fuchsia{'); + html = html.replaceAll('', '$f{'); html = html.replaceAll('', '$b{'); - html = html.replaceAll('', '$lime{'); + html = html.replaceAll('', '$li{'); html = html.replaceAll('', '$y{'); html = html.replaceAll('', ''); html = html.replaceAll('', '$gr.dim{'); @@ -26,7 +26,7 @@ function greet() { console.log(highlight(code)); -function run(str, color = true) { +function print(str, color = true) { console.log(); console.log(str); console.log(); @@ -40,62 +40,76 @@ const disk = 70; const getColor = (n) => (n <= 50 ? 'g' : n > 50 && n <= 70 ? 'y' : 'r'); -run(` +print(` CPU: $${getColor(cpu)}{${cpu}%} RAM: $${getColor(ram)}{${ram}%} DISK: $${getColor(disk)}{${disk}%} `); -run('$r{RED} $r.bol{RED} $r.dim{RED}'); +print('$r{RED} $r.bol{RED} $r.dim{RED}'); -run('$g.bol{SALE! - $blk.r{50% OFFER}}'); +print('$g.bol{SALE! - $blk.r{50% OFFER}}'); -run('This is normal string $r{RED} $g{GREEN}'); +print('This is normal string $r{RED} $g{GREEN}'); -run( +print( "$bgbl.b{THE QUICK $g{BROWN $r.bol{CAT} JUMPED} OVER THE LAZY $r.bol{DOG}'S BACK}" ); -run('$bol{foo $r.dun{bar} $ovl.o{hindi} baz} $und{UNDERLINE}'); +print('$bol{foo $r.dbu{bar} $ovl.o{hindi} baz} $und{UNDERLINE}'); -run( +print( 'This is a $o.bgbl{Hello World} example, This is a $r.bol{RED} $g.ita{GREEN} $b.str{BLUE} $gr.dim{GREY} $y.und{YELLOW} $w.bgbl.inv{INVERSE} $bol.rgb(57,204,204){TEAL} $bgrgb(133,20,75).w.bol{MAROON} $m{Magenta} text.' ); -run( +print( '🍊 - An $o{orange} is a fruit of various citrus species in the family Rutaceae.' ); -run( +print( `$bgy.bl{The $r.bol{R}$g.bol{G}$b.bol{B} color model is an additive color model in which the $r.bol{red}, $g.bol{green} and $b.bol{blue} primary colors of light are added together in various ways to reproduce a broad array of colors.}` ); -run( +print( '$g{I am a green line $b.und.bol{with a blue substring} that becomes green again!}' ); -run('$bol.w.bgg{ PASS }'); +print('$bol.w.bgg{ PASS }'); -run('$r.bgw.bol.inv{ FAILED }'); +print('$r.bgw.bol.inv{ FAILED }'); -run('Normal text | $bol{Bold text} | $dim{Dimmed text}'); +print('Normal text | $bol{Bold text} | $dim{Dimmed text}'); -run( - '$ita.fuchsia.bol.bgw{"This poem is endless,\n the odds against us are endless,\n our chances of being alive together statistically nonexistent;\n still we have made it"}' +print( + '$ita.f.bol.bgw{"This poem is endless,\n the odds against us are endless,\n our chances of being alive together statistically nonexistent;\n still we have made it"}' ); -run('Highlighted fruits: $und{Apple}, cat, $und{Banana}'); +print('Highlighted fruits: $und{Apple}, cat, $und{Banana}'); -run('Price: $str.r{\\$75.00} $g{\\$50.00}'); +print('Price: $str.r{\\$75.00} $g{\\$50.00}'); -run('$rgb(197, 24, 117){Some random rgb text}'); +print('$rgb(197, 24, 117){Some random rgb text}'); -run('$rgb(157,154,117){RGB text without space}'); +print('$rgb(157,154,117){RGB text without space}'); -run('$bgrgb(5, 255, 55).navy.bol{Some random rgb text}'); +print('$bgrgb(5, 255, 55).n.bol{Some random rgb text}'); -run('$inv{inverse}'); +print('$inv{inverse}'); -run( +print( `<$hex(#39CCCC){input} name=$y{"price"} value=$y{"\\$\\{ Cost + Tax \\}.00"} />` ); + +print('$und{Straight underline}'); +print('$und{Straight $nou{(No underline here)} underline}'); +print('$dbu{Double underline}'); +print('$cru{Curly underline}'); +print('$dou{Dotted underline}'); +print('$dau{Dashed underline}'); +print('$und.ug{Straight underline colored}'); +print('$dbu.uy{Double underline colored}'); +print('$cru.ur{Curly underline colored}'); +print('$dou.ub{Dotted underline colored}'); +print('$dau.uo{Dashed underline colored}'); +print('$und.urgb(100,200,255){Straight underline RGB colored}'); +print('$und.ub{Straight $ruc{underline reset} colored}'); diff --git a/src/applyStyles.ts b/src/applyStyles.ts index 09aa542..7ee694d 100644 --- a/src/applyStyles.ts +++ b/src/applyStyles.ts @@ -1,41 +1,5 @@ -import { arrRm, hexToRGB } from '@opentf/std'; -import { - ANSI_END, - ANSI_START, - BG_CODE, - FG_CODE, - MODIFIERS, - RESET, - STANDARD_COLORS, -} from './constants'; - -const colorKeys = Object.keys(STANDARD_COLORS); -const modfierKeys = Object.keys(MODIFIERS); - -function getStylesArr(styles: string, colorKeys: string[]) { - const fgArr: number[] = []; - const bgArr: number[] = []; - const stylesArr = styles.split('.'); - - for (let i = 0; i < stylesArr.length; i++) { - const s = stylesArr[i] as string; - if (colorKeys.includes(s) || s.startsWith('hex') || s.startsWith('rgb')) { - fgArr.push(i); - } - - if ( - (s.startsWith('bg') && colorKeys.includes(s.substring(2))) || - s.startsWith('bghex') || - s.startsWith('bgrgb') - ) { - bgArr.push(i); - } - } - - const rmarr = [...arrRm(fgArr), ...arrRm(bgArr)]; - - return stylesArr.filter((_, i) => !rmarr.includes(i)); -} +import { ANSI_END, ANSI_START, RESET } from './constants'; +import processStyles from './processStyles'; export default function applyStyles(str: string, styles: string): string { if (!styles) { @@ -43,56 +7,10 @@ export default function applyStyles(str: string, styles: string): string { } let out = ''; - const stylesArr = getStylesArr(styles, colorKeys); - - for (let i = 0; i < stylesArr.length; i++) { - const style = stylesArr[i] as string; - - if (colorKeys.includes(style)) { - out += ANSI_START + FG_CODE + STANDARD_COLORS[style] + ANSI_END; - continue; - } - - if (style.startsWith('bgrgb')) { - const s = style - .slice(6, -1) - .split(',') - .map((v) => v.trim()) - .join(';'); - out += ANSI_START + BG_CODE + s + ANSI_END; - continue; - } - - if (style.startsWith('bghex')) { - const rgb = hexToRGB(style.slice(6, -1)); - out += ANSI_START + BG_CODE + rgb.join(';') + ANSI_END; - continue; - } - - if (style.startsWith('bg')) { - out += ANSI_START + BG_CODE + STANDARD_COLORS[style.slice(2)] + ANSI_END; - continue; - } - - if (modfierKeys.includes(style)) { - out += ANSI_START + MODIFIERS[style] + ANSI_END; - continue; - } - - if (style.startsWith('rgb')) { - const s = style - .slice(4, -1) - .split(',') - .map((v) => v.trim()) - .join(';'); - out += ANSI_START + FG_CODE + s + ANSI_END; - continue; - } + const styleMap = processStyles(styles); - if (style.startsWith('hex')) { - const rgb = hexToRGB(style.slice(4, -1)); - out += ANSI_START + FG_CODE + rgb.join(';') + ANSI_END; - } + for (const v of styleMap.values()) { + out += ANSI_START + v.code + v.value + ANSI_END; } return out + str + RESET; diff --git a/src/constants.ts b/src/constants.ts index 4b2b9b1..a884554 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,6 +8,8 @@ export const FG_CODE = '38;2;'; export const BG_CODE = '48;2;'; +export const UND_COLOR_CODE = '58:2:0:'; + export const STANDARD_COLORS: Record = { r: '255;65;54', // Red g: '46;204;64', // Green @@ -18,28 +20,42 @@ export const STANDARD_COLORS: Record = { bl: '17;17;17', // Black gr: '170;170;170', // Grey m: '255;105;193', // Magenta - navy: '0;31;63', - aqua: '127;219;255', - teal: '57;204;204', - purple: '177;13;201', - fuchsia: '240;18;190', - maroon: '133;20;75', - olive: '61;153;112', - lime: '1;255;112', - silver: '221;221;221', -}; + c: '154;236;254', // Cyan + n: '0;31;63', // Navy + a: '127;219;255', // Aqua + t: '57;204;204', // Teal + p: '177;13;201', // Purple + f: '240;18;190', // Fuchsia + ma: '133;20;75', // Maroon + ol: '61;153;112', // Olive + li: '1;255;112', // Lime + s: '221;221;221', // Silver + pi: '255;191;203', // Pink +} as const; -export const MODIFIERS: Record = { - res: '0', - nor: '22', - blk: '5', - dun: '21', - hid: '8', - ovl: '53', - bol: '1', - dim: '2', - ita: '3', - und: '4', - inv: '7', - str: '9', -}; +export const MODIFIERS = { + dfg: '39', // Default foreground color + dbg: '49', // Default background color + res: '0', // Reset + nor: '22', // Normal intensity + blk: '5', // Blink + nob: '25', // No Blink + hid: '8', // Hidden + vis: '28', // Visible + ovl: '53', // Overline + noo: '55', // No Overline + bol: '1', // Bold + dim: '2', // Dimmed text + ita: '3', // Italic + inv: '7', // Inverse fg to bg & vice versa + noi: '27', // Not inversed + str: '9', // Strikethrough + nos: '29', // No Strikethrough + und: '4', // Straight underline + dbu: '21', // Double underline + nou: '24', // No underline + cru: '4:3', // Curly underline + dou: '4:4', // Dotted underline + dau: '4:5', // Dashed underline + ruc: '59', // Reset underline color +} as const; diff --git a/src/processStyles.ts b/src/processStyles.ts new file mode 100644 index 0000000..8b37b1c --- /dev/null +++ b/src/processStyles.ts @@ -0,0 +1,270 @@ +import { hexToRGB } from '@opentf/std'; +import { + BG_CODE, + FG_CODE, + MODIFIERS, + STANDARD_COLORS, + UND_COLOR_CODE, +} from './constants'; + +const colorKeys = Object.keys(STANDARD_COLORS); + +function dimColor(color: string) { + return color + .split(';') + .map((c) => parseInt(c) * (1 / 2)) + .map((c) => Math.floor(c)) + .join(';'); +} + +export default function processStyles(styles: string) { + const arr = styles.split('.'); + const map = new Map(); + + for (let i = 0; i < arr.length; i++) { + const style = arr[i] as string; + + if (style === 'dfg') { + map.delete('color'); + map.set('dfg', { value: MODIFIERS[style], code: '' }); + } + + if (colorKeys.includes(style)) { + map.delete('dfg'); + let value; + if (map.has('color') && map.get('color').dim) { + value = dimColor(STANDARD_COLORS[style] as string); + map.set('color', { + value, + code: FG_CODE, + dim: true, + color: STANDARD_COLORS[style], + }); + continue; + } else if (map.has('dim')) { + value = dimColor(STANDARD_COLORS[style] as string); + map.delete('dim'); + map.set('color', { + value, + code: FG_CODE, + dim: true, + color: STANDARD_COLORS[style], + }); + continue; + } else { + value = STANDARD_COLORS[style]; + } + map.set('color', { value, code: FG_CODE }); + continue; + } + + if (style === 'dim') { + if (map.has('color')) { + map.set('color', { + value: dimColor(map.get('color').value), + code: FG_CODE, + dim: true, + }); + } else { + map.set('dim', { value: MODIFIERS[style], code: '' }); + } + continue; + } + + if (style.startsWith('rgb')) { + const s = style + .slice(4, -1) + .split(',') + .map((v) => v.trim()) + .join(';'); + map.delete('fg'); + map.set('color', { value: s, code: FG_CODE }); + continue; + } + + if (style.startsWith('hex')) { + map.delete('fg'); + const rgb = hexToRGB(style.slice(4, -1)).join(';'); + map.set('color', { + value: rgb, + code: FG_CODE, + }); + continue; + } + + if (style.startsWith('bghex')) { + map.delete('bg'); + const rgb = hexToRGB(style.slice(6, -1)).join(';'); + map.set('bgcolor', { + value: rgb, + code: BG_CODE, + }); + continue; + } + + if (style.startsWith('bgrgb')) { + const s = style + .slice(6, -1) + .split(',') + .map((v) => v.trim()) + .join(';'); + map.delete('fg'); + map.set('bgcolor', { + value: s, + code: BG_CODE, + }); + continue; + } + + if (style.startsWith('bg')) { + map.delete('bg'); + map.set('bgcolor', { + value: STANDARD_COLORS[style.slice(2)], + code: BG_CODE, + }); + continue; + } + + if (style === 'dbg') { + map.delete('bgcolor'); + map.set('dbg', { value: MODIFIERS[style], code: '' }); + } + + if (style === 'bol') { + map.delete('nor'); + map.set('bold', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'res') { + map.clear(); + map.set('reset', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'dfg') { + map.delete('color'); + map.set('dfg', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'nor') { + map.delete('bold'); + if (map.has('color') && map.get('color').dim) { + const obj = map.get('color'); + map.set('color', { + ...obj, + value: obj.color, + }); + } + map.set('nor', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'hid') { + map.delete('vis'); + map.set('hid', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'vis') { + map.delete('hid'); + map.set('vis', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'inv') { + map.delete('noi'); + map.set('inv', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'noi') { + map.delete('inv'); + map.set('noi', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'blk') { + map.delete('nob'); + map.set('blk', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'nob') { + map.delete('blk'); + map.set('nob', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'ovl') { + map.delete('noo'); + map.set('ovl', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'noo') { + map.delete('ovl'); + map.set('noo', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'str') { + map.delete('nos'); + map.set('str', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'nos') { + map.delete('str'); + map.set('nos', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'ita') { + map.set('ita', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style.startsWith('urgb')) { + const s = style + .slice(5, -1) + .split(',') + .map((v) => v.trim()) + .join(';'); + map.set('uColor', { value: s, code: UND_COLOR_CODE }); + continue; + } + + if (['und', 'dbu', 'cru', 'dou', 'dau'].includes(style)) { + map.delete('nou'); + map.set('und', { + value: MODIFIERS[style as keyof typeof MODIFIERS], + code: '', + }); + continue; + } + + if (style === 'nou') { + map.delete('und'); + map.set('nou', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style === 'ruc') { + map.delete('uColor'); + map.set('ruc', { value: MODIFIERS[style], code: '' }); + continue; + } + + if (style.startsWith('u')) { + map.set('uColor', { + value: STANDARD_COLORS[style.slice(1)]?.replaceAll(';', ':'), + code: UND_COLOR_CODE, + }); + continue; + } + } + + return map; +} diff --git a/src/style.ts b/src/style.ts index 7f21aef..dfd7a2a 100644 --- a/src/style.ts +++ b/src/style.ts @@ -1,3 +1,4 @@ +import { isStr, shallowMerge } from '@opentf/std'; import applyStyles from './applyStyles'; import isSupportsColor from './isSupportsColor'; import parser, { type StyleObj } from './parser'; @@ -6,7 +7,7 @@ function renderWithStyles(obj: StyleObj, color: boolean) { let out = ''; obj.text.forEach((t) => { - if (typeof t === 'string') { + if (isStr(t)) { out += color ? applyStyles(t, obj.styles) : t; } else { out += renderWithStyles(t, color); @@ -17,28 +18,24 @@ function renderWithStyles(obj: StyleObj, color: boolean) { } function render(arr: (string | StyleObj)[], color: boolean) { - let out = ''; - arr.forEach((item) => { - if (typeof item === 'string') { - out += item; - } else { - out += renderWithStyles(item, color); - } - }); - - return out; + return arr.reduce((acc, cur) => { + return (acc += isStr(cur) ? cur : renderWithStyles(cur, color)); + }, '') as string; } -export default function style( - str: string, - options?: { color: boolean } -): string { - let color = options && 'color' in options ? options.color : true; - if (color) { - color = isSupportsColor(); +type Options = { + color?: boolean; +}; + +export default function style(str: string, options?: Options): string { + const opts = shallowMerge( + { color: true }, + options as object + ) as Required; + if (opts.color) { + opts.color = isSupportsColor(); } const parsedValue = parser(str); - const out = render(parsedValue, color); - return out; + return render(parsedValue, opts.color); }