refactor(core): 迁移 post-content 到 yggdrasil-core,删除 public/js

逐行 TypeScript 化(逻辑不变),改走 window.__initPostContent 全局入口。
post-content.js 已迁移进 libs/yggdrasil-core/,public/js/ 目录随之删除。
This commit is contained in:
xfy 2026-06-26 13:53:43 +08:00
parent afabdf4655
commit ac1f92d816
5 changed files with 126 additions and 53 deletions

View File

@ -1,11 +1,11 @@
import { initPostContent } from './post-content';
declare global { declare global {
interface Window { interface Window {
__initPostContent: (selector: string) => void; __initPostContent: (selector: string) => void;
} }
} }
window.__initPostContent = (_selector: string): void => { window.__initPostContent = initPostContent;
// Task 2 实现
};
export {}; export {};

View File

@ -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 = '<pre><code>console.log(1)</code></pre>';
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 =
'<pre><code>x</code><button class="copy-code">copy</button></pre>';
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 = '<pre><code>hello</code></pre>';
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();
});
});

View File

@ -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);
}

View File

@ -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);
};
})();

View File

@ -10,14 +10,14 @@ use dioxus::prelude::*;
/// - `content_html`:服务端渲染的文章 HTML 字符串 /// - `content_html`:服务端渲染的文章 HTML 字符串
/// ///
/// 关键行为: /// 关键行为:
/// - 在 `target_arch = "wasm32"` 环境下执行 `post-content.js`(代码块复制)。 /// - 在 `target_arch = "wasm32"` 环境下调用 `window.__initPostContent` 初始化代码块
/// 复制按钮(`yggdrasil-core.js` 已由 `Dioxus.toml` 全局注入)。
/// 灯箱(图片灯箱 + 懒加载)改由 `Dioxus.toml` 全局注入 `lightbox.js` /// 灯箱(图片灯箱 + 懒加载)改由 `Dioxus.toml` 全局注入 `lightbox.js`
/// 这里仅设置其初始化配置 `__lightboxSelectors` 并兜底调用。 /// 这里仅设置其初始化配置 `__lightboxSelectors` 并兜底调用。
#[component] #[component]
pub fn PostContent(content_html: String) -> Element { pub fn PostContent(content_html: String) -> Element {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use_effect(move || { use_effect(move || {
let _ = js_sys::eval(include_str!("../../../public/js/post-content.js"));
let _ = js_sys::eval("window.__initPostContent('.post-content')"); let _ = js_sys::eval("window.__initPostContent('.post-content')");
// lightbox 改由 Dioxus.toml 全局 <script src> 加载(不再 include_str!)。 // lightbox 改由 Dioxus.toml 全局 <script src> 加载(不再 include_str!)。
// 双保险契约:先设配置,若 lightbox.js 已加载则立即调用; // 双保险契约:先设配置,若 lightbox.js 已加载则立即调用;