Skip to content

feat: add support for processing inline code #336

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

techfg
Copy link

@techfg techfg commented May 17, 2025

Adds support for processing inline code

Apologies in advance for the long description but providing for two reasons:

  1. There hasn't been a design discussion around how to implement inline code processing other than the very high-level comments in Support for inline code syntax highlighting #250 so wanted to provide context on why this approach was taken to hopefully pre-emptively address as many questions as possible to accelerate approval/merge 😃
  2. While this PR achieves the design goals stated below, I believe there is a better way to approach this but it would mean some additional generated HTML changes, albeit for the better IMHO, so I wanted to provide full context. See Bottom line for details.

TL;DR

Version Markdown Rendered
v0.41.2 Inline code example console.log('Hello!') image
This PR Inline code example console.log('Hello!'){:js} image

Design Goals

  1. 100% backwards compatible with v0.41.2 - Achieved with two exceptions specific to generated HTML output, neither of which should cause any functional backwards compatibility issues UNTIL the user opts-in to inline code processing and even then, they may or may not cause issues (see Styling for more information):
    1. Generated Styles
      1. .expressive-code CSS defaults - Previously would be directly under .expressive-code but is now wrapped in a .ec-container-block class
      2. InlineStyleAnnotation CSS Selector - Need to account for new .ec-line-inline class
      3. .ec-container-inline CSS class - New class included in generated styles that did not previously exist
    2. Generated HTML - ec-type attributed added to pre element
  2. Ability for any plugin to opt-in to handling inline code - Achieved
  3. All existing tests should pass without modification - Achieved with two exceptions both specific to the Generated Styles and HTML mentioned above:
    1. testing data attributes
    2. testing theme styles
  4. Inline code language detection should be extensible to support multiple formats - Achieved via InlineCodeHandlers with a credit to rehype-shiki for the inspiration.

Functional Implementation

