From e8107402fa92ae01e0386ee7b1fc2c1b6af393e1 Mon Sep 17 00:00:00 2001 From: Michael Hatherly Date: Fri, 8 May 2015 10:32:00 +0200 Subject: [PATCH] Backport Markdown from Julia 0.4. --- REQUIRE | 3 +- src/Common/Common.jl | 9 +- src/Common/block.jl | 276 +++++++++++++++++++----------- src/Common/inline.jl | 105 +++++++----- src/GitHub/GitHub.jl | 86 ++++++---- src/GitHub/table.jl | 146 ++++++++++++++++ src/IPython/IPython.jl | 30 ++-- src/Julia/Julia.jl | 8 +- src/Julia/interp.jl | 58 ++++--- src/Markdown.jl | 72 +++++--- src/parse/config.jl | 91 +++++----- src/parse/parse.jl | 102 ++++++----- src/parse/util.jl | 225 +++++++++++++----------- src/render/html.jl | 148 ++++++++++------ src/render/latex.jl | 166 ++++++++++-------- src/render/plain.jl | 60 ++++--- src/render/rich.jl | 22 +-- src/render/terminal/formatting.jl | 124 +++++++------- src/render/terminal/render.jl | 105 +++++++----- test/markdown.jl | 178 +++++++++++++++++++ test/runtests.jl | 9 +- 21 files changed, 1323 insertions(+), 700 deletions(-) create mode 100644 src/GitHub/table.jl create mode 100644 test/markdown.jl diff --git a/REQUIRE b/REQUIRE index e2749de..a2a2155 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1 +1,2 @@ -Lazy +Compat + diff --git a/src/Common/Common.jl b/src/Common/Common.jl index b98cb68..6b58077 100644 --- a/src/Common/Common.jl +++ b/src/Common/Common.jl @@ -1,5 +1,10 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + include("block.jl") include("inline.jl") -@flavor common [list, indentcode, blockquote, hashheader, paragraph, - escapes, en_dash, inline_code, asterisk_bold, asterisk_italic, image, link] +@flavor common [list, indentcode, blockquote, hashheader, horizontalrule, + paragraph, + + linebreak, escapes, inline_code, + asterisk_bold, asterisk_italic, image, link] diff --git a/src/Common/block.jl b/src/Common/block.jl index 4b0379a..0b735f0 100644 --- a/src/Common/block.jl +++ b/src/Common/block.jl @@ -1,32 +1,34 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + # –––––––––– # Paragraphs # –––––––––– type Paragraph - content + content end -Paragraph() = Paragraph({}) - -function paragraph(stream::IO, md::MD, config::Config) - buffer = IOBuffer() - p = Paragraph() - push!(md, p) - skipwhitespace(stream) - while !eof(stream) - char = read(stream, Char) - if char == '\n' || char == '\r' - if blankline(stream) || parse(stream, md, config, breaking = true) - break - else - write(buffer, ' ') - end - else - write(buffer, char) +Paragraph() = Paragraph([]) + +function paragraph(stream::IO, md::MD) + buffer = IOBuffer() + p = Paragraph() + push!(md, p) + skipwhitespace(stream) + while !eof(stream) + char = read(stream, Char) + if char == '\n' || char == '\r' + if blankline(stream) || parse(stream, md, breaking = true) + break + else + write(buffer, ' ') + end + else + write(buffer, char) + end end - end - p.content = parseinline(seek(buffer, 0), config) - return true + p.content = parseinline(seek(buffer, 0), md) + return true end # ––––––– @@ -34,29 +36,55 @@ end # ––––––– type Header{level} - text + text end Header(s, level::Int) = Header{level}(s) Header(s) = Header(s, 1) @breaking true -> -function hashheader(stream::IO, md::MD, config::Config) - startswith(stream, "#") || return false - level = 1 - while startswith(stream, "#") - level += 1 - end - h = readline(stream) |> chomp - h = match(r"\s*(.*)(? 6 && return false + + c = ' ' + # Allow empty headers, but require a space + !eof(stream) && (c = read(stream, Char); !(c in " \n")) && + return false + + if c != '\n' # Empty header + h = readline(stream) |> strip + h = match(r"(.*?)( +#+)?$", h).captures[1] + buffer = IOBuffer() + print(buffer, h) + push!(md.content, Header(parseinline(seek(buffer, 0), md), level)) + else + push!(md.content, Header("", level)) + end + return true + end +end + +function setextheader(stream::IO, md::MD) + withstream(stream) do + eatindent(stream) || return false + header = readline(stream) |> strip + header == "" && return false + + eatindent(stream) || return false + underline = readline(stream) |> strip + length(underline) < 3 && return false + u = underline[1] + u in "-=" || return false + all(c -> c == u, underline) || return false + level = (u == '=') ? 1 : 2 + + push!(md.content, Header(parseinline(header, md), level)) + return true + end end # –––– @@ -64,22 +92,28 @@ end # –––– type Code - language::UTF8String - code::UTF8String + language::UTF8String + code::UTF8String end Code(code) = Code("", code) -function indentcode(stream::IO, block::MD, config::Config) - withstream(stream) do - buffer = IOBuffer() - while startswith(stream, " ") || startswith(stream, "\t") - write(buffer, readline(stream)) +function indentcode(stream::IO, block::MD) + withstream(stream) do + buffer = IOBuffer() + while !eof(stream) + if startswith(stream, " ") || startswith(stream, "\t") + write(buffer, readline(stream)) + elseif blankline(stream) + write(buffer, '\n') + else + break + end + end + code = takebuf_string(buffer) + !isempty(code) && (push!(block, Code(rstrip(code))); return true) + return false end - code = takebuf_string(buffer) - !isempty(code) && (push!(block, Code(chomp(code))); return true) - return false - end end # –––––– @@ -87,28 +121,28 @@ end # –––––– type BlockQuote - content + content end -BlockQuote() = BlockQuote({}) +BlockQuote() = BlockQuote([]) # TODO: Laziness @breaking true -> -function blockquote(stream::IO, block::MD, config::Config) - withstream(stream) do - buffer = IOBuffer() - while startswith(stream, ">") - startswith(stream, " ") - write(buffer, readline(stream)) - end - md = takebuf_string(buffer) - if !isempty(md) - push!(block, BlockQuote(parse(md, flavor = config).content)) - return true - else - return false +function blockquote(stream::IO, block::MD) + withstream(stream) do + buffer = IOBuffer() + empty = true + while eatindent(stream) && startswith(stream, '>') + startswith(stream, " ") + write(buffer, readline(stream)) + empty = false + end + empty && return false + + md = takebuf_string(buffer) + push!(block, BlockQuote(parse(md, flavor = config(block)).content)) + return true end - end end # ––––– @@ -116,51 +150,89 @@ end # ––––– type List - items::Vector{Any} - ordered::Bool + items::Vector{Any} + ordered::Bool - List(x::AbstractVector) = new(x) + List(x::AbstractVector, b::Bool) = new(x, b) + List(x::AbstractVector) = new(x, false) + List(b::Bool) = new(Any[], b) end -List(xs...) = List([xs...]) +List(xs...) = List(vcat(xs...)) -const bullets = ["* ", "• ", "+ ", "- "] +const bullets = "*•+-" +const num_or_bullets = r"^(\*|•|\+|-|\d+(\.|\))) " # Todo: ordered lists, inline formatting -function list(stream::IO, block::MD, config::Config) - withstream(stream) do - skipwhitespace(stream) - startswith(stream, bullets) || return false - the_list = List() - buffer = IOBuffer() - fresh_line = false - while !eof(stream) - if fresh_line - skipwhitespace(stream) - if startswith(stream, bullets) - push!(the_list.items, parseinline(takebuf_string(buffer), config)) - buffer = IOBuffer() - else - write(buffer, ' ') +function list(stream::IO, block::MD) + withstream(stream) do + eatindent(stream) || return false + b = startswith(stream, num_or_bullets) + (b == nothing || b == "") && return false + ordered = !(b[1] in bullets) + if ordered + b = b[end - 1] == '.' ? r"^\d+\. " : r"^\d+\) " + # TODO start value end + the_list = List(ordered) + + buffer = IOBuffer() fresh_line = false - else - c = read(stream, Char) - if c == '\n' - eof(stream) && break - next = peek(stream) - if next == '\n' - break - else - fresh_line = true - end - else - write(buffer, c) + while !eof(stream) + if fresh_line + sp = startswith(stream, r"^ {0,3}") + if !(startswith(stream, b) in [false, ""]) + push!(the_list.items, parseinline(takebuf_string(buffer), block)) + buffer = IOBuffer() + else + # TODO write a newline here, and deal with nested + write(buffer, ' ', sp) + end + fresh_line = false + else + c = read(stream, Char) + if c == '\n' + eof(stream) && break + next = peek(stream) + if next == '\n' + break + else + fresh_line = true + end + else + write(buffer, c) + end + end end - end + push!(the_list.items, parseinline(takebuf_string(buffer), block)) + push!(block, the_list) + return true end - push!(the_list.items, parseinline(takebuf_string(buffer), config)) - push!(block, the_list) - return true - end +end + +# –––––––––––––– +# HorizontalRule +# –––––––––––––– + +type HorizontalRule +end + +function horizontalrule(stream::IO, block::MD) + withstream(stream) do + n, rule = 0, ' ' + while !eof(stream) + char = read(stream, Char) + char == '\n' && break + isspace(char) && continue + if n==0 || char==rule + rule = char + n += 1 + else + return false + end + end + is_hr = (n ≥ 3 && rule in "*-") + is_hr && push!(block, HorizontalRule()) + return is_hr + end end diff --git a/src/Common/inline.jl b/src/Common/inline.jl index 8933aeb..1b86e30 100644 --- a/src/Common/inline.jl +++ b/src/Common/inline.jl @@ -1,25 +1,27 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + # –––––––– # Emphasis # –––––––– type Italic - text + text end @trigger '*' -> -function asterisk_italic(stream::IO) - result = parse_inline_wrapper(stream, "*") - return result == nothing ? nothing : Italic(parseinline(result)) +function asterisk_italic(stream::IO, md::MD) + result = parse_inline_wrapper(stream, "*") + return result == nothing ? nothing : Italic(parseinline(result, md)) end type Bold - text + text end @trigger '*' -> -function asterisk_bold(stream::IO) - result = parse_inline_wrapper(stream, "**") - return result == nothing ? nothing : Bold(parseinline(result)) +function asterisk_bold(stream::IO, md::MD) + result = parse_inline_wrapper(stream, "**") + return result == nothing ? nothing : Bold(parseinline(result, md)) end # –––– @@ -27,9 +29,9 @@ end # –––– @trigger '`' -> -function inline_code(stream::IO) - result = parse_inline_wrapper(stream, "`") - return result == nothing ? nothing : Code(result) +function inline_code(stream::IO, md::MD) + result = parse_inline_wrapper(stream, "`"; rep=true) + return result == nothing ? nothing : Code(result) end # –––––––––––––– @@ -37,61 +39,70 @@ end # –––––––––––––– type Image - url::UTF8String - alt::UTF8String + url::UTF8String + alt::UTF8String end @trigger '!' -> -function image(stream::IO) - withstream(stream) do - startswith(stream, "![") || return - alt = readuntil(stream, ']', match = '[') - alt ≡ nothing && return - skipwhitespace(stream) - startswith(stream, '(') || return - url = readuntil(stream, ')', match = '(') - url ≡ nothing && return - return Image(url, alt) - end +function image(stream::IO, md::MD) + withstream(stream) do + startswith(stream, "![") || return + alt = readuntil(stream, ']', match = '[') + alt ≡ nothing && return + skipwhitespace(stream) + startswith(stream, '(') || return + url = readuntil(stream, ')', match = '(') + url ≡ nothing && return + return Image(url, alt) + end end type Link - text - url::UTF8String + text + url::UTF8String end @trigger '[' -> -function link(stream::IO) - withstream(stream) do - startswith(stream, '[') || return - text = readuntil(stream, ']', match = '[') - text ≡ nothing && return - skipwhitespace(stream) - startswith(stream, '(') || return - url = readuntil(stream, ')', match = '(') - url ≡ nothing && return - return Link(parseinline(text), url) - end +function link(stream::IO, md::MD) + withstream(stream) do + startswith(stream, '[') || return + text = readuntil(stream, ']', match = '[') + text ≡ nothing && return + skipwhitespace(stream) + startswith(stream, '(') || return + url = readuntil(stream, ')', match = '(') + url ≡ nothing && return + return Link(parseinline(text, md), url) + end end # ––––––––––– # Punctuation # ––––––––––– +type LineBreak end + +@trigger '\\' -> +function linebreak(stream::IO, md::MD) + if startswith(stream, "\\\n") + return LineBreak() + end +end + @trigger '-' -> -function en_dash(stream::IO) - if startswith(stream, "--") - return "–" - end +function en_dash(stream::IO, md::MD) + if startswith(stream, "--") + return "–" + end end -const escape_chars = "\\`*_#+-.!{[(" +const escape_chars = "\\`*_#+-.!{[(\$" @trigger '\\' -> -function escapes(stream::IO) - withstream(stream) do - if startswith(stream, "\\") && !eof(stream) && (c = read(stream, Char)) in escape_chars - return string(c) +function escapes(stream::IO, md::MD) + withstream(stream) do + if startswith(stream, "\\") && !eof(stream) && (c = read(stream, Char)) in escape_chars + return string(c) + end end - end end diff --git a/src/GitHub/GitHub.jl b/src/GitHub/GitHub.jl index 74cc956..c0ed6b9 100644 --- a/src/GitHub/GitHub.jl +++ b/src/GitHub/GitHub.jl @@ -1,39 +1,61 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + +include("table.jl") + @breaking true -> -function fencedcode(stream::IO, block::MD, config::Config) - startswith(stream, "```", padding = true) || return false - readline(stream) - buffer = IOBuffer() - while !eof(stream) - startswith(stream, "```") && break - write(buffer, readline(stream)) - end - push!(block, Code(takebuf_string(buffer) |> chomp)) - return true +function fencedcode(stream::IO, block::MD) + withstream(stream) do + startswith(stream, "~~~", padding = true) || startswith(stream, "```", padding = true) || return false + skip(stream, -1) + ch = read(stream, Char) + trailing = strip(readline(stream)) + flavor = lstrip(trailing, ch) + n = 3 + length(trailing) - length(flavor) + + # inline code block + ch in flavor && return false + + buffer = IOBuffer() + while !eof(stream) + line_start = position(stream) + if startswith(stream, string(ch) ^ n) + if !startswith(stream, string(ch)) + push!(block, Code(flavor, takebuf_string(buffer) |> chomp)) + return true + else + seek(stream, line_start) + end + end + write(buffer, readline(stream)) + end + return false + end end -function github_paragraph(stream::IO, md::MD, config::Config) - skipwhitespace(stream) - buffer = IOBuffer() - p = Paragraph() - push!(md, p) - while !eof(stream) - char = read(stream, Char) - if char == '\n' - eof(stream) && break - if blankline(stream) || parse(stream, md, config, breaking = true) - break - else - write(buffer, '\n') - end - else - write(buffer, char) +function github_paragraph(stream::IO, md::MD) + skipwhitespace(stream) + buffer = IOBuffer() + p = Paragraph() + push!(md, p) + while !eof(stream) + char = read(stream, Char) + if char == '\n' + eof(stream) && break + if blankline(stream) || parse(stream, md, breaking = true) + break + else + write(buffer, '\n') + end + else + write(buffer, char) + end end - end - p.content = parseinline(seek(buffer, 0), config) - return true + p.content = parseinline(seek(buffer, 0), md) + return true end -# TODO: tables +@flavor github [list, indentcode, blockquote, fencedcode, hashheader, + github_table, github_paragraph, -@flavor github [list, indentcode, blockquote, fencedcode, hashheader, github_paragraph, - en_dash, inline_code, asterisk_bold, asterisk_italic, image, link] + linebreak, escapes, en_dash, inline_code, asterisk_bold, + asterisk_italic, image, link] diff --git a/src/GitHub/table.jl b/src/GitHub/table.jl new file mode 100644 index 0000000..c8ab061 --- /dev/null +++ b/src/GitHub/table.jl @@ -0,0 +1,146 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + +type Table + rows::Vector{Vector{Any}} + align::Vector{Symbol} +end + +function parserow(stream::IO) + withstream(stream) do + line = readline(stream) |> chomp + row = split(line, "|") + length(row) == 1 && return + row[1] == "" && shift!(row) + map!(strip, row) + row[end] == "" && pop!(row) + return row + end +end + +function rowlength!(row, len) + while length(row) < len push!(row, "") end + while length(row) > len pop!(row) end + return row +end + +const default_align = :r + +function parsealign(row) + align = Symbol[] + for s in row + (length(s) ≥ 3 && s ⊆ Set("-:")) || return + push!(align, + s[1] == ':' ? (s[end] == ':' ? :c : :l) : + s[end] == ':' ? :r : + default_align) + end + return align +end + +function github_table(stream::IO, md::MD) + withstream(stream) do + skipblank(stream) + rows = Any[] + cols = 0 + align = nothing + while (row = parserow(stream)) != nothing + if length(rows) == 0 + row[1] == "" && return false + cols = length(row) + end + if align == nothing && length(rows) == 1 # Must have a --- row + align = parsealign(row) + (align == nothing || length(align) != cols) && return false + else + push!(rows, map(x -> parseinline(x, md), rowlength!(row, cols))) + end + end + length(rows) <= 1 && return false + push!(md, Table(rows, align)) + return true + end +end + +function html(io::IO, md::Table) + withtag(io, :table) do + for (i, row) in enumerate(md.rows) + withtag(io, :tr) do + for c in md.rows[i] + withtag(io, i == 1 ? :th : :td) do + htmlinline(io, c) + end + end + end + end + end +end + +mapmap(f, xss) = map(xs->map(f, xs), xss) + +colwidths(rows; len = length, min = 0) = + max(min, convert(Vector{Vector{Int}}, mapmap(len, rows))...) + +padding(width, twidth, a) = + a == :l ? (0, twidth - width) : + a == :r ? (twidth - width, 0) : + a == :c ? (floor(Int, (twidth-width)/2), ceil(Int, (twidth-width)/2)) : + error("Invalid alignment $a") + +function padcells!(rows, align; len = length, min = 0) + widths = colwidths(rows, len = len, min = min) + for i = 1:length(rows), j = 1:length(rows[1]) + cell = rows[i][j] + lpad, rpad = padding(len(cell), widths[j], align[j]) + rows[i][j] = " "^lpad * cell * " "^rpad + end + return rows +end + +_dash(width, align) = + align == :l ? ":" * "-"^(width-1) : + align == :r ? "-"^(width-1) * ":" : + align == :c ? ":" * "-"^(width-2) * ":" : + throw(ArgumentError("Invalid alignment $align")) + +function plain(io::IO, md::Table) + cells = mapmap(plaininline, md.rows) + padcells!(cells, md.align, len = length, min = 3) + for i = 1:length(cells) + print_joined(io, cells[i], " | ") + println(io) + if i == 1 + print_joined(io, [_dash(length(cells[i][j]), md.align[j]) for j = 1:length(cells[1])], " | ") + println(io) + end + end +end + +function term(io::IO, md::Table, columns) + cells = mapmap(terminline, md.rows) + padcells!(cells, md.align, len = ansi_length) + for i = 1:length(cells) + print_joined(io, cells[i], " ") + println(io) + if i == 1 + print_joined(io, ["–"^ansi_length(cells[i][j]) for j = 1:length(cells[1])], " ") + println(io) + end + end +end + +function latex(io::IO, md::Table) + wrapblock(io, "tabular") do + align = md.align + println(io, "{$(join(align, " | "))}") + for (i, row) in enumerate(md.rows) + for (j, cell) in enumerate(row) + j != 1 && print(io, " & ") + latexinline(io, cell) + end + println(io, " \\\\") + if i == 1 + println("\\hline") + end + end + end +end diff --git a/src/IPython/IPython.jl b/src/IPython/IPython.jl index f73ab12..fead30f 100644 --- a/src/IPython/IPython.jl +++ b/src/IPython/IPython.jl @@ -1,27 +1,29 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + type LaTeX - formula::UTF8String + formula::UTF8String end @trigger '$' -> -function latex(stream::IO) - result = parse_inline_wrapper(stream, "\$", rep = true) - return result == nothing ? nothing : LaTeX(result) +function tex(stream::IO, md::MD) + result = parse_inline_wrapper(stream, "\$", rep = true) + return result == nothing ? nothing : LaTeX(result) end -function blocktex(stream::IO, md::MD, config::Config) - withstream(stream) do - ex = latex(stream) - if ex ≡ nothing - return false - else - push!(md, ex) - return true +function blocktex(stream::IO, md::MD) + withstream(stream) do + ex = tex(stream, md) + if ex ≡ nothing + return false + else + push!(md, ex) + return true + end end - end end writemime(io::IO, ::MIME"text/plain", tex::LaTeX) = - print(io, '$', tex.formula, '$') + print(io, '$', tex.formula, '$') term(io::IO, tex::LaTeX, cols) = println_with_format(:magenta, io, tex.formula) terminline(io::IO, tex::LaTeX) = print_with_format(:magenta, io, tex.formula) diff --git a/src/Julia/Julia.jl b/src/Julia/Julia.jl index 58153e8..333fbbc 100644 --- a/src/Julia/Julia.jl +++ b/src/Julia/Julia.jl @@ -1,3 +1,5 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + """ This file contains markdown extensions designed to make documenting Julia easy peasy. @@ -8,7 +10,7 @@ We start by borrowing GitHub's `fencedcode` extension – more to follow. include("interp.jl") @flavor julia [blocktex, blockinterp, hashheader, list, indentcode, fencedcode, - blockquote, paragraph, + blockquote, github_table, horizontalrule, setextheader, paragraph, - escapes, latex, interp, en_dash, inline_code, asterisk_bold, - asterisk_italic, image, link] + linebreak, escapes, tex, interp, en_dash, inline_code, + asterisk_bold, asterisk_italic, image, link] diff --git a/src/Julia/interp.jl b/src/Julia/interp.jl index 899ee78..2a1514c 100644 --- a/src/Julia/interp.jl +++ b/src/Julia/interp.jl @@ -1,38 +1,40 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + function Base.parse(stream::IOBuffer; greedy::Bool = true, raise::Bool = true) - pos = position(stream) - ex, Δ = Base.parse(readall(stream), 1, greedy = greedy, raise = raise) - seek(stream, pos + Δ - 1) - return ex + pos = position(stream) + ex, Δ = Base.parse(readall(stream), 1, greedy = greedy, raise = raise) + seek(stream, pos + Δ - 1) + return ex end function interpinner(stream::IO, greedy = false) - startswith(stream, '$') || return - (eof(stream) || peek(stream) in whitespace) && return - try - return Base.parse(stream::IOBuffer, greedy = greedy) - catch e - return - end + startswith(stream, '$') || return + (eof(stream) || peek(stream) in whitespace) && return + try + return Base.parse(stream::IOBuffer, greedy = greedy) + catch e + return + end end @trigger '$' -> -function interp(stream::IO) - withstream(stream) do - ex = interpinner(stream) - return ex - end +function interp(stream::IO, md::MD) + withstream(stream) do + ex = interpinner(stream) + return ex + end end -function blockinterp(stream::IO, md::MD, config::Config) - withstream(stream) do - ex = interpinner(stream) - if ex ≡ nothing - return false - else - push!(md, ex) - return true +function blockinterp(stream::IO, md::MD) + withstream(stream) do + ex = interpinner(stream) + if ex ≡ nothing + return false + else + push!(md, ex) + return true + end end - end end toexpr(x) = x @@ -40,9 +42,9 @@ toexpr(x) = x toexpr(xs::Vector{Any}) = Expr(:cell1d, map(toexpr, xs)...) function deftoexpr(T) - @eval function toexpr(md::$T) - Expr(:call, $T, $(map(x->:(toexpr(md.$x)), names(T))...)) - end + @eval function toexpr(md::$T) + Expr(:call, typeof(md), $(map(x->:(toexpr(md.$x)), fieldnames(T))...)) + end end map(deftoexpr, [MD, Paragraph, Header, diff --git a/src/Markdown.jl b/src/Markdown.jl index 7610f29..05a4c95 100644 --- a/src/Markdown.jl +++ b/src/Markdown.jl @@ -1,7 +1,39 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + module Markdown +using Compat + +## Backport `Pair`. ## + +immutable Pair{A,B} + first::A + second::B +end + +Base.start(p::Pair) = 1 +Base.done(p::Pair, i) = i>2 +Base.next(p::Pair, i) = (getfield(p,i), i+1) + +Base.indexed_next(p::Pair, i::Int, state) = (getfield(p,i), i+1) + +Base.hash(p::Pair, h::UInt) = hash(p.second, hash(p.first, h)) + +==(p::Pair, q::Pair) = (p.first==q.first) & (p.second==q.second) +Base.isequal(p::Pair, q::Pair) = isequal(p.first,q.first) & isequal(p.second,q.second) + +Base.isless(p::Pair, q::Pair) = ifelse(!isequal(p.first,q.first), isless(p.first,q.first), + isless(p.second,q.second)) +Base.getindex(p::Pair,i::Int) = getfield(p,i) +Base.getindex(p::Pair,i::Real) = getfield(p, convert(Int, i)) +Base.reverse(p::Pair) = Pair(p.second, p.first) + +## End backport. ## + import Base: writemime +typealias String AbstractString + include("parse/config.jl") include("parse/util.jl") include("parse/parse.jl") @@ -13,11 +45,11 @@ include("Julia/Julia.jl") include("render/plain.jl") include("render/html.jl") -# include("render/latex.jl") +include("render/latex.jl") include("render/terminal/render.jl") -export readme, license, @md_str, @md_mstr, @doc_str, @doc_mstr +export readme, license, @md_str, @doc_str, @md_mstr, @doc_mstr parse(markdown::String; flavor = julia) = parse(IOBuffer(markdown), flavor = flavor) parse_file(file::String; flavor = julia) = parse(readall(file), flavor = flavor) @@ -29,42 +61,40 @@ license(pkg::String; flavor = github) = parse_file(Pkg.dir(pkg, "LICENSE.md"), f license(pkg::Module; flavor = github) = license(string(pkg), flavor = flavor) function mdexpr(s, flavor = :julia) - md = parse(s, flavor = symbol(flavor)) - esc(toexpr(md)) + md = parse(s, flavor = symbol(flavor)) + esc(toexpr(md)) end function docexpr(s, flavor = :julia) - quote - let md = $(mdexpr(s, flavor)) - md.meta[:path] = @__FILE__ - md.meta[:module] = current_module() - md + quote + let md = $(mdexpr(s, flavor)) + md.meta[:path] = @__FILE__ + md.meta[:module] = current_module() + md + end end - end end macro md_str(s, t...) - mdexpr(s, t...) + mdexpr(s, t...) end -macro md_mstr(s, t...) - s = Base.triplequoted(s) - mdexpr(s, t...) +macro doc_str(s, t...) + docexpr(s, t...) end -macro doc_str(s, t...) - docexpr(s, t...) +macro md_mstr(s, t...) + mdexpr(Base.triplequoted(s), t...) end macro doc_mstr(s, t...) - s = Base.triplequoted(s) - docexpr(s, t...) + docexpr(Base.triplequoted(s), t...) end function Base.display(d::Base.REPL.REPLDisplay, md::Vector{MD}) - for md in md - display(d, md) - end + for md in md + display(d, md) + end end end diff --git a/src/parse/config.jl b/src/parse/config.jl index 822b0d6..7780058 100644 --- a/src/parse/config.jl +++ b/src/parse/config.jl @@ -1,9 +1,11 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + typealias InnerConfig Dict{Char, Vector{Function}} type Config - breaking::Vector{Function} - regular::Vector{Function} - inner::InnerConfig + breaking::Vector{Function} + regular::Vector{Function} + inner::InnerConfig end Config() = Config(Function[], Function[], InnerConfig()) @@ -26,46 +28,46 @@ isexpr(x::Expr, ts...) = x.head in ts isexpr{T}(x::T, ts...) = T in ts macro breaking (ex) - isexpr(ex, :->) || error("invalid @breaking form, use ->") - b, def = ex.args - if b - quote - f = $(esc(def)) - breaking!(f) - f + isexpr(ex, :->) || error("invalid @breaking form, use ->") + b, def = ex.args + if b + quote + f = $(esc(def)) + breaking!(f) + f + end + else + esc(def) end - else - esc(def) - end end macro trigger (ex) - isexpr(ex, :->) || error("invalid @triggers form, use ->") - ts, def = ex.args - quote - f = $(esc(def)) - triggers!(f, $ts) - f - end + isexpr(ex, :->) || error("invalid @triggers form, use ->") + ts, def = ex.args + quote + f = $(esc(def)) + triggers!(f, $ts) + f + end end # Construction function config(parsers::Function...) - c = Config() - for parser in parsers - ts = triggers(parser) - if breaking(parser) - push!(c.breaking, parser) - elseif !isempty(ts) - for t in ts - push!(getset(c.inner, t, Function[]), parser) - end - else - push!(c.regular, parser) + c = Config() + for parser in parsers + ts = triggers(parser) + if breaking(parser) + push!(c.breaking, parser) + elseif !isempty(ts) + for t in ts + push!(getset(c.inner, t, Function[]), parser) + end + else + push!(c.regular, parser) + end end - end - return c + return c end # Flavour definitions @@ -73,23 +75,8 @@ end const flavors = Dict{Symbol, Config}() macro flavor (name, features) - quote - const $(esc(name)) = config($(map(esc,features.args)...)) - flavors[$(Expr(:quote, name))] = $(esc(name)) - end -end - -# Dynamic scoping of current config - -_config_ = nothing - -function withconfig(f, config) - global _config_ - old = _config_ - _config_ = config - try - f() - finally - _config_ = old - end + quote + const $(esc(name)) = config($(map(esc,features.args)...)) + flavors[$(Expr(:quote, name))] = $(esc(name)) + end end diff --git a/src/parse/parse.jl b/src/parse/parse.jl index 2605630..034cca9 100644 --- a/src/parse/parse.jl +++ b/src/parse/parse.jl @@ -1,12 +1,22 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + type MD - content::Vector{Any} - meta::Dict{Any, Any} + content::Vector{Any} + meta::Dict{Any, Any} - MD(content::AbstractVector, meta::Dict = Dict()) = - new(content, meta) + MD(content::AbstractVector, meta::Dict = Dict()) = + new(content, meta) end -MD(xs...) = MD([xs...]) +MD(xs...) = MD(vcat(xs...)) + +function MD(cfg::Config, xs...) + md = MD(xs...) + md.meta[:config] = cfg + return md +end + +config(md::MD) = md.meta[:config]::Config # Forward some array methods @@ -17,6 +27,8 @@ Base.endof(md::MD) = endof(md.content) Base.length(md::MD) = length(md.content) Base.isempty(md::MD) = isempty(md.content) +==(a::MD, b::MD) = (html(a) == html(b)) + # Parser functions: #  md – should be modified appropriately #  return – basically, true if parse was successful @@ -28,57 +40,59 @@ Base.isempty(md::MD) = isempty(md.content) # Inner parsing -function innerparse(stream::IO, parsers::Vector{Function}) - for parser in parsers - inner = parser(stream) - inner ≡ nothing || return inner - end +function parseinline(stream::IO, md::MD, parsers::Vector{Function}) + for parser in parsers + inner = parser(stream, md) + inner ≡ nothing || return inner + end end -innerparse(stream::IO, config::Config) = - innerparse(stream, config.inner.parsers) - -function parseinline(stream::IO, config::Config) - content = {} - buffer = IOBuffer() - while !eof(stream) - char = peek(stream) - if haskey(config.inner, char) && - (inner = innerparse(stream, config.inner[char])) != nothing - c = takebuf_string(buffer) - !isempty(c) && push!(content, c) - buffer = IOBuffer() - push!(content, inner) - else - write(buffer, read(stream, Char)) +function parseinline(stream::IO, md::MD, config::Config) + content = Any[] + buffer = IOBuffer() + while !eof(stream) + char = peek(stream) + if haskey(config.inner, char) && + (inner = parseinline(stream, md, config.inner[char])) != nothing + c = takebuf_string(buffer) + !isempty(c) && push!(content, c) + buffer = IOBuffer() + push!(content, inner) + else + write(buffer, read(stream, Char)) + end end - end - c = takebuf_string(buffer) - !isempty(c) && push!(content, c) - return content + c = takebuf_string(buffer) + !isempty(c) && push!(content, c) + return content end -parseinline(s::String, c::Config) = - parseinline(IOBuffer(s), c) +parseinline(s::String, md::MD, c::Config) = + parseinline(IOBuffer(s), md, c) + +# TODO remove once GH #9888 is fixed +parseinline{T}(s::SubString{T}, md::MD, c::Config) = + parseinline(convert(T, s), md, c) -parseinline(s) = parseinline(s, _config_) +parseinline(s, md::MD) = parseinline(s, md, config(md)) # Block parsing function parse(stream::IO, block::MD, config::Config; breaking = false) - skipblank(stream) - eof(stream) && return false - for parser in (breaking ? config.breaking : [config.breaking, config.regular]) - parser(stream, block, config) && return true - end - return false + skipblank(stream) + eof(stream) && return false + for parser in (breaking ? config.breaking : [config.breaking; config.regular]) + parser(stream, block) && return true + end + return false end +parse(stream::IO, block::MD; breaking = false) = + parse(stream, block, config(block), breaking = breaking) + function parse(stream::IO; flavor = julia) - isa(flavor, Symbol) && (flavor = flavors[flavor]) - markdown = MD() - withconfig(flavor) do + isa(flavor, Symbol) && (flavor = flavors[flavor]) + markdown = MD(flavor) while parse(stream, markdown, flavor) end - end - return markdown + return markdown end diff --git a/src/parse/util.jl b/src/parse/util.jl index 58ae863..2258450 100644 --- a/src/parse/util.jl +++ b/src/parse/util.jl @@ -1,11 +1,13 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + import Base: peek macro dotimes(n, body) - quote - for i = 1:$(esc(n)) - $(esc(body)) + quote + for i = 1:$(esc(n)) + $(esc(body)) + end end - end end const whitespace = " \t\r" @@ -14,53 +16,53 @@ const whitespace = " \t\r" Skip any leading whitespace. Returns io. """ function skipwhitespace(io::IO; newlines = true) - while !eof(io) && (peek(io) in whitespace || (newlines && peek(io) == '\n')) - read(io, Char) - end - return io + while !eof(io) && (peek(io) in whitespace || (newlines && peek(io) == '\n')) + read(io, Char) + end + return io end """ Skip any leading blank lines. Returns the number skipped. """ function skipblank(io::IO) - start = position(io) - i = 0 - while !eof(io) - c = read(io, Char) - c == '\n' && (start = position(io); i+=1; continue) - c in whitespace || break - end - seek(io, start) - return i + start = position(io) + i = 0 + while !eof(io) + c = read(io, Char) + c == '\n' && (start = position(io); i+=1; continue) + c in whitespace || break + end + seek(io, start) + return i end """ -Returns true if the line contains only (and +Returns true if the line contains only (and, unless allowempty, at least one of) the characters given. """ function linecontains(io::IO, chars; allow_whitespace = true, eat = true, allowempty = false) - start = position(io) - l = readline(io) |> chomp - length(l) == 0 && return allowempty - - result = false - for c in l - c in whitespace && (allow_whitespace ? continue : (result = false; break)) - c in chars && (result = true; continue) - result = false; break - end - !(result && eat) && seek(io, start) - return result + start = position(io) + l = readline(io) |> chomp + length(l) == 0 && return allowempty + + result = allowempty + for c in l + c in whitespace && (allow_whitespace ? continue : (result = false; break)) + c in chars && (result = true; continue) + result = false; break + end + !(result && eat) && seek(io, start) + return result end blankline(io::IO; eat = true) = - linecontains(io, "", - allow_whitespace = true, - allowempty = true, - eat = eat) + linecontains(io, "", + allow_whitespace = true, + allowempty = true, + eat = eat) """ Test if the stream starts with the given string. @@ -68,40 +70,40 @@ Test if the stream starts with the given string. `padding` specifies whether leading whitespace should be ignored. """ function startswith(stream::IO, s::String; eat = true, padding = false, newlines = true) - start = position(stream) - padding && skipwhitespace(stream, newlines = newlines) - result = true - for char in s - !eof(stream) && read(stream, Char) == char || - (result = false; break) - end - !(result && eat) && seek(stream, start) - return result + start = position(stream) + padding && skipwhitespace(stream, newlines = newlines) + result = true + for char in s + !eof(stream) && read(stream, Char) == char || + (result = false; break) + end + !(result && eat) && seek(stream, start) + return result end function startswith(stream::IO, c::Char; eat = true) - if peek(stream) == c - eat && read(stream, Char) - return true - else - return false - end + if !eof(stream) && peek(stream) == c + eat && read(stream, Char) + return true + else + return false + end end function startswith{T<:String}(stream::IO, ss::Vector{T}; kws...) - any(s->startswith(stream, s; kws...), ss) + any(s->startswith(stream, s; kws...), ss) end function startswith(stream::IO, r::Regex; eat = true, padding = false) - @assert beginswith(r.pattern, "^") - start = position(stream) - padding && skipwhitespace(stream) - line = chomp(readline(stream)) - seek(stream, start) - m = match(r, line) - m == nothing && return "" - eat && @dotimes length(m.match) read(stream, Char) - return m.match + @assert Base.beginswith(r.pattern, "^") + start = position(stream) + padding && skipwhitespace(stream) + line = chomp(readline(stream)) + seek(stream, start) + m = match(r, line) + m == nothing && return "" + eat && @dotimes length(m.match) read(stream, Char) + return m.match end """ @@ -109,10 +111,23 @@ Executes the block of code, and if the return value is `nothing`, returns the stream to its initial position. """ function withstream(f, stream) - pos = position(stream) - result = f() - (result ≡ nothing || result ≡ false) && seek(stream, pos) - return result + pos = position(stream) + result = f() + (result ≡ nothing || result ≡ false) && seek(stream, pos) + return result +end + +""" +Consume the standard allowed markdown indent of +three spaces. Returns false if there are more than +three present. +""" +function eatindent(io::IO, n = 3) + withstream(io) do + m = 0 + while startswith(io, ' ') m += 1 end + return m <= n + end end """ @@ -122,26 +137,32 @@ Returns nothing and resets the stream if delim is not found. """ function readuntil(stream::IO, delimiter; newlines = false, match = nothing) - withstream(stream) do - buffer = IOBuffer() - count = 0 - while !eof(stream) - if startswith(stream, delimiter) - if count == 0 - return takebuf_string(buffer) - else - count -= 1 - write(buffer, delimiter) + withstream(stream) do + buffer = IOBuffer() + count = 0 + while !eof(stream) + if startswith(stream, delimiter) + if count == 0 + return takebuf_string(buffer) + else + count -= 1 + write(buffer, delimiter) + continue + end + end + char = read(stream, Char) + char == match && (count += 1) + !newlines && char == '\n' && break + write(buffer, char) end - end - char = read(stream, Char) - char == match && (count += 1) - !newlines && char == '\n' && break - write(buffer, char) end - end end +# TODO: refactor this. If we're going to assume +# the delimiter is a single character + a minimum +# repeat we may as well just pass that into the +# function. + """ Parse a symmetrical delimiter which wraps words. i.e. `*word word*` but not `*word * word`. @@ -149,26 +170,36 @@ i.e. `*word word*` but not `*word * word`. Escaped delimiters are not yet supported. """ function parse_inline_wrapper(stream::IO, delimiter::String; rep = false) - withstream(stream) do - startswith(stream, delimiter) || return nothing - n = 1 - while rep && startswith(stream, delimiter); (n += 1) end - - buffer = IOBuffer() - while !eof(stream) - char = read(stream, Char) - if !(char in whitespace || char == '\n') && startswith(stream, delimiter^n) - write(buffer, char) - return takebuf_string(buffer) - end - write(buffer, char) + delimiter, nmin = string(delimiter[1]), length(delimiter) + withstream(stream) do + if position(stream) >= 1 + # check the previous byte isn't a delimiter + skip(stream, -1) + (read(stream, Char) in delimiter) && return nothing + end + n = nmin + startswith(stream, delimiter^n) || return nothing + while startswith(stream, delimiter); n += 1; end + !rep && n > nmin && return nothing + !eof(stream) && peek(stream) in whitespace && return nothing + + buffer = IOBuffer() + while !eof(stream) + char = read(stream, Char) + write(buffer, char) + if !(char in whitespace || char == '\n' || char in delimiter) && startswith(stream, delimiter^n) + trailing = 0 + while startswith(stream, delimiter); trailing += 1; end + trailing == 0 && return takebuf_string(buffer) + write(buffer, delimiter ^ (n + trailing)) + end + end end - end end function showrest(io::IO) - start = position(io) - show(readall(io)) - println() - seek(io, start) + start = position(io) + show(readall(io)) + println() + seek(io, start) end diff --git a/src/render/html.jl b/src/render/html.jl index 64438d8..fbdfd5b 100644 --- a/src/render/html.jl +++ b/src/render/html.jl @@ -1,59 +1,107 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + include("rich.jl") # Utils -function withtag(f, io, tag) - print(io, "<$tag>") - f() - print(io, "") +function withtag(f, io::IO, tag, attrs...) + print(io, "<$tag") + for (attr, value) in attrs + print(io, " ") + htmlesc(io, attr) + print(io, "=\"") + htmlesc(io, value) + print(io, "\"") + end + f == nothing && return print(io, " />") + + print(io, ">") + f() + print(io, "") +end + +tag(io::IO, tag, attrs...) = withtag(nothing, io, tag, attrs...) + +const _htmlescape_chars = @compat(Dict('<'=>"<", '>'=>">", + '"'=>""", '&'=>"&", + # ' '=>" ", + )) +for ch in "'`!@\$\%()=+{}[]" + _htmlescape_chars[ch] = "&#$(@compat(Int(ch)));" +end + +function htmlesc(io::IO, s::String) + # s1 = replace(s, r"&(?!(\w+|\#\d+);)", "&") + for ch in s + print(io, get(_htmlescape_chars, ch, ch)) + end +end +function htmlesc(io::IO, s::Symbol) + htmlesc(io, string(s)) +end +function htmlesc(io::IO, xs::Union(String, Symbol)...) + for s in xs + htmlesc(io, s) + end +end +function htmlesc(s::Union(String, Symbol)) + sprint(htmlesc, s) end # Block elements function html(io::IO, content::Vector) - for md in content - html(io, md) - println(io) - end + for md in content + html(io, md) + println(io) + end end html(io::IO, md::MD) = html(io, md.content) function html{l}(io::IO, header::Header{l}) - withtag(io, "h$l") do - htmlinline(io, header.text) - end + withtag(io, "h$l") do + htmlinline(io, header.text) + end end function html(io::IO, code::Code) - withtag(io, :pre) do - withtag(io, :code) do - print(io, code.code) + withtag(io, :pre) do + maybe_lang = code.language != "" ? @compat(Dict(:class=>"language-$(code.language)")) : Dict() + withtag(io, :code, maybe_lang...) do + htmlesc(io, code.code) + # TODO should print newline if this is longer than one line ? + end end - end end function html(io::IO, md::Paragraph) - withtag(io, :p) do - htmlinline(io, md.content) - end + withtag(io, :p) do + htmlinline(io, md.content) + end end function html(io::IO, md::BlockQuote) - withtag(io, :blockquote) do - html(io, block.content) - end + withtag(io, :blockquote) do + println(io) + html(io, md.content) + end end function html(io::IO, md::List) - withtag(io, :ul) do - for item in md.items - withtag(io, :li) do - htmlinline(io, item) + withtag(io, md.ordered ? :ol : :ul) do + for item in md.items + println(io) + withtag(io, :li) do + htmlinline(io, item) + end + end println(io) - end end - end +end + +function html(io::IO, md::HorizontalRule) + tag(io, :hr) end html(io::IO, x) = tohtml(io, x) @@ -61,41 +109,45 @@ html(io::IO, x) = tohtml(io, x) # Inline elements function htmlinline(io::IO, content::Vector) - for x in content - htmlinline(io, x) - end + for x in content + htmlinline(io, x) + end end function htmlinline(io::IO, code::Code) - withtag(io, :code) do - print(io, code.code) - end + withtag(io, :code) do + htmlesc(io, code.code) + end end -function htmlinline(io::IO, md::String) - print(io, md) +function htmlinline(io::IO, md::Union(Symbol, String)) + htmlesc(io, md) end function htmlinline(io::IO, md::Bold) - withtag(io, :strong) do - htmlinline(io, md.text) - end + withtag(io, :strong) do + htmlinline(io, md.text) + end end function htmlinline(io::IO, md::Italic) - withtag(io, :em) do - htmlinline(io, md.text) - end + withtag(io, :em) do + htmlinline(io, md.text) + end end function htmlinline(io::IO, md::Image) - print(io, """$(md.alt)""") + tag(io, :img, Pair(:src, md.url), Pair(:alt, md.alt)) end function htmlinline(io::IO, link::Link) - print(io, """""") - htmlinline(io, link.text) - print(io,"""""") + withtag(io, :a, Pair(:href, link.url)) do + htmlinline(io, link.text) + end +end + +function htmlinline(io::IO, br::LineBreak) + tag(io, :br) end htmlinline(io::IO, x) = tohtml(io, x) @@ -107,7 +159,7 @@ export html html(md) = sprint(html, md) function writemime(io::IO, ::MIME"text/html", md::MD) - println(io, """
""") - html(io, md) - println(io, """
""") + withtag(io, :div, Pair(:class, "markdown")) do + html(io, md) + end end diff --git a/src/render/latex.jl b/src/render/latex.jl index 4381510..ce3ce81 100644 --- a/src/render/latex.jl +++ b/src/render/latex.jl @@ -1,9 +1,11 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + export latex function wrapblock(f, io, env) - println(io, "\\begin{", env, "}") - f() - println(io, "\\end{", env, "}") + println(io, "\\begin{", env, "}") + f() + println(io, "\\end{", env, "}") end function wrapinline(f, io, cmd) @@ -12,99 +14,127 @@ function wrapinline(f, io, cmd) print(io, "}") end -writemime(io::IO, ::MIME"text/latex", md::Content) = - writemime(io, "text/plain", md) +# Block elements -function writemime(io::IO, mime::MIME"text/latex", block::Block) - for md in block.content[1:end-1] - writemime(io::IO, mime, md) - println(io) - end - writemime(io::IO, mime, block.content[end]) +latex(io::IO, md::MD) = latex(io, md.content) + +function latex(io::IO, content::Vector) + for c in content + latex(io, c) + end end -function writemime{l}(io::IO, mime::MIME"text/latex", header::Header{l}) - tag = l < 4 ? "sub"^(l-1) * "section" : "sub"^(l-4) * "paragraph" - wrapinline(io, tag) do - print(io, header.text) - end - println(io) +function latex{l}(io::IO, header::Header{l}) + tag = l < 4 ? "sub"^(l-1) * "section" : "sub"^(l-4) * "paragraph" + wrapinline(io, tag) do + latexinline(io, header.text) + end + println(io) end -function writemime(io::IO, ::MIME"text/latex", code::BlockCode) - wrapblock(io, "verbatim") do - println(io, code.code) - end +function latex(io::IO, code::Code) + wrapblock(io, "verbatim") do + # TODO latex escape + println(io, code.code) + end end -function writemime(io::IO, ::MIME"text/latex", code::InlineCode) - wrapinline(io, "texttt") do - print(io, code.code) - end +function latexinline(io::IO, code::Code) + wrapinline(io, "texttt") do + print(io, code.code) + end end -function writemime(io::IO, ::MIME"text/latex", md::Paragraph) - for md in md.content - latex_inline(io, md) - end - println(io) +function latex(io::IO, md::Paragraph) + for md in md.content + latexinline(io, md) + end + println(io) end -function writemime(io::IO, ::MIME"text/latex", md::BlockQuote) - wrapblock(io, "quote") do - writemime(io, "text/latex", Block(md.content)) - end +function latex(io::IO, md::BlockQuote) + wrapblock(io, "quote") do + latex(io, md.content) + end end -function writemime(io::IO, ::MIME"text/latex", md::List) - wrapblock(io, "itemize") do - for item in md.content - print(io, "\\item ") - latex_inline(io, item) - println(io) +function latex(io::IO, md::List) + env = md.ordered ? "enumerate" : "itemize" + wrapblock(io, env) do + for item in md.items + print(io, "\\item ") + latexinline(io, item) + println(io) + end end - end +end + +function writemime(io::IO, ::MIME"text/latex", md::HorizontalRule) + println(io, "\\rule{\\textwidth}{1pt}") end # Inline elements -function writemime(io::IO, ::MIME"text/latex", md::Plain) - print(io, md.text) +function latexinline(io::IO, md::Vector) + for c in md + latexinline(io, c) + end end -function writemime(io::IO, ::MIME"text/latex", md::Bold) - wrapinline(io, "textbf") do - print(io, md.text) - end +function latexinline(io::IO, md::String) + latexesc(io, md) end -function writemime(io::IO, ::MIME"text/latex", md::Italic) - wrapinline(io, "emph") do - print(io, md.text) - end +function latexinline(io::IO, md::Bold) + wrapinline(io, "textbf") do + latexinline(io, md.text) + end end -function writemime(io::IO, ::MIME"text/latex", md::Image) - wrapblock(io, "figure") do - println(io, "\\centering") - wrapinline(io, "includegraphics") do - print(io, md.url) +function latexinline(io::IO, md::Italic) + wrapinline(io, "emph") do + latexinline(io, md.text) end - println(io) - wrapinline(io, "caption") do - print(io, md.alt) +end + +function latexinline(io::IO, md::Image) + wrapblock(io, "figure") do + println(io, "\\centering") + wrapinline(io, "includegraphics") do + print(io, md.url) + end + println(io) + wrapinline(io, "caption") do + latexinline(io, md.alt) + end + println(io) end - println(io) - end end -function writemime(io::IO, ::MIME"text/latex", md::Link) - wrapinline(io, "href") do - print(io, md.url) - end - print(io, "{", md.text, "}") +function latexinline(io::IO, md::Link) + wrapinline(io, "href") do + print(io, md.url) + end + print(io, "{") + latexinline(io, md.text) + print(io, "}") +end + +const _latexescape_chars = @compat(Dict{Char, String}( + '~'=>"{\\sim}", '^'=>"\\^{}", '\\'=>"{\\textbackslash}")) +for ch in "&%\$#_{}" + _latexescape_chars[ch] = "\\$ch" +end + +function latexesc(io, s::String) + for ch in s + print(io, get(_latexescape_chars, ch, ch)) + end end -latex_inline(io::IO, el::Content) = writemime(io, "text/latex", el) +latex(md) = sprint(latex, md) +latexinline(md) = sprint(latexinline, md) +latexesc(s) = sprint(latexesc, s) -latex(md::Content) = stringmime("text/latex", md) +writemime(io::IO, ::MIME"text/latex", md::MD) = latex(io, md) +#writemime(io::IO, ::MIME"text/latex", md::MD) = writemime(io, "text/plain", md) diff --git a/src/render/plain.jl b/src/render/plain.jl index 3dbaf04..6261a9a 100644 --- a/src/render/plain.jl +++ b/src/render/plain.jl @@ -1,56 +1,60 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + plain(x) = sprint(plain, x) function plain(io::IO, content::Vector) - isempty(content) && return - for md in content[1:end-1] - plain(io, md) - println(io) - end - plain(io, content[end]) + isempty(content) && return + for md in content[1:end-1] + plain(io, md) + println(io) + end + plain(io, content[end]) end plain(io::IO, md::MD) = plain(io, md.content) function plain{l}(io::IO, header::Header{l}) - print(io, "#"^l*" ") - plaininline(io, header.text) - println(io) + print(io, "#"^l*" ") + plaininline(io, header.text) + println(io) end function plain(io::IO, code::Code) - println(io, "```", code.language) - println(io, code.code) - println(io, "```") + println(io, "```", code.language) + println(io, code.code) + println(io, "```") end function plain(io::IO, p::Paragraph) - for md in p.content - plaininline(io, md) - end - println(io) + plaininline(io, p.content) + println(io) end function plain(io::IO, list::List) - for item in list.items - print(io, " * ") - plaininline(io, item) - println(io) - end + for (i, item) in enumerate(list.items) + print(io, list.ordered ? "$i. " : " * ") + plaininline(io, item) + println(io) + end end -plain(io::IO, x) = tohtml(io, x) +function plain(io::IO, md::HorizontalRule) + println(io, "–" ^ 3) +end # Inline elements +plaininline(x) = sprint(plaininline, x) + function plaininline(io::IO, md...) - for el in md - plaininline(io, el) - end + for el in md + plaininline(io, el) + end end -plaininline(io::IO, md::Vector) = plaininline(io, md...) +plaininline(io::IO, md::Vector) = !isempty(md) && plaininline(io, md...) -plaininline(io::IO, md::Image) = print(io, "![$(md.alt)]($(md.url))") +plaininline(io::IO, md::Image) = plaininline(io, "![", md.alt, "](", md.url, ")") plaininline(io::IO, s::String) = print(io, s) @@ -60,6 +64,8 @@ plaininline(io::IO, md::Italic) = plaininline(io, "*", md.text, "*") plaininline(io::IO, md::Code) = print(io, "`", md.code, "`") +plaininline(io::IO, br::LineBreak) = println(io) + plaininline(io::IO, x) = writemime(io, MIME"text/plain"(), x) # writemime diff --git a/src/render/rich.jl b/src/render/rich.jl index 89a7066..4d041aa 100644 --- a/src/render/rich.jl +++ b/src/render/rich.jl @@ -1,28 +1,30 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + function tohtml(io::IO, m::MIME"text/html", x) - writemime(io, m, x) + writemime(io, m, x) end function tohtml(io::IO, m::MIME"text/plain", x) - writemime(io, m, x) + htmlesc(io, sprint(writemime, m, x)) end function tohtml(io::IO, m::MIME"image/png", img) - print(io, """") + print(io, """") end function tohtml(m::MIME"image/svg+xml", img) - writemime(io, m, img) + writemime(io, m, img) end # Display infrastructure function bestmime(val) - for mime in ("text/html", "image/svg+xml", "image/png", "text/plain") - mimewritable(mime, val) && return MIME(symbol(mime)) - end - error("Cannot render $val to Markdown.") + for mime in ("text/html", "image/svg+xml", "image/png", "text/plain") + mimewritable(mime, val) && return MIME(symbol(mime)) + end + error("Cannot render $val to Markdown.") end tohtml(io::IO, x) = tohtml(io, bestmime(x), x) diff --git a/src/render/terminal/formatting.jl b/src/render/terminal/formatting.jl index 8e8a8c9..25e506d 100644 --- a/src/render/terminal/formatting.jl +++ b/src/render/terminal/formatting.jl @@ -1,51 +1,57 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + # Styles -const text_formats = [ - :black => "\e[30m", - :red => "\e[31m", - :green => "\e[32m", - :yellow => "\e[33m", - :blue => "\e[34m", - :magenta => "\e[35m", - :cyan => "\e[36m", - :white => "\e[37m", - :reset => "\e[0m", - :bold => "\e[1m", - :underline => "\e[4m", - :blink => "\e[5m", - :negative => "\e[7m"] +const text_formats = @compat(Dict( + :black => ("\e[30m", "\e[39m"), + :red => ("\e[31m", "\e[39m"), + :green => ("\e[32m", "\e[39m"), + :yellow => ("\e[33m", "\e[39m"), + :blue => ("\e[34m", "\e[39m"), + :magenta => ("\e[35m", "\e[39m"), + :cyan => ("\e[36m", "\e[39m"), + :white => ("\e[37m", "\e[39m"), + :reset => ("\e[0m", "\e[0m"), + :bold => ("\e[1m", "\e[22m"), + :underline => ("\e[4m", "\e[24m"), + :blink => ("\e[5m", "\e[25m"), + :negative => ("\e[7m", "\e[27m"))) function with_output_format(f::Function, formats::Vector{Symbol}, io::IO, args...) - Base.have_color && for format in formats - print(io, get(text_formats, format, "")) - end - try f(io, args...) - finally - Base.have_color && print(io, text_formats[:reset]) - end + Base.have_color && for format in formats + haskey(text_formats, format) && + print(io, text_formats[format][1]) + end + try f(io, args...) + finally + Base.have_color && for format in formats + haskey(text_formats, format) && + print(io, text_formats[format][2]) + end + end end with_output_format(f::Function, format::Symbol, args...) = - with_output_format(f, [format], args...) + with_output_format(f, [format], args...) with_output_format(format, f::Function, args...) = - with_output_format(f, format, args...) + with_output_format(f, format, args...) function print_with_format(format, io::IO, x) - with_output_format(format, io) do io - print(io, x) - end + with_output_format(format, io) do io + print(io, x) + end end function println_with_format(format, io::IO, x) - print_with_format(format, io, x) - println(io) + print_with_format(format, io, x) + println(io) end # Wrapping function ansi_length(s) - replace(s, r"\e\[[0-9]+m", "") |> length + replace(s, r"\e\[[0-9]+m", "") |> length end words(s) = split(s, " ") @@ -53,47 +59,49 @@ lines(s) = split(s, "\n") # This could really be more efficient function wrapped_lines(s::String; width = 80, i = 0) - if ismatch(r"\n", s) - return [map(s->wrapped_lines(s, width = width, i = i), split(s, "\n"))...] - end - ws = words(s) - lines = String[ws[1]] - i += ws[1] |> ansi_length - for word in ws[2:end] - word_length = ansi_length(word) - if i + word_length + 1 > width - i = word_length - push!(lines, word) - else - i += word_length + 1 - lines[end] *= " " * word + if ismatch(r"\n", s) + return vcat(map(s->wrapped_lines(s, width = width, i = i), split(s, "\n"))...) end - end - return lines + ws = words(s) + lines = String[ws[1]] + i += ws[1] |> ansi_length + for word in ws[2:end] + word_length = ansi_length(word) + if i + word_length + 1 > width + i = word_length + push!(lines, word) + else + i += word_length + 1 + lines[end] *= " " * word + end + end + return lines end wrapped_lines(f::Function, args...; width = 80, i = 0) = - wrapped_lines(sprint(f, args...), width = width, i = 0) + wrapped_lines(sprint(f, args...), width = width, i = 0) function print_wrapped(io::IO, s...; width = 80, pre = "", i = 0) - lines = wrapped_lines(s..., width = width, i = i) - println(io, lines[1]) - for line in lines[2:end] - println(io, pre, line) - end + lines = wrapped_lines(s..., width = width, i = i) + println(io, lines[1]) + for line in lines[2:end] + println(io, 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...) function print_centred(io::IO, s...; columns = 80, width = columns) - lines = wrapped_lines(s..., width = width) - for line in lines - print(io, " "^(div(columns-ansi_length(line), 2))) - println(io, line) - end + lines = wrapped_lines(s..., width = width) + for line in lines + print(io, " "^(div(columns-ansi_length(line), 2))) + println(io, line) + end + length(lines), length(pre) + length(lines[end]) end function centred(s, columns) - pad = div(columns - ansi_length(s), 2) - " "^pad * s + pad = div(columns - ansi_length(s), 2) + " "^pad * s end diff --git a/src/render/terminal/render.jl b/src/render/terminal/render.jl index 31a2efd..af5dd39 100644 --- a/src/render/terminal/render.jl +++ b/src/render/terminal/render.jl @@ -1,64 +1,83 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + include("formatting.jl") const margin = 2 cols() = Base.tty_size()[2] function term(io::IO, content::Vector, cols) - isempty(content) && return - for md in content[1:end-1] - term(io, md, cols) - println(io) - end - term(io, content[end], cols) + isempty(content) && return + for md in content[1:end-1] + term(io, md, cols) + println(io) + end + term(io, content[end], cols) end term(io::IO, md::MD, columns = cols()) = 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) - end + print(io, " "^margin) + print_wrapped(io, width = columns-2margin, pre = " "^margin) do io + terminline(io, md.content) + end end function term(io::IO, md::BlockQuote, columns) - s = sprint(io->term(io, Block(md.content), columns - 10)) - for line in split(rstrip(s), "\n") - println(io, " "^margin, "|", line) - end - println(io) + s = sprint(io->term(io, md.content, columns - 10)) + for line in split(rstrip(s), "\n") + println(io, " "^margin, "|", line) + end + println(io) end function term(io::IO, md::List, columns) - for point in md.items - print(io, " "^2margin, "• ") - print_wrapped(io, width = columns-(4margin+2), pre = " "^(2margin+2), i = 2margin+2) do io - terminline(io, point) + for (i, point) in enumerate(md.items) + print(io, " "^2margin, md.ordered ? "$i. " : "• ") + print_wrapped(io, width = columns-(4margin+2), pre = " "^(2margin+2), + i = 2margin+2) do io + terminline(io, point) + end end - end end -function term(io::IO, md::Header{1}, columns) - text = terminline(md.text) - with_output_format(:bold, io) do io - print_centred(io, text, width = columns - 4margin, columns = columns) - end - print_centred(io, "-"*"–"^min(length(text), div(columns, 2))*"-", columns = columns) +function _term_header(io::IO, md, char, columns) + text = terminline(md.text) + with_output_format(:bold, io) do io + print(io, " "^(2margin), " ") + line_no, lastline_width = print_wrapped(io, text, + width=columns - 4margin; pre=" ") + line_width = min(1 + lastline_width, columns) + if line_no > 1 + line_width = max(line_width, div(columns, 3)) + end + char != ' ' && println(io, " "^(2margin), string(char) ^ line_width) + end end +const _header_underlines = collect("≡=–-⋅ ") +# TODO settle on another option with unicode e.g. "≡=≃–∼⋅" ? + function term{l}(io::IO, md::Header{l}, columns) - print(io, "#"^l, " ") - terminline(io, md.text) - println(io) + underline = _header_underlines[l] + _term_header(io, md, underline, columns) end function term(io::IO, md::Code, columns) - with_output_format(:cyan, io) do io - for line in lines(md.code) - print(io, " "^margin) - println(io, line) + with_output_format(:cyan, io) do io + for line in lines(md.code) + print(io, " "^margin) + println(io, line) + end end - end +end + +function term(io::IO, br::LineBreak, columns) + println(io) +end + +function term(io::IO, br::HorizontalRule, columns) + println(io, " " ^ margin, "-" ^ (columns - 2margin)) end term(io::IO, x, _) = writemime(io, MIME"text/plain"(), x) @@ -68,33 +87,33 @@ term(io::IO, x, _) = writemime(io, MIME"text/plain"(), x) terminline(md) = sprint(terminline, md) function terminline(io::IO, content::Vector) - for md in content - terminline(io, md) - end + for md in content + terminline(io, md) + end end function terminline(io::IO, md::String) - print(io, md) + print(io, md) end function terminline(io::IO, md::Bold) - with_output_format(:bold, terminline, io, md.text) + with_output_format(:bold, terminline, io, md.text) end function terminline(io::IO, md::Italic) - with_output_format(:underline, terminline, io, md.text) + with_output_format(:underline, terminline, io, md.text) end function terminline(io::IO, md::Image) - print(io, "(Image: $(md.alt))") + print(io, "(Image: $(md.alt))") end function terminline(io::IO, md::Link) - terminline(io, md.text) + terminline(io, md.text) end function terminline(io::IO, code::Code) - print_with_format(:cyan, io, code.code) + print_with_format(:cyan, io, code.code) end terminline(io::IO, x) = writemime(io, MIME"text/plain"(), x) diff --git a/test/markdown.jl b/test/markdown.jl new file mode 100644 index 0000000..79800df --- /dev/null +++ b/test/markdown.jl @@ -0,0 +1,178 @@ +# This file is a part of Julia. License is MIT: http://julialang.org/license + +using .Markdown +import .Markdown: MD, Paragraph, Header, Italic, Bold, plain, term, html, Table, Code +import Base: writemime + +# Basics +# Equality is checked by making sure the HTML output is +# the same – the structure itself may be different. + +@test md"foo" == MD(Paragraph("foo")) +@test md"foo *bar* baz" == MD(Paragraph(["foo ", Italic("bar"), " baz"])) + +@test md"#no title" == MD(Paragraph(["#no title"])) +@test md"# title" == MD(Header{1}("title")) +@test md""" + # + empty + """ == MD(Header{1}(""), Paragraph("empty")) +@test md"## section" == MD(Header{2}("section")) +@test md"# title *foo* `bar` **baz**" == + MD(Header{1}(["title ", Italic("foo")," ",Code("bar")," ",Bold("baz")])) +@test md""" +h1 +===""" == md"# h1" +@test md""" +h2 + ---""" == md"## h2" + +@test md"**foo *bar* baz**" == MD(Paragraph(Bold(["foo ", Italic("bar"), " baz"]))) +@test md"*foo **bar** baz*" == MD(Paragraph(Italic(["foo ", Bold("bar"), " baz"]))) + +@test md"""```julia +foo +``` +""" == MD(Code("julia", "foo")) +@test md"``code```more code``" == MD(Any[Paragraph(Any[Code("","code```more code")])]) +@test md"``code``````more code``" == MD(Any[Paragraph(Any[Code("","code``````more code")])]) + +@test md""" +* one +* two + +1. pirate +2. ninja +3. zombie""" == Markdown.MD([Markdown.List(["one", "two"]), + Markdown.List(["pirate", "ninja", "zombie"], true)]) + +@test md"Foo [bar]" == MD(Paragraph("Foo [bar]")) +@test md"Foo [bar](baz)" != MD(Paragraph("Foo [bar](baz)")) +@test md"Foo \[bar](baz)" == MD(Paragraph("Foo [bar](baz)")) + +# Basic plain (markdown) output + +@test md"foo" |> plain == "foo\n" +@test md"foo *bar* baz" |> plain == "foo *bar* baz\n" +@test md"# title" |> plain == "# title\n" +@test md"## section" |> plain == "## section\n" +@test md"## section `foo`" |> plain == "## section `foo`\n" +@test md"""Hello + +--- +World""" |> plain == "Hello\n\n–––\n\nWorld\n" + +# HTML output + +@test md"foo *bar* baz" |> html == "

