Compare commits

...
10 Commits
Author SHA1 Message Date
swaits 1e1cb91163 feat(glossy): terms now link to glossary entries
- Merges [16](https://github.com/swaits/typst-collection/pull/16).
- Fixes [3](https://github.com/swaits/typst-collection/issues/3).

A big PR from [@eltos](https://github.com/eltos)!

This adds labels for each `key`, so that auto-complete (in the typst.app
/ `typst-ide`) works properly. It also makes it so terms can link to
their glossary entry. In the case that terms are used but no glossary is
shown (which is a core feature of Glossy), placeholder labels are used
so that the auto-complete still works.

All tests, but one (theme-gallery), passed straight from the PR. The
ddiff in the `theme-gallery` test were non-visible (ie a few pixels),
making it effectively passing too. Updated that test so it passes, and
updated the theme screenshot to match its output as well.

Huge thanks to [@eltos](https://github.com/eltos) who demonstrates a
deep understanding of Typst!
2024-12-19 09:36:49 -07:00
Philipp Niedermayer 255bda30d4 Typo 2024-12-18 20:21:27 +00:00
Philipp Niedermayer 2b6debb7d1 Make terms clickable links to glossary list 2024-12-18 20:07:01 +00:00
Philipp Niedermayer 80eeb9240c Fix intendation 2024-12-18 20:07:01 +00:00
Philipp Niedermayer 76fb6b9ea7 Simplifies term usage tracking
- Uniqfies the duplicate term usage tracking via counter + state to just using the counter. Refreactor the code with the two methods "mark_term_used" and "is_term_used".
2024-12-18 20:06:53 +00:00
Philipp Niedermayer 16ceebdc70 Simply label usage
- Adds a label with the entry key for autocompletion
  - either in the glossary (if it is visible and there is a corresponding entry)
  - or invisible during init-glossary
- Use a single label for all usages of the entry in the text. This simplifies the back-referencing logic significantly and also avoids cluttering the auto-complete list

closes https://github.com/swaits/typst-collection/issues/3
2024-12-18 20:06:53 +00:00
swaits 2286037bd6 chore(glossy): release version 0.4.0
## New features:

**Articles with `a`/`an` modifier**

Now you can include articles using this new modifier. The articles may
be defined in your dictionary entry (fields `artcile`, and
`longarticle`). But, if you don't define them, Glossy will do a
reasonably good job at guessing them.

This modifier is also allowed before the term. For example, you might
have this term:

```yaml
www: Worldwide Web
```

And you could display it with an article by any of these:

```typst
@a:www
@an:www
@www:a
@www:an
```

These, `a` and `an` are exactly equivalent. They're just provided for
your convenience. Use either or both.

The `a`/`an` modifier works with all modifiers except `pl` and
`def`/`desc`.

If you want to define custom articles for your terms, do so like this:

```typst
#let glossary = (
  rf: (
    short: "RF",
    article: "an",
    long: "radio frequency",
    longarticle: "a",
  )
  uts: (
    short: "UTS",
    article: "a",
    long: "untranslatable string",
    longarticle: "an",
  )
)
```

But again, in most cases (for English) you shouldn't need to because
Glossy should usually get it right.

Fixes [#12](https://github.com/swaits/typst-collection/issues/12).

## Other changes:

- added test which exhausts all reference options
- added panic if 'def'/'desc' used when no `description` field exists
- significantly improved the (English) pluralizer function
- `__gls()` function refactored, cleaned up
- removed a rogue PDF

## Contributors:

- [@eltos](https://github.com/eltos)
- [@theRealProHacker](https://github.com/theRealProHacker)
2024-12-18 10:15:20 -07:00
swaits a8e0ef7c02 test(glossy): add test to exhaust all reference options 2024-12-18 10:14:18 -07:00
swaits ba8fdda1bf feat(glossy): improve pluralize 2024-12-18 06:08:52 -07:00
swaits b1aa11705c chore(glossy): cleanup rogue PDF 2024-12-18 06:08:51 -07:00
10 changed files with 913 additions and 136 deletions

No files matched your search

+1 -1
View File
@@ -41,7 +41,7 @@ different goals:
### Import the package ### Import the package
```typst ```typst
#import "@preview/glossy:0.3.0": * #import "@preview/glossy:0.4.0": *
``` ```
### Defining Glossary Terms ### Defining Glossary Terms
+102 -121
View File
@@ -5,7 +5,6 @@
#import "./utils.typ": * #import "./utils.typ": *
#let __gloss_entries = state("__gloss_entries", (:)) #let __gloss_entries = state("__gloss_entries", (:))
#let __gloss_used = state("__gloss_used", (:))
#let __gloss_label_prefix = "__gloss:" #let __gloss_label_prefix = "__gloss:"
@@ -110,70 +109,56 @@
entries.at(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. // Creates a label object for term usage in documents.
// //
// Parameters: // Parameters:
// key (string): The glossary entry key // key (string): The glossary entry key
// index (int): The occurrence index of the term in the document
// //
// Returns: // Returns:
// label: A Typst label object for the term usage // label: A Typst label object for the term usage
// //
#let __term_label(key, index) = { #let __term_label(key) = {
label(__term_label_str(key, index)) label(__gloss_label_prefix + key)
} }
// Updates the term usage count in the glossary state. // Updates the term usage in the glossary state.
// //
// Parameters: // Parameters:
// key (string): The glossary entry key // key (string): The glossary entry key
// count (int): The number of times the term has been used
// //
// Returns: // Returns:
// none: Updates state as a side effect // none: Updates state as a side effect
// //
#let __save_term_usage(key, count) = { #let __mark_term_used(key) = {
__gloss_used.update(state => { counter(__gloss_label_prefix + key).step()
state.insert(key, count) }
state
}) // Queries the term usage
//
// Parameters:
// key (string): The glossary entry key
// location (location): The location in the document. Use none for end of document
//
// Returns:
// boolean: If the entry is used above the location in the document
//
#let __is_term_used(key, location: none) = {
let c = counter(__gloss_label_prefix + key)
c = if location == none {c.final()} else {c.at(location)}
c.at(0) > 0
}
// Determine if the glossary contains a visible entry
//
// Parameters:
// key (string): The glossary entry key
//
// Returns:
// boolean: If the term is visible in the glossary (and the label is unique)
//
#let __has_glossary_entry(key) = {
query(<glossary>).len() > 0 and query(selector(label(key)).after(<glossary>)).len() == 1
} }
// Renders a glossary term with various formatting options. // Renders a glossary term with various formatting options.
@@ -189,6 +174,7 @@
// - "def" or "desc": Show the term's description instead of its name. // - "def" or "desc": Show the term's description instead of its name.
// - "a" or "an": Prepend an article, chosen from the entry (short or long form). // - "a" or "an": Prepend an article, chosen from the entry (short or long form).
// show-term (function): A function that renders the chosen term. // show-term (function): A function that renders the chosen term.
// term-links (boolean): If terms should be clickable links leading to glossary
// //
// Behavior: // Behavior:
// - Without modifiers, the first use of a term shows "Long form (short form)", // - Without modifiers, the first use of a term shows "Long form (short form)",
@@ -201,7 +187,7 @@
// content: The fully formatted term, including optional article, capitalization, // content: The fully formatted term, including optional article, capitalization,
// usage tracking metadata, and the chosen form (short, long, or both). // usage tracking metadata, and the chosen form (short, long, or both).
// //
#let __gls(key, modifiers: array, show-term: function) = { #let __gls(key, modifiers: array, show-term: function, term-links: true) = {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Check for illegal modifier combinations // Check for illegal modifier combinations
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -216,27 +202,22 @@
// Retrieve the glossary entry and its label // Retrieve the glossary entry and its label
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
let entry = __get_entry(key) let entry = __get_entry(key)
let entry_label = __dict_label_str(key)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// If "def" or "desc" modifier is present, show the entry's description immediately // If "def" or "desc" modifier is present, show the entry's description immediately
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
if "def" in modifiers or "desc" in modifiers { if "def" in modifiers or "desc" in modifiers {
if entry.description == none {
panic("Use of 'def'/'desc' requires a description be defined.")
}
return show-term([#entry.description]) return show-term([#entry.description])
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Manage term usage counting and determine if it's the first reference // Manage term usage counting and determine if it's the first reference
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
let entry_counter = counter(entry_label) let is_first_use = not __is_term_used(key, location: here())
entry_counter.step() __mark_term_used(key)
let key_index = entry_counter.get().first()
let is_first_use = key_index == 0
// ---------------------------------------------------------------------------
// Record another usage of this term (increment the usage counter)
// ---------------------------------------------------------------------------
__save_term_usage(key, key_index + 1)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helper Functions // Helper Functions
@@ -363,14 +344,19 @@
// Construct and return the final output // Construct and return the final output
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
context { context {
[#article#show-term(term)#metadata(term)#__term_label(key, key_index)] let text = if term-links and __has_glossary_entry(key) {
link(label(key), term)
} else {
term
}
[#article#show-term(text)#metadata(term)#__term_label(key)]
// |^^^^^^^|^^^^^^^^^ |^^^^^^^^ |^^^^^^^^^^^^ // |^^^^^^^|^^^^^^^^^ |^^^^^^^^ |^^^^^^^^^^^^
// \_art. | | | // \_art. | | |
// \_ apply user formatting function to the term // \_ apply user formatting function to the term
// | | // | |
// \_ metadata lets us label (ie it's "labelable") // \_ metadata lets us label (ie it's "labelable")
// | // |
// \_ i.e. <__gloss.key.0>, etc. // \_ i.e. <__gloss:key>, etc.
// for backlink from glossary // for backlink from glossary
} }
} }
@@ -383,36 +369,22 @@
// //
// Parameters: // Parameters:
// key (string): The glossary entry key // key (string): The glossary entry key
// count (int): Number of times the term appears in the document
// //
// Returns: // Returns:
// content: Comma-separated list of linked page numbers // content: Comma-separated list of linked page numbers
// //
// Example output: "1, 3, 5" where each number links to the term usage on that page // Example output: "1, 3, 5" where each number links to the term usage on that page
// //
#let __create_backlinks(key, count) = { #let __create_backlinks(key) = {
// Convert term (key + count) to labels with their displayed page numbers return context query(__term_label(key)) // find all reference
let page_labels = range(count).map(i => { .map(meta => { // extract location and page number (or symbol)
let label = __term_label(key, i) let loc = meta.location()
let loc = locate(label) let page = numbering(__default(loc.page-numbering(), "1"), ..counter(page).at(loc))
( (loc, page)
label, })
numbering(__default(loc.page-numbering(), "1"), ..counter(page).at(loc)) .dedup(key: ((loc, page)) => page) // deduplicate by page
) .map(((loc, page)) => link(loc, page)) // create links
}) .join(", ")
// 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. // Initializes the glossary system and sets up term reference handling.
@@ -429,6 +401,7 @@
// - values are entry dictionaries containing term details // - values are entry dictionaries containing term details
// show-term ((content) => content): Optional function to customize term rendering // show-term ((content) => content): Optional function to customize term rendering
// Default: Returns term content unchanged // Default: Returns term content unchanged
// term-links (boolean): If terms should be clickable links leading to glossary
// body (content): Document content to process // body (content): Document content to process
// //
// Returns: // Returns:
@@ -440,6 +413,7 @@
#let init-glossary( #let init-glossary(
entries, entries,
show-term: (term-body) => term-body, show-term: (term-body) => term-body,
term-links: true,
body body
) = context { ) = context {
// Type checking // Type checking
@@ -455,6 +429,10 @@
// Process and store each glossary entry // Process and store each glossary entry
for (key, entry) in entries { for (key, entry) in entries {
__add_entry(key, __normalize_entry(key, entry)) __add_entry(key, __normalize_entry(key, entry))
// Create placeholder labels for autocompletion (if not already present)
context if not __has_glossary_entry(key) [
#metadata(key)#label(key)
]
} }
// Set up reference handling for glossary terms // Set up reference handling for glossary terms
@@ -479,7 +457,7 @@
// Now see if this is an actual glossary term key // Now see if this is an actual glossary term key
if __has_entry(key) { if __has_entry(key) {
// Found in dictionary, render via __gls() // Found in dictionary, render via __gls()
__gls(key, modifiers: modifiers.map(lower), show-term: show-term) __gls(key, modifiers: modifiers.map(lower), show-term: show-term, term-links: term-links)
} else { } else {
// Not one of ours, so just pass it through // Not one of ours, so just pass it through
r r
@@ -539,7 +517,7 @@
// Collect and organize entries by group // Collect and organize entries by group
let output = (:) let output = (:)
let all_entries = __gloss_entries.final() let all_entries = __gloss_entries.final()
let all_used = __gloss_used.final() let all_used = all_entries.keys().filter(key => __is_term_used(key))
// Determine which groups to process // Determine which groups to process
let all_groups = all_entries let all_groups = all_entries
@@ -569,52 +547,55 @@
let current_entries = () let current_entries = ()
// Collect all used entries for this group // Collect all used entries for this group
for (key, count) in all_used { for key in all_used {
let entry = all_entries.at(key) let entry = all_entries.at(key)
if entry.at("group") == group { if entry.at("group") == group {
current_entries.push(( current_entries.push((
short: entry.at("short"), short: entry.at("short"),
long: entry.at("long"), long: entry.at("long"),
description: entry.at("description"), description: entry.at("description"),
label: [#metadata(key)#__dict_label(key)], label: [#metadata(key)#label(key)],
pages: __create_backlinks(key, count) pages: __create_backlinks(key)
)) ))
} }
} }
// Add non-empty groups to output // Add non-empty groups to output
if current_entries.len() > 0 { if current_entries.len() > 0 {
group = if group == none { "" } else { group } group = if group == none { "" } else { group }
// sort entries by case insensitivity if requested // sort entries by case insensitivity if requested
let sorted_entries = current_entries let sorted_entries = current_entries
// 1. create array of tuples with (lower [if ignore-case], entry) // 1. create array of tuples with (lower [if ignore-case], entry)
.map(e => { if ignore-case { (lower(e.short), e) } else { (e.short, e) } }) .map(e => { if ignore-case { (lower(e.short), e) } else { (e.short, e) } })
// 2. sort the tuples (by first element then second) // 2. sort the tuples (by first element then second)
.sorted(key: t => t.first()) .sorted(key: t => t.first())
// 3. strip away the tuple's first element, leaving an array of entries // 3. strip away the tuple's first element, leaving an array of entries
.map(t => t.last()) .map(t => t.last())
// add entries to this group's output map // add entries to this group's output map
output.insert(group, sorted_entries) output.insert(group, sorted_entries)
}
} }
}
// Render the glossary using the theme // Render the glossary using the theme
let group_index = 0 let group_index = 0
(checked-theme.section)( [
title, #metadata("glossary")<glossary>
for (group, entries) in output { #(checked-theme.section)(
(checked-theme.group)( title,
group, for (group, entries) in output {
group_index, (checked-theme.group)(
output.len(), group,
for (i, entry) in entries.enumerate() { group_index,
(checked-theme.entry)(entry, i, entries.len()) output.len(),
} for (i, entry) in entries.enumerate() {
) (checked-theme.entry)(entry, i, entries.len())
group_index += 1 }
} )
) group_index += 1
}
)
]
} }
+110 -12
View File
@@ -10,29 +10,127 @@
// word: The singular word to pluralize // word: The singular word to pluralize
// Returns: The pluralized form of the word, or none if input is none // Returns: The pluralized form of the word, or none if input is none
#let __pluralize(word) = { #let __pluralize(word) = {
// early exit with invalid input // Early exit for invalid input
if word == none { if word == none {
return none return none
} }
// Define helper functions for checking endings // Helper functions for checking endings
let ends_with = (suffix) => lower(word).ends-with(suffix) let ends_with = (suffix) => lower(word).ends-with(suffix)
let ends_with_any = (suffixes) => suffixes.any((suffix) => 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) // Map of irregular plurals for common words
if ends_with_any(( "s", "x", "z", "sh", "ch" )) { let irregulars = (
"alumna": "alumnae",
"alumnus": "alumni",
"analysis": "analyses",
"appendix": "appendices",
"basis": "bases",
"cactus": "cacti",
"child": "children",
"crisis": "crises",
"criterion": "criteria",
"datum": "data",
"foot": "feet",
"fungus": "fungi",
"goose": "geese",
"man": "men",
"medium": "media",
"mouse": "mice",
"nucleus": "nuclei",
"oasis": "oases",
"person": "people",
"phenomenon": "phenomena",
"stimulus": "stimuli",
"thesis": "theses",
"tooth": "teeth",
"woman": "women",
)
// Set of plural-only words
let plural_only = (
"advice",
"alumni",
"bison",
"children",
"criteria",
"data",
"deer",
"feet",
"fish",
"geese",
"information",
"media",
"men",
"mice",
"money",
"moose",
"octopi",
"octopodes",
"people",
"scissors",
"series",
"sheep",
"species",
"teeth",
"trousers",
"women",
)
// Irregular suffixes
let f_to_ves = ("leaf", "loaf", "calf", "half", "wolf", "thief")
let fe_to_ves = ("wife", "knife", "life")
let o_to_os = ("photo", "piano", "halo", "radio", "video", "studio", "solo", "taco", "memo", "zero")
// Convenience
let w = lower(word)
// Check for words already plural
if plural_only.contains(w) {
return word
}
// Check for irregular singulars
if irregulars.keys().contains(w) {
return irregulars.at(w)
}
// Standard pluralization rules
if ends_with("iz") {
word + "zes"
} else if ends_with_any(("s", "x", "z", "sh", "ch")) {
word + "es" word + "es"
} else if ends_with("y") and not ends_with_any(( "a", "e", "i", "o", "u" )) { } else if ends_with("y") {
// Convert y to ies when y follows a consonant if not ends_with_any(("ay", "ey", "iy", "oy", "uy")) {
word.slice(0, -1) + "ies" word.slice(0, -1) + "ies"
} else {
word + "s"
}
} else if ends_with("f") { } else if ends_with("f") {
// Convert f to ves if f_to_ves.contains(w) {
word.slice(0, -1) + "ves" word.slice(0, -1) + "ves"
} else {
word + "s"
}
} else if ends_with("fe") { } else if ends_with("fe") {
// Convert fe to ves if fe_to_ves.contains(w) {
word.slice(0, -2) + "ves" word.slice(0, -2) + "ves"
} else {
word + "s"
}
} else if ends_with("us") {
word.slice(0, -2) + "i"
} else if ends_with("is") {
word.slice(0, -2) + "es"
} else if ends_with("on") {
word + "s"
} else if ends_with("o") {
if o_to_os.contains(w) {
word + "s"
} else {
word + "es"
}
} else { } else {
// Default case: just add s
word + "s" word + "s"
} }
} }
+1 -1
View File
@@ -17,7 +17,7 @@
short: "TPS", short: "TPS",
long: "test procedure specification", long: "test procedure specification",
description: "A document on how to run all the test procedures"), description: "A document on how to run all the test procedures"),
unused: "Unused term, which shouldn't print in the glossary." unused: "Unused term, which shouldn't print in the glossary."
) )
#show: init-glossary.with(myGlossary, show-term: (body) => [#emph(body)]) #show: init-glossary.with(myGlossary, show-term: (body) => [#emph(body)])
Binary file not shown.
+308
View File
@@ -0,0 +1,308 @@
#import "/src/utils.typ": __pluralize
#let plural_tests = (
("BUS", "BUSes"),
("Box", "Boxes"),
("Buzz", "Buzzes"),
("CHURCH", "CHURCHes"),
("Lady", "Ladies"),
("Leaf", "Leaves"),
("Loaf", "Loaves"),
("ability", "abilities"),
("account", "accounts"),
("advice", "advice"),
("age", "ages"),
("agent", "agents"),
("alumna", "alumnae"),
("alumni", "alumni"),
("alumnus", "alumni"),
("answer", "answers"),
("appendix", "appendices"),
("apple", "apples"),
("art", "arts"),
("artist", "artists"),
("attention", "attentions"),
("ax", "axes"),
("baby", "babies"),
("ball", "balls"),
("bank", "banks"),
("basis", "bases"),
("beauty", "beauties"),
("bed", "beds"),
("belief", "beliefs"),
("berry", "berries"),
("bird", "birds"),
("book", "books"),
("bottle", "bottles"),
("box", "boxes"),
("boy", "boys"),
("branch", "branches"),
("brother", "brothers"),
("brush", "brushes"),
("bus", "buses"),
("business", "businesses"),
("cactus", "cacti"),
("cake", "cakes"),
("calf", "calves"),
("car", "cars"),
("care", "cares"),
("carrot", "carrots"),
("cart", "carts"),
("cat", "cats"),
("chair", "chairs"),
("chance", "chances"),
("change", "changes"),
("chef", "chefs"),
("chief", "chiefs"),
("child", "children"),
("church", "churches"),
("city", "cities"),
("class", "classes"),
("cloud", "clouds"),
("coach", "coaches"),
("comfort", "comforts"),
("company", "companies"),
("computer", "computers"),
("confidence", "confidences"),
("cookie", "cookies"),
("corpus", "corpuses"),
("country", "countries"),
("courage", "courages"),
("cow", "cows"),
("criterion", "criteria"),
("culture", "cultures"),
("cup", "cups"),
("danger", "dangers"),
("dart", "darts"),
("day", "days"),
("deer", "deer"),
("desk", "desks"),
("development", "developments"),
("dish", "dishes"),
("dog", "dogs"),
("door", "doors"),
("dream", "dreams"),
("education", "educations"),
("egg", "eggs"),
("elephant", "elephants"),
("employee", "employees"),
("energy", "energies"),
("entertainment", "entertainments"),
("exam", "exams"),
("experience", "experiences"),
("family", "families"),
("farm", "farms"),
("father", "fathers"),
("fish", "fish"),
("floor", "floors"),
("flower", "flowers"),
("fly", "flies"),
("focus", "focuses"),
("foot", "feet"),
("fork", "forks"),
("fox", "foxes"),
("freedom", "freedoms"),
("friend", "friends"),
("future", "futures"),
("game", "games"),
("garden", "gardens"),
("gas", "gases"),
("gift", "gifts"),
("girl", "girls"),
("glass", "glasses"),
("goat", "goats"),
("goose", "geese"),
("grass", "grasses"),
("guy", "guys"),
("half", "halves"),
("hand", "hands"),
("hat", "hats"),
("head", "heads"),
("health", "healths"),
("help", "helps"),
("hero", "heroes"),
("history", "histories"),
("holiday", "holidays"),
("hope", "hopes"),
("horse", "horses"),
("house", "houses"),
("idea", "ideas"),
("image", "images"),
("information", "information"),
("interest", "interests"),
("island", "islands"),
("journey", "journeys"),
("joy", "joys"),
("justice", "justices"),
("key", "keys"),
("keyboard", "keyboards"),
("king", "kings"),
("kiss", "kisses"),
("kitchen", "kitchens"),
("knife", "knives"),
("knowledge", "knowledges"),
("lady", "ladies"),
("lamp", "lamps"),
("laptop", "laptops"),
("law", "laws"),
("leaf", "leaves"),
("learning", "learnings"),
("leg", "legs"),
("letter", "letters"),
("life", "lives"),
("light", "lights"),
("line", "lines"),
("lion", "lions"),
("loaf", "loaves"),
("love", "loves"),
("luck", "lucks"),
("man", "men"),
("map", "maps"),
("market", "markets"),
("match", "matches"),
("memory", "memories"),
("milk", "milks"),
("money", "money"),
("monitor", "monitors"),
("month", "months"),
("mosquito", "mosquitoes"),
("mother", "mothers"),
("mountain", "mountains"),
("mouse", "mice"),
("movie", "movies"),
("name", "names"),
("nation", "nations"),
("neck", "necks"),
("night", "nights"),
("nucleus", "nuclei"),
("ocean", "oceans"),
("octopi", "octopi"),
("octopodes", "octopodes"),
("octopus", "octopuses"),
("office", "offices"),
("onion", "onions"),
("opinion", "opinions"),
("orange", "oranges"),
("page", "pages"),
("park", "parks"),
("party", "parties"),
("peace", "peaces"),
("peach", "peaches"),
("pen", "pens"),
("person", "people"),
("phenomenon", "phenomena"),
("phone", "phones"),
("photo", "photos"),
("piano", "pianos"),
("picture", "pictures"),
("pig", "pigs"),
("place", "places"),
("plane", "planes"),
("plant", "plants"),
("plate", "plates"),
("play", "plays"),
("player", "players"),
("pocket", "pockets"),
("potato", "potatoes"),
("power", "powers"),
("printer", "printers"),
("problem", "problems"),
("program", "programs"),
("proof", "proofs"),
("puppy", "puppies"),
("queen", "queens"),
("quiz", "quizzes"),
("radio", "radios"),
("rain", "rains"),
("respect", "respects"),
("responsibility", "responsibilities"),
("river", "rivers"),
("road", "roads"),
("roof", "roofs"),
("room", "rooms"),
("safe", "safes"),
("school", "schools"),
("scissors", "scissors"),
("sea", "seas"),
("series", "series"),
("sheep", "sheep"),
("ship", "ships"),
("shoe", "shoes"),
("shop", "shops"),
("shore", "shores"),
("sister", "sisters"),
("sky", "skies"),
("snow", "snows"),
("solo", "solos"),
("son", "sons"),
("song", "songs"),
("sound", "sounds"),
("spoon", "spoons"),
("star", "stars"),
("station", "stations"),
("stone", "stones"),
("store", "stores"),
("story", "stories"),
("street", "streets"),
("student", "students"),
("studio", "studios"),
("success", "successes"),
("sun", "suns"),
("syllabus", "syllabuses"),
("table", "tables"),
("taco", "tacos"),
("teacher", "teachers"),
("team", "teams"),
("television", "televisions"),
("test", "tests"),
("thief", "thieves"),
("thing", "things"),
("time", "times"),
("tomato", "tomatoes"),
("tooth", "teeth"),
("town", "towns"),
("toy", "toys"),
("tree", "trees"),
("trousers", "trousers"),
("truth", "truths"),
("umbrella", "umbrellas"),
("valley", "valleys"),
("victory", "victories"),
("video", "videos"),
("village", "villages"),
("wall", "walls"),
("watch", "watches"),
("water", "waters"),
("wealth", "wealths"),
("week", "weeks"),
("wife", "wives"),
("window", "windows"),
("wisdom", "wisdoms"),
("wish", "wishes"),
("wolf", "wolves"),
("woman", "women"),
("wood", "woods"),
("word", "words"),
("work", "works"),
("year", "years"),
("zero", "zeros"),
(none, none),
)
#for (singular, expected) in plural_tests {
let result = __pluralize(singular)
if result != expected {
panic(
"Pluralization failed for: "
+ (if singular == none { "none" } else { str(singular) })
+ ". Expected: "
+ (if expected == none { "none" } else { str(expected) })
+ ", got: "
+ (if result == none { "none" } else { str(result) })
)
}
}
#set page(height: auto, width: auto, margin: 1em)
"Success! All pluralizations determined properly!"
+390
View File
@@ -0,0 +1,390 @@
// Brutal test for `glossy` functionality
//
// This file tries to exercise every known feature of `glossy` based on the demo.
// The goal is to cover all possible term definitions, modifiers, and reference forms.
//
// - Test terms with all combinations of fields: short, long, description, group, article, longarticle
// - Terms with missing fields
// - Terms with vowel/consonant starting for testing articles
// - Terms with conflicting modifiers (short/long/both), capitalization (cap), plural (pl), article (a/an), desc, def
// - Terms that appear and terms that are never referenced
// - Multiple glossaries with different themes and groups
// - Custom show-term callbacks
//
// This is not a narrative; it's a comprehensive brute-force test.
//
// Assumptions/Features Inferred:
// - @key references a glossary term by its key
// - Modifiers: short, long, both, cap, pl, a, an, def, desc
// - a/an modifiers can appear before or after the key
// - Terms can have custom 'article' or 'longarticle' fields to override default articles
// - Terms not referenced should not appear in a glossary unless forced
// - Conflicting modifiers: short/long/both - only one displays, in priority: both > long > short?
// - desc and def presumably show description or definition fields inline
// - The `show-term` callback in `init-glossary` modifies how inline references are displayed
// - `#glossary` can be called multiple times with different filters and themes
// - Terms can be grouped, and `#glossary` can be limited to certain groups
// - Terms with unusual keys or missing fields
// - Terms starting with vowels to test articles (e.g., 'an' vs 'a')
// - Terms that define custom `article` or `longarticle` fields
//
// We attempt to create a large dictionary with diverse entries.
#import "/lib.typ": *
#let exhaustiveGlossary = (
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 documents",
group: "Web"
),
// Term with no description
js: (
short: "JS",
long: "JavaScript",
group: "Web"
),
// Term with only a short field
tcp: (
short: "TCP",
group: "Networking"
),
// Term with only a long field
// should panic!
//udp: (
// long: "User Datagram Protocol",
// group: "Networking"
//),
// Term with short and description, but no long
ip: (
short: "IP",
description: "A unique identifier for a host or network interface",
group: "Networking"
),
// Term with custom articles and everything defined
api: (
short: "API",
long: "Application Programming Interface",
description: "A set of protocols and tools for building software",
article: "an", // custom article for short form
longarticle: "an", // custom article for long form
group: "Software"
),
// Term starting with a vowel, no custom article, to test default a/an logic
xml: (
short: "XML",
long: "Extensible Markup Language",
description: "A markup language defining rules for encoding documents",
group: "Data"
),
// Term starting with a consonant, no custom article
cpu: (
short: "CPU",
long: "Central Processing Unit",
description: "The primary component of a computer that performs calculations",
group: "Hardware"
),
// Term without any fields (just a string), should be handled gracefully
bare: "A bare term with no fields",
// Term with only description
// should panic!
//desconly: (
// description: "Just a description, no short or long",
// group: "Misc"
//),
// Term with article fields but missing short and long (should test fallback)
// should panic!
//articonly: (
// article: "an",
// longarticle: "a",
// description: "Has only articles and a description",
// group: "Misc"
//),
// Term with uppercase key
SSL: (
short: "SSL",
long: "Secure Sockets Layer",
description: "A protocol for encrypting information over the internet",
group: "Security"
),
// Term that will not be referenced
unusedterm: (
short: "UNUSED",
long: "Unused Term",
description: "Should not appear since it's never referenced"
),
// Term that tests pluralization with custom fields maybe
doc: (
short: "doc",
long: "document",
description: "A single piece of written matter",
group: "Misc"
),
// Terms that might require checking conflicting modifiers
tps: (
short: "TPS",
long: "test procedure specification",
description: "A document describing test procedures",
group: "Testing"
),
// Term that might appear multiple times
http: (
short: "HTTP",
long: "Hypertext Transfer Protocol",
description: "A protocol for distributed, collaborative, hypermedia information systems",
group: "Web"
)
)
// Initialize the glossary with a custom show-term callback
#show: init-glossary.with(
exhaustiveGlossary,
show-term: (body) => [#strong(body)]
)
#set heading(numbering: "1.1")
#set page(height: 9.5in, width: 7in, margin: 1em, numbering: "1")
= Brutal `glossy` Test
This document tries all reference forms and modifiers on a large set of terms.
== Basic references without modifiers
- @html
- @css
- @js
- @tcp
//- @udp // should panic!
- @ip
- @api
- @xml
- @cpu
- @bare
//- @desconly // should panic!
//- @articonly // should panic!
- @SSL
- @doc
- @tps
- @http
== Using plural modifier (pl)
- @html:pl
- @css:pl
- @js:pl
- @tcp:pl
//- @udp:pl // should panic!
- @ip:pl
- @api:pl
- @xml:pl
- @cpu:pl
- @bare:pl
//- @desconly:pl // should panic!
//- @articonly:pl // should panic!
- @SSL:pl
- @doc:pl
- @tps:pl
- @http:pl
== Using capitalization modifier (cap)
- @html:cap
- @css:cap
- @js:cap
- @tcp:cap
//- @udp:cap // should panic!
- @ip:cap
- @api:cap
- @xml:cap
- @cpu:cap
- @bare:cap
//- @desconly:cap // should panic!
//- @articonly:cap // should panic!
- @SSL:cap
- @doc:cap
- @tps:cap
- @http:cap
== Using articles (a/an) before the key
- @a:html (consonant start)
- @a:css
- @an:ip (starts with vowel sound?)
- @an:api (has custom article 'an')
- @a:cpu (consonant)
- @an:xml (starts with vowel)
//- @an:articonly (has custom article fields) // should panic!
- @an:SSL (S pronounced 'es')
- @a:doc
== Using articles after the key
- @html:a
- @css:an
- @ip:a
- @api:a (custom article should still apply)
- @cpu:an (might not make sense since CPU starts with C sound - test behavior)
- @xml:a
//- @articonly:an // should panic!
- @SSL:a
- @doc:an
== Using def and desc modifiers
- @html:def (show definition/long?)
- @css:desc (show description)
//- @js:def // should panic!
- @ip:desc
- @api:def
- @xml:desc
- @cpu:def
//- @desconly:def (no short/long, just desc) // should panic!
//- @articonly:desc // should panic!
- @tps:def
- @tps:desc
- @http:def
- @http:desc
== Using short, long, both modifiers
- @html:short
- @html:long
- @html:both
- @css:short
- @css:long
- @css:both
//- @udp:long (only has long) // should panic!
- @tcp:short (only has short)
- @ip:short
- @ip:long (no long defined, fallback?)
- @api:long
- @xml:both
- @cpu:both
- @doc:short
- @doc:long
- @doc:both
- @tps:short
- @tps:long
- @tps:both
== Conflicting multiple modifiers combined (short, long, both together)
- @tps:short:long
- @tps:short:both
- @tps:long:both
- @tps:short:long:both
- @html:both:long:short
- @doc:long:short
- @cpu:both:long:short
- @api:short:long:both
== Combining capitalization and plural with short/long/both
- @html:long:pl:cap
- @css:both:cap:pl
- @doc:short:pl:cap
- @api:both:pl:cap
- @ip:long:pl:cap (no long, see what happens)
- @bare:long:pl:cap (no fields)
//- @desconly:both:pl:cap (no short/long) // should panic!
//- @articonly:short:cap:pl (no short defined) // should panic!
- @SSL:both:pl:cap
- @tps:long:pl:cap
- @http:both:pl:cap
== Combining articles with other modifiers
- @a:html:long
- @an:ip:long
- @api:a:long (custom article)
//- @xml:an:both:pl (articles + plural? Should fail or ignore?) // should panic!
- @cpu:an:both:cap
- @SSL:a:short:cap
//- @doc:an:long:pl // should panic!
(We know articles + pl is not compatible, checking behavior.)
//== Attempt referencing nonexistent keys
//
//- @nonexistent (Should show something like ?? or error?) // should panic!
//- @nonexistent:long:pl:cap:a:def (extreme) // should panic!
== Create a big table testing all fields of one term
#table(
columns: 3,
table.header([*Input*],[*Output*],[*Comment*]),
[`@api` ], [@api], ["default"],
[`@api:short` ], [@api:short], ["short only"],
[`@api:long` ], [@api:long], ["long only"],
[`@api:both` ], [@api:both], ["both"],
[`@api:def` ], [@api:def], ["definition (long)"],
[`@api:desc` ], [@api:desc], ["description"],
[`@a:api` ], [@a:api], ["article before key"],
[`@api:a` ], [@api:a], ["article after key"],
[`@api:cap` ], [@api:cap], ["capitalize"],
[`@api:pl` ], [@api:pl], ["plural"],
[`@api:long:cap`], [@api:long:cap],["long + cap"],
[`@api:both:pl` ], [@api:both:pl], ["both + pl"]
)
== Multiple glossaries with themes and group filtering
= Glossaries
== Full Glossary (all terms)
#glossary(title: "Full Glossary", theme: theme-chicago-index)
== Web Only
#glossary(title: "Web Terms", groups: ("Web",), theme: theme-compact)
== Networking Only
#glossary(title: "Networking Terms", groups: ("Networking",), theme: theme-basic)
== Misc and Data
#glossary(title: "Misc & Data", groups: ("Misc","Data"), theme: theme-basic)
== Just Testing terms
#glossary(title: "Testing Terms", groups: ("Testing",), theme: theme-chicago-index)
== Hardware group
#glossary(title: "Hardware Terms", groups: ("Hardware",), theme: theme-chicago-index)
== Unreferenced entries
#glossary(title: "Unreferenced (Should Contain Unused?)", groups: ("Security",), theme: theme-chicago-index)
== Empty group glossary
#glossary(title: "Empty Group Glossary", groups: (""), theme: theme-basic)
= End of Brutal Test
Binary file not shown.

Before

Width:  |  Height:  |  Size: 651 KiB

After

Width:  |  Height:  |  Size: 651 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 651 KiB

After

Width:  |  Height:  |  Size: 651 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "glossy" name = "glossy"
version = "0.3.0" version = "0.4.0"
entrypoint = "lib.typ" entrypoint = "lib.typ"
authors = ["Stephen Waits <steve@waits.net>"] authors = ["Stephen Waits <steve@waits.net>"]
license = "MIT" license = "MIT"