Skip to content

Commit 703d8b7

Browse files
committed
Add support for citations in PDFs/LaTeX
1 parent 07d99f0 commit 703d8b7

17 files changed

+798
-102
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ docs: test/Manifest.toml ## Build the documentation
3636
$(JULIA) --project=test docs/make.jl
3737
@echo "Done. Consider using 'make devrepl'"
3838

39+
pdf: test/Manifest.toml ## Build the documentation in PDF format
40+
$(JULIA) --project=test docs/makepdf.jl
41+
@echo "Done. Consider using 'make devrepl'"
42+
3943
servedocs: test/Manifest.toml ## Build (auto-rebuild) and serve documentation at PORT=8000
4044
$(JULIA) --project=test -e 'include("devrepl.jl"); servedocs(port=$(PORT), verbose=true)'
4145

NEWS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
* In general (depending on the style and citation syntax), citation links may now render to arbitrarily complex expressions.
1818
* Citation comments can now have inline markdown elements, e.g., `[GoerzQ2022; definition of $J$ in section *Running costs*](@cite)`
1919
* 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.
20+
* 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][]]
21+
* Functions `DocumenterCitations.set_latex_options` and `DocumenterCitations.reset_latex_options` to tweak the rendering of bibliographies in PDFs.
22+
2023

2124
### Internal Changes
2225

@@ -31,6 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3134
* Exposed the internal function `format_labeled_bibliography_reference` that implements `format_bibliography_reference` for the built-in styles `:numeric` and `:alpha`.
3235
* Exposed the internal function `format_authoryear_bibliography_reference` that implements `format_bibliography_reference` for the built-in style `:authoryear:`.
3336
* The example custom styles `:enumauthoryear` and `:keylabels` have been rewritten using the above internal functions, illustrating that custom styles will usually not have to rely on the undocumented and even more internal functions like `format_names` and `tex2unicode`.
37+
* Any `@bibliography` block is now internally expanded into an internal `BibliographyNode` instead of a raw HTML node. This `BibliographyNode` can then be translated into the desired output format by `Documenter.HTMLWriter` or `Documenter.LaTeXWriter`. This is how support for bibliographies with `format=Documenter.LaTeX()` can be achieved.
38+
* The routine `format_bibliography_reference` must now return a markdown string instead of an HTML string.
3439

3540

