Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d296f3a3f1
|
||
|
|
338fee564f
|
||
|
|
1e095a8b26
|
||
|
|
df889d8d72 | ||
|
|
c008b5bd86
|
||
|
|
842166c211
|
||
|
|
c37c6750c6
|
||
|
|
03d82458bb
|
||
|
|
725f657f49
|
||
|
|
cf139b66f6
|
||
|
|
461439d2f9
|
||
|
|
607814713a
|
||
|
|
4d94a3f7f2
|
||
|
|
bc011bfa6b
|
No files matched your search
@@ -1,13 +1,27 @@
|
|||||||
image: alpine/edge
|
image: alpine/edge
|
||||||
|
packages:
|
||||||
|
- git
|
||||||
|
|
||||||
secrets:
|
secrets:
|
||||||
- 88cce185-d2ec-4573-9546-e33f36f79bbf
|
- 88cce185-d2ec-4573-9546-e33f36f79bbf
|
||||||
sources:
|
|
||||||
- git+ssh://git@git.sr.ht/~swaits/typst-collection
|
environment:
|
||||||
|
# repo and user names on both sr.ht and github
|
||||||
|
REPO: typst-collection
|
||||||
|
USER: swaits
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- mirror-to-github: |
|
- fetch-all-branches: |
|
||||||
cd ~/typst-collection
|
cd "$HOME/$REPO"
|
||||||
ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts
|
|
||||||
# Explicitly fetch all branches & tags
|
|
||||||
git fetch origin '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' --prune
|
git fetch origin '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' --prune
|
||||||
git remote add github git@github.com:swaits/typst-collection.git
|
|
||||||
|
- get-github-public-key: |
|
||||||
|
ssh-keyscan -t rsa github.com >> "$HOME/.ssh/known_hosts"
|
||||||
|
|
||||||
|
- add-github-remote: |
|
||||||
|
cd "$HOME/$REPO"
|
||||||
|
git remote add github "git@github.com:$USER/$REPO.git"
|
||||||
|
|
||||||
|
- mirror-to-github: |
|
||||||
|
cd "$HOME/$REPO"
|
||||||
git push --mirror github
|
git push --mirror github
|
||||||
@@ -17,6 +17,9 @@ links below to their main page.
|
|||||||
into everyday docs.
|
into everyday docs.
|
||||||
- [**swank-tex**](https://typst.app/universe/package/swank-tex): Render those
|
- [**swank-tex**](https://typst.app/universe/package/swank-tex): Render those
|
||||||
funky logos for TeX, LaTeX, and friends.
|
funky logos for TeX, LaTeX, and friends.
|
||||||
|
- [**tasteful-pairings**](https://typst.app/universe/package/tasteful-pairings):
|
||||||
|
A carefully curated collection of font pairings focusing on readability and
|
||||||
|
typographic harmony.
|
||||||
|
|
||||||
## Templates
|
## Templates
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ different goals:
|
|||||||
### Import the package
|
### Import the package
|
||||||
|
|
||||||
```typst
|
```typst
|
||||||
#import "@preview/glossy:0.4.0": *
|
#import "@preview/glossy:0.5.1": *
|
||||||
```
|
```
|
||||||
|
|
||||||
### Defining Glossary Terms
|
### Defining Glossary Terms
|
||||||
@@ -186,8 +186,9 @@ Display the glossary using the `glossary()` function:
|
|||||||
|
|
||||||
```typst
|
```typst
|
||||||
#glossary(
|
#glossary(
|
||||||
title: "Web Development Glossary", // Optional: defaults to Glossary
|
title: "Web Development Glossary", // Optional: defaults to Glossary theme:
|
||||||
theme: my-theme, // Optionaal: defaults to theme-twocol
|
my-theme, // Optional: defaults to theme-academic
|
||||||
|
sort: true, // Optional: whether or not to sort the glossary
|
||||||
ignore-case: false, // Optional: ignore case when sorting terms
|
ignore-case: false, // Optional: ignore case when sorting terms
|
||||||
groups: ("Web") // Optional: Filter to specific groups
|
groups: ("Web") // Optional: Filter to specific groups
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
// Returns:
|
// Returns:
|
||||||
// dictionary: Normalized dictionary with all expected keys populated
|
// dictionary: Normalized dictionary with all expected keys populated
|
||||||
//
|
//
|
||||||
// Throws:
|
// Panics:
|
||||||
// - If 'short' key is missing when using normal (non-abbreviated) syntax
|
// - If 'short' key is missing when using normal (non-abbreviated) syntax
|
||||||
// - If 'short' value is not a string
|
// - If 'short' value is not a string
|
||||||
// - If 'short' value is an empty string
|
// - If 'short' value is an empty string
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
// Returns:
|
// Returns:
|
||||||
// dictionary: The glossary entry associated with the key
|
// dictionary: The glossary entry associated with the key
|
||||||
//
|
//
|
||||||
// Throws:
|
// Panics:
|
||||||
// - If the key does not exist in the glossary
|
// - If the key does not exist in the glossary
|
||||||
//
|
//
|
||||||
#let __get_entry(key) = {
|
#let __get_entry(key) = {
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
// content: The fully formatted term, including optional article, capitalization,
|
// content: The fully formatted term, including optional article, capitalization,
|
||||||
// usage tracking metadata, and the chosen form (short, long, or both).
|
// 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
|
// 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.
|
// get_article(mode): Returns the appropriate article string based on the mode.
|
||||||
// - If wants_article is false, returns an empty string.
|
// - If wants_article is false, returns an empty string.
|
||||||
// - If mode is "short", return the short article.
|
// - If mode is "short", return the short article.
|
||||||
@@ -337,19 +316,33 @@
|
|||||||
// Determine mode using the helper function
|
// Determine mode using the helper function
|
||||||
let mode = determine_mode(wants_both, wants_long, wants_short, is_first_use, long_available)
|
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
|
// Pluralize (if requested)
|
||||||
let (article, term) = capitalize_term(get_article(mode), get_term(mode))
|
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
|
// Construct and return the final output
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
context {
|
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)
|
link(label(key), term)
|
||||||
} else {
|
} else {
|
||||||
term
|
term
|
||||||
}
|
}
|
||||||
[#article#show-term(text)#metadata(term)#__term_label(key)]
|
[#article#show-term(linked-term)#metadata(term)#__term_label(key)]
|
||||||
// |^^^^^^^|^^^^^^^^^ |^^^^^^^^ |^^^^^^^^^^^^
|
// |^^^^^^^|^^^^^^^^^ |^^^^^^^^ |^^^^^^^^^^^^
|
||||||
// \_art. | | |
|
// \_art. | | |
|
||||||
// \_ apply user formatting function to the term
|
// \_ apply user formatting function to the term
|
||||||
@@ -387,6 +380,47 @@
|
|||||||
.join(", ")
|
.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.
|
// Initializes the glossary system and sets up term reference handling.
|
||||||
//
|
//
|
||||||
// This function is typically used with `#show: init-glossary`. It performs
|
// This function is typically used with `#show: init-glossary`. It performs
|
||||||
@@ -407,12 +441,13 @@
|
|||||||
// Returns:
|
// Returns:
|
||||||
// content: Processed document content with glossary functionality enabled
|
// content: Processed document content with glossary functionality enabled
|
||||||
//
|
//
|
||||||
// Throws:
|
// Panics:
|
||||||
// - If entries parameter is not a dictionary
|
// - If entries parameter is not a dictionary
|
||||||
//
|
//
|
||||||
#let init-glossary(
|
#let init-glossary(
|
||||||
entries,
|
entries,
|
||||||
show-term: (term-body) => term-body,
|
format-term: __default-format-term,
|
||||||
|
show-term: __default-show-term,
|
||||||
term-links: true,
|
term-links: true,
|
||||||
body
|
body
|
||||||
) = context {
|
) = context {
|
||||||
@@ -423,6 +458,7 @@
|
|||||||
let checked-entry = z.parse(entry, dict-schema, scope: ("dictionary entry",))
|
let checked-entry = z.parse(entry, dict-schema, scope: ("dictionary entry",))
|
||||||
checked-entries.insert(checked-key, checked-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-show-term = z.parse(show-term, z.function(), scope: ("show-term",))
|
||||||
let checked-body = z.parse(body, z.content(), scope: ("body",))
|
let checked-body = z.parse(body, z.content(), scope: ("body",))
|
||||||
|
|
||||||
@@ -457,7 +493,7 @@
|
|||||||
// Now see if this is an actual glossary term key
|
// Now see if this is an actual glossary term key
|
||||||
if __has_entry(key) {
|
if __has_entry(key) {
|
||||||
// Found in dictionary, render via __gls()
|
// 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 {
|
} else {
|
||||||
// Not one of ours, so just pass it through
|
// Not one of ours, so just pass it through
|
||||||
r
|
r
|
||||||
@@ -499,12 +535,13 @@
|
|||||||
// Returns:
|
// Returns:
|
||||||
// content: Formatted glossary content
|
// content: Formatted glossary content
|
||||||
//
|
//
|
||||||
// Throws:
|
// Panics:
|
||||||
// - If a requested group doesn't exist
|
// - If a requested group doesn't exist
|
||||||
//
|
//
|
||||||
#let glossary(
|
#let glossary(
|
||||||
title: "Glossary",
|
title: "Glossary",
|
||||||
theme: theme-twocol,
|
theme: theme-academic,
|
||||||
|
sort: true,
|
||||||
ignore-case: false,
|
ignore-case: false,
|
||||||
groups: (),
|
groups: (),
|
||||||
) = context {
|
) = context {
|
||||||
@@ -565,13 +602,17 @@
|
|||||||
group = if group == none { "" } else { group }
|
group = if group == none { "" } else { group }
|
||||||
|
|
||||||
// sort entries by case insensitivity if requested
|
// sort entries by case insensitivity if requested
|
||||||
let sorted_entries = current_entries
|
let sorted_entries = if sort {
|
||||||
// 1. create array of tuples with (lower [if ignore-case], entry)
|
current_entries
|
||||||
.map(e => { if ignore-case { (lower(e.short), e) } else { (e.short, e) } })
|
// 1. create array of tuples with (lower [if ignore-case], entry)
|
||||||
// 2. sort the tuples (by first element then second)
|
.map(e => { if ignore-case { (lower(e.short), e) } else { (e.short, e) } })
|
||||||
.sorted(key: t => t.first())
|
// 2. sort the tuples (by first element then second)
|
||||||
// 3. strip away the tuple's first element, leaving an array of entries
|
.sorted() // NOTE: sorted() is NOT language-aware
|
||||||
.map(t => t.last())
|
// 3. strip away the tuple's first element, leaving an array of entries
|
||||||
|
.map(t => t.last())
|
||||||
|
} else {
|
||||||
|
current_entries
|
||||||
|
}
|
||||||
|
|
||||||
// add entries to this group's output map
|
// add entries to this group's output map
|
||||||
output.insert(group, sorted_entries)
|
output.insert(group, sorted_entries)
|
||||||
|
|||||||
@@ -203,7 +203,7 @@
|
|||||||
|
|
||||||
if is_acronym {
|
if is_acronym {
|
||||||
// Use acronym rules
|
// Use acronym rules
|
||||||
let first_char = upper_word.slice(0,1)
|
let first_char = upper_word.codepoints().slice(0,1).first()
|
||||||
if acronym_vowels.contains(first_char) {
|
if acronym_vowels.contains(first_char) {
|
||||||
"an"
|
"an"
|
||||||
} else {
|
} else {
|
||||||
@@ -211,7 +211,7 @@
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Non-acronym rules
|
// Non-acronym rules
|
||||||
let first_char = lower_word.slice(0,1)
|
let first_char = lower_word.codepoints().slice(0,1).first()
|
||||||
|
|
||||||
// Special cases for words matching a set of known 'silent h' prefixes
|
// Special cases for words matching a set of known 'silent h' prefixes
|
||||||
let silent_h_words = (
|
let silent_h_words = (
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.5 MiB |
@@ -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]
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
first:
|
||||||
|
short: "DAL"
|
||||||
|
long: "first"
|
||||||
|
|
||||||
|
second:
|
||||||
|
short: "cab"
|
||||||
|
long: "second"
|
||||||
|
|
||||||
|
third:
|
||||||
|
short: "delta"
|
||||||
|
long: "third"
|
||||||
|
|
||||||
|
fourth:
|
||||||
|
short: "COO"
|
||||||
|
long: "fourth"
|
||||||
|
|
||||||
|
fifth:
|
||||||
|
short: "cuckoo"
|
||||||
|
long: "fifth"
|
||||||
|
|
||||||
|
sixth:
|
||||||
|
short: "ČR"
|
||||||
|
long: "sixth"
|
||||||
|
After Width: | Height: | Size: 183 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
#import "/lib.typ":*
|
||||||
|
|
||||||
|
#show: init-glossary.with(yaml("glossary.yml"))
|
||||||
|
|
||||||
|
@first, @second, @third, @fourth, @fifth, @sixth
|
||||||
|
|
||||||
|
#glossary(ignore-case: false, sort: true)
|
||||||
|
#line(length: 20em)
|
||||||
|
#glossary(ignore-case: true, sort: true)
|
||||||
|
#line(length: 20em)
|
||||||
|
#glossary(ignore-case: false, sort: false)
|
||||||
|
#line(length: 20em)
|
||||||
|
#glossary(ignore-case: true, sort: false)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
cab:
|
||||||
|
short: "cab"
|
||||||
|
long: "taxi cab"
|
||||||
|
|
||||||
|
coo:
|
||||||
|
short: "COO"
|
||||||
|
long: "Chief Operations Officer"
|
||||||
|
|
||||||
|
cuckoo:
|
||||||
|
short: "cuckoo"
|
||||||
|
long: "really crazy"
|
||||||
|
|
||||||
|
delta:
|
||||||
|
short: "delta"
|
||||||
|
long: "difference between two values"
|
||||||
|
|
||||||
|
dal:
|
||||||
|
short: "DAL"
|
||||||
|
long: "Delta Airlines"
|
||||||
|
|
||||||
|
čr:
|
||||||
|
short: "ČR"
|
||||||
|
long: "Česká Republika"
|
||||||
|
After Width: | Height: | Size: 133 KiB |
@@ -0,0 +1,20 @@
|
|||||||
|
#import "/lib.typ":*
|
||||||
|
|
||||||
|
#set text(lang: "cs", region: "CZ")
|
||||||
|
|
||||||
|
#show: init-glossary.with(yaml("glossary.yml"))
|
||||||
|
#glossary(ignore-case: true)
|
||||||
|
|
||||||
|
#line(length: 20em)
|
||||||
|
|
||||||
|
@čr
|
||||||
|
|
||||||
|
@cab
|
||||||
|
|
||||||
|
@coo
|
||||||
|
|
||||||
|
@cuckoo
|
||||||
|
|
||||||
|
@delta
|
||||||
|
|
||||||
|
@dal
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "glossy"
|
name = "glossy"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
entrypoint = "lib.typ"
|
entrypoint = "lib.typ"
|
||||||
authors = ["Stephen Waits <steve@waits.net>"]
|
authors = ["Stephen Waits <steve@waits.net>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
tests/**/out/
|
||||||
|
tests/**/diff/
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Stephen Waits <steve@waits.net>
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Tasteful Pairings
|
||||||
|
|
||||||
|
A carefully curated collection of font pairings for [Typst](https://typst.app/),
|
||||||
|
focusing on readability and typographic harmony. Each pairing combines a
|
||||||
|
sans-serif heading font with a serif body font, following established
|
||||||
|
typographic principles for clear visual hierarchy and optimal reading
|
||||||
|
experience.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```typ
|
||||||
|
#import "@preview/tasteful-pairings:0.1.0": *
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typ
|
||||||
|
#let pairing = font-pairings.at("modern-heritage")
|
||||||
|
|
||||||
|
#show heading: set text(font: pairing.heading)
|
||||||
|
#set text(font: pairing.body)
|
||||||
|
|
||||||
|
= Your Document Title
|
||||||
|
Your content here...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Pairings
|
||||||
|
|
||||||
|
- **`ibm-plex`** - Modern corporate-friendly superfamily
|
||||||
|
- **`noto`** - Extensive language support, highly legible
|
||||||
|
- **`source`** - Adobe's refined open-source pair
|
||||||
|
- **`friendly-weather`** - Open Fira Sans with space-efficient Merriweather
|
||||||
|
- **`android`** - Complete Roboto family look
|
||||||
|
- **`kindle`** - Amazon's reading-optimized fonts
|
||||||
|
- **`office`** - Classic Microsoft Office combination
|
||||||
|
- **`modern-heritage`** - Contemporary Montserrat with timeless Georgia Pro
|
||||||
|
- **`legible`** - Reading-focused Lexend and Literata
|
||||||
|
|
||||||
|
## Gallery
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
The pairings in this collection follow these principles:
|
||||||
|
|
||||||
|
1. **Sans for Headlines, Serif for Body** - Following traditional typographic
|
||||||
|
wisdom for clear hierarchy
|
||||||
|
2. **Readability First** - Each pairing is tested for optimal reading experience
|
||||||
|
3. **Complementary Characteristics** - Fonts are matched based on x-height,
|
||||||
|
weight balance, and character width
|
||||||
|
4. **Practical Applications** - Selected for real-world use cases from corporate
|
||||||
|
documents to digital reading
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT License](LICENSE).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#import "src/main.typ": font-pairings
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#let font-pairings = (
|
||||||
|
"ibm-plex": (
|
||||||
|
heading: "IBM Plex Sans",
|
||||||
|
body: "IBM Plex Serif",
|
||||||
|
description: "A modern corporate-friendly superfamily with a wide weight range. Clean, neutral, and highly legible."
|
||||||
|
),
|
||||||
|
|
||||||
|
"noto": (
|
||||||
|
heading: "Noto Sans",
|
||||||
|
body: "Noto Serif",
|
||||||
|
description: "Huge language coverage, highly legible, and neutral. Great for multilingual documents."
|
||||||
|
),
|
||||||
|
|
||||||
|
"source": (
|
||||||
|
heading: "Source Sans 3",
|
||||||
|
body: "Source Serif 4",
|
||||||
|
description: "Adobe's open-source pair. Clean, highly readable, and well-hinted for both screen and print."
|
||||||
|
),
|
||||||
|
|
||||||
|
"friendly-weather": (
|
||||||
|
heading: "Fira Sans",
|
||||||
|
body: "Merriweather",
|
||||||
|
description: "Open, friendly sans plus a space-efficient modern serif. Good for text-heavy documents."
|
||||||
|
),
|
||||||
|
|
||||||
|
"android": (
|
||||||
|
heading: "Roboto",
|
||||||
|
body: "Roboto Serif",
|
||||||
|
description: "A complete Android-friendly look. Roboto is extremely common; Roboto Serif is its matching new serif companion."
|
||||||
|
),
|
||||||
|
|
||||||
|
"kindle": (
|
||||||
|
heading: "Amazon Ember",
|
||||||
|
body: "Bookerly",
|
||||||
|
description: "The Kindle pairing. Bookerly is a warm, book-like serif; Amazon Ember is a clean, readable sans."
|
||||||
|
),
|
||||||
|
|
||||||
|
"office": (
|
||||||
|
heading: "Calibri",
|
||||||
|
body: "Cambria",
|
||||||
|
description: "A classic Microsoft Office pairing. Calibri (sans) for headings, Cambria (serif) for body text."
|
||||||
|
),
|
||||||
|
|
||||||
|
"modern-heritage": (
|
||||||
|
heading: "Montserrat",
|
||||||
|
body: "Georgia Pro",
|
||||||
|
description: "Merges the warm, traditional forms of Georgia Pro with Montserrat’s clean, geometric lines. A modern-yet-classic pairing, equally suited to formal reports or stylish web layouts."
|
||||||
|
),
|
||||||
|
|
||||||
|
"legible": (
|
||||||
|
heading: "Lexend",
|
||||||
|
body: "Literata",
|
||||||
|
description: "An ultra-legible pairing designed for reading fluency. Lexend’s tall x-height and open shapes reduce visual stress, while Literata brings a warm, bookish feel to body text."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
After Width: | Height: | Size: 994 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
#import "/lib.typ": *
|
||||||
|
|
||||||
|
#set page(margin: 1em, height: auto, width: 5.5in)
|
||||||
|
#set par(justify: true, linebreaks: "optimized")
|
||||||
|
|
||||||
|
#let keys = font-pairings.keys().sorted()
|
||||||
|
|
||||||
|
#for key in keys {
|
||||||
|
block(
|
||||||
|
stroke: 1pt + navy,
|
||||||
|
inset: 1em,
|
||||||
|
radius: 1em,
|
||||||
|
[
|
||||||
|
#let pairing = font-pairings.at(key)
|
||||||
|
#show heading: set text(font: pairing.heading, weight: "bold")
|
||||||
|
= #pairing.heading & #pairing.body #h(1fr) #text(fill: eastern, weight: "light", size: 0.8em, baseline: -3pt, ["#key"])
|
||||||
|
#set text(font: pairing.body, weight: "light")
|
||||||
|
#pairing.description
|
||||||
|
|
||||||
|
#lorem(20)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 366 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "tasteful-pairings"
|
||||||
|
version = "0.1.0"
|
||||||
|
entrypoint = "lib.typ"
|
||||||
|
authors = ["Stephen Waits <steve@waits.net>"]
|
||||||
|
license = "MIT"
|
||||||
|
description = "A carefully curated collection of font pairings."
|
||||||
|
keywords = ["font", "pairing", "typography"]
|
||||||
|
repository = "https://github.com/swaits/typst-collection"
|
||||||
|
exclude = ["thumbnail.png", "tests"]
|
||||||