StyledStrings

Note

The API for StyledStrings and AnnotatedStrings is considered experimental and is subject to change between Julia versions.

Styling

When working with strings, formatting and styling often appear as a secondary concern.

For instance, when printing to a terminal you might want to sprinkle ANSI escape sequences in the output, when outputting HTML styling constructs (<span style="...">, etc.) serve a similar purpose, and so on. It is possible to simply insert the raw styling constructs into the string next to the content itself, but it quickly becomes apparent that this is not well suited for anything but the most basic use cases. Not all terminals support the same ANSI codes, the styling constructs need to be painstakingly removed when calculating the width of already-styled content, and that's before you even get into handling multiple output formats.

Instead of leaving this headache to be widely experienced downstream, it is tackled head-on by the introduction of a special string type (AnnotatedString). This string type wraps any other AbstractString type and allows for formatting information to be applied to regions (e.g. characters 1 through to 7 are bold and red).

Regions of a string are styled by applying Faces (think "typeface") to them — a structure that holds styling information. As a convenience, faces in the global faces dictionary (e.g. shadow) can just be named instead of giving the Face directly.

Along with these capabilities, we also provide a convenient way for constructing AnnotatedStrings, detailed in Styled String Literals.

julia> using StyledStrings
julia> styled"{yellow:hello} {blue:there}""hello there"

Styling via AnnotatedStrings

Faces

The Face type

A Face specifies details of a typeface that text can be set in. It covers a set of basic attributes that generalize well across different formats, namely:

  • font
  • height
  • weight
  • slant
  • foreground
  • background
  • underline
  • strikethrough
  • inverse
  • inherit

For details on the particular forms these attributes take, see the Face docstring, but of particular interest is inherit as it allows you to inherit attributes from other Faces.

The global faces dictionary

To make referring to particular styles more convenient, there is a global Dict{Symbol, Face} that allows for Faces to be referred to simply by name. Packages can add faces to this dictionary via the addface! function, and the loaded faces can be easily customized.

Appropriate face naming

Any package registering new faces should ensure that they are prefixed by the package name, i.e. follow the format mypackage_myface. This is important for predictability, and to prevent name clashes.

Furthermore, packages should take care to use (and introduce) semantic faces (like code) over direct colours and styles (like cyan). This is helpful in a number of ways, from making the intent in usage more obvious, aiding composability, and making user customisation more intuitive.

There are two set of exemptions to the package-prefix rule:

  • the set of basic faces that are part of the default value of the faces dictionary
  • faces introduced by Julia's own standard library, namely JuliaSyntaxHighlighting

Basic faces

Basic faces are intended to represent a general idea that is widely applicable.

For setting some text with a certain attribute, we have the bold, light, italic, underline, strikethrough, and inverse faces.

There are also named faces for the 16 terminal colors: black, red, green, yellow, blue, magenta, cyan, white, bright_black/grey/gray, bright_red, bright_green, bright_blue, bright_magenta, bright_cyan, and bright_white.

For shadowed text (i.e. dim but there) there is the shadow face. To indicate a selected region, there is the region face. Similarly for emphasis and highlighting the emphasis and highlight faces are defined. There is also code for code-like text.

For visually indicating the severity of messages, the error, warning, success, info, note, and tip faces are defined.

Customisation of faces (Faces.toml)

It is good for the name faces in the global face dictionary to be customizable. Theming and aesthetics are nice, and it is important for accessibility reasons too. A TOML file can be parsed into a list of Face specifications that are merged with the pre-existing entry in the face dictionary.

A Face is represented in TOML like so:

[facename]
attribute = "value"
...

[package.facename]
attribute = "value"

For example, if the shadow face is too hard to read it can be made brighter like so:

[shadow]
foreground = "white"

On initialization, the config/faces.toml file under the first Julia depot (usually ~/.julia) is loaded.

Applying faces to a AnnotatedString

