-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* format * copy src over * move generation code to its own file * rename to SimpleLog * docs * renames * copy tests over * rm toplevel url * rm unused example * tweak comments * add to Changelog's changelog * wip * add comment * wip * more tests * mv * up * test show methods * document parsing functionality * reorganize code to prepare for adding parsing code * mark mutating * relax stdlib compat * 1.6-compatible multiple replacements * format * tweak VersionInfo struct to have two fields * rename SimpleLog -> SimpleChangelog * Rename SimpleChangeLog.jl to SimpleChangelog.jl * add `tryparse` and `tryparsefile` * format * document try* * typo * rm OrderedDict & need for unique section names
- Loading branch information
1 parent
3c1511a
commit 791e67a
Showing
24 changed files
with
5,039 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
""" | ||
VersionInfo | ||
A struct representing the information in a changelog about a particular version, with properties: | ||
- `version::Union{Nothing, String}`: a string representation of a version number or name (e.g. "Unreleased" or "1.2.3"). | ||
- `url::Union{Nothing, String}`: a URL associated to the version, if available | ||
- `date::Union{Nothing, Date}`: a date associated to the version, if available | ||
- `toplevel_changes::Vector{String}`: a list of changes which are not within a section | ||
- `sectioned_changes::Vector{Pair{String, Vector{String}}}`: an ordered mapping of section name to a list of changes in that section. | ||
""" | ||
struct VersionInfo | ||
version::Union{Nothing, String} | ||
url::Union{Nothing, String} | ||
date::Union{Nothing, Date} | ||
toplevel_changes::Vector{String} | ||
sectioned_changes::Vector{Pair{String, Vector{String}}} | ||
end | ||
function Base.show(io::IO, ::MIME"text/plain", v::VersionInfo) | ||
return full_show(io, v) | ||
end | ||
|
||
function full_show(io, v::VersionInfo; indent = 0, showtype = true) | ||
pad = " "^indent | ||
if showtype | ||
print(io, pad, VersionInfo, " with") | ||
print(io, pad, "\n- version: ", v.version) | ||
else | ||
print(io, pad, "- ", v.version) | ||
pad *= " " | ||
end | ||
if v.url !== nothing | ||
print(io, "\n", pad, "- url: ", v.url) | ||
end | ||
print(io, "\n", pad, "- date: ", v.date) | ||
return if isempty(v.sectioned_changes) && isempty(v.toplevel_changes) | ||
print(io, "\n", pad, "- and no documented changes") | ||
else | ||
print(io, "\n", pad, "- changes") | ||
if !isempty(v.toplevel_changes) | ||
for b in v.toplevel_changes | ||
print(io, "\n", pad, " - $b") | ||
end | ||
end | ||
|
||
if !isempty(v.sectioned_changes) | ||
for (section_name, bullets) in v.sectioned_changes | ||
print(io, "\n", pad, " - $section_name") | ||
for b in bullets | ||
print(io, "\n", pad, " - $b") | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
||
""" | ||
SimpleChangelog | ||
A simple in-memory changelog format, with properties: | ||
- `title::Union{Nothing, String}` | ||
- `intro::Union{Nothing, String}` | ||
- `versions::Vector{VersionInfo}` | ||
A `SimpleChangelog` can be parsed out of a markdown-formatted string with `Base.parse`. | ||
SimpleChangelogs are not intended to be roundtrippable in-memory representations of markdown | ||
changelogs; rather, they discard most formatting and other details to provide a simple | ||
view to make it easy to query if the changelog has an entry for some particular version, | ||
or what the changes are for that version. | ||
See also: [`VersionInfo`](@ref), [`parsefile`](@ref). | ||
""" | ||
struct SimpleChangelog | ||
title::Union{Nothing, String} | ||
intro::Union{Nothing, String} | ||
versions::Vector{VersionInfo} | ||
end | ||
|
||
function Base.show(io::IO, ::MIME"text/plain", c::SimpleChangelog) | ||
print(io, SimpleChangelog, " with") | ||
print(io, "\n- title: ", c.title) | ||
print(io, "\n- intro: ", c.intro) | ||
n_versions = length(c.versions) | ||
plural = n_versions > 1 ? "s" : "" | ||
print(io, "\n- $(n_versions) version$plural:") | ||
n_to_show = 5 | ||
for v in first(c.versions, n_to_show) | ||
print(io, "\n") | ||
full_show(io, v; showtype = false, indent = 2) | ||
end | ||
if n_versions > n_to_show | ||
print(io, "\n ⋮") | ||
end | ||
return | ||
end | ||
|
||
""" | ||
parse(::Type{SimpleChangelog}, text::AbstractString) | ||
Parse a [`SimpleChangelog`](@ref) from a markdown-formatted string. | ||
!!! note | ||
This functionality is primarily intended for parsing [KeepAChangeLog](https://keepachangelog.com/en/1.1.0/)-style changelogs, that have a title as a H1 (e.g. `#`) markdown header, followed by a list of versions with H2-level headers (`##`) formatted like `[1.1.0] - 2019-02-15` with or without a link on the version number, followed by a bulleted list of changes, potentially in subsections, each with H3 header. For such changelogs, parsing should be stable. We may also attempt to parse a wider variety of headers, for which the extent that we can parse may change in non-breaking releases (typically improving the parsing, but potentially regressing in some cases). | ||
""" | ||
function Base.parse(::Type{SimpleChangelog}, text::AbstractString) | ||
# parse into CommonMark AST | ||
parser = CM.Parser() | ||
CM.enable!(parser, CM.FootnoteRule()) | ||
ast = parser(text) | ||
# convert to MarkdownAST AST | ||
ast = md_convert(MarkdownAST.Node, ast) | ||
return _parse_simple_changelog!(ast) | ||
end | ||
|
||
""" | ||
tryparse(::Type{SimpleChangelog}, text::AbstractString) | ||
Try to parse a [`SimpleChangelog`](@ref) from a markdown-formatted string, | ||
returning `nothing` if unable to. | ||
""" | ||
function Base.tryparse(::Type{SimpleChangelog}, text::AbstractString) | ||
return try | ||
parse(SimpleChangelog, text) | ||
catch e | ||
# This may be handy occasionally if we want to understand why we couldn't parse | ||
# and don't want to manually run `parse(SimpleChangelog, text)`. | ||
@debug "Error when parsing `SimpleChangelog` from changelog, returning `nothing`" exception = sprint(Base.display_error, e, catch_backtrace()) | ||
nothing | ||
end | ||
end | ||
|
||
""" | ||
parsefile(path) -> SimpleChangelog | ||
Parse a [`SimpleChangelog`](@ref) from a file path `path`. | ||
""" | ||
function parsefile(path) | ||
return parse(SimpleChangelog, read(path, String)) | ||
end | ||
|
||
""" | ||
tryparsefile(path) -> SimpleChangelog | ||
Try to parse a [`SimpleChangelog`](@ref) from a file path `path`, returning | ||
`nothing` if unable to. | ||
""" | ||
function tryparsefile(path) | ||
return tryparse(SimpleChangelog, read(path, String)) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
# Copy of: | ||
# https://github.com/MichaelHatherly/CommonMark.jl/pull/56 | ||
# by Morten Piibeleht | ||
# That PR is not merged, so we will vendor a copy here. | ||
# We rename the function `md_convert` instead of `Base.convert` | ||
# to avoid piracy. | ||
# We stick it into a module to avoid unexpected side-effects, so hopefully we can cleanly | ||
# delete this file once the PR is merged. | ||
module CommonMarkMarkdownASTInterop | ||
using CommonMark: Node, AbstractContainer, NULL_NODE, | ||
Document, Paragraph, BlockQuote, ThematicBreak, HtmlBlock, DisplayMath, | ||
Heading, CodeBlock, Admonition, List, Item, FootnoteDefinition, | ||
LineBreak, Backslash, SoftBreak, Emph, Strong, HtmlInline, Math, FootnoteLink, Text, | ||
Code, Image, Link, JuliaValue, Table, TableBody, TableCell, TableHeader, TableRow | ||
|
||
import MarkdownAST | ||
|
||
function md_convert(::Type{MarkdownAST.Node}, node::Node) | ||
mdast = _mdast_node(node) | ||
let child = node.first_child | ||
while child != NULL_NODE | ||
mdast_child = md_convert(MarkdownAST.Node, child) | ||
push!(mdast.children, mdast_child) | ||
child = child.nxt | ||
end | ||
end | ||
return mdast | ||
end | ||
|
||
_mdast_node(node::Node) = _mdast_node(node, node.t) | ||
|
||
# Fallback convert function | ||
struct UnsupportedContainerError <: Exception | ||
container_type::Type{<:AbstractContainer} | ||
end | ||
function Base.showerror(io::IO, e::UnsupportedContainerError) | ||
return print( | ||
io, | ||
"UnsupportedContainerError: container of type '$(e.container_type)' is not supported in MarkdownAST", | ||
) | ||
end | ||
|
||
function _mdast_node(::Node, ::T) where {T <: AbstractContainer} | ||
throw(UnsupportedContainerError(T)) | ||
end | ||
|
||
# For all singleton containers that map trivially (i.e. they have no attributes), | ||
# we can have a single implementation. | ||
const SINGLETON_CONTAINER_MAP = Dict( | ||
Document => MarkdownAST.Document, | ||
Paragraph => MarkdownAST.Paragraph, | ||
BlockQuote => MarkdownAST.BlockQuote, | ||
ThematicBreak => MarkdownAST.ThematicBreak, | ||
LineBreak => MarkdownAST.LineBreak, | ||
Backslash => MarkdownAST.Backslash, | ||
SoftBreak => MarkdownAST.SoftBreak, | ||
Emph => MarkdownAST.Emph, | ||
Strong => MarkdownAST.Strong, | ||
# CommonMark.Item contains a field, but it's discarded in MarkdownAST | ||
Item => MarkdownAST.Item, | ||
# Internal nodes for tables | ||
TableBody => MarkdownAST.TableBody, | ||
TableHeader => MarkdownAST.TableHeader, | ||
TableRow => MarkdownAST.TableRow, | ||
) | ||
const SINGLETON_CONTAINERS = Union{keys(SINGLETON_CONTAINER_MAP)...} | ||
function _mdast_node(node::Node, container::SINGLETON_CONTAINERS) | ||
e = SINGLETON_CONTAINER_MAP[typeof(container)]() | ||
return MarkdownAST.Node(e) | ||
end | ||
|
||
# Some containers use the .literal field of the Node object to store the content, | ||
# which generally maps to MarkdownAST.T(node.literal). | ||
const LITERAL_CONTAINER_MAP = Dict( | ||
Text => MarkdownAST.Text, | ||
HtmlBlock => MarkdownAST.HTMLBlock, | ||
HtmlInline => MarkdownAST.HTMLInline, | ||
DisplayMath => MarkdownAST.DisplayMath, | ||
Math => MarkdownAST.InlineMath, | ||
Code => MarkdownAST.Code, | ||
) | ||
const LITERAL_CONTAINERS = Union{keys(LITERAL_CONTAINER_MAP)...} | ||
function _mdast_node(node::Node, container::LITERAL_CONTAINERS) | ||
e = LITERAL_CONTAINER_MAP[typeof(container)](node.literal) | ||
return MarkdownAST.Node(e) | ||
end | ||
|
||
# Containers that need special handling | ||
_mdast_node(n::Node, c::Heading) = MarkdownAST.Node(MarkdownAST.Heading(c.level)) | ||
_mdast_node(n::Node, c::Link) = MarkdownAST.Node(MarkdownAST.Link(c.destination, c.title)) | ||
_mdast_node(n::Node, c::Image) = MarkdownAST.Node(MarkdownAST.Image(c.destination, c.title)) | ||
_mdast_node(n::Node, c::List) = | ||
MarkdownAST.Node(MarkdownAST.List(c.list_data.type, c.list_data.tight)) | ||
_mdast_node(n::Node, c::CodeBlock) = | ||
MarkdownAST.Node(MarkdownAST.CodeBlock(c.info, n.literal)) | ||
_mdast_node(n::Node, c::Admonition) = | ||
MarkdownAST.Node(MarkdownAST.Admonition(c.category, c.title)) | ||
_mdast_node(n::Node, c::FootnoteDefinition) = | ||
MarkdownAST.Node(MarkdownAST.FootnoteDefinition(c.id)) | ||
_mdast_node(n::Node, c::FootnoteLink) = MarkdownAST.Node(MarkdownAST.FootnoteLink(c.id)) | ||
_mdast_node(n::Node, c::Table) = MarkdownAST.Node(MarkdownAST.Table(c.spec)) | ||
_mdast_node(n::Node, c::TableCell) = | ||
MarkdownAST.Node(MarkdownAST.TableCell(c.align, c.header, c.column)) | ||
_mdast_node(n::Node, c::JuliaValue) = MarkdownAST.Node(MarkdownAST.JuliaValue(c.ex, c.ref)) | ||
|
||
# Unsupported containers (no MarkdownAST equivalent currently): | ||
# | ||
# Attributes, Citation, CitationBracket, FrontMatter, ReferenceList, References, | ||
# LaTeXBlock, LaTeXInline | ||
# | ||
# Should never appear in a CommonMark tree: | ||
# | ||
# TablePipe (internal use), TableComponent (abstract), JuliaExpression (internal use) | ||
|
||
end # module |
Oops, something went wrong.