fix(frontend): 修复 aardio 注释着色、代码行距与折叠 hydration
注释不再被运算符拆开;去掉 grid 行 min-height;挂载后再折叠以避免 SSR/CSR 不一致。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -96,27 +96,53 @@ function readProp(
|
||||
return typeof v === "string" ? v : undefined;
|
||||
}
|
||||
|
||||
/** Markdown 代码块:语言标签、复制、>5 行默认折叠;客户端仅在未 SSR 高亮时补色 */
|
||||
/** 子树里是否已有 Shiki 双主题 token(--shiki-light),用于判断 SSR 是否真正着色成功 */
|
||||
function hasShikiTokens(node: ReactNode): boolean {
|
||||
if (node == null || typeof node === "boolean") return false;
|
||||
if (Array.isArray(node)) return node.some(hasShikiTokens);
|
||||
if (!isReactElement(node)) return false;
|
||||
const props = node.props as Record<string, unknown>;
|
||||
const style = props.style;
|
||||
if (typeof style === "string" && style.includes("--shiki-light")) return true;
|
||||
if (
|
||||
style &&
|
||||
typeof style === "object" &&
|
||||
("--shiki-light" in (style as object) ||
|
||||
Object.values(style as Record<string, unknown>).some(
|
||||
(v) => typeof v === "string" && String(v).includes("#")
|
||||
))
|
||||
) {
|
||||
// React 可能把 CSS 变量写成 style={{ ['--shiki-light']: '#xxx' }}
|
||||
const rec = style as Record<string, unknown>;
|
||||
if ("--shiki-light" in rec || "color" in rec) {
|
||||
// 有 --shiki-light 即视为已着色;仅有 color 不够(可能是别的)
|
||||
if ("--shiki-light" in rec) return true;
|
||||
}
|
||||
for (const k of Object.keys(rec)) {
|
||||
if (k.includes("shiki")) return true;
|
||||
}
|
||||
}
|
||||
return hasShikiTokens(props.children as ReactNode);
|
||||
}
|
||||
|
||||
/** Markdown 代码块:语言标签、复制、>5 行默认折叠;客户端仅在未真正着色时补色 */
|
||||
export default function MdCodeBlock({ children, className, ...rest }: MdCodeBlockProps) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const hostRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const restMap = rest as Record<string, unknown>;
|
||||
const dataLanguage = readProp(restMap, "data-language");
|
||||
const dataTheme = readProp(restMap, "data-theme");
|
||||
|
||||
const plainText = useMemo(() => extractText(children), [children]);
|
||||
const approxLines = useMemo(() => countLines(null, plainText), [plainText]);
|
||||
|
||||
// rehype-pretty-code 会去掉 pre 上的 shiki class,但会保留 data-theme / data-language
|
||||
const serverHighlighted =
|
||||
(typeof className === "string" && className.includes("shiki")) ||
|
||||
!!dataTheme ||
|
||||
!!dataLanguage;
|
||||
// 以是否存在 --shiki-light token 为准;勿仅凭 data-language(失败回退 plaintext 时仍带该属性)
|
||||
const serverHighlighted = useMemo(() => hasShikiTokens(children), [children]);
|
||||
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [lineCount, setLineCount] = useState(approxLines);
|
||||
/** 挂载后再启用折叠,避免 SSR/CSR 行数统计不一致导致 hydration mismatch */
|
||||
const [foldReady, setFoldReady] = useState(false);
|
||||
const [lineCount, setLineCount] = useState(0);
|
||||
const [language, setLanguage] = useState(() =>
|
||||
readLanguage(null, className, dataLanguage)
|
||||
);
|
||||
@@ -127,9 +153,10 @@ export default function MdCodeBlock({ children, className, ...rest }: MdCodeBloc
|
||||
const pre = host?.querySelector("pre") ?? null;
|
||||
setLineCount(countLines(pre, plainText));
|
||||
setLanguage(readLanguage(pre, className, dataLanguage));
|
||||
setFoldReady(true);
|
||||
}, [plainText, className, dataLanguage, clientHtml]);
|
||||
|
||||
// 仅客户端预览(无 SSR 高亮)时动态补双主题;切勿覆盖服务端已渲染内容
|
||||
// SSR 未真正着色(如自定义语言 loadLanguage 失败回退)时,客户端补双主题
|
||||
useEffect(() => {
|
||||
if (serverHighlighted || !plainText.trim()) return;
|
||||
const lang = readLanguage(null, className, dataLanguage) || "plaintext";
|
||||
@@ -149,7 +176,7 @@ export default function MdCodeBlock({ children, className, ...rest }: MdCodeBloc
|
||||
}, [plainText, className, dataLanguage, serverHighlighted]);
|
||||
|
||||
const effectiveLines = lineCount > 0 ? lineCount : approxLines;
|
||||
const canCollapse = effectiveLines > COLLAPSE_LINE_THRESHOLD;
|
||||
const canCollapse = foldReady && effectiveLines > COLLAPSE_LINE_THRESHOLD;
|
||||
const collapsed = canCollapse && !expanded;
|
||||
|
||||
const onCopy = async () => {
|
||||
|
||||
Reference in New Issue
Block a user