By convention, the :face attributes of a AnnotatedString hold information on the Faces that currently apply. This can be given in multiple forms, as a single Symbol naming a Faces in the global face dictionary, a Face itself, or a vector of either.

The show(::IO, ::MIME"text/plain", ::AnnotatedString) and show(::IO, ::MIME"text/html", ::AnnotatedString) methods both look at the :face attributes and merge them all together when determining the overall styling.

We can supply :face attributes to a AnnotatedString during construction, add them to the properties list afterwards, or use the convenient Styled String literals.

julia> str1 = Base.AnnotatedString("blue text", [(1:9, :face => :blue)])"blue text"
julia> str2 = styled"{blue:blue text}""blue text"
julia> str1 == str2true
julia> sprint(print, str1, context = :color => true)"\e[34mblue text\e[39m"
julia> sprint(show, MIME("text/html"), str1, context = :color => true)"<span style=\"color: #195eb3\">blue text</span>"

Styled String Literals

To ease construction of AnnotatedStrings with Faces applied, the styled"..." styled string literal allows for the content and attributes to be easily expressed together via a custom grammar.

Within a styled"..." literal, curly braces are considered special characters and must be escaped in normal usage (\{, \}). This allows them to be used to express annotations with (nestable) {annotations...:text} constructs.

The annotations... component is a comma-separated list of three types of annotations.

  • Face names
  • Inline Face expressions (key=val,...)
  • key=value pairs

Interpolation is possible everywhere except for inline face keys.

For more information on the grammar, see the extended help of the styled"..." docstring.

As an example, we can demonstrate the list of built-in faces mentioned above like so:

julia> println(styled"
The basic font-style attributes are {bold:bold}, {light:light}, {italic:italic},
{underline:underline}, and {strikethrough:strikethrough}.

In terms of color, we have named faces for the 16 standard terminal colors:
 {black:■} {red:■} {green:■} {yellow:■} {blue:■} {magenta:■} {cyan:■} {white:■}
 {bright_black:■} {bright_red:■} {bright_green:■} {bright_yellow:■} {bright_blue:■} {bright_magenta:■} {bright_cyan:■} {bright_white:■}

Since {code:bright_black} is effectively grey, we define two aliases for it:
{code:grey} and {code:gray} to allow for regional spelling differences.

To flip the foreground and background colors of some text, you can use the
{code:inverse} face, for example: {magenta:some {inverse:inverse} text}.

The intent-based basic faces are {shadow:shadow} (for dim but visible text),
{region:region} for selections, {emphasis:emphasis}, and {highlight:highlight}.
As above, {code:code} is used for code-like text.

Lastly, we have the 'message severity' faces: {error:error}, {warning:warning},
{success:success}, {info:info}, {note:note}, and {tip:tip}.

Remember that all these faces (and any user or package-defined ones) can
arbitrarily nest and overlap, {region,tip:like {bold,italic:so}}.")
 The basic font-style attributes are bold, light, italic,
 underline, and strikethrough.

 In terms of color, we have named faces for the 16 standard terminal colors:
         
         

 Since bright_black is effectively grey, we define two aliases for it:
 grey and gray to allow for regional spelling differences.

 To flip the foreground and background colors of some text, you can use the
 inverse face, for example: some inverse text.

 The intent-based basic faces are shadow (for dim but visible text),
 region for selections, emphasis, and highlight.
 As above, code is used for code-like text.

 Lastly, we have the 'message severity' faces: error, warning,
 success, info, note, and tip.

 Remember that all these faces (and any user or package-defined ones) can
 arbitrarily nest and overlap, like so.

API reference

StyledStrings.StyledMarkup.@styled_strMacro
@styled_str -> AnnotatedString

Construct a styled string. Within the string, {<specs>:<content>} structures apply the formatting to <content>, according to the list of comma-separated specifications <specs>. Each spec can either take the form of a face name, an inline face specification, or a key=value pair. The value must be wrapped by {...} should it contain any of the characters ,=:{}.

String interpolation with $ functions in the same way as regular strings, except quotes need to be escaped. Faces, keys, and values can also be interpolated with $.

Example

styled"The {bold:{italic:quick} {(foreground=#cd853f):brown} fox} jumped over the {link={https://en.wikipedia.org/wiki/Laziness}:lazy} dog"

Extended help

This macro can be described by the following EBNF grammar:

styledstring = { styled | interpolated | escaped | plain } ;

specialchar = '{' | '}' | '$' | '\"' ;
anychar = [\u0-\u1fffff] ;
plain = { anychar - specialchar } ;
escaped = '\\', specialchar ;

interpolated = '$', ? expr ? | '$(', ? expr ?, ')' ;

styled = '{', ws, annotations, ':', content, '}' ;
content = { interpolated | plain | escaped | styled } ;
annotations = annotation | annotations, ws, ',', ws, annotation ;
annotation = face | inlineface | keyvalue ;
ws = { ' ' | '\t' | '\n' } ; (* whitespace *)

face = facename | interpolated ;
facename = [A-Za-z0-9_]+ ;

inlineface = '(', ws, [ faceprop ], { ws, ',', faceprop }, ws, ')' ;
faceprop = [a-z]+, ws, '=', ws, ( [^,)]+ | interpolated) ;

