Compare commits

..
Author SHA1 Message Date
swaits d30225f4e6 feat(glossy): 0.1.2 2024-12-10 20:44:44 +00:00
swaits 347d44b5da feat(glossy): 0.1.1 2024-12-10 20:44:16 +00:00
swaits 927c52d402 feat(glossy): 0.1.0 2024-12-10 20:37:43 +00:00
102 changed files with 0 additions and 5247 deletions

No files matched your search

-198
View File
@@ -1,198 +0,0 @@
# Glossy
This package provides utilities to manage and render glossaries within
documents. It includes functions to define and use glossary terms, track their
usage, and generate a glossary list with references to where terms are used in
the document.
![image of sample output](thumbnail.png)
## Motivation
Glossy is heavily inspired by
[glossarium](https://typst.app/universe/package/glossarium), with a few key
different goals:
1. Provide a simple interface which allows for complete control over glossary
display. To do this, `glossy`'s `#glossary()` function accepts a theme
parameter. The goal here is to separate presentation and logic.
1. Simplify the user interface as much as possible. Glossy has exactly two
exports, `init-glossary` and `glossary`.
1. Double-down on `glossy`'s excellent `@term` reference approach, completely
eliminating the need to make any calls to `gls()` and friends.
1. Mimic established patterns and best practices. For example, `glossy`'s
`#glossary()` function is intentionally similar (in naming and parameters) to
`typst`'s built-int `#bibliography`, to the degree possible.
1. Simplify the implementation. The `glossy` code is significantly shorter and
easier to understand.
## Features
- Define glossary terms with short and long forms, descriptions, and grouping.
- Automatically tracks term usage in the document, making references simple
through @labels.
- Supports modifiers to adjust how terms are displayed (capitalize, pluralize,
etc.).
- Generates a formatted glossary section with backlinks to term occurrences in
the document.
- Easily customizable themes for rendering glossary sections, groups, and
entries.
## Usage
### Import the package
```typst
#import "@preview/glossy:0.1.0": *
```
### Defining Glossary Terms
Use the `init-glossary` function to initialize glossary entries and process
document content:
```typst
#let myGlossary = (
(
key: "html",
short: "HTML",
long: "Hypertext Markup Language",
description: "A standard language for creating web pages",
group: "Web"
),
(
key: "css",
short: "CSS",
long: "Cascading Style Sheets",
description: "A stylesheet language used for describing the presentation of a document",
group: "Web"
),
(
key: "tps",
short: "TPS",
long: "test procedure specification"
),
// Add more entries as needed
)
#show: init-glossary.with(myGlossary)
// Your document content here, where glossary terms are used.
```
Note that you could also load glossary entries from a data file using
[#yaml()](https://typst.app/docs/reference/data-loading/yaml/),
[#json()](https://typst.app/docs/reference/data-loading/json/), or
[similar](https://typst.app/docs/reference/data-loading/).
### Using Glossary Terms in Your Document
Use glossary terms within your text using the `@reference` functionality in
typst.
For example, `@html` and `@css` are used in the following text:
```typst
In modern web development, languages like @html and @css are essential.
Now make sure I get your @tps:short reports by 2pm!
```
... which should output:
```text
In modern web development, languages like Hypertext Markup Language (HTML) and
Cascading Style Sheets (CSS) are essential.
Now make sure I get your TPS reports by 2pm!
```
You can control how each term is displayed using the following modifiers:
- **cap**: Capitalizes the term.
- **pl**: Uses the plural form of the term.
- **both**: Displays both the long and short forms (e.g., "Long Form (Short Form)").
- **short**: Displays only the short form.
- **long**: Displays only the long form.
Each modifier is separated by a colon (':') character. Examples:
| **Input** | **Output** |
| ----------------------- | -------------------------------------------- |
| `@tps` (first use) | "technical procedure specification (TPS)" |
| `@tps` (subsequent use) | "TPS" |
| `@tps:short` | "TPS" |
| `@tps:long` | "technical procedure specification (TPS)" |
| `@tps:both` | "technical procedure specification (TPS)" |
| `@tps:long:cap` | "Technical procedure specification" |
| `@tps:long:pl` | "technical procedure specifications" |
| `@tps:short:pl` | "TPSes" |
| `@tps:both:pl:cap` | "Technical procedure specifications (TPSes)" |
Combine modifiers in any way you like. But know that if you combine any of
`short`, `long`, and/or `both`, only one will be used.
### Generating the Glossary
To display the glossary in your document, use the `glossary` function. Customize
the title and specify a theme if desired:
```typst
#glossary(title: "Web Development Glossary", theme: my-theme, groups: ("Web"))
```
### Themes
Customize the appearance of the glossary using themes. Each theme defines how
sections, groups, and entries are displayed.
- **section**: This is a main glossary section. It contains multiple glossary groups.
- **group**: A group within the main glossary section. Contains multiple
entries. Can be `none`.
- **entry**: A single entry within a group. This is the actual term, and can
include any of the fields in entry. Contains the following fields:
- `key`: The term key. (always present and unique)
- `short`: The short form of the term. (always present)
- `long`: The long form. (can be `none`)
- `description`: The description or definition. (can be `none`)
- `pages`: Pages with links to term usage in doc. (always present)
Example of a minimal theme:
```typst
#let my-theme = (
section: (title, body) => {
heading(level: 1, title)
body
},
group: (name, body) => {
if name != none and name != "" {
heading(level: 2, name)
}
body
},
entry: (entry, i, n) => {
// i is the entry's index, n is total number of entries
let output = [#entry.short]
if entry.long != none {
output = [#output -- #entry.long]
}
if entry.description != none {
output = [#output: #entry.description]
}
block(
grid(
columns: (auto, 1fr, auto),
output,
repeat([#h(0.25em) . #h(0.25em)]),
entry.pages,
)
)
}
)
```
## License
This project is licensed under the MIT License.
Binary file not shown.
-63
View File
@@ -1,63 +0,0 @@
// a minimal example, also listed in the README
#import "@preview/glossy:0.1.0": *
#let myGlossary = (
(key: "html", short: "HTML", long: "Hypertext Markup Language", description: "A standard language for creating web pages", group: "Web"),
(key: "css", short: "CSS", long: "Cascading Style Sheets", description: "A language used for describing the presentation of a document", group: "Web"),
(key: "tps", short: "TPS", long: "test procedure specification"),
// Add more entries as needed
)
#show: init-glossary.with(myGlossary)
In modern web development, languages like @html and @css are essential.
Now make sure I get your @tps:short reports by 2pm!
#table(
columns: 2,
table.header([*Input*], [*Output*]),
[`@tps:short` ], [@tps:short],
[`@tps:long` ], [@tps:long],
[`@tps:both` ], [@tps:both],
[`@tps:long:cap` ], [@tps:long:cap],
[`@tps:long:pl` ], [@tps:long:pl],
[`@tps:short:pl` ], [@tps:short:pl],
[`@tps:both:pl:cap`], [@tps:both:pl:cap],
)
#let my-theme = (
section: (title, body) => {
heading(level: 1, title)
body
},
group: (name, body) => {
if name != none and name != "" {
heading(level: 2, name)
}
body
},
entry: (entry, i, n) => {
// i is the entry's index, n is total number of entries
let output = [#entry.short]
if entry.long != none {
output = [#output -- #entry.long]
}
if entry.description != none {
output = [#output: #entry.description]
}
block(
grid(
columns: (auto, 1fr, auto),
output,
repeat([#h(0.25em) . #h(0.25em)]),
entry.pages,
)
)
}
)
#glossary(theme: my-theme)
-320
View File
@@ -1,320 +0,0 @@
#import "./themes.typ": *
#import "./utils.typ": *
#let __gloss_entries = state("__gloss_entries", (:))
#let __gloss_used = state("__gloss_used", (:))
#let __gloss_label_prefix = "__gloss:"
// given an array of dictionaries, make sure each has all the keys we'll
// reference, using default values if needed
#let __normalize_entries(entry-list) = {
// TODO: panic if key or short missing, all others optional
let new-list = ()
for entry in entry-list {
let long = entry.at("long", default: none)
let longplural = entry.at("longplural", default: none)
if long != none and longplural == none {
longplural = __pluralize(long)
}
new-list.push((
key: entry.key,
short: entry.short,
plural: entry.at("plural", default: __pluralize(entry.short)),
long: long,
longplural: longplural,
description: entry.at("description", default: none),
group: entry.at("group", default: ""),
))
}
return new-list
}
// update our state with a glossary entry
#let __add_entry(entry) = {
// make sure our final glossary state does not already have this key
if __gloss_entries.final().at(entry.key, default: false) == true {
panic("Glossary error. Duplicate key: " + entry.key)
}
// add it to the state
__gloss_entries.update(st => {
st.insert(entry.key, entry)
return st
})
}
// fetch a glossary entry from our state, or panic
#let __get_entry(key) = {
let entries = __gloss_entries.final()
if key not in entries {
panic("Glossary error. Missing key: " + key)
}
entries.at(key)
}
// returns true if an entry with `key` is in our glossary state
#let __has_entry(key) = {
let entries = __gloss_entries.final()
key in entries
}
// helper to prefix a key for label/reference scoping for the dictionary entry
#let __dict_label_str(key) = {
__gloss_label_prefix + key
}
// helper to get a label with prefix for the dictionary entry
#let __dict_label(key) = {
label(__dict_label_str(key))
}
// helper to prefix a key for label/reference scoping for the term use in a doc
#let __term_label_str(key, index) = {
__gloss_label_prefix + key + "." + str(index)
}
// helper to get a label with prefix for the term use in a doc
#let __term_label(key, index) = {
label(__term_label_str(key, index))
}
// update the usage count for a given key
#let __save_term_usage(key, count) = {
__gloss_used.update(st => {
st.insert(key, count)
return st;
})
}
// the main function which emits terms used in a document
//
// handles all the modifiers:
//
// - cap: capitalize the term
// - pl: pluralize the term
// - both: emit "Long form (short form)"
// - short: emit just the short form
// - long: emit just the long form
#let __gls(
key,
modifiers: array
) = {
// Get the term
let entry = __get_entry(key)
let entry_label = __dict_label_str(key)
// Lookup and increment the count
let entry_counter = counter(entry_label)
entry_counter.step()
// See if this is the first use
let key_index = entry_counter.get().first()
let first = key_index == 0
// Count the entry as used so we can link back from glossary
__save_term_usage(entry.key, key_index + 1)
// Helper: Apply pluralization if needed
let pluralize_term = (singular, plural) => {
if "pl" in modifiers and plural != none { plural } else { singular }
}
// Helper: Capitalize term if "cap" modifier present
let capitalize_term = (term) => {
if "cap" in modifiers { upper(term.first()) + term.slice(1) } else { term }
}
// Helper: Select and format the displayed term (long, short, or both)
let select_term = (is_long_mode, use_both) => {
// Derive pluralized and capitalized versions of the long and short forms
let long_form = capitalize_term(pluralize_term(entry.long, entry.longplural))
let short_form = capitalize_term(pluralize_term(entry.short, entry.plural))
// If "both" modifier is present, show both the long form and short form
if use_both {
// If the long form exists, show "Long Form (Short Form)"
if long_form != none {
[#long_form (#short_form)]
}
// Fallback: If no long form exists, just show the short form
else {
[#short_form]
}
}
// If in "long mode", show the long form, fallback to short if long form is missing
else if is_long_mode {
if long_form != none {
[#long_form]
}
// Fallback: if no long form, show short
else {
[#short_form]
}
}
// Default case: Just show the short form
else {
[#short_form]
}
}
// Determine which form to display: "short", "long", or "both"
let is_both = "both" in modifiers
let is_long = "long" in modifiers and not is_both
let is_short = "short" in modifiers and not is_both and not is_long
context {
// Final display logic
let display = if is_both or is_long or is_short {
// User requested specific behavior via modifiers
select_term(is_long, is_both)
} else {
// Default behavior: show "both" on first use, else "short"
select_term(false, first)
}
// TODO: figure out how to link to __dict_label(key) if the glossary exists
// NOTE: this is a low priority, I don't think it's that important or useful.
// Emit with labels for this instance of the term usage
[#display#metadata(display)#__term_label(key, key_index)]
}
}
// Create all the backlinks to term uses in a doc, in the form of page numbers
// linked. Used when emitting a glossary, so its page numbers can link back to
// the uses.
#let __create_backlinks(key, count) = {
// create array of labels to link to
let labels = for i in range(count) {
(__term_label(key, i),)
}
// create arrays of locations, and page number display text
let pages = labels
.map(l => { locate(l) })
.map((loc) => { numbering(__default(loc.page-numbering(), "1"), loc.page()) })
// convert labels to links, filtering out duplicated pages
let seen = ()
let links = for i in range(labels.len()) {
let l = labels.at(i)
let p = pages.at(i)
if seen.contains(p) {
(none,)
} else {
seen.push(p)
(link(l, p),)
}
}.filter(l => l != none)
// connect links with commas and return
links.join(", ")
}
// Main wrapper (usually used in a `#show: init-glossary`) which loads the
// entries passed in into our state. Furthermore, hooks into references so that
// we can intercept term usage in a doc and label them appropriately.
#let init-glossary(entries, body) = context {
for entry in __normalize_entries(entries) {
__add_entry(entry)
}
// convert refs we recognize into links with labels
show ref: r => {
let (key, ..modifiers) = str(r.target).split(":")
if __has_entry(key) {
__gls(key, modifiers: modifiers)
} else {
r
}
}
body
}
// Used to print a glossary. Can customize title, theme, and/or specify groups.
//
// A theme is a dictionary with three attributes:
//
// #let my-theme = (
// section: (title, body) => {
// // how to display the glossary section and its body which will contain
// // groups, each with their entries
// },
//
// group: (name, body) => {
// // how to display a group name and its body (which will contain the
// // entries in that group)
// },
//
// entry: (entry, i, n) => {
// // how to display a single entry, along with its index and total count
// }
// }
#let glossary(title: "Glossary", theme: theme-2col, groups: ()) = context {
// our output is a map of group name to array of entries (each entry is a map)
let output = (:)
// pull in entire dictionary
let all_entries = __gloss_entries.final()
// filter down to just what we used
let all_used = __gloss_used.final()
// TODO: what about entries with no group? Or group == "" or == none
// get all groups
let all_groups = all_entries.values().map(e => e.at("group")).dedup().sorted()
let groups = if groups.len() == 0 { all_groups } else { groups }
// make sure requested groups are legit
for g in groups {
if g not in all_groups {
panic("Requested group not found: " + g)
}
}
// iterate one group at a time
for g in groups {
// collect used entries in this group
let cur = ()
for (key, count) in all_used {
let e = all_entries.at(key)
if e.at("group") == g {
// this term is both in this group and was used, add to our output
let short = e.at("short")
let long = e.at("long")
let description = e.at("description")
let label = [#metadata(key)#__dict_label(key)]
let pages = __create_backlinks(key, count)
cur.push((short: short, long: long, description: description, label: label, pages: pages))
}
}
if cur.len() > 0 {
if g == "" { g = none }
output.insert(g, cur.sorted(key: e => e.short))
}
}
// TODO: rendering for a) just one group, or b) the default group (ie terms
// with no group)?? -- in this case should we just not use the theme.group()
// function?
// render it using our theme
(theme.section)(
title,
for (group, entries) in output {
(theme.group)(
group,
for (i,e) in entries.enumerate() {
(theme.entry)(e, i, entries.len())
},
)
}
)
}
-47
View File
@@ -1,47 +0,0 @@
#let theme-2col = (
section: (title, body) => {
set par.line(numbering: none)
heading(level: 1, title)
columns(2, body)
},
group: (name, body) => {
if name != none {
heading(level: 2, name)
}
body
},
entry: (entry, i, n) => {
// build our title, etc
let short-display = text(weight: "regular", entry.short)
let long-display = if entry.long == none {
[]
} else {
[#h(0.25em) -- #entry.long]
}
// build our description
let description = if entry.description == none {
[]
} else {
[: #entry.description]
}
text(
size: 0.75em,
weight: "light",
grid(
columns: (auto,auto,auto,1fr,1em,auto),
align: (left, left, left, center, center, right),
[#short-display#entry.label],
[#long-display],
[#description],
[#repeat(h(0.25em) + "." + h(0.25em))],
[ . ],
[#entry.pages]
)
)
},
)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

-199
View File
@@ -1,199 +0,0 @@
# Glossy
This package provides utilities to manage and render glossaries within
documents. It includes functions to define and use glossary terms, track their
usage, and generate a glossary list with references to where terms are used in
the document.
![image of sample output](thumbnail.png)
## Motivation
Glossy is heavily inspired by
[glossarium](https://typst.app/universe/package/glossarium), with a few key
different goals:
1. Provide a simple interface which allows for complete control over glossary
display. To do this, `glossy`'s `#glossary()` function accepts a theme
parameter. The goal here is to separate presentation and logic.
1. Simplify the user interface as much as possible. Glossy has exactly two
exports, `init-glossary` and `glossary`.
1. Double-down on `glossy`'s excellent `@term` reference approach, completely
eliminating the need to make any calls to `gls()` and friends.
1. Mimic established patterns and best practices. For example, `glossy`'s
`#glossary()` function is intentionally similar (in naming and parameters) to
`typst`'s built-int `#bibliography`, to the degree possible.
1. Simplify the implementation. The `glossy` code is significantly shorter and
easier to understand.
## Features
- Define glossary terms with short and long forms, descriptions, and grouping.
- Automatically tracks term usage in the document, making references simple
through @labels.
- Supports modifiers to adjust how terms are displayed (capitalize, pluralize,
etc.).
- Generates a formatted glossary section with backlinks to term occurrences in
the document.
- Easily customizable themes for rendering glossary sections, groups, and
entries.
## Usage
### Import the package
```typst
#import "@preview/glossy:0.1.1": *
```
### Defining Glossary Terms
Use the `init-glossary` function to initialize glossary entries and process
document content:
```typst
#let myGlossary = (
(
key: "html",
short: "HTML",
long: "Hypertext Markup Language",
description: "A standard language for creating web pages",
group: "Web"
),
(
key: "css",
short: "CSS",
long: "Cascading Style Sheets",
description: "A stylesheet language used for describing the presentation of a document",
group: "Web"
),
(
key: "tps",
short: "TPS",
long: "test procedure specification"
),
// Add more entries as needed
)
#show: init-glossary.with(myGlossary)
// Your document content here, where glossary terms are used.
```
Note that you could also load glossary entries from a data file using
[#yaml()](https://typst.app/docs/reference/data-loading/yaml/),
[#json()](https://typst.app/docs/reference/data-loading/json/), or
[similar](https://typst.app/docs/reference/data-loading/).
### Using Glossary Terms in Your Document
Use glossary terms within your text using the `@reference` functionality in
typst.
For example, `@html` and `@css` are used in the following text:
```typst
In modern web development, languages like @html and @css are essential.
Now make sure I get your @tps:short reports by 2pm!
```
... which should output:
```text
In modern web development, languages like Hypertext Markup Language (HTML) and
Cascading Style Sheets (CSS) are essential.
Now make sure I get your TPS reports by 2pm!
```
You can control how each term is displayed using the following modifiers:
- **cap**: Capitalizes the term.
- **pl**: Uses the plural form of the term.
- **both**: Displays both the long and short forms (e.g., "Long Form (Short Form)").
- **short**: Displays only the short form.
- **long**: Displays only the long form.
Each modifier is separated by a colon (':') character. Examples:
| **Input** | **Output** |
| ----------------------- | -------------------------------------------- |
| `@tps` (first use) | "technical procedure specification (TPS)" |
| `@tps` (subsequent use) | "TPS" |
| `@tps:short` | "TPS" |
| `@tps:long` | "technical procedure specification (TPS)" |
| `@tps:both` | "technical procedure specification (TPS)" |
| `@tps:long:cap` | "Technical procedure specification" |
| `@tps:long:pl` | "technical procedure specifications" |
| `@tps:short:pl` | "TPSes" |
| `@tps:both:pl:cap` | "Technical procedure specifications (TPSes)" |
Combine modifiers in any way you like. But know that if you combine any of
`short`, `long`, and/or `both`, only one will be used.
### Generating the Glossary
To display the glossary in your document, use the `glossary` function. Customize
the title and specify a theme if desired:
```typst
#glossary(title: "Web Development Glossary", theme: my-theme, groups: ("Web"))
```
### Themes
Customize the appearance of the glossary using themes. Each theme defines how
sections, groups, and entries are displayed.
- **section**: This is a main glossary section. It contains multiple glossary groups.
- **group**: A group within the main glossary section. Contains multiple
entries. Can be an empty string for terms with no group (`""`).
- **entry**: A single entry within a group. This is the actual term, and can
include any of the fields in entry. Contains the following fields:
- `key`: The term key. (always present and unique)
- `short`: The short form of the term. (always present)
- `long`: The long form. (can be `none`)
- `description`: The description or definition. (can be `none`)
- `pages`: Pages with links to term usage in doc. (always present)
Example of a minimal theme:
```typst
#let my-theme = (
section: (title, body) => {
heading(level: 1, title)
body
},
group: (name, i, n, body) => {
// i is the group's index, n is total number of groups
if name != "" and n > 1 {
heading(level: 2, name)
}
body
},
entry: (entry, i, n) => {
// i is the entry's index, n is total number of entries
let output = [#entry.short]
if entry.long != none {
output = [#output -- #entry.long]
}
if entry.description != none {
output = [#output: #entry.description]
}
block(
grid(
columns: (auto, 1fr, auto),
output,
repeat([#h(0.25em) . #h(0.25em)]),
entry.pages,
)
)
}
)
```
## License
This project is licensed under the MIT License.
Binary file not shown.
-88
View File
@@ -1,88 +0,0 @@
// a minimal example, also listed in the README
#import "@preview/glossy:0.1.1": *
#let myGlossary = (
(key: "html", short: "HTML", long: "Hypertext Markup Language", description: "A standard language for creating web pages", group: "Web"),
(key: "css", short: "CSS", long: "Cascading Style Sheets", description: "A language used for describing the presentation of a document", group: "Web"),
(key: "tps", short: "TPS", long: "test procedure specification"),
// Add more entries as needed
)
#show: init-glossary.with(myGlossary)
#set heading(numbering: "1.1")
= Hello, `glossy`!
In modern web development, languages like @html and @css are essential.
Now make sure I get your @tps:short reports by 2pm!
= Examples with modifiers
#table(
columns: 2,
table.header([*Input*], [*Output*]),
[`@tps:short` ], [@tps:short],
[`@tps:long` ], [@tps:long],
[`@tps:both` ], [@tps:both],
[`@tps:long:cap` ], [@tps:long:cap],
[`@tps:long:pl` ], [@tps:long:pl],
[`@tps:short:pl` ], [@tps:short:pl],
[`@tps:both:pl:cap`], [@tps:both:pl:cap],
)
== Using conflicting modifiers (short, long, both)
These modifiers are semantically mutually exclusive, by they can be combined
syntactically. When multiple are used, `glossy` tries to make a smart choice on
which one to display.
#table(
columns: 2,
table.header([*Input*], [*Output*]),
[`@tps:short:long` ], [@tps:short:long],
[`@tps:long:short` ], [@tps:long:short],
[`@tps:short:both` ], [@tps:short:both],
[`@tps:long:both` ], [@tps:long:both],
[`@tps:short:long:both` ], [@tps:short:long:both],
[`@tps:long:pl` ], [@tps:long:pl],
)
#let my-theme = (
section: (title, body) => {
heading(level: 1, title)
body
},
group: (name, i, n, body) => {
if name != "" and n > 1 {
heading(level: 2, name)
}
body
},
entry: (entry, i, n) => {
// i is the entry's index, n is total number of entries
let output = [#entry.short]
if entry.long != none {
output = [#output -- #entry.long]
}
if entry.description != none {
output = [#output: #entry.description]
}
block(
grid(
columns: (auto, 1fr, auto),
output,
repeat([#h(0.25em) . #h(0.25em)]),
entry.pages,
)
)
}
)
#glossary(theme: my-theme)
#glossary(title: "Glossary with just empty group", groups: ("",), theme: my-theme)
#glossary(title: "Glossary with just Web group", groups: ("Web",), theme: my-theme)
-1
View File
@@ -1 +0,0 @@
#import "src/gloss.typ": init-glossary, glossary
-325
View File
@@ -1,325 +0,0 @@
#import "./themes.typ": *
#import "./utils.typ": *
#let __gloss_entries = state("__gloss_entries", (:))
#let __gloss_used = state("__gloss_used", (:))
#let __gloss_label_prefix = "__gloss:"
// given an array of dictionaries, make sure each has all the keys we'll
// reference, using default values if needed
#let __normalize_entries(entry-list) = {
// TODO: panic if key or short missing, all others optional
let new-list = ()
for entry in entry-list {
let long = entry.at("long", default: none)
let longplural = entry.at("longplural", default: none)
if long != none and longplural == none {
longplural = __pluralize(long)
}
new-list.push((
key: entry.key,
short: entry.short,
plural: entry.at("plural", default: __pluralize(entry.short)),
long: long,
longplural: longplural,
description: entry.at("description", default: none),
group: entry.at("group", default: ""),
))
}
return new-list
}
// update our state with a glossary entry
#let __add_entry(entry) = {
// make sure our final glossary state does not already have this key
if __gloss_entries.final().at(entry.key, default: false) == true {
panic("Glossary error. Duplicate key: " + entry.key)
}
// add it to the state
__gloss_entries.update(st => {
st.insert(entry.key, entry)
return st
})
}
// fetch a glossary entry from our state, or panic
#let __get_entry(key) = {
let entries = __gloss_entries.final()
if key not in entries {
panic("Glossary error. Missing key: " + key)
}
entries.at(key)
}
// returns true if an entry with `key` is in our glossary state
#let __has_entry(key) = {
let entries = __gloss_entries.final()
key in entries
}
// helper to prefix a key for label/reference scoping for the dictionary entry
#let __dict_label_str(key) = {
__gloss_label_prefix + key
}
// helper to get a label with prefix for the dictionary entry
#let __dict_label(key) = {
label(__dict_label_str(key))
}
// helper to prefix a key for label/reference scoping for the term use in a doc
#let __term_label_str(key, index) = {
__gloss_label_prefix + key + "." + str(index)
}
// helper to get a label with prefix for the term use in a doc
#let __term_label(key, index) = {
label(__term_label_str(key, index))
}
// update the usage count for a given key
#let __save_term_usage(key, count) = {
__gloss_used.update(st => {
st.insert(key, count)
return st;
})
}
// the main function which emits terms used in a document
//
// handles all the modifiers:
//
// - cap: capitalize the term
// - pl: pluralize the term
// - both: emit "Long form (short form)"
// - short: emit just the short form
// - long: emit just the long form
#let __gls(
key,
modifiers: array
) = {
// Get the term
let entry = __get_entry(key)
let entry_label = __dict_label_str(key)
// Lookup and increment the count
let entry_counter = counter(entry_label)
entry_counter.step()
// See if this is the first use
let key_index = entry_counter.get().first()
let first = key_index == 0
// Count the entry as used so we can link back from glossary
__save_term_usage(entry.key, key_index + 1)
// Helper: Apply pluralization if needed
let pluralize_term = (singular, plural) => {
if "pl" in modifiers and plural != none { plural } else { singular }
}
// Helper: Capitalize term if "cap" modifier present
let capitalize_term = (term) => {
if "cap" in modifiers { upper(term.first()) + term.slice(1) } else { term }
}
// Helper: Format a term with pluralization and capitalization
let format_term = (term, plural) => {
capitalize_term(pluralize_term(term, plural))
}
// Helper: Select and format the displayed term (long, short, or both)
let select_term = (is_long_mode, use_both) => {
// Derive pluralized and capitalized versions of the long and short forms
let short_form = format_term(entry.short, entry.plural)
let long_form = if entry.long != none { format_term(entry.long, entry.longplural) } else { none }
// Return logic for term selection:
if use_both and long_form != none {
// 1. If "both" is requested AND we have a long form available:
// Show "Long Form (Short Form)"
[#long_form (#short_form)]
} else if is_long_mode and long_form != none {
// 2. If long mode is requested AND we have a long form available:
// Show just the long form
[#long_form]
} else {
// 3. Fallback case - use short form when:
// - "both" was requested but no long form exists
// - long mode was requested but no long form exists
// - short form was explicitly requested
// - no specific mode was requested
[#short_form]
}
}
// Determine which form to display: "short", "long", or "both"
let is_both = "both" in modifiers
let is_long = "long" in modifiers and not is_both
let is_short = "short" in modifiers and not is_both and not is_long
context {
// Final display logic
let display = if is_both or is_long or is_short {
// User requested specific behavior via modifiers
select_term(is_long, is_both)
} else {
// Default behavior: show "both" on first use, else "short"
select_term(false, first)
}
// TODO: figure out how to link to __dict_label(key) if the glossary exists
// NOTE: this is a low priority, I don't think it's that important or useful.
// Emit with labels for this instance of the term usage
[#display#metadata(display)#__term_label(key, key_index)]
}
}
// Create all the backlinks to term uses in a doc, in the form of page numbers
// linked. Used when emitting a glossary, so its page numbers can link back to
// the uses.
#let __create_backlinks(key, count) = {
// create array of labels to link to
let labels = for i in range(count) {
(__term_label(key, i),)
}
// create arrays of locations, and page number display text
let pages = labels
.map(l => { locate(l) })
.map((loc) => { numbering(__default(loc.page-numbering(), "1"), loc.page()) })
// convert labels to links, filtering out duplicated pages
let seen = ()
let links = for i in range(labels.len()) {
let l = labels.at(i)
let p = pages.at(i)
if seen.contains(p) {
(none,)
} else {
seen.push(p)
(link(l, p),)
}
}.filter(l => l != none)
// connect links with commas and return
links.join(", ")
}
// Main wrapper (usually used in a `#show: init-glossary`) which loads the
// entries passed in into our state. Furthermore, hooks into references so that
// we can intercept term usage in a doc and label them appropriately.
#let init-glossary(entries, body) = context {
for entry in __normalize_entries(entries) {
__add_entry(entry)
}
// convert refs we recognize into links with labels
show ref: r => {
let (key, ..modifiers) = str(r.target).split(":")
if __has_entry(key) {
__gls(key, modifiers: modifiers)
} else {
r
}
}
body
}
// Used to print a glossary. Can customize title, theme, and/or specify groups.
//
// A theme is a dictionary with three attributes:
//
// #let my-theme = (
// section: (title, body) => {
// // how to display the glossary section and its body which will contain
// // groups, each with their entries
// },
//
// group: (name, body) => {
// // how to display a group name and its body (which will contain the
// // entries in that group)
// },
//
// entry: (entry, i, n) => {
// // how to display a single entry, along with its index and total count
// }
// }
#let glossary(title: "Glossary", theme: theme-2col, groups: ()) = context {
// our output is a map of group name to array of entries (each entry is a map)
let output = (:)
// pull in entire dictionary
let all_entries = __gloss_entries.final()
// filter down to just what we used
let all_used = __gloss_used.final()
// TODO: what about entries with no group? Or group == "" or == none
// get all groups
let all_groups = all_entries.values().map(e => e.at("group")).dedup().sorted()
let groups = if groups.len() == 0 { all_groups } else { groups }
// make sure requested groups are legit
for g in groups {
if g not in all_groups {
panic("Requested group not found: " + g)
}
}
// iterate one group at a time
for g in groups {
// collect used entries in this group
let cur = ()
for (key, count) in all_used {
let e = all_entries.at(key)
if e.at("group") == g {
// this term is both in this group and was used, add to our output
let short = e.at("short")
let long = e.at("long")
let description = e.at("description")
let label = [#metadata(key)#__dict_label(key)]
let pages = __create_backlinks(key, count)
cur.push((short: short, long: long, description: description, label: label, pages: pages))
}
}
if cur.len() > 0 {
if g == none { g = "" }
output.insert(g, cur.sorted(key: e => e.short))
}
}
// TODO: rendering for a) just one group, or b) the default group (ie terms
// with no group)?? -- in this case should we just not use the theme.group()
// function?
// index for group because dictionary don't have enumerate()
let i_group = 0;
// render it using our theme
(theme.section)(
title,
// section body (all groups)
for (group, entries) in output {
(theme.group)(
group,
i_group,
output.len(),
// group body (all entries)
for (i,e) in entries.enumerate() {
(theme.entry)(e, i, entries.len())
},
)
i_group += 1
}
)
}
-19
View File
@@ -1,19 +0,0 @@
#let __default(val, default) = if val == none { default } else { val }
#let __pluralize(word) = {
// Define helper functions for checking endings
let ends_with = (suffix) => lower(word).ends-with(suffix)
let ends_with_any = (suffixes) => suffixes.any((suffix) => ends_with(suffix))
if ends_with_any(( "s", "x", "z", "sh", "ch" )) { // Handle special cases for "es" endings
word + "es"
} else if ends_with("y") and not ends_with_any(( "a", "e", "i", "o", "u" )) { // Handle "y" -> "ies"
word.slice(0, -1) + "ies"
} else if ends_with("f") { // Handle "f" or "fe" -> "ves"
word.slice(0, -1) + "ves"
} else if ends_with("fe") {
word.slice(0, -2) + "ves"
} else { // Default to adding "s"
word + "s"
}
}
-11
View File
@@ -1,11 +0,0 @@
[package]
name = "glossy"
version = "0.1.1"
entrypoint = "lib.typ"
authors = ["Stephen Waits <steve@waits.net>"]
license = "MIT"
description = "A very simple glossary system with easily customizable output."
categories = ["model"]
keywords = ["glossary", "index", "dictionary", "gloss"]
repository = "https://github.com/swaits/typst-collection"
exclude = ["thumbnail.png", "examples/readme.pdf"]
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Stephen Waits <steve@waits.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-1
View File
@@ -1 +0,0 @@
#import "src/gloss.typ": init-glossary, glossary
-47
View File
@@ -1,47 +0,0 @@
#let theme-2col = (
section: (title, body) => {
set par.line(numbering: none)
heading(level: 1, title)
columns(2, body)
},
group: (name, i, n, body) => {
if name != "" and n > 1 {
heading(level: 2, name)
}
body
},
entry: (entry, i, n) => {
// build our title, etc
let short-display = text(weight: "regular", entry.short)
let long-display = if entry.long == none {
[]
} else {
[#h(0.25em) -- #entry.long]
}
// build our description
let description = if entry.description == none {
[]
} else {
[: #entry.description]
}
text(
size: 0.75em,
weight: "light",
grid(
columns: (auto,auto,auto,1fr,1em,auto),
align: (left, left, left, center, center, right),
[#short-display#entry.label],
[#long-display],
[#description],
[#repeat(h(0.25em) + "." + h(0.25em))],
[ . ],
[#entry.pages]
)
)
},
)
-19
View File
@@ -1,19 +0,0 @@
#let __default(val, default) = if val == none { default } else { val }
#let __pluralize(word) = {
// Define helper functions for checking endings
let ends_with = (suffix) => lower(word).ends-with(suffix)
let ends_with_any = (suffixes) => suffixes.any((suffix) => ends_with(suffix))
if ends_with_any(( "s", "x", "z", "sh", "ch" )) { // Handle special cases for "es" endings
word + "es"
} else if ends_with("y") and not ends_with_any(( "a", "e", "i", "o", "u" )) { // Handle "y" -> "ies"
word.slice(0, -1) + "ies"
} else if ends_with("f") { // Handle "f" or "fe" -> "ves"
word.slice(0, -1) + "ves"
} else if ends_with("fe") {
word.slice(0, -2) + "ves"
} else { // Default to adding "s"
word + "s"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

-11
View File
@@ -1,11 +0,0 @@
[package]
name = "glossy"
version = "0.1.2"
entrypoint = "lib.typ"
authors = ["Stephen Waits <steve@waits.net>"]
license = "MIT"
description = "A very simple glossary system with easily customizable output."
categories = ["model"]
keywords = ["glossary", "index", "dictionary", "gloss"]
repository = "https://github.com/swaits/typst-collection"
exclude = ["thumbnail.png", "examples/readme.pdf"]
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Stephen Waits <steve@waits.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-280
View File
@@ -1,280 +0,0 @@
# Glossy
This package provides utilities to manage and render glossaries within
documents. It includes functions to define and use glossary terms, track their
usage, and generate a glossary list with references to where terms are used in
the document.
![image of sample output](thumbnail.png)
## Motivation
Glossy is heavily inspired by
[glossarium](https://typst.app/universe/package/glossarium), with a few key
different goals:
1. Provide a simple interface which allows for complete control over glossary
display. To do this, `glossy`'s `#glossary()` function accepts a theme
parameter. The goal here is to separate presentation and logic.
2. Simplify the user interface as much as possible. Glossy has exactly two
exports, `init-glossary` and `glossary`.
3. Double-down on `glossy`'s excellent `@term` reference approach, completely
eliminating the need to make any calls to `gls()` and friends.
4. Mimic established patterns and best practices. For example, `glossy`'s
`#glossary()` function is intentionally similar (in naming and parameters) to
`typst`'s built-in `#bibliography`, to the degree possible.
5. Simplify the implementation. The `glossy` code is significantly shorter and
easier to understand.
## Features
- Define glossary terms with short and long forms, descriptions, and grouping
- Automatically tracks term usage in the document through `@labels`
- Supports modifiers to adjust how terms are displayed (capitalize, pluralize, etc.)
- Generates a formatted glossary section with backlinks to term occurrences
- Customizable themes for rendering glossary sections, groups, and entries
- Automatic pluralization of terms with custom override options
- Page number references back to term usage locations
## Usage
### Import the package
```typst
#import "@preview/glossy:0.2.0": *
```
### Defining Glossary Terms
Use the `init-glossary` function to initialize glossary entries:
```typst
#let myGlossary = (
html: (
short: "HTML",
long: "Hypertext Markup Language",
description: "A standard language for creating web pages",
group: "Web"
),
css: (
short: "CSS",
long: "Cascading Style Sheets",
description: "A stylesheet language used for describing the presentation of a document",
group: "Web"
),
tps: (
short: "TPS",
long: "test procedure specification",
description: "A formal document describing test steps and expected results",
// Optional: Override automatic pluralization
plural: "TPSes",
longplural: "test procedure specifications"
)
)
#show: init-glossary.with(myGlossary)
```
Each glossary entry supports the following fields:
- `short` (required): Short form of the term
- `long` (optional): Long form of the term
- `description` (optional): Term description (often a definition)
- `group` (optional): Category grouping
- `plural` (optional): Override automatic pluralization of short form
- `longplural` (optional): Override automatic pluralization of long form
You can also load glossary entries from a data file using #yaml(), #json(), or similar.
For example, the above glossary could be in this YAML file:
```yaml
html:
short: HTML
long: Hypertext Markup Language
description: A standard language for creating web pages
group: Web
css:
short: CSS
long: Cascading Style Sheets
description: A stylesheet language used for describing the presentation of a document
group: Web
tps:
short: TPS
long: test procedure specification
description: A formal document describing test steps and expected results
plural: TPSes
longplural: test procedure specifications
```
And then loaded during initialization as follows:
```typst
#show: init-glossary.with(yaml("glossary.yaml"))
```
### Using Glossary Terms
Reference glossary terms using Typst's `@reference` syntax:
```typst
In modern web development, languages like @html and @css are essential.
The @tps:pl need to be submitted by Friday.
```
Available modifiers:
- **cap**: Capitalizes the term
- **pl**: Uses the plural form
- **both**: Shows "Long Form (Short Form)"
- **short**: Shows only short form
- **long**: Shows only long form
- **def** or **desc**: Shows the description
Modifiers can be combined with colons:
| **Input** | **Output** |
| ------------------- | -------------------------------------------------------------- |
| `@tps` (first use) | "test procedure specification (TPS)" |
| `@tps` (subsequent) | "TPS" |
| `@tps:short` | "TPS" |
| `@tps:long` | "test procedure specification" |
| `@tps:both` | "test procedure specification (TPS)" |
| `@tps:long:cap` | "Test procedure specification" |
| `@tps:long:pl` | "test procedure specifications" |
| `@tps:short:pl` | "TPSes" |
| `@tps:both:pl:cap` | "Technical procedure specifications (TPSes)" |
| `@tps:def` | "A formal document describing test steps and expected results" |
### Generating the Glossary
Display the glossary using the `glossary()` function:
```typst
#glossary(
title: "Web Development Glossary",
theme: my-theme,
groups: ("Web") // Optional: Filter to specific groups
)
```
### Customizing Term Display
Control how terms appear in the document by providing a custom `show-term` function:
```typst
#let emph-term(term-body) = { emph(term-body) }
#show: init-glossary.with(
myGlossary,
show-term: emph-term
)
```
### Glossary Themes
#### Included Themes
Glossy comes with several built-in themes that can be used directly or serve as examples for custom themes:
##### theme-twocol
A professional two-column layout ideal for technical documentation. Features:
- Two-column layout for efficient space usage
- Dotted leaders to page numbers
- Clear hierarchy with optional group headings
- Compact but readable formatting
- Terms in regular weight with long forms and descriptions inline
```typst
#glossary(theme: theme-twocol)
```
##### theme-basic
A traditional single-column layout similar to book glossaries. Features:
- Bold terms with indented content
- Clear separation between entries
- Hanging indentation for wrapped lines
- Parenthetical long forms
- Page numbers with "pp." prefix
- Simple, clean design
```typst
#glossary(theme: theme-basic)
```
##### theme-compact
A space-efficient layout perfect for technical documents or appendices. Features:
- Reduced vertical spacing
- Smaller font sizes for secondary information
- Color-coded term components
- Grid-based alignment
- Minimal decorative elements
- Gray text for supplementary information
- Bullet separators between components
```typst
#glossary(theme: theme-compact)
```
#### Custom Themes
Customize glossary appearance by defining a theme with three functions:
```typst
#let my-theme = (
// Main glossary section
section: (title, body) => {
heading(level: 1, title)
body
},
// Group of related terms
group: (name, index, total, body) => {
// index = group index, total = total groups
if name != "" and total > 1 {
heading(level: 2, name)
}
body
},
// Individual glossary entry
entry: (entry, index, total) => {
// index = entry index, total = total entries in group
let output = [#entry.short]
if entry.long != none {
output = [#output -- #entry.long]
}
if entry.description != none {
output = [#output: #entry.description]
}
block(
grid(
columns: (auto, 1fr, auto),
output,
repeat([#h(0.25em) . #h(0.25em)]),
entry.pages,
)
)
}
)
```
Entry fields available to themes:
- `short`: Short form (always present)
- `long`: Long form (can be `none`)
- `description`: Term description (can be `none`)
- `label`: Term's dictionary label
- `pages`: Linked page numbers where term appears
## License
This project is licensed under the MIT License.
Binary file not shown.
-99
View File
@@ -1,99 +0,0 @@
// a minimal example, also listed in the README
#import "@preview/glossy:0.2.0": *
#let myGlossary = (
html: (
short: "HTML",
long: "Hypertext Markup Language",
description: "A standard language for creating web pages",
group: "Web"),
css: (
short: "CSS",
long: "Cascading Style Sheets",
description: "A language used for describing the presentation of a document",
group: "Web"),
tps: (
short: "TPS",
long: "test procedure specification",
description: "A document on how to run all the test procedures"),
)
#show: init-glossary.with(myGlossary, show-term: (body) => [#emph(body)])
#set heading(numbering: "1.1")
= Hello, `glossy`!
In modern web development, languages like @html and @css are essential.
Now make sure I get your @tps:short reports by 2pm!
@tps:cap is defined as: @tps:def.
= Examples with modifiers
#table(
columns: 2,
table.header([*Input*], [*Output*]),
[`@tps:short` ], [@tps:short],
[`@tps:long` ], [@tps:long],
[`@tps:both` ], [@tps:both],
[`@tps:long:cap` ], [@tps:long:cap],
[`@tps:long:pl` ], [@tps:long:pl],
[`@tps:short:pl` ], [@tps:short:pl],
[`@tps:both:pl:cap`], [@tps:both:pl:cap],
[`@tps:def` ], [@tps:def],
[`@tps:desc` ], [@tps:desc],
)
== Using conflicting modifiers (short, long, both)
These modifiers are semantically mutually exclusive, by they can be combined
syntactically. When multiple are used, `glossy` tries to make a smart choice on
which one to display.
#table(
columns: 2,
table.header([*Input*], [*Output*]),
[`@tps:short:long` ], [@tps:short:long],
[`@tps:short:both` ], [@tps:short:both],
[`@tps:long:both` ], [@tps:long:both],
[`@tps:short:long:both`], [@tps:short:long:both],
)
#let my-theme = (
section: (title, body) => {
heading(level: 1, title)
body
},
group: (name, i, n, body) => {
if name != "" and n > 1 {
heading(level: 2, name)
}
body
},
entry: (entry, i, n) => {
// i is the entry's index, n is total number of entries
let output = [#entry.short]
if entry.long != none {
output = [#output -- #entry.long]
}
if entry.description != none {
output = [#output: #entry.description]
}
block(
grid(
columns: (auto, 1fr, auto),
output,
repeat([#h(0.25em) . #h(0.25em)]),
entry.pages,
)
)
}
)
#glossary(theme: my-theme)
#glossary(title: "Glossary with just empty group", groups: ("",), theme: theme-basic)
#glossary(title: "Glossary with just Web group", groups: ("Web",), theme: theme-compact)
-2
View File
@@ -1,2 +0,0 @@
#import "src/gloss.typ": init-glossary, glossary
#import "src/themes.typ": *
-464
View File
@@ -1,464 +0,0 @@
#import "./themes.typ": *
#import "./utils.typ": *
#let __gloss_entries = state("__gloss_entries", (:))
#let __gloss_used = state("__gloss_used", (:))
#let __gloss_label_prefix = "__gloss:"
// Normalizes a dictionary entry by ensuring all required and optional keys exist
// with appropriate default values.
//
// Parameters:
// entry (dictionary): Input dictionary containing at least 'short' and optionally
// 'long', 'plural', 'longplural', 'description', and 'group'
//
// Returns:
// dictionary: Normalized dictionary with all expected keys populated
//
// Throws:
// - If 'short' key is missing
// - If 'short' value is not a string
// - If 'short' value is an empty string
//
#let __normalize_entry(entry) = {
// Validate required fields
if not "short" in entry {
panic("Entry must contain a 'short' key")
}
if type(entry.short) != "string" {
panic("Entry 'short' must be a string")
}
if entry.short.trim() == "" {
panic("Entry 'short' cannot be empty")
}
// Extract values with defaults, using consistent none for missing optionals
let long = entry.at("long", default: none)
// Return the normalized entry
(
short: entry.short,
plural: entry.at("plural", default: __pluralize(entry.short)),
long: long,
longplural: entry.at("longplural", default: __pluralize(long)),
description: entry.at("description", default: none),
group: entry.at("group", default: "")
)
}
// Checks if a key exists in the glossary state.
//
// Parameters:
// key (string): The key to look up in the glossary entries
//
// Returns:
// boolean: True if the key exists in the glossary, false otherwise
//
#let __has_entry(key) = {
key in __gloss_entries.final().keys()
}
// Updates the glossary state by adding or updating an entry.
//
// Parameters:
// key (string): The key under which to store the entry
// entry (dictionary): The normalized glossary entry to store
//
// Returns:
// none: Updates state as a side effect
//
#let __add_entry(key, entry) = {
__gloss_entries.update(state => {
state.insert(key, entry)
state
})
}
// Retrieves a glossary entry from the state.
//
// Parameters:
// key (string): The key of the entry to retrieve
//
// Returns:
// dictionary: The glossary entry associated with the key
//
// Throws:
// - If the key does not exist in the glossary
//
#let __get_entry(key) = {
let entries = __gloss_entries.final()
if key not in entries.keys() {
panic("Glossary error: Missing key '" + key + "'")
}
entries.at(key)
}
// Creates a prefixed label string for dictionary entries and term usage.
//
// Parameters:
// key (string): The glossary entry key
//
// Returns:
// string: The prefixed label string for dictionary entries
//
#let __dict_label_str(key) = {
__gloss_label_prefix + key
}
// Creates a label object for dictionary entries.
//
// Parameters:
// key (string): The glossary entry key
//
// Returns:
// label: A Typst label object for the dictionary entry
//
#let __dict_label(key) = {
label(__dict_label_str(key))
}
// Creates a prefixed label string for term usage in documents.
//
// Parameters:
// key (string): The glossary entry key
// index (int): The occurrence index of the term in the document
//
// Returns:
// string: The prefixed label string for term usage
//
#let __term_label_str(key, index) = {
__gloss_label_prefix + key + "." + str(index)
}
// Creates a label object for term usage in documents.
//
// Parameters:
// key (string): The glossary entry key
// index (int): The occurrence index of the term in the document
//
// Returns:
// label: A Typst label object for the term usage
//
#let __term_label(key, index) = {
label(__term_label_str(key, index))
}
// Updates the term usage count in the glossary state.
//
// Parameters:
// key (string): The glossary entry key
// count (int): The number of times the term has been used
//
// Returns:
// none: Updates state as a side effect
//
#let __save_term_usage(key, count) = {
__gloss_used.update(state => {
state.insert(key, count)
state
})
}
// Renders a glossary term with various formatting options.
//
// This function handles the display of terms in the document, managing both first
// and subsequent uses, pluralization, capitalization, and different display forms
// (short, long, or combined forms).
//
// Parameters:
// key (string): The glossary entry key to render
// modifiers (array): Array of modifier strings that control term rendering:
// - "cap": Capitalize the first letter of the term
// - "pl": Use plural form of the term
// - "both": Show "Long form (short form)"
// - "short": Show only the short form
// - "long": Show only the long form
// - "def" or "desc": Show the term's description instead
//
// Returns:
// content: Formatted term with appropriate labels and metadata
//
// Behavior:
// - Without modifiers, first use shows "Long form (short form)", subsequent uses show short form
// - Explicit modifiers override the default first-use behavior
// - If long form is requested but unavailable, falls back to short form
// - Maintains usage counter for term references
//
#let __gls(key, modifiers: array) = {
// Fetch the entry and prepare label
let entry = __get_entry(key)
let entry_label = __dict_label_str(key)
// Handle description mode
if "def" in modifiers or "desc" in modifiers {
return [#entry.description]
}
// Track term usage
let entry_counter = counter(entry_label)
entry_counter.step()
let key_index = entry_counter.get().first()
let first = key_index == 0
__save_term_usage(key, key_index + 1)
// Helper: Apply pluralization if requested
let pluralize_term = (singular, plural) => {
if "pl" in modifiers and plural != none { plural } else { singular }
}
// Helper: Apply capitalization if requested
let capitalize_term = (term) => {
if "cap" in modifiers { upper(term.first()) + term.slice(1) } else { term }
}
// Helper: Apply both pluralization and capitalization
let format_term = (term, plural) => {
capitalize_term(pluralize_term(term, plural))
}
// Helper: Select and format the appropriate term form
let select_term = (is_long_mode, use_both) => {
let short_form = format_term(entry.short, entry.plural)
let long_form = if entry.long != none {
format_term(entry.long, entry.longplural)
} else {
none
}
if use_both and long_form != none {
[#long_form (#short_form)]
} else if is_long_mode and long_form != none {
[#long_form]
} else {
[#short_form]
}
}
// Determine display mode from modifiers
let is_both = "both" in modifiers
let is_long = "long" in modifiers and not is_both
let is_short = "short" in modifiers and not is_both and not is_long
context {
// Generate final display based on modifiers or default behavior
let display = if is_both or is_long or is_short {
select_term(is_long, is_both)
} else {
select_term(false, first)
}
// Emit term with metadata and usage label
[#display#metadata(display)#__term_label(key, key_index)]
}
}
// Creates page number links back to each usage of a glossary term.
//
// This function generates a comma-separated list of page numbers, where each
// number links back to a usage of the term in the document. Duplicate page
// numbers are removed to avoid redundancy.
//
// Parameters:
// key (string): The glossary entry key
// count (int): Number of times the term appears in the document
//
// Returns:
// content: Comma-separated list of linked page numbers
//
// Example output: "1, 3, 5" where each number links to the term usage on that page
//
#let __create_backlinks(key, count) = {
// Generate labels for each term usage
let labels = for i in range(count) {
(__term_label(key, i),)
}
// Get page numbers for each usage
let pages = labels
.map(l => locate(l))
.map(loc => numbering(
__default(loc.page-numbering(), "1"),
loc.page()
))
// Create links, excluding duplicate page numbers
let seen = ()
let links = for i in range(labels.len()) {
let label = labels.at(i)
let page = pages.at(i)
if seen.contains(page) {
(none,)
} else {
seen.push(page)
(link(label, page),)
}
}.filter(l => l != none)
// Join links with commas
links.join(", ")
}
// Initializes the glossary system and sets up term reference handling.
//
// This function is typically used with `#show: init-glossary`. It performs
// three main tasks:
// 1. Validates and loads glossary entries into the state
// 2. Sets up reference handling to intercept and format term usage
// 3. Applies custom term formatting if provided
//
// Parameters:
// entries (dictionary): Dictionary of glossary entries where:
// - keys are term identifiers
// - values are entry dictionaries containing term details
// show-term ((content) => content): Optional function to customize term rendering
// Default: Returns term content unchanged
// body (content): Document content to process
//
// Returns:
// content: Processed document content with glossary functionality enabled
//
// Throws:
// - If entries parameter is not a dictionary
//
#let init-glossary(
entries,
show-term: (term-body) => term-body,
body
) = context {
// Validate entries parameter type
if type(entries) != dictionary {
panic("entries must be a dictionary, instead got: " + repr(entries))
}
// Process and store each glossary entry
for (key, entry) in entries {
__add_entry(key, __normalize_entry(entry))
}
// Set up reference handling for glossary terms
show ref: r => {
let (key, ..modifiers) = str(r.target).split(":")
if __has_entry(key) {
show-term(__gls(key, modifiers: modifiers))
} else {
r
}
}
body
}
// Renders a complete glossary with customizable formatting and grouping.
//
// The glossary displays all used terms, optionally grouped by category, with page
// references back to term usage. The appearance is controlled by a theme that
// defines how sections, groups, and entries are formatted.
//
// Parameters:
// title (string): Glossary section title (default: "Glossary")
// theme (dictionary): Controls glossary appearance with three functions:
// - section(title, body): Renders the main glossary section
// - title: The glossary title
// - body: Content containing all groups and entries
// - group(name, index, total, body): Renders a group of related terms
// - name: Group name
// - index: Zero-based group index
// - total: Total number of groups
// - body: Content containing the group's entries
// - entry(entry, index, total): Renders a single glossary entry
// - entry: Dictionary containing:
// - short: Short form of term
// - long: Long form of term (optional)
// - description: Term description (optional)
// - label: Term's dictionary label
// - pages: Linked page numbers where term appears
// - index: Zero-based entry index within group
// - total: Total entries in group
// groups (array): Optional list of groups to include
// If empty, includes all groups
//
// Returns:
// content: Formatted glossary content
//
// Throws:
// - If a requested group doesn't exist
//
#let glossary(
title: "Glossary",
theme: theme-twocol,
groups: ()
) = context {
// Collect and organize entries by group
let output = (:)
let all_entries = __gloss_entries.final()
let all_used = __gloss_used.final()
// Determine which groups to process
let all_groups = all_entries
.values()
.map(e => e.at("group"))
.dedup()
.sorted()
let target_groups = if groups.len() == 0 {
all_groups
} else {
// Validate requested groups exist
for g in groups {
if g not in all_groups {
panic("Requested group not found: " + g)
}
}
groups
}
// Process entries group by group
for group in target_groups {
let current_entries = ()
// Collect all used entries for this group
for (key, count) in all_used {
let entry = all_entries.at(key)
if entry.at("group") == group {
current_entries.push((
short: entry.at("short"),
long: entry.at("long"),
description: entry.at("description"),
label: [#metadata(key)#__dict_label(key)],
pages: __create_backlinks(key, count)
))
}
}
// Add non-empty groups to output
if current_entries.len() > 0 {
group = if group == none { "" } else { group }
output.insert(
group,
current_entries.sorted(key: e => e.short)
)
}
}
// Render the glossary using the theme
let group_index = 0
(theme.section)(
title,
for (group, entries) in output {
(theme.group)(
group,
group_index,
output.len(),
for (i, entry) in entries.enumerate() {
(theme.entry)(entry, i, entries.len())
}
)
group_index += 1
}
)
}
-236
View File
@@ -1,236 +0,0 @@
// Theme definition for a two-column glossary layout with hierarchical sections
//
// This theme provides formatting for three levels of glossary content:
// 1. Section: The overall glossary container
// 2. Groups: Optional categorization of related terms
// 3. Entries: Individual glossary terms and their definitions
//
#let theme-twocol = (
// Renders the main glossary section as a two-column layout
// Parameters:
// title: The glossary section title
// body: Content containing all groups and entries
section: (title, body) => {
set par.line(numbering: none)
heading(level: 1, title)
columns(2, body)
},
// Renders a group of related glossary terms
// Parameters:
// name: Group name (empty string for ungrouped terms)
// index: Zero-based group index
// total: Total number of groups
// body: Content containing the group's entries
group: (name, index, total, body) => {
if name != "" and total > 1 {
heading(level: 2, name)
}
body
},
// Renders a single glossary entry with term, definition, and page references
// Parameters:
// entry: Dictionary containing term data:
// - short: Short form of term
// - long: Long form of term (optional)
// - description: Term description (optional)
// - label: Term's dictionary label
// - pages: Linked page numbers where term appears
// index: Zero-based entry index within group
// total: Total entries in group
entry: (entry, index, total) => {
// Format the short form and optional long form of the term
let short-display = text(weight: "regular", entry.short)
let long-display = if entry.long == none {
[]
} else {
[#h(0.25em) -- #entry.long]
}
// Format the optional description
let description = if entry.description == none {
[]
} else {
[: #entry.description]
}
// Render the complete entry with dotted leader line to page numbers
text(
size: 0.75em,
weight: "light",
grid(
columns: (auto,auto,auto,1fr,1em,auto),
align: (left, left, left, center, center, right),
[#short-display#entry.label], // Term with label
[#long-display], // Long form (if any)
[#description], // Description (if any)
[#repeat(h(0.25em) + "." + h(0.25em))], // Dotted leader line
[ . ], // Final dot
[#entry.pages] // Page references
)
)
},
)
// Theme definition for a basic single-column glossary layout
//
// This theme provides a traditional glossary format with:
// 1. Bold terms
// 2. Indented descriptions
// 3. Simple page references
// 4. Optional group headings
//
#let theme-basic = (
// Renders the main glossary section as a single column
// Parameters:
// title: The glossary section title
// body: Content containing all groups and entries
section: (title, body) => {
heading(level: 1, title)
body
},
// Renders a group of related glossary terms
// Parameters:
// name: Group name (empty string for ungrouped terms)
// index: Zero-based group index
// total: Total number of groups
// body: Content containing the group's entries
group: (name, index, total, body) => {
if name != "" and total > 1 {
heading(level: 2, name)
}
body
},
// Renders a single glossary entry with term, definition, and page references
// Parameters:
// entry: Dictionary containing term data:
// - short: Short form of term
// - long: Long form of term (optional)
// - description: Term description (optional)
// - label: Term's dictionary label
// - pages: Linked page numbers where term appears
// index: Zero-based entry index within group
// total: Total entries in group
entry: (entry, index, total) => {
// Format the term parts
let term = text(weight: "bold", entry.short)
let long-form = if entry.long == none {
[]
} else {
[, #entry.long]
}
// Format the description with proper spacing
let description = if entry.description == none {
[]
} else {
[: #entry.description]
}
// Create the complete entry with hanging indent
block(
spacing: 0.5em,
pad(
left: 1em,
bottom: 0.5em,
block(
[#term#entry.label#long-form#description #h(1em) (pp. #entry.pages)]
)
)
)
},
)
// Theme definition for a compact glossary layout optimized for space efficiency
//
// This theme provides a dense but readable format with:
// 1. Terms and definitions on same line
// 2. Smaller font size
// 3. Minimal vertical spacing
// 4. Left-aligned groups for quick scanning
// 5. Condensed page references
//
#let theme-compact = (
// Renders the main glossary section with minimal spacing
// Parameters:
// title: The glossary section title
// body: Content containing all groups and entries
section: (title, body) => {
set par(leading: 0.65em)
heading(level: 1, outlined: false, text(weight: "bold", size: 1.1em, title))
body
},
// Renders a group of related glossary terms
// Parameters:
// name: Group name (empty string for ungrouped terms)
// index: Zero-based group index
// total: Total number of groups
// body: Content containing the group's entries
group: (name, index, total, body) => {
if name != "" and total > 1 {
block(
spacing: 0.5em,
pad(
top: 0.5em,
text(weight: "bold", size: 0.9em, name)
)
)
}
body
},
// Renders a single glossary entry with term, definition, and page references
// Parameters:
// entry: Dictionary containing term data:
// - short: Short form of term
// - long: Long form of term (optional)
// - description: Term description (optional)
// - label: Term's dictionary label
// - pages: Linked page numbers where term appears
// index: Zero-based entry index within group
// total: Total entries in group
entry: (entry, index, total) => {
// Format term components with minimal spacing
let term = text(
size: 0.65em,
weight: "medium",
fill: gray.darken(60%),
entry.short
)
let long-form = if entry.long == none {
[]
} else {
text(size: 0.65em, fill: gray.darken(20%), [ (#entry.long)])
}
let description = if entry.description == none {
[]
} else {
text(size: 0.65em, [· #entry.description])
}
// Create the complete entry with tight spacing
block(
spacing: 0.4em,
grid(
columns: (auto, 1fr, auto),
gutter: 0.5em,
// Term and description column
box[#term#entry.label#long-form #description],
// Dots....
repeat(h(0.25em) + text(fill: gray, ".") + h(0.25em)),
// Page references with smaller font
text(
size: 0.6em,
fill: gray.darken(20%),
entry.pages
)
)
)
},
)
-38
View File
@@ -1,38 +0,0 @@
// Returns the provided value if it is not none, otherwise returns the default value
// Parameters:
// val: The value to check
// default: The fallback value to use if val is none
// Returns: val if not none, otherwise default
#let __default(val, default) = if val == none { default } else { val }
// Converts an English word to its plural form following standard English pluralization rules
// Parameters:
// word: The singular word to pluralize
// Returns: The pluralized form of the word, or none if input is none
#let __pluralize(word) = {
// early exit with invalid input
if word == none {
return none
}
// Define helper functions for checking endings
let ends_with = (suffix) => lower(word).ends-with(suffix)
let ends_with_any = (suffixes) => suffixes.any((suffix) => ends_with(suffix))
// Handle special cases for "es" endings (s, x, z, sh, ch)
if ends_with_any(( "s", "x", "z", "sh", "ch" )) {
word + "es"
} else if ends_with("y") and not ends_with_any(( "a", "e", "i", "o", "u" )) {
// Convert y to ies when y follows a consonant
word.slice(0, -1) + "ies"
} else if ends_with("f") {
// Convert f to ves
word.slice(0, -1) + "ves"
} else if ends_with("fe") {
// Convert fe to ves
word.slice(0, -2) + "ves"
} else {
// Default case: just add s
word + "s"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

-11
View File
@@ -1,11 +0,0 @@
[package]
name = "glossy"
version = "0.2.0"
entrypoint = "lib.typ"
authors = ["Stephen Waits <steve@waits.net>"]
license = "MIT"
description = "A very simple glossary system with easily customizable output."
categories = ["model"]
keywords = ["glossary", "index", "dictionary", "gloss"]
repository = "https://github.com/swaits/typst-collection"
exclude = ["thumbnail.png", "examples/readme.pdf"]
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Stephen Waits <steve@waits.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-239
View File
@@ -1,239 +0,0 @@
# Glossy
This package provides utilities to manage and render glossaries within
documents. It includes functions to define and use glossary terms, track their
usage, and generate a glossary list with references to where terms are used in
the document.
![image of sample output](thumbnail.png)
## Motivation
Glossy is heavily inspired by
[glossarium](https://typst.app/universe/package/glossarium), with a few key
different goals:
1. Provide a simple interface which allows for complete control over glossary
display. To do this, `glossy`'s `#glossary()` function accepts a theme
parameter. The goal here is to separate presentation and logic.
2. Simplify the user interface as much as possible. Glossy has exactly two
exports, `init-glossary` and `glossary`.
3. Double-down on `glossy`'s excellent `@term` reference approach, completely
eliminating the need to make any calls to `gls()` and friends.
4. Mimic established patterns and best practices. For example, `glossy`'s
`#glossary()` function is intentionally similar (in naming and parameters) to
`typst`'s built-in `#bibliography`, to the degree possible.
5. Simplify the implementation. The `glossy` code is significantly shorter and
easier to understand.
## Features
- Define glossary terms with short and long forms, descriptions, and grouping
- Automatically tracks term usage in the document through `@labels`
- Supports modifiers to adjust how terms are displayed (capitalize, pluralize, etc.)
- Generates a formatted glossary section with backlinks to term occurrences
- Customizable themes for rendering glossary sections, groups, and entries
- Automatic pluralization of terms with custom override options
- Page number references back to term usage locations
## Usage
### Import the package
```typst
#import "@preview/glossy:0.2.1": *
```
### Defining Glossary Terms
Use the `init-glossary` function to initialize glossary entries:
```typst
#let myGlossary = (
html: (
short: "HTML",
long: "Hypertext Markup Language",
description: "A standard language for creating web pages",
group: "Web"
),
css: (
short: "CSS",
long: "Cascading Style Sheets",
description: "A stylesheet language used for describing the presentation of a document",
group: "Web"
),
tps: (
short: "TPS",
long: "test procedure specification",
description: "A formal document describing test steps and expected results",
// Optional: Override automatic pluralization
plural: "TPSes",
longplural: "test procedure specifications"
)
)
#show: init-glossary.with(myGlossary)
```
Each glossary entry supports the following fields:
- `short` (required): Short form of the term
- `long` (optional): Long form of the term
- `description` (optional): Term description (often a definition)
- `group` (optional): Category grouping
- `plural` (optional): Override automatic pluralization of short form
- `longplural` (optional): Override automatic pluralization of long form
You can also load glossary entries from a data file using #yaml(), #json(), or similar.
For example, the above glossary could be in this YAML file:
```yaml
html:
short: HTML
long: Hypertext Markup Language
description: A standard language for creating web pages
group: Web
css:
short: CSS
long: Cascading Style Sheets
description: A stylesheet language used for describing the presentation of a document
group: Web
tps:
short: TPS
long: test procedure specification
description: A formal document describing test steps and expected results
plural: TPSes
longplural: test procedure specifications
```
And then loaded during initialization as follows:
```typst
#show: init-glossary.with(yaml("glossary.yaml"))
```
### Using Glossary Terms
Reference glossary terms using Typst's `@reference` syntax:
```typst
In modern web development, languages like @html and @css are essential.
The @tps:pl need to be submitted by Friday.
```
Available modifiers:
- **cap**: Capitalizes the term
- **pl**: Uses the plural form
- **both**: Shows "Long Form (Short Form)"
- **short**: Shows only short form
- **long**: Shows only long form
- **def** or **desc**: Shows the description
Modifiers can be combined with colons:
| **Input** | **Output** |
| ------------------- | -------------------------------------------------------------- |
| `@tps` (first use) | "test procedure specification (TPS)" |
| `@tps` (subsequent) | "TPS" |
| `@tps:short` | "TPS" |
| `@tps:long` | "test procedure specification" |
| `@tps:both` | "test procedure specification (TPS)" |
| `@tps:long:cap` | "Test procedure specification" |
| `@tps:long:pl` | "test procedure specifications" |
| `@tps:short:pl` | "TPSes" |
| `@tps:both:pl:cap` | "Technical procedure specifications (TPSes)" |
| `@tps:def` | "A formal document describing test steps and expected results" |
### Generating the Glossary
Display the glossary using the `glossary()` function:
```typst
#glossary(
title: "Web Development Glossary", // Optional: defaults to Glossary
theme: my-theme, // Optionaal: defaults to theme-twocol
ignore-case: false, // Optional: ignore case when sorting terms
groups: ("Web") // Optional: Filter to specific groups
)
```
### Customizing Term Display
Control how terms appear in the document by providing a custom `show-term` function:
```typst
#let emph-term(term-body) = { emph(term-body) }
#show: init-glossary.with(
myGlossary,
show-term: emph-term
)
```
### Glossary Themes
#### Included Themes
Glossy comes with several built-in themes that can be used directly or serve as
examples for custom themes:
![theme gallery image](themeshots.png)
#### Custom Themes
Customize glossary appearance by defining a theme with three functions:
```typst
#let my-theme = (
// Main glossary section
section: (title, body) => {
heading(level: 1, title)
body
},
// Group of related terms
group: (name, index, total, body) => {
// index = group index, total = total groups
if name != "" and total > 1 {
heading(level: 2, name)
}
body
},
// Individual glossary entry
entry: (entry, index, total) => {
// index = entry index, total = total entries in group
let output = [#entry.short]
if entry.long != none {
output = [#output -- #entry.long]
}
if entry.description != none {
output = [#output: #entry.description]
}
block(
grid(
columns: (auto, 1fr, auto),
output,
repeat([#h(0.25em) . #h(0.25em)]),
entry.pages,
)
)
}
)
```
Entry fields available to themes:
- `short`: Short form (always present)
- `long`: Long form (can be `none`)
- `description`: Term description (can be `none`)
- `label`: Term's dictionary label
- `pages`: Linked page numbers where term appears
## License
This project is licensed under the MIT License.
-2
View File
@@ -1,2 +0,0 @@
#import "src/gloss.typ": init-glossary, glossary
#import "src/themes.typ": *
-473
View File
@@ -1,473 +0,0 @@
#import "./themes.typ": *
#import "./utils.typ": *
#let __gloss_entries = state("__gloss_entries", (:))
#let __gloss_used = state("__gloss_used", (:))
#let __gloss_label_prefix = "__gloss:"
// Normalizes a dictionary entry by ensuring all required and optional keys exist
// with appropriate default values.
//
// Parameters:
// entry (dictionary): Input dictionary containing at least 'short' and optionally
// 'long', 'plural', 'longplural', 'description', and 'group'
//
// Returns:
// dictionary: Normalized dictionary with all expected keys populated
//
// Throws:
// - If 'short' key is missing
// - If 'short' value is not a string
// - If 'short' value is an empty string
//
#let __normalize_entry(entry) = {
// Validate required fields
if not "short" in entry {
panic("Entry must contain a 'short' key")
}
if type(entry.short) != "string" {
panic("Entry 'short' must be a string")
}
if entry.short.trim() == "" {
panic("Entry 'short' cannot be empty")
}
// Extract values with defaults, using consistent none for missing optionals
let long = entry.at("long", default: none)
// Return the normalized entry
(
short: entry.short,
plural: entry.at("plural", default: __pluralize(entry.short)),
long: long,
longplural: entry.at("longplural", default: __pluralize(long)),
description: entry.at("description", default: none),
group: entry.at("group", default: "")
)
}
// Checks if a key exists in the glossary state.
//
// Parameters:
// key (string): The key to look up in the glossary entries
//
// Returns:
// boolean: True if the key exists in the glossary, false otherwise
//
#let __has_entry(key) = {
key in __gloss_entries.final().keys()
}
// Updates the glossary state by adding or updating an entry.
//
// Parameters:
// key (string): The key under which to store the entry
// entry (dictionary): The normalized glossary entry to store
//
// Returns:
// none: Updates state as a side effect
//
#let __add_entry(key, entry) = {
__gloss_entries.update(state => {
state.insert(key, entry)
state
})
}
// Retrieves a glossary entry from the state.
//
// Parameters:
// key (string): The key of the entry to retrieve
//
// Returns:
// dictionary: The glossary entry associated with the key
//
// Throws:
// - If the key does not exist in the glossary
//
#let __get_entry(key) = {
let entries = __gloss_entries.final()
if key not in entries.keys() {
panic("Glossary error: Missing key '" + key + "'")
}
entries.at(key)
}
// Creates a prefixed label string for dictionary entries and term usage.
//
// Parameters:
// key (string): The glossary entry key
//
// Returns:
// string: The prefixed label string for dictionary entries
//
#let __dict_label_str(key) = {
__gloss_label_prefix + key
}
// Creates a label object for dictionary entries.
//
// Parameters:
// key (string): The glossary entry key
//
// Returns:
// label: A Typst label object for the dictionary entry
//
#let __dict_label(key) = {
label(__dict_label_str(key))
}
// Creates a prefixed label string for term usage in documents.
//
// Parameters:
// key (string): The glossary entry key
// index (int): The occurrence index of the term in the document
//
// Returns:
// string: The prefixed label string for term usage
//
#let __term_label_str(key, index) = {
__gloss_label_prefix + key + "." + str(index)
}
// Creates a label object for term usage in documents.
//
// Parameters:
// key (string): The glossary entry key
// index (int): The occurrence index of the term in the document
//
// Returns:
// label: A Typst label object for the term usage
//
#let __term_label(key, index) = {
label(__term_label_str(key, index))
}
// Updates the term usage count in the glossary state.
//
// Parameters:
// key (string): The glossary entry key
// count (int): The number of times the term has been used
//
// Returns:
// none: Updates state as a side effect
//
#let __save_term_usage(key, count) = {
__gloss_used.update(state => {
state.insert(key, count)
state
})
}
// Renders a glossary term with various formatting options.
//
// This function handles the display of terms in the document, managing both first
// and subsequent uses, pluralization, capitalization, and different display forms
// (short, long, or combined forms).
//
// Parameters:
// key (string): The glossary entry key to render
// modifiers (array): Array of modifier strings that control term rendering:
// - "cap": Capitalize the first letter of the term
// - "pl": Use plural form of the term
// - "both": Show "Long form (short form)"
// - "short": Show only the short form
// - "long": Show only the long form
// - "def" or "desc": Show the term's description instead
//
// Returns:
// content: Formatted term with appropriate labels and metadata
//
// Behavior:
// - Without modifiers, first use shows "Long form (short form)", subsequent uses show short form
// - Explicit modifiers override the default first-use behavior
// - If long form is requested but unavailable, falls back to short form
// - Maintains usage counter for term references
//
#let __gls(key, modifiers: array) = {
// Fetch the entry and prepare label
let entry = __get_entry(key)
let entry_label = __dict_label_str(key)
// Handle description mode
if "def" in modifiers or "desc" in modifiers {
return [#entry.description]
}
// Track term usage
let entry_counter = counter(entry_label)
entry_counter.step()
let key_index = entry_counter.get().first()
let first = key_index == 0
__save_term_usage(key, key_index + 1)
// Helper: Apply pluralization if requested
let pluralize_term = (singular, plural) => {
if "pl" in modifiers and plural != none { plural } else { singular }
}
// Helper: Apply capitalization if requested
let capitalize_term = (term) => {
if "cap" in modifiers { upper(term.first()) + term.slice(1) } else { term }
}
// Helper: Apply both pluralization and capitalization
let format_term = (term, plural) => {
capitalize_term(pluralize_term(term, plural))
}
// Helper: Select and format the appropriate term form
let select_term = (is_long_mode, use_both) => {
let short_form = format_term(entry.short, entry.plural)
let long_form = if entry.long != none {
format_term(entry.long, entry.longplural)
} else {
none
}
if use_both and long_form != none {
[#long_form (#short_form)]
} else if is_long_mode and long_form != none {
[#long_form]
} else {
[#short_form]
}
}
// Determine display mode from modifiers
let is_both = "both" in modifiers
let is_long = "long" in modifiers and not is_both
let is_short = "short" in modifiers and not is_both and not is_long
context {
// Generate final display based on modifiers or default behavior
let display = if is_both or is_long or is_short {
select_term(is_long, is_both)
} else {
select_term(false, first)
}
// Emit term with metadata and usage label
[#display#metadata(display)#__term_label(key, key_index)]
}
}
// Creates page number links back to each usage of a glossary term.
//
// This function generates a comma-separated list of page numbers, where each
// number links back to a usage of the term in the document. Duplicate page
// numbers are removed to avoid redundancy.
//
// Parameters:
// key (string): The glossary entry key
// count (int): Number of times the term appears in the document
//
// Returns:
// content: Comma-separated list of linked page numbers
//
// Example output: "1, 3, 5" where each number links to the term usage on that page
//
#let __create_backlinks(key, count) = {
// Generate labels for each term usage
let labels = for i in range(count) {
(__term_label(key, i),)
}
// Get page numbers for each usage
let pages = labels
.map(l => locate(l))
.map(loc => numbering(
__default(loc.page-numbering(), "1"),
loc.page()
))
// Create links, excluding duplicate page numbers
let seen = ()
let links = for i in range(labels.len()) {
let label = labels.at(i)
let page = pages.at(i)
if seen.contains(page) {
(none,)
} else {
seen.push(page)
(link(label, page),)
}
}.filter(l => l != none)
// Join links with commas
links.join(", ")
}
// Initializes the glossary system and sets up term reference handling.
//
// This function is typically used with `#show: init-glossary`. It performs
// three main tasks:
// 1. Validates and loads glossary entries into the state
// 2. Sets up reference handling to intercept and format term usage
// 3. Applies custom term formatting if provided
//
// Parameters:
// entries (dictionary): Dictionary of glossary entries where:
// - keys are term identifiers
// - values are entry dictionaries containing term details
// show-term ((content) => content): Optional function to customize term rendering
// Default: Returns term content unchanged
// body (content): Document content to process
//
// Returns:
// content: Processed document content with glossary functionality enabled
//
// Throws:
// - If entries parameter is not a dictionary
//
#let init-glossary(
entries,
show-term: (term-body) => term-body,
body
) = context {
// Validate entries parameter type
if type(entries) != dictionary {
panic("entries must be a dictionary, instead got: " + repr(entries))
}
// Process and store each glossary entry
for (key, entry) in entries {
__add_entry(key, __normalize_entry(entry))
}
// Set up reference handling for glossary terms
show ref: r => {
let (key, ..modifiers) = str(r.target).split(":")
if __has_entry(key) {
show-term(__gls(key, modifiers: modifiers))
} else {
r
}
}
body
}
// Renders a complete glossary with customizable formatting and grouping.
//
// The glossary displays all used terms, optionally grouped by category, with page
// references back to term usage. The appearance is controlled by a theme that
// defines how sections, groups, and entries are formatted.
//
// Parameters:
// title (string): Glossary section title (default: "Glossary")
// theme (dictionary): Controls glossary appearance with three functions:
// - section(title, body): Renders the main glossary section
// - title: The glossary title
// - body: Content containing all groups and entries
// - group(name, index, total, body): Renders a group of related terms
// - name: Group name
// - index: Zero-based group index
// - total: Total number of groups
// - body: Content containing the group's entries
// - entry(entry, index, total): Renders a single glossary entry
// - entry: Dictionary containing:
// - short: Short form of term
// - long: Long form of term (optional)
// - description: Term description (optional)
// - label: Term's dictionary label
// - pages: Linked page numbers where term appears
// - index: Zero-based entry index within group
// - total: Total entries in group
// groups (array): Optional list of groups to include
// If empty, includes all groups
//
// Returns:
// content: Formatted glossary content
//
// Throws:
// - If a requested group doesn't exist
//
#let glossary(
title: "Glossary",
theme: theme-twocol,
ignore-case: false,
groups: (),
) = context {
// Collect and organize entries by group
let output = (:)
let all_entries = __gloss_entries.final()
let all_used = __gloss_used.final()
// Determine which groups to process
let all_groups = all_entries
.values()
.map(e => e.at("group"))
.dedup()
.sorted()
let target_groups = if groups.len() == 0 {
all_groups
} else {
// Validate requested groups exist
for g in groups {
if g not in all_groups {
panic("Requested group not found: " + g)
}
}
groups
}
// Process entries group by group
for group in target_groups {
let current_entries = ()
// Collect all used entries for this group
for (key, count) in all_used {
let entry = all_entries.at(key)
if entry.at("group") == group {
current_entries.push((
short: entry.at("short"),
long: entry.at("long"),
description: entry.at("description"),
label: [#metadata(key)#__dict_label(key)],
pages: __create_backlinks(key, count)
))
}
}
// Add non-empty groups to output
if current_entries.len() > 0 {
group = if group == none { "" } else { group }
// sort entries by case insensitivity if requested
let sorted_entries = current_entries
// 1. create array of tuples with (lower [if ignore-case], entry)
.map(e => { if ignore-case { (lower(e.short), e) } else { (e.short, e) } })
// 2. sort the tuples (by first element then second)
.sorted(key: t => t.first())
// 3. strip away the tuple's first element, leaving an array of entries
.map(t => t.last())
// add entries to this group's output map
output.insert(group, sorted_entries)
}
}
// Render the glossary using the theme
let group_index = 0
(theme.section)(
title,
for (group, entries) in output {
(theme.group)(
group,
group_index,
output.len(),
for (i, entry) in entries.enumerate() {
(theme.entry)(entry, i, entries.len())
}
)
group_index += 1
}
)
}
-5
View File
@@ -1,5 +0,0 @@
#import "themes/academic.typ": theme-academic
#import "themes/basic.typ": theme-basic
#import "themes/chicago-index.typ": theme-chicago-index
#import "themes/compact.typ": theme-compact
#import "themes/twocol.typ": theme-twocol
@@ -1,50 +0,0 @@
// Inspired by academic textbook glossaries
#let theme-academic = (
section: (title, body) => {
set text(font: "New Computer Modern")
heading(level: 1, title)
v(1em)
body
},
group: (name, index, total, body) => {
if name != "" and total > 1 {
v(1.5em)
align(left, text(weight: "bold", size: 1.2em, name))
v(0.75em)
line(length: 100%, stroke: 0.5pt)
v(0.75em)
}
body
},
entry: (entry, index, total) => {
let short-display = text(weight: "bold", entry.short)
let long-display = if entry.long == none {
[]
} else {
[. #entry.long]
}
let description = if entry.description == none {
[]
} else {
[. #entry.description]
}
block(
below: 1em,
text(
size: 0.95em,
{
grid(
columns: (1fr, auto),
gutter: 0.75em,
[#short-display#long-display#description],
text(fill: rgb("#666666"), entry.pages)
)
}
)
)
},
)
-70
View File
@@ -1,70 +0,0 @@
// Theme definition for a basic single-column glossary layout
//
// This theme provides a traditional glossary format with:
// 1. Bold terms
// 2. Indented descriptions
// 3. Simple page references
// 4. Optional group headings
//
#let theme-basic = (
// Renders the main glossary section as a single column
// Parameters:
// title: The glossary section title
// body: Content containing all groups and entries
section: (title, body) => {
heading(level: 1, title)
body
},
// Renders a group of related glossary terms
// Parameters:
// name: Group name (empty string for ungrouped terms)
// index: Zero-based group index
// total: Total number of groups
// body: Content containing the group's entries
group: (name, index, total, body) => {
if name != "" and total > 1 {
heading(level: 2, name)
}
body
},
// Renders a single glossary entry with term, definition, and page references
// Parameters:
// entry: Dictionary containing term data:
// - short: Short form of term
// - long: Long form of term (optional)
// - description: Term description (optional)
// - label: Term's dictionary label
// - pages: Linked page numbers where term appears
// index: Zero-based entry index within group
// total: Total entries in group
entry: (entry, index, total) => {
// Format the term parts
let term = text(weight: "bold", entry.short)
let long-form = if entry.long == none {
[]
} else {
[, #entry.long]
}
// Format the description with proper spacing
let description = if entry.description == none {
[]
} else {
[: #entry.description]
}
// Create the complete entry with hanging indent
block(
spacing: 0.5em,
pad(
left: 1em,
bottom: 0.5em,
block(
[#term#entry.label#long-form#description #h(1em) (pp. #entry.pages)]
)
)
)
},
)
@@ -1,47 +0,0 @@
// Inspired by the Chicago Manual of Style's index format
#let theme-chicago-index = (
section: (title, body) => {
set par(hanging-indent: 1em)
heading(level: 1, smallcaps(title))
body
},
group: (name, index, total, body) => {
if name != "" and total > 1 {
v(1em)
text(weight: "medium", style: "italic", name)
v(0.5em)
}
body
},
entry: (entry, index, total) => {
let short-display = text(weight: "regular", entry.short)
let long-display = if entry.long == none {
[]
} else {
[ — #entry.long] // Using em-dash for Chicago style
}
let description = if entry.description == none {
[]
} else {
text(style: "italic", [: #entry.description])
}
block(
below: 0.65em,
text(
size: 0.9em,
{
grid(
columns: (1fr, auto),
gutter: 1em,
[#short-display#entry.label#long-display#description],
[#entry.pages]
)
}
)
)
},
)
@@ -1,91 +0,0 @@
// Theme definition for a compact glossary layout optimized for space efficiency
//
// This theme provides a dense but readable format with:
// 1. Terms and definitions on same line
// 2. Smaller font size
// 3. Minimal vertical spacing
// 4. Left-aligned groups for quick scanning
// 5. Condensed page references
//
#let theme-compact = (
// Renders the main glossary section with minimal spacing
// Parameters:
// title: The glossary section title
// body: Content containing all groups and entries
section: (title, body) => {
set par(leading: 0.65em)
heading(level: 1, outlined: false, text(weight: "bold", size: 1.1em, title))
body
},
// Renders a group of related glossary terms
// Parameters:
// name: Group name (empty string for ungrouped terms)
// index: Zero-based group index
// total: Total number of groups
// body: Content containing the group's entries
group: (name, index, total, body) => {
if name != "" and total > 1 {
block(
spacing: 0.5em,
pad(
top: 0.5em,
text(weight: "bold", size: 0.9em, name)
)
)
}
body
},
// Renders a single glossary entry with term, definition, and page references
// Parameters:
// entry: Dictionary containing term data:
// - short: Short form of term
// - long: Long form of term (optional)
// - description: Term description (optional)
// - label: Term's dictionary label
// - pages: Linked page numbers where term appears
// index: Zero-based entry index within group
// total: Total entries in group
entry: (entry, index, total) => {
// Format term components with minimal spacing
let term = text(
size: 0.65em,
weight: "medium",
fill: gray.darken(60%),
entry.short
)
let long-form = if entry.long == none {
[]
} else {
text(size: 0.65em, fill: gray.darken(20%), [ (#entry.long)])
}
let description = if entry.description == none {
[]
} else {
text(size: 0.65em, [· #entry.description])
}
// Create the complete entry with tight spacing
block(
spacing: 0.4em,
grid(
columns: (auto, 1fr, auto),
align: left+bottom,
gutter: 0.5em,
// Term and description column
box[#term#entry.label#long-form #description],
// Dots....
repeat(h(0.25em) + text(fill: gray, ".") + h(0.25em)),
// Page references with smaller font
text(
size: 0.6em,
fill: gray.darken(20%),
entry.pages
)
)
)
},
)
@@ -1,72 +0,0 @@
// Theme definition for a two-column glossary layout with hierarchical sections
//
// This theme provides formatting for three levels of glossary content:
// 1. Section: The overall glossary container
// 2. Groups: Optional categorization of related terms
// 3. Entries: Individual glossary terms and their definitions
//
#let theme-twocol = (
// Renders the main glossary section as a two-column layout
// Parameters:
// title: The glossary section title
// body: Content containing all groups and entries
section: (title, body) => {
set par.line(numbering: none)
heading(level: 1, title)
columns(2, body)
},
// Renders a group of related glossary terms
// Parameters:
// name: Group name (empty string for ungrouped terms)
// index: Zero-based group index
// total: Total number of groups
// body: Content containing the group's entries
group: (name, index, total, body) => {
if name != "" and total > 1 {
heading(level: 2, name)
}
body
},
// Renders a single glossary entry with term, definition, and page references
// Parameters:
// entry: Dictionary containing term data:
// - short: Short form of term
// - long: Long form of term (optional)
// - description: Term description (optional)
// - label: Term's dictionary label
// - pages: Linked page numbers where term appears
// index: Zero-based entry index within group
// total: Total entries in group
entry: (entry, index, total) => {
// Format the short form and optional long form of the term
let short-display = text(weight: "regular", entry.short)
let long-display = if entry.long == none {
[]
} else {
[#h(0.25em) -- #entry.long]
}
// Format the optional description
let description = if entry.description == none {
[]
} else {
[: #entry.description]
}
// Render the complete entry with dotted leader line to page numbers
text(
size: 0.75em,
weight: "light",
grid(
columns: (auto,1fr,1em,auto),
align: (left, center, center, right),
[#short-display#entry.label#long-display#description], // Term with label
[#repeat(h(0.25em) + "." + h(0.25em))], // Dotted leader line
[ . ], // A 1em wide dot so we definitely get some break between term and pages
[#entry.pages] // Page references
)
)
},
)
-38
View File
@@ -1,38 +0,0 @@
// Returns the provided value if it is not none, otherwise returns the default value
// Parameters:
// val: The value to check
// default: The fallback value to use if val is none
// Returns: val if not none, otherwise default
#let __default(val, default) = if val == none { default } else { val }
// Converts an English word to its plural form following standard English pluralization rules
// Parameters:
// word: The singular word to pluralize
// Returns: The pluralized form of the word, or none if input is none
#let __pluralize(word) = {
// early exit with invalid input
if word == none {
return none
}
// Define helper functions for checking endings
let ends_with = (suffix) => lower(word).ends-with(suffix)
let ends_with_any = (suffixes) => suffixes.any((suffix) => ends_with(suffix))
// Handle special cases for "es" endings (s, x, z, sh, ch)
if ends_with_any(( "s", "x", "z", "sh", "ch" )) {
word + "es"
} else if ends_with("y") and not ends_with_any(( "a", "e", "i", "o", "u" )) {
// Convert y to ies when y follows a consonant
word.slice(0, -1) + "ies"
} else if ends_with("f") {
// Convert f to ves
word.slice(0, -1) + "ves"
} else if ends_with("fe") {
// Convert fe to ves
word.slice(0, -2) + "ves"
} else {
// Default case: just add s
word + "s"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB

-99
View File
@@ -1,99 +0,0 @@
// a minimal example, also listed in the README
#import "/lib.typ": *
#let myGlossary = (
html: (
short: "HTML",
long: "Hypertext Markup Language",
description: "A standard language for creating web pages",
group: "Web"),
css: (
short: "CSS",
long: "Cascading Style Sheets",
description: "A language used for describing the presentation of a document",
group: "Web"),
tps: (
short: "TPS",
long: "test procedure specification",
description: "A document on how to run all the test procedures"),
)
#show: init-glossary.with(myGlossary, show-term: (body) => [#emph(body)])
#set heading(numbering: "1.1")
= Hello, `glossy`!
In modern web development, languages like @html and @css are essential.
Now make sure I get your @tps:short reports by 2pm!
@tps:cap is defined as: @tps:def.
= Examples with modifiers
#table(
columns: 2,
table.header([*Input*], [*Output*]),
[`@tps:short` ], [@tps:short],
[`@tps:long` ], [@tps:long],
[`@tps:both` ], [@tps:both],
[`@tps:long:cap` ], [@tps:long:cap],
[`@tps:long:pl` ], [@tps:long:pl],
[`@tps:short:pl` ], [@tps:short:pl],
[`@tps:both:pl:cap`], [@tps:both:pl:cap],
[`@tps:def` ], [@tps:def],
[`@tps:desc` ], [@tps:desc],
)
== Using conflicting modifiers (short, long, both)
These modifiers are semantically mutually exclusive, by they can be combined
syntactically. When multiple are used, `glossy` tries to make a smart choice on
which one to display.
#table(
columns: 2,
table.header([*Input*], [*Output*]),
[`@tps:short:long` ], [@tps:short:long],
[`@tps:short:both` ], [@tps:short:both],
[`@tps:long:both` ], [@tps:long:both],
[`@tps:short:long:both`], [@tps:short:long:both],
)
#let my-theme = (
section: (title, body) => {
heading(level: 1, title)
body
},
group: (name, i, n, body) => {
if name != "" and n > 1 {
heading(level: 2, name)
}
body
},
entry: (entry, i, n) => {
// i is the entry's index, n is total number of entries
let output = [#entry.short]
if entry.long != none {
output = [#output -- #entry.long]
}
if entry.description != none {
output = [#output: #entry.description]
}
block(
grid(
columns: (auto, 1fr, auto),
output,
repeat([#h(0.25em) . #h(0.25em)]),
entry.pages,
)
)
}
)
#glossary(theme: my-theme)
#glossary(title: "Glossary with just empty group", groups: ("",), theme: theme-basic)
#glossary(title: "Glossary with just Web group", groups: ("Web",), theme: theme-compact)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

@@ -1,38 +0,0 @@
#import "/lib.typ": *
#let myGlossary = (
html: (
short: "HTML",
long: "Hypertext Markup Language",
description: "A standard language for creating web pages",
),
iphone: (
short: "iPhone",
long: "iPhone",
),
css: (
short: "CSS",
long: "Cascading Style Sheets",
description: "A language used for describing the presentation of a document",
),
atom: (
short: "atom",
long: "atom",
),
tps: (
short: "TPS",
long: "test procedure specification",
description: "A document on how to run all the test procedures"
),
)
#show: init-glossary.with(myGlossary, show-term: (body) => [#emph(body)])
= Refer to our terms so they show up in the glossary
@html, @iphone:long, @css, @css:cap:pl, @atom:pl, @tps:long
= Default sorting
#glossary(theme: theme-compact)
= Case-insensitive sorting (`ignore-case: true`)
#glossary(ignore-case: true, theme: theme-compact)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 521 KiB

@@ -1,54 +0,0 @@
#import "/lib.typ": *
#let myGlossary = (
html: (
short: "HTML",
long: "Hypertext Markup Language",
description: "A standard language for creating web pages",
),
iphone: (
short: "iPhone",
description: "Revolutionized phones."
),
css: (
short: "CSS",
long: "Cascading Style Sheets",
description: "A language used for describing the presentation of a document",
),
atom: (
short: "atom",
),
tps: (
short: "TPS",
long: "test procedure specification",
description: "A document on how to run all the test procedures"
),
)
#show: init-glossary.with(myGlossary, show-term: (body) => [#emph(body)])
#set page(
margin: 1cm,
height: auto,
)
#text(size: 24pt, weight: "bold")[`glossy`: theme gallery]
#text(size: 1pt, fill: rgb(0,0,0,0))[
First, we refer to each term at least once so they'll actually show up in the
glossaries. Our terms include: @atom, @iphone, @html:cap, @css, and @tps.
]
#v(-2em)
#import "/src/themes.typ"
#for theme in dictionary(themes) {
block(
breakable: false,
width: 100%,
spacing: 2em,
inset: 1em,
stroke: 1pt+gray,
height: if theme.first() == "theme-twocol" { 1.5in } else { auto },
glossary(title: raw(theme.first()), theme: theme.last(), ignore-case: true)
)
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

-11
View File
@@ -1,11 +0,0 @@
[package]
name = "glossy"
version = "0.2.1"
entrypoint = "lib.typ"
authors = ["Stephen Waits <steve@waits.net>"]
license = "MIT"
description = "A very simple glossary system with easily customizable output."
categories = ["model"]
keywords = ["glossary", "index", "dictionary", "gloss"]
repository = "https://github.com/swaits/typst-collection"
exclude = ["thumbnail.png", "themeshots.pjng", "tests"]
-2
View File
@@ -1,2 +0,0 @@
tests/**/out/
tests/**/diff/
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Stephen Waits <steve@waits.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-239
View File
@@ -1,239 +0,0 @@
# Glossy
This package provides utilities to manage and render glossaries within
documents. It includes functions to define and use glossary terms, track their
usage, and generate a glossary list with references to where terms are used in
the document.
![image of sample output](thumbnail.png)
## Motivation
Glossy is heavily inspired by
[glossarium](https://typst.app/universe/package/glossarium), with a few key
different goals:
1. Provide a simple interface which allows for complete control over glossary
display. To do this, `glossy`'s `#glossary()` function accepts a theme
parameter. The goal here is to separate presentation and logic.
2. Simplify the user interface as much as possible. Glossy has exactly two
exports, `init-glossary` and `glossary`.
3. Double-down on `glossy`'s excellent `@term` reference approach, completely
eliminating the need to make any calls to `gls()` and friends.
4. Mimic established patterns and best practices. For example, `glossy`'s
`#glossary()` function is intentionally similar (in naming and parameters) to
`typst`'s built-in `#bibliography`, to the degree possible.
5. Simplify the implementation. The `glossy` code is significantly shorter and
easier to understand.
## Features
- Define glossary terms with short and long forms, descriptions, and grouping
- Automatically tracks term usage in the document through `@labels`
- Supports modifiers to adjust how terms are displayed (capitalize, pluralize, etc.)
- Generates a formatted glossary section with backlinks to term occurrences
- Customizable themes for rendering glossary sections, groups, and entries
- Automatic pluralization of terms with custom override options
- Page number references back to term usage locations
## Usage
### Import the package
```typst
#import "@preview/glossy:0.2.2": *
```
### Defining Glossary Terms
Use the `init-glossary` function to initialize glossary entries:
```typst
#let myGlossary = (
html: (
short: "HTML",
long: "Hypertext Markup Language",
description: "A standard language for creating web pages",
group: "Web"
),
css: (
short: "CSS",
long: "Cascading Style Sheets",
description: "A stylesheet language used for describing the presentation of a document",
group: "Web"
),
tps: (
short: "TPS",
long: "test procedure specification",
description: "A formal document describing test steps and expected results",
// Optional: Override automatic pluralization
plural: "TPSes",
longplural: "test procedure specifications"
)
)
#show: init-glossary.with(myGlossary)
```
Each glossary entry supports the following fields:
- `short` (required): Short form of the term
- `long` (optional): Long form of the term
- `description` (optional): Term description (often a definition)
- `group` (optional): Category grouping
- `plural` (optional): Override automatic pluralization of short form
- `longplural` (optional): Override automatic pluralization of long form
You can also load glossary entries from a data file using #yaml(), #json(), or similar.
For example, the above glossary could be in this YAML file:
```yaml
html:
short: HTML
long: Hypertext Markup Language
description: A standard language for creating web pages
group: Web
css:
short: CSS
long: Cascading Style Sheets
description: A stylesheet language used for describing the presentation of a document
group: Web
tps:
short: TPS
long: test procedure specification
description: A formal document describing test steps and expected results
plural: TPSes
longplural: test procedure specifications
```
And then loaded during initialization as follows:
```typst
#show: init-glossary.with(yaml("glossary.yaml"))
```
### Using Glossary Terms
Reference glossary terms using Typst's `@reference` syntax:
```typst
In modern web development, languages like @html and @css are essential.
The @tps:pl need to be submitted by Friday.
```
Available modifiers:
- **cap**: Capitalizes the term
- **pl**: Uses the plural form
- **both**: Shows "Long Form (Short Form)"
- **short**: Shows only short form
- **long**: Shows only long form
- **def** or **desc**: Shows the description
Modifiers can be combined with colons:
| **Input** | **Output** |
| ------------------- | -------------------------------------------------------------- |
| `@tps` (first use) | "test procedure specification (TPS)" |
| `@tps` (subsequent) | "TPS" |
| `@tps:short` | "TPS" |
| `@tps:long` | "test procedure specification" |
| `@tps:both` | "test procedure specification (TPS)" |
| `@tps:long:cap` | "Test procedure specification" |
| `@tps:long:pl` | "test procedure specifications" |
| `@tps:short:pl` | "TPSes" |
| `@tps:both:pl:cap` | "Technical procedure specifications (TPSes)" |
| `@tps:def` | "A formal document describing test steps and expected results" |
### Generating the Glossary
Display the glossary using the `glossary()` function:
```typst
#glossary(
title: "Web Development Glossary", // Optional: defaults to Glossary
theme: my-theme, // Optionaal: defaults to theme-twocol
ignore-case: false, // Optional: ignore case when sorting terms
groups: ("Web") // Optional: Filter to specific groups
)
```
### Customizing Term Display
Control how terms appear in the document by providing a custom `show-term` function:
```typst
#let emph-term(term-body) = { emph(term-body) }
#show: init-glossary.with(
myGlossary,
show-term: emph-term
)
```
### Glossary Themes
#### Included Themes
Glossy comes with several built-in themes that can be used directly or serve as
examples for custom themes:
![theme gallery image](themeshots.png)
#### Custom Themes
Customize glossary appearance by defining a theme with three functions:
```typst
#let my-theme = (
// Main glossary section
section: (title, body) => {
heading(level: 1, title)
body
},
// Group of related terms
group: (name, index, total, body) => {
// index = group index, total = total groups
if name != "" and total > 1 {
heading(level: 2, name)
}
body
},
// Individual glossary entry
entry: (entry, index, total) => {
// index = entry index, total = total entries in group
let output = [#entry.short]
if entry.long != none {
output = [#output -- #entry.long]
}
if entry.description != none {
output = [#output: #entry.description]
}
block(
grid(
columns: (auto, 1fr, auto),
output,
repeat([#h(0.25em) . #h(0.25em)]),
entry.pages,
)
)
}
)
```
Entry fields available to themes:
- `short`: Short form (always present)
- `long`: Long form (can be `none`)
- `description`: Term description (can be `none`)
- `label`: Term's dictionary label
- `pages`: Linked page numbers where term appears
## License
This project is licensed under the MIT License.
-2
View File
@@ -1,2 +0,0 @@
#import "src/gloss.typ": init-glossary, glossary
#import "src/themes.typ": *
-466
View File
@@ -1,466 +0,0 @@
#import "./themes.typ": *
#import "./utils.typ": *
#let __gloss_entries = state("__gloss_entries", (:))
#let __gloss_used = state("__gloss_used", (:))
#let __gloss_label_prefix = "__gloss:"
// Normalizes a dictionary entry by ensuring all required and optional keys exist
// with appropriate default values.
//
// Parameters:
// entry (dictionary): Input dictionary containing at least 'short' and optionally
// 'long', 'plural', 'longplural', 'description', and 'group'
//
// Returns:
// dictionary: Normalized dictionary with all expected keys populated
//
// Throws:
// - If 'short' key is missing
// - If 'short' value is not a string
// - If 'short' value is an empty string
//
#let __normalize_entry(entry) = {
// Validate required fields
if not "short" in entry {
panic("Entry must contain a 'short' key")
}
if type(entry.short) != "string" {
panic("Entry 'short' must be a string")
}
if entry.short.trim() == "" {
panic("Entry 'short' cannot be empty")
}
// Extract values with defaults, using consistent none for missing optionals
let long = entry.at("long", default: none)
// Return the normalized entry
(
short: entry.short,
plural: entry.at("plural", default: __pluralize(entry.short)),
long: long,
longplural: entry.at("longplural", default: __pluralize(long)),
description: entry.at("description", default: none),
group: entry.at("group", default: "")
)
}
// Checks if a key exists in the glossary state.
//
// Parameters:
// key (string): The key to look up in the glossary entries
//
// Returns:
// boolean: True if the key exists in the glossary, false otherwise
//
#let __has_entry(key) = {
key in __gloss_entries.final().keys()
}
// Updates the glossary state by adding or updating an entry.
//
// Parameters:
// key (string): The key under which to store the entry
// entry (dictionary): The normalized glossary entry to store
//
// Returns:
// none: Updates state as a side effect
//
#let __add_entry(key, entry) = {
__gloss_entries.update(state => {
state.insert(key, entry)
state
})
}
// Retrieves a glossary entry from the state.
//
// Parameters:
// key (string): The key of the entry to retrieve
//
// Returns:
// dictionary: The glossary entry associated with the key
//
// Throws:
// - If the key does not exist in the glossary
//
#let __get_entry(key) = {
let entries = __gloss_entries.final()
if key not in entries.keys() {
panic("Glossary error: Missing key '" + key + "'")
}
entries.at(key)
}
// Creates a prefixed label string for dictionary entries and term usage.
//
// Parameters:
// key (string): The glossary entry key
//
// Returns:
// string: The prefixed label string for dictionary entries
//
#let __dict_label_str(key) = {
__gloss_label_prefix + key
}
// Creates a label object for dictionary entries.
//
// Parameters:
// key (string): The glossary entry key
//
// Returns:
// label: A Typst label object for the dictionary entry
//
#let __dict_label(key) = {
label(__dict_label_str(key))
}
// Creates a prefixed label string for term usage in documents.
//
// Parameters:
// key (string): The glossary entry key
// index (int): The occurrence index of the term in the document
//
// Returns:
// string: The prefixed label string for term usage
//
#let __term_label_str(key, index) = {
__gloss_label_prefix + key + "." + str(index)
}
// Creates a label object for term usage in documents.
//
// Parameters:
// key (string): The glossary entry key
// index (int): The occurrence index of the term in the document
//
// Returns:
// label: A Typst label object for the term usage
//
#let __term_label(key, index) = {
label(__term_label_str(key, index))
}
// Updates the term usage count in the glossary state.
//
// Parameters:
// key (string): The glossary entry key
// count (int): The number of times the term has been used
//
// Returns:
// none: Updates state as a side effect
//
#let __save_term_usage(key, count) = {
__gloss_used.update(state => {
state.insert(key, count)
state
})
}
// Renders a glossary term with various formatting options.
//
// This function handles the display of terms in the document, managing both first
// and subsequent uses, pluralization, capitalization, and different display forms
// (short, long, or combined forms).
//
// Parameters:
// key (string): The glossary entry key to render
// modifiers (array): Array of modifier strings that control term rendering:
// - "cap": Capitalize the first letter of the term
// - "pl": Use plural form of the term
// - "both": Show "Long form (short form)"
// - "short": Show only the short form
// - "long": Show only the long form
// - "def" or "desc": Show the term's description instead
//
// Returns:
// content: Formatted term with appropriate labels and metadata
//
// Behavior:
// - Without modifiers, first use shows "Long form (short form)", subsequent uses show short form
// - Explicit modifiers override the default first-use behavior
// - If long form is requested but unavailable, falls back to short form
// - Maintains usage counter for term references
//
#let __gls(key, modifiers: array) = {
// Fetch the entry and prepare label
let entry = __get_entry(key)
let entry_label = __dict_label_str(key)
// Handle description mode
if "def" in modifiers or "desc" in modifiers {
return [#entry.description]
}
// Track term usage
let entry_counter = counter(entry_label)
entry_counter.step()
let key_index = entry_counter.get().first()
let first = key_index == 0
__save_term_usage(key, key_index + 1)
// Helper: Apply pluralization if requested
let pluralize_term = (singular, plural) => {
if "pl" in modifiers and plural != none { plural } else { singular }
}
// Helper: Apply capitalization if requested
let capitalize_term = (term) => {
if "cap" in modifiers { upper(term.first()) + term.slice(1) } else { term }
}
// Helper: Apply both pluralization and capitalization
let format_term = (term, plural) => {
capitalize_term(pluralize_term(term, plural))
}
// Helper: Select and format the appropriate term form
let select_term = (is_long_mode, use_both) => {
let short_form = format_term(entry.short, entry.plural)
let long_form = if entry.long != none {
format_term(entry.long, entry.longplural)
} else {
none
}
if use_both and long_form != none {
[#long_form (#short_form)]
} else if is_long_mode and long_form != none {
[#long_form]
} else {
[#short_form]
}
}
// Determine display mode from modifiers
let is_both = "both" in modifiers
let is_long = "long" in modifiers and not is_both
let is_short = "short" in modifiers and not is_both and not is_long
context {
// Generate final display based on modifiers or default behavior
let display = if is_both or is_long or is_short {
select_term(is_long, is_both)
} else {
select_term(false, first)
}
// Emit term with metadata and usage label
[#display#metadata(display)#__term_label(key, key_index)]
}
}
// Creates page number links back to each usage of a glossary term.
//
// This function generates a comma-separated list of page numbers, where each
// number links back to a usage of the term in the document. Duplicate page
// numbers are removed to avoid redundancy.
//
// Parameters:
// key (string): The glossary entry key
// count (int): Number of times the term appears in the document
//
// Returns:
// content: Comma-separated list of linked page numbers
//
// Example output: "1, 3, 5" where each number links to the term usage on that page
//
#let __create_backlinks(key, count) = {
// Convert term (key + count) to labels with their displayed page numbers
let page_labels = range(count).map(i => {
let label = __term_label(key, i)
let loc = locate(label)
(
label,
numbering(__default(loc.page-numbering(), "1"), ..counter(page).at(loc))
)
})
// Filter out repeated page numbers using a regular loop
let seen = (:)
let singulated = ()
for (label, page) in page_labels {
if not seen.keys().contains(page) {
seen.insert(page, true)
singulated.push((label, page))
}
}
// Convert resulting set to array of links
singulated.map(((label, page)) => link(label, page)).join(", ")
}
// Initializes the glossary system and sets up term reference handling.
//
// This function is typically used with `#show: init-glossary`. It performs
// three main tasks:
// 1. Validates and loads glossary entries into the state
// 2. Sets up reference handling to intercept and format term usage
// 3. Applies custom term formatting if provided
//
// Parameters:
// entries (dictionary): Dictionary of glossary entries where:
// - keys are term identifiers
// - values are entry dictionaries containing term details
// show-term ((content) => content): Optional function to customize term rendering
// Default: Returns term content unchanged
// body (content): Document content to process
//
// Returns:
// content: Processed document content with glossary functionality enabled
//
// Throws:
// - If entries parameter is not a dictionary
//
#let init-glossary(
entries,
show-term: (term-body) => term-body,
body
) = context {
// Validate entries parameter type
if type(entries) != dictionary {
panic("entries must be a dictionary, instead got: " + repr(entries))
}
// Process and store each glossary entry
for (key, entry) in entries {
__add_entry(key, __normalize_entry(entry))
}
// Set up reference handling for glossary terms
show ref: r => {
let (key, ..modifiers) = str(r.target).split(":")
if __has_entry(key) {
show-term(__gls(key, modifiers: modifiers))
} else {
r
}
}
body
}
// Renders a complete glossary with customizable formatting and grouping.
//
// The glossary displays all used terms, optionally grouped by category, with page
// references back to term usage. The appearance is controlled by a theme that
// defines how sections, groups, and entries are formatted.
//
// Parameters:
// title (string): Glossary section title (default: "Glossary")
// theme (dictionary): Controls glossary appearance with three functions:
// - section(title, body): Renders the main glossary section
// - title: The glossary title
// - body: Content containing all groups and entries
// - group(name, index, total, body): Renders a group of related terms
// - name: Group name
// - index: Zero-based group index
// - total: Total number of groups
// - body: Content containing the group's entries
// - entry(entry, index, total): Renders a single glossary entry
// - entry: Dictionary containing:
// - short: Short form of term
// - long: Long form of term (optional)
// - description: Term description (optional)
// - label: Term's dictionary label
// - pages: Linked page numbers where term appears
// - index: Zero-based entry index within group
// - total: Total entries in group
// groups (array): Optional list of groups to include
// If empty, includes all groups
//
// Returns:
// content: Formatted glossary content
//
// Throws:
// - If a requested group doesn't exist
//
#let glossary(
title: "Glossary",
theme: theme-twocol,
ignore-case: false,
groups: (),
) = context {
// Collect and organize entries by group
let output = (:)
let all_entries = __gloss_entries.final()
let all_used = __gloss_used.final()
// Determine which groups to process
let all_groups = all_entries
.values()
.map(e => e.at("group"))
.dedup()
.sorted()
let target_groups = if groups.len() == 0 {
all_groups
} else {
// Validate requested groups exist
for g in groups {
if g not in all_groups {
panic("Requested group not found: " + g)
}
}
groups
}
// Process entries group by group
for group in target_groups {
let current_entries = ()
// Collect all used entries for this group
for (key, count) in all_used {
let entry = all_entries.at(key)
if entry.at("group") == group {
current_entries.push((
short: entry.at("short"),
long: entry.at("long"),
description: entry.at("description"),
label: [#metadata(key)#__dict_label(key)],
pages: __create_backlinks(key, count)
))
}
}
// Add non-empty groups to output
if current_entries.len() > 0 {
group = if group == none { "" } else { group }
// sort entries by case insensitivity if requested
let sorted_entries = current_entries
// 1. create array of tuples with (lower [if ignore-case], entry)
.map(e => { if ignore-case { (lower(e.short), e) } else { (e.short, e) } })
// 2. sort the tuples (by first element then second)
.sorted(key: t => t.first())
// 3. strip away the tuple's first element, leaving an array of entries
.map(t => t.last())
// add entries to this group's output map
output.insert(group, sorted_entries)
}
}
// Render the glossary using the theme
let group_index = 0
(theme.section)(
title,
for (group, entries) in output {
(theme.group)(
group,
group_index,
output.len(),
for (i, entry) in entries.enumerate() {
(theme.entry)(entry, i, entries.len())
}
)
group_index += 1
}
)
}
-6
View File
@@ -1,6 +0,0 @@
#import "themes/academic.typ": theme-academic
#import "themes/basic.typ": theme-basic
#import "themes/chicago-index.typ": theme-chicago-index
#import "themes/compact.typ": theme-compact
#import "themes/table.typ": theme-table
#import "themes/twocol.typ": theme-twocol
@@ -1,50 +0,0 @@
// Inspired by academic textbook glossaries
#let theme-academic = (
section: (title, body) => {
set text(font: "New Computer Modern")
heading(level: 1, title)
v(1em)
body
},
group: (name, index, total, body) => {
if name != "" and total > 1 {
v(1.5em)
align(left, text(weight: "bold", size: 1.2em, name))
v(0.75em)
line(length: 100%, stroke: 0.5pt)
v(0.75em)
}
body
},
entry: (entry, index, total) => {
let short-display = text(weight: "bold", entry.short)
let long-display = if entry.long == none {
[]
} else {
[. #entry.long]
}
let description = if entry.description == none {
[]
} else {
[. #entry.description]
}
block(
below: 1em,
text(
size: 0.95em,
{
grid(
columns: (1fr, auto),
gutter: 0.75em,
[#short-display#long-display#description],
text(fill: rgb("#666666"), entry.pages)
)
}
)
)
},
)
-70
View File
@@ -1,70 +0,0 @@
// Theme definition for a basic single-column glossary layout
//
// This theme provides a traditional glossary format with:
// 1. Bold terms
// 2. Indented descriptions
// 3. Simple page references
// 4. Optional group headings
//
#let theme-basic = (
// Renders the main glossary section as a single column
// Parameters:
// title: The glossary section title
// body: Content containing all groups and entries
section: (title, body) => {
heading(level: 1, title)
body
},
// Renders a group of related glossary terms
// Parameters:
// name: Group name (empty string for ungrouped terms)
// index: Zero-based group index
// total: Total number of groups
// body: Content containing the group's entries
group: (name, index, total, body) => {
if name != "" and total > 1 {
heading(level: 2, name)
}
body
},
// Renders a single glossary entry with term, definition, and page references
// Parameters:
// entry: Dictionary containing term data:
// - short: Short form of term
// - long: Long form of term (optional)
// - description: Term description (optional)
// - label: Term's dictionary label
// - pages: Linked page numbers where term appears
// index: Zero-based entry index within group
// total: Total entries in group
entry: (entry, index, total) => {
// Format the term parts
let term = text(weight: "bold", entry.short)
let long-form = if entry.long == none {
[]
} else {
[, #entry.long]
}
// Format the description with proper spacing
let description = if entry.description == none {
[]
} else {
[: #entry.description]
}
// Create the complete entry with hanging indent
block(
spacing: 0.5em,
pad(
left: 1em,
bottom: 0.5em,
block(
[#term#entry.label#long-form#description #h(1em) (pp. #entry.pages)]
)
)
)
},
)
@@ -1,47 +0,0 @@
// Inspired by the Chicago Manual of Style's index format
#let theme-chicago-index = (
section: (title, body) => {
set par(hanging-indent: 1em)
heading(level: 1, smallcaps(title))
body
},
group: (name, index, total, body) => {
if name != "" and total > 1 {
v(1em)
text(weight: "medium", style: "italic", name)
v(0.5em)
}
body
},
entry: (entry, index, total) => {
let short-display = text(weight: "regular", entry.short)
let long-display = if entry.long == none {
[]
} else {
[ — #entry.long] // Using em-dash for Chicago style
}
let description = if entry.description == none {
[]
} else {
text(style: "italic", [: #entry.description])
}
block(
below: 0.65em,
text(
size: 0.9em,
{
grid(
columns: (1fr, auto),
gutter: 1em,
[#short-display#entry.label#long-display#description],
[#entry.pages]
)
}
)
)
},
)
@@ -1,91 +0,0 @@
// Theme definition for a compact glossary layout optimized for space efficiency
//
// This theme provides a dense but readable format with:
// 1. Terms and definitions on same line
// 2. Smaller font size
// 3. Minimal vertical spacing
// 4. Left-aligned groups for quick scanning
// 5. Condensed page references
//
#let theme-compact = (
// Renders the main glossary section with minimal spacing
// Parameters:
// title: The glossary section title
// body: Content containing all groups and entries
section: (title, body) => {
set par(leading: 0.65em)
heading(level: 1, outlined: false, text(weight: "bold", size: 1.1em, title))
body
},
// Renders a group of related glossary terms
// Parameters:
// name: Group name (empty string for ungrouped terms)
// index: Zero-based group index
// total: Total number of groups
// body: Content containing the group's entries
group: (name, index, total, body) => {
if name != "" and total > 1 {
block(
spacing: 0.5em,
pad(
top: 0.5em,
text(weight: "bold", size: 0.9em, name)
)
)
}
body
},
// Renders a single glossary entry with term, definition, and page references
// Parameters:
// entry: Dictionary containing term data:
// - short: Short form of term
// - long: Long form of term (optional)
// - description: Term description (optional)
// - label: Term's dictionary label
// - pages: Linked page numbers where term appears
// index: Zero-based entry index within group
// total: Total entries in group
entry: (entry, index, total) => {
// Format term components with minimal spacing
let term = text(
size: 0.65em,
weight: "medium",
fill: gray.darken(60%),
entry.short
)
let long-form = if entry.long == none {
[]
} else {
text(size: 0.65em, fill: gray.darken(20%), [ (#entry.long)])
}
let description = if entry.description == none {
[]
} else {
text(size: 0.65em, [· #entry.description])
}
// Create the complete entry with tight spacing
block(
spacing: 0.4em,
grid(
columns: (auto, 1fr, auto),
align: left+bottom,
gutter: 0.5em,
// Term and description column
box[#term#entry.label#long-form #description],
// Dots....
repeat(h(0.25em) + text(fill: gray, ".") + h(0.25em)),
// Page references with smaller font
text(
size: 0.6em,
fill: gray.darken(20%),
entry.pages
)
)
)
},
)
-32
View File
@@ -1,32 +0,0 @@
// A tabular theme courtesy of [@Fevol](https://github.com/Fevol)
#let theme-table = (
section: (title, body) => {
heading(level: 1, title)
body
},
group: (name, index, total, body) => {
if name != "" and total > 1 {
heading(level: 2, name)
}
table(columns: 4,
stroke: none,
inset: (x, y) => {
if (x == 0) {
(left: 0pt, rest: 5pt)
} else if (x == 3) {
(right: 0pt, rest: 5pt)
} else {
5pt
}
},
table.header([*Abbreviation*], [*Full Name*], [*Description*], [*Pages*]),
..body
)
},
entry: (entry, index, total) => {
(entry.short, entry.long, entry.description, entry.pages)
}
)
@@ -1,72 +0,0 @@
// Theme definition for a two-column glossary layout with hierarchical sections
//
// This theme provides formatting for three levels of glossary content:
// 1. Section: The overall glossary container
// 2. Groups: Optional categorization of related terms
// 3. Entries: Individual glossary terms and their definitions
//
#let theme-twocol = (
// Renders the main glossary section as a two-column layout
// Parameters:
// title: The glossary section title
// body: Content containing all groups and entries
section: (title, body) => {
set par.line(numbering: none)
heading(level: 1, title)
columns(2, body)
},
// Renders a group of related glossary terms
// Parameters:
// name: Group name (empty string for ungrouped terms)
// index: Zero-based group index
// total: Total number of groups
// body: Content containing the group's entries
group: (name, index, total, body) => {
if name != "" and total > 1 {
heading(level: 2, name)
}
body
},
// Renders a single glossary entry with term, definition, and page references
// Parameters:
// entry: Dictionary containing term data:
// - short: Short form of term
// - long: Long form of term (optional)
// - description: Term description (optional)
// - label: Term's dictionary label
// - pages: Linked page numbers where term appears
// index: Zero-based entry index within group
// total: Total entries in group
entry: (entry, index, total) => {
// Format the short form and optional long form of the term
let short-display = text(weight: "regular", entry.short)
let long-display = if entry.long == none {
[]
} else {
[#h(0.25em) -- #entry.long]
}
// Format the optional description
let description = if entry.description == none {
[]
} else {
[: #entry.description]
}
// Render the complete entry with dotted leader line to page numbers
text(
size: 0.75em,
weight: "light",
grid(
columns: (auto,1fr,1em,auto),
align: (left, center, center, right),
[#short-display#entry.label#long-display#description], // Term with label
[#repeat(h(0.25em) + "." + h(0.25em))], // Dotted leader line
[ . ], // A 1em wide dot so we definitely get some break between term and pages
[#entry.pages] // Page references
)
)
},
)
-38
View File
@@ -1,38 +0,0 @@
// Returns the provided value if it is not none, otherwise returns the default value
// Parameters:
// val: The value to check
// default: The fallback value to use if val is none
// Returns: val if not none, otherwise default
#let __default(val, default) = if val == none { default } else { val }
// Converts an English word to its plural form following standard English pluralization rules
// Parameters:
// word: The singular word to pluralize
// Returns: The pluralized form of the word, or none if input is none
#let __pluralize(word) = {
// early exit with invalid input
if word == none {
return none
}
// Define helper functions for checking endings
let ends_with = (suffix) => lower(word).ends-with(suffix)
let ends_with_any = (suffixes) => suffixes.any((suffix) => ends_with(suffix))
// Handle special cases for "es" endings (s, x, z, sh, ch)
if ends_with_any(( "s", "x", "z", "sh", "ch" )) {
word + "es"
} else if ends_with("y") and not ends_with_any(( "a", "e", "i", "o", "u" )) {
// Convert y to ies when y follows a consonant
word.slice(0, -1) + "ies"
} else if ends_with("f") {
// Convert f to ves
word.slice(0, -1) + "ves"
} else if ends_with("fe") {
// Convert fe to ves
word.slice(0, -2) + "ves"
} else {
// Default case: just add s
word + "s"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB

-100
View File
@@ -1,100 +0,0 @@
// a minimal example, also listed in the README
#import "/lib.typ": *
#let myGlossary = (
html: (
short: "HTML",
long: "Hypertext Markup Language",
description: "A standard language for creating web pages",
group: "Web"),
css: (
short: "CSS",
long: "Cascading Style Sheets",
description: "A language used for describing the presentation of a document",
group: "Web"),
tps: (
short: "TPS",
long: "test procedure specification",
description: "A document on how to run all the test procedures"),
)
#show: init-glossary.with(myGlossary, show-term: (body) => [#emph(body)])
#set heading(numbering: "1.1")
#set page(numbering: "1")
= Hello, `glossy`!
In modern web development, languages like @html and @css are essential.
Now make sure I get your @tps:short reports by 2pm!
@tps:cap is defined as: @tps:def.
= Examples with modifiers
#table(
columns: 2,
table.header([*Input*], [*Output*]),
[`@tps:short` ], [@tps:short],
[`@tps:long` ], [@tps:long],
[`@tps:both` ], [@tps:both],
[`@tps:long:cap` ], [@tps:long:cap],
[`@tps:long:pl` ], [@tps:long:pl],
[`@tps:short:pl` ], [@tps:short:pl],
[`@tps:both:pl:cap`], [@tps:both:pl:cap],
[`@tps:def` ], [@tps:def],
[`@tps:desc` ], [@tps:desc],
)
== Using conflicting modifiers (short, long, both)
These modifiers are semantically mutually exclusive, by they can be combined
syntactically. When multiple are used, `glossy` tries to make a smart choice on
which one to display.
#table(
columns: 2,
table.header([*Input*], [*Output*]),
[`@tps:short:long` ], [@tps:short:long],
[`@tps:short:both` ], [@tps:short:both],
[`@tps:long:both` ], [@tps:long:both],
[`@tps:short:long:both`], [@tps:short:long:both],
)
#let my-theme = (
section: (title, body) => {
heading(level: 1, title)
body
},
group: (name, i, n, body) => {
if name != "" and n > 1 {
heading(level: 2, name)
}
body
},
entry: (entry, i, n) => {
// i is the entry's index, n is total number of entries
let output = [#entry.short]
if entry.long != none {
output = [#output -- #entry.long]
}
if entry.description != none {
output = [#output: #entry.description]
}
block(
grid(
columns: (auto, 1fr, auto),
output,
repeat([#h(0.25em) . #h(0.25em)]),
entry.pages,
)
)
}
)
#glossary(theme: my-theme)
#glossary(title: "Glossary with just empty group", groups: ("",), theme: theme-basic)
#glossary(title: "Glossary with just Web group", groups: ("Web",), theme: theme-compact)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.
@@ -1,51 +0,0 @@
#import "/lib.typ": *
#let myGlossary = (
html: (
short: "HTML",
long: "Hypertext Markup Language",
description: "A standard language for creating web pages",
),
iphone: (
short: "iPhone",
),
css: (
short: "CSS",
long: "Cascading Style Sheets",
description: "A language used for describing the presentation of a document",
),
atom: (
short: "atom",
),
tps: (
short: "TPS",
long: "test procedure specification",
description: "A document on how to run all the test procedures"
),
)
#show: init-glossary.with(myGlossary)
#set page(numbering: "i")
@html, @iphone, @css
#pagebreak()
#set page(numbering: "1")
@html, @iphone, @css, @atom
#pagebreak()
#counter(page).update(1)
#set page(numbering: "A")
@html, @iphone, @css, @tps
#pagebreak()
#set page(numbering: "I")
@css, @iphone, @tps
#glossary(theme: theme-chicago-index, ignore-case: true)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

@@ -1,40 +0,0 @@
#import "/lib.typ": *
#let myGlossary = (
html: (
short: "HTML",
long: "Hypertext Markup Language",
description: "A standard language for creating web pages",
),
iphone: (
short: "iPhone",
long: "iPhone",
),
css: (
short: "CSS",
long: "Cascading Style Sheets",
description: "A language used for describing the presentation of a document",
),
atom: (
short: "atom",
long: "atom",
),
tps: (
short: "TPS",
long: "test procedure specification",
description: "A document on how to run all the test procedures"
),
)
#show: init-glossary.with(myGlossary, show-term: (body) => [#emph(body)])
#set page(numbering: "1")
= Refer to our terms so they show up in the glossary
@html, @iphone:long, @css, @css:cap:pl, @atom:pl, @tps:long
= Default sorting
#glossary(theme: theme-compact)
= Case-insensitive sorting (`ignore-case: true`)
#glossary(ignore-case: true, theme: theme-compact)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 651 KiB

@@ -1,55 +0,0 @@
#import "/lib.typ": *
#let myGlossary = (
html: (
short: "HTML",
long: "Hypertext Markup Language",
description: "A standard language for creating web pages",
),
iphone: (
short: "iPhone",
description: "Revolutionized phones."
),
css: (
short: "CSS",
long: "Cascading Style Sheets",
description: "A language used for describing the presentation of a document",
),
atom: (
short: "atom",
),
tps: (
short: "TPS",
long: "test procedure specification",
description: "A document on how to run all the test procedures"
),
)
#show: init-glossary.with(myGlossary, show-term: (body) => [#emph(body)])
#set page(
margin: 1cm,
height: auto,
numbering: "1",
)
#text(size: 24pt, weight: "bold")[`glossy`: theme gallery]
#text(size: 1pt, fill: rgb(0,0,0,0))[
First, we refer to each term at least once so they'll actually show up in the
glossaries. Our terms include: @atom, @iphone, @html:cap, @css, and @tps.
]
#v(-2em)
#import "/src/themes.typ"
#for theme in dictionary(themes) {
block(
breakable: false,
width: 100%,
spacing: 2em,
inset: 1em,
stroke: 1pt+gray,
height: if theme.first() == "theme-twocol" { 1.5in } else { auto },
glossary(title: raw(theme.first()), theme: theme.last(), ignore-case: true)
)
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 651 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

-11
View File
@@ -1,11 +0,0 @@
[package]
name = "glossy"
version = "0.2.2"
entrypoint = "lib.typ"
authors = ["Stephen Waits <steve@waits.net>"]
license = "MIT"
description = "A very simple glossary system with easily customizable output."
categories = ["model"]
keywords = ["glossary", "index", "dictionary", "gloss"]
repository = "https://github.com/swaits/typst-collection"
exclude = ["thumbnail.png", "themeshots.pjng", "tests"]
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 990 KiB

-2
View File
@@ -1,2 +0,0 @@
tests/**/out/
tests/**/diff/
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more