Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions apps/extension/src/inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@
const hasBilibili = syncAccounts.some(a => (a.uid || a.type) === 'bilibili')
const hasWeibo = syncAccounts.some(a => (a.uid || a.type) === 'weibo')
const hasXiaohongshu = syncAccounts.some(a => (a.uid || a.type) === 'xiaohongshu')
const hasToutiao = syncAccounts.some(a => (a.uid || a.type) === 'toutiao')
let clipboardHtmlContent = null
if (
hasWechat ||
Expand All @@ -505,7 +506,8 @@
hasSspai ||
hasBilibili ||
hasWeibo ||
hasXiaohongshu
hasXiaohongshu ||
hasToutiao
) {
// 先点击复制按钮,将带样式的内容复制到剪贴板
const copyBtn =
Expand Down Expand Up @@ -553,7 +555,7 @@
markdown: post.markdown,
thumb: post.thumb,
desc: post.desc,
// 微信公众号、百家号、网易号、Medium、少数派、B站专栏、微博头条和小红书使用剪贴板中带样式的 HTML
// 微信公众号、今日头条、百家号、网易号、Medium、少数派、B站专栏、微博头条和小红书使用剪贴板中带样式的 HTML
wechatHtml:
platformId === 'wechat' ||
platformId === 'baijiahao' ||
Expand All @@ -562,7 +564,8 @@
platformId === 'sspai' ||
platformId === 'bilibili' ||
platformId === 'weibo' ||
platformId === 'xiaohongshu'
platformId === 'xiaohongshu' ||
platformId === 'toutiao'
? clipboardHtmlContent
: null,
},
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/platforms/toutiao.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const ToutiaoPlatform = {
import { injectUtils } from './common.js'

// 今日头条内容填充函数(在页面主世界中执行)
function fillToutiaoContentInPage(title, body) {
function fillToutiaoContentInPage(title, body, wechatHtml) {
// 等待满足条件的元素出现
function waitForElement(predicate, timeout = 10000) {
return new Promise(resolve => {
Expand Down Expand Up @@ -64,7 +64,7 @@ function fillToutiaoContentInPage(title, body) {
// 头条使用 ProseMirror 富文本编辑器
const editor = await waitForElement(() => document.querySelector('.ProseMirror'))

if (editor && body) {
if (editor && (wechatHtml || body)) {
editor.focus()

// 对于 ProseMirror,我们需要更智能的方式来填充内容
Expand All @@ -83,7 +83,7 @@ function fillToutiaoContentInPage(title, body) {
selection.addRange(range)

// 创建 HTML 内容
const htmlContent = lines.map(line => `<p>${line}</p>`).join('')
const htmlContent = wechatHtml || lines.map(line => `<p>${line}</p>`).join('')

// 使用 insertHTML 命令
document.execCommand('insertHTML', false, htmlContent)
Expand Down Expand Up @@ -126,7 +126,7 @@ async function syncToutiaoContent(tab, content, helpers) {
const result = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: fillToutiaoContentInPage,
args: [content.title, content.body || content.markdown || ''],
args: [content.title, content.body || content.markdown || '', content.wechatHtml],
world: 'MAIN',
})

Expand Down
118 changes: 118 additions & 0 deletions tests/toutiao.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import assert from 'node:assert/strict'
import test from 'node:test'

import {
fillToutiaoContentInPage,
syncToutiaoContent,
} from '../packages/core/src/platforms/toutiao.js'

async function withToutiaoEnvironment(callback) {
const previousGlobals = {
document: globalThis.document,
window: globalThis.window,
InputEvent: globalThis.InputEvent,
Event: globalThis.Event,
setTimeout: globalThis.setTimeout,
}
const insertedHtml = []
const titleInput = {
value: '',
focus() {},
dispatchEvent() {},
}
const editor = {
innerHTML: '',
focus() {},
dispatchEvent() {},
}

class TestEvent {
constructor(type, init = {}) {
this.type = type
Object.assign(this, init)
}
}

class TestTextArea {}
Object.defineProperty(TestTextArea.prototype, 'value', {
set(value) {
titleInput.value = value
},
})

globalThis.InputEvent = TestEvent
globalThis.Event = TestEvent
globalThis.setTimeout = callback => {
callback()
return 0
}
globalThis.window = {
HTMLTextAreaElement: TestTextArea,
getSelection: () => ({ removeAllRanges() {}, addRange() {} }),
}
globalThis.document = {
body: {},
querySelector: selector => {
if (selector === 'textarea[placeholder*="标题"]') return titleInput
if (selector === '.ProseMirror') return editor
return null
},
createRange: () => ({ selectNodeContents() {}, collapse() {} }),
execCommand: (command, _showUi, html) => {
assert.equal(command, 'insertHTML')
insertedHtml.push(html)
return true
},
}

try {
await callback({ editor, insertedHtml, titleInput })
} finally {
for (const [key, value] of Object.entries(previousGlobals)) {
if (value === undefined) delete globalThis[key]
else globalThis[key] = value
}
}
}

test('Toutiao receives rendered HTML so formula markup is preserved', async () => {
const formulaHtml = '<section class="katex-block"><svg><text>x = y</text></svg></section>'

await withToutiaoEnvironment(async ({ insertedHtml }) => {
await fillToutiaoContentInPage('Title', '$$x = y$$', formulaHtml)
assert.deepEqual(insertedHtml, [formulaHtml])
})
})

test('Toutiao retains the plain-text fallback when rendered HTML is unavailable', async () => {
await withToutiaoEnvironment(async ({ insertedHtml }) => {
await fillToutiaoContentInPage('Title', 'first line\nsecond line')
assert.deepEqual(insertedHtml, ['<p>first line</p><p>second line</p>'])
})
})

test('Toutiao sync forwards the rendered HTML to the page script', async () => {
const formulaHtml = '<section class="katex-block"><svg><text>x = y</text></svg></section>'

await withToutiaoEnvironment(async () => {
const calls = []
const result = await syncToutiaoContent(
{ id: 1 },
{ title: 'Title', body: '$$x = y$$', wechatHtml: formulaHtml },
{
chrome: {
scripting: {
executeScript: async options => {
calls.push(options)
return [{ result: { success: true } }]
},
},
},
waitForTab: async () => {},
}
)

assert.equal(result.success, true)
assert.deepEqual(calls.at(-1).args, ['Title', '$$x = y$$', formulaHtml])
})
})