Skip to content

Commit

Permalink
Fix behavior of Pages in @bibliography blocks
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
goerz committed Oct 15, 2023
1 parent 756db7b commit eb11fe6
Show file tree
Hide file tree
Showing 19 changed files with 732 additions and 39 deletions.
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/src/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
10 changes: 9 additions & 1 deletion src/DocumenterCitations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
109 changes: 92 additions & 17 deletions src/expand_bibliography.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
42 changes: 42 additions & 0 deletions test/file_content.jl
Original file line number Diff line number Diff line change
@@ -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, "<Content of $(f.name)>")

Base.in(str, file::FileContent) = contains(file.content, str)

Base.contains(file::FileContent, str) = contains(file.content, str)
5 changes: 5 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading

0 comments on commit eb11fe6

Please sign in to comment.