Prospective changes to AnnotatedString and StyledStrings

A request for feedback and testing for a large piece of work I’ve undertaken

Background

The StyledStrings was first conceptualised in June 2021, PR’d in May 2023, merged in Oct 2023, and shipped in Julia v1.11.

Since then, the initial design has undergone several rounds of refinement. Lilith has been fantastic helping hash out some of the particular semantics, Sukera helped me use Supposition.jl’s PBT to make the styled""" markup syntax parsing robust, Kristoffer has identified and implemented a handful of performance wins, and most recently, Cody has been a big help in working out how to work around the Base/stdlib split and thorny invalidations caused by it.

While all of these improvements have been valuable additions, time has also revealed which of the decisions made in the original design/implementation have caused the biggest headaches and what compromises have struck a poor balance in hindsight.

I’ve recently pushed some work that’s been discussed on-and-off with various people to a viable state, to address all of the headaches and shortcomings that have emerged as major annoyances/impediments that I’m (a) aware of, and (b) feel confident to handle. This has taken a while in large part because I’ve been waiting for a few key design decisions to settle and feel “right” in my mind.

I’m now very interested in getting feedback on the proposed changes, and encouraging anybody interested to take the WIP branch for a spin and see what you think in practice/if you can find anything I’ve overlooked.

The headaches and shortcomings

  • Piracy with the dispatch on printing/display of AnnotatedString (defined in Base, implemented in StyledStrings)
  • The AnnotatedString behaviour
  • It not being possible for any other package to define its own annotation type with custom display/printing
  • A lack of namespacing
  • The Any value type in annotations (AnnotatedString/AnnotatedChar/AnnotatedIO)
  • No validation of named faces (styled"{bluue:text}" is silently unstyled)
  • Long prefixes when using custom faces within a package (e.g. REPL_History_search_unselected)
  • Consistent face name prefixes relying on package authors always following the recommendations (in other words: inconsistent face naming)
  • The use of Faces directly mixed with the use of Symbols to refer to named faces (and all the Union{Symbol, Face} code that results)
  • The inability to remap faces, except when displaying (withfaces)
  • The inability to unset a property of a face in user configuration (only set it to a different value)

In the end, about half of these items are collectively addressed in a suite of tightly connected changes that I’m collectively terming “Face primacy”. More on that later.

Now, onto the solutions/proposed changes.

Oh, by the way, everything here is fully backwards-compatible, up to Annotated{String,Char,IOBuffer} gaining an extra type parameter.

Parameterising the value of annotations

To remove the value::Any from the various annotated types, we change it to value::V where V. By itself, this wouldn’t be terribly helpful since we’d then have StyledStrings using a combination of face names (Symbols) and Face values: value::Symbol, value::Face, and value::Union{Symbol, Face}. As well as this seeming likely to make inference more complex, the use of Symbol prevents the StyledStrings display methods from being based solely on StyledStrings-defined types, which is what we need to avoid piracy.

Hybrid-interned Faces, instead of Symbols

With value-parameterised annotations, it would be simplest and nicest to just have value::Face, but eliminating Symbol-named Faces both makes the Public API much more inconvenient, and makes some detailes of the implementation very tricky (colours are implemented as named Faces, without names there is no “red” foreground/background).

The neat solution to this I’ve landed on is naming Faces with Faces. Specifically, by:

  1. Renaming the immutable Face struct
  2. Creating a mutable wrapper type
  3. Interning Faces by name (into an IdDict)
  4. Creating a convenience for accessing/interning faces: face"<name>"

This lets us drop the use of :red to refer to a particular Face (Symbol => Face), and instead use face"red" (Face => Face). Customisations etc. are referenced against that particular Face, instead of the name.

This is integrated into the styled""" macro, so that styled"{red:text}" uses the new interning system.

Note: there’s more going on here than just interning (hence the “hybrid”), as you’ll see in the next section.

Old Sample

importantface = :emphasis

astr = styled"{$importantface:oh hey there!}"

withfaces(:emphasis => :magenta) do
   println(astr)
end

New Sample

importantface = face"emphasis"

astr = styled"{$importantface:oh hey there!}" # Unchanged

withfaces(face"emphasis" => face"magenta") do
   println(astr)
end

Namespaces and palettes

