Skip to content

Commit 71ed822

Browse files
pergamon:0.4.0 (#3234)
1 parent 0fbb79e commit 71ed822

File tree

13 files changed

+3870
-0
lines changed

13 files changed

+3870
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Alexander Koller
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Pergamon: BibLaTeX-style bibliographies for Typst
2+
3+
Pergamon is a package for typesetting bibliographies in Typst.
4+
It is inspired by [BibLaTeX](https://ctan.org/pkg/biblatex), in that
5+
the way in which it typesets bibliographies can be easily customized
6+
through Typst code. Like Typst's regular bibliography management model,
7+
Pergamon can be configured to use different styles for typesetting
8+
references and citations; unlike it, these styles are all defined through
9+
Typst code, rather than CSL.
10+
11+
Pergamon is documented in the [user guide](https://github.com/alexanderkoller/pergamon/blob/main/docs/pergamon-0.4.0.pdf).
12+
See a somewhat complex example: [Typst](https://github.com/alexanderkoller/pergamon/blob/main/example.typ), [PDF](https://github.com/alexanderkoller/pergamon/blob/main/example.pdf).
13+
14+
Pergamon has a number of advantages over the builtin Typst bibliographies:
15+
16+
- Pergamon styles are simply pieces of Typst code and can be easily configured or modified.
17+
- The document can be easily split into different `refsection`s, each of which can have its own bibliography
18+
(similar to [Alexandria](https://typst.app/universe/package/alexandria/)).
19+
- Paper titles can be automatically made into hyperlinks - as in [blinky](https://typst.app/universe/package/blinky/), but much more flexibly and correctly.
20+
- Bibliographies can be filtered, and bibliography entries programmatically highlighted, which is useful e.g. for CVs.
21+
- References retain nonstandard Bibtex fields ([unlike in Hayagriva](https://github.com/typst/hayagriva/issues/240)),
22+
making it e.g. possible to split bibliographies based on keywords.
23+
24+
At the same time, Pergamon is very new and has a number of important limitations compared to
25+
the builtin system. I have implemented those parts of Pergamon that I need for my own writing,
26+
but I would welcome your pull request to make it more feature-complete.
27+
28+
- Pergamon currently supports only bibliographies in Bibtex format, not the Hayagriva YAML format.
29+
- Only a handful of styles are supported at this point, in contrast to the large number of available CSL styles. Pergamon comes with implementations of the BibLaTeX styles `numeric`, `alphabetic`, and `authoryear`.
30+
- Pergamon still requires a lot of testing and tweaking.
31+
32+
[Pergamon](https://en.wikipedia.org/wiki/Pergamon) was an ancient Greek city state in Asia Minor.
33+
Its library was second only to the Library of Alexandria around 200 BC.
34+
35+
36+
37+
## Example
38+
39+
The following piece of code typesets a bibliography using Pergamon.
40+
41+
```typ
42+
#import "@preview/pergamon:0.4.0": *
43+
44+
#let style = format-citation-numeric()
45+
46+
#add-bib-resource(read("bibliography.bib"))
47+
48+
#refsection(format-citation: style.format-citation)[
49+
... some text here ...
50+
#cite("bender20:_climb_nlu")
51+
52+
#print-bibliography(
53+
format-reference: format-reference(reference-label: style.reference-label),
54+
label-generator: style.label-generator)
55+
]
56+
```
57+
58+
It generates citations and a bibliography that look like this:
59+
60+
<img src="https://github.com/alexanderkoller/pergamon/blob/main/docs/materials/example-output.png" style='border:1px solid #000000' />
61+
62+
You can try out [a more complex example](https://github.com/alexanderkoller/pergamon/blob/main/example.typ) yourself;
63+
here's [the PDF it generates](https://github.com/alexanderkoller/pergamon/blob/main/example.pdf).
64+
65+
## Documentation
66+
67+
Please see the [Pergamon guide](https://github.com/alexanderkoller/pergamon/blob/main/docs/pergamon-0.4.0.pdf) for more details.
68+
69+
## Contributors
70+
71+
- [@ironupiwada](https://www.github.com/ironupiwada) contributed code for date parsing in 0.2.0.
72+
73+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
#import "src/bibtypst.typ": add-bib-resource, refsection, print-bibliography, if-citation, cite, citet, citep, citen, citeg, citename, citeyear, count-bib-entries
3+
#import "src/bibtypst-styles.typ": format-citation-authoryear, format-citation-alphabetic, format-citation-numeric, format-reference
4+
#import "src/names.typ": family-names, format-name
5+
#import "src/bib-util.typ": fd, ifdef, nn, concatenate-names
6+
#import "src/bibstrings.typ": default-bibstring
7+
#import "src/content-to-string.typ": content-to-string
8+
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
2+
#import "bibstrings.typ": default-bibstring
3+
4+
#let matches-completely(s, re) = {
5+
let result = s.match(re)
6+
7+
if result == none {
8+
return false
9+
} else {
10+
// [#result]
11+
result.start == 0 and result.end == s.len()
12+
}
13+
}
14+
15+
16+
// Checks whether a string can be converted into an int
17+
#let is-integer(s) = {
18+
// s = s.trim()
19+
20+
// TODO: allow negative numbers at some point
21+
matches-completely(s.trim(), regex("\d+"))
22+
}
23+
24+
25+
/// Concatenates an array of names. If there is only one name, it is returned
26+
/// unmodified. If there are two names, they are concatenated with the
27+
/// value `options.list-end-delim-two` ("and"). If there are three names, they
28+
/// are concatenated with the value `options.list-end-delim-two` (", "), except
29+
/// the last name is joined with `options.list-end-delim-many` (", and").
30+
///
31+
/// -> str | content
32+
#let concatenate-names(
33+
/// An array of names. Each name can a string or content. If the names are strings,
34+
/// the function will return a string; if at least one name is content, the
35+
/// function will return content.
36+
///
37+
/// -> array
38+
names,
39+
40+
/// Options that control the concatenation. `concatenate-names` defines reasonable
41+
/// default options for `list-end-delim-two`,
42+
/// `list-end-delim-two`, `list-end-delim-many`, and `bibstring`.
43+
/// You can override these options by passing them in a dictionary here.
44+
///
45+
/// -> dictionary
46+
options: (:),
47+
48+
/// Maximum number of names that is displayed before the name list is truncated
49+
/// with "et al." See the `maxnames` parameter in @format-reference for details.
50+
maxnames: 2,
51+
52+
/// Minimum number of names that is guaranteed to be displayed. See the `minnames`
53+
/// parameter in @format-reference for details.
54+
minnames: 1
55+
) = {
56+
let etal = names.len() > maxnames and names.len() > minnames // print "et al.", at least one name dropped
57+
let num-names = if etal { calc.min(minnames, names.len()) } else { names.len() } // #names that will be printed
58+
let options = (list-end-delim-two: " and ", list-middle-delim: ", ", list-end-delim-many: ", and ", bibstring: default-bibstring) + options
59+
60+
if etal {
61+
let nn = names.slice(0, num-names).join(options.list-middle-delim)
62+
nn + " " + options.bibstring.andothers
63+
} else {
64+
if names.len() == 1 {
65+
names.at(0)
66+
} else if names.len() == 2 {
67+
names.at(0) + options.list-end-delim-two + names.at(1)
68+
} else {
69+
names.join(options.list-middle-delim, last: options.list-end-delim-many)
70+
}
71+
}
72+
}
73+
74+
75+
// Map "modern" Biblatex field names to legacy field names as they
76+
// might appear in the bib file. Should be complete, as per biblatex.def
77+
#let field-aliases = (
78+
"journaltitle": "journal",
79+
"langid": "hyphenation",
80+
"location": "address",
81+
"institution": "school",
82+
"annotation": "annote",
83+
"eprinttype": "archiveprefix",
84+
"eprintclass": "primaryclass",
85+
"sortkey": "key",
86+
"file": "pdf"
87+
)
88+
89+
90+
// Map legacy Bibtex entry types to their "modern" Biblatex names.
91+
#let type-aliases = (
92+
"conference": reference => { reference.insert("entry_type", "inproceedings"); return reference },
93+
"electronic": reference => { reference.insert("entry_type", "online"); return reference },
94+
"www": reference => { reference.insert("entry_type", "online"); return reference },
95+
"mastersthesis": reference => {
96+
reference.insert("entry_type", "thesis")
97+
if not "type" in reference.fields {
98+
reference.fields.insert("type", "mathesis")
99+
}
100+
return reference
101+
},
102+
"phdthesis": reference => {
103+
reference.insert("entry_type", "thesis")
104+
if not "type" in reference.fields {
105+
reference.fields.insert("type", "phdthesis")
106+
}
107+
return reference
108+
},
109+
"techreport": reference => {
110+
reference.insert("entry_type", "report")
111+
reference.fields.insert("type", "techreport")
112+
return reference
113+
},
114+
)
115+
116+
117+
118+
#let fd(reference, field, options, format: x => x) = {
119+
let legacy-field = field-aliases.at(field, default: "dummy-field-name")
120+
121+
if field in options.at("suppressed-fields", default: ()) {
122+
return none
123+
} else if field in reference.fields {
124+
return format(reference.fields.at(field))
125+
} else if legacy-field in reference.fields {
126+
return format(reference.fields.at(legacy-field))
127+
} else {
128+
return none
129+
}
130+
}
131+
132+
133+
#let ifdef(reference, field, options, fn) = {
134+
let value = fd(reference, field, options)
135+
136+
if value == none { none } else { fn(value) }
137+
}
138+
139+
// Convert an array of (key, value) pairs into a "multimap":
140+
// a dictionary in which each key is assigned to an array of all
141+
// the values with which it appeared.
142+
//
143+
// Example: (("a", 1), ("a", 2), ("b", 3)) -> (a: (1, 2), b: (3,))
144+
#let collect-deduplicate(pairs) = {
145+
let ret = (:)
146+
147+
for (key, value) in pairs {
148+
if key in ret {
149+
ret.at(key).push(value)
150+
} else {
151+
ret.insert(key, (value,))
152+
}
153+
}
154+
155+
return ret
156+
}
157+
158+
159+
/// Wraps a function in `none`-handling code.
160+
/// `nn(func)` is a function that
161+
/// behaves like `func` on arguments that are not `none`,
162+
/// but if the argument is `none`, it simply returns `none`.
163+
/// Only works for functions `func` that have a single argument.
164+
/// -> function
165+
#let nn(func) = {
166+
it => if it == none { none } else { func(it) }
167+
}
168+
169+
170+

0 commit comments

Comments
 (0)