Skip to content

_brand.yml: typography: fonts: source: file option fails #12267

Open

Description

@floholl

Bug description

The source: file option does not work for me as expected when trying to specify a custom font in a brand.yml. It insists the source field's value must be google (but I do use it successfully with source: system, for example).

Steps to reproduce

Copy follwing files to same test directory

_brand.yml

typography:
  fonts:
    - family: Amplitude-Light
      source: file
      files: Amplitude-Light.ttf
  base: Amplitude-Light
  headings: Amplitude-Light

_quarto.yml

title: Test

my.qmd

# Foo bar

- Test
- Foo
- Bar

Amplitude-Light.ttf

(not included here, but could be replaced by other suitable font file)

Compile

quarto render my.qmd --to html

Actual behavior

I am getting the following error. Notice in particular the claim that Field "source" has value file, which must instead be 'google', even though I use source: system with system fonts quite successfully on the same machine.

ERROR: Brand validation failed for /home/me/font_test/_brand.yml.

In file _brand.yml
(line 5, columns 7--12) property name files is invalid
4:       source: file
5:       files: Amplitude-Light.ttf
        ~~~~~~
6:   base: Amplitude-Light
ℹ The error happened in location typography:fonts:0:files.


In file _brand.yml
(line 4, columns 15--19) Field "source" has value file, which must instead be 'google'
3:     - family: Amplitude-Light
4:       source: file
                ~~~~~
5:       files: Amplitude-Light.ttf


Stack trace:

In file _brand.yml
(line 5, columns 7--12) property name files is invalid
4:       source: file
5:       files: Amplitude-Light.ttf
        ~~~~~~
6:   base: Amplitude-Light
ℹ The error happened in location typography:fonts:0:files.


In file _brand.yml
(line 4, columns 15--19) Field "source" has value file, which must instead be 'google'
3:     - family: Amplitude-Light
4:       source: file
                ~~~~~