Currently, all named faces are put in a shared global dictionary, where packages have to specify the full names set/used. This is done for four reasons:

  1. Having full names makes mapping user configuration straightforward
  2. I didn’t think of a nice way to do namespacing
  3. Nobody suggested a good way of doing namespacing
  4. Over in Emacs-land, this is what’s done, and everybody adheres strictly to the conventions (it might help that in Emacs, if you try to define a face without your package prefix, you get a warning)

After talking about the design with Cody, I’ve now got a system for both module-level face namespaces, and named collections of faces — palettes. If you hadn’t guessed already, the module namespaces are just palettes with a predictable name.

The public API for this consists of three new macros:

  • @defpalette! for creating a palette
  • @registerpalette! (placed within __init___) for performing some work at runtime (needed to make user customisation work, along with light/dark theming)
  • @usepalettes! for bringing in faces from certain palettes (think using). Note that this may be used exactly once per module.

Palettes affect how face"<name>" looks up the Face belonging to <name>, letting you skip package prefixes. Instead of :markdown_heading you can call @usepalletes! Markdown and then do face"heading". Dot-access also works (and allows you to reference different faces with the same name from different modules): face"Markdown.heading".

Use of @defpalette!, @usepalettes!, or face"" opts you into a post-Face primacy API, where all faces looked up (by face"" or styled"") must be defined in one of the palettes used.

This all works by having @defpalette! store the definitions in a special named tuple accessed by @usepalettes! and face"". This allows for faces to be looked up at compile (macroexpansion) time instead of runtime. To preserve runtime behaviour (backwards compatibility, the ability to customise faces, etc.) we then intern these faces with @registerpalette! during __init__.

This is the “hybrid” in “hybrid Face interning”. Named Faces are created at compile-time, and passed around/referenced between modules at compile time, and then interned at runtime. This removes the need for addfaces!.

Old Sample

module MyColours

using StyledStrings

const FACES = (
    :MyColours_gold => Face(foreground = 0xffd700),
    :MyColours_lavender => Face(foreground = 0xe6e6fa),
    :MyColours_salmon => Face(foreground = 0xfa8072),
    # ...
)

__init__() = foreach(addface!, FACES)

end

# --

module MyTables

using StyledStrings
using MyColours

const FACES = (
    :MyTables_header => Face(inherit = :MyColours_lavender, bold = true),
    :MyTables_row => Face(inherit = :MyColours_salmon),
    :MyTables_rowsep => Face(inherit = :MyColours_gold),
    # ...
)

__init__() = foreach(addface!, FACES)

function printtable(header::String, rows::Vector{String}, width::Int)
    println(styled"{MyTables_header:$header}\n{MyTables_rowsep:$('-'^width)}")
    for row in rows
        println(styled"{MyTables_row:$row}")
    end
end

end

New Sample

module MyColours

using StyledStrings

@defpalette! begin
    gold = Face(foreground = 0xffd700)
    lavender = Face(foreground = 0xe6e6fa)
    salmon = Face(foreground = 0xfa8072)
    # ...
end

__init__() = @registerpalette!

end

# --

module MyTables

using StyledStrings
using MyColours

@usepalettes! MyColours

@defpalette! begin
    header = Face(inherit = lavender, bold = true)
    row = Face(inherit = salmon)
    rowsep = Face(inherit = gold)
end

__init__() = @registerpalette!

function printtable(header::String, rows::Vector{String}, width::Int)
    println(styled"{header:$header}\n{rowsep:$('-'^width)}")
    for row in rows
        println(styled"{row:$row}")
    end
end

end

Remapping faces

For backwards compatibility with the hybrid face interning system, we needed the ability to remap faces resolved at compile time to a different face at runtime. After doing this work, it was trivial to introduce a new withfaces-like context wrapper that applies to face construction rather than display.

For example,

mapstr = remapfaces(face"red" => face"blue") do
    styled"some {red:important} text"
end

produces styled"some {blue:important} text".

Unsetting faces

In your faces.toml, you can now set attributes to "default" to unset that attribute.

I’m considering renaming the special value string to "unset".

I’m planning on extending this to families/palettes, e.g. Markdown = "unset".

Bonus: adaptive colouring and colour blending

Since I’m doing all this writing, I’ll also mention a separate new feature that I’m quite excited for: the ability to define light/dark face variants and also detect terminal colours and blend faces at runtime to adapt to the colour scheme the user sees.

