Skip to content

Introduce FaceRef and use it to wrap any un-resolved Faces #110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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.

```@docs
StyledStrings.FaceRef
StyledStrings.ANSI_4BIT_COLORS
StyledStrings.FACES
StyledStrings.HTML_BASIC_COLORS
Expand Down
36 changes: 29 additions & 7 deletions src/faces.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

const RGBTuple = NamedTuple{(:r, :g, :b), NTuple{3, UInt8}}

"""
struct FaceRef

A reference to a lazily-resolved Face. This is required so that the
AnnotatedString printer in Base can dispatch to StyledStrings (w/o
type-piracy) for the display of annotations.
"""
struct FaceRef
# At some point in the future, this may also include a handle
# to a 'Palette' where this face will be looked up.
face::Symbol
end

wrap_symbol(face) = face
wrap_symbol(face::Symbol) = FaceRef(face)

"""
struct SimpleColor

Expand Down Expand Up @@ -546,13 +562,14 @@ Base.merge(a::Face, b::Face, others::Face...) = merge(merge(a, b), others...)
# Putting these inside `getface` causes the julia compiler to box it
_mergedface(face::Face) = face
_mergedface(face::Symbol) = get(Face, FACES.current[], face)
_mergedface(ref::FaceRef) = get(Face, FACES.current[], ref.face)
_mergedface(faces::Vector) = mapfoldl(_mergedface, merge, Iterators.reverse(faces))

"""
getface(faces)

Obtain the final merged face from `faces`, an iterator of
[`Face`](@ref)s, face name `Symbol`s, and lists thereof.
[`Face`](@ref)s, [`FaceRef`](@ref)s, face name `Symbol`s, and lists thereof.
"""
function getface(faces)
isempty(faces) && return FACES.current[][:default]
Expand All @@ -575,6 +592,7 @@ end

getface(face::Face) = merge(FACES.current[][:default], merge(Face(), face))
getface(face::Symbol) = getface(get(Face, FACES.current[], face))
getface(ref::FaceRef) = getface(get(Face, FACES.current[], ref.face))

"""
getface()
Expand Down Expand Up @@ -606,13 +624,17 @@ getface(c::AnnotatedChar) = getface(c.annotations)

Apply `face` to `str`, along `range` if specified or the whole of `str`.
"""
face!(s::Union{<:AnnotatedString, <:SubString{<:AnnotatedString}},
range::UnitRange{Int}, face::Union{Symbol, Face, <:Vector{<:Union{Symbol, Face}}}) =
annotate!(s, range, :face, face)
face!(s::Union{AnnotatedString, SubString{<:AnnotatedString}},
range::UnitRange{Int}, face::Union{Symbol, Face, FaceRef}) =
annotate!(s, range, :face, wrap_symbol(face))

face!(s::Union{AnnotatedString, SubString{<:AnnotatedString}},
range::UnitRange{Int}, faces::Vector{<:Union{Symbol, Face, FaceRef}}) =
annotate!(s, range, :face, wrap_symbol.(faces))

face!(s::Union{<:AnnotatedString, <:SubString{<:AnnotatedString}},
face::Union{Symbol, Face, <:Vector{<:Union{Symbol, Face}}}) =
annotate!(s, firstindex(s):lastindex(s), :face, face)
face!(s::Union{AnnotatedString, SubString{<:AnnotatedString}},
face::Union{Symbol, Face, FaceRef, Vector{<:Union{Symbol, Face, FaceRef}}}) =
face!(s, firstindex(s):lastindex(s), face)

## Reading face definitions from a dictionary ##

Expand Down
7 changes: 4 additions & 3 deletions src/regioniterator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ an iterator which provides each substring and the applicable annotations as a

```jldoctest
julia> collect(StyledStrings.eachregion(AnnotatedString(
"hey there", [(1:3, :face, :bold), (5:9, :face, :italic)])))
"hey there", [(1:3, :face, StyledStrings.FaceRef(:bold)),
(5:9, :face, StyledStrings.FaceRef(:italic))])))
3-element Vector{Tuple{SubString{String}, Vector{@NamedTuple{label::Symbol, value}}}}:
("hey", [@NamedTuple{label::Symbol, value}((:face, :bold))])
("hey", [@NamedTuple{label::Symbol, value}((:face, StyledStrings.FaceRef(:bold)))])
(" ", [])
("there", [@NamedTuple{label::Symbol, value}((:face, :italic))])
("there", [@NamedTuple{label::Symbol, value}((:face, StyledStrings.FaceRef(:italic)))])
```
"""
function eachregion(s::AnnotatedString, subregion::UnitRange{Int}=firstindex(s):lastindex(s))
Expand Down
80 changes: 41 additions & 39 deletions src/styledmarkup.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Of course, as usual, the devil is in the details.
module StyledMarkup

using Base: AnnotatedString, annotations, annotatedstring
using ..StyledStrings: Face, SimpleColor
using ..StyledStrings: Face, FaceRef, SimpleColor, wrap_symbol

export @styled_str, styled

Expand Down Expand Up @@ -325,7 +325,7 @@ function readexpr!(state::State, pos::Int = first(popfirst!(state.s)) + 1)
if isempty(state.s)
styerr!(state,
AnnotatedString("Identifier or parenthesised expression expected after \$ in string",
[(55:55, :face, :warning)]),
[(55:55, :face, FaceRef(:warning))]),
-1, "right here")
return "", pos
end
Expand Down Expand Up @@ -401,7 +401,7 @@ and register it in the active styles list.
"""
function begin_style!(state::State, i::Int, char::Char)
hasvalue = false
newstyles = Vector{Tuple{Int, Int, Union{Symbol, Expr, Tuple{Symbol, Any}}}}()
newstyles = Vector{Tuple{Int, Int, Union{FaceRef, Expr, Tuple{Symbol, Any}}}}()
while read_annotation!(state, i, char, newstyles) end
push!(state.active_styles, reverse!(newstyles))
# Adjust bytes/offset based on how much the index
Expand Down Expand Up @@ -535,9 +535,9 @@ function read_inlineface!(state::State, i::Int, char::Char, newstyles)
valid_options = join(VALID_UNDERLINE_STYLES, ", ", ", or ")
styerr!(state,
AnnotatedString("Invalid underline style '$ustyle_word' (should be $valid_options)",
[(26:25+ncodeunits(ustyle_word), :face, :warning)
[(26:25+ncodeunits(ustyle_word), :face, FaceRef(:warning))
(28+ncodeunits(ustyle_word):39+ncodeunits(ustyle_word)+ncodeunits(valid_options),
:face, :light)]),
:face, FaceRef(:light))]),
-length(ustyle_word) - 3)
end
ustyle = Symbol(ustyle_word)
Expand Down Expand Up @@ -658,24 +658,24 @@ function read_inlineface!(state::State, i::Int, char::Char, newstyles)
else
invalid, lastchar = readsymbol!(state, lastchar)
styerr!(state, AnnotatedString("Invalid height '$invalid', should be a natural number or positive float",
[(17:16+ncodeunits(string(invalid)), :face, :warning)]),
[(17:16+ncodeunits(string(invalid)), :face, FaceRef(:warning))]),
-3)
end
elseif key ∈ (:weight, :slant)
v, lastchar = readalph!(state, lastchar)
if key == :weight && v ∉ VALID_WEIGHTS
valid_options = join(VALID_WEIGHTS, ", ", ", or ")
styerr!(state, AnnotatedString("Invalid weight '$v' (should be $valid_options)",
[(17:16+ncodeunits(v), :face, :warning),
[(17:16+ncodeunits(v), :face, FaceRef(:warning)),
(19+ncodeunits(v):30+ncodeunits(v)+ncodeunits(valid_options),
:face, :light)]),
:face, FaceRef(:light))]),
-3)
elseif key == :slant && v ∉ VALID_SLANTS
valid_options = join(VALID_SLANTS, ", ", ", or ")
styerr!(state, AnnotatedString("Invalid slant '$v' (should be $valid_options)",
[(16:15+ncodeunits(v), :face, :warning),
[(16:15+ncodeunits(v), :face, FaceRef(:warning)),
(18+ncodeunits(v):29+ncodeunits(v)+ncodeunits(valid_options),
:face, :light)]),
:face, FaceRef(:light))]),
-3)
end
Symbol(v) |> if ismacro(state) QuoteNode else identity end
Expand All @@ -702,7 +702,7 @@ function read_inlineface!(state::State, i::Int, char::Char, newstyles)
else
styerr!(state, AnnotatedString(
"Uses unrecognised face key '$key'. Recognised keys are: $(join(VALID_FACE_ATTRS, ", ", ", and "))",
[(29:28+ncodeunits(String(key)), :face, :warning)]),
[(29:28+ncodeunits(String(key)), :face, FaceRef(:warning))]),
-length(str_key) - 2)
end
if ismacro(state) && !any(k -> first(k.args) == key, kwargs)
Expand All @@ -711,7 +711,7 @@ function read_inlineface!(state::State, i::Int, char::Char, newstyles)
push!(kwargs, key => val)
else
styerr!(state, AnnotatedString("Contains repeated face key '$key'",
[(29:28+ncodeunits(String(key)), :face, :warning)]),
[(29:28+ncodeunits(String(key)), :face, FaceRef(:warning))]),
-length(str_key) - 2)
end
isempty(state.s) && styerr!(state, "Incomplete inline face declaration", -1)
Expand All @@ -723,16 +723,16 @@ function read_inlineface!(state::State, i::Int, char::Char, newstyles)
break
end
end
face = Expr(:call, Face, kwargs...)
push!(newstyles,
(i, i + state.offset + 1,
if !ismacro(state)
:face, Face(; NamedTuple(kwargs)...)
elseif needseval
:((:face, $face))
else
:face, hygienic_eval(state, face)
end))
face_expr = Expr(:call, Face, kwargs...)
if !ismacro(state)
face = (:face, Face(; NamedTuple(kwargs)...))
elseif needseval
face = :((:face, $face_expr))
else
face = (:face, hygienic_eval(state, face_expr))
end
offset = i + state.offset + 1
push!(newstyles, (i, offset, face))
end

"""
Expand Down Expand Up @@ -800,23 +800,25 @@ function read_face_or_keyval!(state::State, i::Int, char::Char, newstyles)
end
String(chars)
end
push!(newstyles,
(i, i + state.offset + ncodeunits('{'),
if key isa String && !(value isa Symbol || value isa Expr)
Symbol(key), value
elseif key isa Expr || key isa Symbol
:(($key, $value))
else
:(($(QuoteNode(Symbol(key))), $value))
end))
if key isa String && !(value isa Symbol || value isa Expr)
face = (Symbol(key), value)
elseif key isa Expr || key isa Symbol
face = :(($key, $value))
else
face = :(($(QuoteNode(Symbol(key))), $value))
end
offset = i + state.offset + ncodeunits('{')
push!(newstyles, (i, offset, face))
elseif key !== ""
push!(newstyles,
(i, i + state.offset + ncodeunits('{'),
if key isa Symbol || key isa Expr
:((:face, $key))
else # Face symbol
:face, Symbol(key)
end))
if key isa Symbol || key isa Expr
face = :((:face, $wrap_symbol($key)))
elseif ismacro(state) # Face symbol
face = :((:face, $FaceRef($(QuoteNode(Symbol(key))))))
else # Face symbol
face = (:face, FaceRef(Symbol(key)))
end
offset = i + state.offset + ncodeunits('{')
push!(newstyles, (i, offset, face))
end
if isempty(state.s) || last(peek(state.s)) ∉ (' ', '\t', '\n', '\r', ',', ':')
styerr!(state, "Incomplete annotation declaration", prevind(state.content, i), "starts here")
Expand Down Expand Up @@ -857,7 +859,7 @@ function run_state_machine!(state::State)
end
for incomplete in Iterators.flatten(state.active_styles)
styerr!(state, AnnotatedString("Unterminated annotation (missing closing '}')",
[(43:43, :face, :warning)]),
[(43:43, :face, FaceRef(:warning))]),
prevind(state.content, first(incomplete)), "starts here")
end
end
Expand Down
Loading