foo bar baz

\n" +@test md"something ***" |> html == "

something ***

\n" +@test md"# h1## " |> html == "

h1##

\n" +@test md"## h2 ### " |> html == "

h2

\n" +@test md"###### h6" |> html == "
h6
\n" +@test md"####### h7" |> html == "

####### h7

\n" +@test md" >" |> html == "
\n
\n" +@test md"1. Hello" |> html == "
    \n
  1. Hello
  2. \n
\n" +@test md"* World" |> html == "\n" +@test md"# title *blah*" |> html == "

title blah

\n" +@test md"## title *blah*" |> html == "

title blah

\n" +@test md"""Hello + +--- +World""" |> html == "

Hello

\n
\n

World

\n" +@test md"`escape`" |> html == "

escape</code>

\n" + +@test md""" + code1 + + code2 +""" |> html == "
code1\n\ncode2
\n" # single code block + +# @test md""" +# - Foo +# --- +# - Bar""" |> html == "\n
\n\n" +@test md""" +h1 +=== +h2 +--- +not +== =""" |> html == "

h1

\n

h2

\n

not == =

\n" + +# Latex output +book = md""" +# Title + +Some discussion + +> A quote + +## Section *important* + +Some **bolded** + +- list1 +- list2 +""" +@test latex(book) == "\\section{Title}\nSome discussion\n\\begin{quote}\nA quote\n\\end{quote}\n\\subsection{Section \\emph{important}}\nSome \\textbf{bolded}\n\\begin{itemize}\n\\item list1\n\\item list2\n\\end{itemize}\n" + +# Interpolation / Custom types + +type Reference + ref +end + +ref(x) = Reference(x) + +# if Base.USE_GPL_LIBS + +ref(fft) + +writemime(io::IO, m::MIME"text/plain", r::Reference) = + print(io, "$(r.ref) (see Julia docs)") + +fft_ref = md"Behaves like $(ref(fft))" +@test plain(fft_ref) == "Behaves like fft (see Julia docs)\n" +@test html(fft_ref) == "