Light/dark face variants can be defined like so:

@defpalette! begin
    region = Face(background=0x636363)
    region.light = Face(background=0xaaaaaa)
    region.dark = region => Face(background=0x363636)
end

The 16 ANSI colours are also read from the current terminal (when possible), making it possible to adjust styling to match the user’s terminal. This lets us break beyond the limits of 16-bit colour without any compromises, letting us do things like add pretty and customisable (and hence accessible) region highlights.

NB: please pretend you see face"background" instead of :background, these screenshots are a little out of date

Status

As of writing, this has all been implemented, and existing tests are passing. There’s a ~3000 loc PR to StyledStrings I’d love to get a code review on if anybody’s up for the task, and a much smaller ~300 loc WIP PR to the main Julia repo (#60527).

There are some minor issues blocking CI currently (apparently depwarn is an error in Julia CI?), and I’d like to write a bunch of new tests for the new functionality, but it’s all implemented at this point, and seemingly working well :crossed_fingers:.


There’s some more I’d write about, but this is already much longer than I anticipated, and it’s much later than I’d like, so I’ll just call it here for now :slight_smile: I’m very happy to clarify details, and go into depth on aspects of the design (e.g. topologically sorting face dependencies within a palette to allow for order-independent definition).

23 Likes

For another angle on this (and in case I can interest anyone in some proofreading), I’ve drafted the relevant changes to the documentation.

The draft

Note: this will look a bit funny because it hasn’t been processed by Documenter. Hence the @ref/@id links, and example blocks that are supposed to be executed to produce the output content shown in the docs.


[StyledStrings](@id stdlib-styledstrings)

CurrentModule = StyledStrings
DocTestSetup = quote
    using StyledStrings
end

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

[Styling](@id stdlib-styledstrings-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](@ref Base.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 [Face](@ref StyledStrings.Face)s
(think “typeface”) to them — a structure that holds styling information. As a
convenience, faces can be named (e.g. shadow) and then referenced instead of
giving the [Face](@ref StyledStrings.Face) directly.

Along with these capabilities, we also provide a convenient way for constructing
[AnnotatedString](@ref Base.AnnotatedString)s, detailed in [Styled String
Literals](@ref stdlib-styledstring-literals).

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

[Annotated Strings](@id man-annotated-strings)

It is sometimes useful to be able to hold metadata relating to regions of a
string. A [AnnotatedString](@ref Base.AnnotatedString) wraps another string and
allows for regions of it to be annotated with labelled values (:label => value).
All generic string operations are applied to the underlying string. However,
when possible, styling information is preserved. This means you can manipulate a
[AnnotatedString](@ref Base.AnnotatedString) —taking substrings, padding them,
concatenating them with other strings— and the metadata annotations will “come
along for the ride”.

This string type is fundamental to the [StyledStrings stdlib](@ref
stdlib-styledstrings), which uses :face-labelled annotations to hold styling
information, but arbitrary textual metadata can also be held with the string,
such as part of speech labels.

When concatenating a [AnnotatedString](@ref Base.AnnotatedString), take care
to use [annotatedstring](@ref StyledStrings.annotatedstring) instead of
string if you want to keep the string annotations. The annotations of
a [AnnotatedString](@ref Base.AnnotatedString) can be accessed and modified
via the [annotations](@ref StyledStrings.annotations) and [annotate!](@ref
StyledStrings.annotate!) functions.

julia> str = AnnotatedString("hello there", [(1:5, :pos, :interjection), (7:11, :pos, :pronoun)])
"hello there"

julia> lpad(str, 14)
"   hello there"

julia> typeof(lpad(str, 7))
AnnotatedString{String, Symbol}

julia> str2 = AnnotatedString(" julia", [(2:6, :pos, :noun)])
" julia"

julia> str3 = Base.annotatedstring(str, str2)
"hello there julia"

julia> Base.annotations(str3)
3-element Vector{@NamedTuple{region::UnitRange{Int64}, label::Symbol, value::Symbol}}:
 (region = 1:5, label = :pos, value = :interjection)
 (region = 7:11, label = :pos, value = :pronoun)
 (region = 13:17, label = :pos, value = :noun)

julia> str1 * str2 == str3 # *-concatenation works
true

Note that here the AnnotatedString type parameters depend on both the string
being wrapped, as well as the type of the annotation values.

!!! compat “Julia 1.14”
The use of a type parameter for annotation values was introduced with Julia 1.14.

[Faces](@id stdlib-styledstrings-faces)

Styles are specified through the Face type, which is designed to represent a common set of useful typeface attributes across multiple mediums (e.g. Terminals, HTML renderers, and LaTeX/Typst). Suites of faces can be defined in packages, conveniently referenced by name, reused in other packages, and [customised by users](@ref stdlib-styledstrings-face-toml).

The Face type

A [Face](@ref StyledStrings.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](@ref StyledStrings.Face) docstring. However, it is worth drawing
particular attention to inherit, as it allows us to inherit attributes from other
[Face](@ref StyledStrings.Face)s.

The attributes that specify color (foreground, background, and optionally underline) may either specify a 24-bit RGB colour (e.g. #4063d8) or name another face whose foreground colour is used. In this way a set of named colors can be created by defining faces with foreground colors.

!!! compat “Julia 1.14”
Direct use of Faces as colors was introduced with Julia 1.14.

juliablue = Face(foreground = 0x4063d8)  # Hex-style colour
juliapurple = Face(foreground = (r = 0x95, g = 0x58, b = 0xb2)) # RGB tuple
Face(foreground = juliablue, underline = juliapurple)

Notice that the foreground and underline color are labelled as “unregistered”.
While assigning Faces to variables is fine for one-off use, it is often more
useful to create named faces that you can reuse, and other packages can build on.

[Named faces and face""](@id stdlib-styledstrings-named-faces)

StyledStrings comes with 32 named faces. The default face fully specifies all
attributes, and represents the expected “default state” of displayed text. The
foreground and background faces give the default foreground and background
of the default face. For convenience, the faces bold, light, italic,
underline, strikethrough, and inverse are defined to save the trouble of
frequently creating Faces just to set the relevant attribute.

We then have 16 faces for the 8 standard terminal colors, and their bright
variants: 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. These Faces, together
with foreground and background are special in that they are their own
foreground. This is unique to these 18 faces, as their particular color will
depend on the terminal they are shown in.

A small collection of semantic faces are also defined, for common uses. 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, key for keybindings, and link for links. For
visually indicating the severity of messages, the error, warning (with the
alias: warn), success, info, note, and tip faces are defined.

These faces can be easily retrieved using the [face""](@ref @face_str) macro.
For instance, face"blue" returns the Face that defines the colour blue, and
face"emphasis" returns the Face defined for marking emphasised text.

face"default"
face"highlight"
face"tip"

!!! compat “Julia 1.14”
The face"" macro and current face naming system was introduced with Julia 1.14.
In Julia 1.11 through to 1.13 (and the backwards compatibility package registered in General)
faces are named with Symbols, and a global faces dictionary is used. The old API is still
supported for backwards compatibility, but it is strongly recommended to support the new
system by putting palette definitions behind a version gate if compatibility with older library
versions is required.

[Palettes](@id stdlib-styledstrings-face-palettes)

While the base faces provided are often enough for basic styling, they convey
little semantic meaning. Instead of a package say using face"bold" or an
anonymous Face for table headers it would be more appropriate to create a
named face for each distinct purpose. This benefits three sets of people:

  • Package authors: as it’s much easier to implement readable code and consistent styling by naming the styles you use
  • The ecosystem: as it allows for style reuse across packages
  • End users: as named faces allow for customisation (more on that later)

!!! tip “Effective use of named faces”
It is strongly recommended that packages should take care to use and introduce
semantic faces (like code and table_header) over direct colors and styles (like cyan).

Sets of named faces are created with the @defpalette! macro. Simply
wrap a series of <name> = Face(...) statements in a begin ... end block and
declare all the faces you want to create. For example:

@defpalette! begin
    table_header = Face(weight = :bold, underline = true)
    table_row_even = Face(background = bright_black)
    table_row_odd = Face(background = black)
    table_separator = Face(foreground = yellow)
end

All faces defined by @defpalette! are recognised by [face""](@ref
@face_str). In our table example, this means that face"table_header" will work
just as face"cyan" does.

To support face customisation, along with other runtime features, it is
necessary that whenever @defpalette! is used a call to @registerpalette! is
put in the module’s __init__ function.

function __init__()
    @registerpalette!
end

The ability to use a face defined within a module, like face"table_header", is
specific to that module. Should face"table_header" be put in another module,
it will not be found. In order to use faces defined in another module or
package, we can invoke @usepalettes!. This imports the faces defined
by the modules provided as arguments.

@usepalettes! MyColors

face"burgundy" # defined in MyColors

!!! note “Declare and import faces before using them”
Face resolution with face"" is performed at macro-expansion (compile) time.
A consequence of this is that faces must be defined and imported with @defpalette!
and @usepalettes! before any face"" calls referencing those faces.

It is also possible to specify a color provided by another module using a
qualified name, of the form face"<module path>.<name>. In our example,
face"MyColors.burgundy" could be used if @usepalettes! wasn’t called.

[Dynamic face theming](@id stdlib-styledstrings-theming)

When trying to create well-designed content for the terminal, only being able to assume 8 colors with two shades, but not knowing what those colors are, can be frustratingly limiting. However, reaching outside the 16 shades is fraught. Take picking a highlight colour. For any single color you pick, there’s a user with a terminal theme that makes the resulting content unreadable.

By hooking into REPL initialisation, StyledStrings is able to query the terminal state and determine what the actual colors used by the terminal are. This allows for simplistic light/dark detection, as well as more sophisticated color blending.

Light and dark variants of a face can be embedded in the @usepalettes! call that defines the faces, by using .light and .dark suffixes. For example:

@defpalette! begin
    table_highlight = Face(background = 0xc2990b) # A muddy yellow. Not great but often legible
    table_highlight.light = Face(background = 0xffda90) # A pale yellow for light themes
    table_highlight.dark = Face(background = 0x876804) # A dull yellow for dark themes
end

Users can customise the light and dark face variants, even if no variants are
declared in @defpalette!. At runtime, the light and dark variants will
automatically be applied when a light/dark terminal theme is detected.

This helps us avoid the worst case of illegible content, but we can still do better. People are still using odd themes which make it hard to pick shades and hues that are completely reliable, and it’s easy to clash with the colour hues already used in the terminal color theme (for example, if the particular green you pick clashes). To produce the best experience, we can blend the colors already used in the terminal theme to produce the best hue and shade. This is done with three key functions:

  • recolor as a hook for when theme information has been collected/updated
  • blend for blending colors
  • setface! for updating the face style

Using these tools, we can set table_highlight to fit in seamlessly with the existing terminal theme.

# Must be placed within `__init__`
StyledStrings.recolor() do
    faintyellow = StyledStrings.blend(face"yellow", face"background", 0.7)
    StyledStrings.setface!(face"table_highlight", Face(background = faintyellow))
end

Whether the extra capability afforded by this approach will vary face-by-face. It is most valuable when reaching for colors in-between those offered by the terminal, or based on the particular foreground/background shades or hues.

Applying faces to a AnnotatedString

By convention, the :face attributes of a [AnnotatedString](@ref
Base.AnnotatedString) hold information on the [Face](@ref StyledStrings.Face)s
that currently apply.

!!! compat “Julia 1.14”
Faces used to be given by either a single Face, a Symbol naming a face, or a vector
of Faces/Symbols. As of version 1.14 this is deprecated and only supported for
backwards compatibility. This should not be used in any new code.

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](@ref stdlib-styledstring-literals).

str1 = AnnotatedString("blue text", [(1:9, :face, face"blue")])
str2 = styled"{blue:blue text}"
str1 == str2
sprint(print, str1, context = :color => true)
sprint(show, MIME("text/html"), str1, context = :color => true)

[Styled String Literals](@id stdlib-styledstring-literals)

To ease construction of [AnnotatedString](@ref Base.AnnotatedString)s with [Face](@ref StyledStrings.Face)s applied,
the [styled"..."](@ref @styled_str) styled string literal allows for the content and
attributes to be easily expressed together via a custom grammar.

Within a [styled"..."](@ref @styled_str) 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 (resolved using the same process described for face"")
  • 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"..."](@ref @styled_str) 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}}.")
