Skip to content

Fonts: Create sanitized CSS @font-face font-family on upload - #76782

Draft
sirreal wants to merge 64 commits into
trunkfrom
trac-63568/uploaded-font-name-minimal-fix
Draft

sirreal wants to merge 64 commits into
trunkfrom
trac-63568/uploaded-font-name-minimal-fix

Conversation

@sirreal

@sirreal sirreal commented Mar 24, 2026 •

Copy link
Copy Markdown
Member

What?

Fixes Trac #63568 / #70426

Use quoted and escaped strings for @font-face font-family values of uploaded fonts.

The problem was that the REST API request was made with a plain string for the font-family, whereas the backend expects a string containing CSS text. Something like WordPress' font is invalid, it is unquoted (not a CSS string) the unescaped ' would start a CSS string.

With this change, arbitrary font names from uploaded fonts are likely to work well. The font family name is transformed into a CSS string with a number of CSS Unicode escape sequences to remove characters that are often problematic for other parts of the application like KSES or font normalization.

Why?

The REST API call was made incorrectly, providing a plain string instead of a string containing CSS text. This caused fonts with names containing special characters (e.g. O'Reilly Sans) to produce invalid CSS and break the fonts or more CSS on the site.

How?

  • createCssString() — New function that produces a properly quoted, Unicode-escaped CSS string, neutralizing characters problematic in CSS and WordPress HTML contexts.
  • CSSStyleSheet instead of FontFace API — Font faces are now inserted as @font-face rules into managed adoptedStyleSheets, giving full control over CSS output. loadFontFaceInBrowser is now synchronous.
  • Separated display name from CSS value — New FontFileMetadata type carries the raw fontDisplayName (for UI/slugs) separately from the escaped fontFamily (for CSS).

The FontFace API was very problematic because of its inconsistent behavior across browsers. It also expects a "plain" font-family string, which made it more difficult to use with the CSS font-family that the rest of the WordPress Font API expects to use. The family of a FontFace instance has inconsistent normalization and is not a CSS string, making it very difficult to compart correctly when attempting to unload a font face.

An E2E test is added with a problematic font name "Ephesis" font with <special \> {chars} & things, ya'know? to confirm it's working well.

Testing Instructions

  1. Site Editor > Styles > Typography > Manage Fonts > Upload. (/wp-admin/site-editor.php?p=%2Fstyles&section=%2Ftypography)
  2. Upload a font with a single quote in its name (e.g. "O'Reilly Sans" from Trac #63568). This PR also includes a font that can be used for testing at test/e2e/assets/Ephesis-modified-name.ttf. It has more and different special characters.
  3. Assign to an element, save, reload — font should persist.
  4. Check frontend — font should render.
  5. Verify a normal font still uploads and works correctly.

Check for any regressions:

  • Try using fonts provided by a theme, like Fira Code and Manrope in twentytwentyfive.
  • Try various Google Font installations and ensure that they continue to work correctly.

Known issues

I discovered some issues with font display name and font-family that are edge cases that should not impact the font's behavior. These are related to some sanitization that was introduced in #58636.

The expected font-family is incorrectly sanitized by the REST API (these are the decoded values):

-"Ephesis" font with <special \> {chars} & things, ya'know?
+"Ephesis"font with <special \>{chars}&things,ya'know?

This is incorrect, although it may be harmless. It appears to be the result of sanitize_text_field() stripping "extra" whitespace. This could be addressed in this PR by Unicode escaping carriage returns, line feeds, horizontal tabs, and spaces but that seems excessive. The collapsing of multiple spaces to a single space should maintain correct Unicode escapes, but it means that the following whitespace disappears because it is consumed by the Unicode escape: \3E following text becomes \3E following text which is decoded as >following text instead of the expected > following text. Because the font-family mostly just needs to match, this issue should not be critical although it is worth addressing in follow-up work.

Font display names are also sanitized in a destructive way.

Font families are stored in a wp_font_family post type, where the font family name is stored in the post title. JSON is stored in the post content.

The family name is sanitized, so a family name is changed:

-"Ephesis" font with <special \> {chars} & things, ya'know?
+"Ephesis" font with {chars} & things, ya'know?

The family name could be stored without additional sanitization in the JSON instead of the post title.

Use of AI Tools

Claude Code (Claude Opus 4.6) was used to perform some development tasks.
Various AI tools were used for PR feedback.

@sirreal sirreal added the [Status] In Progress Tracking issues with work in progress label Mar 24, 2026
@github-actions

github-actions Bot commented Mar 24, 2026 •

Copy link
Copy Markdown

Size Change: +716 B (+0.01%)

Total Size: 7.73 MB

📦 View Changed
Filename Size Change
build/scripts/edit-site/index.min.js 335 kB +9 B (0%)
build/scripts/editor/index.min.js 532 kB +707 B (+0.13%)

compressed-size-action

@github-actions

github-actions Bot commented Mar 24, 2026 •

Copy link
Copy Markdown

Flaky tests detected in 45adc19.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/29041733920
📝 Reported issues:

@sirreal
sirreal force-pushed the trac-63568/uploaded-font-name-minimal-fix branch from 83fa554 to 6613fb8 Compare March 25, 2026 12:35
@sirreal
sirreal requested a review from Copilot March 25, 2026 20:23

This comment was marked as outdated.

@sirreal
sirreal force-pushed the trac-63568/uploaded-font-name-minimal-fix branch from c5ea4cb to fa23641 Compare March 26, 2026 19:18
@sirreal
sirreal requested a review from Copilot March 26, 2026 19:18
@sirreal sirreal added [Type] Bug An existing feature does not function as intended [Feature] Font Library labels Mar 26, 2026

This comment was marked as outdated.

@sirreal
sirreal force-pushed the trac-63568/uploaded-font-name-minimal-fix branch from 4674759 to 3498d76 Compare March 27, 2026 09:52
@sirreal
sirreal requested a review from Copilot March 27, 2026 09:52

This comment was marked as outdated.

This comment was marked as outdated.

This comment was marked as outdated.

@sirreal
sirreal marked this pull request as ready for review March 27, 2026 15:39
@sirreal sirreal removed the [Status] In Progress Tracking issues with work in progress label Mar 27, 2026
@sirreal sirreal mentioned this pull request Mar 27, 2026
3 of 6 tasks
@github-actions

github-actions Bot commented Mar 27, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @Copilot.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Unlinked contributors: Copilot.

Co-authored-by: sirreal <jonsurrell@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@sirreal

This comment was marked as outdated.

@sirreal

sirreal commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

Testing the full round trip through core surfaced two problems with createCssString() as implemented here (shortest-form escapes with space terminators, literal spaces preserved, quoted-string output).

1. The output is not stable through core's sanitization.

WP_Font_Utils::sanitize_font_family() applies sanitize_text_field(), which:

  • collapses whitespace runs and trims the ends,
  • strips % followed by two hex digits (percent-octet stripping, in a loop),
  • strips <…> sequences as tags.

Escapes shorter than six digits need a whitespace terminator when the following character is a hex digit or a space, and that terminator interacts badly with whitespace collapsing. Example: Tom & Jerry escapes to "Tom \26 Jerry" — terminator space plus literal space. Sanitization collapses the two spaces to one, the CSS parser consumes that single space as the escape terminator, and the literal space is gone: the stored family is Tom &Jerry. Client-computed and server-stored values now name different families, so the @font-face rule created after install doesn't match the preview rule loaded at upload time, unloading can't find the rule to remove, and the display name is silently mangled. Same class of failure for % + hex (Font 50%AB → Font 50) and literal tabs.

The e2e test doesn't catch this because its assertions match /Ephesis/ loosely rather than the full special-character name.

2. Core prints the value unquoted.

WP_Font_Face_Resolver::maybe_parse_name_from_comma_separated_list() strips the surrounding quotes from the stored value, and WP_Font_Face only re-adds quotes when the value contains a literal space — which never happens once spaces are escaped. So the escaped content is emitted into the front-end @font-face as a bare identifier sequence. Characters that are harmless inside a CSS string but invalid in an identifier — ?, a leading digit, - followed by a digit — produce an invalid rule that browsers drop silently.

Constraints these impose on the escaper:

  • The output must be a byte-identical fixed point of WP_Font_Utils::sanitize_font_family(): no literal whitespace, <, %, quotes, commas, or semicolons anywhere. In practice that means escaping every character outside [A-Za-z0-9_-]/non-ASCII — an allowlist, since a blocklist amounts to betting against sanitize_text_field() internals.
  • Escapes must use the fixed six-digit form (\XXXXXX), which needs no whitespace terminator, so the output contains no whitespace at all (e.g. Exo 2 → "Exo\0000202").
  • A leading digit or - + digit must also be escaped (12345 → "\0000312345") so the content parses as a valid unquoted identifier sequence.
  • The same bytes must decode to the same name in both contexts: as a quoted string (as stored) and as a bare identifier sequence (as printed by core).

The fixed-point property of the reworked escaper was verified against the actual sanitizer with a PHP round trip, and unit round-trip tests plus exact-name e2e assertions pin down encode → sanitize → quote-strip → parse → same name. The rework is on trac-63568/uploaded-font-name-minimal-v2.

Resolve conflicts in packages/global-styles-ui after the move to vitest.
Use the trunk package.json and package-lock.json, and drop @jest/globals.
Keep the gutenberg-env types in tsconfig.json for globalThis.SCRIPT_DEBUG.
Import the createCssString test helpers from vitest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

📦 Bundle size

Size Change: +593 B (+0.01%)

Total Size: 8.29 MB

📦 View Changed
Filename Size Change
build/scripts/edit-site/index.min.js 386 kB +16 B (0%)
build/scripts/editor/index.min.js 619 kB +577 B (+0.09%)

86cf7ba Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric a19c81a trunk % Change
timeToFirstByte 57 ms +8.77% -3.07% 59.4 ms +8.5% -6.06% -4.04%
largestContentfulPaint 100 ms +0% -8% 100 ms +0% -8% 0%
lcpMinusTtfb 37.95 ms +13.7% -12.12% 36 ms +13.33% -8.75% 5.42%
wpBeforeTemplate 28.71 ms +19.02% -1.85% 28.91 ms +18.26% -2.21% -0.69%
wpTemplate 24.23 ms +4.83% -3.05% 24.28 ms +4.37% -1.61% -0.21%
wpTotal 53.33 ms +9.02% -2.68% 55.85 ms +7.7% -6.5% -4.51%
wpMemoryUsage 7.63 MB +0% -0% 7.59 MB +0% -0% 0.54%
wpDbQueries 17 +0% -0% 17 +0% -0% 0%

front-end-classic-theme

Metric a19c81a trunk % Change
timeToFirstByte 48.2 ms +6.95% -1.97% 48.15 ms +3.84% -1.56% 0.1%
largestContentfulPaint 104 ms +3.85% -0% 104 ms +3.85% -3.85% 0%
lcpMinusTtfb 56.65 ms +0.97% -1.5% 54.15 ms +4.43% -2.86% 4.62%
wpBeforeTemplate 26.86 ms +13.74% -0.97% 26.94 ms +1.45% -1.6% -0.3%
wpTemplate 17.83 ms +3.2% -1.01% 17.79 ms +4.44% -1.24% 0.22%
wpTotal 45.28 ms +7.33% -2.08% 45.14 ms +3.59% -1.48% 0.31%
wpMemoryUsage 6.26 MB +0% -0% 6.21 MB +0% -0% 0.8%
wpDbQueries 14 +0% -0% 14 +0% -0% 0%

media-processing

Metric a19c81a trunk % Change
mediaProcessingJpeg 399.23 ms +1.87% -0.26% 396.36 ms +1.68% -0.29% 0.72%
mediaProcessingAvif 6027.31 ms +0.56% -0.07% 6040.22 ms +0.1% -0.12% -0.21%
mediaProcessingJpegToAvif 4156.49 ms +0.5% -0.19% 4157.36 ms +0.13% -0.04% -0.02%

media-upload

Metric a19c81a trunk % Change
jpegUploadProcessing 1441.36 ms +34.92% -2.62% 1408.33 ms +1.15% -0.08% 2.35%
pngUploadProcessing 177.31 ms +15.9% -2.94% 171.1 ms +10.46% -1.91% 3.63%
largeJpegUploadProcessing 1396.79 ms +1.57% -0.31% 1408.14 ms +0.45% -0.34% -0.81%
multipleImageUploadProcessing 1594.51 ms +29.58% -3.26% 1547.35 ms +2.44% -1.15% 3.05%

post-editor

Metric a19c81a trunk % Change
serverResponse 390.27 ms +6.76% -4.59% 378.23 ms +3.98% -1.51% 3.18%
firstPaint 214.62 ms +8.27% -6.69% 198.61 ms +30.79% -8.65% 8.06%
domContentLoaded 964.5 ms +0.52% -2.06% 943.26 ms +0.84% -4.66% 2.25%
loaded 965.61 ms +0.52% -2.07% 944.37 ms +0.83% -4.67% 2.25%
firstContentfulPaint 414.01 ms +2.16% -3.62% 397.95 ms +3.73% -16.42% 4.04%
firstBlock 2716.3 ms +0.27% -0.57% 2700.45 ms +0.66% -0.57% 0.59%
type 16.88 ms +2.31% -1.66% 16.9 ms +3.02% -1.54% -0.12%
typeWithoutInspector 16.15 ms +4.95% -2.41% 16.72 ms +2.33% -4.61% -3.41%
typeWithTopToolbar 21.64 ms +4.76% -4.07% 21.29 ms +3.1% -5.87% 1.64%
typeContainer 8.14 ms +9.46% -1.97% 8.15 ms +5.89% -8.96% -0.12%
focus 62.56 ms +14.08% -2.54% 64.15 ms +11.52% -2.6% -2.48%
firstFocus 190.25 ms +0% -0% 169.07 ms +0% -0% 12.53%
selectAll 384.75 ms +3.84% -1.63% 378.12 ms +2.88% -1.33% 1.75%
listViewOpen 54.09 ms +6.06% -6.82% 52.76 ms +5.29% -8.4% 2.52%
inserterOpen 20.92 ms +6.74% -4.3% 21.42 ms +19.05% -6.54% -2.33%
inserterHover 2.02 ms +7.43% -12.38% 1.86 ms +16.67% -16.67% 8.6%
inserterSearch 7.78 ms +3.86% -4.5% 7.39 ms +12.58% -5.41% 5.28%
loadPatterns 564.22 ms +11.06% -3.14% 568.4 ms +6.33% -2.1% -0.74%
wpTotal 380.6 ms +6.86% -4.58% 368.74 ms +3.97% -1.69% 3.22%
wpMemoryUsage 13.18 MB +0% -0% 13.14 MB +0% -0% 0.33%
wpDbQueries 54 +0% -0% 54 +0% -0% 0%

site-editor

Metric a19c81a trunk % Change
serverResponse 260.46 ms +2.58% -0.98% 259.39 ms +7.34% -5.49% 0.41%
firstPaint 166.74 ms +24.01% -10.99% 161.19 ms +98.74% -15.07% 3.44%
domContentLoaded 735.95 ms +0.41% -2.12% 739.32 ms +2.5% -2.96% -0.46%
loaded 736.97 ms +0.41% -2.11% 740.38 ms +2.53% -2.96% -0.46%
firstContentfulPaint 314.17 ms +2.88% -3.87% 320.43 ms +1.87% -3.46% -1.95%
firstBlock 2534.34 ms +0.95% -0.74% 2617.47 ms +3.02% -1.9% -3.18%
type 13.9 ms +3.24% -5.4% 14.2 ms +8.66% -3.8% -2.11%
navigate 94.66 ms +2.84% -11.49% 87.04 ms +23.44% -10.5% 8.75%
loadPatterns 909.18 ms +1.93% -5.99% 923.94 ms +2.7% -6.9% -1.6%
loadPages 973.94 ms +8.27% -1.94% 984.2 ms +4.13% -0.98% -1.04%
wpTotal 253.93 ms +2.59% -1.08% 252.73 ms +7.32% -5.73% 0.47%
wpMemoryUsage 12.14 MB +0% -0% 12.10 MB +0% -0% 0.36%
wpDbQueries 44 +0% -2.27% 44 +0% -0% 0%

86cf7ba Run

🏁 Flaky tests

Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

should cut and paste individual blocks with collapsed selection in /test/e2e/specs/editor/various/copy-cut-paste.spec.js, passed after 1 failed attempt.
Error: expect(string).toMatchSnapshot(expected) failed

  @@ -1,3 +1,3 @@
   <!-- wp:paragraph -->
  -<p>2Cut - collapsed selection</p>
  +<p>2</p>
   <!-- /wp:paragraph -->


    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/copy-cut-paste.spec.js:45:49
three users concurrently edit a large post with diverse blocks in /test/e2e/specs/editor/collaboration/collaboration-stress.spec.ts, passed after 1 failed attempt.
TimeoutError: locator.waitFor: Timeout 17500ms exceeded.
Call log:
  - waiting for getByRole('button', { name: /Collaborators list/ }) to be visible

    at map (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/collaboration/fixtures/collaboration-utils.ts:193:7)
    at CollaborationUtils.waitForMutualDiscovery (/home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/collaboration/fixtures/collaboration-utils.ts:188:10)
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/collaboration/collaboration-stress.spec.ts:416:28

86cf7ba Run

matiasbenedetto and others added 3 commits October 5, 2026 13:21
A browser does not throw on an invalid font-family descriptor in
`insertRule()`. It removes the descriptor and keeps the rule. Older data,
theme.json files, and font collections on older WordPress versions can
hold a plain name that is not valid CSS, such as `Exo 2`.

For such a name, `getCssFontFaceRule()` returned a rule with no family.
`unloadFontFaceInBrowser()` then compared only the style and the weight,
and deleted every managed font face with the same values.

Now `getCssFontFaceRule()` rejects a rule without a font family. It then
tries again with the first name of the value as a CSS string.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Remove the `FontFileMetadata` type. `getFontFaceMetadata()` keeps the
plain name in `fontFamily`, as in trunk. `makeFamiliesFromFaces()` and
the upload preview convert the name to a CSS string with
`createCssString()`.

The behavior does not change: the upload still trims the name, skips a
font with an empty name, and sends a CSS string to the REST API.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`getCssFontFaceRule()` now writes only the font family, style, weight,
and source, as the trunk code did with the FontFace API. The other
descriptors are not necessary for a preview of the font.

`formatFontFaceName()` has no callers, because the loader now reads
the font family as CSS.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@matiasbenedetto

Copy link
Copy Markdown
Contributor

Hi @sirreal. I tested this PR together with the core PR WordPress/wordpress-develop#13610 in Chrome. I uploaded eight fonts with special names, for example O'Reilly Sans, ACME, Sans, Font (Pro), 12345, and your Ephesis test font. With both PRs, all eight fonts load in the editor and on the frontend, and the Font Library previews are correct. Without this PR, the core PR rejects names with (, ;, or <, and it splits ACME, Sans into two names.

I pushed three commits to this branch. Please review them, and revert any part that you do not agree with.

1. 315167d: Reject font face rules without a font family (bug fix)

Chrome does not throw in insertRule() for an invalid font-family descriptor. It removes the descriptor and keeps the rule. Collections on older WordPress versions, theme files, and old data can hold plain names that are not valid CSS, such as Exo 2 or Slabo 27px. For these names, the preview rule had no family. Then unloadFontFaceInBrowser() compared only style and weight, and deleted every managed face with the same values.

  • insertCssFontFaceRule() now returns null if the rule has no font-family.
  • getCssFontFaceRule() then tries the first name of the value as a CSS string. Old core uses the same first name on the frontend.
  • I tested this with the built module before and after the change. Before, an unload of Exo 2 also removed Roboto and Inter. After, it removes only Exo 2.

2. ef0e98f: Keep the upload changes close to trunk

I removed the FontFileMetadata type. getFontFaceMetadata() keeps the plain name in fontFamily, as trunk does. makeFamiliesFromFaces() and the upload preview call createCssString(). The trim, the empty-name check, and the name table guards stay. These three files changed 111 lines against trunk before, and now they change 39. The behavior does not change.

3. 346d863: Simplify the font face rule and remove formatFontFaceName

  • getCssFontFaceRule() now writes only font-family, font-style, font-weight, and src, as the trunk FontFace code did. The preview does not get unicode-range or the other optional descriptors. The frontend does not change, because core writes its own @font-face CSS.
  • formatFontFaceName() has no callers after this PR, so I deleted it.

This commit reverses two of your design choices. If you want the more exact preview, please restore the descriptors.

Related change in core

The core PR now escapes , in serialize_font_family_name(), as createCssString() does. Before, core stored "ACME, Sans" with a literal comma, and formatFontFamily() split it into "ACME", "Sans" in the Font Library preview.

The `lint:tsconfig` check requires a dev project for TypeScript test
files, and `global-styles-ui` has none. The test has no TypeScript
syntax, and the other tests in this package are JavaScript files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@matiasbenedetto

matiasbenedetto commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Testing evidence: core trunk with and without this PR, 86 edge-case fonts

I tested this PR (86cf7bad0dc) with core trunk (57c4558589), without the core fix in WordPress/wordpress-develop#13610. I used the 86 fonts and the test steps of the font name test guide in that PR. The test ran in Chromium 149, Chrome 153, and Firefox 151, with Playwright and Twenty Twenty-Five.

For each case, the test deletes all fonts, uploads the case font in Appearance → Fonts → Upload, sets the font as the body text font, and opens the home page. A case passes when all of these are true:

  • The stored fontFamily decodes to one font name with the exact text of the case.
  • document.fonts.load() finds a loaded face with that name.
  • Text in that name, and the body text on the page, have the same width as text in a control font with the same glyphs.

Summary

The table shows how many of the 57 cases that must work pass, and how many of all 86 fonts work.

Setup Chromium 149 Chrome 153 Firefox 151
Core trunk 29 of 57 (29 of 86 fonts) 29 of 57 (29 of 86 fonts) 29 of 57 (29 of 86 fonts)
Core trunk + this PR 40 of 57 (45 of 86 fonts) 40 of 57 (45 of 86 fonts) 40 of 57 (46 of 86 fonts)
Core PR #13610 + this PR 57 of 57 (79 of 86 fonts) 57 of 57 (79 of 86 fonts) 57 of 57 (79 of 86 fonts)

The last row comes from the videos on the core PR.

  • This PR fixes 19 cases on trunk: 1, 2, 3, 4, 6, 7, 9, 10, 30, 31, 32, 35, 37, 38, 54, 55, 67, 81, and 82.
  • 3 cases work on trunk but fail with this PR: 33, 34, and 42. This PR sends an escape with its space, for example "Tom \26 Jerry". Core trunk changes the two spaces to one, so it stores "Tom \26 Jerry". The escape uses the only space, and the name becomes Tom &Jerry. The text renders in the font, but under a different name. Core PR Fonts: Keep font names through CSS validation, storage, and output wordpress-develop#13610 keeps the spaces, and these cases pass with both PRs.
  • 17 cases that must work still fail with this PR on trunk. Core trunk changes or rejects the value. These cases need Fonts: Keep font names through CSS validation, storage, and output wordpress-develop#13610:
    • No face (cases 11, 12, 13, 20, 22, 23, 39, 40, 43): trunk prints the name without quotes in @font-face, for example font-family:A=B;. The browser rejects the rule.
    • Changed name (cases 27, 28, 33, 34, 36, 41, 42): trunk removes percent sequences or a space after an escape.
    • Rejected (case 44): trunk reads the name 0 as empty and returns 400.
  • Browser differences:
    • Case 60 (revert-layer) with this PR: trunk prints font-family:revert-layer; without quotes. Chromium and Chrome reject the rule. Firefox accepts it, so the case passes only in Firefox.
    • Case 67 (A[U+000A]B) on trunk without this PR: Firefox sends no REST request. Chromium and Chrome store the name A B. With this PR, the case passes in all three browsers.

Videos: core trunk + this PR

Chromium 149

edge-4-trunk-and-gutenberg-pr-chromium.mp4
Chrome 153 and Firefox 151

Chrome 153

edge-4-trunk-and-gutenberg-pr-chrome.mp4

Firefox 151

edge-4-trunk-and-gutenberg-pr-firefox.mp4

Videos: core trunk only

Chromium 149

edge-1-trunk-chromium.mp4
Chrome 153 and Firefox 151

Chrome 153

edge-1-trunk-chrome.mp4

Firefox 151

edge-1-trunk-firefox.mp4

In each video, case 1 plays at normal speed, and cases 2–87 play at 5× speed. Each video ends with a summary table.

Result of each case

When the browsers give different results, the cell shows each browser. "No face" means that the browser has no face with the name, because the @font-face rule is invalid. "Not sent" means that the editor sends no REST request.

Case Name Expected Core trunk Core trunk + this PR
1 O'Reilly Sans Works ✘ no face ✔
2 O"Reilly Sans Works ✘ stored as invalid CSS "O"Reilly Sans" ✔
3 O'Reilly "Sans" Works ✘ stored as invalid CSS "O'Reilly "Sans" ✔
4 Suisse BP Int'l Works ✘ no face ✔
5 ‘Curly’ “Quotes” Works ✔ ✔
6 'Leading apostrophe Works ✘ stored name Leading apostrophe ✔
7 Trailing quote" Works ✘ stored name Trailing quote ✔
8 ACME, Sans Limit (comma) ✘ stored as a list: ACME, Sans ✘ stored name ACME,Sans
9 A;B Works ✘ no face ✔
10 A{B} Works ✘ no face ✔
11 A=B Works ✘ no face ✘ no face
12 What? Works ✘ no face ✘ no face
13 A:B Works ✘ no face ✘ no face
14 Font (Display) Works ✔ ✔
15 Font [Beta] Works ✔ ✔
16 Font !important Works ✔ ✔
17 Dr. Font Works ✔ ✔
18 Font #1 Works ✔ ✔
19 Font @Home Works ✔ ✔
20 Font/Slash Works ✘ no face ✘ no face
21 A/*c*/B Limit (comment) ✘ no face ✘ no face
22 Bodoni* Works ✘ no face ✘ no face
23 Jost* Works ✘ no face ✘ no face
24 Rounded M+ 1c Works ✔ ✔
25 C++ Mono Works ✔ ✔
26 50% Gray Works ✔ ✔
27 Font 50%AB Works ✘ stored name Font 50 ✘ stored name Font 50
28 Font%2c Sans Works ✘ stored name Font Sans ✘ stored name Font Sans
29 Font, Sans Limit (comma) ✘ stored as a list: Font, Sans ✘ stored name Font,Sans
30 A\B Limit (escape) ✘ stored name A[U+000B] ✔
31 Trailing\ Works ✘ stored as invalid CSS "Trailing\" ✔
32 \0030 Limit (escape) ✘ stored name 0 ✔
33 Tom & Jerry Works ✔ ✘ stored name Tom &Jerry
34 Tom &amp; Jerry Works ✔ ✘ stored name Tom &amp;Jerry
35 A<B> Works ✘ stored name A ✔
36 Test </style> Sans Works ✘ stored name Test Sans ✘ stored name Test </style>Sans
37 </style><script>alert(1)</script> Works ✘ stored as an empty value ✔
38 <!-- x --> Works ✘ stored as an empty value ✔
39 url(javascript:alert(1)) Works ✘ no face ✘ no face
40 expression(alert(1)) Works ✘ no face ✘ no face
41 A"; color: red; x:" Works ✘ stored as invalid CSS "A"; color: red; x:" ✘ stored name A";color: red;x:"
42 A} body { color: red Works ✔ ✘ stored name A}body {color: red
43 12345 Works ✘ no face ✘ no face
44 0 Works ✘ 400 ✘ 400
45 -1 Font Works ✔ ✔
46 1942 report Works ✔ ✔
47 Press Start 2P Works ✔ ✔
48 --custom Works ✔ ✔
49 -apple-system Works ✔ ✔
50 serif Limit (keyword) ✘ stored as generic serif ✘ no face
51 Serif Limit (keyword) ✘ stored as generic serif ✘ no face
52 sans-serif Limit (keyword) ✘ stored as generic sans-serif ✘ no face
53 system-ui Limit (keyword) ✘ stored as generic system-ui ✘ no face
54 emoji Limit (keyword) ✘ stored as generic emoji ✔
55 fangsong Limit (keyword) ✘ stored as generic fangsong ✔
56 inherit Limit (keyword) ✘ stored as keyword inherit ✘ no face
57 INHERIT Limit (keyword) ✘ stored as keyword inherit ✘ no face
58 initial Limit (keyword) ✘ stored as keyword initial ✘ no face
59 unset Limit (keyword) ✘ stored as keyword unset ✘ no face
60 revert-layer Limit (keyword) ✘ stored as keyword revert-layer Chromium: ✘ no face
Chrome: ✘ no face
Firefox: ✔
61 default Limit (keyword) ✘ stored as keyword default ✘ no face
62 generic(kai) Limit (keyword) ✘ stored as generic generic(kai) ✘ no face
63 A B Limit (spaces) ✘ stored name A B ✘ stored name A B
64 Leading space Works (trimmed name) ✔ ✔
65 Trailing space Works (trimmed name) ✔ ✔
66 A[U+0009]B Limit (spaces) ✘ stored name A B ✘ stored name A B
67 A[U+000A]B Limit (spaces) Chromium: ✘ stored name A B
Chrome: ✘ stored name A B
Firefox: ✘ not sent
✔
68 A[U+00A0]B Works ✔ ✔
69 A[U+3000]B Works ✔ ✔
70 A[U+200B]B Works ✔ ✔
71 日本語 😀 Limit (slug) ✘ 400 ✘ 400
72 微软雅黑 Limit (slug) ✘ 400 ✘ 400
73 MS ゴシック Limit (slug) ✘ 400 ✘ 400
74 Ñandú Works ✔ ✔
75 Café Works ✔ ✔
76 Café Works ✔ ✔
77 وزیرمتن Limit (slug) ✘ 400 ✘ 400
78 A[U+202E]B Works ✔ ✔
79 Dev 👩[U+200D]💻 Works ✔ ✔
80 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA… Works ✔ ✔
81 A\A\A\A\A\A\A\A\A\A\A\A\A\A\A\A\A\A\… Works ✘ stored as invalid CSS "A\A\A\A\A\A\A\A\A\A\A\A\A\A\A\A\A\A… ✔
82 A[U+0000]B Works ✘ no face ✔
83 A[U+0001]B Rejected (400) ✘ no face ✘ no face
85 A[U+D800]B Rejected (400) ✘ 400 ✘ 400
86 (empty) Rejected (no name) ✘ 400 ✘ not sent
87 (one space) Rejected (no name) ✘ 400 ✘ not sent

Limits: I did not test WebKit or Safari. A script sets the body font in the global styles post, as an admin save does. It does not use the Styles UI.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Font Library [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants