Compare commits

...
14 Commits
Author SHA1 Message Date
swaits 93a7794c2e fix(glossy): fix forward linking (broken themes)
Fixes [#19](https://github.com/swaits/typst-collection/issues/19).

Themes must emit `entry.label` for terms to be able to forward-link to
their glossary entrie.
2025-01-07 22:54:43 -07:00
swaits 40cc652aeb test(glossy): add bin/autotest.sh (uses watchexec) 2025-01-07 22:54:43 -07:00
swaits 05a66f4956 test(glossy): repair utf-handling test 2024-12-22 13:40:13 -07:00
swaits df889d8d72 feat(glossy): add sort: true parameter to glossary() 2024-12-21 22:27:07 -07:00
swaits c008b5bd86 feat(glossy): release 0.5.1 2024-12-20 12:48:19 -07:00
swaits 842166c211 fix(glossy): use codepoints (not bytes!) in article code
Fixes [#18](https://github.com/swaits/typst-collection/issues/18)
2024-12-20 12:37:21 -07:00
swaits c37c6750c6 feat(glossy): release 0.5.0
New feature:

`init-glossary()` now accepts a `format-term()` function which gives the
user control over how the short-form and long-form of the terms are
assembled based on the needed mode ("short", "long", or "both").

Change:

Default theme changed to `theme-academic`, which is probably more
generally useful than the prior (`theme-twocol`).
2024-12-20 11:28:32 -07:00
swaits 03d82458bb feat(glossy): change default theme to theme-academic 2024-12-20 11:28:32 -07:00
swaits 725f657f49 feat(glossy): adds format-term customizability
Idea [from Discord
conversation](https://discord.com/channels/1054443721975922748/1318670789150642246/1319338465757757562).

Let's users more precisely control how short-form and long-form terms
are used based on the mode ("short", "long", or "both").

Defaults to `__default-format-term()` as below:

```typst
// Formats a term string based on the specified mode.
//
// Given mode, long-form, and short-form, this function returns the formatted
// term based on the mode. When mode is ...
//
// - "short" -> returns "short-form"
// - "long" -> returns "long-form"
// - "both" -> returns "short-form (long-form)"
//
// Parameters:
//   mode (string): The mode in which to format the term. Possible values are "short", "long", or "both".
//   short-form (string): The short form of the term.
//   long-form (string): The long form of the term.
//
// Returns:
//   string: The formatted term based on the specified mode.
//
#let __default-format-term(mode, short-form, long-form) = {
    if mode == "short" {
      short-form
    } else if mode == "long" {
      long-form
    } else {
      // mode assumed to be "both"
      long-form + " (" + short-form + ")"
    }
}
```
2024-12-20 11:28:32 -07:00
swaits cf139b66f6 chore(glossy): rm trailing whitespace 2024-12-20 11:28:31 -07:00
swaits 461439d2f9 chore: update .build.yml (missing cd into repo dir) 2024-12-19 10:54:12 -07:00
swaits 607814713a chore: update .build.yml (change $HOME for ~) 2024-12-19 10:52:13 -07:00
swaits 4d94a3f7f2 chore: update .build.yml (remove duplicate clone) 2024-12-19 10:48:11 -07:00
swaits bc011bfa6b chore: update .build.yml 2024-12-19 10:44:36 -07:00
23 changed files with 454 additions and 57 deletions

No files matched your search

+21 -7
View File
@@ -1,13 +1,27 @@
image: alpine/edge
packages:
- git
secrets:
- 88cce185-d2ec-4573-9546-e33f36f79bbf
sources:
- git+ssh://git@git.sr.ht/~swaits/typst-collection
environment:
# repo and user names on both sr.ht and github
REPO: typst-collection
USER: swaits
tasks:
- mirror-to-github: |
cd ~/typst-collection
ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts
# Explicitly fetch all branches & tags
- fetch-all-branches: |
cd "$HOME/$REPO"
git fetch origin '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' --prune
git remote add github git@github.com:swaits/typst-collection.git
- get-github-public-key: |
ssh-keyscan -t rsa github.com >> "$HOME/.ssh/known_hosts"
- add-github-remote: |
cd "$HOME/$REPO"
git remote add github "git@github.com:$USER/$REPO.git"
- mirror-to-github: |
cd "$HOME/$REPO"
git push --mirror github
+8 -4
View File
@@ -41,7 +41,7 @@ different goals:
### Import the package
```typst
#import "@preview/glossy:0.4.0": *
#import "@preview/glossy:0.5.1": *
```
### Defining Glossary Terms
@@ -186,8 +186,9 @@ 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
title: "Web Development Glossary", // Optional: defaults to Glossary theme:
my-theme, // Optional: defaults to theme-academic
sort: true, // Optional: whether or not to sort the glossary
ignore-case: false, // Optional: ignore case when sorting terms
groups: ("Web") // Optional: Filter to specific groups
)
@@ -252,7 +253,7 @@ Customize glossary appearance by defining a theme with three functions:
// Individual glossary entry
entry: (entry, index, total) => {
// index = entry index, total = total entries in group
let output = [#entry.short]
let output = [#entry.short#entry.label] // **NOTE:** Label here!
if entry.long != none {
output = [#output -- #entry.long]
}
@@ -279,6 +280,9 @@ Entry fields available to themes:
- `label`: Term's dictionary label
- `pages`: Linked page numbers where term appears
**NOTE:** If the theme does not emit `entry.label`, linking form terms to their
glossary entry will not work.
## License
This project is licensed under the MIT License.
+6
View File
@@ -0,0 +1,6 @@
watchexec \
--watch . \
--clear \
--ignore 'tests/**/diff/**' \
--ignore 'tests/**/out/**' \
"typst-test r"
+81 -40
View File
@@ -23,7 +23,7 @@
// Returns:
// dictionary: Normalized dictionary with all expected keys populated
//
// Throws:
// Panics:
// - If 'short' key is missing when using normal (non-abbreviated) syntax
// - If 'short' value is not a string
// - If 'short' value is an empty string
@@ -97,7 +97,7 @@
// Returns:
// dictionary: The glossary entry associated with the key
//
// Throws:
// Panics:
// - If the key does not exist in the glossary
//
#let __get_entry(key) = {
@@ -187,7 +187,7 @@
// content: The fully formatted term, including optional article, capitalization,
// usage tracking metadata, and the chosen form (short, long, or both).
//
#let __gls(key, modifiers: array, show-term: function, term-links: true) = {
#let __gls(key, modifiers: array, format-term: function, show-term: function, term-links: true) = {
// ---------------------------------------------------------------------------
// Check for illegal modifier combinations
// ---------------------------------------------------------------------------
@@ -248,27 +248,6 @@
}
}
// get_term(mode): Returns the appropriate term string based on the mode.
// mode is one of "short", "long", or "both".
// - "short": Returns the short form.
// - "long": Returns the long form.
// - "both": Returns "Long form (short form)".
// We rely on prior logic to ensure that when mode = "both" or "long", a long form is available.
let get_term = (mode) => {
let short_form = pluralize_term(entry.short, entry.plural)
let long_form = pluralize_term(entry.long, entry.longplural) // none safe here
if mode == "short" {
// Just the short form.
short_form
} else if mode == "long" {
// Just the long form.
long_form
} else {
// mode == "both": "Long form (short form)".
long_form + " (" + short_form + ")"
}
}
// get_article(mode): Returns the appropriate article string based on the mode.
// - If wants_article is false, returns an empty string.
// - If mode is "short", return the short article.
@@ -337,19 +316,33 @@
// Determine mode using the helper function
let mode = determine_mode(wants_both, wants_long, wants_short, is_first_use, long_available)
// Get the article and term for the chosen mode, then apply capitalization if requested
let (article, term) = capitalize_term(get_article(mode), get_term(mode))
// Pluralize (if requested)
let short-form = pluralize_term(entry.short, entry.plural)
let long-form = pluralize_term(entry.long, entry.longplural) // `none` safe here
// Apply format-term() to get the term
let formatted-term = format-term(mode, short-form, long-form)
if type(formatted-term) != "string" {
// This is because we still need to capitalize the term, and we cannot do
// that with content, thus requiring a string here.
// TODO: consider if we want to capitalize *before* this. First intuition is
// "no".
panic("Your cutsom format-term() function must return a string.")
}
// Get the article, then capitalize either the article or term (if requested)
let (article, term) = capitalize_term(get_article(mode), formatted-term)
// ---------------------------------------------------------------------------
// Construct and return the final output
// ---------------------------------------------------------------------------
context {
let text = if term-links and __has_glossary_entry(key) {
let linked-term = if term-links and __has_glossary_entry(key) {
link(label(key), term)
} else {
term
}
[#article#show-term(text)#metadata(term)#__term_label(key)]
[#article#show-term(linked-term)#metadata(term)#__term_label(key)]
// |^^^^^^^|^^^^^^^^^ |^^^^^^^^ |^^^^^^^^^^^^
// \_art. | | |
// \_ apply user formatting function to the term
@@ -387,6 +380,47 @@
.join(", ")
}
// Formats a term string based on the specified mode.
//
// Given mode, long-form, and short-form, this function returns the formatted
// term based on the mode. When mode is ...
//
// - "short" -> returns "short-form"
// - "long" -> returns "long-form"
// - "both" -> returns "short-form (long-form)"
//
// Parameters:
// mode (string): The mode in which to format the term. Possible values are "short", "long", or "both".
// short-form (string): The short form of the term.
// long-form (string): The long form of the term.
//
// Returns:
// string: The formatted term based on the specified mode.
//
#let __default-format-term(mode, short-form, long-form) = {
if mode == "short" {
short-form
} else if mode == "long" {
long-form
} else {
// mode assumed to be "both"
long-form + " (" + short-form + ")"
}
}
// Styles a term to control its display in a document.
//
// Parameters:
// term-body (content'ish): the unstyled output from __gls()
//
// Returns:
// content: display ready content (assumed based on `term-body`)
//
#let __default-show-term(term-body) = {
// Default: just render it like normal text
term-body
}
// Initializes the glossary system and sets up term reference handling.
//
// This function is typically used with `#show: init-glossary`. It performs
@@ -407,12 +441,13 @@
// Returns:
// content: Processed document content with glossary functionality enabled
//
// Throws:
// Panics:
// - If entries parameter is not a dictionary
//
#let init-glossary(
entries,
show-term: (term-body) => term-body,
format-term: __default-format-term,
show-term: __default-show-term,
term-links: true,
body
) = context {
@@ -423,6 +458,7 @@
let checked-entry = z.parse(entry, dict-schema, scope: ("dictionary entry",))
checked-entries.insert(checked-key, checked-entry)
}
let checked-format-term = z.parse(show-term, z.function(), scope: ("format-term",))
let checked-show-term = z.parse(show-term, z.function(), scope: ("show-term",))
let checked-body = z.parse(body, z.content(), scope: ("body",))
@@ -457,7 +493,7 @@
// Now see if this is an actual glossary term key
if __has_entry(key) {
// Found in dictionary, render via __gls()
__gls(key, modifiers: modifiers.map(lower), show-term: show-term, term-links: term-links)
__gls(key, modifiers: modifiers.map(lower), format-term: format-term, show-term: show-term, term-links: term-links)
} else {
// Not one of ours, so just pass it through
r
@@ -499,12 +535,13 @@
// Returns:
// content: Formatted glossary content
//
// Throws:
// Panics:
// - If a requested group doesn't exist
//
#let glossary(
title: "Glossary",
theme: theme-twocol,
theme: theme-academic,
sort: true,
ignore-case: false,
groups: (),
) = context {
@@ -565,13 +602,17 @@
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())
let sorted_entries = if sort {
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() // NOTE: sorted() is NOT language-aware
// 3. strip away the tuple's first element, leaving an array of entries
.map(t => t.last())
} else {
current_entries
}
// add entries to this group's output map
output.insert(group, sorted_entries)
+1 -1
View File
@@ -40,7 +40,7 @@
grid(
columns: (1fr, auto),
gutter: 0.75em,
[#short-display#long-display#description],
[#short-display#long-display#description#entry.label],
text(fill: rgb("#666666"), entry.pages)
)
}
+1 -1
View File
@@ -37,7 +37,7 @@
grid(
columns: (1fr, auto),
gutter: 1em,
[#short-display#entry.label#long-display#description],
[#short-display#entry.label#long-display#description#entry.label],
[#entry.pages]
)
}
+1 -1
View File
@@ -27,6 +27,6 @@
},
entry: (entry, index, total) => {
(entry.short, entry.long, entry.description, entry.pages)
(entry.short + entry.label, entry.long, entry.description, entry.pages)
}
)
+2 -2
View File
@@ -203,7 +203,7 @@
if is_acronym {
// Use acronym rules
let first_char = upper_word.slice(0,1)
let first_char = upper_word.codepoints().slice(0,1).first()
if acronym_vowels.contains(first_char) {
"an"
} else {
@@ -211,7 +211,7 @@
}
} else {
// Non-acronym rules
let first_char = lower_word.slice(0,1)
let first_char = lower_word.codepoints().slice(0,1).first()
// Special cases for words matching a set of known 'silent h' prefixes
let silent_h_words = (
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

+208
View File
@@ -0,0 +1,208 @@
#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"),
concise: "Concise syntax!",
shortonly: (
short: "shortonly!!!"),
unused: "Unused term, which shouldn't print in the glossary."
)
#set page(width: 6.5in, height: auto, margin: 1em)
#let test-content = {
table(
columns: 3,
table.header(
[`short`], [`long`], [`both`],
),
[@html:short], [@html:long], [@html:both],
[@css:short], [@css:long], [@css:both],
[@tps:short], [@tps:long], [@tps:both],
[@concise:short], [@concise:long], [@concise:both],
[@shortonly:short], [@shortonly:long], [@shortonly:both],
table.hline(stroke: 2pt),
[@html:short:cap], [@html:long:cap], [@html:both:cap],
[@a:css:short], [@a:css:long], [@a:css:both],
[@tps:short:pl], [@tps:long:pl], [@tps:both:pl],
[@concise:a:cap:short], [@concise:a:cap:long], [@concise:a:cap:both],
[@an:shortonly:short], [@an:shortonly:long], [@an:shortonly:both],
)
[@a:tps:cap is @tps:def.]
}
// ---------------------------------------------------------------------------------
#let format-basic(mode, short-form, long-form) = {
if mode == "short" {
short-form
} else if mode == "long" {
long-form
} else {
long-form + " (" + short-form + ")"
}
}
#init-glossary(myGlossary, format-term: format-basic)[#test-content]
// ---------------------------------------------------------------------------------
#let format-reverse(mode, short-form, long-form) = {
if mode == "short" {
short-form
} else if mode == "long" {
long-form
} else {
short-form + " [" + long-form + "]"
}
}
#init-glossary(myGlossary, format-term: format-reverse)[#test-content]
// ---------------------------------------------------------------------------------
#let format-uppercase-short(mode, short-form, long-form) = {
let short-up = upper(short-form);
if mode == "short" {
short-up
} else if mode == "long" {
long-form
} else {
long-form + " (" + short-up + ")"
}
}
#init-glossary(myGlossary, format-term: format-uppercase-short)[#test-content]
// ---------------------------------------------------------------------------------
#let format-prefix-suffix(mode, short-form, long-form) = {
if mode == "short" {
"Term: " + short-form + "."
} else if mode == "long" {
"Term: " + long-form + "."
} else {
"Term: " + long-form + " (" + short-form + ")."
}
}
#init-glossary(myGlossary, format-term: format-prefix-suffix)[#test-content]
// ---------------------------------------------------------------------------------
#let format-conditional-separator(mode, short-form, long-form) = {
let separator = if short-form.len() > 4 { " / " } else { ", " };
let has_long = long-form != none and long-form != "";
if mode == "short" {
short-form
} else if mode == "long" {
if has_long { long-form } else { short-form }
} else {
if has_long {
long-form + separator + short-form
} else {
short-form
}
}
}
#init-glossary(myGlossary, format-term: format-conditional-separator)[#test-content]
// ---------------------------------------------------------------------------------
// 1. Braces and Labels
// Idea: Use different bracket styles and labels based on the mode.
#let format-braces(mode, short-form, long-form) = {
if mode == "short" {
"{" + short-form + "}"
} else if mode == "long" {
"[" + long-form + "]"
} else {
long-form + " <" + short-form + ">"
}
}
#init-glossary(myGlossary, format-term: format-braces)[#test-content]
// ---------------------------------------------------------------------------------
// 2. Mode-Based Prefix
// Idea: Add a prefix indicating the mode, and then show the term(s).
#let format-mode-prefix(mode, short-form, long-form) = {
let prefix = if mode == "short" { "S:" } else if mode == "long" { "L:" } else { "B:" };
if mode == "short" {
prefix + short-form
} else if mode == "long" {
prefix + long-form
} else {
prefix + long-form + " & " + short-form
}
}
#init-glossary(myGlossary, format-term: format-mode-prefix)[#test-content]
// ---------------------------------------------------------------------------------
// 3. Conditional Fallback for Missing Long
// Idea: If no long-form is provided, use the short-form in all cases.
#let format-fallback-long(mode, short-form, long-form) = {
let has_long = long-form != none and long-form != "";
if mode == "short" {
short-form
} else if mode == "long" {
if has_long { long-form } else { short-form }
} else {
if has_long { long-form + " (" + short-form + ")" } else { short-form }
}
}
#init-glossary(myGlossary, format-term: format-fallback-long)[#test-content]
// ---------------------------------------------------------------------------------
// 4. Uppercase Both Terms in Both Mode
// Idea: Always uppercase for both mode, normal otherwise.
#let format-uppercase-both(mode, short-form, long-form) = {
if mode == "short" {
short-form
} else if mode == "long" {
long-form
} else {
upper(long-form) + " (" + upper(short-form) + ")"
}
}
#init-glossary(myGlossary, format-term: format-uppercase-both)[#test-content]
// ---------------------------------------------------------------------------------
// 5. Append Length Info
// Idea: In both mode, append the length of each string.
#let format-length-info(mode, short-form, long-form) = {
let s_len = str(short-form.len())
let l_len = if long-form != none and long-form != "" { str(long-form.len()) } else { "0" }
if mode == "short" {
short-form + " [len:" + s_len + "]"
} else if mode == "long" {
long-form + " [len:" + l_len + "]"
} else {
long-form + " (" + short-form + ") [L:" + l_len + ", S:" + s_len + "]"
}
}
#init-glossary(myGlossary, format-term: format-length-info)[#test-content]
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+45
View File
@@ -0,0 +1,45 @@
#import "/lib.typ": *
#import "@preview/glossarium:0.5.1": *
#set page(height: auto, width: 4.5in)
#show link: set text(fill: red)
// -----------------------------------------------------------------------------
= `glossy`:
#box(
stroke: 1pt,
inset: 1em,
[
#show: init-glossary.with((WWW: "World Wide Web"))
This should be a link: @WWW
This should be a link, too: @WWW
#glossary()
]
)
// -----------------------------------------------------------------------------
= `glossarium`:
#box(
stroke: 1pt,
inset: 1em,
[
#show: make-glossary
#let entry-list = (
(key: "ML", short: "ML", long: "Machine Learning"),
)
#register-glossary(entry-list)
This should be a link: @ML
This should be a link, too: @ML
#print-glossary(entry-list)
]
)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 79 KiB

