在进行中英文混排的文本编写时,常常需要在中文字符和英文字符之间添加空格,以提升可读性和美观度。然而,手动添加这些空格既繁琐又容易出错。尤其是针对历史文章进行一一手动处理,工作量极大,而且容易遗漏。为了解决这个问题,我们可以利用 Astro 的强大功能,自动化地为中英文混排文本添加适当的空格。主要参考了 中文文案排版指北 这篇文章中的建议。
由于我用的 MDX 格式编写文章,有时为了实现更加复杂的样式会使用纯 HTML 代码来替代 Markdown 语法。所以实际使用了两个插件分别对 Markdown 和 HTML 内容进行处理。这时候 Astro 的性能优势就体现出来了,所有的处理都在构建阶段完成,不会影响页面加载速度。
创建 remark 和 rehype 插件文件
首先需要在项目根目录下新建两个文件,分别命名为 remark-cn-spacing.mjs 和 rehype-cn-spacing.mjs,用于处理 Markdown 和 HTML 内容。
mkdir -p scripts/remark
mkdir -p scripts/rehype
touch scripts/remark/remark-cn-spacing.mjs
touch scripts/rehype/rehype-cn-spacing.mjs
编辑 remark 和 rehype 插件内容
// srcipts/remark/remark-cn-spacing.mjs
import { visit } from "unist-util-visit";
const ZH = /[\u4e00-\u9fff]/;
const EN = /[A-Za-z0-9]/;
// =====================
// text 节点内部
// =====================
function fixTextSpacing(text) {
return text
.replace(/([\u4e00-\u9fff])([A-Za-z0-9])/g, "$1 $2")
.replace(/([A-Za-z0-9])([\u4e00-\u9fff])/g, "$1 $2")
.replace(/([\u4e00-\u9fff])([()])/g, "$1 $2")
.replace(/([()])([\u4e00-\u9fff])/g, "$1 $2");
}
// =====================
// 节点边界判断
// =====================
function nodeEndsWithZH(node) {
if (node.type === "text") {
const v = node.value.trimEnd();
return v && ZH.test(v[v.length - 1]);
}
return false;
}
function nodeStartsWithENLike(node) {
return (
node.type === "link" ||
node.type === "inlineCode" ||
node.type === "emphasis" ||
node.type === "strong"
);
}
function nodeEndsWithENLike(node) {
return (
node.type === "link" ||
node.type === "inlineCode" ||
node.type === "emphasis" ||
node.type === "strong"
);
}
function nodeStartsWithZH(node) {
if (node.type === "text") {
const v = node.value.trimStart();
return v && ZH.test(v[0]);
}
return false;
}
// =====================
// 插件主体
// =====================
export default function remarkCnSpacing() {
return (tree) => {
/**
* 1️⃣ 修 text 节点内部
*/
visit(tree, "text", (node, index, parent) => {
if (
parent?.type === "code" ||
parent?.type === "inlineCode" ||
parent?.type?.startsWith("mdxJsx")
) {
return;
}
const fixed = fixTextSpacing(node.value);
if (fixed !== node.value) {
node.value = fixed;
}
});
/**
* 2️⃣ 修 paragraph 内相邻节点边界
*/
visit(tree, "paragraph", (node) => {
const children = node.children;
for (let i = 0; i < children.length - 1; i++) {
const cur = children[i];
const next = children[i + 1];
// 中文 + EN-like(link / code / emphasis / strong)
if (nodeEndsWithZH(cur) && nodeStartsWithENLike(next)) {
children.splice(i + 1, 0, { type: "text", value: " " });
i++;
continue;
}
// EN-like + 中文
if (nodeEndsWithENLike(cur) && nodeStartsWithZH(next)) {
children.splice(i + 1, 0, { type: "text", value: " " });
i++;
}
}
});
};
}
// scripts/rehype/rehype-cn-spacing.mjs
import { visit } from "unist-util-visit";
const ZH = /[\u4e00-\u9fff]/;
const EN = /[A-Za-z0-9]/;
function fixText(text) {
return text
.replace(/([\u4e00-\u9fff])([A-Za-z0-9])/g, "$1 $2")
.replace(/([A-Za-z0-9])([\u4e00-\u9fff])/g, "$1 $2")
.replace(/([\u4e00-\u9fff])([()])/g, "$1 $2")
.replace(/([()])([\u4e00-\u9fff])/g, "$1 $2");
}
function isInlineElement(node) {
return (
node.tagName === "a" ||
node.tagName === "span" ||
node.tagName === "code" ||
node.tagName === "strong" ||
node.tagName === "em"
);
}
export default function rehypeCnSpacing() {
return (tree) => {
visit(tree, "text", (node, index, parent) => {
if (!parent) return;
// 1️⃣ 修 text 内部
node.value = fixText(node.value);
// 2️⃣ 修 inline 元素边界
const siblings = parent.children;
if (!siblings) return;
const prev = siblings[index - 1];
const next = siblings[index + 1];
// 前一个是 inline element + 中文
if (
prev &&
isInlineElement(prev) &&
ZH.test(node.value[0])
) {
node.value = " " + node.value;
}
// 后一个是 inline element + 中文
if (
next &&
isInlineElement(next) &&
ZH.test(node.value[node.value.length - 1])
) {
node.value = node.value + " ";
}
});
};
}
修改 astro.config.mjs 文件
// astro.config.mjs
import mdx from "@astrojs/mdx";
import remarkCnSpacing from "./scripts/remark/remark-cn-spacing.mjs";
import rehypeCnSpacing from "./scripts/rehype/rehype-cn-spacing.mjs";
export default {
integrations: [
mdx({
remarkPlugins: [remarkCnSpacing],
rehypePlugins: [rehypeCnSpacing],
}),
],
};
然后重新 npm run dev 即可生效了。