feat(glossy): add support for articles through modifiers
This commit is contained in:
5 files changed
+247
-70
No files matched your search
@@ -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
@@ -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 |
@@ -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 |
Reference in new issue
Block a user