Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix behavior of Pages in @bibliography blocks #48

Merged
merged 1 commit into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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