diff --git a/examples/README.md b/examples/README.md index 157ac7a..f623a42 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,7 @@ # Example Programs - [base64](base64/): A base64 encoding/decoding library. +- [colorful](colorful/): A DSL useful for rendering terminal colors. - [game](game/): An example game using raylib. - [http](http/http.tm): An HTTP library to make basic synchronous HTTP requests. - [ini](ini/ini.tm): An INI configuration file reader tool. diff --git a/examples/colorful/README.md b/examples/colorful/README.md new file mode 100644 index 0000000..2253431 --- /dev/null +++ b/examples/colorful/README.md @@ -0,0 +1,93 @@ +# Colorful Lang + +Colorful is a `lang` that lets you write colorful text for the terminal without +having to stress about managing state for color highlighting. + +## Grammar + +The grammar looks like this: + +``` +colorful <- ("@(at)" / "@(lparen)" / "@(rparen)" # Escapes + / "@(" attributes ":" colorful ")" # Colorful text + / .)* # Plain text + +attributes <- (attribute ("," attribute)*)? + +attribute <- color # Color defaults to foreground + / "fg=" color # Foreground color + / "bg=" color # Background color + / "ul=" color # Underline color + / "b" / "bold" + / "d" / "dim" + / "u" / "underline" + / "i" / "italic" + / "B" / "blink" + / "r" / "reverse" + # These are rarely supported by terminals: + / "fraktur" + / "frame" + / "encircle" + / "overline" + / "super" / "superscript" + / "sub" / "subscript" + +color <- "black" / "red" / "green" / "yellow" / "blue" / "magenta" / "cyan" / "white" + # All caps colors are "bright" colors (not always supported): + / "BLACK" / "RED" / "GREEN" / "YELLOW" / "BLUE" / "MAGENTA" / "CYAN" / "WHITE" + / "default" + / "#" 6 hex # Values 0x000000-0xFFFFFF + / "#" 3 hex # Values 0x000-0xFFF + / 1-3 digit # Values 0-255 +``` + +## Command Line Usage + +You can run `colorful` as a standalone executable to render colorful text with +ANSI escape sequences so it looks nice on a terminal. + +``` +colorful [--inplace] [file] [--by-line] +``` + +## Library Usage + +`colorful` can also be used as a Tomo library: + +```tomo +use colorful + +$Colorful" + @(blue:Welcome to the @(bold:party)!) + We have @(green,bold:colors)! +":print() +``` + +You can very easily introduce your own syntax highlighting for a custom DSL: + +```tomo +lang Markdown: + func Colorful(md:Markdown -> Colorful): + text := md.text:replace_all({ + $/@/="@(at)", + $/(/="@(lparen)", + $/)/="@(rparen)", + $/**{..}**/="@(b:\1)", + $/*{..}*/="@(i:\1)", + $/[?](?)/="@(blue,underline:\1)", + }) + return Colorful.without_escaping(text) + + func colorful(md:Markdown -> Colorful): + return $Colorful"$md" +... + +md := $Markdown" + This is [a link with **bold** inside](example.com)! +" +>> colorful := md:colorful() += $Colorful"This is @(blue,underline:a link with @(b:bold) inside)!" +>> colorful:for_terminal() += "$\e[mThis is $\e[4;34ma link with $\e[1mbold$\e[22m inside$\e[24;39m!" +colorful:print() +``` diff --git a/examples/colorful/colorful.tm b/examples/colorful/colorful.tm new file mode 100644 index 0000000..4840581 --- /dev/null +++ b/examples/colorful/colorful.tm @@ -0,0 +1,200 @@ +# Colorful language +HELP := " + colorful: A domain-specific language for writing colored text to the terminal + Usage: colorful [--by-line] [files...] +" + +CSI := "$\033[" + +enum Color(Default, Bright(color:Int16), Color8Bit(color:Int16), Color24Bit(color:Int32)): + func from_text(text:Text -> Color?): + if text:matches($/#{3-6 hex}/): + hex := text:from(2) + return none unless hex.length == 3 or hex.length == 6 + if hex.length == 3: + hex = hex[1]++hex[1]++hex[2]++hex[2]++hex[3]++hex[3] + n := Int32.parse("0x" ++ hex) or return none + return Color24Bit(n) + else if text:matches($/{1-3 digit}/): + n := Int16.parse(text) or return none + if n >= 0 and n <= 255: return Color8Bit(n) + else if text == "black": return Color.Color8Bit(0) + else if text == "red": return Color.Color8Bit(1) + else if text == "green": return Color.Color8Bit(2) + else if text == "yellow": return Color.Color8Bit(3) + else if text == "blue": return Color.Color8Bit(4) + else if text == "magenta": return Color.Color8Bit(5) + else if text == "cyan": return Color.Color8Bit(6) + else if text == "white": return Color.Color8Bit(7) + else if text == "default": return Color.Default + else if text == "BLACK": return Color.Bright(0) + else if text == "RED": return Color.Bright(1) + else if text == "GREEN": return Color.Bright(2) + else if text == "YELLOW": return Color.Bright(3) + else if text == "BLUE": return Color.Bright(4) + else if text == "MAGENTA": return Color.Bright(5) + else if text == "CYAN": return Color.Bright(6) + else if text == "WHITE": return Color.Bright(7) + return none + + func fg(c:Color -> Text): + when c is Color8Bit(color): + if color >= 0 and color <= 7: return "$(30+color)" + else if color >= 0 and color <= 255: return "38;5;$color" + is Color24Bit(hex): + if hex >= 0 and hex <= 0xFFFFFF: + return "38;2;$((hex >> 16) and 0xFF);$((hex >> 8) and 0xFF);$((hex >> 0) and 0xFF)" + is Bright(color): + if color <= 7: return "$(90+color)" + is Default: + return "39" + fail("Invalid foreground color: '$c'") + + func bg(c:Color -> Text): + when c is Color8Bit(color): + if color >= 0 and color <= 7: return "$(40+color)" + else if color >= 0 and color <= 255: return "48;5;$color" + is Color24Bit(hex): + if hex >= 0 and hex <= 0xFFFFFF: + return "48;2;$((hex >> 16) and 0xFF);$((hex >> 8) and 0xFF);$((hex >> 0) and 0xFF)" + is Bright(color): + if color <= 7: return "$(90+color)" + is Default: + return "49" + fail("Invalid background color: '$c'") + + func underline(c:Color -> Text): + when c is Color8Bit(color): + if color >= 0 and color <= 255: return "58;5;$color" + is Color24Bit(hex): + if hex >= 0 and hex <= 0xFFFFFF: + return "58;2;$((hex >> 16) and 0xFF);$((hex >> 8) and 0xFF);$((hex >> 0) and 0xFF)" + is Default: + return "59" + is Bright(color): + pass + fail("Invalid underline color: '$c'") + +struct TermState( + bold=no, dim=no, italic=no, underline=no, blink=no, + reverse=no, conceal=no, strikethrough=no, fraktur=no, frame=no, + encircle=no, overline=no, superscript=no, subscript=no, + bg=Color.Default, fg=Color.Default, underline_color=Color.Default, +): + func _toggle(sequences:&[Text], cur,new:Bool, apply,unapply:Text; inline): + if new and not cur: + sequences:insert(apply) + else if cur and not new: + sequences:insert(unapply) + + func _toggle2(sequences:&[Text], cur1,cur2,new1,new2:Bool, apply1,apply2,unapply:Text; inline): + return if new1 == cur1 and new2 == cur2 + if (cur1 and not new1) or (cur2 and not new2): # Gotta wipe at least one + sequences:insert(unapply) + cur1, cur2 = no, no # Wiped out + + if new1 and not cur1: + sequences:insert(apply1) + if new2 and not cur2: + sequences:insert(apply2) + + func apply(old,new:TermState -> Text): + sequences := &[:Text] + _toggle2(sequences, old.bold, old.dim, new.bold, new.dim, "1", "2", "22") + _toggle2(sequences, old.italic, old.fraktur, new.italic, new.fraktur, "3", "20", "23") + _toggle(sequences, old.underline, new.underline, "4", "24") + _toggle(sequences, old.blink, new.blink, "5", "25") + _toggle(sequences, old.reverse, new.reverse, "7", "27") + _toggle(sequences, old.conceal, new.conceal, "8", "28") + _toggle(sequences, old.strikethrough, new.strikethrough, "9", "29") + _toggle2(sequences, old.frame, old.encircle, new.frame, new.frame, "51", "52", "54") + _toggle(sequences, old.overline, new.overline, "53", "55") + _toggle2(sequences, old.subscript, old.subscript, new.superscript, new.superscript, "73", "74", "75") + + if new.bg != old.bg: + sequences:insert(new.bg:bg()) + + if new.fg != old.fg: + sequences:insert(new.fg:fg()) + + if new.underline_color != old.underline_color: + sequences:insert(new.underline_color:underline()) + + if sequences.length == 0: + return "" + return CSI ++ ";":join(sequences) ++ "m" + +lang Colorful: + func Colorful(text:Text -> Colorful): + text = text:replace_all({$/@/="@(at)", $/(/="@(lparen)", $/)/="@(rparen)"}) + return Colorful.without_escaping(text) + + func add_ansi_sequences(text:Text, prev_state:TermState -> Text): + if text == "lparen": return "(" + else if text == "rparen": return ")" + else if text == "@" or text == "at": return "@" + parts := ( + text:matches($/{0+..}:{0+..}/) or + return "@("++Colorful.for_terminal(Colorful.without_escaping(text), prev_state)++")" + ) + attributes := parts[1]:split($/{0+space},{0+space}/) + new_state := prev_state + for attr in attributes: + if attr:starts_with("fg="): + new_state.fg = Color.from_text(attr:from(4))! + else if attr:starts_with("bg="): + new_state.bg = Color.from_text(attr:from(4))! + else if attr:starts_with("ul="): + new_state.underline_color = Color.from_text(attr:from(4))! + else if color := Color.from_text(attr): + new_state.fg = color + else if attr == "b" or attr == "bold": + new_state.bold = yes + else if attr == "d" or attr == "dim": + new_state.dim = yes + else if attr == "i" or attr == "italic": + new_state.italic = yes + else if attr == "u" or attr == "underline": + new_state.underline = yes + else if attr == "B" or attr == "blink": + new_state.blink = yes + else if attr == "r" or attr == "reverse": + new_state.reverse = yes + else if attr == "fraktur": + new_state.fraktur = yes + else if attr == "frame": + new_state.frame = yes + else if attr == "encircle": + new_state.encircle = yes + else if attr == "overline": + new_state.overline = yes + else if attr == "super" or attr == "superscript": + new_state.superscript = yes + else if attr == "sub" or attr == "subscript": + new_state.subscript = yes + else: + fail("Invalid attribute: '$attr'") + + result := prev_state:apply(new_state) + result ++= parts[2]:map($/@(?)/, func(m:Match): Colorful.add_ansi_sequences(m.captures[1], new_state)) + result ++= new_state:apply(prev_state) + return result + + func for_terminal(c:Colorful, state=none:TermState -> Text): + cur_state := state or TermState() + result := c.text:map($/@(?)/, func(m:Match): Colorful.add_ansi_sequences(m.captures[1], cur_state)) + if not state: result = CSI ++ "m" ++ result + return result + + func print(c:Colorful, newline=yes): + say(c:for_terminal(), newline=newline) + +func main(files=[(/dev/stdin)], by_line=no): + for file in files: + if by_line: + for line in file:by_line() or exit("Couldn't read file: $file"): + colorful := Colorful.without_escaping(line) + colorful:print() + else: + colorful := Colorful.without_escaping(file:read() or exit("Couldn't read file: $file")) + colorful:print(newline=no) diff --git a/examples/colorful/test.colors b/examples/colorful/test.colors new file mode 100644 index 0000000..314b8b0 --- /dev/null +++ b/examples/colorful/test.colors @@ -0,0 +1,9 @@ +This is some text that has @(bold:@(red:c)@(yellow:o)@(green:l)@(cyan:o)@(blue:r)@(magenta:s))! + +@(fg=#aaf,b:You can have @(red:nested) color directives and stuff (even in +parens) will be handled @(i:right)) + +@(dim:The output @(bold:ANSI) sequences will be optimal, even if you have +nested stuff like bold and dim) + +(which is distinct from @(bold:BOLD) by itself)