From eb11fe6fc685960e1538708bdc135123b24fd380 Mon Sep 17 00:00:00 2001 From: Michael Goerz Date: Sat, 14 Oct 2023 22:02:37 -0400 Subject: [PATCH] Fix behavior of `Pages` in `@bibliography` blocks The `Pages` field now behaves exactly as `Pages` in `@contents` and `@index` blocks: all files are relative to the directory containing the current file. Also, by popular demand, `@__FILE__` is supported to refer to the current file (even though `@contents` and `@index` does not support this feature. --- NEWS.md | 3 + docs/src/syntax.md | 2 +- src/DocumenterCitations.jl | 10 +- src/expand_bibliography.jl | 109 +++++++++-- test/file_content.jl | 42 ++++ test/runtests.jl | 5 + test/test_bibliography_block_pages.jl | 181 ++++++++++++++++++ .../src/addendum.md | 3 + .../src/index.md | 21 ++ .../src/part1/section1/p1_s1_page.md | 28 +++ .../src/part1/section2/p1_s2_page.md | 29 +++ .../src/part2/section1/p2_s1_page.md | 39 ++++ .../src/part2/section2/p2_s2_page.md | 41 ++++ .../src/part3/section1/p3_s1_page.md | 41 ++++ .../src/part3/section2/invalidpages.md | 45 +++++ .../src/part3/section2/p3_s2_page.md | 46 +++++ .../src/references.md | 13 ++ test/test_latex_rendering.jl | 45 +++-- test/test_parse_bibliography_block.jl | 68 +++++++ 19 files changed, 732 insertions(+), 39 deletions(-) create mode 100644 test/file_content.jl create mode 100644 test/test_bibliography_block_pages.jl create mode 100644 test/test_bibliography_block_pages/src/addendum.md create mode 100644 test/test_bibliography_block_pages/src/index.md create mode 100644 test/test_bibliography_block_pages/src/part1/section1/p1_s1_page.md create mode 100644 test/test_bibliography_block_pages/src/part1/section2/p1_s2_page.md create mode 100644 test/test_bibliography_block_pages/src/part2/section1/p2_s1_page.md create mode 100644 test/test_bibliography_block_pages/src/part2/section2/p2_s2_page.md create mode 100644 test/test_bibliography_block_pages/src/part3/section1/p3_s1_page.md create mode 100644 test/test_bibliography_block_pages/src/part3/section2/invalidpages.md create mode 100644 test/test_bibliography_block_pages/src/part3/section2/p3_s2_page.md create mode 100644 test/test_bibliography_block_pages/src/references.md diff --git a/NEWS.md b/NEWS.md index 4f7d705..38acd81 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Skip the expansion of citations and bibliographies when running in doctest mode [[#34][]] * Support underscores in citation keys [[#14][]] +* The `Pages` in a `@bibliography` block are now relative to the folder containing the current file. The behavior is consistent with `Pages` in Documenter's `@index` and `@contents` blocks. [[#22][]] ### Added @@ -19,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * When running in non-strict mode, missing bibliographic references (either because the `.bib` file does not contain an entry with a specific BibTeX key, or because of a missing `@biblography` block) are now handled similarly to missing references in LaTeX: They will show as (unlinked) question marks. * Support for bibliographies in PDFs generate via LaTeX (`format=Documenter.LaTeX()`). Citations and references are rendered exactly as in the HTML version. Specifically, the support does not depend on `bibtex`/`biblatex` and supports any style (including custom styles). [[#18][]] * Functions `DocumenterCitations.set_latex_options` and `DocumenterCitations.reset_latex_options` to tweak the rendering of bibliographies in PDFs. +* The `Pages` in a `@bibliography` block can now use `@__FILE__` to refer to the current file. [[#22][]] ### Internal Changes @@ -121,6 +123,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#34]: https://github.com/JuliaDocs/DocumenterCitations.jl/issues/34 [#32]: https://github.com/JuliaDocs/DocumenterCitations.jl/pull/32 [#31]: https://github.com/JuliaDocs/DocumenterCitations.jl/pull/31 +[#22]: https://github.com/JuliaDocs/DocumenterCitations.jl/issues/22 [#20]: https://github.com/JuliaDocs/DocumenterCitations.jl/issues/20 [#19]: https://github.com/JuliaDocs/DocumenterCitations.jl/issues/19 [#18]: https://github.com/JuliaDocs/DocumenterCitations.jl/issues/18 diff --git a/docs/src/syntax.md b/docs/src/syntax.md index 725b651..a166325 100644 --- a/docs/src/syntax.md +++ b/docs/src/syntax.md @@ -109,7 +109,7 @@ This exact setup is shown on the [References](@ref) page. Sometimes it can be useful to render a subset of the bibliography, e.g., to show the references for a particular page. Two things are required to achieve this: -* To filter the bibliography to a specific page (or set of pages), add a `Pages` field to the `@bibliography` block. +* To filter the bibliography to a specific page (or set of pages), add a `Pages` field to the `@bibliography` block. Exactly like the `Pages` field in Documenter's [`@index`](https://documenter.juliadocs.org/stable/man/syntax/#@index-block) and [`@contents`](https://documenter.juliadocs.org/stable/man/syntax/#@contents-block) blocks, it must evaluate to a list of paths to `.md` files, relative to the folder containing the current file. The value `@__FILE__` as an element in the list can be used to refer to the current file. * To get around the caveat with [multiple `@bibliography` blocks](@ref canonical) that there can only be one canonical target for each citation, add `Canonical = false` to the `@bibliography` block. The resulting bibliography will be rendered in full, but it will not serve as a link target. This is the only way to have a reference rendered more than once. diff --git a/src/DocumenterCitations.jl b/src/DocumenterCitations.jl index d724361..1c3498a 100644 --- a/src/DocumenterCitations.jl +++ b/src/DocumenterCitations.jl @@ -48,20 +48,28 @@ should not be considered part of the stable API. blocks """ struct CitationBibliography <: Documenter.Plugin + # name of bib file bibfile::String + # Style name or object (built-in styles are symbols, but custom styles can # be anything) style::Any + # citation key => entry (set on instantiation; private) entries::OrderedDict{String,<:Bibliography.AbstractEntry} + # citation key => order index (when citation was first seen; private) citations::OrderedDict{String,Int64} - # page file name => set of citation keys (private) + + # page file name => set of citation keys (private). The page file names are + # relative to `doc.user.source`, which matches `doc.plueprint.pages` page_citations::Dict{String,Set{String}} + # AnchorMap object that stores the link anchors to all references in # canonical bibliography blocks anchor_map::Documenter.AnchorMap + end function CitationBibliography(bibfile::AbstractString=""; style=nothing) diff --git a/src/expand_bibliography.jl b/src/expand_bibliography.jl index f434d19..75a27b4 100644 --- a/src/expand_bibliography.jl +++ b/src/expand_bibliography.jl @@ -164,7 +164,9 @@ function parse_bibliography_block(block, doc, page) lines = String[] for (ex, str) in Documenter.parseblock(block, doc, page; raise=false) if Documenter.isassign(ex) - fields[ex.args[1]] = Core.eval(Main, ex.args[2]) + key = ex.args[1] + val = Core.eval(Main, ex.args[2]) + fields[key] = val else line = String(strip(str)) if length(line) > 0 @@ -177,22 +179,45 @@ function parse_bibliography_block(block, doc, page) end allowed_fields = Set{Symbol}((:Canonical, :Pages, :Sorting, :Style)) # Note: :Sorting and :Style are undocumented features + warn_loc = "N/A" + if (doc ≢ nothing) && (page ≢ nothing) + warn_loc = Documenter.locrepr( + page.source, + Documenter.find_block_in_file(block, page.source) + ) + end for field in keys(fields) if field ∉ allowed_fields - warn_loc = "N/A" - if (doc ≢ nothing) && (page ≢ nothing) - warn_loc = Documenter.locrepr( - page.source, - Documenter.find_block_in_file(block, page.source) - ) - end @warn("Invalid field $field ∉ $allowed_fields in $warn_loc") (doc ≢ nothing) && push!(doc.internal.errors, :bibliography_block) end end + if (:Canonical in keys(fields)) && !(fields[:Canonical] isa Bool) + @warn "The field `Canonical` in $warn_loc must evaluate to a boolean. Setting invalid `Canonical=$(repr(fields[:Canonical]))` to `Canonical=false`" + fields[:Canonical] = false + (doc ≢ nothing) && push!(doc.internal.errors, :bibliography_block) + end + if (:Pages in keys(fields)) && !(fields[:Pages] isa Vector) + @warn "The field `Pages` in $warn_loc must evaluate to a list of strings. Setting invalid `Pages = $(repr(fields[:Pages]))` to `Pages = []`" + fields[:Pages] = String[] + (doc ≢ nothing) && push!(doc.internal.errors, :bibliography_block) + elseif :Pages in keys(fields) + # Pages is a Vector, but maybe not a Vector of strings + fields[:Pages] = [_assert_string(name, doc, warn_loc) for name in fields[:Pages]] + end return fields, lines end +function _assert_string(val, doc, warn_loc) + str = string(val) # doesn't ever seem to fail + if str != val + @warn "The value `$(repr(val))` in $warn_loc is not a string. Replacing with $(repr(str))" + (doc ≢ nothing) && push!(doc.internal.errors, :bibliography_block) + end + return str +end + + # Expand a single @bibliography block function expand_bibliography(node::MarkdownAST.Node, meta, page, doc) @assert node.element isa MarkdownAST.CodeBlock @@ -227,24 +252,53 @@ function expand_bibliography(node::MarkdownAST.Node, meta, page, doc) keys_to_show = OrderedSet{String}() # first, cited keys (filter by Pages) - if :Pages in keys(fields) - for key in keys(citations) - for file in fields[:Pages] - if key in page_citations[file] - push!(keys_to_show, key) - @debug "Add $key to keys_to_show (from page $file)" - break # only need the first page that cites the key + if (length(citations) > 0) && (:Pages in keys(fields)) + page_folder = dirname(Documenter.pagekey(doc, page)) + # The `page_folder` is relative to `doc.user.source` (corresponding to + # the definition of the keys in `page_citations`) + keys_in_pages = Set{String}() # not ordered (see below) + Pages = _resolve__FILE__(fields[:Pages], page) + @debug "filtering citations to Pages" Pages + for name in Pages + # names in `Pages` are supposed to be relative to the folder + # containing the file containing the `@bibliography` block, + # i.e., `page_folder` + file = normpath(page_folder, name) + # `file` should now be a valid key in `page_citations` + try + @debug "Add keys cited in $file to keys_to_show" + push!(keys_in_pages, page_citations[file]...) + catch exc + @assert exc isa KeyError + expected_file = normpath(doc.user.source, page_folder, name) + if isfile(expected_file) + @error "Invalid $(repr(name)) in Pages attribute of @bibliography block on page $(page.source): File $(repr(expected_file)) exists but no references were collected." + else + # Files that don't contain any citations don't show up in + # `page_citations`. + @error "Invalid $(repr(name)) in Pages attribute of @bibliography block on page $(page.source): No such file $(repr(expected_file))." end + push!(doc.internal.errors, :bibliography_block) + continue end end + keys_to_add = [k for k in keys(citations) if k in keys_in_pages] + if length(keys_to_add) > 0 + push!(keys_to_show, keys_to_add...) + @debug "Collected keys_to_show from Pages" keys_to_show + elseif length(lines) == 0 + # Only warn if there are no explicit keys. Otherwise, the common + # idiom of `Pages = []` (with explicit keys) would fail + @warn "No cited keys remaining after filtering to Pages" Pages + end else # all cited keys if length(citations) > 0 push!(keys_to_show, keys(citations)...) + @debug "Add all cited keys to keys_to_show" keys(citations) else @warn "There were no citations" end - @debug "Add all cited keys to keys_to_show" citations end # second, explicitly listed keys @@ -264,7 +318,7 @@ function expand_bibliography(node::MarkdownAST.Node, meta, page, doc) end end - @debug "Determined keys to show" keys_to_show + @debug "Determined full list of keys to show" keys_to_show tag = bib_html_list_style(style) allowed_tags = (:ol, :ul, :dl) @@ -332,3 +386,24 @@ function expand_bibliography(node::MarkdownAST.Node, meta, page, doc) node.element = bibliography_node end + + +# Deal with `@__FILE__` in `Pages`, convert it to the name of the current file. +function _resolve__FILE__(Pages, page) + __FILE__ = let ex = Meta.parse("_ = @__FILE__", 1; raise=false)[1] + # What does a `@__FILE__` in the Pages list evaluate to? + # Cf. `Core.eval` in `parse_bibliography_block`. + # Should be the string "none", but that's an implementation detail. + Core.eval(Main, ex.args[2]) + end + result = String[] + for name in Pages + if name == __FILE__ + # Replace @__FILE__ in Pages with the current file: + name = basename(page.source) + @debug "__@FILE__ -> $(repr(name)) in Pages attribute of @bibliography block on page $(page.source)" + end + push!(result, name) + end + return result +end diff --git a/test/file_content.jl b/test/file_content.jl new file mode 100644 index 0000000..f947e33 --- /dev/null +++ b/test/file_content.jl @@ -0,0 +1,42 @@ +"""Wrapper around the content of a text file, for testing. + +```julia +file = FileContent(name) +``` + +can be used as + +```julia +@test file.exists +@test "string" in file +@test contains(file, "string") +``` + +Apart from providing the convenient `in`, when the test fails, this produces +more useful output than the equivalent + +``` +file = read(name, String) +@test contains(file, "string") +``` + +in that it doesn't dump the entire content of the file to the screen. +""" +struct FileContent + name::String + exists::Bool + content::String + function FileContent(filename) + if isfile(filename) + new(abspath(filename), true, read(filename, String)) + else + new(abspath(filename), false, "") + end + end +end + +Base.show(io::IO, f::FileContent) = print(io, "") + +Base.in(str, file::FileContent) = contains(file.content, str) + +Base.contains(file::FileContent, str) = contains(file.content, str) diff --git a/test/runtests.jl b/test/runtests.jl index 94cf695..805f44b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -16,6 +16,11 @@ using DocumenterCitations include("test_parse_bibliography_block.jl") end + println("\n* bibliography_block_pages (test_bibliography_block_pages.jl):") + @time @safetestset "bibliography_block_pages" begin + include("test_bibliography_block_pages.jl") + end + println("\n* parse_citation_link (test_parse_citation_link.jl):") @time @safetestset "parse_citation_link" begin include("test_parse_citation_link.jl") diff --git a/test/test_bibliography_block_pages.jl b/test/test_bibliography_block_pages.jl new file mode 100644 index 0000000..4147ef0 --- /dev/null +++ b/test/test_bibliography_block_pages.jl @@ -0,0 +1,181 @@ +using DocumenterCitations +using Test + +include("run_makedocs.jl") +include("file_content.jl") + +@testset "Bibliograhies with Pages" begin + + bib = CitationBibliography(DocumenterCitations.example_bibfile, style=:numeric) + + # non-strict + run_makedocs( + splitext(@__FILE__)[1]; + sitename="Test", + warnonly=true, + plugins=[bib], + format=Documenter.HTML(prettyurls=false, repolink=""), + pages=[ + "Home" => "index.md", + "A" => joinpath("part1", "section1", "p1_s1_page.md"), + "B" => joinpath("part1", "section2", "p1_s2_page.md"), + "C" => joinpath("part2", "section1", "p2_s1_page.md"), + "D" => joinpath("part2", "section2", "p2_s2_page.md"), + "E" => joinpath("part3", "section1", "p3_s1_page.md"), + "F" => joinpath("part3", "section2", "p3_s2_page.md"), + "Invalid" => joinpath("part3", "section2", "invalidpages.md"), + "References" => "references.md", + "Addendum" => "addendum.md", + ], + check_success=true + ) do dir, result, success, backtrace, output + + # regex for strings containing paths + prx(s) = Regex(replace(s, "/" => "[\\/]+", "." => "\\.", "[" => "\\[", "]" => "\\]")) + + @test success + #! format: off + @test contains(output, prx("Error: Invalid \"index.md\" in Pages attribute of @bibliography block on page src/part3/section2/invalidpages.md: No such file \"src/part3/section2/index.md\".")) + @test contains(output, prx("Error: Invalid \"p3_s1_page.md\" in Pages attribute of @bibliography block on page src/part3/section2/invalidpages.md: No such file \"src/part3/section2/p3_s1_page.md\".")) + @test contains(output, prx("Error: Invalid \"noexist.md\" in Pages attribute of @bibliography block on page src/part3/section2/invalidpages.md: No such file \"src/part3/section2/noexist.md\".")) + @test contains(output, "Warning: No cited keys remaining after filtering to Pages") + @test contains(output, prx("Error: Invalid \"../../addendum.md\" in Pages attribute of @bibliography block on page src/part3/section2/invalidpages.md: File \"src/addendum.md\" exists but no references were collected.")) + @test contains(output, prx("Error: Invalid \"p3_s1_page.md\" in Pages attribute of @bibliography block on page src/part3/section2/invalidpages.md: No such file \"src/part3/section2/p3_s1_page.md\".")) + @test contains(output, prx("Warning: The field `Pages` in src/part3/section2/invalidpages.md:41-44 must evaluate to a list of strings. Setting invalid `Pages = \"none\"` to `Pages = []`")) + #! format: on + + build(paths...) = joinpath(dir, "build", paths...) + citation(n) = Regex("\\[$n\\]") + contentlink(name) = ".html#$(replace(name, " " => "-"))\">$name" + + index_html = FileContent(build("index.html")) + @test index_html.exists + @test citation(1) in index_html + + p1_s1_page_html = FileContent(build("part1", "section1", "p1_s1_page.html")) + @test p1_s1_page_html.exists + @test citation(2) in p1_s1_page_html + @test citation(3) in p1_s1_page_html + @test "
[2]
" in p1_s1_page_html + @test "
[3]
" in p1_s1_page_html + @test contentlink("A: Part 1.1") in p1_s1_page_html + + p1_s2_page_html = FileContent(build("part1", "section2", "p1_s2_page.html")) + @test p1_s2_page_html.exists + @test citation(3) in p1_s2_page_html + @test citation(4) in p1_s2_page_html + @test !("
[2]
" in p1_s2_page_html) + @test "
[3]
" in p1_s2_page_html + @test "
[4]
" in p1_s2_page_html + @test "content here is empty" in p1_s2_page_html + + p2_s1_page_html = FileContent(build("part2", "section1", "p2_s1_page.html")) + @test p2_s1_page_html.exists + @test citation(5) in p2_s1_page_html + @test citation(5) in p2_s1_page_html + @test "
[2]
" in p2_s1_page_html + @test "
[3]
" in p2_s1_page_html + @test "
[4]
" in p2_s1_page_html + @test "
[5]
" in p2_s1_page_html + @test "
[6]
" in p2_s1_page_html + @test contentlink("C: Part 2.1") in p2_s1_page_html + @test contentlink("A: Part 1.1") in p2_s1_page_html + @test contentlink("B: Part 1.2") in p2_s1_page_html + + p2_s2_page_html = FileContent(build("part2", "section2", "p2_s2_page.html")) + @test p2_s2_page_html.exists + @test citation(7) in p2_s2_page_html + @test citation(8) in p2_s2_page_html + @test "
[2]
" in p2_s2_page_html + @test "
[3]
" in p2_s2_page_html + @test "
[4]
" in p2_s2_page_html + @test "
[5]
" in p2_s2_page_html + @test "
[6]
" in p2_s2_page_html + @test !("
[7]
" in p2_s2_page_html) + @test !("
[8]
" in p2_s2_page_html) + @test contentlink("A: Part 1.1") in p2_s2_page_html + @test contentlink("B: Part 1.2") in p2_s2_page_html + @test contentlink("C: Part 2.1") in p2_s2_page_html + @test !(contentlink("D: Part 2.2") in p2_s2_page_html) + + p3_s1_page_html = FileContent(build("part3", "section1", "p3_s1_page.html")) + @test p3_s1_page_html.exists + @test citation(9) in p3_s1_page_html + @test citation(10) in p3_s1_page_html + @test "
[2]
" in p3_s1_page_html + @test "
[3]
" in p3_s1_page_html + @test "
[4]
" in p3_s1_page_html + @test "
[5]
" in p3_s1_page_html + @test "
[6]
" in p3_s1_page_html + @test "
[7]
" in p3_s1_page_html + @test "
[8]
" in p3_s1_page_html + @test !("
[9]
" in p3_s1_page_html) + @test !("
[10]
" in p3_s1_page_html) + @test contentlink("A: Part 1.1") in p3_s1_page_html + @test contentlink("B: Part 1.2") in p3_s1_page_html + @test contentlink("C: Part 2.1") in p3_s1_page_html + @test contentlink("D: Part 2.2") in p3_s1_page_html + @test !(contentlink("E: Part 3.1") in p3_s1_page_html) + + p3_s2_page_html = FileContent(build("part3", "section2", "p3_s2_page.html")) + @test p3_s2_page_html.exists + @test citation(11) in p3_s2_page_html + @test citation(12) in p3_s2_page_html + @test "
[2]
" in p3_s2_page_html + @test "
[3]
" in p3_s2_page_html + @test "
[4]
" in p3_s2_page_html + @test "
[5]
" in p3_s2_page_html + @test "
[6]
" in p3_s2_page_html + @test "
[7]
" in p3_s2_page_html + @test "
[8]
" in p3_s2_page_html + @test "
[9]
" in p3_s2_page_html + @test "
[10]
" in p3_s2_page_html + @test !("
[11]
" in p3_s2_page_html) + @test !("
[12]
" in p3_s2_page_html) + @test contentlink("A: Part 1.1") in p3_s2_page_html + @test contentlink("C: Part 2.1") in p3_s2_page_html + @test contentlink("B: Part 1.2") in p3_s2_page_html + @test contentlink("D: Part 2.2") in p3_s2_page_html + @test contentlink("E: Part 3.1") in p3_s2_page_html + + invalidpages_html = FileContent(build("part3", "section2", "invalidpages.html")) + @test invalidpages_html.exists + @test "Nothing should render here" in invalidpages_html + @test "Again, nothing should render here" in invalidpages_html + @test "
[11]
" in invalidpages_html + @test "
[12]
" in invalidpages_html + + addendum_html = FileContent(build("addendum.html")) + @test "No references are cited on this page" in addendum_html + + end + + # strict + run_makedocs( + splitext(@__FILE__)[1]; + sitename="Test", + warnonly=false, + plugins=[bib], + format=Documenter.HTML(prettyurls=false, repolink=""), + check_failure=true + ) do dir, result, success, backtrace, output + + # regex for strings containing paths + prx(s) = Regex(replace(s, "/" => "[\\/]+", "." => "\\.", "[" => "\\[", "]" => "\\]")) + + @test !success + #! format: off + @test contains(output, prx("Error: Invalid \"index.md\" in Pages attribute of @bibliography block on page src/part3/section2/invalidpages.md: No such file \"src/part3/section2/index.md\".")) + @test contains(output, prx("Error: Invalid \"p3_s1_page.md\" in Pages attribute of @bibliography block on page src/part3/section2/invalidpages.md: No such file \"src/part3/section2/p3_s1_page.md\".")) + @test contains(output, prx("Error: Invalid \"noexist.md\" in Pages attribute of @bibliography block on page src/part3/section2/invalidpages.md: No such file \"src/part3/section2/noexist.md\".")) + @test contains(output, "Warning: No cited keys remaining after filtering to Pages") + @test contains(output, prx("Error: Invalid \"../../addendum.md\" in Pages attribute of @bibliography block on page src/part3/section2/invalidpages.md: File \"src/addendum.md\" exists but no references were collected.")) + @test contains(output, prx("Error: Invalid \"p3_s1_page.md\" in Pages attribute of @bibliography block on page src/part3/section2/invalidpages.md: No such file \"src/part3/section2/p3_s1_page.md\".")) + @test contains(output, prx("Warning: The field `Pages` in src/part3/section2/invalidpages.md:41-44 must evaluate to a list of strings. Setting invalid `Pages = \"none\"` to `Pages = []`")) + #! format: on + @test result isa ErrorException + @test occursin("`makedocs` encountered an error [:bibliography_block]", result.msg) + + end + +end diff --git a/test/test_bibliography_block_pages/src/addendum.md b/test/test_bibliography_block_pages/src/addendum.md new file mode 100644 index 0000000..e93b39d --- /dev/null +++ b/test/test_bibliography_block_pages/src/addendum.md @@ -0,0 +1,3 @@ +# Addendum + +No references are cited on this page. diff --git a/test/test_bibliography_block_pages/src/index.md b/test/test_bibliography_block_pages/src/index.md new file mode 100644 index 0000000..e201c3e --- /dev/null +++ b/test/test_bibliography_block_pages/src/index.md @@ -0,0 +1,21 @@ +# Test Project + +Ref [BrifNJP2010](@cite) + +## Site content + +The following table of contents replicates the order in the side bar. + +```@contents +Pages = [ + joinpath("part1", "section1", "p1_s1_page.md"), + joinpath("part1", "section2", "p1_s2_page.md"), + joinpath("part2", "section1", "p2_s1_page.md"), + joinpath("part2", "section2", "p2_s2_page.md"), + joinpath("part3", "section1", "p3_s1_page.md"), + joinpath("part3", "section2", "p3_s2_page.md"), + joinpath("part3", "section2", "invalidpages.md"), + "references.md", + "addendum.md", +] +``` diff --git a/test/test_bibliography_block_pages/src/part1/section1/p1_s1_page.md b/test/test_bibliography_block_pages/src/part1/section1/p1_s1_page.md new file mode 100644 index 0000000..252c7bf --- /dev/null +++ b/test/test_bibliography_block_pages/src/part1/section1/p1_s1_page.md @@ -0,0 +1,28 @@ +# A: Part 1.1 + +## First Section of Part 1.1 + +Ref [Shapiro2012](@cite) + + +## Second Section of Part 1.1 + +Ref [KochJPCM2016](@cite) + + +## Local References of Part 1.1 + +**Content** + +The content matches the bibliography. The point of this (here and in the following pages) is to verify that the `Pages` attribute behaves consistently between `@contents` blocks and `@bibliography` blocks. + +```@contents +Pages = ["p1_s1_page.md"] +``` + +**Bibliography** + +```@bibliography +Canonical = false +Pages = ["p1_s1_page.md"] +``` diff --git a/test/test_bibliography_block_pages/src/part1/section2/p1_s2_page.md b/test/test_bibliography_block_pages/src/part1/section2/p1_s2_page.md new file mode 100644 index 0000000..11aa4db --- /dev/null +++ b/test/test_bibliography_block_pages/src/part1/section2/p1_s2_page.md @@ -0,0 +1,29 @@ +# B: Part 1.2 + + +## First Section of Part 1.2 + +Ref [KochJPCM2016](@cite) (duplicated from [Second Section of Part 1.1](@ref)) + + +## Second Section of Part 1.2 + +Ref [SolaAAMOP2018](@cite) + + +## Local References of Part 1.2 + +**Content** + +The content here is empty, because `@contents` doesn't support `@__FILE__` (but doesn't complain either). + +```@contents +Pages = [@__FILE__] +``` + +**Bibliography** + +```@bibliography +Canonical = false +Pages = [@__FILE__] # We can have comments! +``` diff --git a/test/test_bibliography_block_pages/src/part2/section1/p2_s1_page.md b/test/test_bibliography_block_pages/src/part2/section1/p2_s1_page.md new file mode 100644 index 0000000..bb63a7f --- /dev/null +++ b/test/test_bibliography_block_pages/src/part2/section1/p2_s1_page.md @@ -0,0 +1,39 @@ +# C: Part 2.1 + + +## First Section of Part 2.1 + +Ref [MorzhinRMS2019](@cite) + + +## Second Section of Part 2.1 + +Ref [BrumerShapiro2003](@cite) + + +## Local References of Part 2.1 and Earlier Parts + +This excludes the reference in `index.md`. + +**Content** + +The content matches the bibliography + +```@contents +Pages = [ + "p2_s1_page.md", # @__FILE__ is not supported + "../../part1/section1/p1_s1_page.md", + "../../part1/section2/p1_s2_page.md", +] +``` + +**Bibliography** + +```@bibliography +Canonical = false +Pages = [ + @__FILE__, # In the @bibliography block, we can use `@__FILE` + "../../part1/section1/p1_s1_page.md", + "../../part1/section2/p1_s2_page.md", +] +``` diff --git a/test/test_bibliography_block_pages/src/part2/section2/p2_s2_page.md b/test/test_bibliography_block_pages/src/part2/section2/p2_s2_page.md new file mode 100644 index 0000000..fddb6c7 --- /dev/null +++ b/test/test_bibliography_block_pages/src/part2/section2/p2_s2_page.md @@ -0,0 +1,41 @@ +# D: Part 2.2 + + +## First Section of Part 2.2 + +Ref [GoerzDiploma2010](@cite) + + +## Second Section of Part 2.2 + +Ref [GoerzJPB2011](@cite) + + +## Local References of Earlier Parts + +This excludes the reference in `index.md` as well as the references in the *current* document. + +One of the `Pages` uses `joinpath`, as an example for something where `eval` has to do some work. + +**Content** + +The content matches the bibliography + +```@contents +Pages = [ + joinpath("..", "..", "part1", "section1", "p1_s1_page.md"), + "../../part1/section2/p1_s2_page.md", + "../section1/p2_s1_page.md", +] +``` + +**Bibliography** + +```@bibliography +Canonical = false +Pages = [ + joinpath("..", "..", "part1", "section1", "p1_s1_page.md"), + "../../part1/section2/p1_s2_page.md", + "../section1/p2_s1_page.md", +] +``` diff --git a/test/test_bibliography_block_pages/src/part3/section1/p3_s1_page.md b/test/test_bibliography_block_pages/src/part3/section1/p3_s1_page.md new file mode 100644 index 0000000..3c467b8 --- /dev/null +++ b/test/test_bibliography_block_pages/src/part3/section1/p3_s1_page.md @@ -0,0 +1,41 @@ +# E: Part 3.1 + + +## First Section of Part 3.1 + +Ref [TomzaPRA2012](@cite) + + +## Second Section of Part 3.1 + +Ref [GoerzNJP2014](@cite) + + +## Local References of Earlier Parts + +This excludes the reference in `index.md` as well as the references in the *current* document. + +**Content** + +The content matches the bibliography + +```@contents +Pages = [ + "../../part1/section1/p1_s1_page.md", + "../../part1/section2/p1_s2_page.md", + "../../part2/section1/p2_s1_page.md", + "../../part2/section2/p2_s2_page.md", +] +``` + +**Bibliography** + +```@bibliography +Canonical = false +Pages = [ + "../../part1/section1/p1_s1_page.md", + "../../part1/section2/p1_s2_page.md", + "../../part2/section1/p2_s1_page.md", + "../../part2/section2/p2_s2_page.md", +] +``` diff --git a/test/test_bibliography_block_pages/src/part3/section2/invalidpages.md b/test/test_bibliography_block_pages/src/part3/section2/invalidpages.md new file mode 100644 index 0000000..8ff61bd --- /dev/null +++ b/test/test_bibliography_block_pages/src/part3/section2/invalidpages.md @@ -0,0 +1,45 @@ +# Test of Invalid Pages + +## References from Nonexisting Pages + +Nothing should render here + +```@bibliography +Canonical = false +Pages = [ + "index.md", + "p3_s1_page.md", + "noexist.md", +] +``` + +## References only from Pages that contain no references + +Again, nothing should render here. + +```@bibliography +Canonical = false +Pages = [ + "../../addendum.md", +] +``` + +## References Mixing Existing and Nonexisting Pages + +```@bibliography +Canonical = false +Pages = [ + "p3_s1_page.md", + "p3_s2_page.md", +] +``` + +The above bibliography should render only the references in [F: Part 3.2](@ref) (since the file `p3_s1_page.md` for [E: Part 3.1](@ref) exists in a different folder). + +## Not passing a list to Pages + +```@bibliography +Canonical = false +Pages = @__FILE__ +``` + diff --git a/test/test_bibliography_block_pages/src/part3/section2/p3_s2_page.md b/test/test_bibliography_block_pages/src/part3/section2/p3_s2_page.md new file mode 100644 index 0000000..ac0124f --- /dev/null +++ b/test/test_bibliography_block_pages/src/part3/section2/p3_s2_page.md @@ -0,0 +1,46 @@ +# F: Part 3.2 + + +## First Section of Part 3.2 + +Ref [GoerzPRA2014](@cite) + + +## Second Section of Part 3.2 + +Ref [JaegerPRA2014](@cite) + + +## Local References of Earlier Parts + +This excludes the reference in `index.md` as well as the references in the *current* document. + +The bibliography also uses a pretty fancy expression to test that we can `eval` non-trivial specification of `Pages`, not just a list of strings. + + +**Content** + +The content matches the bibliography + +```@contents +Pages = [ + [ + joinpath("..", "..", "part$p", "section$s", "p$(p)_s$(s)_page.md") for + (p, s) in Iterators.product((1, 2), (1, 2)) + ]..., + "../section1/p3_s1_page.md", +] +``` + +**Bibliography** + +```@bibliography +Canonical = false +Pages = [ + [ + joinpath("..", "..", "part$p", "section$s", "p$(p)_s$(s)_page.md") for + (p, s) in Iterators.product((1, 2), (1, 2)) + ]..., + "../section1/p3_s1_page.md", +] +``` diff --git a/test/test_bibliography_block_pages/src/references.md b/test/test_bibliography_block_pages/src/references.md new file mode 100644 index 0000000..4a8633f --- /dev/null +++ b/test/test_bibliography_block_pages/src/references.md @@ -0,0 +1,13 @@ +# Site references + +```@bibliography +Pages = [ + "index.md", + "part1/section1/p1_s1_page.md", + "part1/section2/p1_s2_page.md", + "part2/section1/p2_s1_page.md", + "part2/section2/p2_s2_page.md", + "part3/section1/p3_s1_page.md", + "part3/section2/p3_s2_page.md", +] +``` diff --git a/test/test_latex_rendering.jl b/test/test_latex_rendering.jl index e00600b..e5a31e7 100644 --- a/test/test_latex_rendering.jl +++ b/test/test_latex_rendering.jl @@ -3,6 +3,7 @@ using Documenter using Test include("run_makedocs.jl") +include("file_content.jl") CUSTOM1 = joinpath(@__DIR__, "..", "docs", "custom_styles", "enumauthoryear.jl") CUSTOM2 = joinpath(@__DIR__, "..", "docs", "custom_styles", "keylabels.jl") @@ -77,20 +78,22 @@ end tex_outfile = joinpath(dir, "build", "DocumenterCitations.jl.tex") @test isfile(tex_outfile) - tex = read(tex_outfile, String) - tex_contains(str) = contains(tex, str) - @test tex_contains(raw"{\raggedright% @bibliography") - @test tex_contains(raw"}% end @bibliography") + tex = FileContent(tex_outfile) + @test raw"{\raggedright% @bibliography" in tex + @test raw"}% end @bibliography" in tex # must use `\hypertarget{id}{}`, not `\hypertarget{id}` - @test tex_contains(r"\\hypertarget{\d+}{}") - @test tex_contains( + @test r"\\hypertarget{\d+}{}" in tex + @test contains( + tex, r"\\hypertarget{\d+}{}\\href{http://qist\.lanl\.gov}{\\emph{Quantum Computation Roadmap}} \(2004\)" ) - @test tex_contains( + @test contains( + tex, raw"\hangindent=0.33in {\makebox[{\ifdim0.33in<\dimexpr\width+1ex\relax\dimexpr\width+1ex\relax\else0.33in\fi}][l]{[1]}}" ) nbsp = "\u00A0" # nonbreaking space - @test tex_contains( + @test contains( + tex, "\\hangindent=0.33in Brif,$(nbsp)C.; Chakrabarti,$(nbsp)R. and Rabitz,$(nbsp)H. (2010)." ) # authoryear :ul @@ -136,12 +139,12 @@ end tex_outfile = joinpath(dir, "build", "DocumenterCitations.jl.tex") @test isfile(tex_outfile) - tex = read(tex_outfile, String) - tex_contains(str) = contains(tex, str) - @test tex_contains(raw"{% @bibliography") - @test tex_contains(raw"}% end @bibliography") + tex = FileContent(tex_outfile) + @test raw"{% @bibliography" in tex + @test raw"}% end @bibliography" in tex nbsp = "\u00A0" # nonbreaking space - @test tex_contains( + @test contains( + tex, "\\begin{itemize}\n\\item Brif,$(nbsp)C.; Chakrabarti,$(nbsp)R. and Rabitz,$(nbsp)H. (2010)." ) # authoryear :ul @@ -200,18 +203,20 @@ end tex_outfile = joinpath(dir, "build", "DocumenterCitations.jl.tex") @test isfile(tex_outfile) - tex = read(tex_outfile, String) - tex_contains(str) = contains(tex, str) - @test tex_contains(raw"{\raggedright% @bibliography") - @test tex_contains(raw"}% end @bibliography") + tex = FileContent(tex_outfile) + @test raw"{\raggedright% @bibliography" in tex + @test raw"}% end @bibliography" in tex nbsp = "\u00A0" # nonbreaking space - @test tex_contains( + @test contains( + tex, "\\hangindent=1cm Brif,$(nbsp)C.; Chakrabarti,$(nbsp)R. and Rabitz,$(nbsp)H. (2010)." ) # authoryear :ul - @test tex_contains( + @test contains( + tex, raw"\hangindent=1.5cm {\makebox[{\ifdim2.0cm<\dimexpr\width+1ex\relax\dimexpr\width+1ex\relax\else2.0cm\fi}][l]{[BCR10]}}" ) # :alpha style - @test tex_contains( + @test contains( + tex, raw"\hangindent=1.5cm {\makebox[{\ifdim2.0cm<\dimexpr\width+1ex\relax\dimexpr\width+1ex\relax\else2.0cm\fi}][l]{[1]}}" ) # :numeric style diff --git a/test/test_parse_bibliography_block.jl b/test/test_parse_bibliography_block.jl index 7bcdb16..7e2cb9c 100644 --- a/test/test_parse_bibliography_block.jl +++ b/test/test_parse_bibliography_block.jl @@ -1,5 +1,6 @@ using Test using DocumenterCitations: parse_bibliography_block +using IOCapture: IOCapture @testset "parse_bibliography_block" begin @@ -14,6 +15,20 @@ using DocumenterCitations: parse_bibliography_block @test fields[:Pages] == ["index.md", "references.md"] @test lines == ["*", "GoerzPRA2010"] + block = raw""" + Pages = [ + "index.md", + "references.md" + ] + Canonical = true + + GoerzPRA2010 + """ + fields, lines = parse_bibliography_block(block, nothing, nothing) + @test fields[:Canonical] == true + @test fields[:Pages] == ["index.md", "references.md"] + @test lines == ["GoerzPRA2010"] + block = raw""" Canonical = false * @@ -29,3 +44,56 @@ using DocumenterCitations: parse_bibliography_block @test lines == [] end + + +@testset "invalid bibliography blocks" begin + + block = raw""" + Pages = "index.md" # not a list + Canonical = false + """ + c = IOCapture.capture() do + fields, lines = parse_bibliography_block(block, nothing, nothing) + @test fields[:Canonical] == false + @test fields[:Pages] == [] + @test lines == [] + end + @test contains( + c.output, + "Warning: The field `Pages` in N/A must evaluate to a list of strings. Setting invalid `Pages = \"index.md\"` to `Pages = []`" + ) + + block = raw""" + Pages = [1, 2] # not a list of strings + """ + c = IOCapture.capture() do + fields, lines = parse_bibliography_block(block, nothing, nothing) + @test fields[:Canonical] == true + @test fields[:Pages] == ["1", "2"] + @test lines == [] + end + @test contains( + c.output, + "Warning: The value `1` in N/A is not a string. Replacing with \"1\"" + ) + @test contains( + c.output, + "Warning: The value `2` in N/A is not a string. Replacing with \"2\"" + ) + + block = raw""" + Pages = ["index.md"] + Canonical = "true" # not a Bool + """ + c = IOCapture.capture() do + fields, lines = parse_bibliography_block(block, nothing, nothing) + @test fields[:Canonical] == false + @test fields[:Pages] == ["index.md"] + @test lines == [] + end + @test contains( + c.output, + "Warning: The field `Canonical` in N/A must evaluate to a boolean. Setting invalid `Canonical=\"true\"` to `Canonical=false`" + ) + +end