From ac1f92d816ff2c1b662f84541ac20e8e8c734473 Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 26 Jun 2026 13:53:43 +0800 Subject: [PATCH] =?UTF-8?q?refactor(core):=20=E8=BF=81=E7=A7=BB=20post-con?= =?UTF-8?q?tent=20=E5=88=B0=20yggdrasil-core,=E5=88=A0=E9=99=A4=20public/j?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 逐行 TypeScript 化(逻辑不变),改走 window.__initPostContent 全局入口。 post-content.js 已迁移进 libs/yggdrasil-core/,public/js/ 目录随之删除。 --- libs/yggdrasil-core/src/index.ts | 6 +- libs/yggdrasil-core/src/post-content.test.ts | 73 ++++++++++++++++++++ libs/yggdrasil-core/src/post-content.ts | 48 +++++++++++++ public/js/post-content.js | 48 ------------- src/components/post/post_content.rs | 4 +- 5 files changed, 126 insertions(+), 53 deletions(-) create mode 100644 libs/yggdrasil-core/src/post-content.test.ts create mode 100644 libs/yggdrasil-core/src/post-content.ts delete mode 100644 public/js/post-content.js diff --git a/libs/yggdrasil-core/src/index.ts b/libs/yggdrasil-core/src/index.ts index 0ef5cdb..cf8c324 100644 --- a/libs/yggdrasil-core/src/index.ts +++ b/libs/yggdrasil-core/src/index.ts @@ -1,11 +1,11 @@ +import { initPostContent } from './post-content'; + declare global { interface Window { __initPostContent: (selector: string) => void; } } -window.__initPostContent = (_selector: string): void => { - // Task 2 实现 -}; +window.__initPostContent = initPostContent; export {}; diff --git a/libs/yggdrasil-core/src/post-content.test.ts b/libs/yggdrasil-core/src/post-content.test.ts new file mode 100644 index 0000000..38daa4b --- /dev/null +++ b/libs/yggdrasil-core/src/post-content.test.ts @@ -0,0 +1,73 @@ +/** + * post-content 测试:钉住代码块 copy 按钮的生成与复制行为。 + * 黑盒驱动:只通过 window.__initPostContent 入口。 + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import './index'; + +describe('initPostContent', () => { + beforeEach(() => { + document.body.innerHTML = ''; + vi.restoreAllMocks(); + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + document.body.innerHTML = ''; + }); + + it('为 pre>code 注入 .copy-code 按钮', () => { + const root = document.createElement('div'); + root.className = 'post-content'; + root.innerHTML = '
console.log(1)
'; + document.body.appendChild(root); + + window.__initPostContent('.post-content'); + + const btn = root.querySelector('pre .copy-code'); + expect(btn).not.toBeNull(); + expect(btn?.textContent).toBe('copy'); + }); + + it('已有 .copy-code 的 pre 不重复注入', () => { + const root = document.createElement('div'); + root.className = 'post-content'; + root.innerHTML = + '
x
'; + document.body.appendChild(root); + + window.__initPostContent('.post-content'); + + const btns = root.querySelectorAll('pre .copy-code'); + expect(btns.length).toBe(1); + }); + + it('点击按钮调用 clipboard.writeText 并回显 copied!', () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, 'clipboard', { + value: { writeText }, + configurable: true, + }); + + const root = document.createElement('div'); + root.className = 'post-content'; + root.innerHTML = '
hello
'; + document.body.appendChild(root); + + window.__initPostContent('.post-content'); + + const btn = root.querySelector('.copy-code') as HTMLButtonElement; + btn.click(); + + expect(writeText).toHaveBeenCalledWith('hello'); + expect(btn.textContent).toBe('copied!'); + + // 2 秒后还原回 'copy' + vi.advanceTimersByTime(2000); + expect(btn.textContent).toBe('copy'); + }); + + it('selector 未命中时不报错', () => { + expect(() => window.__initPostContent('.not-exist')).not.toThrow(); + }); +}); diff --git a/libs/yggdrasil-core/src/post-content.ts b/libs/yggdrasil-core/src/post-content.ts new file mode 100644 index 0000000..4e15c29 --- /dev/null +++ b/libs/yggdrasil-core/src/post-content.ts @@ -0,0 +1,48 @@ +/** + * 代码块 copy 按钮。 + * + * 为 pre>code 注入一个 .copy-code 按钮,点击复制代码文本并回显 copied!。 + * 2 秒后还原文案。样式见 input.css 的 .copy-code 规则(全局 Tailwind 构建)。 + */ + +function initCopyButtons(root: Element): void { + const blocks = root.querySelectorAll('pre > code'); + blocks.forEach((code) => { + const pre = code.parentElement; + if (!pre) return; + if (pre.querySelector('.copy-code')) return; + + const btn = document.createElement('button'); + btn.className = 'copy-code'; + btn.textContent = 'copy'; + btn.setAttribute('aria-label', 'Copy code'); + + const codeText = code.textContent || ''; + btn.addEventListener('click', () => { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(codeText); + } else { + const ta = document.createElement('textarea'); + ta.value = codeText; + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); + } + btn.textContent = 'copied!'; + setTimeout(() => { + btn.textContent = 'copy'; + }, 2000); + }); + + pre.appendChild(btn); + }); +} + +export function initPostContent(selector: string): void { + const root = document.querySelector(selector); + if (!root) return; + initCopyButtons(root); +} diff --git a/public/js/post-content.js b/public/js/post-content.js deleted file mode 100644 index c392afe..0000000 --- a/public/js/post-content.js +++ /dev/null @@ -1,48 +0,0 @@ -(function () { - "use strict"; - - function initCopyButtons(root) { - var blocks = root.querySelectorAll("pre > code"); - for (var i = 0; i < blocks.length; i++) { - var code = blocks[i]; - var pre = code.parentElement; - if (!pre) continue; - if (pre.querySelector(".copy-code")) continue; - - var btn = document.createElement("button"); - btn.className = "copy-code"; - btn.textContent = "copy"; - btn.setAttribute("aria-label", "Copy code"); - - (function (codeText) { - btn.addEventListener("click", function () { - var self = this; - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(codeText); - } else { - var ta = document.createElement("textarea"); - ta.value = codeText; - ta.style.position = "fixed"; - ta.style.opacity = "0"; - document.body.appendChild(ta); - ta.select(); - document.execCommand("copy"); - document.body.removeChild(ta); - } - self.textContent = "copied!"; - setTimeout(function () { - self.textContent = "copy"; - }, 2000); - }); - })(code.textContent || ""); - - pre.appendChild(btn); - } - } - - window.__initPostContent = function (selector) { - var root = document.querySelector(selector); - if (!root) return; - initCopyButtons(root); - }; -})(); diff --git a/src/components/post/post_content.rs b/src/components/post/post_content.rs index 157f9e4..cfc7145 100644 --- a/src/components/post/post_content.rs +++ b/src/components/post/post_content.rs @@ -10,14 +10,14 @@ use dioxus::prelude::*; /// - `content_html`:服务端渲染的文章 HTML 字符串 /// /// 关键行为: -/// - 在 `target_arch = "wasm32"` 环境下执行 `post-content.js`(代码块复制)。 +/// - 在 `target_arch = "wasm32"` 环境下调用 `window.__initPostContent` 初始化代码块 +/// 复制按钮(`yggdrasil-core.js` 已由 `Dioxus.toml` 全局注入)。 /// 灯箱(图片灯箱 + 懒加载)改由 `Dioxus.toml` 全局注入 `lightbox.js`, /// 这里仅设置其初始化配置 `__lightboxSelectors` 并兜底调用。 #[component] pub fn PostContent(content_html: String) -> Element { #[cfg(target_arch = "wasm32")] use_effect(move || { - let _ = js_sys::eval(include_str!("../../../public/js/post-content.js")); let _ = js_sys::eval("window.__initPostContent('.post-content')"); // lightbox 改由 Dioxus.toml 全局