diff --git a/preview/glossy/0.2.1/LICENSE b/preview/glossy/0.2.1/LICENSE new file mode 100644 index 0000000..35d39a7 --- /dev/null +++ b/preview/glossy/0.2.1/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Stephen Waits + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/preview/glossy/0.2.1/README.md b/preview/glossy/0.2.1/README.md new file mode 100644 index 0000000..a55e4b6 --- /dev/null +++ b/preview/glossy/0.2.1/README.md @@ -0,0 +1,280 @@ +# Glossy + +This package provides utilities to manage and render glossaries within +documents. It includes functions to define and use glossary terms, track their +usage, and generate a glossary list with references to where terms are used in +the document. + +![image of sample output](thumbnail.png) + +## Motivation + +Glossy is heavily inspired by +[glossarium](https://typst.app/universe/package/glossarium), with a few key +different goals: + +1. Provide a simple interface which allows for complete control over glossary + display. To do this, `glossy`'s `#glossary()` function accepts a theme + parameter. The goal here is to separate presentation and logic. +2. Simplify the user interface as much as possible. Glossy has exactly two + exports, `init-glossary` and `glossary`. +3. Double-down on `glossy`'s excellent `@term` reference approach, completely + eliminating the need to make any calls to `gls()` and friends. +4. Mimic established patterns and best practices. For example, `glossy`'s + `#glossary()` function is intentionally similar (in naming and parameters) to + `typst`'s built-in `#bibliography`, to the degree possible. +5. Simplify the implementation. The `glossy` code is significantly shorter and + easier to understand. + +## Features + +- Define glossary terms with short and long forms, descriptions, and grouping +- Automatically tracks term usage in the document through `@labels` +- Supports modifiers to adjust how terms are displayed (capitalize, pluralize, etc.) +- Generates a formatted glossary section with backlinks to term occurrences +- Customizable themes for rendering glossary sections, groups, and entries +- Automatic pluralization of terms with custom override options +- Page number references back to term usage locations + +## Usage + +### Import the package + +```typst +#import "@preview/glossy:0.2.1": * +``` + +### Defining Glossary Terms + +Use the `init-glossary` function to initialize glossary entries: + +```typst +#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 stylesheet language used for describing the presentation of a document", + group: "Web" + ), + tps: ( + short: "TPS", + long: "test procedure specification", + description: "A formal document describing test steps and expected results", + // Optional: Override automatic pluralization + plural: "TPSes", + longplural: "test procedure specifications" + ) +) + +#show: init-glossary.with(myGlossary) +``` + +Each glossary entry supports the following fields: + +- `short` (required): Short form of the term +- `long` (optional): Long form of the term +- `description` (optional): Term description (often a definition) +- `group` (optional): Category grouping +- `plural` (optional): Override automatic pluralization of short form +- `longplural` (optional): Override automatic pluralization of long form + +You can also load glossary entries from a data file using #yaml(), #json(), or similar. + +For example, the above glossary could be in this YAML file: + +```yaml +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 stylesheet language used for describing the presentation of a document + group: Web + +tps: + short: TPS + long: test procedure specification + description: A formal document describing test steps and expected results + plural: TPSes + longplural: test procedure specifications +``` + +And then loaded during initialization as follows: + +```typst +#show: init-glossary.with(yaml("glossary.yaml")) +``` + +### Using Glossary Terms + +Reference glossary terms using Typst's `@reference` syntax: + +```typst +In modern web development, languages like @html and @css are essential. +The @tps:pl need to be submitted by Friday. +``` + +Available modifiers: + +- **cap**: Capitalizes the term +- **pl**: Uses the plural form +- **both**: Shows "Long Form (Short Form)" +- **short**: Shows only short form +- **long**: Shows only long form +- **def** or **desc**: Shows the description + +Modifiers can be combined with colons: + +| **Input** | **Output** | +| ------------------- | -------------------------------------------------------------- | +| `@tps` (first use) | "test procedure specification (TPS)" | +| `@tps` (subsequent) | "TPS" | +| `@tps:short` | "TPS" | +| `@tps:long` | "test procedure specification" | +| `@tps:both` | "test procedure specification (TPS)" | +| `@tps:long:cap` | "Test procedure specification" | +| `@tps:long:pl` | "test procedure specifications" | +| `@tps:short:pl` | "TPSes" | +| `@tps:both:pl:cap` | "Technical procedure specifications (TPSes)" | +| `@tps:def` | "A formal document describing test steps and expected results" | + +### Generating the Glossary + +Display the glossary using the `glossary()` function: + +```typst +#glossary( + title: "Web Development Glossary", + theme: my-theme, + groups: ("Web") // Optional: Filter to specific groups +) +``` + +### Customizing Term Display + +Control how terms appear in the document by providing a custom `show-term` function: + +```typst +#let emph-term(term-body) = { emph(term-body) } + +#show: init-glossary.with( + myGlossary, + show-term: emph-term +) +``` + +### Glossary Themes + +#### Included Themes + +Glossy comes with several built-in themes that can be used directly or serve as examples for custom themes: + +##### theme-twocol + +A professional two-column layout ideal for technical documentation. Features: + +- Two-column layout for efficient space usage +- Dotted leaders to page numbers +- Clear hierarchy with optional group headings +- Compact but readable formatting +- Terms in regular weight with long forms and descriptions inline + +```typst +#glossary(theme: theme-twocol) +``` + +##### theme-basic + +A traditional single-column layout similar to book glossaries. Features: + +- Bold terms with indented content +- Clear separation between entries +- Hanging indentation for wrapped lines +- Parenthetical long forms +- Page numbers with "pp." prefix +- Simple, clean design + +```typst +#glossary(theme: theme-basic) +``` + +##### theme-compact + +A space-efficient layout perfect for technical documents or appendices. Features: + +- Reduced vertical spacing +- Smaller font sizes for secondary information +- Color-coded term components +- Grid-based alignment +- Minimal decorative elements +- Gray text for supplementary information +- Bullet separators between components + +```typst +#glossary(theme: theme-compact) +``` + +#### Custom Themes + +Customize glossary appearance by defining a theme with three functions: + +```typst +#let my-theme = ( + // Main glossary section + section: (title, body) => { + heading(level: 1, title) + body + }, + + // Group of related terms + group: (name, index, total, body) => { + // index = group index, total = total groups + if name != "" and total > 1 { + heading(level: 2, name) + } + body + }, + + // Individual glossary entry + entry: (entry, index, total) => { + // index = entry index, total = total entries in group + let output = [#entry.short] + if entry.long != none { + output = [#output -- #entry.long] + } + if entry.description != none { + output = [#output: #entry.description] + } + block( + grid( + columns: (auto, 1fr, auto), + output, + repeat([#h(0.25em) . #h(0.25em)]), + entry.pages, + ) + ) + } +) +``` + +Entry fields available to themes: + +- `short`: Short form (always present) +- `long`: Long form (can be `none`) +- `description`: Term description (can be `none`) +- `label`: Term's dictionary label +- `pages`: Linked page numbers where term appears + +## License + +This project is licensed under the MIT License. diff --git a/preview/glossy/0.2.1/examples/readme.pdf b/preview/glossy/0.2.1/examples/readme.pdf new file mode 100644 index 0000000..12e41bf Binary files /dev/null and b/preview/glossy/0.2.1/examples/readme.pdf differ diff --git a/preview/glossy/0.2.1/examples/readme.typ b/preview/glossy/0.2.1/examples/readme.typ new file mode 100644 index 0000000..92bd9bc --- /dev/null +++ b/preview/glossy/0.2.1/examples/readme.typ @@ -0,0 +1,99 @@ +// a minimal example, also listed in the README + +#import "@preview/glossy:0.2.1": * + +#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"), +) + +#show: init-glossary.with(myGlossary, show-term: (body) => [#emph(body)]) + +#set heading(numbering: "1.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! + +@tps:cap is defined as: @tps:def. + += Examples with modifiers + +#table( + columns: 2, + table.header([*Input*], [*Output*]), + + [`@tps:short` ], [@tps:short], + [`@tps:long` ], [@tps:long], + [`@tps:both` ], [@tps:both], + [`@tps:long:cap` ], [@tps:long:cap], + [`@tps:long:pl` ], [@tps:long:pl], + [`@tps:short:pl` ], [@tps:short:pl], + [`@tps:both:pl:cap`], [@tps:both:pl:cap], + [`@tps:def` ], [@tps:def], + [`@tps:desc` ], [@tps:desc], +) + +== Using conflicting modifiers (short, long, both) + +These modifiers are semantically mutually exclusive, by they can be combined +syntactically. When multiple are used, `glossy` tries to make a smart choice on +which one to display. + +#table( + columns: 2, + table.header([*Input*], [*Output*]), + + [`@tps:short:long` ], [@tps:short:long], + [`@tps:short:both` ], [@tps:short:both], + [`@tps:long:both` ], [@tps:long:both], + [`@tps:short:long:both`], [@tps:short:long:both], +) + +#let my-theme = ( + section: (title, body) => { + heading(level: 1, title) + body + }, + group: (name, i, n, body) => { + if name != "" and n > 1 { + heading(level: 2, name) + } + body + }, + entry: (entry, i, n) => { + // i is the entry's index, n is total number of entries + let output = [#entry.short] + if entry.long != none { + output = [#output -- #entry.long] + } + if entry.description != none { + output = [#output: #entry.description] + } + block( + grid( + columns: (auto, 1fr, auto), + output, + repeat([#h(0.25em) . #h(0.25em)]), + entry.pages, + ) + ) + } +) + +#glossary(theme: my-theme) +#glossary(title: "Glossary with just empty group", groups: ("",), theme: theme-basic) +#glossary(title: "Glossary with just Web group", groups: ("Web",), theme: theme-compact) diff --git a/preview/glossy/0.2.1/lib.typ b/preview/glossy/0.2.1/lib.typ new file mode 100644 index 0000000..eeff265 --- /dev/null +++ b/preview/glossy/0.2.1/lib.typ @@ -0,0 +1,2 @@ +#import "src/gloss.typ": init-glossary, glossary +#import "src/themes.typ": * diff --git a/preview/glossy/0.2.1/src/gloss.typ b/preview/glossy/0.2.1/src/gloss.typ new file mode 100644 index 0000000..092a887 --- /dev/null +++ b/preview/glossy/0.2.1/src/gloss.typ @@ -0,0 +1,464 @@ +#import "./themes.typ": * +#import "./utils.typ": * + +#let __gloss_entries = state("__gloss_entries", (:)) +#let __gloss_used = state("__gloss_used", (:)) + +#let __gloss_label_prefix = "__gloss:" + +// Normalizes a dictionary entry by ensuring all required and optional keys exist +// with appropriate default values. +// +// Parameters: +// entry (dictionary): Input dictionary containing at least 'short' and optionally +// 'long', 'plural', 'longplural', 'description', and 'group' +// +// Returns: +// dictionary: Normalized dictionary with all expected keys populated +// +// Throws: +// - If 'short' key is missing +// - If 'short' value is not a string +// - If 'short' value is an empty string +// +#let __normalize_entry(entry) = { + // Validate required fields + if not "short" in entry { + panic("Entry must contain a 'short' key") + } + if type(entry.short) != "string" { + panic("Entry 'short' must be a string") + } + if entry.short.trim() == "" { + panic("Entry 'short' cannot be empty") + } + + // Extract values with defaults, using consistent none for missing optionals + let long = entry.at("long", default: none) + + // Return the normalized entry + ( + short: entry.short, + plural: entry.at("plural", default: __pluralize(entry.short)), + long: long, + longplural: entry.at("longplural", default: __pluralize(long)), + description: entry.at("description", default: none), + group: entry.at("group", default: "") + ) +} + +// Checks if a key exists in the glossary state. +// +// Parameters: +// key (string): The key to look up in the glossary entries +// +// Returns: +// boolean: True if the key exists in the glossary, false otherwise +// +#let __has_entry(key) = { + key in __gloss_entries.final().keys() +} + +// Updates the glossary state by adding or updating an entry. +// +// Parameters: +// key (string): The key under which to store the entry +// entry (dictionary): The normalized glossary entry to store +// +// Returns: +// none: Updates state as a side effect +// +#let __add_entry(key, entry) = { + __gloss_entries.update(state => { + state.insert(key, entry) + state + }) +} + +// Retrieves a glossary entry from the state. +// +// Parameters: +// key (string): The key of the entry to retrieve +// +// Returns: +// dictionary: The glossary entry associated with the key +// +// Throws: +// - If the key does not exist in the glossary +// +#let __get_entry(key) = { + let entries = __gloss_entries.final() + if key not in entries.keys() { + panic("Glossary error: Missing key '" + 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. +// +// Parameters: +// key (string): The glossary entry key +// index (int): The occurrence index of the term in the document +// +// Returns: +// label: A Typst label object for the term usage +// +#let __term_label(key, index) = { + label(__term_label_str(key, index)) +} + +// Updates the term usage count in the glossary state. +// +// Parameters: +// key (string): The glossary entry key +// count (int): The number of times the term has been used +// +// Returns: +// none: Updates state as a side effect +// +#let __save_term_usage(key, count) = { + __gloss_used.update(state => { + state.insert(key, count) + state + }) +} + +// 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 +// 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 +// +// 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 +// +#let __gls(key, modifiers: array) = { + // Fetch the entry and prepare label + let entry = __get_entry(key) + let entry_label = __dict_label_str(key) + + // Handle description mode + if "def" in modifiers or "desc" in modifiers { + return [#entry.description] + } + + // Track term usage + let entry_counter = counter(entry_label) + entry_counter.step() + let key_index = entry_counter.get().first() + let first = key_index == 0 + + __save_term_usage(key, key_index + 1) + + // Helper: Apply pluralization if requested + 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) + } 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] + } + } + + // 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 + + 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)] + } +} + +// Creates page number links back to each usage of a glossary term. +// +// This function generates a comma-separated list of page numbers, where each +// number links back to a usage of the term in the document. Duplicate page +// numbers are removed to avoid redundancy. +// +// Parameters: +// key (string): The glossary entry key +// count (int): Number of times the term appears in the document +// +// Returns: +// content: Comma-separated list of linked page numbers +// +// Example output: "1, 3, 5" where each number links to the term usage on that page +// +#let __create_backlinks(key, count) = { + // Generate labels for each term usage + let labels = for i in range(count) { + (__term_label(key, i),) + } + + // Get page numbers for each usage + let pages = labels + .map(l => locate(l)) + .map(loc => numbering( + __default(loc.page-numbering(), "1"), + loc.page() + )) + + // Create links, excluding duplicate page numbers + let seen = () + let links = for i in range(labels.len()) { + let label = labels.at(i) + let page = pages.at(i) + + if seen.contains(page) { + (none,) + } else { + seen.push(page) + (link(label, page),) + } + }.filter(l => l != none) + + // Join links with commas + links.join(", ") +} + +// Initializes the glossary system and sets up term reference handling. +// +// This function is typically used with `#show: init-glossary`. It performs +// three main tasks: +// 1. Validates and loads glossary entries into the state +// 2. Sets up reference handling to intercept and format term usage +// 3. Applies custom term formatting if provided +// +// Parameters: +// entries (dictionary): Dictionary of glossary entries where: +// - keys are term identifiers +// - values are entry dictionaries containing term details +// show-term ((content) => content): Optional function to customize term rendering +// Default: Returns term content unchanged +// body (content): Document content to process +// +// Returns: +// content: Processed document content with glossary functionality enabled +// +// Throws: +// - If entries parameter is not a dictionary +// +#let init-glossary( + entries, + show-term: (term-body) => term-body, + body +) = context { + // Validate entries parameter type + if type(entries) != dictionary { + panic("entries must be a dictionary, instead got: " + repr(entries)) + } + + // Process and store each glossary entry + for (key, entry) in entries { + __add_entry(key, __normalize_entry(entry)) + } + + // 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)) + } else { + r + } + } + + body +} + +// Renders a complete glossary with customizable formatting and grouping. +// +// The glossary displays all used terms, optionally grouped by category, with page +// references back to term usage. The appearance is controlled by a theme that +// defines how sections, groups, and entries are formatted. +// +// Parameters: +// title (string): Glossary section title (default: "Glossary") +// theme (dictionary): Controls glossary appearance with three functions: +// - section(title, body): Renders the main glossary section +// - title: The glossary title +// - body: Content containing all groups and entries +// - group(name, index, total, body): Renders a group of related terms +// - name: Group name +// - index: Zero-based group index +// - total: Total number of groups +// - body: Content containing the group's entries +// - entry(entry, index, total): Renders a single glossary entry +// - entry: Dictionary containing: +// - short: Short form of term +// - long: Long form of term (optional) +// - description: Term description (optional) +// - label: Term's dictionary label +// - pages: Linked page numbers where term appears +// - index: Zero-based entry index within group +// - total: Total entries in group +// groups (array): Optional list of groups to include +// If empty, includes all groups +// +// Returns: +// content: Formatted glossary content +// +// Throws: +// - If a requested group doesn't exist +// +#let glossary( + title: "Glossary", + theme: theme-twocol, + groups: () +) = context { + // Collect and organize entries by group + let output = (:) + let all_entries = __gloss_entries.final() + let all_used = __gloss_used.final() + + // Determine which groups to process + let all_groups = all_entries + .values() + .map(e => e.at("group")) + .dedup() + .sorted() + + let target_groups = if groups.len() == 0 { + all_groups + } else { + // Validate requested groups exist + for g in groups { + if g not in all_groups { + panic("Requested group not found: " + g) + } + } + groups + } + + // Process entries group by group + for group in target_groups { + let current_entries = () + + // Collect all used entries for this group + for (key, count) in all_used { + let entry = all_entries.at(key) + if entry.at("group") == group { + current_entries.push(( + short: entry.at("short"), + long: entry.at("long"), + description: entry.at("description"), + label: [#metadata(key)#__dict_label(key)], + pages: __create_backlinks(key, count) + )) + } + } + + // Add non-empty groups to output + if current_entries.len() > 0 { + group = if group == none { "" } else { group } + output.insert( + group, + current_entries.sorted(key: e => e.short) + ) + } +} + + // Render the glossary using the theme + let group_index = 0 + + (theme.section)( + title, + for (group, entries) in output { + (theme.group)( + group, + group_index, + output.len(), + for (i, entry) in entries.enumerate() { + (theme.entry)(entry, i, entries.len()) + } + ) + group_index += 1 + } + ) +} diff --git a/preview/glossy/0.2.1/src/themes.typ b/preview/glossy/0.2.1/src/themes.typ new file mode 100644 index 0000000..7775002 --- /dev/null +++ b/preview/glossy/0.2.1/src/themes.typ @@ -0,0 +1,236 @@ +// Theme definition for a two-column glossary layout with hierarchical sections +// +// This theme provides formatting for three levels of glossary content: +// 1. Section: The overall glossary container +// 2. Groups: Optional categorization of related terms +// 3. Entries: Individual glossary terms and their definitions +// +#let theme-twocol = ( + // Renders the main glossary section as a two-column layout + // Parameters: + // title: The glossary section title + // body: Content containing all groups and entries + section: (title, body) => { + set par.line(numbering: none) + heading(level: 1, title) + columns(2, body) + }, + + // Renders a group of related glossary terms + // Parameters: + // name: Group name (empty string for ungrouped terms) + // index: Zero-based group index + // total: Total number of groups + // body: Content containing the group's entries + group: (name, index, total, body) => { + if name != "" and total > 1 { + heading(level: 2, name) + } + body + }, + + // Renders a single glossary entry with term, definition, and page references + // Parameters: + // entry: Dictionary containing term data: + // - short: Short form of term + // - long: Long form of term (optional) + // - description: Term description (optional) + // - label: Term's dictionary label + // - pages: Linked page numbers where term appears + // index: Zero-based entry index within group + // total: Total entries in group + entry: (entry, index, total) => { + // Format the short form and optional long form of the term + let short-display = text(weight: "regular", entry.short) + let long-display = if entry.long == none { + [] + } else { + [#h(0.25em) -- #entry.long] + } + + // Format the optional description + let description = if entry.description == none { + [] + } else { + [: #entry.description] + } + + // Render the complete entry with dotted leader line to page numbers + text( + size: 0.75em, + weight: "light", + grid( + columns: (auto,auto,auto,1fr,1em,auto), + align: (left, left, left, center, center, right), + [#short-display#entry.label], // Term with label + [#long-display], // Long form (if any) + [#description], // Description (if any) + [#repeat(h(0.25em) + "." + h(0.25em))], // Dotted leader line + [ . ], // Final dot + [#entry.pages] // Page references + ) + ) + }, +) + +// Theme definition for a basic single-column glossary layout +// +// This theme provides a traditional glossary format with: +// 1. Bold terms +// 2. Indented descriptions +// 3. Simple page references +// 4. Optional group headings +// +#let theme-basic = ( + // Renders the main glossary section as a single column + // Parameters: + // title: The glossary section title + // body: Content containing all groups and entries + section: (title, body) => { + heading(level: 1, title) + body + }, + + // Renders a group of related glossary terms + // Parameters: + // name: Group name (empty string for ungrouped terms) + // index: Zero-based group index + // total: Total number of groups + // body: Content containing the group's entries + group: (name, index, total, body) => { + if name != "" and total > 1 { + heading(level: 2, name) + } + body + }, + + // Renders a single glossary entry with term, definition, and page references + // Parameters: + // entry: Dictionary containing term data: + // - short: Short form of term + // - long: Long form of term (optional) + // - description: Term description (optional) + // - label: Term's dictionary label + // - pages: Linked page numbers where term appears + // index: Zero-based entry index within group + // total: Total entries in group + entry: (entry, index, total) => { + // Format the term parts + let term = text(weight: "bold", entry.short) + let long-form = if entry.long == none { + [] + } else { + [, #entry.long] + } + + // Format the description with proper spacing + let description = if entry.description == none { + [] + } else { + [: #entry.description] + } + + // Create the complete entry with hanging indent + block( + spacing: 0.5em, + pad( + left: 1em, + bottom: 0.5em, + block( + [#term#entry.label#long-form#description #h(1em) (pp. #entry.pages)] + ) + ) + ) + }, +) + +// Theme definition for a compact glossary layout optimized for space efficiency +// +// This theme provides a dense but readable format with: +// 1. Terms and definitions on same line +// 2. Smaller font size +// 3. Minimal vertical spacing +// 4. Left-aligned groups for quick scanning +// 5. Condensed page references +// +#let theme-compact = ( + // Renders the main glossary section with minimal spacing + // Parameters: + // title: The glossary section title + // body: Content containing all groups and entries + section: (title, body) => { + set par(leading: 0.65em) + heading(level: 1, outlined: false, text(weight: "bold", size: 1.1em, title)) + body + }, + + // Renders a group of related glossary terms + // Parameters: + // name: Group name (empty string for ungrouped terms) + // index: Zero-based group index + // total: Total number of groups + // body: Content containing the group's entries + group: (name, index, total, body) => { + if name != "" and total > 1 { + block( + spacing: 0.5em, + pad( + top: 0.5em, + text(weight: "bold", size: 0.9em, name) + ) + ) + } + body + }, + + // Renders a single glossary entry with term, definition, and page references + // Parameters: + // entry: Dictionary containing term data: + // - short: Short form of term + // - long: Long form of term (optional) + // - description: Term description (optional) + // - label: Term's dictionary label + // - pages: Linked page numbers where term appears + // index: Zero-based entry index within group + // total: Total entries in group + entry: (entry, index, total) => { + // Format term components with minimal spacing + let term = text( + size: 0.65em, + weight: "medium", + fill: gray.darken(60%), + entry.short + ) + + let long-form = if entry.long == none { + [] + } else { + text(size: 0.65em, fill: gray.darken(20%), [ (#entry.long)]) + } + + let description = if entry.description == none { + [] + } else { + text(size: 0.65em, [ยท #entry.description]) + } + + // Create the complete entry with tight spacing + block( + spacing: 0.4em, + grid( + columns: (auto, 1fr, auto), + gutter: 0.5em, + // Term and description column + box[#term#entry.label#long-form #description], + // Dots.... + repeat(h(0.25em) + text(fill: gray, ".") + h(0.25em)), + // Page references with smaller font + text( + size: 0.6em, + fill: gray.darken(20%), + entry.pages + ) + ) + ) + }, +) diff --git a/preview/glossy/0.2.1/src/utils.typ b/preview/glossy/0.2.1/src/utils.typ new file mode 100644 index 0000000..0acab16 --- /dev/null +++ b/preview/glossy/0.2.1/src/utils.typ @@ -0,0 +1,38 @@ +// Returns the provided value if it is not none, otherwise returns the default value +// Parameters: +// val: The value to check +// default: The fallback value to use if val is none +// Returns: val if not none, otherwise default +#let __default(val, default) = if val == none { default } else { val } + +// Converts an English word to its plural form following standard English pluralization rules +// Parameters: +// word: The singular word to pluralize +// Returns: The pluralized form of the word, or none if input is none +#let __pluralize(word) = { + // early exit with invalid input + if word == none { + return none + } + + // Define helper functions for checking endings + let ends_with = (suffix) => lower(word).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) + if ends_with_any(( "s", "x", "z", "sh", "ch" )) { + word + "es" + } else if ends_with("y") and not ends_with_any(( "a", "e", "i", "o", "u" )) { + // Convert y to ies when y follows a consonant + word.slice(0, -1) + "ies" + } else if ends_with("f") { + // Convert f to ves + word.slice(0, -1) + "ves" + } else if ends_with("fe") { + // Convert fe to ves + word.slice(0, -2) + "ves" + } else { + // Default case: just add s + word + "s" + } +} diff --git a/preview/glossy/0.2.1/thumbnail.png b/preview/glossy/0.2.1/thumbnail.png new file mode 100644 index 0000000..7635045 Binary files /dev/null and b/preview/glossy/0.2.1/thumbnail.png differ diff --git a/preview/glossy/0.2.1/typst.toml b/preview/glossy/0.2.1/typst.toml new file mode 100644 index 0000000..289f335 --- /dev/null +++ b/preview/glossy/0.2.1/typst.toml @@ -0,0 +1,11 @@ +[package] +name = "glossy" +version = "0.2.1" +entrypoint = "lib.typ" +authors = ["Stephen Waits "] +license = "MIT" +description = "A very simple glossary system with easily customizable output." +categories = ["model"] +keywords = ["glossary", "index", "dictionary", "gloss"] +repository = "https://github.com/swaits/typst-collection" +exclude = ["thumbnail.png", "examples/readme.pdf"]