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 Oct 29, 2023
1 parent 59d2f93 commit 0bfb3b2
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 139 deletions.
8 changes: 6 additions & 2 deletions doc/Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ git-tree-sha1 = "3c837543ddb02250ef42f4738347454f95079d4e"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "0.21.3"

[[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 All @@ -68,7 +72,7 @@ uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"

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

[[deps.MbedTLS_jll]]
Expand All @@ -94,7 +98,7 @@ deps = ["Unicode"]
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"

[[deps.REPL]]
deps = ["InteractiveUtils", "Markdown", "Sockets", "StyledStrings", "Unicode"]
deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"]
uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"

[[deps.Random]]
Expand Down
8 changes: 4 additions & 4 deletions pkgimage.mk
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,11 @@ $(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,Tar,ArgTools,SHA))
$(eval $(call stdlib_builder,DelimitedFiles,Mmap))
$(eval $(call stdlib_builder,JuliaSyntaxHighlighting,))
$(eval $(call stdlib_builder,JuliaSyntaxHighlighting,StyledStrings))

# 2-depth packages
$(eval $(call stdlib_builder,LLD_jll,Zlib_jll libLLVM_jll Artifacts Libdl))
Expand All @@ -107,19 +106,20 @@ $(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,InteractiveUtils,Markdown))
$(eval $(call stdlib_builder,SharedArrays,Distributed Mmap Random Serialization))
$(eval $(call stdlib_builder,TOML,Dates))
$(eval $(call stdlib_builder,Test,Logging Random Serialization InteractiveUtils))

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

# 5-depth packages
$(eval $(call stdlib_builder,Downloads,ArgTools FileWatching LibCURL NetworkOptions))
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 @@ -5,9 +5,12 @@ Tools for working with the Markdown file format. Mainly for documentation.
"""
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 @@ -28,6 +31,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
105 changes: 51 additions & 54 deletions stdlib/Markdown/src/render/terminal/formatting.jl
Original file line number Diff line number Diff line change
@@ -1,68 +1,65 @@
# 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(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 f annots
aio = if io isa AnnotatedIOBuffer io else io.io end
start = position(aio) + 1
f(io)
stop = position(aio)
for annot in annots
push!(aio.annotations, (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 s[i] == ' '
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(' ', @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 0bfb3b2

Please sign in to comment.