Documenter doesn't properly represent all the styling above, so I've converted it manually to HTML and LaTeX.
<pre>
 The basic font-style attributes are <span style="font-weight: 700;">bold</span>, <span style="font-weight: 300;">light</span>, <span style="font-style: italic;">italic</span>,
 <span style="text-decoration: underline;">underline</span>, and <span style="text-decoration: line-through">strikethrough</span>.

 In terms of color, we have named faces for the 16 standard terminal colors:
  <span style="color: #1c1a23;">■</span> <span style="color: #a51c2c;">■</span> <span style="color: #25a268;">■</span> <span style="color: #e5a509;">■</span> <span style="color: #195eb3;">■</span> <span style="color: #803d9b;">■</span> <span style="color: #0097a7;">■</span> <span style="color: #dddcd9;">■</span>
  <span style="color: #76757a;">■</span> <span style="color: #ed333b;">■</span> <span style="color: #33d079;">■</span> <span style="color: #f6d22c;">■</span> <span style="color: #3583e4;">■</span> <span style="color: #bf60ca;">■</span> <span style="color: #26c6da;">■</span> <span style="color: #f6f5f4;">■</span>

 Since <span style="color: #0097a7;">bright_black</span> is effectively grey, we define two aliases for it:
 <span style="color: #0097a7;">grey</span> and <span style="color: #0097a7;">gray</span> to allow for regional spelling differences.

 To flip the foreground and background colors of some text, you can use the
 <span style="color: #0097a7;">inverse</span> face, for example: <span style="color: #803d9b;">some </span><span style="background-color: #803d9b;">inverse</span><span style="color: #803d9b;"> text</span>.

 The intent-based basic faces are <span style="color: #76757a;">shadow</span> (for dim but visible text),
 <span style="background-color: #3a3a3a;">region</span> for selections, <span style="color: #195eb3;">emphasis</span>, and <span style="background-color: #195eb3;">highlight</span>.
 As above, <span style="color: #0097a7;">code</span> is used for code-like text.

 Lastly, we have the 'message severity' faces: <span style="color: #ed333b;">error</span>, <span style="color: #e5a509;">warning</span>,
 <span style="color: #25a268;">success</span>, <span style="color: #26c6da;">info</span>, <span style="color: #76757a;">note</span>, and <span style="color: #33d079;">tip</span>.

 Remember that all these faces (and any user or package-defined ones) can
 arbitrarily nest and overlap, <span style="color: #33d079;background-color: #3a3a3a;">like <span style="font-weight: 700;font-style: italic;">so</span></span>.</pre>
