Skip to content

Commit cbb3d9d

Browse files
goerzSeelengrab
andcommitted
Add support for citations in PDFs/LaTeX
Co-authored-by: Sukera <[email protected]>
1 parent 07d99f0 commit cbb3d9d

19 files changed

+844
-108
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{Michael 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: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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+
function BibliographyNode(list_style, canonical, items)
37+
if list_style in (:dl, :ul, :ol)
38+
new(list_style, canonical, items)
39+
else
40+
throw(
41+
ArgumentError(
42+
"`list_style` must be one of `:dl`, `:ul`, or `:ol`, not `$(repr(list_style))`"
43+
)
44+
)
45+
end
46+
end
47+
end
48+
49+
50+
function Documenter.MDFlatten.mdflatten(io, ::MarkdownAST.Node, b::BibliographyNode)
51+
for item in b.items
52+
Documenter.MDFlatten.mdflatten(io, item.reference)
53+
print(io, "\n\n\n\n")
54+
end
55+
end
56+
57+
58+
function Documenter.HTMLWriter.domify(
59+
dctx::Documenter.HTMLWriter.DCtx,
60+
node::Documenter.Node,
61+
bibliography::BibliographyNode
62+
)
63+
@assert node.element === bibliography
64+
return domify_bib(dctx, bibliography)
65+
end
66+
67+
68+
function domify_bib(dctx::Documenter.HTMLWriter.DCtx, bibliography::BibliographyNode)
69+
Documenter.DOM.@tags dl ul ol li div dt dd
70+
list_tag = dl
71+
if bibliography.list_style == :ul
72+
list_tag = ul
73+
elseif bibliography.list_style == :ol
74+
list_tag = ol
75+
end
76+
html_list = list_tag()
77+
for item in bibliography.items
78+
anchor_id = isnothing(item.anchor_key) ? "" : "#$(item.anchor_key)"
79+
html_reference = Documenter.HTMLWriter.domify(dctx, item.reference.children)
80+
if bibliography.list_style == :dl
81+
html_label = Documenter.HTMLWriter.domify(dctx, item.label.children)
82+
push!(html_list.nodes, dt(html_label))
83+
push!(html_list.nodes, dd(div[anchor_id](html_reference)))
84+
else
85+
push!(html_list.nodes, li(div[anchor_id](html_reference)))
86+
end
87+
end
88+
class = ".citation"
89+
if bibliography.canonical
90+
class *= ".canonical"
91+
else
92+
class *= ".noncanonical"
93+
end
94+
return div[class](html_list)
95+
end
96+
97+
98+
_hash(x) = string(hash(x))
99+
100+
101+
function _wrapblock(f, io, env)
102+
if !isnothing(env)
103+
println(io, "\\begin{", env, "}")
104+
end
105+
f()
106+
if !isnothing(env)
107+
println(io, "\\end{", env, "}")
108+
end
109+
end
110+
111+
112+
function _labelbox(f, io; width="0in")
113+
local width_val
114+
try
115+
width_val = parse(Float64, string(match(r"[\d.]+", width).match))
116+
catch
117+
throw(ArgumentError("width $(repr(width)) must be a valid LaTeX width"))
118+
end
119+
@show width_val
120+
if width_val == 0.0
121+
# do not use a makebox if width is zero
122+
print(io, "{")
123+
f()
124+
print(io, "} ")
125+
return
126+
end
127+
print(
128+
io,
129+
"\\makebox[{\\ifdim$(width)<\\dimexpr\\width+1ex\\relax\\dimexpr\\width+1ex\\relax\\else$(width)\\fi}][l]{"
130+
)
131+
f()
132+
print(io, "}")
133+
end
134+
135+
136+
function Documenter.LaTeXWriter.latex(
137+
lctx::Documenter.LaTeXWriter.Context,
138+
node::MarkdownAST.Node,
139+
bibliography::BibliographyNode
140+
)
141+
142+
if bibliography.list_style == :ol
143+
texenv = "enumerate"
144+
elseif bibliography.list_style == :ul
145+
if _LATEX_OPTIONS[:ul_as_hanging]
146+
texenv = nothing
147+
else
148+
texenv = "itemize"
149+
end
150+
else
151+
@assert bibliography.list_style == :dl
152+
# We emulate a definition list manually with hangindent and labelwidth
153+
texenv = nothing
154+
end
155+
156+
io = lctx.io
157+
158+
function tex_item(n, item)
159+
if bibliography.list_style == :ul
160+
if _LATEX_OPTIONS[:ul_as_hanging]
161+
print(io, "\\hangindent=$(_LATEX_OPTIONS[:ul_hangindent]) ")
162+
else
163+
print(io, "\\item ")
164+
end
165+
elseif bibliography.list_style == :ol # enumerate
166+
print(io, "\\item ")
167+
else
168+
@assert bibliography.list_style == :dl
169+
print(io, "\\hangindent=$(_LATEX_OPTIONS[:dl_hangindent]) {")
170+
_labelbox(io; width=_LATEX_OPTIONS[:dl_labelwidth]) do
171+
Documenter.LaTeXWriter.latex(lctx, item.label.children)
172+
end
173+
print(io, "}")
174+
end
175+
end
176+
177+
println(io, "{$(_LATEX_OPTIONS[:bib_blockformat])% @bibliography\n")
178+
_wrapblock(io, texenv) do
179+
for (n, item) in enumerate(bibliography.items)
180+
tex_item(n, item)
181+
if !isnothing(item.anchor_key)
182+
id = _hash(item.anchor_key)
183+
print(io, "\\hypertarget{", id, "}{}")
184+
end
185+
Documenter.LaTeXWriter.latex(lctx, item.reference.children)
186+
print(io, "\n\n")
187+
end
188+
end
189+
println(io, "}% end @bibliography")
190+
191+
end

0 commit comments

Comments
 (0)