To publish npm package: markdown-it-pangu-pro
May 31, 2026
Beginning of the Story
When I was working on this website last week. I noticed that the spacing between CJK characters and Latin looks too close and unbalanced. It immediately reminded me of a concept that I had heard about: pangu spacing. It basically means that there needs to be spacing of 0.25em width between any CJK and Latin. However, before this incident, I had only known this concept but never cared about it or worried about it, as nowadays, most input methods and operating systems automatically handle this. However, as I am making my personal website I have to solve it myself.
Naive Attempt
At first, I was silly enough to try to solve this problem by manually inserting a half-width space between CJK and Latin in every single markdown. However, it turned out to be impossible. The first problem: I have never had this habit before, so I hardly ever remembered to add the space at all. Another problem is that when inputting Chinese, the space actually defaults to full-width, which is around 0.5em.
Those two problems are, in fact, possible to solve. But the real problem is that the spacing actually varies in width from font to font and weight to weight, which means that it is only technically around 0.25em and can fluctuate.
In that case, we can never solve the problem ideally with manual or auto spacing method.
Journey to My Solution
Then I consulted with Claude.
As the markdown to html conversion is handled by
markdown-it,
we came up with this plan to detect the CJK-Latin pattern and
insert a span element with a fixed width of 0.25em in between.
We use core.ruler.push to access the raw token tree
at the final stage of markdown-it conversion and insert a
function to do the edit.
md.core.ruler.push("cjk_spacing", cjkSpacing);
LLM explanation for markdown-it data flow:
markdown-it processes a document in stages. It first parses the source into a stream of block tokens (paragraphs, headings, lists, code fences); each block that contains inline content is then re-parsed into a child stream of inline tokens (text, emphasis, links, code spans); finally the whole token tree is rendered to HTML.
Between parsing and rendering, there is a third rule chain called core. Core rules run over the fully-parsed token tree — both block and inline — and can mutate it before rendering. Built-in core rules include things like smart-quote replacement and the
typographerpass.core.ruler.push("cjk_spacing", cjkSpacing)appends our rule to the end of that chain, so by the timecjkSpacingruns, every text token already has its final content and we only need to walk the tree, find CJK ↔ Latin transitions inside text tokens, and splice in our<span>token.
To check the full code, see my github repo.
I basically had zero experience with JavaScript before. With this opportunity, I learned a few interesting facts about it.
Built-in and Powerful Support for Regex
This time we need to detect CJK-Latin or Latin-CJK pattern. And this is definitely not a simple pattern to constrain. However, with regex it is simple.
const CJK = '一-鿿㐀-䶿豈--ゟ゠-ヿ';
const LATIN = 'A-Za-z0-9';
const pattern = new RegExp(`([${CJK}]) ?([${LATIN}])|([${LATIN}]) ?([${CJK}])`, "g");
With js’s built-in regex object, we can simply do something like above. And then we can run the search by
let match = pattern.exec(child.content)
and match will be an array whose [0] is the whole pattern,
and [1,2,3,4] will be each captured pattern within ().
Also match.index will be the position of match.
What’s more impressive is that we also have pattern.lastIndex access
which we can use to manually set the starting position of next iteration.
In our particular use case, we can do something like this to prevent
skipping “中文hello中文” where the second match is also first match of next
CJK and Latin combination.
const secondChar = match[2] || match[4];
// some code
lastIndex = match.index + match[0].length - secondChar.length;
pattern.lastIndex = lastIndex;
Arrow function
In JavaScript we can define an inline anonymous function with =>.
Basically we can do
const add = (a, b) => a + b
to quickly define a function inline.
In our use case, we frequently need to insert a processing function
into markdown-it’s routine.
Because of this feature, we can md.core.ruler.push a function
inline without naming that function.
Since this function is very likely not being reused,
it is actually quite a convenient way of defining a function.
However, I don’t really like coding this way.
I always prefer to make code as readable as possible,
so I still define the function with an explicit name.
In that way, we can also push all the edits we want to make together,
and define those actual functions later, which makes the code cleaner.
Here is a sample snippet from my .eleventy.js, which is the tool
I use to generate this website.
I will talk more about
eleventy
later, after some more refinement to this website.
eleventyConfig.addTransform("resolve-relative-paths", resolveRelativePaths);
eleventyConfig.addTransform("rewrite-md-hrefs", rewriteMdHrefs);
eleventyConfig.addCollection("thoughts", sortThoughts);
eleventyConfig.addCollection("noise", sortNoise);
// helper functions declared later
// ...
By the way, the anonymous feature is not provided by arrow functions.
With the function syntax we can also define one anonymously.
But it is definitely fancier to use the arrow.
Miscellaneous
Another interesting fact is that for some historical reason, JavaScript has two popular import systems: ESM and CommonJS.
npm Package
After coming up with this complete solution and making sure that it was working,
I thought it is actually a common need for anyone writing technical
articles with pure markdown and a simple text editor.
So I thought I could probably make it into a
markdown-it
plugin.
It was also quite a new experience for me.
I was pretty sure that a markdown-it plugin is generally just an npm package,
as I had tried some of them. So I probably needed to publish it to npm.
But I didn’t know what the process was.
It turned out to be simple and straightforward.
I only needed an npm account and to publish through the CLI with npm publish.
The only problem was that two-factor verification was somehow not working
on my device, so I ended up generating a 7-day access token to log in to the CLI.
And here is a link to my npm package.