\begingroup
\ttfamily
\setlength{\parindent}{0pt}
\setlength{\parskip}{\baselineskip}

The basic font-style attributes are {\fontseries{b}\selectfont bold}, {\fontseries{l}\selectfont light}, {\fontshape{it}\selectfont italic},\\
\underline{underline}, and {strikethrough}.

In terms of color, we have named faces for the 16 standard terminal colors:\\
{\color[HTML]{1c1a23}\(\blacksquare\)} {\color[HTML]{a51c2c}\(\blacksquare\)} {\color[HTML]{25a268}\(\blacksquare\)}
{\color[HTML]{e5a509}\(\blacksquare\)} {\color[HTML]{195eb3}\(\blacksquare\)} {\color[HTML]{803d9b}\(\blacksquare\)}
{\color[HTML]{0097a7}\(\blacksquare\)} {\color[HTML]{dddcd9}\(\blacksquare\)} \\
{\color[HTML]{76757a}\(\blacksquare\)} {\color[HTML]{ed333b}\(\blacksquare\)} {\color[HTML]{33d079}\(\blacksquare\)} {\color[HTML]{f6d22c}\(\blacksquare\)} {\color[HTML]{3583e4}\(\blacksquare\)} {\color[HTML]{bf60ca}\(\blacksquare\)} {\color[HTML]{26c6da}\(\blacksquare\)} {\color[HTML]{f6f5f4}\(\blacksquare\)}

