Internals
Everything documented in this page is internal and subject to breaking changes, even in minor version updates of Julia or StyledStrings.jl. If you are curious about the internals, read on, but if you want to depend on them, please consider opening a pull request or issue to discuss making them part of the public API.
StyledStrings.ANSI_4BIT_COLORS
— ConstantA mapping between ANSI named colours and indices in the standard 256-color table. The standard colors are 0-7, and high intensity colors 8-15.
The high intensity colors are prefixed by "bright". The "brightblack" color is given two aliases: "grey" and "gray".
StyledStrings.FACES
— ConstantGlobally named Face
s.
default
gives the initial values of the faces, and current
holds the active (potentially modified) set of faces. This two-set system allows for any modifications to the active faces to be undone.
StyledStrings.HTML_BASIC_COLORS
— ConstantA mapping between ANSI named colors and 8-bit colors for use in HTML representations.
StyledStrings.Legacy.ANSI_256_COLORS
— ConstantA mapping from 256-color codes indicies to 8-bit colours.
StyledStrings.Legacy.NAMED_COLORS
— ConstantA list of all named colors recognised, including both the old light_*
and new bright_*
named colors.
StyledStrings.Legacy.RENAMED_COLORS
— ConstantA mapping from old named colours to the new names, specifically from light_*
to bright_*
.
StyledStrings.Legacy.legacy_color
— Functionlegacy_color(color::Union{String, Symbol, Int})
Attempt to obtain a SimpleColor
for a "legacy" color value color
.
When this is not possible, nothing
is returned.
StyledStrings.Legacy.load_env_colors!
— Functionload_env_colors!()
Try to emulate the effect of the various *_color()
functions of Base
, by loading any specified colours as foregrounds of the relevant faces.
StyledStrings.ansi_4bit_color_code
— Functionansi_4bit_color_code(color::Symbol, background::Bool=false)
Provide the color code (30-37, 40-47, 90-97, 100-107) for color
, as an integer. When background
is set the background variant will be provided, otherwise the provided code is for setting the foreground color.
StyledStrings.eachregion
— Functioneachregion(s::AnnotatedString{S})
eachregion(s::SubString{AnnotatedString{S}})
Identify the contiguous substrings of s
with a constant annotations, and return an iterator which provides each substring and the applicable annotations as a Tuple{SubString{S}, Vector{@NamedTuple{label::Symbol, value::Any}}}
.
Examples
julia> collect(StyledStrings.eachregion(AnnotatedString(
"hey there", [(1:3, :face, :bold), (5:9, :face, :italic)])))
3-element Vector{Tuple{SubString{String}, Vector{@NamedTuple{label::Symbol, value}}}}:
("hey", [@NamedTuple{label::Symbol, value}((:face, :bold))])
(" ", [])
("there", [@NamedTuple{label::Symbol, value}((:face, :italic))])
StyledStrings.annotation_events
— Functionannotation_events(string::AbstractString, annots::Vector{@NamedTuple{region::UnitRange{Int}, label::Symbol, value::Any}}, subregion::UnitRange{Int})
annotation_events(string::AnnotatedString, subregion::UnitRange{Int})
Find all annotation "change events" that occur within a subregion
of annots
, with respect to string
. When string
is styled, annots
is inferred.
Each change event is given in the form of a @NamedTuple{pos::Int, active::Bool, index::Int}
where pos
is the position of the event, active
is a boolean indicating whether the annotation is being activated or deactivated, and index
is the index of the annotation in question.
StyledStrings.face!
— Functionface!(str::Union{<:AnnotatedString, <:SubString{<:AnnotatedString}},
[range::UnitRange{Int},] face::Union{Symbol, Face})
Apply face
to str
, along range
if specified or the whole of str
.
StyledStrings.getface
— Functiongetface(faces)
Obtain the final merged face from faces
, an iterator of Face
s, face name Symbol
s, and lists thereof.
getface(annotations::Vector{@NamedTuple{label::Symbol, value::Any}})
Combine all of the :face
annotations with getfaces
.
getface()
Obtain the default face.
getface(s::AnnotatedString, i::Integer)
Get the merged Face
that applies to s
at index i
.
getface(c::AnnotatedChar)
Get the merged Face
that applies to c
.
StyledStrings.loadface!
— Functionloadface!(name::Symbol => update::Face)
Merge the face name
in FACES
.current
with update
. If the face name
does not already exist in FACES
.current
, then it is set to update
. To reset a face, update
can be set to nothing
.
Examples
julia> loadface!(:red => Face(foreground=0xff0000))
Face (sample)
foreground: #ff0000
StyledStrings.loaduserfaces!
— Functionloaduserfaces!(faces::Dict{String, Any})
For each face specified in Dict
, load it to FACES
.current
.
loaduserfaces!(tomlfile::String)
Load all faces declared in the Faces.toml file tomlfile
.
StyledStrings.resetfaces!
— Functionresetfaces!()
Reset the current global face dictionary to the default value.
resetfaces!(name::Symbol)
Reset the face name
to its default value, which is returned.
If the face name
does not exist, nothing is done and nothing
returned. In the unlikely event that the face name
does not have a default value, it is deleted, a warning message is printed, and nothing
returned.
StyledStrings.termcolor
— Functiontermcolor(io::IO, color::SimpleColor, category::Char)
Print to io
the SGR code to set the category
's slot to color
, where category
is set as follows:
'3'
sets the foreground color'4'
sets the background color'5'
sets the underline color
If color
is a SimpleColor{Symbol}
, the value should be a a member of ANSI_4BIT_COLORS
. Any other value will cause the color to be reset.
If color
is a SimpleColor{RGBTuple}
and get_have_truecolor()
returns true, 24-bit color is used. Otherwise, an 8-bit approximation of color
is used.
termcolor(io::IO, ::Nothing, category::Char)
Print to io
the SGR code to reset the color for category
.
StyledStrings.termcolor24bit
— Functiontermcolor24bit(io::IO, color::RGBTuple, category::Char)
Print to io
the 24-bit SGR color code to set the category
8 slot to color
.
StyledStrings.termcolor8bit
— Functiontermcolor8bit(io::IO, color::RGBTuple, category::Char)
Print to io
the best 8-bit SGR color code that sets the category
color to be close to color
.
StyledStrings.load_customisations!
— Functionload_customisations!(; force::Bool=false)
Load customisations from the user's faces.toml
file, if it exists as well as the current environment.
This function should be called before producing any output in situations where the user's customisations should be considered. This is called automatically when printing text or HTML output, and when calling withfaces
, but may need to be called manually in unusual situations.
Unless force
is set, customisations are only applied when this function is called for the first time, and subsequent calls are a no-op.
Styled Markup parsing
While some of the internals above are useful outside StyledStrings
, and unlikely to be broken (but with no guarantees!), apart from the exported string macro and styled
function, the details of StyledMarkup
documented below consists entirely of implementation details that should under no circumstances be referenced outside of StyledStrings
.
If you're curious about how exactly styled markup strings are parsed, they should provide some insight though.
StyledStrings.StyledMarkup
— ModuleStyledMarkup
A sub-module of StyledStrings
that specifically deals with parsing styled markup strings. To this end, two entrypoints are provided:
- The
styled""
string macro, which is generally preferred. - They
styled
function, which allows for use with runtime-provided strings, when needed.
Overall, this module essentially functions as a state machine with a few extra niceties (like detailed error reporting) sprinkled on top. The overall design can be largely summed up with the following diagram:
╭String─────────╮
│ Styled markup │
╰──────┬────────╯
│╭╴[module]
││
╭┴┴State─╮
╰┬───────╯
│
╭╴run_state_machine!╶╮
│ ╭─────┼─╼ escaped!
│ Apply rules: │ │
│ "\\" ▶──────╯ ╭───┼─╼[interpolated!] ──▶ readexpr!, addpart!
│ "$" ▶────────╯ │
│ "{" ▶────────────┼─╼ begin_style! ──▶ read_annotation!
│ "}" ▶─────╮ │ ├─╼ read_inlineface! [readexpr!]
│ ╰──────┼─╼ end_style! ╰─╼ read_face_or_keyval!
│ addpart!(...) │
╰╌╌╌╌╌┬╌╌╌╌╌╌╌╌╌╌╌╌╌╌╯
│
▼
Result
Of course, as usual, the devil is in the details.
StyledStrings.StyledMarkup.State
— TypeState
A struct representing of the parser state (if you squint, a state monad even).
To create the initial state, use the constructor: State(content::AbstractString, mod::Union{Module, Nothing}=nothing) -> State
Its fields are as follows:
content::String
, the (unescaped) input stringbytes::Vector{UInt8}
, the codeunits ofcontent
. This is aVector{UInt8}
instead of aCodeUnits{UInt8}
because we need to be able to modify the array, for instance when erasing escape characters.s::Iterators.Stateful
, an(index, char)
iterator ofcontent
mod::Union{Module, Nothing}
, the (optional) context with which to evaluate inline expressions in. This should be provided iff the styled markup comes from a macro invocation.parts::Vector{Any}
, the result of the parsing, a list of elements that when passed toannotatedstring
produce the styled markup string. The types of its values are highly diverse, hence theAny
element type.active_styles::Vector{Vector{Tuple{Int, Int, Union{Symbol, Expr, Tuple{Symbol, Any}}}}}}
, A list of batches of styles that have yet to be applied to any content. Entries of a batch consist of(source_position, start_position, style)
tuples, wherestyle
may be just a symbol (referring to a face), aTuple{Symbol, Any}
annotation, or anExpr
that evaluates to a valid annotation (whenmod
is set).pending_styles::Vector{Tuple{UnitRange{Int}, Union{Symbol, Expr, Tuple{Symbol, Any}}}}
, A list of styles that have been terminated, and so are known to occur over a certain range, but have yet to be applied.offset::Int
, a record of the between thecontent
index and the index in the resulting styled string, as markup structures are absorbed.point::Int
, the current index incontent
.escape::Bool
, whether the last character seen was an escape character.interpolations::Int
, how many interpolated values have been seen. Knowing whether or not anything needs to be evaluated allows the resulting string to be computed at macroexpansion time, when possible, and knowing how many allows for some micro-optimisations.errors::Vector
, any errors raised during parsing. We collect them instead of immediately throwing so that we can list as many issues as possible at once, instead of forcing the author of the invalid styled markup to resolve each issue one at a time. This is expected to be populated by invocations ofstyerr!
.
StyledStrings.StyledMarkup.isnextchar
— Functionisnextchar(state::State, char::Char) -> Bool
isnextchar(state::State, chars::NTuple{N, Char}) -> Bool
Check if state
has a next character, and if so whether it is char
or one of chars
.
StyledStrings.StyledMarkup.ismacro
— Functionismacro(state::State) -> Bool
Check whether state
is indicated to come from a macro invocation, according to whether state.mod
is set or not.
While this function is rather trivial, it clarifies the intent when used instead of just checking state.mod
.
StyledStrings.StyledMarkup.styerr!
— Functionstyerr!(state::State, message::AbstractString, position::Union{Nothing, Int}=nothing, hint::String="around here")
Register an error in state
based on erroneous content at or around position
(if known, and with a certain hint
as to the location), with the nature of the error given by message
.
StyledStrings.StyledMarkup.hygienic_eval
— Functionhygienic_eval(state::State, expr)
Evaluate expr
within the scope of state
's module. This replicates part of the behind-the-scenes behaviour of macro expansion, we just need to manually invoke it due to the particularities around dealing with code from a foreign module that we parse ourselves.
StyledStrings.StyledMarkup.addpart!
— Functionaddpart!(state::State, stop::Int)
Create a new part from state.point
to stop
, applying all pending styles.
This consumes all the content between state.point
and stop
, and shifts state.point
to be the index after stop
.
addpart!(state::State, start::Int, expr, stop::Int)
Create a new part based on (the eventual evaluation of) expr
, running from start
to stop
, taking the currently active styles into account.
StyledStrings.StyledMarkup.escaped!
— Functionescaped!(state::State, i::Int, char::Char)
Parse the escaped character char
, at index i
, into state
StyledStrings.StyledMarkup.interpolated!
— Functioninterpolated!(state::State, i::Int, _)
Interpolate the expression starting at i
, and add it as a part to state
.
StyledStrings.StyledMarkup.readexpr!
— Functionreadexpr!(state::State, pos::Int = first(popfirst!(state.s)) + 1)
Read the expression starting at pos
in state.content
, and consume state.s
as appropriate to align the iterator to the end of the expression.
StyledStrings.StyledMarkup.skipwhitespace!
— Functionskipwhitespace!(state::State)
Skip forwards all space, tab, and newline characters in state.s
StyledStrings.StyledMarkup.read_while!
— Functionread_while!(f::Function, state::Base.Stateful, lastchar::Char)
Read state
until f(::Char)
is false
.
Given a Stateful
that iterates (_, char::Char)
pairs, and a predicate f(::Char)::Bool
, return (str, lastchar)
, where str::String
contains all the char
for which f(char) == true
, and lastchar
the last char
element seen, or the input lastchar
there are no elements of state
.
Examples
julia> s = Base.Stateful(pairs("abc"));
julia> read_while!(isnumeric, s, 'w')
("", 'a')
julia> first(s) # s is mutated
2 => 'b'
julia> read_while!(isascii, Base.Stateful(pairs("123Σω")), 'k')
("123", 'Σ')
julia> read_while!(isascii, Base.Stateful(pairs("abcde")), 'α')
("abcde", 'e')
julia> read_while!(isascii , Base.Stateful(pairs("")), 'k')
("", 'k')
StyledStrings.StyledMarkup.begin_style!
— Functionbegin_style!(state::State, i::Int, char::Char)
Parse the style declaration beginning at i
(char
) with read_annotation!
, and register it in the active styles list.
StyledStrings.StyledMarkup.end_style!
— Functionend_style!(state::State, i::Int, char::Char)
Close of the most recent active style in state
, making it a pending style.
StyledStrings.StyledMarkup.read_annotation!
— Functionread_annotation!(state::State, i::Int, char::Char, newstyles::Vector) -> Bool
Read the annotations at i
(char
), and push the style read to newstyles
.
This skips whitespace and checks what the next character in state.s
is, detects the form of the annotation, and parses it using the appropriate specialised function like so:
:
, end of annotation, do nothing(
, inline face declaration, useread_inlineface!
- otherwise, use
read_face_or_keyval!
After parsing the annotation, returns a boolean value signifying whether there is an immediately subsequent annotation to be read.
StyledStrings.StyledMarkup.read_inlineface!
— Functionread_inlineface!(state::State, i::Int, char::Char, newstyles)
Read an inline face declaration from state
, at position i
(char
), and add it to newstyles
.
StyledStrings.StyledMarkup.read_face_or_keyval!
— Functionread_face_or_keyval!(state::State, i::Int, char::Char, newstyles)
Read an inline face or key-value pair from state
at position i
(char
), and add it to newstyles
.
StyledStrings.StyledMarkup.run_state_machine!
— Functionrun_state_machine!(state::State)
Iterate through state.s
, applying the parsing rules for the top-level of syntax and calling the relevant specialised functions.
Upon completion, state.s
should be fully consumed and state.parts
fully populated (along with state.errors
).
StyledStrings.StyledMarkup.annotatedstring_optimize!
— Functionannotatedstring_optimize!(str::AnnotatedString)
Merge contiguous identical annotations in str
.