keyvalue = key, ws, '=', ws, value ;
key = ( [^\0${}=,:], [^\0=,:]* ) | interpolated ;
value = simplevalue | curlybraced | interpolated ;
curlybraced = '{' { escaped | plain } '}' ;
simplevalue = [^${},:], [^,:]* ;

An extra stipulation not encoded in the above grammar is that plain should be a valid input to unescape_string, with specialchar kept.

The above grammar for inlineface is simplified, as the actual implementation is a bit more sophisticated. The full behaviour is given below.

faceprop = ( 'face', ws, '=', ws, ( ? string ? | interpolated ) ) |
           ( 'height', ws, '=', ws, ( ? number ? | interpolated ) ) |
           ( 'weight', ws, '=', ws, ( symbol | interpolated ) ) |
           ( 'slant', ws, '=', ws, ( symbol | interpolated ) ) |
           ( ( 'foreground' | 'fg' | 'background' | 'bg' ),
               ws, '=', ws, ( simplecolor | interpolated ) ) |
           ( 'underline', ws, '=', ws, ( underline | interpolated ) ) |
           ( 'strikethrough', ws, '=', ws, ( bool | interpolated ) ) |
           ( 'inverse', ws, '=', ws, ( bool | interpolated ) ) |
           ( 'inherit', ws, '=', ws, ( inherit | interpolated ) ) ;

nothing = 'nothing' ;
bool = 'true' | 'false' ;
symbol = [^ ,)]+ ;
hexcolor = ('#' | '0x'), [0-9a-f]{6} ;
simplecolor = hexcolor | symbol | nothing ;

underline = nothing | bool | simplecolor | underlinestyled;
underlinestyled = '(', ws, ('' | nothing | simplecolor | interpolated), ws,
                  ',', ws, ( symbol | interpolated ), ws ')' ;

inherit = ( '[', inheritval, { ',', inheritval }, ']' ) | inheritval;
inheritval = ws, ':'?, symbol ;
StyledStrings.StyledMarkup.styledFunction
styled(content::AbstractString) -> AnnotatedString

Construct a styled string. Within the string, {<specs>:<content>} structures apply the formatting to <content>, according to the list of comma-separated specifications <specs>. Each spec can either take the form of a face name, an inline face specification, or a key=value pair. The value must be wrapped by {...} should it contain any of the characters ,=:{}.

This is a functional equivalent of the @styled_str macro, just without interpolation capabilities.

StyledStrings.FaceType

A Face is a collection of graphical attributes for displaying text. Faces control how text is displayed in the terminal, and possibly other places too.

Most of the time, a Face will be stored in the global faces dicts as a unique association with a face name Symbol, and will be most often referred to by this name instead of the Face object itself.