Since {\color[HTML]{0097a7}bright\_black} is effectively grey, we define two aliases for it:\\
{\color[HTML]{0097a7}grey} and {\color[HTML]{0097a7}gray} to allow for regional spelling differences.

To flip the foreground and background colors of some text, you can use the\\
{\color[HTML]{0097a7}inverse} face, for example: {\color[HTML]{803d9b}some \colorbox[HTML]{803d9b}{\color[HTML]{000000}inverse} text}.

The intent-based basic faces are {\color[HTML]{76757a}shadow} (for dim but visible text),\\
\colorbox[HTML]{3a3a3a}{region} for selections, {\color[HTML]{195eb3}emphasis}, and \colorbox[HTML]{195eb3}{highlight}.\\
As above, {\color[HTML]{0097a7}code} is used for code-like text.

Lastly, we have the 'message severity' faces: {\color[HTML]{ed333b}error}, {\color[HTML]{e5a509}warning},\\
{\color[HTML]{25a268}success}, {\color[HTML]{26c6da}info}, {\color[HTML]{76757a}note}, and {\color[HTML]{33d079}tip}.

Remember that all these faces (and any user or package-defined ones) can\\
arbitrarily nest and overlap, \colorbox[HTML]{3a3a3a}{\color[HTML]{33d079}like
  {\fontseries{b}\fontshape{it}\selectfont so}}.
