Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/scripts/validate_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ class ValidateSchema
# But we should warn if it's missing for clarity
SCHEMA_V5_OPTIONAL_BUT_RECOMMENDED = %w[name description].freeze

CATEGORY_STYLES = %w[serif sans-serif monospace display script handwriting decorative].freeze
CATEGORY_SCRIPTS = %w[latin cjk arabic cyrillic hebrew devanagari thai other].freeze
CATEGORY_USE_CASES = %w[body heading code ui decorative caption].freeze

def initialize(args)
OptionParser.new do |opts|
opts.banner = "Usage: ruby validate_schema.rb [options]"
Expand Down Expand Up @@ -77,6 +81,8 @@ def validate_file(file)

validate_fonts_or_collections(file, content)

validate_categories(file, content)

# Common validations
validate_fonts(file, content)
validate_naming(file, content)
Expand Down Expand Up @@ -116,6 +122,50 @@ def validate_v5_schema(file, content)
end
end

def validate_categories(file, content)
categories = content["categories"]
return unless categories

unless categories.is_a?(Hash)
add_error(file, "categories must be a mapping (hash), got #{categories.class}")
return
end

validate_category_enum(file, categories, "style", CATEGORY_STYLES, array: false)
validate_category_enum(file, categories, "script", CATEGORY_SCRIPTS, array: true)
validate_category_enum(file, categories, "use_case", CATEGORY_USE_CASES, array: false)

return unless categories.key?("variable") &&
![true, false].include?(categories["variable"])

add_error(file, "categories.variable must be boolean, " \
"got #{categories["variable"].inspect}")
end

def validate_category_enum(file, categories, key, allowed, array:)
return unless categories.key?(key)

value = categories[key]
values = array ? normalize_array(value, key) : [value]
return if values.nil?

values.each do |v|
next if allowed.include?(v)

add_warning(file, "categories.#{key} unknown value '#{v}' " \
"— allowed: #{allowed.join(', ')}")
end
end

def normalize_array(value, key)
return [value] unless value.is_a?(Array)

value
rescue StandardError
add_error(file, "categories.#{key} must be string or array")
nil
end

def validate_v5_resource(file, name, resource)
# Google-sourced resources
if resource["source"] == "google"
Expand Down
23 changes: 23 additions & 0 deletions docs/guide/formula-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,29 @@ extract:
format: gzip # For GZIP files
```

## Categories

Optional structured classification used by the docs site's browse filters and the formula detail page badges. All fields are optional — omit the ones you don't know.

```yaml
categories:
style: sans-serif # Primary style classification
script: [latin, cjk] # Supported scripts (string or array)
variable: false # Whether the font is a variable font
use_case: body # Intended primary use case
```

### Controlled Vocabulary

| Field | Type | Allowed Values |
|-------|------|----------------|
| `style` | string | `serif`, `sans-serif`, `monospace`, `display`, `script`, `handwriting`, `decorative` |
| `script` | string or array | `latin`, `cjk`, `arabic`, `cyrillic`, `hebrew`, `devanagari`, `thai`, `other` |
| `variable` | boolean | `true`, `false` (inferred from `resources.format` if omitted) |
| `use_case` | string | `body`, `heading`, `code`, `ui`, `decorative`, `caption` |

Values outside this vocabulary emit a warning (not error) during schema validation, so the vocabulary can evolve as new fonts are added.

## Example: Complete Formula

```yaml
Expand Down
Loading