+23
View File
@@ -0,0 +1,23 @@
first:
short: "DAL"
long: "first"
second:
short: "cab"
long: "second"
third:
short: "delta"
long: "third"
fourth:
short: "COO"
long: "fourth"
fifth:
short: "cuckoo"
long: "fifth"
sixth:
short: "ČR"
long: "sixth"
Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

+13
View File
@@ -0,0 +1,13 @@
#import "/lib.typ":*
#show: init-glossary.with(yaml("glossary.yml"))
@first, @second, @third, @fourth, @fifth, @sixth
#glossary(ignore-case: false, sort: true)
#line(length: 20em)
#glossary(ignore-case: true, sort: true)
#line(length: 20em)
#glossary(ignore-case: false, sort: false)
#line(length: 20em)
#glossary(ignore-case: true, sort: false)
+23
View File
@@ -0,0 +1,23 @@
cab:
short: "cab"
long: "taxi cab"
coo:
short: "COO"
long: "Chief Operations Officer"
cuckoo:
short: "cuckoo"
long: "really crazy"
delta:
short: "delta"
long: "difference between two values"
dal:
short: "DAL"
long: "Delta Airlines"
čr:
short: "ČR"
long: "Česká Republika"
Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

+20
View File
@@ -0,0 +1,20 @@
#import "/lib.typ":*
#set text(lang: "cs", region: "CZ")
#show: init-glossary.with(yaml("glossary.yml"))
#glossary(ignore-case: true, theme: theme-twocol)
#line(length: 20em)
@čr
@cab
@coo
@cuckoo
@delta
@dal
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "glossy"
version = "0.4.0"
version = "0.5.1"
entrypoint = "lib.typ"
authors = ["Stephen Waits <steve@waits.net>"]
license = "MIT"