Four (4) different approaches were considered:

  1. Call the existing hook signatures and rely on the hook to conditionally determine if it supports inline.
    • Pros: Minimizes the amount of code changes required
    • Cons: Not 100% backwards compatible as while the EC plugins could be adjusted as part of this PR to opt-in/out of inline, any EC plugin out in the wild could potentially break until it's updated to explicitly decide on supporting inline
  2. Add a <hookname>Inline set of hooks
    • Pros:
      • Explicit entry point for plugins to handle inline blocks
      • Allows for discrete Code Block classes that would be passed (e.g., ExpressiveCodeBlock, ExpressiveInlineCodeBlock) and Hook function signatures to differentiate parameters (e.g., inline does not support gutters so addGutterElement wouldn't be defined on it's hook properties)
      • 100% backwards compatible
    • Cons: Doubles the number of EC hooks and moving forward, if new "Code Block" types are added (unlikely) or additional hooks added (possible), would further increase the number of hooks
  3. Add a supportedCodeBlockTypes plugin option that returns an array of which types of code blocks that the plugin wants its hooks called for with the default being block only if supportedCodeBlockTypes is not defined by the plugin
    • Pros:
      • Minimizes the amount of code changes required
      • 100% backwards compatible
    • Cons:
      • Adds one property to hook definition API
      • Doesn't provide discrete method of handling Code Blocks types and/or hooks as it requires conditional logic to handle inline and codeblock differently within the existing hooks
  4. Add a plugin hook registration API - similar to Option 3 but extends the concept to do a full registration of all hooks and for which types a plugin supports
    • Pros:
      • Explicit approach for plugins to inform EC where it wants to be involved in the processing and specifically which hooks and for which types (e.g., could support inline on only a subset of hooks)
      • The "registration api" is mentioned in feat: add support for controlling bundle size #335 (comment) regarding styles and having an explicit way to register styles could help eliminate side effects that are currently in-play and prohibit EC and EC's various packages from being marked sideEffects: false in package.json which leads to unnecessary increase to bundle size in some situations. If a style registration API were to be added, it could be coupled with a hook registration API.
    • Cons:
      • Changes the current hook API approach (although this potentially could be for the better)
      • Requires modifications to all existing plugins to call the registration entry point (default functionality could still be invoked if a plugin doesn't register and the old approach could be deprecated)

Decision: Option 2 is the most robust and type-safe approach, however it requires much more code to be written to accomidate. Additionally, it's likely an over-enginered approach since there are only a few differences between inline and block in terms of ExpressiveCodeBlock handling. For these reasons, Option 3 was chosen since it is least invasive and most straightforward.

Styling

Unlike the functional implementation, accounting for styles was not a straightforward solution. There were two main reasons for this:

  1. The styling system is in v0.41.2 is written to assume there are only code blocks within .expressive-code so styles are loosely applied in certain cases. For example, any .ec-line, even if it doesn't live within a pre is styled by plugin-frames. Similarly, expressive-code itself styles any .ec-line that lives anywhere inside of .expressive-code.
    • QUESTION: Curious why these situations occur and the styles are not constrained at least to pre and more specifically pre > code? Are there situations that possibly I'm overlooking where an ec-line could exist outside of pre > code that would require styling directly under .expressive-code?
  2. render supports multiple ExpressiveCodeBlock instances which may now contain a mixture of inline and block so there needs to be a way to target styles specific to the ExpressiveCodeBlockType for the given code block.

Given the above, the approach taken had to ensure that it did not use some of the existing classes, like .ec-line to avoid styling issues when inline code is processed.

To achieve this, a couple of minor adjustments to generated styles and HTML were required as mentioned in Design Goals. The current approach is as follows with the changes between v0.41.2 and this PR highlighted:

Single code block rendered regardless of inline configuration:

- <div class="expressive-code">
+ <div class="expressive-code ec-container-block">
    ....
-    <pre data-language="js">
+    <pre data-language="js" data-ec-type="block">
        <code>
            <div class="ec-line">
                <div class="code">...</div>
            </div>
            ....
        </code>
    </pre>
</div>

Multiple code blocks rendered regardless of inline configuration:

- <div class="expressive-code">
+ <div class="expressive-code ec-container-block">
    ....
-    <pre data-language="js">
+    <pre data-language="js" data-ec-type="block">
        <code>
            <div class="ec-line">
                <div class="code">...</div>
            </div>
            ....
        </code>
-    <pre data-language="js">
+    <pre data-language="js" data-ec-type="block">
        <code>
            <div class="ec-line">
                <div class="code">...</div>
            </div>
            ....
        </code>
    </pre>
</div>

The structure above is the same for inline code (single or multiple) when render is called with only inline block(s) with the only differences being span instead of div and the classes/attributes being inline variants.

Single inline code rendered with inline enabled

<span class="expressive-code ec-container-inline">
    <span data-language="js" data-ec-type="inline">
        <code>
            <span class="ec-line-inline">
                <span class="code-inline">...</span>
            </span>
            ...
        </code>
    </span>
</span>

Multiple inline code rendered with inline enabled

<span class="expressive-code ec-container-inline">
    <span data-language="js" data-ec-type="inline">
        <code>
            <span class="ec-line-inline">
                <span class="code-inline">...</span>
            </span>
            ...
        </code>
    </span>
    <span data-language="js" data-ec-type="inline">
        <code>
            <span class="ec-line-inline">
                <span class="code-inline">...</span>
            </span>
            ...
        </code>
    </span>    
</span>

When a mixture of block types is passed to render, the structure with this PR is as follows:

- <div class="expressive-code">
+ <div class="expressive-code ec-container-mixed">
+    <div class="ec-container-block">
        ...
-       <pre data-language="js">
+       <pre data-language="js" data-ec-type="block">
            <code>
                <div class="ec-line">
                    <div class="code">...</div>
                </div>
                ...
            </code>
         </pre>
    </div>
    <span class="ec-container-inline">
        <span data-language="js" data-ec-type="inline">
            <code>
                <span class="ec-line-inline">
                    <span class="code-inline">...</span>
                </span>
                ...
            </code>
        </span>
    </span>
</div>

Bottom Line

While the existing PR achieves the desired outcome with minimal changes to generated styles and HTML, I believe that a better approach would be to change the generated HTML to structure each individual ExpressiveCodeBlock in its own container within the outer .expressive-code wrapper. Doing so would provide the following benefits:

  1. The structure of the HTML would be 100% identical regardless of how many ExpressCodeBlocks are rendered and what their ExpressCodeBlockType is
  2. EC and all EC plugins would have an easier way of targeting style differences between inline and block by targeting classes and/or data attributes instead of tag names
  3. "Common" elements between block and inline could use the same classes (e.g., ec-line instead of ec-line & ec-line-inline)

This approach, in some situations and depending on styling could introduce a breaking change. With that said, EC and it's stock plugins would all be updated and versioned to handle in a release so it really comes down to non-EC core plugins and any user specified CSS styling that is applied. The user based styling would be easy enough to account for by the users as they upgrade EC. Looking at the community plugins, most define peerDependencies as ^0.#.# (e.g., color chips). Unfortunately, it does not appear that peerDependencies semver rules work the same as dependencies/devDependencies when it comes to install/update. With dependencies/devDependencies, package managers respect the leading zero semver and treat any minor bump as a breaking change and therefore ^0.#.# won't update when minor increases. However, for peerDependencies, it's more loose and will not warn when the segment that is considered "major", in this case the minor value itself, increases. For this reason, there is no automatic way to warn users upon upgrading EC that a plugin they are using doesn't support the EC version. Given this, it may still be necessary to use different classes (e.g., ec-line and ec-line-inline) to minimize potential breaking changes and even then, depending on the styles that the plugin adds and what selectors they target, it could blead in to inline blocks. One thing to consider is making this change and releasing as v1.0.0 which is something I think is a goal anyway. This would allow for users to be warned on upgrading EC if the plugin doesn't support v1 yet. One exception to this is twoslash which defines peer deps as >=0.40.0.

Suggested HTML structure:

Multiple code blocks

<div class="expressive-code">
    <div class="ec-container-block" data-language="js" data-ec-type="block">
        ...
        <pre>
            <code>
                <div class="ec-line">
                    <div class="code">...</div>
                </div>
                ...
            </code>
        </pre>
    </div>
    <div class="ec-container-block" data-language="js" data-ec-type="block">
        ...
        <pre>
            <code>
                <div class="ec-line">
                    <div class="code">...</div>
                </div>
                ...
            </code>
        </pre>
    </div>
    ...    
</div>

Multiple inline code

<span class="expressive-code">
    <span class="ec-container-inline" data-language="js" data-ec-type="inline">
        <span>
            <code>
                <span class="ec-line">
                    <span class="code">...</span>
                </span>
                ...
            </code>
        </span>
    </span>
    <span class="ec-container-inline" data-language="js" data-ec-type="inline">
        <span>
            <code>
                <span class="ec-line">
                    <span class="code">...</span>
                </span>
                ...
            </code>
        </span>
    </span>
    ...    
</span>

Multiple inline code & code block

<div class="expressive-code">
    <div class="ec-container-block" data-language="js" data-ec-type="block">
        ...
        <pre>
            <code>
                <div class="ec-line">
                    <div class="code">...</div>
                </div>
                ...
            </code>
        </pre>
    </div>
    <span class="ec-container-inline" data-language="js" data-ec-type="inline">
        <span>
            <code>
                <span class="ec-line">
                    <span class="code">...</span>
                </span>
                ...
            </code>
        </span>
    </span>
    ...
</div>

Resolves #250

Copy link

netlify bot commented May 17, 2025

Deploy Preview for expressive-code ready!

Name Link
🔨 Latest commit 36b0c17
🔍 Latest deploy log https://app.netlify.com/projects/expressive-code/deploys/68699397b955e900080f5c21
😎 Deploy Preview https://deploy-preview-336--expressive-code.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 project configuration.

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.

Support for inline code syntax highlighting
1 participant