A workaround for Shiki's Twoslash transformer that fixes misaligned code annotations and positioning issues
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.
"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!
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!
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.
# npm
npm install shiki-twoslash-fix
# yarn
yarn add shiki-twoslash-fix
# pnpm
pnpm add shiki-twoslash-fix
# bun
bun add shiki-twoslash-fiximport { 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.
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;
}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,
});import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
const transformer = transformerTwoslash({
explicitTrigger: true,
renderer: "rich",
});
const fixedTransformer = twoslashBugWorkaround(transformer);The bug occurs due to two main issues in the interaction between Twoslash and Shiki:
- 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
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
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
);
}
}
}- 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
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);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 transformer// astro.config.mjs
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
export default defineConfig({
markdown: {
shikiConfig: {
transformers: [twoslashBugWorkaround(transformerTwoslash())],
},
},
});// 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/config.ts
import { transformerTwoslash } from "@shikijs/twoslash";
import { twoslashBugWorkaround } from "shiki-twoslash-fix";
export default defineConfig({
markdown: {
codeTransformers: [twoslashBugWorkaround(transformerTwoslash())],
},
});// 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())],
},
],
],
},
},
],
],
};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.
Contributions are welcome! Please feel free to submit a Pull Request.
# 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 buildThe 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 --watchIf 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
MIT © suin