mirror of
https://github.com/daylinmorgan/bbansi.git
synced 2024-12-21 18:40:44 -06:00
use proper types for bbstring
This commit is contained in:
parent
0df16a2654
commit
82486a89e6
3 changed files with 222 additions and 91 deletions
194
src/bbansi.nim
194
src/bbansi.nim
|
@ -1,74 +1,156 @@
|
|||
# This is just an example to get you started. A typical hybrid package
|
||||
# uses this file as the main entry point of the application.
|
||||
import std/[os, strutils]
|
||||
import std/[os, sequtils, strutils, sugar]
|
||||
import bbansi/styles
|
||||
|
||||
# TODO: add support for some kind of FORCE_COLOR
|
||||
# TODO: add support for some kind of FORCE_COLOR and detect terminals...
|
||||
let noColor = os.getEnv("NO_COLOR") != ""
|
||||
|
||||
type
|
||||
BbSpan = object
|
||||
styles: seq[string]
|
||||
slice: array[2, int]
|
||||
BbString = object
|
||||
raw: string
|
||||
plain: string
|
||||
spans: seq[BbSpan]
|
||||
|
||||
proc bb*(s: string): string =
|
||||
proc debug(bbs: BbString): string =
|
||||
echo "bbString("
|
||||
echo " raw: ", bbs.raw
|
||||
echo " plain: ", bbs.plain
|
||||
echo " spans: ", bbs.spans
|
||||
echo ")"
|
||||
|
||||
proc `&`*(x: BbString, y: string): BbString =
|
||||
result = x
|
||||
result.raw &= y
|
||||
result.plain &= y
|
||||
result.spans[^1].slice[1] = result.plain.len-1
|
||||
|
||||
proc `&`*(x: string, y: BbString): BbString =
|
||||
result.raw = x & y.raw
|
||||
result.plain = x & y.plain
|
||||
result.spans.add BbSpan(styles: @[],slice: [0, x.len-1] )
|
||||
for span in y.spans:
|
||||
let
|
||||
length = x.len
|
||||
styles = span.styles
|
||||
slice = span.slice
|
||||
result.spans.add BbSpan(styles: styles, slice: [slice[0]+length,slice[1]+length])
|
||||
|
||||
func len*(bbs: BbString): int = bbs.plain.len
|
||||
|
||||
proc `$`*(bbs: BbString): string =
|
||||
if noColor: return bbs.plain
|
||||
|
||||
for span in bbs.spans:
|
||||
var codes = ""
|
||||
if span.styles.len > 0:
|
||||
codes = collect(for style in span.styles: style.toAnsiCode).join("")
|
||||
|
||||
result.add codes
|
||||
result.add bbs.plain[span.slice[0]..span.slice[1]]
|
||||
|
||||
if codes != "":
|
||||
result.add bbReset
|
||||
|
||||
proc endSpan(bbs: var BbString) =
|
||||
bbs.spans[^1].slice[1] = bbs.plain.len-1
|
||||
|
||||
proc newSpan(bbs: var BbString, pattern: string) =
|
||||
bbs.spans.add BbSpan(styles: @[pattern], slice: [bbs.plain.len, 0])
|
||||
|
||||
proc resetSpan(bbs: var BbString) =
|
||||
bbs.endSpan
|
||||
bbs.spans.add BbSpan(styles: @[], slice: [bbs.plain.len, 0])
|
||||
|
||||
proc closeLastStyle(bbs: var BbString) =
|
||||
bbs.endSpan
|
||||
let newStyle = bbs.spans[^1].styles[0..^2] # drop the latest style
|
||||
bbs.spans.add BbSpan(styles: newStyle, slice: [bbs.plain.len, 0])
|
||||
|
||||
proc addToSpan(bbs: var BbString, pattern: string) =
|
||||
bbs.endSpan
|
||||
let currStyl = bbs.spans[^1].styles
|
||||
bbs.spans.add BbSpan(styles: currStyl & @[pattern], slice: [bbs.plain.len, 0])
|
||||
|
||||
proc closeStyle(bbs: var BbString, pattern: string) =
|
||||
let style = pattern[1..^1].strip()
|
||||
if style in bbs.spans[^1].styles:
|
||||
bbs.endSpan
|
||||
let newStyle = bbs.spans[^1].styles.filterIt(it != style) # use sets instead
|
||||
bbs.spans.add BbSpan(styles: newStyle, slice: [bbs.plain.len, 0])
|
||||
|
||||
proc closeFinalSpan(bbs: var BbString) =
|
||||
if bbs.spans.len >= 1 and bbs.spans[^1].slice[1] == 0:
|
||||
bbs.endSpan
|
||||
|
||||
proc bb*(s: string): BbString =
|
||||
## convert bbcode markup to ansi escape codes
|
||||
var
|
||||
pattern: string
|
||||
i = 0
|
||||
addReset = false
|
||||
pattern = ""
|
||||
preChar = ' '
|
||||
|
||||
template next = result.plain.add s[i]; inc i
|
||||
template incPattern = pattern.add s[i]; inc i
|
||||
template resetPattern = pattern = ""; inc i
|
||||
|
||||
result.raw = s
|
||||
if not s.startswith('[') or s.startswith("[["):
|
||||
result.spans.add BbSpan()
|
||||
|
||||
while i < s.len:
|
||||
# start extracting pattern when you see '[' but not '[['
|
||||
if s[i] == '\\':
|
||||
inc i
|
||||
if s[i] == '[':
|
||||
result.add s[i]
|
||||
case s[i]:
|
||||
of '\\':
|
||||
if i < s.len and s[i+1] == '[':
|
||||
inc i
|
||||
next
|
||||
of '[':
|
||||
if i < s.len and s[i+1] == '[':
|
||||
inc i; next; continue
|
||||
inc i
|
||||
continue
|
||||
|
||||
if s[i] == '[' and preChar != '\\':
|
||||
inc i
|
||||
while i < s.len and s[i] != ']':
|
||||
preChar = s[i]
|
||||
pattern.add s[i]
|
||||
inc i
|
||||
|
||||
if noColor:
|
||||
inc i
|
||||
continue
|
||||
if pattern in ["reset","/"]:
|
||||
result.add bbReset
|
||||
addReset = false
|
||||
while i < s.len and s[i] != ']':
|
||||
incPattern
|
||||
pattern = pattern.strip()
|
||||
if result.spans.len > 0:
|
||||
if pattern == "/": result.closeLastStyle
|
||||
elif pattern == "reset": result.resetSpan
|
||||
elif pattern.startswith('/'): result.closeStyle pattern
|
||||
else: result.addToSpan pattern
|
||||
else: result.newSpan pattern
|
||||
resetPattern
|
||||
else:
|
||||
for style in pattern.splitWhitespace():
|
||||
if style in codeMap:
|
||||
addReset = true
|
||||
result.add "\e[" & codeMap[style] & "m"
|
||||
pattern = ""
|
||||
inc i
|
||||
else:
|
||||
preChar = s[i]
|
||||
result.add s[i]
|
||||
inc i
|
||||
next
|
||||
result.closeFinalSpan
|
||||
|
||||
if addReset:
|
||||
result.add "\e[0m"
|
||||
proc `&`*(x: BbString, y: BbString): Bbstring =
|
||||
# there is probably a more efficient way to do this
|
||||
bb(x.raw & y.raw)
|
||||
|
||||
|
||||
# ---- cli
|
||||
when isMainModule:
|
||||
import std/[strformat, parseopt]
|
||||
const version = staticExec "git describe --tags --always --dirty=-dev"
|
||||
let help = &"""
|
||||
{bb"[bold]bbansi[/] [green]<args>[/] [black]<-h|-v>[/]"}
|
||||
{bb"[bold]bbansi[/] \[[green]args...[/]] [faint]\[-h|-v][/]"}
|
||||
|
||||
usage:
|
||||
{bb"[italic]usage"}:
|
||||
bbansi "[yellow] yellow text!"
|
||||
|-> {bb"[yellow] yellow text!"}
|
||||
bbansi "[bold red] bold red[/] plain text..."
|
||||
bbansi "[bold red] bold red text[/] plain text..."
|
||||
|-> {bb"[bold red] bold red text[/] plain text..."}
|
||||
"""
|
||||
|
||||
flags:
|
||||
""" & $(bb(collect(for (s, l, d) in [
|
||||
("h", "help", "show this help"),
|
||||
("v", "version", "show version")]:
|
||||
&"[yellow]-{s}[/] [green]--{l.alignLeft(8)}[/] | {d}").join("\n ")
|
||||
))
|
||||
proc writeHelp() =
|
||||
echo help
|
||||
quit(QuitSuccess)
|
||||
proc writeVersion() =
|
||||
echo "bbansi version -> ", version
|
||||
echo bb(&"[yellow]bbansi version[/][red] ->[/] [bold]{version}[/]")
|
||||
quit(QuitSuccess)
|
||||
var strArgs: seq[string]
|
||||
var p = initOptParser()
|
||||
|
@ -78,22 +160,20 @@ usage:
|
|||
of cmdShortOption, cmdLongOption:
|
||||
case key:
|
||||
of "help", "h": writeHelp()
|
||||
of "version","v": writeVersion()
|
||||
of "version", "v": writeVersion()
|
||||
else:
|
||||
echo bb"[red]ERROR[/]: unexpected option/value -> ", key, ", ", val
|
||||
echo "Option and value: ", key, ", ", val
|
||||
|
||||
of cmdArgument:
|
||||
strArgs.add key
|
||||
if strArgs.len == 0:
|
||||
echo help
|
||||
quit(QuitSuccess)
|
||||
for arg in strArgs:
|
||||
echo arg.bb
|
||||
|
||||
if strArgs.len != 0:
|
||||
for arg in strArgs:
|
||||
echo arg.bb
|
||||
else:
|
||||
echo "[bold]---------------------".bb
|
||||
echo bb"[bold]bold"
|
||||
echo bb"[red]red"
|
||||
echo bb"[bold red]bold red"
|
||||
echo bb"[bold red]bold red[reset] no more red"
|
||||
echo bb"[unknown]this text is red no?"
|
||||
echo bb"\[red] <- not a pattern "
|
||||
echo "---------->"
|
||||
echo "\e[31mRed Text\e[0m\nNext Line"
|
||||
echo "[red]Red Text[/red]\nNext Line".bb
|
||||
echo "---------->"
|
||||
|
|
|
@ -1,28 +1,58 @@
|
|||
|
||||
import strtabs
|
||||
import std/[strtabs, strutils]
|
||||
|
||||
export strtabs
|
||||
|
||||
let bbReset* ="\e[0m"
|
||||
let bbReset* = "\e[0m"
|
||||
|
||||
type
|
||||
BbStyle = enum
|
||||
bold = 1,
|
||||
faint,
|
||||
italic,
|
||||
underline,
|
||||
blink,
|
||||
reverse,
|
||||
conceal,
|
||||
strike,
|
||||
black = 30
|
||||
red,
|
||||
green,
|
||||
yellow,
|
||||
blue,
|
||||
magenta,
|
||||
cyan,
|
||||
white
|
||||
|
||||
|
||||
proc toAnsiCode*(s: string): string =
|
||||
var
|
||||
styles: seq[string]
|
||||
bgStyle: string
|
||||
if " on " in s:
|
||||
let fg_bg_split = s.rsplit(" on ", maxsplit=1)
|
||||
styles = fg_bg_split[0].splitWhitespace()
|
||||
bgStyle = fg_bg_split[1].strip()
|
||||
else:
|
||||
styles = s.splitWhitespace()
|
||||
for style in styles:
|
||||
try:
|
||||
var bbStyle: BbStyle
|
||||
if style.len == 1:
|
||||
bbstyle = parseEnum[BbStyle](
|
||||
case style:
|
||||
of "b": "bold"
|
||||
of "i": "italic"
|
||||
of "u": "underline"
|
||||
else: "" # this parse enum lookup is unneccesary
|
||||
)
|
||||
else:
|
||||
bbstyle = parseEnum[BbStyle](style)
|
||||
# if we fail to parse treat it like a noop..
|
||||
result.add "\e[" & $bbStyle.ord() & "m"
|
||||
except ValueError: discard
|
||||
try:
|
||||
let bbStyle = parseEnum[BbStyle](bgStyle)
|
||||
result.add "\e[" & $(bbStyle.ord()+10) & "m"
|
||||
except ValueError: discard
|
||||
|
||||
# should these be an enum?
|
||||
let
|
||||
codeMap* = {
|
||||
"reset":"0",
|
||||
"bold": "1",
|
||||
"faint": "2",
|
||||
"italic":"3",
|
||||
"underline":"4",
|
||||
"blink":"5",
|
||||
"reverse":"7",
|
||||
"conceal":"8",
|
||||
"strike":"9",
|
||||
"black":"30",
|
||||
"red": "31",
|
||||
"green":"32",
|
||||
"yellow":"33",
|
||||
"blue":"34",
|
||||
"magenta":"35",
|
||||
"cyan":"36",
|
||||
"white":"37",
|
||||
}.newStringTable(modeCaseInsensitive)
|
||||
|
|
|
@ -8,14 +8,35 @@
|
|||
import unittest
|
||||
|
||||
import bbansi
|
||||
test "basic":
|
||||
check "\e[31mRed Text\e[0m" == bb"[red]Red Text"
|
||||
check "No Style" == bb"No Style"
|
||||
check "Unknown Style" == bb"[unknown]Unknown Style"
|
||||
check "\e[1m\e[31mBold Red Text\e[0m" == bb"[bold red]Bold Red Text"
|
||||
check "\e[1m\e[31mBold Red Text\e[0mPlain Text" == bb"[bold red]Bold Red Text[reset]Plain Text"
|
||||
check "\e[1mBold\e[0m Not Bold" == bb"[bold]Bold[/] Not Bold"
|
||||
# not sure how rich handles this
|
||||
|
||||
test "escaped":
|
||||
check "[red] ignored pattern" == bb"\[red] ignored pattern"
|
||||
|
||||
suite "basic":
|
||||
test "simple":
|
||||
check "\e[31mRed Text\e[0m" == $bb"[red]Red Text"
|
||||
check "\e[33mYellow Text\e[0m" == $bb"[yellow]Yellow Text"
|
||||
check "\e[1m\e[31mBold Red Text\e[0m" == $bb"[bold red]Bold Red Text"
|
||||
|
||||
test "closing":
|
||||
check "\e[1mBold\e[0m\e[1m\e[31m Bold Red\e[0m\e[1m Bold Only\e[0m" ==
|
||||
$bb"[bold]Bold[red] Bold Red[/red] Bold Only"
|
||||
|
||||
test "abbreviated":
|
||||
check "\e[1mBold\e[0m Not Bold" == $bb"[b]Bold[/] Not Bold"
|
||||
|
||||
test "noop":
|
||||
check "No Style" == $bb"No Style"
|
||||
check "Unknown Style" == $bb"[unknown]Unknown Style"
|
||||
|
||||
test "escaped":
|
||||
check "[red] ignored pattern" == $"[[red] ignored pattern".bb
|
||||
|
||||
test "newlines":
|
||||
# Proc Strings: raw strings,
|
||||
# but the method name that prefixes the string is called
|
||||
# so that foo"12\" -> foo(r"12\")
|
||||
check "\e[31mRed Text\e[0m\nNext Line" == $"[red]Red Text[/]\nNext Line".bb
|
||||
|
||||
test "concat-ops":
|
||||
check "[red]RED[/]".bb & " plain string" == "[red]RED[/] plain string".bb
|
||||
check "[red]RED[/]".bb.len == 3
|
||||
check bb("[blue]Blue[/]") & " " & bb("[red]Red[/]") == "[blue]Blue[/] [red]Red[/]".bb
|
||||
check "a plain string" & "[blue] a blue string".bb == "a plain string[blue] a blue string".bb
|
||||
|
|
Loading…
Reference in a new issue