feat(glossy): add support for articles through modifiers

This commit is contained in:
swaits committed 2024-12-18 06:39:43 -07:00
1 parent e14331cfc0
commit 3dce3d1734
5 files changed
+247 -70

No files matched your search

+25
View File
@@ -79,7 +79,9 @@ Use the `init-glossary` function to initialize glossary entries:
Each glossary entry supports the following fields:
- `short` (required): Short form of the term
- `article` (optional): Article for use with the short form (i.e. "a", "an")
- `long` (optional): Long form of the term
- `longarticle` (optional): Article for use with the long form (i.e. "a", "an")
- `description` (optional): Term description (often a definition)
- `group` (optional): Category grouping
- `plural` (optional): Override automatic pluralization of short form
@@ -96,7 +98,9 @@ For example, the above glossary could be in this YAML file:
```yaml
html:
short: HTML
article: an
long: Hypertext Markup Language
longarticle: a
description: A standard language for creating web pages
group: Web
@@ -139,6 +143,8 @@ Available modifiers:
- **short**: Shows only short form
- **long**: Shows only long form
- **def** or **desc**: Shows the description
- **a** or **an**: Include the article (`an` is just an alias of `a`, they're
equivalent)
Modifiers can be combined with colons:
@@ -155,6 +161,25 @@ Modifiers can be combined with colons:
| `@tps:both:pl:cap` | "Technical procedure specifications (TPSes)" |
| `@tps:def` | "A formal document describing test steps and expected results" |
The `a`/`an` modifier is special because it can either precede or follow the
term's key.
For the English language, you don't need to define `article` and `longarticle`
in most cases. Glossy makes a decent attempt at computing those automatically.
For example:
| **Input** | **Output** |
| --------------------- | ---------------------------------------------- |
| `@a:tps`(first use) | "a test procedure specification (TPS)" |
| `@tps:an`(subsequent) | "a TPS" |
| `@an:tps:long:cap` | "A test procedure specification" |
| `@tps:long:an` | "a test procedure specifications" |
| `@tps:a:both:cap` | "A technical procedure specifications (TPSes)" |
Note that the `a`/`an` (article) modifier cannot be combined with the `pl`
(plural) modifier.
### Generating the Glossary
Display the glossary using the `glossary()` function:
+194 -68
View File
@@ -53,8 +53,10 @@
(
short: entry.short,
plural: entry.at("plural", default: __pluralize(entry.short)),
article: entry.at("article", default: __determine_article(entry.short)),
long: long,
longplural: entry.at("longplural", default: __pluralize(long)),
longarticle: entry.at("longarticle", default: __determine_article(long)),
description: entry.at("description", default: none),
group: entry.at("group", default: "")
)
@@ -176,95 +178,200 @@
// 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
// 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
// - "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 of its name.
// - "a" or "an": Prepend an article, chosen from the entry (short or long form).
// show-term (function): A function that renders the chosen term.
//
// 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
// - Without modifiers, the first use of a term shows "Long form (short form)",
// subsequent uses show only the short form.
// - Explicit modifiers ("long", "short", "both") override the default behavior.
// - If a requested long form doesn't exist, the logic falls back to the short form.
// - A usage counter tracks how many times a term has been referenced.
//
#let __gls(key, modifiers: array) = {
// Fetch the entry and prepare label
// Returns:
// 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) = {
// ---------------------------------------------------------------------------
// Check for illegal modifier combinations
// ---------------------------------------------------------------------------
if ("def" in modifiers or "desc" in modifiers) and modifiers.len() > 1 {
panic("Cannot use 'def'/'desc' with other modifiers.")
}
if ("a" in modifiers or "an" in modifiers) and ("pl" in modifiers) {
panic("Cannot use 'a'/'an' and 'pl' together.")
}
// ---------------------------------------------------------------------------
// Retrieve the glossary entry and its label
// ---------------------------------------------------------------------------
let entry = __get_entry(key)
let entry_label = __dict_label_str(key)
// Handle description mode
// ---------------------------------------------------------------------------
// If "def" or "desc" modifier is present, show the entry's description immediately
// ---------------------------------------------------------------------------
if "def" in modifiers or "desc" in modifiers {
return [#entry.description]
return show-term([#entry.description])
}
// Track term usage
// ---------------------------------------------------------------------------
// Manage term usage counting and determine if it's the first reference
// ---------------------------------------------------------------------------
let entry_counter = counter(entry_label)
entry_counter.step()
let key_index = entry_counter.get().first()
let first = key_index == 0
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: Apply pluralization if requested
// ---------------------------------------------------------------------------
// Helper Functions
// ---------------------------------------------------------------------------
// pluralize_term(singular, plural): Returns plural form if "pl" modifier is present and available;
// otherwise returns the singular form.
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)
if "pl" in modifiers and plural != none {
plural
} 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]
singular
}
}
// 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
// capitalize_term(article, term): If "cap" is in modifiers, capitalizes the first letter
// of whichever text appears first (the article if present, otherwise the term).
// Returns a tuple (article, term) which may be modified to have uppercase first letters.
let capitalize_term = (article, term) => {
if "cap" not in modifiers {
(article, term)
} else if article == "" {
// No article present, so capitalize the term's first letter.
(article, upper(term.first()) + term.slice(1))
} else {
// Article is present; capitalize its first letter instead.
(upper(article.first()) + article.slice(1), term)
}
}
// 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.
// - If mode is "long" or "both", return the long article.
// The calling logic ensures that when mode = "long" or "both", a long form exists.
let get_article = (mode) => {
let wants_article = "a" in modifiers or "an" in modifiers
if not wants_article {
""
} else if mode == "short" {
entry.article + " "
} else {
// mode == "long" or "both"
entry.longarticle + " "
}
}
// determine_mode(wants_both, wants_long, wants_short, is_first_use, long_available):
// Decides which mode ("short", "long", or "both") to use.
// Preference is given to explicit modifiers. If the requested mode can't be fulfilled due to no long form,
// fall back to "short".
// If no explicit mode is given, the default behavior is:
// - On first use and if a long form exists, "both".
// - Otherwise, "short".
let determine_mode = (wants_both, wants_long, wants_short, is_first_use, long_available) => {
if wants_both {
// Explicit request for "both":
// If a long form exists, use "both", otherwise fall back to "short".
if long_available {
"both"
} else {
"short"
}
} else if wants_long {
// Explicit request for "long":
// If a long form exists, use "long", otherwise fall back to "short".
if long_available {
"long"
} else {
"short"
}
} else if wants_short {
// Explicit request for "short":
// Always "short" regardless of availability.
"short"
} else {
// No explicit mode given:
// On first use with a long form available, default to "both".
// Otherwise, use "short".
if is_first_use and long_available {
"both"
} else {
"short"
}
}
}
// ---------------------------------------------------------------------------
// Determine desired options based on modifiers and entry.long availability
// ---------------------------------------------------------------------------
let wants_both = "both" in modifiers
let wants_long = "long" in modifiers
let wants_short = "short" in modifiers
let long_available = entry.long != none
// 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))
// ---------------------------------------------------------------------------
// Construct and return the final output
// ---------------------------------------------------------------------------
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)]
[#article#show-term(term)#metadata(term)#__term_label(key, key_index)]
// |^^^^^^^|^^^^^^^^^ |^^^^^^^^ |^^^^^^^^^^^^
// \_art. | | |
// \_ apply user formatting function to the term
// | |
// \_ metadata lets us label (ie it's "labelable")
// |
// \_ i.e. <__gloss.key.0>, etc.
// for backlink from glossary
}
}
@@ -352,10 +459,29 @@
// 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))
let (raw_key, ..raw_modifiers) = str(r.target).split(":")
// Determine if we need to swap the key and first modifier.
// Conditions for swapping:
// - The original key is "a" or "an" (case-insensitive),
// - That "key" does not correspond to an actual entry,
// - There is at least one modifier,
// - The first modifier corresponds to an existing entry key.
let can_swap = (lower(raw_key) == "a" or lower(raw_key) == "an") and not __has_entry(raw_key) and raw_modifiers.len() > 0 and __has_entry(raw_modifiers.first())
// If we can swap, use the first modifier as the key and insert "a" as the first modifier.
let (key, modifiers) = if can_swap {
(raw_modifiers.first(), ("a",) + raw_modifiers.slice(1))
} else {
(raw_key, raw_modifiers)
}
// 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)
} else {
// Not one of ours, so just pass it through
r
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB

After

Width:  |  Height:  |  Size: 843 KiB

+28 -2
View File
@@ -17,17 +17,18 @@
short: "TPS",
long: "test procedure specification",
description: "A document on how to run all the test procedures"),
unused: "Unused term, which shouldn't print in the glossary."
)
#show: init-glossary.with(myGlossary, show-term: (body) => [#emph(body)])
#set heading(numbering: "1.1")
#set page(numbering: "1")
#set page(height: auto, width: 6.5in, margin: 1em, 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!
Now make sure I get @tps:a:SHORT report by 2pm! @an:tps:cap is very important!!!
@tps:cap is defined as: @tps:def.
@@ -48,6 +49,31 @@ Now make sure I get your @tps:short reports by 2pm!
[`@tps:desc` ], [@tps:desc],
)
== Adding articles via `a`/`an` modifiers
You can add an article to your term by adding the `a` or `an` modifiers (they're
equivalent, just both available as a convenience). These modifiers are special
in that they can precede the term's key, like `@a:key`, or follow the key like
the other modifiers.
Default English modifiers are chosen unless the `article` and/or `longarticle`
keys are defined for the given entry.
Articles are incompatible with the `pl` (plural) modifier.
#table(
columns: 2,
table.header([*Input*], [*Output*]),
[`@a:tps:short` ], [@a:tps:short],
[`@a:tps:long` ], [@a:tps:long],
[`@an:tps:both` ], [@an:tps:both],
[`@an:tps:long:cap`], [@an:tps:long:cap],
[`@tps:a:long` ], [@tps:a:long],
[`@tps:a:short` ], [@tps:a:short],
[`@tps:an:both:cap`], [@tps:an:both:cap],
)
== Using conflicting modifiers (short, long, both)
These modifiers are semantically mutually exclusive, by they can be combined
Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 332 KiB