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.

v0.3.0MITbuild-time

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-rehype

pnpm

pnpm add @love-rox/tcy-rehype

npm

npm i @love-rox/tcy-rehype

yarn

yarn add @love-rox/tcy-rehype

Markdown 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.

Markdown → HTMLts
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:

HTML → HTMLts
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

OptionTypeDefaultDescription
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.
combinebooleantrueMerge consecutive target characters into a single span. Set to false to wrap each character individually.
includestring | string[]undefinedExtra characters to treat as targets regardless of target.
excludestring | string[]undefinedCharacters to exclude. Takes precedence over include.
maxLengthnumberundefinedMaximum 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.
excludeWordsstring[]undefinedExact 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

OptionTypeDefaultDescription
tagNamestring'span'Tag name used for wrapping.
classNamestring | string[]'tcy'Class name(s) applied to the wrapping element. Pass an array for multiple classes.
skipTagsstring[]['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>34 produces two separate spans.
  • Runs filtered out by maxLength or excludeWords are 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.