Usage Examples
There are some examples on this page that don't quite look as they should, see Documenter.jl#2488 for more information and potential improvements to this situation.
With flexibility and composability as major considerations in the design of StyledStrings, it is easy to describe the capabilities of this system while failing to actually convey what it can accomplish. With this in mind, examples can be particularly useful to show how it can be used.
A styled string can be constructed manually, but the styled"..."
literal is almost always a nicer option. We can see what a manual construction would involve by extracting the string and annotation parts of a AnnotatedString
.
julia> using StyledStrings
julia> str = styled"{yellow:hello} {blue:there}"
"hello there"
julia> (String(str), annotations(str))
("hello there", @NamedTuple{region::UnitRange{Int64}, label::Symbol, value}[@NamedTuple{region::UnitRange{Int64}, label::Symbol, value}((1:5, :face, :yellow)), @NamedTuple{region::UnitRange{Int64}, label::Symbol, value}((7:11, :face, :blue))])
Most of the examples here will show AnnotatedString
in a terminal/REPL context, however they can be trivially adapted to produce HTML with show(::IO, ::MIME"text/plain", ::AnnotatedString)
, and other packages wanting do deal with styled content in other contexts will likely find integration with StyledStrings worthwhile.
Basic face application and colouring
As an end-user or a package-author, adding color is one of if not the most frequent application of styling. The default set of colors has the eight ANSI colors, and if we're to be brutally honest, black
/white
are just shades, leaving us with:
julia> styled"{red:■} {green:■} {yellow:■} {blue:■} {magenta:■} {cyan:■}"
"■ ■ ■ ■ ■ ■"
along with "bright" variants
julia> styled"{bright_red:■} {bright_green:■} {bright_yellow:■} \ {bright_blue:■} {bright_magenta:■} {bright_cyan:■}"
"■ ■ ■ ■ ■ ■"
Adding new faces
This seems somewhat limited, because it is. This is only the default set of colors though. It is important to note that the way the color red
is implemented is by having it name a Face(foreground=:red)
value. The ANSI printer knows to handle the ANSI named colors specially, but you can create more "named colors" simply by adding new faces.
julia> StyledStrings.addface!(:orange => StyledStrings.Face(foreground = 0xFF7700))
StyledStrings.Face (sample) foreground: ■ #ff7700
julia> styled"{orange:this is orange text}"
"this is orange text"
The face name orange
is used here as an example, but this would be inappropriate for a package to introduce as it's missing the packagename_
prefix. This is important for predictability, and to prevent name clashes.
The fact that named colors are implemented this way also allows for other nice conveniences. For example, if you wanted a more subtle version of the warning styling, you could print text with the underline color set to the warning foreground color.
julia> styled"{(underline=warning):this is some minor/slight warning text}"
"this is some minor/slight warning text"
julia> styled"A very major {(fg=error,bg=warning),bold:warning!}"
"A very major warning!"
Note that when using faces, it is generally recommended to focus on the semantic intent, not the specific styling (cyan
, bold
, etc.), introducing new faces when there is no pre-existing semantically appropriate face.
Inline face attributes
Should you want to use a particular color just once, the foreground
/fg
and background
/bg
inline face attributes can be set to hex codes.
julia> styled"{(fg=#4063d8):ju}{(fg=#389826):l}{(fg=#cb3c33):i}{(fg=#9558b2):a}"
"julia"
This works just as well with named colours.
julia> styled"Since {orange:orange} is a face with a foreground, \ you can use it as a {(fg=orange):foreground color}."
"Since orange is a face with a foreground, you can use it as a foreground color."
All the other Face
attributes can be set similarly.
Considerations when creating and reusing faces
It is recommended that package authors create faces with a focus on the semantic meaning they wish to impart, and then consider what styling suits. For example, say that a hypothetical package foobar
wants to mark something as important. Creating a named face allows for it to be re-used across the codebase and allows the styling everywhere its used to be updated by only changing the line declaring it. foobar_important
would be an appropriate name for such a face.
julia> StyledStrings.addface!(:foobar_important => StyledStrings.Face(weight = :bold, inherit = :emphasis))
StyledStrings.Face (sample) weight: bold inherit: emphasis(*)
julia> styled"this is some {foobar_important:rather important} content"
"this is some rather important content"
Other packages that interact with foobar
can also re-use the foobar_important
face for consistent styling. This is possible even for packages that don't have foobar
as a direct dependency, as faces that don't exist are just ignored. Consider this styled content as an example:
julia> styled"{info,foobar_important,baz_important:some text}"
"some text"
The styling of "some text"
will be based only on info
if neither foobar_important
or baz_important
are defined. Since foobar_important
is defined, after applying the attributes of info
, the attributes of foobar_important
are applied to "some text"
overwriting any attributes set by info
. Should bar_important
be defined in the future, any attributes it sets will override foobar_important
and info
. Put more simply, the last face mentioned "wins".
The silent ignoring of undefined faces is important in making it so that it's known if a styled"..."
string will cause errors when printed is known at compile-time instead of runtime.
User-customisation of faces
Naming faces also allows for convenient customisation. Once foobar_important
is defined, a user can change how it is styled in their faces.toml
.
[foobar.important]
italic = true
User-customisation is particularly important when using color, as it allows people with color-blindness or other neuro-ophthalmological abnormalities to make text easier to read/distinguish.
Application-customisation of faces
Named faces can also be customised on-the-fly in certain printing contexts created by withfaces
.
julia> StyledStrings.withfaces(:foobar_important => :tip) do println(styled"Sometimes you might want {foobar_important:some text} to look different") end
Sometimes you might want some text to look different
julia> StyledStrings.withfaces(:log_info => [:magenta, :italic]) do @info "Hello there" end
[ Info: Hello there
This feature can be used to for example change the default colors to follow a certain color theme when generating HTML output.
julia> StyledStrings.withfaces(:green => StyledStrings.Face(foreground = 0xb7ba25), :yellow => StyledStrings.Face(foreground = 0xfabc2e), :magenta => StyledStrings.Face(foreground = 0xd2859a)) do str = styled"Sometimes you might want {green:different} {yellow:shades} of {magenta:colors}." println(str, "\n") show(stdout, MIME("text/html"), str) end
Sometimes you might want different shades of colors. Sometimes you might want <span style="color: #b7ba25">different</span> <span style="color: #fabc2e">shades</span> of <span style="color: #d2859a">colors</span>.
Composing and interpolating styled content
As you work with more styled content, the ability to compose styled content and styling information is rather useful. Helpfully, StyledStrings allows for interpolation of both content and attributes.
julia> small_rainbow = (:red, :yellow, :green, :blue, :magenta)
(:red, :yellow, :green, :blue, :magenta)
julia> color = join([styled"{$f:$c}" for (f, c) in tuple.(small_rainbow, collect("color"))])
"color"
julia> styled"It's nice to include $color, and it composes too: {bold,inverse:$color}"
"It's nice to include color, and it composes too: color"
Sometimes it's useful to compose a string incrementally, or interoperate with other IO
-based code. For these use-cases, the AnnotatedIOBuffer
is very handy, as you can read
an AnnotatedString
from it.
julia> aio = AnnotatedIOBuffer()
AnnotatedIOBuffer(0 bytes, 0 annotations)
julia> typ = Int
Int64
julia> print(aio, typ)
julia> while typ != Any # We'll pretend that `supertypes` doesn't exist. typ = supertype(typ) print(aio, styled" {bright_red:<:} $typ") end
julia> read(seekstart(aio), AnnotatedString)
"Int64 <: Signed <: Integer <: Real <: Number <: Any"
StyledStrings adds a specialised printstyled
method printstyled(::AnnotatedIOBuffer, ...)
that means that you can pass an AnnotatedIOBuffer
as IO to "legacy" code written to use printstyled
, and extract all the styling as though it had used styled"..."
macros.
julia> aio = AnnotatedIOBuffer()
ERROR: UndefVarError: `AnnotatedIOBuffer` not defined in `Main` Suggestion: check for spelling errors or missing imports. Hint: a global variable of this name also exists in StyledStrings.
julia> printstyled(aio, 'c', color=:red)
ERROR: UndefVarError: `aio` not defined in `Main` Suggestion: check for spelling errors or missing imports.
julia> printstyled(aio, 'o', color=:yellow)
ERROR: UndefVarError: `aio` not defined in `Main` Suggestion: check for spelling errors or missing imports.
julia> printstyled(aio, 'l', color=:green)
ERROR: UndefVarError: `aio` not defined in `Main` Suggestion: check for spelling errors or missing imports.
julia> printstyled(aio, 'o', color=:blue)
ERROR: UndefVarError: `aio` not defined in `Main` Suggestion: check for spelling errors or missing imports.
julia> printstyled(aio, 'r', color=:magenta)
ERROR: UndefVarError: `aio` not defined in `Main` Suggestion: check for spelling errors or missing imports.
julia> read(seekstart(aio), AnnotatedString)
ERROR: UndefVarError: `aio` not defined in `Main` Suggestion: check for spelling errors or missing imports.
julia> read(seekstart(aio), String)
ERROR: UndefVarError: `aio` not defined in `Main` Suggestion: check for spelling errors or missing imports.