Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(theme-common, theme-classic): Improve CodeBlock Extensibility #11011

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

Danielku15
Copy link

@Danielku15 Danielku15 commented Mar 19, 2025

Pre-flight checklist

Motivation

Closes #11008

Test Plan

Verified and tested via unit tests. I primarily focused on codeBlockUtils.test.ts but all other tests locally are green. Looking at the rendered codeblocks on the website they are equal to before.

Test links

Deploy preview: https://deploy-preview-11011--docusaurus-2.netlify.app/
Relevant Links:

Related issues/PRs

#11008

@facebook-github-bot
Copy link
Contributor

Hi @Danielku15!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at [email protected]. Thanks!

Copy link

netlify bot commented Mar 19, 2025

[V2]

Built without sensitive environment variables

Name Link
🔨 Latest commit ee22ed9
🔍 Latest deploy log https://app.netlify.com/sites/docusaurus-2/deploys/67db1819a897950008228d64
😎 Deploy Preview https://deploy-preview-11011--docusaurus-2.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@facebook-github-bot
Copy link
Contributor

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@facebook-github-bot facebook-github-bot added the CLA Signed Signed Facebook CLA label Mar 19, 2025
Copy link

github-actions bot commented Mar 19, 2025

⚡️ Lighthouse report for the deploy preview of this PR

URL Performance Accessibility Best Practices SEO Report
/ 🔴 42 🟢 98 🟢 100 🟢 100 Report
/docs/installation 🔴 47 🟢 97 🟢 100 🟢 100 Report
/docs/category/getting-started 🟠 71 🟢 100 🟢 100 🟠 86 Report
/blog 🟠 61 🟢 96 🟢 100 🟠 86 Report
/blog/preparing-your-site-for-docusaurus-v3 🔴 45 🟢 92 🟢 100 🟢 100 Report
/blog/tags/release 🟠 62 🟢 96 🟢 100 🟠 86 Report
/blog/tags 🟠 73 🟢 100 🟢 100 🟠 86 Report

@Danielku15 Danielku15 force-pushed the feature/codeblock-extensibility branch from bbfcf38 to ee22ed9 Compare March 19, 2025 19:16
@slorber slorber added the Argos Add this label to run UI visual regression tests. See argos.yml GH action. label Mar 20, 2025
Copy link
Collaborator

@slorber slorber left a comment

Choose a reason for hiding this comment

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

This PR is already quite large and hard to review

It would be better to split it into many subparts. We don't have to merge everything at once, incremental improvements are easier to merge.

magicComments: [],
});

const noInline = meta.options.noinline === true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const noInline = meta.options.noinline === true;
const noInline = meta.options.noInline === true;

Is there a reason to not respect the case?

Copy link
Author

Choose a reason for hiding this comment

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

Initially I wanted to make things case insensitive so that the user-errors on using options is reduced. e.g. some having codeblocks with noInline others noinline. or ShowLineNumbers vs. showLineNumbers.

I think we have 3 options here:

  1. Allow case insensitive keys on the end-user side and ensure in the code we use lowercase keys. (pro: easy on end user side, con: sensitive on component side)
  2. Be case sensitive. (pro: easy on component side: con: risky for end user side)
  3. Hide the values behind a case insensitive Map<> like abstraction. (pro: easy to use on component and end-user side, con: higher complexity in code, not easy to specify in TSX/MDX)
type CodeMetaOptions = {
    get(key:string): CodeMetaOptionValue | undefined;
    set(key:string, value: CodeMetaOptionValue | undefined);
    delete(key:string): void;
};

For the sake of keeping this change simple I'll change things to (2).

Copy link
Collaborator

Choose a reason for hiding this comment

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

The former behavior was case sensitive:

const noInline = props.metastring?.includes('noInline') ?? false;

A refactor shouldn't necessarily change the public API surface, and solely focus on re-organizing existing implementation.

We can consider changing that behavior, but in a dedicated PR that doesn't mix refactoring + behavior changes.

