diff --git a/preview/glossy/0.2.2/.gitignore b/preview/glossy/0.2.2/.gitignore new file mode 100644 index 0000000..f1ca904 --- /dev/null +++ b/preview/glossy/0.2.2/.gitignore @@ -0,0 +1,2 @@ +tests/**/out/ +tests/**/diff/ diff --git a/preview/glossy/0.2.2/LICENSE b/preview/glossy/0.2.2/LICENSE new file mode 100644 index 0000000..35d39a7 --- /dev/null +++ b/preview/glossy/0.2.2/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.2/README.md b/preview/glossy/0.2.2/README.md new file mode 100644 index 0000000..41b5e86 --- /dev/null +++ b/preview/glossy/0.2.2/README.md @@ -0,0 +1,239 @@ +# 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.2": * +``` + +### 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", // Optional: defaults to Glossary + theme: my-theme, // Optionaal: defaults to theme-twocol + ignore-case: false, // Optional: ignore case when sorting terms + 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 gallery image](themeshots.png) + +#### 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.2/lib.typ b/preview/glossy/0.2.2/lib.typ new file mode 100644 index 0000000..eeff265 --- /dev/null +++ b/preview/glossy/0.2.2/lib.typ @@ -0,0 +1,2 @@ +#import "src/gloss.typ": init-glossary, glossary +#import "src/themes.typ": * diff --git a/preview/glossy/0.2.2/src/gloss.typ b/preview/glossy/0.2.2/src/gloss.typ new file mode 100644 index 0000000..3fd2e15 --- /dev/null +++ b/preview/glossy/0.2.2/src/gloss.typ @@ -0,0 +1,466 @@ +#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) = { + // Convert term (key + count) to labels with their displayed page numbers + let page_labels = range(count).map(i => { + let label = __term_label(key, i) + let loc = locate(label) + ( + label, + numbering(__default(loc.page-numbering(), "1"), ..counter(page).at(loc)) + ) + }) + + // 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. +// +// 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, + ignore-case: false, + 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 } + + // sort entries by case insensitivity if requested + let sorted_entries = current_entries + // 1. create array of tuples with (lower [if ignore-case], entry) + .map(e => { if ignore-case { (lower(e.short), e) } else { (e.short, e) } }) + // 2. sort the tuples (by first element then second) + .sorted(key: t => t.first()) + // 3. strip away the tuple's first element, leaving an array of entries + .map(t => t.last()) + + // add entries to this group's output map + output.insert(group, sorted_entries) + } +} + + // 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.2/src/themes.typ b/preview/glossy/0.2.2/src/themes.typ new file mode 100644 index 0000000..8ff4163 --- /dev/null +++ b/preview/glossy/0.2.2/src/themes.typ @@ -0,0 +1,5 @@ +#import "themes/academic.typ": theme-academic +#import "themes/basic.typ": theme-basic +#import "themes/chicago-index.typ": theme-chicago-index +#import "themes/compact.typ": theme-compact +#import "themes/twocol.typ": theme-twocol diff --git a/preview/glossy/0.2.2/src/themes/academic.typ b/preview/glossy/0.2.2/src/themes/academic.typ new file mode 100644 index 0000000..1093fd2 --- /dev/null +++ b/preview/glossy/0.2.2/src/themes/academic.typ @@ -0,0 +1,50 @@ +// Inspired by academic textbook glossaries +#let theme-academic = ( + section: (title, body) => { + set text(font: "New Computer Modern") + heading(level: 1, title) + v(1em) + body + }, + + group: (name, index, total, body) => { + if name != "" and total > 1 { + v(1.5em) + align(left, text(weight: "bold", size: 1.2em, name)) + v(0.75em) + line(length: 100%, stroke: 0.5pt) + v(0.75em) + } + body + }, + + entry: (entry, index, total) => { + let short-display = text(weight: "bold", entry.short) + let long-display = if entry.long == none { + [] + } else { + [. #entry.long] + } + + let description = if entry.description == none { + [] + } else { + [. #entry.description] + } + + block( + below: 1em, + text( + size: 0.95em, + { + grid( + columns: (1fr, auto), + gutter: 0.75em, + [#short-display#long-display#description], + text(fill: rgb("#666666"), entry.pages) + ) + } + ) + ) + }, +) diff --git a/preview/glossy/0.2.2/src/themes/basic.typ b/preview/glossy/0.2.2/src/themes/basic.typ new file mode 100644 index 0000000..b756c27 --- /dev/null +++ b/preview/glossy/0.2.2/src/themes/basic.typ @@ -0,0 +1,70 @@ +// 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)] + ) + ) + ) + }, +) diff --git a/preview/glossy/0.2.2/src/themes/chicago-index.typ b/preview/glossy/0.2.2/src/themes/chicago-index.typ new file mode 100644 index 0000000..fa3c82e --- /dev/null +++ b/preview/glossy/0.2.2/src/themes/chicago-index.typ @@ -0,0 +1,47 @@ +// Inspired by the Chicago Manual of Style's index format +#let theme-chicago-index = ( + section: (title, body) => { + set par(hanging-indent: 1em) + heading(level: 1, smallcaps(title)) + body + }, + + group: (name, index, total, body) => { + if name != "" and total > 1 { + v(1em) + text(weight: "medium", style: "italic", name) + v(0.5em) + } + body + }, + + entry: (entry, index, total) => { + let short-display = text(weight: "regular", entry.short) + let long-display = if entry.long == none { + [] + } else { + [ — #entry.long] // Using em-dash for Chicago style + } + + let description = if entry.description == none { + [] + } else { + text(style: "italic", [: #entry.description]) + } + + block( + below: 0.65em, + text( + size: 0.9em, + { + grid( + columns: (1fr, auto), + gutter: 1em, + [#short-display#entry.label#long-display#description], + [#entry.pages] + ) + } + ) + ) + }, +) diff --git a/preview/glossy/0.2.2/src/themes/compact.typ b/preview/glossy/0.2.2/src/themes/compact.typ new file mode 100644 index 0000000..86a40e3 --- /dev/null +++ b/preview/glossy/0.2.2/src/themes/compact.typ @@ -0,0 +1,91 @@ +// 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), + align: left+bottom, + 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.2/src/themes/twocol.typ b/preview/glossy/0.2.2/src/themes/twocol.typ new file mode 100644 index 0000000..7183e6e --- /dev/null +++ b/preview/glossy/0.2.2/src/themes/twocol.typ @@ -0,0 +1,72 @@ +// 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,1fr,1em,auto), + align: (left, center, center, right), + [#short-display#entry.label#long-display#description], // Term with label + [#repeat(h(0.25em) + "." + h(0.25em))], // Dotted leader line + [ . ], // A 1em wide dot so we definitely get some break between term and pages + [#entry.pages] // Page references + ) + ) + }, +) diff --git a/preview/glossy/0.2.2/src/utils.typ b/preview/glossy/0.2.2/src/utils.typ new file mode 100644 index 0000000..0acab16 --- /dev/null +++ b/preview/glossy/0.2.2/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.2/tests/demo/ref/1.png b/preview/glossy/0.2.2/tests/demo/ref/1.png new file mode 100644 index 0000000..84a3c89 Binary files /dev/null and b/preview/glossy/0.2.2/tests/demo/ref/1.png differ diff --git a/preview/glossy/0.2.2/tests/demo/test.typ b/preview/glossy/0.2.2/tests/demo/test.typ new file mode 100644 index 0000000..b347f4f --- /dev/null +++ b/preview/glossy/0.2.2/tests/demo/test.typ @@ -0,0 +1,100 @@ +// a minimal example, also listed in the README + +#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"), +) + +#show: init-glossary.with(myGlossary, show-term: (body) => [#emph(body)]) + +#set heading(numbering: "1.1") +#set page(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! + +@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.2/tests/page-numbers/ref/1.png b/preview/glossy/0.2.2/tests/page-numbers/ref/1.png new file mode 100644 index 0000000..0658e21 Binary files /dev/null and b/preview/glossy/0.2.2/tests/page-numbers/ref/1.png differ diff --git a/preview/glossy/0.2.2/tests/page-numbers/ref/2.png b/preview/glossy/0.2.2/tests/page-numbers/ref/2.png new file mode 100644 index 0000000..65d36eb Binary files /dev/null and b/preview/glossy/0.2.2/tests/page-numbers/ref/2.png differ diff --git a/preview/glossy/0.2.2/tests/page-numbers/ref/3.png b/preview/glossy/0.2.2/tests/page-numbers/ref/3.png new file mode 100644 index 0000000..48b31dc Binary files /dev/null and b/preview/glossy/0.2.2/tests/page-numbers/ref/3.png differ diff --git a/preview/glossy/0.2.2/tests/page-numbers/ref/4.png b/preview/glossy/0.2.2/tests/page-numbers/ref/4.png new file mode 100644 index 0000000..5a79626 Binary files /dev/null and b/preview/glossy/0.2.2/tests/page-numbers/ref/4.png differ diff --git a/preview/glossy/0.2.2/tests/page-numbers/test.pdf b/preview/glossy/0.2.2/tests/page-numbers/test.pdf new file mode 100644 index 0000000..7c416df Binary files /dev/null and b/preview/glossy/0.2.2/tests/page-numbers/test.pdf differ diff --git a/preview/glossy/0.2.2/tests/page-numbers/test.typ b/preview/glossy/0.2.2/tests/page-numbers/test.typ new file mode 100644 index 0000000..706d873 --- /dev/null +++ b/preview/glossy/0.2.2/tests/page-numbers/test.typ @@ -0,0 +1,51 @@ +#import "/lib.typ": * + +#let myGlossary = ( + html: ( + short: "HTML", + long: "Hypertext Markup Language", + description: "A standard language for creating web pages", + ), + iphone: ( + short: "iPhone", + ), + css: ( + short: "CSS", + long: "Cascading Style Sheets", + description: "A language used for describing the presentation of a document", + ), + atom: ( + short: "atom", + ), + tps: ( + short: "TPS", + long: "test procedure specification", + description: "A document on how to run all the test procedures" + ), +) + +#show: init-glossary.with(myGlossary) + +#set page(numbering: "i") + +@html, @iphone, @css + +#pagebreak() + +#set page(numbering: "1") + +@html, @iphone, @css, @atom + +#pagebreak() + +#counter(page).update(1) +#set page(numbering: "A") + +@html, @iphone, @css, @tps + +#pagebreak() + +#set page(numbering: "I") +@css, @iphone, @tps + +#glossary(theme: theme-chicago-index, ignore-case: true) diff --git a/preview/glossy/0.2.2/tests/sorting/ref/1.png b/preview/glossy/0.2.2/tests/sorting/ref/1.png new file mode 100644 index 0000000..9cbd27f Binary files /dev/null and b/preview/glossy/0.2.2/tests/sorting/ref/1.png differ diff --git a/preview/glossy/0.2.2/tests/sorting/test.typ b/preview/glossy/0.2.2/tests/sorting/test.typ new file mode 100644 index 0000000..6754610 --- /dev/null +++ b/preview/glossy/0.2.2/tests/sorting/test.typ @@ -0,0 +1,40 @@ +#import "/lib.typ": * + +#let myGlossary = ( + html: ( + short: "HTML", + long: "Hypertext Markup Language", + description: "A standard language for creating web pages", + ), + iphone: ( + short: "iPhone", + long: "iPhone", + ), + css: ( + short: "CSS", + long: "Cascading Style Sheets", + description: "A language used for describing the presentation of a document", + ), + atom: ( + short: "atom", + long: "atom", + ), + 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 page(numbering: "1") + += Refer to our terms so they show up in the glossary +@html, @iphone:long, @css, @css:cap:pl, @atom:pl, @tps:long + += Default sorting +#glossary(theme: theme-compact) + += Case-insensitive sorting (`ignore-case: true`) +#glossary(ignore-case: true, theme: theme-compact) diff --git a/preview/glossy/0.2.2/tests/theme-gallery/ref/1.png b/preview/glossy/0.2.2/tests/theme-gallery/ref/1.png new file mode 100644 index 0000000..582ab7a Binary files /dev/null and b/preview/glossy/0.2.2/tests/theme-gallery/ref/1.png differ diff --git a/preview/glossy/0.2.2/tests/theme-gallery/test.typ b/preview/glossy/0.2.2/tests/theme-gallery/test.typ new file mode 100644 index 0000000..ce447b7 --- /dev/null +++ b/preview/glossy/0.2.2/tests/theme-gallery/test.typ @@ -0,0 +1,55 @@ +#import "/lib.typ": * + +#let myGlossary = ( + html: ( + short: "HTML", + long: "Hypertext Markup Language", + description: "A standard language for creating web pages", + ), + iphone: ( + short: "iPhone", + description: "Revolutionized phones." + ), + css: ( + short: "CSS", + long: "Cascading Style Sheets", + description: "A language used for describing the presentation of a document", + ), + atom: ( + short: "atom", + ), + 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 page( + margin: 1cm, + height: auto, + numbering: "1", +) + +#text(size: 24pt, weight: "bold")[`glossy`: theme gallery] + +#text(size: 1pt, fill: rgb(0,0,0,0))[ + First, we refer to each term at least once so they'll actually show up in the + glossaries. Our terms include: @atom, @iphone, @html:cap, @css, and @tps. +] +#v(-2em) + +#import "/src/themes.typ" +#for theme in dictionary(themes) { + block( + breakable: false, + width: 100%, + spacing: 2em, + inset: 1em, + stroke: 1pt+gray, + height: if theme.first() == "theme-twocol" { 1.5in } else { auto }, + glossary(title: raw(theme.first()), theme: theme.last(), ignore-case: true) + ) +} diff --git a/preview/glossy/0.2.2/themeshots.png b/preview/glossy/0.2.2/themeshots.png new file mode 100644 index 0000000..a1d63ee Binary files /dev/null and b/preview/glossy/0.2.2/themeshots.png differ diff --git a/preview/glossy/0.2.2/thumbnail.png b/preview/glossy/0.2.2/thumbnail.png new file mode 100644 index 0000000..7635045 Binary files /dev/null and b/preview/glossy/0.2.2/thumbnail.png differ diff --git a/preview/glossy/0.2.2/typst.toml b/preview/glossy/0.2.2/typst.toml new file mode 100644 index 0000000..4d87082 --- /dev/null +++ b/preview/glossy/0.2.2/typst.toml @@ -0,0 +1,11 @@ +[package] +name = "glossy" +version = "0.2.2" +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", "themeshots.pjng", "tests"]