\endgroup

[Customisation](@id stdlib-styledstrings-customisation)

[Face configuration files (Faces.toml)](@id stdlib-styledstrings-face-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](@ref StyledStrings.Face) specifications that
are merged with the pre-existing entry in the face dictionary.

A [Face](@ref StyledStrings.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"

Should the package MyTables define a table_header face, you could change its
colour in the same manner:

[MyTables]
table_header.foreground = "blue"

Light and dark face variants may be set under the top-level tables [light] and [dark]. For instance, to set the table header to magenta in light mode, one may use:

[light.MyTables]
table_header.foreground = "magenta"

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

Face remapping

One package may construct a styled string without any knowledge of how it is intended to be used. Should you find yourself wanting to substitute particular faces applied by a method, you can wrap the method in remapfaces to substitute the faces applied during construction.

StyledStrings.remapfaces(face"warning" => face"error") do
    styled"you should be {warning:very concerned}"
end

This changes the annotations in the styled strings produced within the remapfaces call.

Display-time face rebinding

While remapfaces is applied during styled string construction, it is also possible to change the meaning of each face while they are printed. This is done via withfaces.

withfaces(face"yellow" => face"red", face"green" => face"blue") do
    println(styled"{yellow:red} and {green:blue} mixed make {magenta:purple}")
end

This is best applied when you want to temporarily change how faces appear,
without modifying the underlying string data.

[API reference](@id stdlib-styledstrings-api)

Styling and Faces

StyledStrings.@styled_str
StyledStrings.styled
StyledStrings.@face_str
StyledStrings.@defpalette!
StyledStrings.@registerpalette!
StyledStrings.@usepalettes!
StyledStrings.Face
StyledStrings.remapfaces
StyledStrings.withfaces
StyledStrings.SimpleColor
StyledStrings.parse(::Type{StyledStrings.SimpleColor}, ::String)
StyledStrings.tryparse(::Type{StyledStrings.SimpleColor}, ::String)
StyledStrings.merge(::StyledStrings.Face, ::StyledStrings.Face)
StyledStrings.recolor
StyledStrings.blend
StyledStrings.setface!

Deprecated API

StyledStrings.addface!
1 Like

I’ve just put Face primacy and palettes by tecosaur · Pull Request #131 · JuliaLang/StyledStrings.jl · GitHub up.

I’ve never used StyledStrings but your docs seem very clear to me on cursory first reading.

You don’t mention this but I wonder if it would be possible to specify colours by name based on Colors.jl? This may be more intuitive for users but probably adds overhead.

I terms of proof reading, I noticed two small things. You use colour/color inconsistently. And this sentence probably needs a comma not a full stop : Whether the extra capability afforded by this approach will vary face-by-face. It is most valuable when reaching for colors in-between those offered by the terminal, or based on the particular foreground/background shades or hues.

That’s great to hear, thanks for taking a look Tim!

On one level, it already is :grin:


julia> using StyledStrings, Colors
[ Info: Precompiling StyledStringsExt [acce9e33-b7be-5bfc-9e27-003c16e7ffe6]

julia> styled"{(fg=$(colorant\"salmon\")):text with a Color.jl provided foreground}"
"text with a Color.jl provided foreground"

but what the palette system now lets you do is:

@usepalettes! MyColors

styled"{salmon:text with a foreground colour provided by MyColors}" 

Colors.jl/src/names_data.jl at master · JuliaGraphics/Colors.jl · GitHub could be easily translated to a large @defpalette! statement.

Ah thanks. I’m not an American, so “colours” is native to me, but I need to use “color” for consistency with the rest of the Julia docs. Grrrr.

I’ve also rewritten the sentence you flagged. Thanks for giving that a read!

4 Likes