Behaves like fft (see Julia docs)

\n" + +writemime(io::IO, m::MIME"text/html", r::Reference) = + Markdown.withtag(io, :a, Markdown.Pair(:href, "test")) do + Markdown.htmlesc(io, Markdown.plaininline(r)) + end +@test html(fft_ref) == "

Behaves like fft (see Julia docs)

\n" + +# end # USE_GPL_LIBS + +@test md""" +````julia +foo() +````""" == md""" +```julia +foo() +```""" + +# GH tables +@test md""" + a | b + ---|--- + 1 | 2""" == MD(Table(Any[["a","b"], + ["1","2"]], [:r, :r])) + +@test md""" + | a | b | c | + | :-- | --: | --- | + | d`gh`hg | hgh**jhj**ge | f |""" == MD(Table(Any[["a","b","c"], + Any[["d",Code("gh"),"hg"], + ["hgh",Bold("jhj"),"ge"], + "f"]], + [:l, :r, :r])) +@test md""" +no|table +no error +""" == MD([Paragraph(Any["no|table no error"])]) + +t = """a | b +:-- | --: +1 | 2 +""" +@test plain(Markdown.parse(t)) == t diff --git a/test/runtests.jl b/test/runtests.jl index 048d8bb..fd5aa7e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,8 @@ -using Markdown +module Tests + using Base.Test -# write your own tests here -@test 1 == 1 +include("../src/Markdown.jl") +include("markdown.jl") + +end