Skip to content

fix: allow hyphenated variables in MDX#1320

Closed
HugoHSun wants to merge 3 commits intonextfrom
hugo/cx-2880-variables-containing-hyphens-cause-validation-errors
Closed

fix: allow hyphenated variables in MDX#1320
HugoHSun wants to merge 3 commits intonextfrom
hugo/cx-2880-variables-containing-hyphens-cause-validation-errors

Conversation

@HugoHSun
Copy link
Copy Markdown
Contributor

@HugoHSun HugoHSun commented Feb 3, 2026

PR App Fix CX-2880

🧰 Changes

Provides a workaround for user variables with hyphen in the MDX Slate editor, originially - always get parsed to the subtraction operator in JS.

  • In the rare case of using the actual subtraction between an user variable and a defined constant, it would need to have space around the operator e.g.
    export const total = 10
    {user.variable - total}
    or else {user.variable-total} get interpreted as a custom variable variable-total

Editor

image

View

image

🧬 QA & Testing

if (expr.type !== 'BinaryExpression' || expr.operator !== '-') return false;

// Check right side is an Identifier
if (expr.right.type !== 'Identifier') return false;
Copy link
Copy Markdown
Contributor Author

@HugoHSun HugoHSun Feb 3, 2026

Choose a reason for hiding this comment

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

To prevent edge cases like {user.number_variable - 1} or {user.num-1} being parsed as hyphenated variable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can you add a test for this and/or other edge cases? i can imagine there are a few others, and if we're only verifying the "right" side to not be an identifier, is this enough?

thinking of some others:

  • if we have something like {1 - user.some-variable} or {1 - user.variable}, what's the correct behavior that should happen here?
  • what about `{user.number_var - 1 - user.another_var}? this check would fail and you may proceed to think this is a hyphenated user var right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this raises some good points

  • for {1 - user.some-variable} or {1 - user.variable}, it's will pass here but not transformed to as it fails the regex check later requiring to start with {user.
  • {user.number_var - 1 - user.another_var} won't transform too
    BinaryExpression ← expr (outermost)
    ├── left: BinaryExpression
    │ ├── left: MemberExpression(user.number_var)
    │ ├── operator: '-'
    │ └── right: Literal(1)
    ├── operator: '-'
    └── right: MemberExpression(user.another_var) ← expr.right

@HugoHSun HugoHSun marked this pull request as ready for review February 3, 2026 06:02
@erunion erunion requested review from dannobytes, kevinports and rafegoldberg and removed request for domharrington and julshotal February 4, 2026 17:34
Copy link
Copy Markdown
Contributor

@rafegoldberg rafegoldberg left a comment

Choose a reason for hiding this comment

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

Logic looks fine, but this code needs clean up.

Comment on lines +18 to +19
// Check right side is an Identifier
if (expr.right.type !== 'Identifier') return false;
Copy link
Copy Markdown
Contributor

@rafegoldberg rafegoldberg Feb 4, 2026

Choose a reason for hiding this comment

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

This comment is redundant (and also says the opposite of what the next line checks for, which is confusing.) Please remove:

Suggested change
// Check right side is an Identifier
if (expr.right.type !== 'Identifier') return false;
if (expr.right.type !== 'Identifier') return false;

Comment on lines +15 to +16
// Must be a BinaryExpression with subtraction operator
if (expr.type !== 'BinaryExpression' || expr.operator !== '-') return false;
Copy link
Copy Markdown
Contributor

@rafegoldberg rafegoldberg Feb 4, 2026

Choose a reason for hiding this comment

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

This comment is redundant (and also says the opposite of what the next line checks for, which is confusing.) Please remove:

Suggested change
// Must be a BinaryExpression with subtraction operator
if (expr.type !== 'BinaryExpression' || expr.operator !== '-') return false;
if (expr.type !== 'BinaryExpression' || expr.operator !== '-') return false;

// Hyphenated variable: `user.X-API-Key` parsed as `user.X - API - Key`
// Extract the full variable name from node.value
const value = node.value;
const match = value.match(/^user\.([a-zA-Z_][a-zA-Z0-9_-]*)$/);
Copy link
Copy Markdown
Contributor

@rafegoldberg rafegoldberg Feb 4, 2026

Choose a reason for hiding this comment

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

This regex is unnecessarily complicated. Let's use the built in alphanumeric character class to simplify this:

Suggested change
const match = value.match(/^user\.([a-zA-Z_][a-zA-Z0-9_-]*)$/);
const match = value.match(/^user\.([\w-]*)$/);

if (expr.type !== 'BinaryExpression' || expr.operator !== '-') return false;

// Check right side is an Identifier
if (expr.right.type !== 'Identifier') return false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can you add a test for this and/or other edge cases? i can imagine there are a few others, and if we're only verifying the "right" side to not be an identifier, is this enough?

thinking of some others:

  • if we have something like {1 - user.some-variable} or {1 - user.variable}, what's the correct behavior that should happen here?
  • what about `{user.number_var - 1 - user.another_var}? this check would fail and you may proceed to think this is a hyphenated user var right?

