rehype plugin
@love-rox/tcy-rehype
A rehype plugin that slots into a `unified` HAST pipeline and applies tate-chu-yoko at build time. Markdown, MDX, plain HTML — all without a client runtime.
Install
@love-rox/tcy-core comes along as a dependency. Use this on any unified-based build (remark / rehype / Astro / eleventy / …). If you specifically want Astro ergonomics, the [Astro adapter](./astro) is purpose-built.
bun
bun add @love-rox/tcy-rehypepnpm
pnpm add @love-rox/tcy-rehypenpm
npm i @love-rox/tcy-rehypeyarn
yarn add @love-rox/tcy-rehypeMarkdown pipeline
Slot rehypeTcy in between remark-rehype and rehype-stringify (or wherever your pipeline finalizes HAST). The wrapping is baked into the HTML before it leaves the build.
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeTcy from "@love-rox/tcy-rehype";
const html = String(
await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeTcy)
.use(rehypeStringify)
.process("第1章 2026年4月"),
);
// <p>第<span class="tcy">1</span>章 <span class="tcy">2026</span>年<span class="tcy">4</span>月</p>HTML-only pipeline
If there's no Markdown in the picture — just HTML in, HTML out — it's the same idea:
import { unified } from "unified";
import rehypeParse from "rehype-parse";
import rehypeStringify from "rehype-stringify";
import rehypeTcy from "@love-rox/tcy-rehype";
const html = String(
await unified()
.use(rehypeParse, { fragment: true })
.use(rehypeTcy)
.use(rehypeStringify)
.process("<p>第1章 2026年4月</p>"),
);Options
The shared options — target, combine, include, exclude, maxLength, excludeWords — behave exactly as they do in the React, Vue, and Astro adapters.
Shared options
| Option | Type | Default | Description |
|---|---|---|---|
| target | 'alphanumeric' | 'alpha' | 'digit' | 'ascii' | RegExp | 'alphanumeric' | What counts as a tate-chu-yoko target. alphanumeric matches [0-9A-Za-z]; ascii covers printable ASCII. A custom RegExp is accepted. |
| combine | boolean | true | Merge consecutive target characters into a single span. Set to false to wrap each character individually. |
| include | string | string[] | undefined | Extra characters to treat as targets regardless of target. |
| exclude | string | string[] | undefined | Characters to exclude. Takes precedence over include. |
| maxLength | number | undefined | Maximum length for a tcy segment. Runs longer than this are demoted back to plain text — for example maxLength: 2 keeps single digits and pairs uprighted while leaving four-digit years like 2026 lying on their side. |
| excludeWords | string[] | undefined | Exact words to exclude from tcy wrapping. Matched against the whole segment value (not a substring) — useful for keeping acronyms like URL / API or specific years out of the upright treatment. |
rehype plugin-only options
| Option | Type | Default | Description |
|---|---|---|---|
| tagName | string | 'span' | Tag name used for wrapping. |
| className | string | string[] | 'tcy' | Class name(s) applied to the wrapping element. Pass an array for multiple classes. |
| skipTags | string[] | ['code', 'pre', 'script', 'style'] | Tags whose subtrees are left untouched. Code/pre/script/style content is skipped by default. |
Behavior notes
- Idempotent. Running the plugin twice on the same HAST gives the same output as running it once — no doubled spans.
- The default
skipTags(code/pre/script/style) keeps code samples and embedded JSON safe from accidental wrapping. - Runs are not joined across element boundaries —
<em>12</em>34produces two separate spans. - Runs filtered out by
maxLengthorexcludeWordsare demoted back to plain text and merged into the surrounding text segment automatically.
If you're on Astro
If you're on Astro 4+ and dealing with .md / .mdx / .astro, the [Astro integration](./astro) wires up the Markdown pipeline and ships a <Tcy> component for .astro files — fewer moving parts to assemble yourself.