3641
## [Version 1.2.1][1.2.1] - 2023-09-22
@@ -118,6 +123,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
118123
[#31]: https://github.com/JuliaDocs/DocumenterCitations.jl/pull/31
119124
[#20]: https://github.com/JuliaDocs/DocumenterCitations.jl/issues/20
120125
[#19]: https://github.com/JuliaDocs/DocumenterCitations.jl/issues/19
126+
[#18]: https://github.com/JuliaDocs/DocumenterCitations.jl/issues/18
121127
[#16]: https://github.com/JuliaDocs/DocumenterCitations.jl/issues/16
122128
[#14]: https://github.com/JuliaDocs/DocumenterCitations.jl/issues/14
123129
[#6]: https://github.com/JuliaDocs/DocumenterCitations.jl/issues/6

docs/makepdf.jl

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,40 @@
1-
# In a future version, this script will generate a PDF version of the package
2-
# documentation
1+
using DocumenterCitations
2+
using Documenter
3+
using Pkg
4+
5+
PROJECT_TOML = Pkg.TOML.parsefile(joinpath(@__DIR__, "..", "Project.toml"))
6+
VERSION = PROJECT_TOML["version"]
7+
NAME = PROJECT_TOML["name"]
8+
AUTHORS = join(PROJECT_TOML["authors"], ", ") * " and contributors"
9+
GITHUB = "https://github.com/JuliaDocs/DocumenterCitations.jl"
10+
11+
bib = CitationBibliography(
12+
joinpath(@__DIR__, "src", "refs.bib");
13+
style=:numeric # default
14+
)
15+
16+
println("Starting makedocs")
17+
18+
include("custom_styles/enumauthoryear.jl")
19+
include("custom_styles/keylabels.jl")
20+
21+
withenv("DOCUMENTER_BUILD_PDF" => "1") do
22+
makedocs(
23+
authors=AUTHORS,
24+
linkcheck=true,
25+
warnonly=[:linkcheck,],
26+
sitename="DocumenterCitations.jl",
27+
format=Documenter.LaTeX(; version=VERSION),
28+
pages=[
29+
"Home" => "index.md",
30+
"Syntax" => "syntax.md",
31+
"Citation Style Gallery" => "gallery.md",
32+
"CSS Styling" => "styling.md",
33+
"Internals" => "internals.md",
34+
"References" => "references.md",
35+
],
36+
plugins=[bib],
37+
)
38+
end
39+
40+
println("Finished makedocs")

docs/src/assets/preamble.tex

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
\documentclass[oneside]{memoir}
2+
\usepackage{./documenter}
3+
\usepackage{./custom}
4+
5+
\renewcommand{\part}[1]{}
6+
7+
8+
\AfterPreamble{\hypersetup{
9+
pdfauthor={Michael H. Goerz and Contributors},
10+
pdftitle={\DocMainTitle{} v\DocVersion{}},
11+
pdfsubject={Documentation of Julia package \DocMainTitle{}}
12+
}}
13+
14+
15+
%% Title Page
16+
\title{%
17+
{\HUGE\DocMainTitle{}}\\
18+
\vspace{16pt}
19+
{\Large version \DocVersion{}}
20+
}
21+
\author{Michel H. Goerz and Contributors}
22+
23+
24+
\settocdepth{section}
25+
26+
27+
%% Main document begin
28+
\begin{document}
29+
\frontmatter
30+
\maketitle
31+
%\clearpage
32+
\tableofcontents
33+
\widowpenalty10000
34+
\clubpenalty10000
35+
\raggedright%
36+
\mainmatter

docs/src/gallery.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ Style = :alpha
8888
Canonical = false
8989
```
9090

91+
```@raw latex
92+
Compared to the HTML version of the documentation, the hanging indent in the above list of references is too small for the longer labels of the \texttt{:alpha} style. This can be remedied by adjusting the \texttt{dl\_hangindent} and \texttt{dl\_labelwidth} parameters with \hyperlinkref{sec:customizing_latex_output}{\texttt{DocumenterCitations.set\_latex\_options}}.
93+
```
94+
9195
Note that the `:alpha` style is able to automatically disambiguate labels:
9296

9397
```@bibliography
@@ -179,3 +183,7 @@ Pages = ["gallery.md"]
179183
Style = :keylabels
180184
Canonical = false
181185
```
186+
187+
```@raw latex
188+
As with the \texttt{:alpha} style, for \LaTeX{} output, the \texttt{dl\_hangindent} and \texttt{dl\_labelwidth} parameters should be adjusted with \hyperlinkref{sec:customizing_latex_output}{\texttt{DocumenterCitations.set\_latex\_options}} to obtain a more suitable hanging indent that matches the HTML version of this documentation.
189+
```

docs/src/index.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,20 @@ github_badge = "[![Github](https://img.shields.io/badge/JuliaDocs-DocumenterCita
1010
1111
version_badge = "![v$VERSION](https://img.shields.io/badge/version-v$(replace("$VERSION", "-" => "--"))-green.svg)"
1212
13-
Markdown.parse("$github_badge $version_badge")
13+
if get(ENV, "DOCUMENTER_BUILD_PDF", "") == ""
14+
Markdown.parse("$github_badge $version_badge")
15+
else
16+
Markdown.parse("""
17+
-----
18+
19+
On Github: [JuliaDocs/DocumenterCitations.jl](https://github.com/JuliaDocs/DocumenterCitations.jl)
20+
21+
Version: $VERSION
22+
23+
-----
24+
25+
""")
26+
end
1427
```
1528

1629
[DocumenterCitations.jl](https://github.com/JuliaDocs/DocumenterCitations.jl#readme) uses [Bibliography.jl](https://github.com/Humans-of-Julia/Bibliography.jl) to add support for BibTeX citations in documentation pages generated by [Documenter.jl](https://github.com/JuliaDocs/Documenter.jl).
@@ -104,6 +117,10 @@ makedocs(;
104117
deploydocs(; repo="github.com/JuliaDocs/DocumenterCitations.jl.git")
105118
```
106119

120+
Bibliographies are also supported in [PDFs generated via LaTeX](https://documenter.juliadocs.org/stable/man/other-formats/#pdf-output). All that is required is to replace `format=Documenter.HTML(…)` in the above code with `format=Documenter.LaTeX()`. See [`docs/makepdf.jl`](https://github.com/JuliaDocs/DocumenterCitations.jl/blob/master/docs/makepdf.jl) for an example. The resulting PDF files for the `DocumenterCitations` package are available as attachments to the [Releases](https://github.com/JuliaDocs/DocumenterCitations.jl/releases).
121+
122+
The rendering of the documentation may be fine-tuned using the [`DocumenterCitations.set_latex_options`](@ref) function. Note that the bibliography in LaTeX is directly rendered for the [different styles](@ref gallery) from the same internal representation as the HTML version. In particular, `bibtex`/`biblatex` is not involved in producing the PDF.
123+
107124

108125
## How to cite references in your documentation
109126

docs/src/internals.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ format_labeled_bibliography_reference
5151
format_authoryear_bibliography_reference
5252
```
5353

54+
### Customizing LaTeX output
55+
56+
```@raw latex
57+
\hypertarget{sec:customizing_latex_output}{}
58+
```
59+
60+
```@docs
61+
set_latex_options
62+
reset_latex_options
63+
```
64+
5465
### Citation links
5566

5667
The standard citation links described in [Syntax](@ref) are internally parsed into the [`DocumenterCitations.CitationLink`](@ref) data structure:

src/DocumenterCitations.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ include("md_ast.jl")
186186
include("citation_link.jl")
187187
include("collect_citations.jl")
188188
include("expand_citations.jl")
189+
include("latex_options.jl")
190+
include("bibliography_node.jl")
189191
include("expand_bibliography.jl")
190192
include("formatting.jl")
191193
include("labeled_styles_utils.jl")
@@ -202,6 +204,7 @@ function __init__()
202204
push!(Documenter.ERROR_NAMES, errname)
203205
end
204206
end
207+
reset_latex_options()
205208
end
206209

207210

src/bibliography_node.jl

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""Representation of a reference within a [`BibliographyNode`}(@ref).
2+
3+
# Properties
4+
5+
* `anchor_key`: the anchor key for the link target as a string (generally the
6+
BibTeX key), or `nothing` if the item is in a non-canonical bibliography
7+
block.
8+
* `label`: `MarkdownAST.Node` for the label of the entry. Usually a simple
9+
`Text` node, as labels in the default styles do not have inline formatting.
10+
or `nothing` if the style does not use labels
11+
the rendered bibliography.
12+
* `reference`: `MarkdownAST.Node` for the paragraph for the fully rendered
13+
reference.
14+
"""
15+
struct BibliographyItem
16+
anchor_key::Union{Nothing,String}
17+
label::Union{Nothing,MarkdownAST.Node{Nothing}}
18+
reference::MarkdownAST.Node{Nothing}
19+
end
20+
21+
22+
"""Node in `MarkdownAST` corresponding to a `@bibliography` block.
23+
24+
# Properties
25+
26+
* `list_style`: One of `:dl`, `:ul`, `:ol`, cf. [`bib_html_list_style`](@ref)
27+
* `canonical`: Whether or not the references in the `@bibliography` block are
28+
link targets.
29+
* `items`: A list of [`BibliographyItem`](@ref) objects, one for each reference
30+
in the block
31+
"""
32+
struct BibliographyNode <: Documenter.AbstractDocumenterBlock
33+
list_style::Symbol # one of :dl, :ul, :ol
34+
canonical::Bool
35+
items::Vector{BibliographyItem}
36+
end
37+
38+
39+
function Documenter.MDFlatten.mdflatten(io, ::MarkdownAST.Node, b::BibliographyNode)
40+
for item in b.items
41+
Documenter.MDFlatten.mdflatten(io, item.reference)
42+
print(io, "\n\n\n\n")
43+
end
44+
end
45+
46+
47+
function Documenter.HTMLWriter.domify(
48+
dctx::Documenter.HTMLWriter.DCtx,
49+
node::Documenter.Node,
50+
bibliography::BibliographyNode
51+
)
52+
@assert node.element === bibliography
53+
return domify_bib(dctx, bibliography)
54+
end
55+
56+
57+
function domify_bib(dctx::Documenter.HTMLWriter.DCtx, bibliography::BibliographyNode)
58+
Documenter.DOM.@tags dl ul ol li div dt dd
59+
list_tag = dl
60+
if bibliography.list_style == :ul
61+
list_tag = ul
62+
elseif bibliography.list_style == :ol
63+
list_tag = ol
64+
end
65+
html_list = list_tag()
66+
for item in bibliography.items
67+
anchor_key = isnothing(item.anchor_key) ? "" : "#$(item.anchor_key)"
68+
html_reference = Documenter.HTMLWriter.domify(dctx, item.reference.children)
69+
if bibliography.list_style == :dl
70+
html_label = Documenter.HTMLWriter.domify(dctx, item.label.children)
71+
push!(html_list.nodes, dt(html_label))
72+
push!(html_list.nodes, dd(div["$(anchor_key)"](html_reference)))
73+
else
74+
push!(html_list.nodes, li(div["$(anchor_key)"](html_reference)))
75+
end
76+
end
77+
class = ".citation"
78+
if bibliography.canonical
79+
class *= ".canonical"
80+
else
81+
class *= ".noncanonical"
82+
end
83+
return div[class](html_list)
84+
end
85+
86+
87+
_hash(x) = string(hash(x))
88+
89+
90+
function _wrapblock(f, io, env)
91+
if !isnothing(env)
92+
println(io, "\\begin{", env, "}")
93+
end
94+
f()
95+
if !isnothing(env)
96+
println(io, "\\end{", env, "}")
97+
end
98+
end
99+
100+
101+
function _labelbox(f, io; width="0in")
102+
try
103+
if parse(Float64, string(match(r"[\d.]+", width).match)) == 0.0
104+
# do not use a makebox if width in zero
105+
print(io, "{")
106+
f()
107+
print(io, "} ")
108+
return
109+
end
110+
catch
111+
throw(ArgumentError("width $(repr(width)) must be a valid LaTeX width"))
112+
end
113+
print(
114+
io,
115+
"\\makebox[{\\ifdim$(width)<\\dimexpr\\width+1ex\\relax\\dimexpr\\width+1ex\\relax\\else$(width)\\fi}][l]{"
116+
)
117+
f()
118+
print(io, "}")
119+
end
120+
121+
122+
function Documenter.LaTeXWriter.latex(
123+
lctx::Documenter.LaTeXWriter.Context,
124+
node::MarkdownAST.Node,
125+
bibliography::BibliographyNode
126+
)
127+
128+
if bibliography.list_style == :ol
129+
texenv = "enumerate"
130+
elseif bibliography.list_style == :ul
131+
if _LATEX_OPTIONS[:ul_as_hanging]
132+
texenv = nothing
133+
else
134+
texenv = "itemize"
135+
end
136+
else
137+
@assert bibliography.list_style == :dl
138+
# We emulate a definition list manually with hangindent and labelwidth
139+
texenv = nothing
140+
end
141+
142+
io = lctx.io
143+
144+
function tex_item(n, item)
145+
if bibliography.list_style == :ul
146+
if _LATEX_OPTIONS[:ul_as_hanging]
147+
print(io, "\\hangindent=$(_LATEX_OPTIONS[:ul_hangindent]) ")
148+
else
149+
print(io, "\\item ")
150+
end
151+
elseif bibliography.list_style == :ol # enumerate
152+
print(io, "\\item ")
153+
else
154+
@assert bibliography.list_style == :dl
155+
print(io, "\\hangindent=$(_LATEX_OPTIONS[:dl_hangindent]) {")
156+
_labelbox(io; width=_LATEX_OPTIONS[:dl_labelwidth]) do
157+
Documenter.LaTeXWriter.latex(lctx, item.label.children)
158+
end
159+
print(io, "}")
160+
end
161+
end
162+
163+
println(io, "{$(_LATEX_OPTIONS[:bib_blockformat])% @bibliography\n")
164+
_wrapblock(io, texenv) do
165+
for (n, item) in enumerate(bibliography.items)
166+
tex_item(n, item)
167+
if !isnothing(item.anchor_key)
168+
id = _hash(item.anchor_key)
169+
print(io, "\\hypertarget{", id, "}{}")
170+
end
171+
Documenter.LaTeXWriter.latex(lctx, item.reference.children)
172+
print(io, "\n\n")
173+
end
174+
end
175+
println(io, "}% end @bibliography")
176+
177+
end

0 commit comments

Comments
 (0)