Skip to content

Commit

Permalink
Reimplement Markdown printing using StyledStrings
Browse files Browse the repository at this point in the history
Using StyledStrings for styled printing has a number of benefits,
including but not limited to:
- Italics "just working" on  terminals that announce support
- Functioning links, for the first time
- Greater compossibility of rendered markdown content
- Customisability of the printing style

Then with JuliaSyntaxHighlighting, we get support for syntax-highlighted
Julia code too.
  • Loading branch information
tecosaur committed Feb 10, 2024
1 parent 2c3509d commit 52f70eb
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 139 deletions.
6 changes: 5 additions & 1 deletion doc/Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2
uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"
version = "8.4.0+0"

[[deps.JuliaSyntaxHighlighting]]
deps = ["StyledStrings"]
uuid = "dc6e5ff7-fb65-4e79-a425-ec3bc9c03011"

[[deps.LibGit2]]
deps = ["Base64", "LibGit2_jll", "NetworkOptions", "Printf", "SHA"]
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"
Expand Down Expand Up @@ -139,7 +143,7 @@ uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
version = "1.11.0"

[[deps.Markdown]]
deps = ["Base64"]
deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"]
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
version = "1.11.0"

Expand Down
6 changes: 3 additions & 3 deletions pkgimage.mk
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ $(eval $(call stdlib_builder,libLLVM_jll,Artifacts Libdl))
$(eval $(call stdlib_builder,libblastrampoline_jll,Artifacts Libdl))
$(eval $(call stdlib_builder,p7zip_jll,Artifacts Libdl))
$(eval $(call stdlib_builder,OpenBLAS_jll,Artifacts Libdl))
$(eval $(call stdlib_builder,Markdown,Base64))
$(eval $(call stdlib_builder,Printf,Unicode))
$(eval $(call stdlib_builder,Random,SHA))
$(eval $(call stdlib_builder,Logging,StyledStrings))
Expand All @@ -122,17 +121,18 @@ $(eval $(call stdlib_builder,Dates,Printf))
$(eval $(call stdlib_builder,Distributed,Random Serialization Sockets))
$(eval $(call stdlib_builder,Future,Random))
$(eval $(call stdlib_builder,UUIDs,Random SHA))
$(eval $(call stdlib_builder,InteractiveUtils,Markdown))
$(eval $(call stdlib_builder,Markdown,Base64 JuliaSyntaxHighlighting StyledStrings))

# 3-depth packages
$(eval $(call stdlib_builder,LibGit2_jll,MbedTLS_jll LibSSH2_jll Artifacts Libdl))
$(eval $(call stdlib_builder,LibCURL_jll,LibSSH2_jll nghttp2_jll MbedTLS_jll Zlib_jll Artifacts Libdl))
$(eval $(call stdlib_builder,REPL,InteractiveUtils Markdown Sockets StyledStrings Unicode))
$(eval $(call stdlib_builder,SharedArrays,Distributed Mmap Random Serialization))
$(eval $(call stdlib_builder,InteractiveUtils,Markdown))
$(eval $(call stdlib_builder,TOML,Dates))
$(eval $(call stdlib_builder,Test,Logging Random Serialization InteractiveUtils))

# 4-depth packages
$(eval $(call stdlib_builder,REPL,InteractiveUtils Markdown Sockets StyledStrings Unicode))
$(eval $(call stdlib_builder,LibGit2,LibGit2_jll NetworkOptions Printf SHA Base64))
$(eval $(call stdlib_builder,LibCURL,LibCURL_jll MozillaCACerts_jll))

Expand Down
2 changes: 2 additions & 0 deletions stdlib/Markdown/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ version = "1.11.0"

[deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
StyledStrings = "f489334b-da3d-4c2e-b8f0-e476e12c162b"
JuliaSyntaxHighlighting = "dc6e5ff7-fb65-4e79-a425-ec3bc9c03011"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Expand Down
6 changes: 3 additions & 3 deletions stdlib/Markdown/src/GitHub/table.jl
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,15 @@ end

function term(io::IO, md::Table, columns)
margin_str = " "^margin
cells = mapmap(x -> terminline_string(io, x), md.rows)
padcells!(cells, md.align, len = ansi_length)
cells = mapmap(x -> annotprint(terminline, x), md.rows)
padcells!(cells, md.align, len = textwidth)
for i = 1:length(cells)
print(io, margin_str)
join(io, cells[i], " ")
if i == 1
println(io)
print(io, margin_str)
join(io, [""^ansi_length(cells[i][j]) for j = 1:length(cells[1])], " ")
join(io, [""^textwidth(cells[i][j]) for j = 1:length(cells[1])], " ")
end
i < length(cells) && println(io)
end
Expand Down
25 changes: 24 additions & 1 deletion stdlib/Markdown/src/Markdown.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ literals `md"..."` and `doc"..."`.
"""
module Markdown

import Base: show, ==, with_output_color, mapany
import Base: AnnotatedString, AnnotatedIOBuffer, show, ==, with_output_color, mapany
using Base64: stringmime

using StyledStrings: StyledStrings, Face, addface!, @styled_str
using JuliaSyntaxHighlighting: highlight, highlight!

# Margin for printing in terminal.
const margin = 2

Expand All @@ -32,6 +35,26 @@ include("render/terminal/render.jl")

export @md_str, @doc_str

const MARKDOWN_FACES = [
:markdown_header => Face(weight=:bold),
:markdown_h1 => Face(height=1.25, inherit=:markdown_header),
:markdown_h2 => Face(height=1.20, inherit=:markdown_header),
:markdown_h3 => Face(height=1.15, inherit=:markdown_header),
:markdown_h4 => Face(height=1.12, inherit=:markdown_header),
:markdown_h5 => Face(height=1.08, inherit=:markdown_header),
:markdown_h6 => Face(height=1.05, inherit=:markdown_header),
:markdown_admonition => Face(weight=:bold),
:markdown_code => Face(inherit=:code),
:markdown_footnote => Face(inherit=:bright_yellow),
:markdown_hrule => Face(inherit=:shadow),
:markdown_inlinecode => Face(inherit=:markdown_code),
:markdown_latex => Face(inherit=:magenta),
:markdown_link => Face(underline=:bright_blue),
:markdown_list => Face(foreground=:blue),
]

__init__() = foreach(addface!, MARKDOWN_FACES)

parse(markdown::AbstractString; flavor = julia) = parse(IOBuffer(markdown), flavor = flavor)
parse_file(file::AbstractString; flavor = julia) = parse(read(file, String), flavor = flavor)

Expand Down
106 changes: 52 additions & 54 deletions stdlib/Markdown/src/render/terminal/formatting.jl
Original file line number Diff line number Diff line change
@@ -1,68 +1,66 @@
# This file is a part of Julia. License is MIT: https://julialang.org/license

# Wrapping
const AnnotIO = Union{AnnotatedIOBuffer, IOContext{AnnotatedIOBuffer}}

function ansi_length(s)
replace(s, r"\e\[[0-9]+m" => "") |> textwidth
function annotprint(f::Function, args...)
buf = AnnotatedIOBuffer()
f(buf, args...)
read(seekstart(buf), AnnotatedString)
end

words(s) = split(s, " ")
lines(s) = split(s, "\n")
"""
with_output_annotations(f::Function, io::AnnotIO, annots::Pair{Symbol, <:Any}...)
function wrapped_line(io::IO, s::AbstractString, width, i)
ws = words(s)
lines = String[]
for word in ws
word_length = ansi_length(word)
word_length == 0 && continue
if isempty(lines) || i + word_length + 1 > width
i = word_length
if length(lines) > 0
last_line = lines[end]
maybe_underline = findlast(Base.text_colors[:underline], last_line)
if !isnothing(maybe_underline)
# disable underline style at end of line if not already disabled.
maybe_disable_underline = max(
last(something(findlast(Base.disable_text_style[:underline], last_line), -1)),
last(something(findlast(Base.text_colors[:normal], last_line), -1)),
)
Call `f(io)`, and apply `annots` to the output created by doing so.
"""
function with_output_annotations(f::Function, io::AnnotIO, annots::Pair{Symbol, <:Any}...)
@nospecialize annots
aio = if io isa AnnotatedIOBuffer io else io.io end
start = position(aio) + 1
f(io)
stop = position(aio)
sortedindex = searchsortedlast(aio.annotations, (start:stop,), by=first)
for (i, annot) in enumerate(annots)
insert!(aio.annotations, sortedindex + i, (start:stop, annot))
end
end

if maybe_disable_underline < 0 || maybe_disable_underline < last(maybe_underline)
"""
wraplines(content::AnnotatedString, width::Integer = 80, column::Integer = 0)
lines[end] = last_line * Base.disable_text_style[:underline]
word = Base.text_colors[:underline] * word
end
Wrap `content` into a vector of lines of at most `width` (according to
`textwidth`), with the first line starting at `column`.
"""
function wraplines(content::Annot, width::Integer = 80, column::Integer = 0) where { Annot <: AnnotatedString}
s, lines = content.string, SubString{Annot}[]
i, lastwrap, slen = firstindex(s), 0, ncodeunits(s)
most_recent_break_oppotunity = 1
while i < slen
if isspace(s[i]) && s[i] != '\n'
most_recent_break_oppotunity = i
elseif s[i] == '\n'
push!(lines, content[nextind(s, lastwrap):prevind(s, i)])
lastwrap = i
column = 0
elseif column >= width && most_recent_break_oppotunity > 1
if lastwrap == most_recent_break_oppotunity
nextbreak = findfirst(isspace, @view s[nextind(s, lastwrap):end])
if isnothing(nextbreak)
break
else
most_recent_break_oppotunity = lastwrap + nextbreak
end
i = most_recent_break_oppotunity
end
push!(lines, word)
else
i += word_length + 1
lines[end] *= " " * word # this could be more efficient
push!(lines, content[nextind(s, lastwrap):prevind(s, most_recent_break_oppotunity)])
lastwrap = most_recent_break_oppotunity
column = 0
end
column += textwidth(s[i])
i = nextind(s, i)
end
return i, lines
end

function wrapped_lines(io::IO, s::AbstractString; width = 80, i = 0)
ls = String[]
for ss in lines(s)
i, line = wrapped_line(io, ss, width, i)
append!(ls, line)
if lastwrap < slen
push!(lines, content[nextind(s, lastwrap):end])
end
return ls
lines
end

wrapped_lines(io::IO, f::Function, args...; width = 80, i = 0) =
wrapped_lines(io, sprint(f, args...; context=io), width = width, i = 0)

function print_wrapped(io::IO, s...; width = 80, pre = "", i = 0)
lines = wrapped_lines(io, s..., width = width, i = i)
isempty(lines) && return 0, 0
print(io, lines[1])
for line in lines[2:end]
print(io, '\n', pre, line)
end
length(lines), length(pre) + ansi_length(lines[end])
end

print_wrapped(f::Function, io::IO, args...; kws...) = print_wrapped(io, f, args...; kws...)
Loading

0 comments on commit 52f70eb

Please sign in to comment.