Skip to content

Concealment Mode Principles: Why Width Calculation Matters ​

What You'll Learn

  • Understand how OpenCode concealment mode works
  • Know why regular formatting tools misalign tables in concealment mode
  • Master the plugin's width calculation algorithm (three steps)
  • Understand the role of Bun.stringWidth

Your Current Challenge ​

You're writing code in OpenCode, and AI generates a beautiful table:

markdown
| 字段 | 类型 | 说明 |
|--- | --- | ---|
| **name** | string | 用户名 |
| age | number | 年龄 |

It looks neat in the source view. But when you switch to preview mode, the table is misaligned:

| 字段     | 类型   | 说明   |
|--- | --- | ---|
| name | string | 用户名 |    ← Why is it shorter?
| age      | number | 年龄   |

Where's the problem? Concealment mode.

What is Concealment Mode ​

OpenCode enables concealment mode by default, which hides Markdown syntax symbols during rendering:

SourceDisplayed in Concealment Mode
**bold**bold(4 characters)
*italic*italic(4 characters)
~~strikethrough~~strikethrough(6 characters)
`code`code(4 characters + background)

Benefits of Concealment Mode

Focus on the content itself, not being distracted by a bunch of ** and * symbols.

Why Regular Formatting Tools Have Problems ​

Regular table formatting tools count **name** as 8 characters when calculating width:

** n a m e ** = 8 characters

But in concealment mode, users see name, which is only 4 characters.

The result: the formatting tool aligns based on 8 characters, but users see 4 characters, so the table is naturally misaligned.

Core Idea: Calculate "Display Width" Instead of "Character Length" ​

The core idea of this plugin is: calculate the width that users actually see, not the character count in the source code.

The algorithm consists of three steps:

Step 1: Protect code blocks (don't strip symbols inside code blocks)
Step 2: Strip Markdown symbols (**, *, ~~, etc.)
Step 3: Use Bun.stringWidth to calculate the final width

Follow Along: Understanding the Three-Step Algorithm ​

Step 1: Protect Code Blocks ​

Why

Markdown symbols in inline code (wrapped in backticks) are "literals". Users see **bold** as 8 characters, not bold as 4 characters.

So before stripping Markdown symbols, we need to "hide" the code block content first.

Source Implementation

typescript
// Step 1: Extract and protect inline code
const codeBlocks: string[] = []
let textWithPlaceholders = text.replace(/`(.+?)`/g, (match, content) => {
  codeBlocks.push(content)
  return `\x00CODE${codeBlocks.length - 1}\x00`
})

How It Works

InputAfter ProcessingcodeBlocks Array
`**bold**`\x00CODE0\x00["**bold**"]
`a` and `b`\x00CODE0\x00 and \x00CODE1\x00["a", "b"]

Replace code blocks with special placeholders like \x00CODE0\x00, so they won't be accidentally damaged when stripping Markdown symbols later.

Step 2: Strip Markdown Symbols ​

Why

In concealment mode, **bold** displays as bold, and *italic* displays as italic. When calculating width, we need to remove these symbols.

Source Implementation

typescript
// Step 2: Strip Markdown symbols from non-code parts
let visualText = textWithPlaceholders
let previousText = ""

while (visualText !== previousText) {
  previousText = visualText
  visualText = visualText
    .replace(/\*\*\*(.+?)\*\*\*/g, "$1") // ***bold italic*** → text
    .replace(/\*\*(.+?)\*\*/g, "$1")     // **bold** → bold
    .replace(/\*(.+?)\*/g, "$1")         // *italic* → italic
    .replace(/~~(.+?)~~/g, "$1")         // ~~strikethrough~~ → strikethrough
    .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "$1")     // ![alt](url) → alt
    .replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)") // [text](url) → text (url)
}

Why Use a While Loop?

To handle nested syntax. For example ***bold italic***:

Round 1: ***bold italic*** → **bold italic**(strip outermost ***)
Round 2: **bold italic** → *bold italic*(strip **)
Round 3: *bold italic* → bold italic(strip *)
Round 4: bold italic = bold italic(no change, exit loop)
Image and Link Processing
  • Images ![alt](url): OpenCode only displays alt text, so it's replaced with alt
  • Links [text](url): Displayed as text (url), preserving URL information

Step 3: Restore Code Blocks + Calculate Width ​

Why

Code block content needs to be restored, then use Bun.stringWidth to calculate the final display width.

Source Implementation

typescript
// Step 3: Restore code block content
visualText = visualText.replace(/\x00CODE(\d+)\x00/g, (match, index) => {
  return codeBlocks[parseInt(index)]
})

return Bun.stringWidth(visualText)

Why Use Bun.stringWidth?

Bun.stringWidth correctly calculates:

Character TypeExampleCharacter CountDisplay Width
ASCIIabc33
Chinese你好24(each occupies 2 cells)
Emoji😀12(occupies 2 cells)
Zero-widtha\u200Bb32(zero-width character doesn't take space)

Ordinary text.length can only count characters, unable to handle these special cases.

Complete Example ​

Assume the cell content is: **`code`** and *text*

Step 1: Protect Code Blocks

Input: **`code`** and *text*
Output: **\x00CODE0\x00** and *text*
codeBlocks = ["code"]

Step 2: Strip Markdown Symbols

Round 1: **\x00CODE0\x00** and *text* → \x00CODE0\x00 and text
Round 2: No change, exit

Step 3: Restore Code Blocks + Calculate Width

After restoration: code and text
Width: Bun.stringWidth("code and text") = 13

Ultimately, the plugin aligns this cell with a width of 13 characters, not the source code's 22 characters.

Checkpoint ​

After completing this lesson, you should be able to answer:

  • [ ] What symbols are hidden in concealment mode? (Answer: **, *, ~~, etc. Markdown syntax symbols)
  • [ ] Why protect code blocks first? (Answer: Symbols inside code blocks are literals and should not be stripped)
  • [ ] Why use a while loop to strip symbols? (Answer: To handle nested syntax, like ***bold italic***)
  • [ ] How is Bun.stringWidth better than text.length? (Answer: Can correctly calculate display width for Chinese, emoji, zero-width characters)

Common Pitfalls ​

Common Misconceptions

Misconception: ** in code blocks will also be stripped

Fact: No. The plugin first protects code block content with placeholders, then strips symbols from other parts, and finally restores.

So the width of `**bold**` is 8(**bold**), not 4(bold)。

Lesson Summary ​

StepPurposeKey Code
Protect Code BlocksPrevent symbols inside code blocks from being accidentally strippedtext.replace(/\(.+?)`/g, ...)`
Strip MarkdownCalculate actual displayed content in concealment modeMultiple rounds of regex replacement
Calculate WidthHandle Chinese, emoji, and other special charactersBun.stringWidth()

Next Lesson Preview ​

In the next lesson, we'll learn Table Specifications.

You will learn:

  • What tables can be formatted
  • 4 rules for table validation
  • How to avoid "invalid table" errors

Appendix: Source Code Reference ​

Click to expand source code locations

Updated: 2026-01-26

FunctionFile PathLine Numbers
Display width calculation entryindex.ts151-159
Code block protectionindex.ts168-173
Markdown symbol strippingindex.ts175-188
Code block restorationindex.ts190-193
Bun.stringWidth callindex.ts195

Key Functions:

  • calculateDisplayWidth(): Width calculation entry with caching
  • getStringWidth(): Core algorithm, strips Markdown symbols and calculates display width

Key Constants:

  • \x00CODE{n}\x00: Code block placeholder format