Attributes

All attributes can be set via the keyword constructor, and default to nothing.

  • height (an Int or Float64): The height in either deci-pt (when an Int), or as a factor of the base size (when a Float64).
  • weight (a Symbol): One of the symbols (from faintest to densest) :thin, :extralight, :light, :semilight, :normal, :medium, :semibold, :bold, :extrabold, or :black. In terminals any weight greater than :normal is displayed as bold, and in terminals that support variable-brightness text, any weight less than :normal is displayed as faint.
  • slant (a Symbol): One of the symbols :italic, :oblique, or :normal.
  • foreground (a SimpleColor): The text foreground color.
  • background (a SimpleColor): The text background color.
  • underline, the text underline, which takes one of the following forms:
    • a Bool: Whether the text should be underlined or not.
    • a SimpleColor: The text should be underlined with this color.
    • a Tuple{Nothing, Symbol}: The text should be underlined using the style set by the Symbol, one of :straight, :double, :curly, :dotted, or :dashed.
    • a Tuple{SimpleColor, Symbol}: The text should be underlined in the specified SimpleColor, and using the style specified by the Symbol, as before.
  • strikethrough (a Bool): Whether the text should be struck through.
  • inverse (a Bool): Whether the foreground and background colors should be inverted.
  • inherit (a Vector{Symbol}): Names of faces to inherit from, with earlier faces taking priority. All faces inherit from the :default face.
StyledStrings.addface!Function
addface!(name::Symbol => default::Face)

Create a new face by the name name. So long as no face already exists by this name, default is added to both FACES.default and (a copy of) to FACES.current, with the current value returned.

Should the face name already exist, nothing is returned.

Examples

julia> addface!(:mypkg_myface => Face(slant=:italic, underline=true))
Face (sample)
         slant: italic
     underline: true
StyledStrings.withfacesFunction
withfaces(f, kv::Pair...)
withfaces(f, kvpair_itr)

Execute f with FACES.current temporarily modified by zero or more :name => val arguments kv, or kvpair_itr which produces kv-form values.

withfaces is generally used via the withfaces(kv...) do ... end syntax. A value of nothing can be used to temporarily unset a face (if it has been set). When withfaces returns, the original FACES.current has been restored.

Examples

julia> withfaces(:yellow => Face(foreground=:red), :green => :blue) do
           println(styled"{yellow:red} and {green:blue} mixed make {magenta:purple}")
       end
red and blue mixed make purple
StyledStrings.SimpleColorType
struct SimpleColor

A basic representation of a color, intended for string styling purposes. It can either contain a named color (like :red), or an RGBTuple which is a NamedTuple specifying an r, g, b color with a bit-depth of 8.

Constructors

SimpleColor(name::Symbol)  # e.g. :red
SimpleColor(rgb::RGBTuple) # e.g. (r=1, b=2, g=3)
SimpleColor(r::Integer, b::Integer, b::Integer)
SimpleColor(rgb::UInt32)   # e.g. 0x123456

Also see tryparse(SimpleColor, rgb::String).

Base.parseMethod
parse(::Type{SimpleColor}, rgb::String)

An analogue of tryparse(SimpleColor, rgb::String) (which see), that raises an error instead of returning nothing.

Base.tryparseMethod
tryparse(::Type{SimpleColor}, rgb::String)

Attempt to parse rgb as a SimpleColor. If rgb starts with # and has a length of 7, it is converted into a RGBTuple-backed SimpleColor. If rgb starts with a-z, rgb is interpreted as a color name and converted to a Symbol-backed SimpleColor.

Otherwise, nothing is returned.

Examples

julia> tryparse(SimpleColor, "blue")
SimpleColor(blue)

julia> tryparse(SimpleColor, "#9558b2")
SimpleColor(#9558b2)

julia> tryparse(SimpleColor, "#nocolor")
Base.mergeMethod
merge(initial::Face, others::Face...)

Merge the properties of the initial face and others, with later faces taking priority.