feat(baoyu-format-markdown): add content type detection and CJK punctuation handling

This commit is contained in:
Jim Liu 宝玉
2026-01-28 22:31:25 -06:00
parent fe09ec546f
commit 058d25d02c
2 changed files with 196 additions and 16 deletions
@@ -11,6 +11,21 @@ export const CJK_CHAR_RE = new RegExp(`[${CJK_SCRIPTS}]`, "u");
const PUNCT_OR_SYMBOL_RE = /[\p{P}\p{S}]/u;
const WORD_CHAR_RE = /[\p{L}\p{N}]/u;
const CJK_PUNCT_PAIRS: Record<string, string> = {
"“": "”",
"": "",
"": "",
"": "",
"〖": "〗",
"〘": "〙",
"〚": "〛",
"「": "」",
"『": "』",
"〈": "〉",
"《": "》",
"【": "】",
};
function findInlineCodeRanges(text: string): Array<[number, number]> {
const ranges: Array<[number, number]> = [];
let i = 0;
@@ -69,8 +84,120 @@ function isPunctuationOrSymbol(ch: string | undefined): boolean {
return !!ch && PUNCT_OR_SYMBOL_RE.test(ch);
}
function fixCjkEmphasisSpacingInBlock(block: string): string {
function isMatchingCjkPunct(open: string, close: string): boolean {
return CJK_PUNCT_PAIRS[open] === close;
}
function mapNonCodeSegments(
block: string,
mapper: (segment: string) => string
): string {
const codeRanges = findInlineCodeRanges(block);
if (codeRanges.length === 0) {
return mapper(block);
}
let result = "";
let lastIndex = 0;
for (const [start, end] of codeRanges) {
if (start > lastIndex) {
result += mapper(block.slice(lastIndex, start));
}
result += block.slice(start, end + 1);
lastIndex = end + 1;
}
if (lastIndex < block.length) {
result += mapper(block.slice(lastIndex));
}
return result;
}
function moveCjkPunctuationOutsideEmphasis(block: string): string {
return mapNonCodeSegments(block, (segment) => {
const delimiterPositions: number[] = [];
let cursor = 0;
while (cursor < segment.length - 1) {
if (
segment[cursor] === "*" &&
segment[cursor + 1] === "*" &&
segment[cursor - 1] !== "*" &&
segment[cursor + 2] !== "*" &&
!isEscaped(segment, cursor)
) {
delimiterPositions.push(cursor);
cursor += 2;
continue;
}
cursor += 1;
}
if (delimiterPositions.length < 2) return segment;
const stack: number[] = [];
const pairs: Array<{ open: number; close: number }> = [];
for (const pos of delimiterPositions) {
if (stack.length === 0) {
stack.push(pos);
} else {
const open = stack.pop() as number;
pairs.push({ open, close: pos });
}
}
const skip = new Set<number>();
const insertBefore = new Map<number, string>();
for (const pair of pairs) {
const openPunctPos = pair.open + 2;
const closePunctPos = pair.close - 1;
if (openPunctPos >= closePunctPos) continue;
const openPunct = segment[openPunctPos];
const closePunct = segment[closePunctPos];
if (!openPunct || !closePunct) continue;
if (!CJK_OPENING_PUNCT_RE.test(openPunct)) continue;
if (!CJK_CLOSING_PUNCT_RE.test(closePunct)) continue;
if (!isMatchingCjkPunct(openPunct, closePunct)) continue;
if (openPunctPos + 1 >= closePunctPos) continue;
const inner = segment.slice(openPunctPos + 1, closePunctPos);
if (inner.length === 0) continue;
skip.add(openPunctPos);
skip.add(closePunctPos);
insertBefore.set(pair.open, (insertBefore.get(pair.open) ?? "") + openPunct);
const afterClose = pair.close + 2;
insertBefore.set(
afterClose,
(insertBefore.get(afterClose) ?? "") + closePunct
);
}
if (skip.size === 0) return segment;
let result = "";
for (let idx = 0; idx < segment.length; idx += 1) {
const insert = insertBefore.get(idx);
if (insert) {
result += insert;
}
if (skip.has(idx)) {
continue;
}
result += segment[idx];
}
const tailInsert = insertBefore.get(segment.length);
if (tailInsert) {
result += tailInsert;
}
return result;
});
}
function fixCjkEmphasisSpacingInBlock(block: string): string {
const normalized = moveCjkPunctuationOutsideEmphasis(block);
const codeRanges = findInlineCodeRanges(normalized);
let rangeIndex = 0;
const delimiters: Array<{
@@ -79,7 +206,7 @@ function fixCjkEmphasisSpacingInBlock(block: string): string {
canClose: boolean;
}> = [];
let cursor = 0;
while (cursor < block.length - 1) {
while (cursor < normalized.length - 1) {
if (rangeIndex < codeRanges.length && cursor >= codeRanges[rangeIndex][0]) {
if (cursor <= codeRanges[rangeIndex][1]) {
cursor = codeRanges[rangeIndex][1] + 1;
@@ -90,14 +217,14 @@ function fixCjkEmphasisSpacingInBlock(block: string): string {
}
if (
block[cursor] === "*" &&
block[cursor + 1] === "*" &&
block[cursor - 1] !== "*" &&
block[cursor + 2] !== "*" &&
!isEscaped(block, cursor)
normalized[cursor] === "*" &&
normalized[cursor + 1] === "*" &&
normalized[cursor - 1] !== "*" &&
normalized[cursor + 2] !== "*" &&
!isEscaped(normalized, cursor)
) {
const before = block[cursor - 1];
const after = block[cursor + 2];
const before = normalized[cursor - 1];
const after = normalized[cursor + 2];
const beforeIsSpace = isWhitespaceChar(before);
const afterIsSpace = isWhitespaceChar(after);
const beforeIsPunct = isPunctuationOrSymbol(before);
@@ -140,12 +267,12 @@ function fixCjkEmphasisSpacingInBlock(block: string): string {
}
}
if (pairs.length === 0) return block;
if (pairs.length === 0) return normalized;
const insertPositions = new Set<number>();
for (const pair of pairs) {
const insideLast = block[pair.close - 1];
const afterClose = block[pair.close + 2];
const insideLast = normalized[pair.close - 1];
const afterClose = normalized[pair.close + 2];
if (!afterClose) continue;
if (
CJK_CLOSING_PUNCT_RE.test(insideLast) &&
@@ -155,16 +282,16 @@ function fixCjkEmphasisSpacingInBlock(block: string): string {
}
}
if (insertPositions.size === 0) return block;
if (insertPositions.size === 0) return normalized;
let result = "";
for (let idx = 0; idx < block.length; idx += 1) {
for (let idx = 0; idx < normalized.length; idx += 1) {
if (insertPositions.has(idx)) {
result += " ";
}
result += block[idx];
result += normalized[idx];
}
if (insertPositions.has(block.length)) {
if (insertPositions.has(normalized.length)) {
result += " ";
}
return result;