diff --git a/glossy/src/gloss.typ b/glossy/src/gloss.typ index e7b4f4c..ec7ee3e 100644 --- a/glossy/src/gloss.typ +++ b/glossy/src/gloss.typ @@ -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,7 +535,7 @@ // Returns: // content: Formatted glossary content // -// Throws: +// Panics: // - If a requested group doesn't exist // #let glossary( diff --git a/glossy/tests/format-term/ref/1.png b/glossy/tests/format-term/ref/1.png new file mode 100644 index 0000000..0e2e0db Binary files /dev/null and b/glossy/tests/format-term/ref/1.png differ diff --git a/glossy/tests/format-term/test.typ b/glossy/tests/format-term/test.typ new file mode 100644 index 0000000..c38ed49 --- /dev/null +++ b/glossy/tests/format-term/test.typ @@ -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] +