mirror of
https://github.com/daylinmorgan/bbansi.git
synced 2024-11-24 17:50:45 -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
|
import std/[os, sequtils, strutils, sugar]
|
||||||
# uses this file as the main entry point of the application.
|
|
||||||
import std/[os, strutils]
|
|
||||||
import bbansi/styles
|
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") != ""
|
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
|
## convert bbcode markup to ansi escape codes
|
||||||
var
|
var
|
||||||
|
pattern: string
|
||||||
i = 0
|
i = 0
|
||||||
addReset = false
|
|
||||||
pattern = ""
|
template next = result.plain.add s[i]; inc i
|
||||||
preChar = ' '
|
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:
|
while i < s.len:
|
||||||
# start extracting pattern when you see '[' but not '[['
|
case s[i]:
|
||||||
if s[i] == '\\':
|
of '\\':
|
||||||
inc i
|
if i < s.len and s[i+1] == '[':
|
||||||
if s[i] == '[':
|
inc i
|
||||||
result.add s[i]
|
next
|
||||||
|
of '[':
|
||||||
|
if i < s.len and s[i+1] == '[':
|
||||||
|
inc i; next; continue
|
||||||
inc i
|
inc i
|
||||||
continue
|
while i < s.len and s[i] != ']':
|
||||||
|
incPattern
|
||||||
if s[i] == '[' and preChar != '\\':
|
pattern = pattern.strip()
|
||||||
inc i
|
if result.spans.len > 0:
|
||||||
while i < s.len and s[i] != ']':
|
if pattern == "/": result.closeLastStyle
|
||||||
preChar = s[i]
|
elif pattern == "reset": result.resetSpan
|
||||||
pattern.add s[i]
|
elif pattern.startswith('/'): result.closeStyle pattern
|
||||||
inc i
|
else: result.addToSpan pattern
|
||||||
|
else: result.newSpan pattern
|
||||||
if noColor:
|
resetPattern
|
||||||
inc i
|
|
||||||
continue
|
|
||||||
if pattern in ["reset","/"]:
|
|
||||||
result.add bbReset
|
|
||||||
addReset = false
|
|
||||||
else:
|
else:
|
||||||
for style in pattern.splitWhitespace():
|
next
|
||||||
if style in codeMap:
|
result.closeFinalSpan
|
||||||
addReset = true
|
|
||||||
result.add "\e[" & codeMap[style] & "m"
|
|
||||||
pattern = ""
|
|
||||||
inc i
|
|
||||||
else:
|
|
||||||
preChar = s[i]
|
|
||||||
result.add s[i]
|
|
||||||
inc i
|
|
||||||
|
|
||||||
if addReset:
|
proc `&`*(x: BbString, y: BbString): Bbstring =
|
||||||
result.add "\e[0m"
|
# there is probably a more efficient way to do this
|
||||||
|
bb(x.raw & y.raw)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- cli
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
import std/[strformat, parseopt]
|
import std/[strformat, parseopt]
|
||||||
const version = staticExec "git describe --tags --always --dirty=-dev"
|
const version = staticExec "git describe --tags --always --dirty=-dev"
|
||||||
let help = &"""
|
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!"
|
bbansi "[yellow] yellow text!"
|
||||||
|-> {bb"[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..."}
|
|-> {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() =
|
proc writeHelp() =
|
||||||
echo help
|
echo help
|
||||||
quit(QuitSuccess)
|
quit(QuitSuccess)
|
||||||
proc writeVersion() =
|
proc writeVersion() =
|
||||||
echo "bbansi version -> ", version
|
echo bb(&"[yellow]bbansi version[/][red] ->[/] [bold]{version}[/]")
|
||||||
quit(QuitSuccess)
|
quit(QuitSuccess)
|
||||||
var strArgs: seq[string]
|
var strArgs: seq[string]
|
||||||
var p = initOptParser()
|
var p = initOptParser()
|
||||||
|
@ -78,22 +160,20 @@ usage:
|
||||||
of cmdShortOption, cmdLongOption:
|
of cmdShortOption, cmdLongOption:
|
||||||
case key:
|
case key:
|
||||||
of "help", "h": writeHelp()
|
of "help", "h": writeHelp()
|
||||||
of "version","v": writeVersion()
|
of "version", "v": writeVersion()
|
||||||
else:
|
else:
|
||||||
echo bb"[red]ERROR[/]: unexpected option/value -> ", key, ", ", val
|
echo bb"[red]ERROR[/]: unexpected option/value -> ", key, ", ", val
|
||||||
echo "Option and value: ", key, ", ", val
|
echo "Option and value: ", key, ", ", val
|
||||||
|
|
||||||
of cmdArgument:
|
of cmdArgument:
|
||||||
strArgs.add key
|
strArgs.add key
|
||||||
|
if strArgs.len == 0:
|
||||||
|
echo help
|
||||||
|
quit(QuitSuccess)
|
||||||
|
for arg in strArgs:
|
||||||
|
echo arg.bb
|
||||||
|
|
||||||
if strArgs.len != 0:
|
echo "---------->"
|
||||||
for arg in strArgs:
|
echo "\e[31mRed Text\e[0m\nNext Line"
|
||||||
echo arg.bb
|
echo "[red]Red Text[/red]\nNext Line".bb
|
||||||
else:
|
echo "---------->"
|
||||||
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 "
|
|
||||||
|
|
|
@ -1,28 +1,58 @@
|
||||||
|
|
||||||
import strtabs
|
import std/[strtabs, strutils]
|
||||||
|
|
||||||
export strtabs
|
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 unittest
|
||||||
|
|
||||||
import bbansi
|
import bbansi
|
||||||
test "basic":
|
|
||||||
check "\e[31mRed Text\e[0m" == bb"[red]Red Text"
|
suite "basic":
|
||||||
check "No Style" == bb"No Style"
|
test "simple":
|
||||||
check "Unknown Style" == bb"[unknown]Unknown Style"
|
check "\e[31mRed Text\e[0m" == $bb"[red]Red Text"
|
||||||
check "\e[1m\e[31mBold Red Text\e[0m" == bb"[bold red]Bold Red Text"
|
check "\e[33mYellow Text\e[0m" == $bb"[yellow]Yellow Text"
|
||||||
check "\e[1m\e[31mBold Red Text\e[0mPlain Text" == bb"[bold red]Bold Red Text[reset]Plain Text"
|
check "\e[1m\e[31mBold Red Text\e[0m" == $bb"[bold red]Bold Red Text"
|
||||||
check "\e[1mBold\e[0m Not Bold" == bb"[bold]Bold[/] Not Bold"
|
|
||||||
# not sure how rich handles this
|
test "closing":
|
||||||
|
check "\e[1mBold\e[0m\e[1m\e[31m Bold Red\e[0m\e[1m Bold Only\e[0m" ==
|
||||||
test "escaped":
|
$bb"[bold]Bold[red] Bold Red[/red] Bold Only"
|
||||||
check "[red] ignored pattern" == bb"\[red] ignored pattern"
|
|
||||||
|
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