From c04d40dcef63e71592be6601f419d02b0556c96f Mon Sep 17 00:00:00 2001 From: TEC Date: Sun, 29 Oct 2023 12:47:26 +0800 Subject: [PATCH 1/6] Introduce MarkdownElement abstract type It's convenient for dispatch. --- stdlib/Markdown/src/Common/Common.jl | 2 ++ stdlib/Markdown/src/Common/block.jl | 16 ++++++++-------- stdlib/Markdown/src/Common/inline.jl | 10 +++++----- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/stdlib/Markdown/src/Common/Common.jl b/stdlib/Markdown/src/Common/Common.jl index 3036f2b4b730b..4bd3e5b4af8d6 100644 --- a/stdlib/Markdown/src/Common/Common.jl +++ b/stdlib/Markdown/src/Common/Common.jl @@ -1,5 +1,7 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license +abstract type MarkdownElement end + include("block.jl") include("inline.jl") diff --git a/stdlib/Markdown/src/Common/block.jl b/stdlib/Markdown/src/Common/block.jl index bd184b60c40fa..1b5cc1a752bcb 100644 --- a/stdlib/Markdown/src/Common/block.jl +++ b/stdlib/Markdown/src/Common/block.jl @@ -4,7 +4,7 @@ # Paragraphs # –––––––––– -mutable struct Paragraph +mutable struct Paragraph <: MarkdownElement content end @@ -39,7 +39,7 @@ end # Headers # ––––––– -mutable struct Header{level} +mutable struct Header{level} <: MarkdownElement text end @@ -95,7 +95,7 @@ end # Code # –––– -mutable struct Code +mutable struct Code <: MarkdownElement language::String code::String end @@ -124,7 +124,7 @@ end # Footnote # -------- -mutable struct Footnote +mutable struct Footnote <: MarkdownElement id::String text end @@ -159,7 +159,7 @@ end # Quotes # –––––– -mutable struct BlockQuote +mutable struct BlockQuote <: MarkdownElement content end @@ -188,7 +188,7 @@ end # Admonitions # ----------- -mutable struct Admonition +mutable struct Admonition <: MarkdownElement category::String title::String content::Vector @@ -246,7 +246,7 @@ end # Lists # ––––– -mutable struct List +mutable struct List <: MarkdownElement items::Vector{Any} ordered::Int # `-1` is unordered, `>= 0` is ordered. loose::Bool # TODO: Renderers should use this field @@ -332,7 +332,7 @@ pushitem!(list, buffer) = push!(list.items, parse(String(take!(buffer))).content # HorizontalRule # –––––––––––––– -mutable struct HorizontalRule +mutable struct HorizontalRule <: MarkdownElement end function horizontalrule(stream::IO, block::MD) diff --git a/stdlib/Markdown/src/Common/inline.jl b/stdlib/Markdown/src/Common/inline.jl index fda716a10fae7..a2a4140f80050 100644 --- a/stdlib/Markdown/src/Common/inline.jl +++ b/stdlib/Markdown/src/Common/inline.jl @@ -4,7 +4,7 @@ # Emphasis # –––––––– -mutable struct Italic +mutable struct Italic <: MarkdownElement text end @@ -20,7 +20,7 @@ function underscore_italic(stream::IO, md::MD) return result === nothing ? nothing : Italic(parseinline(result, md)) end -mutable struct Bold +mutable struct Bold <: MarkdownElement text end @@ -66,7 +66,7 @@ end # Images & Links # –––––––––––––– -mutable struct Image +mutable struct Image <: MarkdownElement url::String alt::String end @@ -85,7 +85,7 @@ function image(stream::IO, md::MD) end end -mutable struct Link +mutable struct Link <: MarkdownElement text url::String end @@ -156,7 +156,7 @@ end # Punctuation # ––––––––––– -mutable struct LineBreak end +mutable struct LineBreak <: MarkdownElement end @trigger '\\' -> function linebreak(stream::IO, md::MD) From 337630b034c97112f766e767d3dd2c5d31498a75 Mon Sep 17 00:00:00 2001 From: TEC Date: Sun, 29 Oct 2023 12:48:40 +0800 Subject: [PATCH 2/6] Reimplement Markdown printing using StyledStrings 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. --- doc/Manifest.toml | 6 +- stdlib/Manifest.toml | 2 +- stdlib/Markdown/Project.toml | 2 + stdlib/Markdown/src/GitHub/table.jl | 6 +- stdlib/Markdown/src/Markdown.jl | 25 ++- stdlib/Markdown/src/render/rst.jl | 4 +- .../src/render/terminal/formatting.jl | 108 +++++------ stdlib/Markdown/src/render/terminal/render.jl | 180 ++++++++++-------- stdlib/Markdown/test/runtests.jl | 19 +- test/precompile.jl | 1 + 10 files changed, 206 insertions(+), 147 deletions(-) diff --git a/doc/Manifest.toml b/doc/Manifest.toml index 254becb59a0c7..cb9d42561be5f 100644 --- a/doc/Manifest.toml +++ b/doc/Manifest.toml @@ -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" @@ -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" diff --git a/stdlib/Manifest.toml b/stdlib/Manifest.toml index 4e4f48b4e6af4..8d59f63c4733c 100644 --- a/stdlib/Manifest.toml +++ b/stdlib/Manifest.toml @@ -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" diff --git a/stdlib/Markdown/Project.toml b/stdlib/Markdown/Project.toml index b40de17b9422d..e2edcdefea537 100644 --- a/stdlib/Markdown/Project.toml +++ b/stdlib/Markdown/Project.toml @@ -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" diff --git a/stdlib/Markdown/src/GitHub/table.jl b/stdlib/Markdown/src/GitHub/table.jl index 29f956e9a0710..7c174007a75ba 100644 --- a/stdlib/Markdown/src/GitHub/table.jl +++ b/stdlib/Markdown/src/GitHub/table.jl @@ -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 diff --git a/stdlib/Markdown/src/Markdown.jl b/stdlib/Markdown/src/Markdown.jl index 93d8dbc39fc59..93c1bbe99ee80 100644 --- a/stdlib/Markdown/src/Markdown.jl +++ b/stdlib/Markdown/src/Markdown.jl @@ -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 @@ -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) diff --git a/stdlib/Markdown/src/render/rst.jl b/stdlib/Markdown/src/render/rst.jl index 752916c581a07..87c2fc6643e4e 100644 --- a/stdlib/Markdown/src/render/rst.jl +++ b/stdlib/Markdown/src/render/rst.jl @@ -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 @@ -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 diff --git a/stdlib/Markdown/src/render/terminal/formatting.jl b/stdlib/Markdown/src/render/terminal/formatting.jl index a031de4d9ad82..009fd2eb3af18 100644 --- a/stdlib/Markdown/src/render/terminal/formatting.jl +++ b/stdlib/Markdown/src/render/terminal/formatting.jl @@ -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...) diff --git a/stdlib/Markdown/src/render/terminal/render.jl b/stdlib/Markdown/src/render/terminal/render.jl index 20b1ef6d041fc..87a1c43b50c3e 100644 --- a/stdlib/Markdown/src/render/terminal/render.jl +++ b/stdlib/Markdown/src/render/terminal/render.jl @@ -16,118 +16,133 @@ end term(io::IO, md::MD, columns = cols(io)) = term(io, md.content, columns) function term(io::IO, md::Paragraph, columns) - print(io, ' '^margin) - print_wrapped(io, width = columns-2margin, pre = ' '^margin) do io - terminline(io, md.content) + lines = wraplines(annotprint(terminline, md.content), columns-2margin) + for (i, line) in enumerate(lines) + print(io, ' '^margin, line) + i < length(lines) && println(io) end end function term(io::IO, md::BlockQuote, columns) - s = sprint(term, md.content, columns - 10; context=io) - lines = split(rstrip(s), '\n') - print(io, ' '^margin, '│', lines[1]) - for i = 2:length(lines) - print(io, '\n', ' '^margin, '│', lines[i]) + content = annotprint(term, md.content, columns - 10) + lines = wraplines(rstrip(content), columns - 10) + for (i, line) in enumerate(lines) + print(io, ' '^margin, '│', line) + i < length(lines) && println(io) end end function term(io::IO, md::Admonition, columns) - col = :default - # If the types below are modified, the page manual/documentation.md must be updated accordingly. - if md.category == "danger" - col = Base.error_color() - elseif md.category == "warning" - col = Base.warn_color() - elseif md.category in ("info", "note") - col = Base.info_color() - elseif md.category == "tip" - col = :green + accent = if md.category == "danger" + :error + elseif md.category in ("warning", "info", "note", "tip") + Symbol(md.category) + elseif md.category == "compat" + :bright_cyan + else + :default end - printstyled(io, ' '^margin, "│ "; color=col, bold=true) - printstyled(io, isempty(md.title) ? md.category : md.title; color=col, bold=true) - printstyled(io, '\n', ' '^margin, '│', '\n'; color=col, bold=true) - s = sprint(term, md.content, columns - 10; context=io) - lines = split(rstrip(s), '\n') - for i in eachindex(lines) - printstyled(io, ' '^margin, '│'; color=col, bold=true) - print(io, lines[i]) - i < lastindex(lines) && println(io) + title = if isempty(md.title) md.category else md.title end + print(io, ' '^margin, styled"{$accent,markdown_admonition:│ $title}", + '\n', ' '^margin, styled"{$accent,markdown_admonition:│}", '\n') + content = annotprint(term, md.content, columns - 10) + lines = split(rstrip(content), '\n') + for (i, line) in enumerate(lines) + print(io, ' '^margin, styled"{$accent,markdown_admonition:│}", line) + i < length(lines) && println(io) end end function term(io::IO, f::Footnote, columns) print(io, ' '^margin, "│ ") - printstyled(io, "[^$(f.id)]", bold=true) + print(io, styled"{markdown_footnote:[^$(f.id)]}") println(io, '\n', ' '^margin, '│') - s = sprint(term, f.text, columns - 10; context=io) - lines = split(rstrip(s), '\n') - for i in eachindex(lines) - print(io, ' '^margin, '│', lines[i]) - i < lastindex(lines) && println(io) + content = annotprint(term, f.text, columns - 10) + lines = split(rstrip(content), '\n') + for (i, line) in enumerate(lines) + print(io, ' '^margin, '│', line) + i < length(lines) && println(io) end end function term(io::IO, md::List, columns) for (i, point) in enumerate(md.items) - print(io, ' '^2margin, isordered(md) ? "$(i + md.ordered - 1). " : "• ") - print_wrapped(io, width = columns-(4margin+2), pre = ' '^(2margin+3), - i = 2margin+2) do io - term(io, point, columns - 10) + bullet = isordered(md) ? "$(i + md.ordered - 1)." : "• " + print(io, ' '^2margin, styled"{markdown_list:$bullet} ") + content = annotprint(term, point, columns - 10) + lines = split(rstrip(content), '\n') + for (l, line) in enumerate(lines) + l > 1 && print(io, ' '^(2margin+3)) + print(io, lstrip(line)) + l < length(lines) && println(io) end - i < lastindex(md.items) && print(io, '\n', '\n') - end -end - -function _term_header(io::IO, md, char, columns) - text = terminline_string(io, md.text) - with_output_color(:bold, io) do io - pre = ' '^margin - print(io, pre) - line_no, lastline_width = print_wrapped(io, text, - width=columns - 4margin; pre) - line_width = min(lastline_width, columns) - if line_no > 1 - line_width = max(line_width, div(columns, 3)+length(pre)) - end - header_width = max(0, line_width-length(pre)) - char != ' ' && header_width > 0 && print(io, '\n', ' '^(margin), char^header_width) + i < length(md.items) && println(io) end end const _header_underlines = collect("≡=–-⋅ ") # TODO settle on another option with unicode e.g. "≡=≃–∼⋅" ? -function term(io::IO, md::Header{l}, columns) where l +function term(io::AnnotIO, md::Header{l}, columns) where l + face = Symbol("markdown_h$l") underline = _header_underlines[l] - _term_header(io, md, underline, columns) + pre = ' '^margin + local line_width + with_output_annotations(io, :face => face) do io + headline = annotprint(terminline, md.text) + lines = wraplines(headline, columns - 4margin) + for (i, line) in enumerate(lines) + print(io, pre, line) + i < length(lines) && println(io) + end + line_width = if length(lines) == 1 + min(textwidth(lines[end]), columns) + elseif length(lines) > 1 + max(textwidth(lines[end]), div(columns, 3)+length(pre)) + else + 0 + end + end + header_width = max(0, line_width) + if underline != ' ' && header_width > 0 + print(io, '\n', ' '^(margin)) + with_output_annotations(io -> print(io, underline^header_width), io, :face => face) + end end function term(io::IO, md::Code, columns) - with_output_color(:cyan, io) do io - L = lines(md.code) - for i in eachindex(L) - print(io, ' '^margin, L[i]) - i < lastindex(L) && println(io) - end + code = if md.language ∈ ("", "julia", "julia-repl", "jldoctest") + highlight(md.code) + else + styled"{markdown_code:$(md.code)}" + end + lines = split(code, '\n') + for (i, line) in enumerate(lines) + print(io, ' '^margin, line) + i < length(lines) && println(io) end end function term(io::IO, tex::LaTeX, columns) - printstyled(io, ' '^margin, tex.formula, color=:magenta) + print(io, ' '^margin, styled"{markdown_latex:$(tex.formula)}") end term(io::IO, br::LineBreak, columns) = nothing # line breaks already printed between subsequent elements function term(io::IO, br::HorizontalRule, columns) - print(io, ' '^margin, '─'^(columns - 2margin)) + print(io, ' '^margin, styled"{markdown_hrule:$('─'^(columns - 2margin))}") +end + +function term(io::IO, md::MarkdownElement, columns) + a = IOContext(AnnotatedIOBuffer(), io) + term(a, md, columns) + print(io, read(seekstart(a.io), AnnotatedString)) end term(io::IO, x, _) = show(io, MIME"text/plain"(), x) # Inline Content -terminline_string(io::IO, md) = sprint(terminline, md; context=io) - terminline(io::IO, content...) = terminline(io, collect(content)) function terminline(io::IO, content::Vector) @@ -140,12 +155,12 @@ function terminline(io::IO, md::AbstractString) print(io, replace(md, r"[\s\t\n]+" => ' ')) end -function terminline(io::IO, md::Bold) - with_output_color(terminline, :bold, io, md.text) +function terminline(io::AnnotIO, md::Bold) + with_output_annotations(io -> terminline(io, md.text), io, :face => :bold) end -function terminline(io::IO, md::Italic) - with_output_color(terminline, :underline, io, md.text) +function terminline(io::AnnotIO, md::Italic) + with_output_annotations(io -> terminline(io, md.text), io, :face => :italic) end function terminline(io::IO, md::LineBreak) @@ -156,20 +171,31 @@ function terminline(io::IO, md::Image) terminline(io, "(Image: $(md.alt))") end -terminline(io::IO, f::Footnote) = with_output_color(terminline, :bold, io, "[^$(f.id)]") +function terminline(io::IO, f::Footnote) + print(io, styled"{markdown_footnote:[^$(f.id)]}") +end -function terminline(io::IO, md::Link) - url = !Base.startswith(md.url, "@ref") ? " ($(md.url))" : "" - text = terminline_string(io, md.text) - terminline(io, text, url) +function terminline(io::AnnotIO, md::Link) + annots = if occursin(r"^(https?|file)://", md.url) + (:face => :markdown_link, :link => md.url) + else + (:face => :markdown_link,) + end + with_output_annotations(io -> terminline(io, md.text), io, annots...) end function terminline(io::IO, code::Code) - printstyled(io, code.code, color=:cyan) + print(io, styled"{markdown_inlinecode:$(code.code)}") end function terminline(io::IO, tex::LaTeX) - printstyled(io, tex.formula, color=:magenta) + print(io, styled"{markdown_latex:$(tex.formula)}") +end + +function terminline(io::IO, md::MarkdownElement) + a = IOContext(AnnotatedIOBuffer(), io) + terminline(a, md) + print(io, read(seekstart(a.io), AnnotatedString)) end terminline(io::IO, x) = show(io, MIME"text/plain"(), x) diff --git a/stdlib/Markdown/test/runtests.jl b/stdlib/Markdown/test/runtests.jl index 116282a0bea3b..f787bcabb3d21 100644 --- a/stdlib/Markdown/test/runtests.jl +++ b/stdlib/Markdown/test/runtests.jl @@ -1,6 +1,6 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license -using Test, Markdown +using Test, Markdown, StyledStrings import Markdown: MD, Paragraph, Header, Italic, Bold, LineBreak, plain, term, html, rst, Table, Code, LaTeX, Footnote import Base: show @@ -233,7 +233,7 @@ World""" |> plain == "Hello\n\n---\n\nWorld\n" # multiple whitespace is ignored @test sprint(term, md"a b") == " a b" -@test sprint(term, md"[x](https://julialang.org)") == " x (https://julialang.org)" +@test sprint(term, md"[x](https://julialang.org)") == " x" @test sprint(term, md"[x](@ref)") == " x" @test sprint(term, md"[x](@ref something)") == " x" @test sprint(term, md"![x](https://julialang.org)") == " (Image: x)" @@ -377,7 +377,7 @@ table = md""" let out = @test sprint(show, "text/plain", book) == " Title\n ≡≡≡≡≡\n\n Some discussion\n\n │ A quote\n\n Section important\n =================\n\n Some bolded\n\n • list1\n\n • list2" - @test sprint(show, "text/plain", md"#") == " " # edge case of empty header + @test sprint(show, "text/plain", md"#") == "" # edge case of empty header @test sprint(show, "text/markdown", book) == """ # Title @@ -1157,7 +1157,7 @@ let buf = IOBuffer() show(buf, "text/markdown", md"*emph*") @test String(take!(buf)) == "*emph*\n" show(IOContext(buf, :color=>true), "text/plain", md"*emph*") - @test String(take!(buf)) == " \e[4memph\e[24m" + @test String(take!(buf)) in (" \e[3memph\e[23m", " \e[4memph\e[24m") end let word = "Markdown" # disable underline when wrapping lines @@ -1166,8 +1166,8 @@ let word = "Markdown" # disable underline when wrapping lines long_italic_text = Markdown.parse('_' * join(fill(word, 10), ' ') * '_') show(ctx, MIME("text/plain"), long_italic_text) lines = split(String(take!(buf)), '\n') - @test endswith(lines[begin], Base.disable_text_style[:underline]) - @test startswith(lines[begin+1], ' '^Markdown.margin * Base.text_colors[:underline]) + @test endswith(lines[begin], r"\e\[2[34]m") + @test startswith(lines[begin+1], Regex(' '^Markdown.margin * "\e\\[[34]m")) end let word = "Markdown" # pre is of size Markdown.margin when wrapping title @@ -1176,7 +1176,9 @@ let word = "Markdown" # pre is of size Markdown.margin when wrapping title long_title = Markdown.parse("# " * join(fill(word, 3))) show(ctx, MIME("text/plain"), long_title) lines = split(String(take!(buf)), '\n') - @test all(startswith(Base.text_colors[:bold] * ' '^Markdown.margin), lines) + @test all(l -> startswith(l, ' '^Markdown.margin * StyledStrings.ANSI_STYLE_CODES.bold_weight) || + startswith(l, StyledStrings.ANSI_STYLE_CODES.bold_weight * ' '^Markdown.margin), + lines) end struct Struct49454 end @@ -1259,8 +1261,9 @@ end s = @md_str """ Misc:\\ - line\\ + break """ - @test sprint(show, MIME("text/plain"), s) == " Misc:\n - line" + @test sprint(show, MIME("text/plain"), s) == " Misc:\n - line\n break" end @testset "pullrequest #41552: a code block has \\end{verbatim}" begin diff --git a/test/precompile.jl b/test/precompile.jl index ffd6e24789aba..ea99653fe89be 100644 --- a/test/precompile.jl +++ b/test/precompile.jl @@ -445,6 +445,7 @@ precompile_test_harness(false) do dir # and their dependencies Dict(Base.PkgId(Base.root_module(Base, :SHA)) => Base.module_build_id(Base.root_module(Base, :SHA))), Dict(Base.PkgId(Base.root_module(Base, :Markdown)) => Base.module_build_id(Base.root_module(Base, :Markdown))), + Dict(Base.PkgId(Base.root_module(Base, :JuliaSyntaxHighlighting)) => Base.module_build_id(Base.root_module(Base, :JuliaSyntaxHighlighting))), Dict(Base.PkgId(Base.root_module(Base, :StyledStrings)) => Base.module_build_id(Base.root_module(Base, :StyledStrings))), # and their dependencies Dict(Base.PkgId(Base.root_module(Base, :Base64)) => Base.module_build_id(Base.root_module(Base, :Base64))), From d998d7c746a965514f6aba978cbfc16e70ecd5cf Mon Sep 17 00:00:00 2001 From: TEC Date: Tue, 31 Oct 2023 18:03:54 +0800 Subject: [PATCH 3/6] Variable spacing markdown list rendering The spacing between list items might as well represent whether the list is a tight or loose list. --- stdlib/Markdown/src/render/terminal/render.jl | 2 +- stdlib/Markdown/test/runtests.jl | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/stdlib/Markdown/src/render/terminal/render.jl b/stdlib/Markdown/src/render/terminal/render.jl index 87a1c43b50c3e..3b2c2ab8264dd 100644 --- a/stdlib/Markdown/src/render/terminal/render.jl +++ b/stdlib/Markdown/src/render/terminal/render.jl @@ -76,7 +76,7 @@ function term(io::IO, md::List, columns) print(io, lstrip(line)) l < length(lines) && println(io) end - i < length(md.items) && println(io) + i < length(md.items) && print(io, '\n'^(1 + md.loose)) end end diff --git a/stdlib/Markdown/test/runtests.jl b/stdlib/Markdown/test/runtests.jl index f787bcabb3d21..a3026683ad1e7 100644 --- a/stdlib/Markdown/test/runtests.jl +++ b/stdlib/Markdown/test/runtests.jl @@ -298,6 +298,7 @@ end let doc = md""" 1. a bc def ghij a bc def ghij a bc def ghij a bc def ghij a bc def ghij a bc def ghij a bc def ghij a bc def ghij a bc def ghij + 2. a bc def ghij a bc def ghij a bc def ghij a bc def ghij a bc def ghij a bc def ghij a bc def ghij a bc def ghij a bc def ghij """ str = sprint(term, doc, 50) @@ -376,7 +377,7 @@ table = md""" # mime output let out = @test sprint(show, "text/plain", book) == - " Title\n ≡≡≡≡≡\n\n Some discussion\n\n │ A quote\n\n Section important\n =================\n\n Some bolded\n\n • list1\n\n • list2" + " Title\n ≡≡≡≡≡\n\n Some discussion\n\n │ A quote\n\n Section important\n =================\n\n Some bolded\n\n • list1\n • list2" @test sprint(show, "text/plain", md"#") == "" # edge case of empty header @test sprint(show, "text/markdown", book) == """ From 80dbf1e2fb38bce29de71a8851826623fcec068a Mon Sep 17 00:00:00 2001 From: TEC Date: Thu, 11 Apr 2024 02:11:09 +0800 Subject: [PATCH 4/6] Synchronise Markdown rendering to RST with term It seems to make sense not to treat everything other than "rst" as Julia. We may as well follow the same heuristics as the terminal rendering for consistency. --- stdlib/Markdown/src/render/rst.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stdlib/Markdown/src/render/rst.jl b/stdlib/Markdown/src/render/rst.jl index 87c2fc6643e4e..ff37bd3e2610c 100644 --- a/stdlib/Markdown/src/render/rst.jl +++ b/stdlib/Markdown/src/render/rst.jl @@ -23,8 +23,11 @@ end function rst(io::IO, code::Code) if code.language == "jldoctest" println(io, ".. doctest::\n") - elseif code.language != "rst" + elseif code.language in ("", "julia", "julia-repl") println(io, ".. code-block:: julia\n") + elseif code.language == "rst" + else + println(io, "::\n") end for l in eachsplit(code.code, '\n') println(io, " ", l) From f2e11552b51975f5023da349de4a4ff45877b05c Mon Sep 17 00:00:00 2001 From: TEC Date: Thu, 11 Apr 2024 02:20:09 +0800 Subject: [PATCH 5/6] Specially render "styled" Markdown code blocks Since we're already using StyledStrings for rendering Julia in the terminal, we can also handle "styled"-labelled code blocks fancily, thanks to the `styled` function provided by StyledStrings. In all non-terminal contexts, the styling metadata is simply discarded, but could be used in the future (for instance StyledStrings currently supports HTML output too). --- stdlib/Markdown/src/Markdown.jl | 2 +- stdlib/Markdown/src/render/html.jl | 6 ++++++ stdlib/Markdown/src/render/latex.jl | 3 +++ stdlib/Markdown/src/render/rst.jl | 3 +++ stdlib/Markdown/src/render/terminal/render.jl | 9 ++++++++- 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/stdlib/Markdown/src/Markdown.jl b/stdlib/Markdown/src/Markdown.jl index 93c1bbe99ee80..935b5d981d6f9 100644 --- a/stdlib/Markdown/src/Markdown.jl +++ b/stdlib/Markdown/src/Markdown.jl @@ -12,7 +12,7 @@ module Markdown import Base: AnnotatedString, AnnotatedIOBuffer, show, ==, with_output_color, mapany using Base64: stringmime -using StyledStrings: StyledStrings, Face, addface!, @styled_str +using StyledStrings: StyledStrings, Face, addface!, @styled_str, styled using JuliaSyntaxHighlighting: highlight, highlight! # Margin for printing in terminal. diff --git a/stdlib/Markdown/src/render/html.jl b/stdlib/Markdown/src/render/html.jl index e7d436f2ccbda..829fa6c7bf986 100644 --- a/stdlib/Markdown/src/render/html.jl +++ b/stdlib/Markdown/src/render/html.jl @@ -67,6 +67,9 @@ end function html(io::IO, code::Code) withtag(io, :pre) do + if code.language == "styled" + code = Code("", String(styled(code.code))) + end maybe_lang = !isempty(code.language) ? Any[:class=>"language-$(code.language)"] : [] withtag(io, :code, maybe_lang...) do htmlesc(io, code.code) @@ -134,6 +137,9 @@ function htmlinline(io::IO, content::Vector) end function htmlinline(io::IO, code::Code) + if code.language == "styled" + code = Code("", String(styled(code.code))) + end withtag(io, :code) do htmlesc(io, code.code) end diff --git a/stdlib/Markdown/src/render/latex.jl b/stdlib/Markdown/src/render/latex.jl index df52b2849f2b0..fad0508ce0e59 100644 --- a/stdlib/Markdown/src/render/latex.jl +++ b/stdlib/Markdown/src/render/latex.jl @@ -33,6 +33,9 @@ function latex(io::IO, header::Header{l}) where l end function latex(io::IO, code::Code) + if code.language == "styled" + code = Code("", String(styled(code.code))) + end occursin("\\end{verbatim}", code.code) && error("Cannot include \"\\end{verbatim}\" in a latex code block") wrapblock(io, "verbatim") do println(io, code.code) diff --git a/stdlib/Markdown/src/render/rst.jl b/stdlib/Markdown/src/render/rst.jl index ff37bd3e2610c..e441ee0495da0 100644 --- a/stdlib/Markdown/src/render/rst.jl +++ b/stdlib/Markdown/src/render/rst.jl @@ -26,6 +26,9 @@ function rst(io::IO, code::Code) elseif code.language in ("", "julia", "julia-repl") println(io, ".. code-block:: julia\n") elseif code.language == "rst" + elseif code.language == "styled" + code = Code("", String(styled(code.code))) + println(io, "::\n") else println(io, "::\n") end diff --git a/stdlib/Markdown/src/render/terminal/render.jl b/stdlib/Markdown/src/render/terminal/render.jl index 3b2c2ab8264dd..2afd0bd99ff9f 100644 --- a/stdlib/Markdown/src/render/terminal/render.jl +++ b/stdlib/Markdown/src/render/terminal/render.jl @@ -113,6 +113,8 @@ end function term(io::IO, md::Code, columns) code = if md.language ∈ ("", "julia", "julia-repl", "jldoctest") highlight(md.code) + elseif md.language == "styled" + styled(md.code) else styled"{markdown_code:$(md.code)}" end @@ -185,7 +187,12 @@ function terminline(io::AnnotIO, md::Link) end function terminline(io::IO, code::Code) - print(io, styled"{markdown_inlinecode:$(code.code)}") + body = if code.language == "styled" + styled(code.code) + else + code.code + end + print(io, styled"{markdown_inlinecode:$body}") end function terminline(io::IO, tex::LaTeX) From ada49c3f3635a6d3c1bc3000fbee439c0ce019ae Mon Sep 17 00:00:00 2001 From: TEC Date: Tue, 30 Apr 2024 23:26:36 +0800 Subject: [PATCH 6/6] Bump StyledStrings In the course of the markdown PR, an issue with the use of deepcopy in StyledStrings was revealed. This has now been fixed, and to obtain the fix StyledStrings is bumped. --- .../md5 | 1 - .../sha512 | 1 - .../md5 | 1 + .../sha512 | 1 + stdlib/StyledStrings.version | 2 +- 5 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 deps/checksums/StyledStrings-ac472083359dde956aed8c61d43b8158ac84d9ce.tar.gz/md5 delete mode 100644 deps/checksums/StyledStrings-ac472083359dde956aed8c61d43b8158ac84d9ce.tar.gz/sha512 create mode 100644 deps/checksums/StyledStrings-d7496d24d3f05536bce6a7eb4cd8ca05a75c02aa.tar.gz/md5 create mode 100644 deps/checksums/StyledStrings-d7496d24d3f05536bce6a7eb4cd8ca05a75c02aa.tar.gz/sha512 diff --git a/deps/checksums/StyledStrings-ac472083359dde956aed8c61d43b8158ac84d9ce.tar.gz/md5 b/deps/checksums/StyledStrings-ac472083359dde956aed8c61d43b8158ac84d9ce.tar.gz/md5 deleted file mode 100644 index 758a74bce9dae..0000000000000 --- a/deps/checksums/StyledStrings-ac472083359dde956aed8c61d43b8158ac84d9ce.tar.gz/md5 +++ /dev/null @@ -1 +0,0 @@ -6969fb6d2e8585d26beef865910ec8ef diff --git a/deps/checksums/StyledStrings-ac472083359dde956aed8c61d43b8158ac84d9ce.tar.gz/sha512 b/deps/checksums/StyledStrings-ac472083359dde956aed8c61d43b8158ac84d9ce.tar.gz/sha512 deleted file mode 100644 index 3d1ac8791e14d..0000000000000 --- a/deps/checksums/StyledStrings-ac472083359dde956aed8c61d43b8158ac84d9ce.tar.gz/sha512 +++ /dev/null @@ -1 +0,0 @@ -281292e8478d72ab66b84cbd4f42e5dc2dd5054e8c54a79de8f0c0537d28962b460e67fe71230ead6b02386b87d0423879d51ce53a2b2427ce55866d62d6ebde diff --git a/deps/checksums/StyledStrings-d7496d24d3f05536bce6a7eb4cd8ca05a75c02aa.tar.gz/md5 b/deps/checksums/StyledStrings-d7496d24d3f05536bce6a7eb4cd8ca05a75c02aa.tar.gz/md5 new file mode 100644 index 0000000000000..3a5fccdec0fba --- /dev/null +++ b/deps/checksums/StyledStrings-d7496d24d3f05536bce6a7eb4cd8ca05a75c02aa.tar.gz/md5 @@ -0,0 +1 @@ +a02cd2c8bedd83b74917cf3821c89f46 diff --git a/deps/checksums/StyledStrings-d7496d24d3f05536bce6a7eb4cd8ca05a75c02aa.tar.gz/sha512 b/deps/checksums/StyledStrings-d7496d24d3f05536bce6a7eb4cd8ca05a75c02aa.tar.gz/sha512 new file mode 100644 index 0000000000000..a042e4f306275 --- /dev/null +++ b/deps/checksums/StyledStrings-d7496d24d3f05536bce6a7eb4cd8ca05a75c02aa.tar.gz/sha512 @@ -0,0 +1 @@ +2e86daa832533f0369e66e359d7d8f47002f93525f83233c809007a13dfd05a201bcd273b3cb4f3eba2586e98cc9afa43c242f67dc18b91fc898d98a0bd8fde9 diff --git a/stdlib/StyledStrings.version b/stdlib/StyledStrings.version index 81a599f125406..2067083aec74b 100644 --- a/stdlib/StyledStrings.version +++ b/stdlib/StyledStrings.version @@ -1,4 +1,4 @@ STYLEDSTRINGS_BRANCH = main -STYLEDSTRINGS_SHA1 = ac472083359dde956aed8c61d43b8158ac84d9ce +STYLEDSTRINGS_SHA1 = d7496d24d3f05536bce6a7eb4cd8ca05a75c02aa STYLEDSTRINGS_GIT_URL := https://github.com/JuliaLang/StyledStrings.jl.git STYLEDSTRINGS_TAR_URL = https://api.github.com/repos/JuliaLang/StyledStrings.jl/tarball/$1