shiki-twoslash-fix
v1.0.2
Published
A workaround for Shiki's Twoslash transformer that fixes misaligned code annotations and positioning issues
Downloads
112
Maintainers
Readme
shiki-twoslash-fix
A workaround for Shiki's Twoslash transformer that fixes misaligned code annotations and positioning issues
The Problem
When using Shiki with Twoslash for syntax highlighting and TypeScript code annotations, you may encounter a bug where code annotations (like @log: tags) appear in the wrong positions. This creates confusing and unprofessional-looking documentation.
Before (Buggy Behavior)
"code (line 1)";
// @log: a tag (line 2)
"code (line 3)";
// @log: a tag (line 4)
"code (line 5)";
// @log: a tag (line 6)
"code (line 7)";
// @log: a tag (line 8)Renders as:
"code (line 1)";
"code (line 3)";
a tag (line 2) ← Wrong position!
"code (line 5)";
a tag (line 4) ← Wrong position!
"code (line 7)";
a tag (line 6) ← Wrong position!
a tag (line 8) ← Wrong position!
After (Fixed Behavior)
Renders as:
"code (line 1)";
a tag (line 2) ← Correct position!
"code (line 3)";
a tag (line 4) ← Correct position!
"code (line 5)";
a tag (line 6) ← Correct position!
"code (line 7)";
a tag (line 8) ← Correct position!
The Solution
This package provides a simple wrapper function that fixes the positioning issues by:
- Correcting line number calculations between Twoslash and Shiki
- Removing trailing newlines that cause layout problems
- Adjusting tag node positions to prevent overflow beyond actual code lines
The fix is non-invasive and works with your existing Shiki/Twoslash setup without breaking changes.
Installation
# npm
npm install shiki-twoslash-fix
# yarn
yarn add shiki-twoslash-fix
# pnpm
pnpm add shiki-twoslash-fix
# bun
bun add shiki-twoslash-fixQuick Start
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
import { codeToHast } from "shiki";
// Create the transformer
const transformer = transformerTwoslash();
// Apply the fix
const fixedTransformer = twoslashBugWorkaround(transformer);
// Use it with Shiki
const hast = await codeToHast(code, {
lang: "ts",
theme: "min-dark",
transformers: [fixedTransformer],
});That's it! Your code annotations will now appear in the correct positions.
Usage Examples
Basic Usage
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
import { codeToHast } from "shiki";
const code = `
const message = "Hello, World!"
// @log: message
console.log(message)
// @log: "Logging to console"
`;
async function highlightCode() {
const transformer = transformerTwoslash();
const fixedTransformer = twoslashBugWorkaround(transformer);
const hast = await codeToHast(code, {
lang: "typescript",
theme: "github-dark",
transformers: [fixedTransformer],
});
return hast;
}With Multiple Transformers
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
import { transformerNotationDiff } from "@shikijs/transformers";
const transformers = [
twoslashBugWorkaround(transformerTwoslash()),
transformerNotationDiff(),
];
const hast = await codeToHast(code, {
lang: "ts",
theme: "vitesse-dark",
transformers,
});Custom Transformer Configuration
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
const transformer = transformerTwoslash({
explicitTrigger: true,
renderer: "rich",
});
const fixedTransformer = twoslashBugWorkaround(transformer);Technical Details
Root Cause Analysis
The bug occurs due to two main issues in the interaction between Twoslash and Shiki:
1. Line Number Discrepancy
- Twoslash uses the line where a tag is written as the line number
- Shiki assumes the line number points to the next line
- This creates an off-by-one error in positioning
2. Trailing Newline Issue
When code ends with @log: tags, Twoslash creates trailing newlines that:
- Cause layout problems in the rendered output
- Lead to incorrect line calculations
- Result in tags appearing after all code instead of inline
Fix Implementation
The twoslashBugWorkaround() function works by:
- Wrapping the preprocess method of the transformer
- Removing trailing newlines from the processed code
- Adjusting tag node positions using two corrections:
- Subtract 1 from line numbers (fixes Twoslash/Shiki discrepancy)
- Cap line numbers at the maximum actual line (prevents overflow)
function adjustTagNodes(nodes, maxLine) {
for (const node of nodes) {
if (node.type === "tag") {
node.line = Math.min(
node.line - 1, // Fix off-by-one error
maxLine, // Prevent overflow
);
}
}
}Edge Cases Handled
- Empty code blocks: Gracefully handles empty or whitespace-only code
- No Twoslash metadata: Safely passes through when Twoslash isn't used
- Multiple trailing newlines: Removes all trailing newlines consistently
- Mixed content: Works with code that has both regular lines and annotations
API Reference
twoslashBugWorkaround<T>(transformer: T): T
Applies the bug workaround to a Shiki transformer that has Twoslash preprocessing.
Parameters:
transformer: T- A Shiki transformer object with apreprocessmethod
Returns:
T- The same transformer object with the bug fix applied
Type Constraints:
T extends ShikiTransformerwhereShikiTransformerhas apreprocessmethod
Example:
const transformer = transformerTwoslash();
const fixed = twoslashBugWorkaround(transformer);TypeScript Support
This package is written in TypeScript and provides full type safety:
import type { TwoslashShikiReturn } from "@shikijs/twoslash";
// All types are properly exported and inferred
const fixedTransformer = twoslashBugWorkaround(transformer);
// fixedTransformer maintains the same type as the input transformerFramework Integration
Astro
// astro.config.mjs
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
export default defineConfig({
markdown: {
shikiConfig: {
transformers: [twoslashBugWorkaround(transformerTwoslash())],
},
},
});Next.js with MDX
// next.config.js
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
const nextConfig = {
pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
experimental: {
mdxRs: true,
},
};
export default withMDX({
options: {
remarkPlugins: [],
rehypePlugins: [
[
rehypeShiki,
{
transformers: [twoslashBugWorkaround(transformerTwoslash())],
},
],
],
},
})(nextConfig);VitePress
// .vitepress/config.ts
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
export default defineConfig({
markdown: {
codeTransformers: [twoslashBugWorkaround(transformerTwoslash())],
},
});Docusaurus
// docusaurus.config.js
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
const config = {
presets: [
[
"classic",
{
docs: {
remarkPlugins: [
[
"@docusaurus/remark-plugin-npm2yarn",
{
converters: ["yarn", "pnpm"],
},
],
],
rehypePlugins: [
[
"rehype-shiki",
{
transformers: [twoslashBugWorkaround(transformerTwoslash())],
},
],
],
},
},
],
],
};Compatibility
This package supports @shikijs/twoslash v3.7 and v4.
The test suite currently runs against:
@shikijs/twoslashv4.0.2shikiv4.0.2twoslashv0.3.7
The @log: tag positioning bug still reproduces with @shikijs/twoslash v4.0.2, so this package is still useful for projects that need stable tag placement.
See the peerDependencies section for the supported version ranges.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Development Setup
# Clone the repository
git clone https://github.com/suin/shiki-twoslash-fix.git
cd shiki-twoslash-fix
# Install dependencies
bun install
# Run tests
bun test
# Build the package
bun run buildRunning Tests
The test suite includes both unit tests and integration tests that verify the fix works correctly:
# Run all tests
bun test
# Run tests in watch mode
bun test --watchBug Reports
If you find a bug, please create an issue with:
- A minimal reproduction case
- Your environment details (Node.js version, package versions)
- Expected vs actual behavior
License
MIT © suin