expect(code.type).toBe('code');
expect(code.value).toBe('{user.X-API-Key}');
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we add a few more tests for other edge cases where the transformer should not parse the raw md as a readme variable?

Copy link
Copy Markdown
Contributor Author

@HugoHSun HugoHSun Feb 5, 2026

Choose a reason for hiding this comment

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

hmmm I think this is more tricky than it seems since the current behaviour (without this PR) is technically correct for JS expressions in MDX.

  • the PR logic transforms {user.a-1-b} as variable a-1-b, but {user.a-b-1} is still the expression user.a - b - 1
  • {user.a-false} is user.a - false since false is a literal not identifier
  • hyphenated variables won't be transformed if it's not at the start of the expression. So {1 - user.some-variable} is still evaluated as the expression {1 - user.some - variable}

I think it's fine to only allow hyphenated variables at the start, but should we allow non-identifiers (e.g. const, true, false, 1...) at the end of the variable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

another concern would be that it's gonna impact the customers who actually want to do subtraction rather than variable names. e.g.
image
image
image

Copy link
Copy Markdown
Contributor

@rafegoldberg rafegoldberg left a comment

Choose a reason for hiding this comment

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

QAed this and it doesn't work at all. Try it for yourself by running the start script on this branch, adding a hyphenated my-var here, and then hitting this link:

Image

Also, per @kevinports' comment here the current approach is to use proper bracket accessor notation in these instances, so: {user['my-var']}. Not sure we actually want to allow random hyphenated values within this expression syntax in the first place?

// Hyphenated variable: `user.X-API-Key` parsed as `user.X - API - Key`
// Extract the full variable name from node.value
const value = node.value;
const match = value.match(/^user\.([\w-]*)$/);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Per this thread with @maximilianfalco, I was wrong about the \w selector. Let's revert back to your original @HugoHSun.

Copy link
Copy Markdown
Contributor Author

@HugoHSun HugoHSun Feb 13, 2026

Choose a reason for hiding this comment

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

Sorry I should have made the context more clear:

This PR is targeted at MDX and variablesTransformer only runs in mdast() (via ast-processor.ts), which is NOT used by compile(). So here it always get compiled as the JS expression.

The only path where this PR's fix actually works is the visual editor in the main app:

  1. Slate's normalizeNode calls renderingLibrary.mdast()variablesTransformer detects the
    hyphenated variable → creates a readme-variable node

  2. On save, readme-to-mdx.ts serializes it as {user["x-api"]} because x-api isn't a valid JS identifier

  3. The viewer's compile()run() handles bracket notation fine

Screen.Recording.2026-02-13.at.5.07.55.pm.mov

Copy link
Copy Markdown
Contributor Author

@HugoHSun HugoHSun Feb 13, 2026

Choose a reason for hiding this comment

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

After thinking about it more, I reckon too we shouldn't allow hyphenated variables in dot notation (e.g. {user.test-var}). Especially in this package since it's also used outside of the app. It introduces ambiguity on whether it's an hyphenated variable or minus operation. Bracket notation for weird variable names (e.g. {user["test-var"], {user["test var"]}) is the way to go since they are valid JS expressions which get evaluated with the user proxy.

Copy link
Copy Markdown
Contributor

@rafegoldberg rafegoldberg left a comment

Choose a reason for hiding this comment

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

whoops, did not mean to approve this as it isn't working as stated rn. see above for detail.

@HugoHSun
Copy link
Copy Markdown
Contributor Author

Update: closing this PR since the fix for bracket variables are in https://github.com/readmeio/readme/pull/17315

@HugoHSun HugoHSun closed this Feb 13, 2026
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.

3 participants