5:       files: Amplitude-Light.ttf

    at readAndValidateYamlFromFile (file:///opt/quarto/bin/quarto.js:20482:15)
    at eventLoopTick (ext:core/01_core.js:175:7)
    at async projectResolveBrand (file:///opt/quarto/bin/quarto.js:40049:27)
    at async resolveFormats (file:///opt/quarto/bin/quarto.js:80545:23)
    at async renderContexts (file:///opt/quarto/bin/quarto.js:80377:21)
    at async renderFileInternal (file:///opt/quarto/bin/quarto.js:85654:20)
    at async renderFiles (file:///opt/quarto/bin/quarto.js:85604:17)
    at async renderProject (file:///opt/quarto/bin/quarto.js:86020:25)
    at async Command.actionHandler (file:///opt/quarto/bin/quarto.js:90654:32)
    at async Command.execute (file:///opt/quarto/bin/quarto.js:8111:13)

Expected behavior

Successful compilation with local font file.

Your environment

  • IDE: vim & terminal
  • OS: Debian

Quarto check output

Quarto 1.6.42
[✓] Checking environment information...
      Quarto cache location: /home/flo/.cache/quarto
[✓] Checking versions of quarto binary dependencies...
      Pandoc version 3.4.0: OK
      Dart Sass version 1.70.0: OK
      Deno version 1.46.3: OK
      Typst version 0.11.0: OK
[✓] Checking versions of quarto dependencies......OK
[✓] Checking Quarto installation......OK
      Version: 1.6.42
      Path: /opt/quarto/bin

[✓] Checking tools....................OK
      TinyTeX: (not installed)
      Chromium: (not installed)

[✓] Checking LaTeX....................OK
      Using: Installation From Path
      Path: /usr/bin
      Version: 2024

[✓] Checking basic markdown render....OK

[✓] Checking Python 3 installation....OK
      Version: 3.12.7
      Path: /usr/bin/python3
      Jupyter: 5.7.2
      Kernels: python3

[✓] Checking Jupyter engine render....OK

[✓] Checking R installation...........OK
      Version: 4.4.1
      Path: /usr/lib/R
      LibPaths:
        - /usr/local/lib/R/site-library
        - /usr/lib/R/site-library
        - /usr/lib/R/library
      knitr: 1.48
      rmarkdown: 2.27

[✓] Checking Knitr engine render......OK

Activity

  1. changed the title [-]_brand.yml: `typography:` > `fonts:` > `source: file` option fails[/-] [+]_brand.yml: `typography: fonts: source: file` option fails[/+] on Mar 11, 2025
  2. changed the title [-]_brand.yml: `typography: fonts: source: file` option fails[/-] [+]`_brand.yml`: `typography: fonts: source: file` option fails[/+] on Mar 11, 2025
  3. mcanouil commented on Mar 11, 2025

    @mcanouil
    Collaborator

    What if you use?

    typography:
      fonts:
        - family: Amplitude-Light
          source: file
          files:
            - path: Amplitude-Light.ttf

    https://posit-dev.github.io/brand-yml/brand/typography.html#comprehensive-example-with-font-definitions

    Note that Quarto mentions the complete documentation several times and is not exhaustive about brand.yml which has its own documentation.

  4. floholl commented on Mar 11, 2025

    @floholl
    Author

    Yes, indeed this did the trick - thanks! Was aware of the brand.yml documentation, actually, but clearly hadn't dug deep enough there.

  5. mcanouil commented on Mar 11, 2025

    @mcanouil
    Collaborator

    Maybe we could add that files expects an array of "path" in Quarto reference page.

  6. added
    documentationDoc improvements & quarto-web
    and removed
    bugSomething isn't working
    on Mar 11, 2025
  7. self-assigned this
    on Mar 12, 2025
  8. added a commit that references this issue on Mar 12, 2025
    d6302ce
  9. mcanouil commented on Mar 12, 2025

    @mcanouil
    Collaborator
  10. added a commit that references this issue on Mar 12, 2025
    43f93e1
  11. added
    yaml-validationIssues with YAML validation and autocompletion in quarto
    on Mar 12, 2025
  12. modified the milestones: v1.9, v1.10 on Feb 13, 2026
  13. modified the milestones: v1.10, v1.11 on Jul 30, 2026
  14. added 2 commits that reference this issue on Oct 25, 2026
    7921983
    c5da0de
  15. cderv commented on Oct 26, 2026

    @cderv
    Member

    I looked at this again while reviewing #12268. The misleading property name files is invalid error depends on the length of the string passed to files.

    With a short value, the validator gives the expected error:

    typography:
      fonts:
        - family: Amp
          source: file
          files: a.ttf
    (line 5, columns 14--19) Field "files" has value a.ttf, which must instead be an array of values, where each element must be at least one of: a string, an object
    

    With a longer one (Amp.ttf still works, Amp-L.ttf does not), it reports property name files is invalid and the source must be 'google' error instead:

    typography:
      fonts:
        - family: Amp
          source: file
          files: Amp-L.ttf
    (line 5, columns 7--12) property name files is invalid
    (line 4, columns 15--19) Field "source" has value file, which must instead be 'google'
    

    Same behavior from 1.6.42 to current dev. Among the anyOf branches, the validator keeps the one with the best error quality, then the smallest total span:

    // prune all but the anyOf error which reports
    // - the least bad overall error in the group
    // - or the error with the smallest total span (presumably showing the error that is the
    // easiest to fix)
    let bestResults: ValidationError[] = [];
    let bestError = [Infinity, Infinity];
    for (const resultGroup of innerResults) {
    let maxQuality = -Infinity;
    let totalSpan = 0;
    for (const result of resultGroup) {
    totalSpan += result.value.end - result.value.start;
    maxQuality = Math.max(maxQuality, errorTypeQuality(result));
    }
    const thisError = [maxQuality, totalSpan];
    if (errorComparator(thisError, bestError) === -1) {
    bestError = thisError;
    bestResults = resultGroup;
    }
    }

    Both branches seem to get the same quality here, so the span decides. The google branch errors span 9 characters (files + file), the file branch error spans the value. Any value of 9 characters or more loses, which is most real font file names. I think this is where to look when we improve the error selection.

  16. cderv commented on Oct 27, 2026

    @cderv
    Member

    I looked further into the length-dependent error from my previous comment, and the span tie-break is only the visible part. The real cause is a heuristic that stopped applying to closed objects.

    The anyOf pruning in the validator is supposed to rank "invalid property name" errors as the least helpful ones, so that a branch which accepted all the keys wins over a branch which rejects one:

    if (e.schemaPath.indexOf("propertyNames") !== -1) {
    // suggesting invalid property names is bad if there are other errors to report
    return 10;
    }

    This only looks for propertyNames in the schema path. When it was added, closed: true objects were compiled to a propertyNames enum, so it covered them. Since #2367 (Quarto 1.2), closed objects are validated with their own closed path:

    result = context.withSchemaPath("closed", () => {

    errors.ts was updated for closed at that time, but not this ranking (nor the isPropertyNamesError check just above it at L200). So for brand-font, the google, bunny and system branches report property name files is invalid with the same weight as the file branch reporting files must be an array, and the smallest span decides. A files value of 9 characters or more loses to the google branch.

    This is not specific to a string value. A single object also gets the misleading error:

    typography:
      fonts:
        - family: Amp
          source: file
          files:
            path: Amplitude-Light.ttf

    Workaround

    files takes a list, with either plain paths or objects with path:

    typography:
      fonts:
        - family: Amplitude-Light
          source: file
          files:
            - Amplitude-Light.ttf

    With closed added to both checks, all of these report Field "files" ... must instead be an array of values, and an unknown key next to a valid source still reports property name ... is invalid. Existing schema validation tests pass with it. As this changes which error wins for every anyOf of closed objects, I want to check the impact on other schemas before proposing it.

  17. cderv commented on Oct 27, 2026

    @cderv
    Member

    I did the impact check from my previous comment, and it changes what I wrote there. The closed path not getting the "invalid property name" ranking is not something to restore. It is what fixed #2349.

    In #2349, comments: utterances: with an unknown crossorigin key reported the whole mapping as must be 'false'. comments is an anyOf of false and a closed object. Since #2367, the closed error gets the same ranking as the false type error and wins on span, so the same input reports property name crossorigin is invalid. Adding closed to the two checks brings back the #2349 message everywhere a closed object sits next to a scalar in an anyOf.

    I validated generated invalid inputs (unknown key, wrong type for a known key, missing required key, key from another branch) against every anyOf with a closed object branch in our schemas: 61 places, 1519 inputs. With the closed checks, 31 errors get better and 103 get worse. For example, in _quarto.yml:

    website:
      search:
        location: navbar
        unknwn-key: x

    reports today:

    property name unknwn-key is invalid
    

    and with the change:

    Field "search" has value
    
    location: navbar
      unknwn-key: x
    
    The value must instead be `true` or `false`.
    

    Same for open-graph, twitter-card, page-footer, comments, navbar items, listing feed, and all brand light/dark values.

    What separates the two cases is whether the branch accepts the type of the value at all. In this issue, all brand-font branches are objects and the value is an object, so the branch to report is the one with the fewest key problems. In #2349, the false branch does not accept a mapping, so its error should not win over an error from inside the object branch. So I tried a second rule on top of the closed checks: an error saying the value itself has the wrong type for a branch ranks below any error from a branch whose type matched (and a null branch ranks last, otherwise a scalar front matter reports must be the null value).

    Against all 329 anyOf in our schemas, 5683 inputs (the ones above, plus wrong-type values, empty values, !expr, key:value and key=value strings), this changes 57 errors: 54 better, 3 neutral, none worse by my reading. It fixes the cases of this issue, and the better ones are mostly the same pattern elsewhere: a valid key reported as invalid by another branch (brand logo paths, funding, book chapters items, sidebar auto), or a mapping given to string | object reported as must be a string instead of naming the bad key (include-in-header, engines, date).

    The 3 neutral ones are brand-font with source: google and files:, which goes from property name files is invalid to source must be 'file'. Both readings make sense to me.

    I also tried error-importance tags on the brand-font branches only, as suggested in #2190 for case-by-case fixes. It fixes the files cases but makes source: file with a missing files report source must be 'google', so I did not go further with it.

    @cscheid this is the risk you raised in #12268 about making other errors worse. Since then quarto dev-call validate-yaml --json gives the full errors in JSON, but no test pins which error is reported across schemas. I could turn the inputs I used into such a test, with the expected error for each input, and propose the change with it. Would this be the kind of testing you wanted before touching the selection ?
    This is possibly rather low problem, considering all the work on q2, and maybe new validation tooling needs to do that better. I don't know much about it (what q2 uses).

    I'll leave this here, the list form in the workaround above works and the description from #12268 points to it.

  18. modified the milestones: v1.11, Future on Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationDoc improvements & quarto-webyaml-validationIssues with YAML validation and autocompletion in quarto

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions