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 May 2, 2024
1 parent c04d40d commit 337630b
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 147 deletions.
6 changes: 5 additions & 1 deletion doc/Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2
uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"
version = "8.6.0+0"

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

[[deps.LibGit2]]
deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"]
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"
Expand Down Expand Up @@ -145,7 +149,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
2 changes: 1 addition & 1 deletion stdlib/Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ uuid = "3a97d323-0669-5f0c-9066-3539efd106a3"
version = "4.2.1+0"

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

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
4 changes: 2 additions & 2 deletions stdlib/Markdown/src/render/rst.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function rst(io::IO, code::Code)
elseif code.language != "rst"
println(io, ".. code-block:: julia\n")
end
for l in lines(code.code)
for l in eachsplit(code.code, '\n')
println(io, " ", l)
end
end
Expand Down Expand Up @@ -90,7 +90,7 @@ end

function rst(io::IO, l::LaTeX)
println(io, ".. math::\n")
for line in lines(l.formula)
for line in eachsplit(l.formula, '\n')
println(io, " ", line)
end
end
Expand Down
108 changes: 54 additions & 54 deletions stdlib/Markdown/src/render/terminal/formatting.jl
Original file line number Diff line number Diff line change
@@ -1,68 +1,68 @@
# 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::Union{Annot, SubString{<:Annot}}, width::Integer = 80, column::Integer = 0) where { Annot <: AnnotatedString}
s, lines = String(content), SubString{Annot}[]
i, lastwrap, slen = firstindex(s), 0, ncodeunits(s)
most_recent_break_opportunity = 1
while i < slen
if isspace(s[i]) && s[i] != '\n'
most_recent_break_opportunity = i
elseif s[i] == '\n'
push!(lines, content[nextind(s, lastwrap):prevind(s, i)])
lastwrap = i
column = 0
elseif column >= width && most_recent_break_opportunity > 1
if lastwrap == most_recent_break_opportunity
nextbreak = findfirst(isspace, @view s[nextind(s, lastwrap):end])
if isnothing(nextbreak)
break
else
most_recent_break_opportunity = lastwrap + nextbreak
end
i = most_recent_break_opportunity
else
i = nextind(s, most_recent_break_opportunity)
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_opportunity)])
lastwrap = most_recent_break_opportunity
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 337630b

Please sign in to comment.