if (showLineNumbersMeta.startsWith('showLineNumbers=')) {
const value = showLineNumbersMeta.replace('showLineNumbers=', '');
return parseInt(value, 10);
function parseCodeBlockOptions(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Will need to review more in depth but that looks quite complicated

I'd prefer if we start by centralizing parsing the options, but keep the old methods like getMetaLineNumbersStart

We can eventually rewrite the parser later in another PR, but I'd prefer is we don't refactor everything at once.

Please split this into multiple sequential PRs:

  • first centralize parsing
  • then refactor how we parse

Not everything at once

Copy link
Author

Choose a reason for hiding this comment

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

IMO Its fairly straight forward considering what docusaurus needs to support. If we would start from scratch, we could add some more restrictions. But this code ensures backwards compatibility with a variety of option syntaxes (no options, plain values etc.

  1. Highlight needs special treatment for backwards compatibilty of lineClassNames
  2. Options without values are used for markers like showLineNumbers
  3. Boolean values are new, but influenced by the linked PRs in #11008 to be able to enable/disable some options.
  4. Number values are for showLineNumbers compatibility.

I'll try to split apart the value handling a bit to reduce the complexity of this single function.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Before we even think about changing the way we parse things, I'd prefer to keep the exact same parsing logic as before, extract it to a single place, and cover it widely with unit tests.

Once this is well-covered, you can start changing the implementation, ensuring all the tests keep passing.

const title = parseCodeBlockTitle(metastring) || titleProp;

const {lineClassNames, code} = parseLines(children, {
const {meta, code} = parseLines(children, {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't see why parseLines should parse the metastring.

Can't we provide the meta as input?

Copy link
Author

Choose a reason for hiding this comment

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

Can be changed. The decision behind this was:

  • Keeping the existing "responsibilities" (parseLines being responsible for parsing the lines related to the codeblock).
  • Assuming we will at some point in future also parse options from metacomments inside the code (see RFC), it would be parseLines parsing additional data into meta.

Copy link
Collaborator

Choose a reason for hiding this comment

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

  • Keeping the existing "responsibilities" (parseLines being responsible for parsing the lines related to the codeblock).

Not sure to understand, this method's responsibility is not really parsing the metastring, but parsing lines magic comments.

  • Assuming we will at some point in future also parse options from metacomments inside the code (see RFC), it would be parseLines parsing additional data into meta.

I'm not sure about the "Metadata Magic Comments" feature.

Even if we eventually introduce this, we'll only do this later once everything is in a better shape, and your design should be researched/challenged. It's always possible to refactor again later once we are sure we want this feature, but for now please ignore it.

* The parsed options, key converted to lowercase.
* e.g. `"title" => "file.js", "showlinenumbers" => true`
*/
readonly options: {[key: string]: CodeMetaOptionValue};
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not ideal, not typesafe and not extendable

Let's use a CodeBlockMetaOptions interface and leverage TS declaration merging to register extra options (for example the live codeblock can register its noInline option)

Copy link
Author

Choose a reason for hiding this comment

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

Easy to change, will do. This was initially inspired by the general code style in this area (e.g. lineClassNames). -> Also see remark regarding case insenstivity below which could influence the design of CodeBlockMetaOptions.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This was initially inspired by the general code style in this area (e.g. lineClassNames).

No idea what you mean 😅

Also see remark regarding case insenstivity below which could influence the design of CodeBlockMetaOptions.

Even if we implement case insensitiveness, we still want noInline as a typesafe option attribute

Comment on lines +497 to +500
export interface Props {
readonly output: TokenOutputProps;
readonly meta?: CodeBlockMeta;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

TokenOutputProps props are directly applicable to spans

I'd prefer to have

type TokenOutputProps = {
    style?: StyleObj;
    className: string;
    children: string;
    [key: string]: unknown;
};

export interface Props extends TokenOutputProps {
    readonly meta?: CodeBlockMeta;
}

Eventually, we could introduce a React code block context to provide the meta to all the subtree. This makes it easier to reduce coupling between code block components, by avoiding passing global context through props (less likely to break on swizzle)

Copy link
Author

Choose a reason for hiding this comment

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

I had this before but decided not to inherit but composite because:

  1. meta should not be applied to span. Hence you end up with something similar const {meta, rest} = props; ... (<span {...rest} />). (I noticed that CodeBlockToken has a bug not having <span {...props.output} /> like it should and <CodeBlockToken /> not receiving meta
  2. When swizzling you can clearly differenciate between the general token inputs (like meta) and what options should be applied to the element for the output.

Generally I'm fine with the change, just wanted to be sure we consider those aspects.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks awkward to me to have {...props.output}, and why we have to name a thing output in the first place. The component shouldn't be designed according to names chosen by a third-party library, it must make sense in isolation. Would you use the name "output" if you weren't using react-prism-renderer for example, and were simply designing a standalone code block token component?

If meta is accessible through context, there's no need to {meta, ...rest} because meta is not a prop.

Copy link
Author

@Danielku15 Danielku15 left a comment

Choose a reason for hiding this comment

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

This PR is already quite large and hard to review

It would be better to split it into many subparts. We don't have to merge everything at once, incremental improvements are easier to merge.

@slorber
I wouldn't consider it "large", but it indeed has a bit of complexity to it. 😅 I'll split it up and refactor some bits as mentioned in the remarks.

I added some comments to give insights in my decision paths. No need to give again anwers, but it might contain some valueable bits for the split PRs.

Comment on lines +497 to +500
export interface Props {
readonly output: TokenOutputProps;
readonly meta?: CodeBlockMeta;
}
Copy link
Author

Choose a reason for hiding this comment

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

I had this before but decided not to inherit but composite because:

  1. meta should not be applied to span. Hence you end up with something similar const {meta, rest} = props; ... (<span {...rest} />). (I noticed that CodeBlockToken has a bug not having <span {...props.output} /> like it should and <CodeBlockToken /> not receiving meta
  2. When swizzling you can clearly differenciate between the general token inputs (like meta) and what options should be applied to the element for the output.

Generally I'm fine with the change, just wanted to be sure we consider those aspects.

const title = parseCodeBlockTitle(metastring) || titleProp;

const {lineClassNames, code} = parseLines(children, {
const {meta, code} = parseLines(children, {
Copy link
Author

Choose a reason for hiding this comment

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

Can be changed. The decision behind this was:

  • Keeping the existing "responsibilities" (parseLines being responsible for parsing the lines related to the codeblock).
  • Assuming we will at some point in future also parse options from metacomments inside the code (see RFC), it would be parseLines parsing additional data into meta.

* The parsed options, key converted to lowercase.
* e.g. `"title" => "file.js", "showlinenumbers" => true`
*/
readonly options: {[key: string]: CodeMetaOptionValue};
Copy link
Author

Choose a reason for hiding this comment

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

Easy to change, will do. This was initially inspired by the general code style in this area (e.g. lineClassNames). -> Also see remark regarding case insenstivity below which could influence the design of CodeBlockMetaOptions.

if (showLineNumbersMeta.startsWith('showLineNumbers=')) {
const value = showLineNumbersMeta.replace('showLineNumbers=', '');
return parseInt(value, 10);
function parseCodeBlockOptions(
Copy link
Author

Choose a reason for hiding this comment

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

IMO Its fairly straight forward considering what docusaurus needs to support. If we would start from scratch, we could add some more restrictions. But this code ensures backwards compatibility with a variety of option syntaxes (no options, plain values etc.

  1. Highlight needs special treatment for backwards compatibilty of lineClassNames
  2. Options without values are used for markers like showLineNumbers
  3. Boolean values are new, but influenced by the linked PRs in #11008 to be able to enable/disable some options.
  4. Number values are for showLineNumbers compatibility.

I'll try to split apart the value handling a bit to reduce the complexity of this single function.

magicComments: [],
});

const noInline = meta.options.noinline === true;
Copy link
Author

Choose a reason for hiding this comment

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

Initially I wanted to make things case insensitive so that the user-errors on using options is reduced. e.g. some having codeblocks with noInline others noinline. or ShowLineNumbers vs. showLineNumbers.

I think we have 3 options here:

  1. Allow case insensitive keys on the end-user side and ensure in the code we use lowercase keys. (pro: easy on end user side, con: sensitive on component side)
  2. Be case sensitive. (pro: easy on component side: con: risky for end user side)
  3. Hide the values behind a case insensitive Map<> like abstraction. (pro: easy to use on component and end-user side, con: higher complexity in code, not easy to specify in TSX/MDX)
type CodeMetaOptions = {
    get(key:string): CodeMetaOptionValue | undefined;
    set(key:string, value: CodeMetaOptionValue | undefined);
    delete(key:string): void;
};

For the sake of keeping this change simple I'll change things to (2).

@slorber
Copy link
Collaborator

slorber commented Mar 20, 2025

I wouldn't consider it "large", but it indeed has a bit of complexity to it. 😅 I'll split it up and refactor some bits as mentioned in the remarks.

Even if it's not a huge PR, it still can be split into many smaller ones.

I'd prefer to progress incrementally. Start by refactoring the code, without changing any behavior. Then apply fine-grained behavior changes, and justify each of them in a dedicated PR.

If you refactor and change everything at once, then we may not agree on everything, and your PR is less likely to be merged fast.

Let's start by focusing on what we agree on:

  • reorganizing code in a better, more testable way
  • 0 implementation detail and behavior change - play it safe
  • more unit tests on parts that we plan to change

@Danielku15
Copy link
Author

I fully agree on your points. Will work on adapted PRs soon-ish (just wrapping up some other general Docs parts on my project) 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Argos Add this label to run UI visual regression tests. See argos.yml GH action. CLA Signed Signed Facebook CLA
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve CodeBlock Extensibility
3 participants