use proper types for bbstring

This commit is contained in:
Daylin Morgan 2023-09-12 00:14:37 -05:00
parent 0df16a2654
commit 82486a89e6
Signed by: daylin
GPG Key ID: C1E52E7DD81DF79F
3 changed files with 222 additions and 91 deletions

View File

@ -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 "---------->"

View File

@ -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)

View File

@ -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