Fonts: Keep font names through CSS validation, storage, and output - #13610
Draft
matiasbenedetto wants to merge 3 commits into
Draft
matiasbenedetto wants to merge 3 commits into
matiasbenedetto wants to merge 3 commits into
Conversation
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
Core applied text operations to CSS `font-family` values. Those operations changed a font name or made invalid CSS. A name with an apostrophe produced the invalid declaration `font-family:O'Reilly Sans;`, so the browser did not use the font. A name with a comma became two families. `sanitize_text_field()` also removed percent sequences, collapsed spaces, and stripped markup. Add `WP_CSS_Font_Family`. The class reads the CSS `font-family` grammar and returns the decoded name of each family, with its type. The serializer writes a decoded name back as a quoted CSS string. It escapes the quote character, the backslash, the control characters, and `<`, `>`, and `&`, so that a name survives HTML output and the KSES post filters. Use the class in these places: - `WP_Font_Utils::sanitize_font_family()` replaces `sanitize_text_field()`, `explode( ',' )`, and quote trimming. - `WP_Font_Utils::get_font_face_slug()` compares decoded names, so that equivalent CSS escapes produce one slug. - `WP_Font_Face_Resolver` selects the first family of a list from the parsed entries. - `WP_Font_Face` writes the `@font-face` descriptor as a quoted CSS string. - Both font REST controllers reject a `fontFamily` value that is not valid CSS and not a plain font name. - `WP_Font_Collection` sanitizes the nested `fontFace.fontFamily` value. - `safecss_filter_attr()` splits declarations with quote and escape awareness, and validates `font-family` with the font family grammar. A named family is now always quoted, and a generic family stays a keyword. For compatibility, a plain font name such as `O'Reilly Sans` still works at the font input boundaries. Core does not require a client-side escape scheme. Props matiasbenedetto. See #63568.
matiasbenedetto
force-pushed
the
fix-font-chars
branch
from
September 18, 2026 16:35
a5795c3 to
560da82
Compare
…ter. Use `mb_chr()` to decode a hexadecimal escape, copy the continuation bytes of an escaped character in place, and remove three private helpers. Simplify the identifier loop, and remove guards that cannot fail. In KSES, check a `font-family` value inside the allowed-property branch and clear the test string when the grammar accepts it. Remove the parenthesis depth tracking from the splitter, because a font name with a semicolon is always a quoted string. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Use bounded regular expressions and native string operations to remove duplicate parser and serializer code. Copy complete CSS declarations in the KSES splitter. Reject undefined generic arguments before CSS serialization. Add parser and REST regression tests for escaped declaration delimiters.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Core changes some font names when it treats CSS values as plain text. For example,
O'Reilly Sansproduces an invalid unquoted@font-facedescriptor. This PR preserves the decoded name through font validation, storage, resolution, and CSS output.Trac ticket: https://core.trac.wordpress.org/ticket/63568
The fix also preserves commas inside quoted names, significant spaces, percent sequences, CSS escapes, and punctuation. It keeps a named family such as
"serif"distinct from the generic keywordserif.Implementation. The new internal class
WP_CSS_Font_Familyparses CSS values and serializes decoded names.parse_list()returns typed entries for named families, generic families, and reserved keywords. It consumes the complete value.parse_list_with_plain_names()adds compatibility for plain names that earlier WordPress versions accepted, includingO'Reilly SansandO"Reilly Sans.parse_descriptor_name()selects the first family for a face, including the supported legacy list input.serialize_name()emits a quoted CSS string with escapes for quotes, backslashes, control characters, and HTML-sensitive characters.serialize_list()preserves family order and the distinction between names and generic keywords.The parser uses anchored regular expressions for identifier bytes and
strspn()for hexadecimal digits. It uses no recursion. The serializer replaces only characters that require an escape. The KSES splitter copies complete declaration substrings and respects quotes and escapes.The
generic()parser accepts onlykai,fangsong,khmer-mul, andnastaliq. It validates the decoded argument before it emits CSS. This prevents an escaped argument from introducing a second declaration. Tests cover valid escaped arguments and rejection through both the parser and REST.The font utilities, resolver, face output, collection schema, and both REST controllers use the shared parser. Invalid REST values return a 400 error. Invalid face values produce a diagnostic and no face output. Core loads the parser before KSES.
safecss_filter_attr()uses the font grammar to accept punctuation inside valid names. Other values still use the current safety checks. Thesafecss_filter_attr_allow_cssfilter remains in the path.Compatibility. Named families now use quotes in canonical CSS. Generic families retain their keyword form. The preset can contain
"ACME, Sans", sans-serif, while its face descriptor contains only"ACME, Sans".The serializer escapes
<,>, and&so names survive HTML output and KSES post filters. Backslashes use hexadecimal escapes becausewp_kses_no_null()can remove a literal backslash before zeros. Short hexadecimal escapes retain their terminator spaces.Font face comparison keys use decoded names so equivalent CSS escapes identify the same face. Percent sequences protect punctuation that can otherwise change key boundaries or post titles. Ordinary names such as
Open Sansretain their previous key format. This PR performs no bulk data migration.A value with additional CSS syntax, such as
"A"; color:red, fails REST validation. In a style attribute,font-family:"A;B";color:redremains two permitted declarations. The semicolon inside the quoted name stays part of that name.Client dependency and limits. Core accepts valid CSS without the special escape scheme from Gutenberg PR #76782. Upload clients must serialize metadata names with commas, parentheses, or angle brackets to preserve each name as one CSS string.
Display-name storage remains outside this fix. The family controller still applies
sanitize_text_field()to the display name inpost_title. A display name with markup can therefore lose that text, independently of the CSS identity.Current automated results. These checks cover commit
7c82a49985.npm run test:php -- --group fonts,kses,restapi-global-stylesnpm run test:php -- -c tests/phpunit/multisite.xml --group fonts,kses,restapi-global-stylesnpm run typecheck:php -- --no-progresstestVersion 7.4-npm run test:phpThe full-suite failure is
Tests_Script_Modules_WpScriptModules::test_default_script_module_files_exist. The local environment lackssrc/wp-includes/js/dist/script-modules/dashboard-init/index.js.The same test fails identically with the source code from PR base
74bb9334f5129ad64bab5b630157067bf5f24931. The control run restored all eight modified pre-existing source files through temporary Docker mounts. It retained the same local build assets. The test and script-module registration code do not differ from the base. The failure therefore does not originate in this PR.A comparison across 60,516 inputs checked the three parser entry points, name serialization, and the KSES splitter against the previous PR revision. The only differences were the intended rejections of invalid
generic()arguments. The corpus included random input, every byte value, and long names, lists, comments, and escape sequences.The regression suites cover REST storage and responses, repeated saves, duplicate detection, legacy records, generated presets, face output, and injection attempts. The new invalid-argument tests failed before the validation fix.
Browser evidence and limits. The earlier implementation report records Chrome 143 checks at revision
a5795c3, with Twenty Twenty-Five and a local DM Sans fixture. A REST-installed family namedO'Reilly, Sansloaded after a public page reload. Its canvas metrics matched a control face from the same file and differed from fallback fonts. A name with HTML-like text remained inert. Those checks did not repeat for the latest commit.The Site Editor upload workflow, a new Playwright spec, Firefox, WebKit, and the two Trac font attachments remain unverified through the interface. The current automated results above do not claim those checks. CI results for earlier revisions do not establish the result for this commit.
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code and Codex
Model(s): Claude Opus 5 and Claude Fable 5.1, as recorded by the implementation author; GPT-6 for this review.
Used for: The implementation, code review, code reduction, regression tests, test execution, and PR description. The latest review reproduced the generic-argument defect and compared the build-file failure with the PR base.
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.