Skip to content
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

[Prototype] Implement display protocol for rendering AnnotatedString #100

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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 @@ -19,6 +19,7 @@ StyledStrings.eachregion
StyledStrings.annotation_events
StyledStrings.face!
StyledStrings.getface
StyledStrings.resolve
StyledStrings.loadface!
StyledStrings.loaduserfaces!
StyledStrings.resetfaces!
Expand Down
3 changes: 1 addition & 2 deletions src/StyledStrings.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module StyledStrings

using Base: AnnotatedString, AnnotatedChar, AnnotatedIOBuffer, annotations, annotate!, annotatedstring
using Base: AnnotatedString, AnnotatedChar, AnnotatedIOBuffer, annotations, annotate!, annotatedstring, eachregion
using Base.ScopedValues: ScopedValue, with, @with

# While these are imported from Base, we claim them as part of the `StyledStrings` API.
Expand All @@ -12,7 +12,6 @@ export @styled_str
public Face, addface!, withfaces, styled, SimpleColor

include("faces.jl")
include("regioniterator.jl")
include("io.jl")
include("styledmarkup.jl")
include("legacy.jl")
Expand Down
136 changes: 55 additions & 81 deletions src/faces.jl
Original file line number Diff line number Diff line change
Expand Up @@ -500,96 +500,70 @@ Merge the properties of the `initial` face and `others`, with
later faces taking priority.
"""
function Base.merge(a::Face, b::Face)
if isempty(b.inherit)
# Extract the heights to help type inference a bit to be able
# to narrow the types in e.g. `aheight * bheight`
aheight = a.height
bheight = b.height
abheight = if isnothing(bheight) aheight
elseif isnothing(aheight) bheight
elseif bheight isa Int bheight
elseif aheight isa Int round(Int, aheight * bheight)
else aheight * bheight end
Face(if isnothing(b.font) a.font else b.font end,
abheight,
if isnothing(b.weight) a.weight else b.weight end,
if isnothing(b.slant) a.slant else b.slant end,
if isnothing(b.foreground) a.foreground else b.foreground end,
if isnothing(b.background) a.background else b.background end,
if isnothing(b.underline) a.underline else b.underline end,
if isnothing(b.strikethrough) a.strikethrough else b.strikethrough end,
if isnothing(b.inverse) a.inverse else b.inverse end,
a.inherit)
else
b_noinherit = Face(
b.font, b.height, b.weight, b.slant, b.foreground, b.background,
b.underline, b.strikethrough, b.inverse, Symbol[])
b_inheritance = map(fname -> get(Face, FACES.current[], fname), Iterators.reverse(b.inherit))
b_resolved = merge(foldl(merge, b_inheritance), b_noinherit)
merge(a, b_resolved)
end
end

Base.merge(a::Face, b::Face, others::Face...) = merge(merge(a, b), others...)

## Getting the combined face from a set of properties ##

# Putting these inside `getface` causes the julia compiler to box it
_mergedface(face::Face) = face
_mergedface(face::Symbol) = get(Face, FACES.current[], 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.
"""
function getface(faces)
isempty(faces) && return FACES.current[][:default]
combined = mapfoldl(_mergedface, merge, faces)::Face
if !isempty(combined.inherit)
combined = merge(Face(), combined)
end
merge(FACES.current[][:default], combined)
# We cannot merge unresolved Faces and resolving a face makes a difference
# to the user so we shouldn't do it automatically either (especially when
# inserting Faces via `addface!`)
# Here we require that a Face be resolved if you're going to merge it.
@assert isempty(a.inherit) && isempty(b.inherit)
return _merge(a, b)
end

"""
getface(annotations::Vector{@NamedTuple{label::Symbol, value::Any}})

Combine all of the `:face` annotations with `getfaces`.
"""
function getface(annotations::Vector{@NamedTuple{label::Symbol, value::Any}})
faces = (ann.value for ann in annotations if ann.label === :face)
getface(faces)
# Merge assuming that `a` and `b` are resolved Faces.
function _merge(a::Face, b::Face)
# Extract the heights to help type inference a bit to be able
# to narrow the types in e.g. `aheight * bheight`
aheight = a.height
bheight = b.height
abheight = if isnothing(bheight) aheight
elseif isnothing(aheight) bheight
elseif bheight isa Int bheight
elseif aheight isa Int round(Int, aheight * bheight)
else aheight * bheight end
Face(if isnothing(b.font) a.font else b.font end,
abheight,
if isnothing(b.weight) a.weight else b.weight end,
if isnothing(b.slant) a.slant else b.slant end,
if isnothing(b.foreground) a.foreground else b.foreground end,
if isnothing(b.background) a.background else b.background end,
if isnothing(b.underline) a.underline else b.underline end,
if isnothing(b.strikethrough) a.strikethrough else b.strikethrough end,
if isnothing(b.inverse) a.inverse else b.inverse end,
Symbol[])
end

getface(face::Face) = merge(FACES.current[][:default], merge(Face(), face))
getface(face::Symbol) = getface(get(Face, FACES.current[], face))
Base.merge(a::Face, b::Face, others::Face...) = merge(merge(a, b), others...)

"""
getface()
## Resolving Faces that are still 'lazy' ##

Obtain the default face.
"""
getface() = FACES.current[][:default]
getface(face::Symbol; resolve::Bool)::Face

## Face/AnnotatedString integration ##
Obtain a [`Face`](@ref) from the active FACES mapping.

"""
getface(s::AnnotatedString, i::Integer)
If `resolve` is set to `false`, the Face is returned exactly
as it is present in the FACES mapping.

Get the merged [`Face`](@ref) that applies to `s` at index `i`.
If `resolve` is set to `true`, any inherited features are
resolved and a merged Face with no inheritance is returned.
"""
getface(s::AnnotatedString, i::Integer) =
getface(map(last, annotations(s, i)))
function getface(face::Symbol; resolve::Bool=true)
face = get(Face, FACES.current[], face)
return resolve ? StyledStrings.resolve(face) : face
end

"""
getface(c::AnnotatedChar)
function resolve(face::Face)
for parent in face.inherit
# We use `_merge` here to bypass the "resolved check" since
# we just want this to merge ignoring the inherited faces
# of `face` (we are currently resolving them)
face = _merge(getface(parent; resolve=true), face)
end
return face
end

Get the merged [`Face`](@ref) that applies to `c`.
"""
getface(c::AnnotatedChar) = getface(c.annotations)
function resolve(face::Symbol)
return resolve(get(Face, FACES.current[], face))
end

"""
face!(str::Union{<:AnnotatedString, <:SubString{<:AnnotatedString}},
Expand All @@ -598,12 +572,12 @@ 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)
range::UnitRange{Int}, face::Union{Symbol, Face}) =
annotate!(s, range, :face, resolve(face))

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

## Reading face definitions from a dictionary ##

Expand Down
Loading
Loading