Skip to content

Extend generic property descriptor handling#306

Merged
domenic merged 14 commits intojsdom:mainfrom
asamuzaK:descriptor
Feb 3, 2026
Merged

Extend generic property descriptor handling#306
domenic merged 14 commits intojsdom:mainfrom
asamuzaK:descriptor

Conversation

@asamuzaK
Copy link
Contributor

Related #305

  • Integrate scripts/generate*.mjs scripts into scripts/prepare.mjs
  • Add parseDimension function in lib/parsers.js
  • Add handlers for various AST_TYPES in lib/utils/propertyDescriptors.js
  • Fix bugs in properties/backgroundPosition.js and properties/backgroundRepeat.js

web-platform-tests/tests/css/cssom/serialize-values.html:

  • Main: 26/687 errors
  • PR: 13/687 errors

@asamuzaK asamuzaK marked this pull request as draft February 1, 2026 07:51
@asamuzaK asamuzaK marked this pull request as ready for review February 1, 2026 08:59
Copy link
Member

@domenic domenic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with nits. We should merge soon because it is a nice progression on the test results.

However let me ask about your general strategy for the generic descriptor.

From my understanding of the CSS specs, the generic algorithm is:

If we were to implement this in jsdom, I guess we would have the generic property descriptor use some totally generic parsing algorithm (maybe like cssTree.parse()?) and then we would check against the grammar from propertyDefinitions.js.

How does that compare to the strategy you are working on? Your strategy involves passing information about "dimension types" and "function types" per property, instead of passing the grammar. And you use them to decide on what parsing to do after the generic cssTree.parse(). I would like to learn more about this strategy and how it compares to the spec.

* @param {object} opts - The options object.
* @param {boolean} opts.caseSensitive - True if value is case-sensitive, false otherwise.
* @param {object} [opts.dimensionTypes] - The dimension types object.
* @param {object} [opts.functionTypes] - The function types object.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you describe these in more detail? When will they be present, and what will they contain?

Copy link
Contributor Author

@asamuzaK asamuzaK Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

opts.dimensionTypes is an object that contains CSS data type and optionally max / min ranges.
For example, <length> is defined as:

{
  type: "length"
}

And <length [0, ∞]> is defined as:

{
  type: "length",
  max: null,
  min: 0
}

type is used as a key to determine how to serialize the value.
If type is "length", it is passed to parseLength(), and if type is "percentage", it is passed to parsePercentage().

The same applies to opts.functionTypes, but currently only color and image have serialization methods, so type only has those two keys.

Note that parseLength() and parsePercentage() would be more accurately named serializeLength() and serializePercentage().
If requested, I will also rename those in this PR.
Renamed them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, that helps!

Is the following accurate?

 * @param {object} [opts.dimensionTypes={}] - A map containing information about the dimension types used by this property, if any. Keys are a type of dimension, which determines which serializer to use, and values are the information used by the serializer to serialize a parsed value.
 * @param {object} [opts.functionTypes={}] - A map containing information about the function types used by this property, if any. Keys are a type of function, which determines which function to use; values are ignored.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about
A map containing ... -> A key-value pair containing ...

asamuzaK and others added 5 commits February 2, 2026 23:20
Co-authored-by: Domenic Denicola <d@domenic.me>
Co-authored-by: Domenic Denicola <d@domenic.me>
Co-authored-by: Domenic Denicola <d@domenic.me>
Co-authored-by: Domenic Denicola <d@domenic.me>
@asamuzaK
Copy link
Contributor Author

asamuzaK commented Feb 2, 2026

From my understanding of the CSS specs, the generic algorithm is:

* Each property lists a grammar. For example, `outline-offset` has a syntax of `<length>`

* So, whenever parsing `outline-offset`, per https://drafts.csswg.org/cssom/#parse-a-css-value , we first parse it generically, and then we check if the result matches `<length>`. If it does not, then we throw away the result.

* If the result matches, then we append per https://drafts.csswg.org/cssom/#set-a-css-declaration, and later serialize per https://drafts.csswg.org/cssom/#serialize-a-css-value .

If we were to implement this in jsdom, I guess we would have the generic property descriptor use some totally generic parsing algorithm (maybe like cssTree.parse()?) and then we would check against the grammar from propertyDefinitions.js.

How does that compare to the strategy you are working on? Your strategy involves passing information about "dimension types" and "function types" per property, instead of passing the grammar. And you use them to decide on what parsing to do after the generic cssTree.parse(). I would like to learn more about this strategy and how it compares to the spec.

To be honest, createGenericPropertyDescriptor() is far from perfect.

  • It covers most longhand properties, but grid-* properties are not supported.
    There may be other longhand properties that are not supported, but this has not been verified.
  • It does not support shorthand properties.
    Also, if a longhand property of a shorthand changed, the change will not be applied to the shorthand, and vice versa.
  • It does not support most CSS function types.

The changes in this PR simply serializes anything that can be serialized, of the things that were previously set as-is.
However, it appears that the steps of the spec, "parse" "match syntax," and "serialize" are followed.
I'm not sure if this answers your question, though.

@domenic
Copy link
Member

domenic commented Feb 3, 2026

I think I understand now. I couldn't find where the "match syntax" step was, but I see that it's in parsePropertyValue() now.

I'm happy to merge this after we get a nice descriptive comment for dimensionTypes + functionTypes.

@domenic
Copy link
Member

domenic commented Feb 3, 2026

When using Claude Code to understand this PR better, it had a suggestion for us:

The current createGenericPropertyDescriptor approach works like this:

  1. Call cssTree.lexer.matchProperty() to validate the value against the grammar
  2. Only check if error || !matched (i.e., treat matched as a boolean)
  3. Re-parse the raw AST nodes and use the dimensionTypes config to decide how to serialize (e.g., whether to call serializeLength vs serializeAngle)

The matched result from css-tree actually contains the semantic type info we need:

matched.match[0].syntax.name  // "length", "angle", "percentage", etc.

So an alternate approach would be:

  1. Call cssTree.lexer.matchProperty() as before
  2. Walk the matched structure to extract both the AST nodes and their matched grammar types
  3. Use the grammar type from matched to decide serialization, instead of a parallel dimensionTypes config

This would eliminate the need to manually specify dimensionTypes: { length: { type: "length" } } for each property, since css-tree already knows what type each property expects. The config would only be needed for constraints that css-tree doesn't enforce (like min/max ranges, if any exist beyond what the grammar captures).

I don't know if this is helpful, and it can be done in a followup anyway. But I wanted to relay it.

@domenic domenic merged commit bde86d8 into jsdom:main Feb 3, 2026
3 checks passed
@asamuzaK asamuzaK deleted the descriptor branch February 3, 2026 03:56
@asamuzaK
Copy link
Contributor Author

asamuzaK commented Feb 3, 2026

The matched result from css-tree actually contains the semantic type info we need:

matched.match[0].syntax.name // "length", "angle", "percentage", etc.

This is true, but there are actually more variations.
However, it may be useful as a partial fast pass.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants