From 945f8ce431b6342741bacecf83c996e299822366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=87=E8=B6=A3=E4=BF=9D=E7=BD=97?= Date: Sun, 4 Aug 2024 18:26:05 +0800 Subject: [PATCH] Add Read "raw_content" contentEditable Element & Edit --- assets/global.less | 2 +- components/biz/read/index.tsx | 13 ++- components/biz/read/read.module.less | 8 ++ components/biz/say/index.tsx | 2 +- components/ui/article/article.module.less | 44 ++++++++++ components/ui/article/index.tsx | 35 ++++++++ components/ui/tab/index.tsx | 9 +- contents/read.ts | 89 +++++++++++++++++++ utils/html.ts | 100 ++++++++++++++++++++++ 9 files changed, 295 insertions(+), 7 deletions(-) create mode 100644 components/biz/read/read.module.less create mode 100644 components/ui/article/article.module.less create mode 100644 components/ui/article/index.tsx create mode 100644 utils/html.ts diff --git a/assets/global.less b/assets/global.less index 16134ec..64d6871 100644 --- a/assets/global.less +++ b/assets/global.less @@ -100,7 +100,7 @@ input[type="checkbox"][role="switch"] { background-color: var(--lighter-gray); transition: border .3s, background-color .3s; - &::before{ + &::before { width: 2em; height: 2em; content: ''; diff --git a/components/biz/read/index.tsx b/components/biz/read/index.tsx index ee7319c..05a2961 100644 --- a/components/biz/read/index.tsx +++ b/components/biz/read/index.tsx @@ -1,10 +1,13 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { sendToBackground } from "@plasmohq/messaging"; import useForm from "~hooks/useForm"; import Tab from "~components/ui/tab"; import Form from "~components/ui/form"; +import Article from "~components/ui/article"; import { add } from "~components/ui/message/utils"; +import styles from "./read.module.less"; + interface FormValue { title: string; link: string; @@ -14,6 +17,7 @@ interface FormValue { image: string; sitename: string; tags: string; + raw_content?: string; } const getInfo = async () => { @@ -46,6 +50,7 @@ const submitForm = (body: FormValue) => { function Read() { const formRef = useRef(); + const [htmlContent, setHtmlContent] = useState(""); const { bindInput, setValues, onSubmit } = useForm({ initialValues: { @@ -60,13 +65,14 @@ function Read() { } setValues(res); + res.raw_content && setHtmlContent(res.raw_content); }) }, []); return ( - + -
+ submitForm({ ...values, raw_content: htmlContent }))}>
@@ -94,6 +100,7 @@ function Read() {
+
diff --git a/components/biz/read/read.module.less b/components/biz/read/read.module.less new file mode 100644 index 0000000..f3d9474 --- /dev/null +++ b/components/biz/read/read.module.less @@ -0,0 +1,8 @@ +.read { + .article { + padding: .75em; + margin-top: 1em; + border-radius: .5em; + border: 1px solid #eee; + } +} diff --git a/components/biz/say/index.tsx b/components/biz/say/index.tsx index 3cc4ade..79114f8 100644 --- a/components/biz/say/index.tsx +++ b/components/biz/say/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from "react"; +import { useRef } from "react"; import { sendToBackground } from "@plasmohq/messaging"; import useForm from "~hooks/useForm"; import Tab from "~components/ui/tab"; diff --git a/components/ui/article/article.module.less b/components/ui/article/article.module.less new file mode 100644 index 0000000..b75af3f --- /dev/null +++ b/components/ui/article/article.module.less @@ -0,0 +1,44 @@ +.article { + overflow-x: auto; + + a { + color: #28b9be; + } + + h1, h2, h3, h4, h5, h6 { + font-weight: bold; + margin-top: 1.5em; + + &:first-child { + margin-top: 0; + } + } + + h1, h2, h3, h4, h5, h6, p { + margin-bottom: 1em; + } + + p { + line-height: 1.7; + } + + img { + height: auto; + } + + pre { + color: #fff; + padding: .5em; + overflow: auto; + border-radius: .5em; + background-color: #333; + } + + :not(pre) > code { + color: brown; + padding: 0 .5em; + border-radius: .5em; + display: inline-block; + background-color: antiquewhite; + } +} diff --git a/components/ui/article/index.tsx b/components/ui/article/index.tsx new file mode 100644 index 0000000..68c7441 --- /dev/null +++ b/components/ui/article/index.tsx @@ -0,0 +1,35 @@ +import { useEffect, useRef } from "react"; +import { clsn } from "~utils"; + +import styles from "./article.module.less"; + +interface ArticleProps { + className?: string; + value: string | number; + onChange: (value: string) => void; +} + +function Article({ className, value, onChange }: ArticleProps) { + const articleRef = useRef(null); + + useEffect(() => { + if (!articleRef.current) { + return; + } + + if (articleRef.current.innerHTML !== String(value)) { + articleRef.current.innerHTML = String(value); + } + }, [value]); + + return ( +
onChange(articleRef.current.innerHTML)} + /> + ); +} + +export default Article; diff --git a/components/ui/tab/index.tsx b/components/ui/tab/index.tsx index ddfb988..8e3870e 100644 --- a/components/ui/tab/index.tsx +++ b/components/ui/tab/index.tsx @@ -1,16 +1,21 @@ +import { clsn } from "~utils"; import { IconBack } from "~assets/icons"; import type { ReactNode, PropsWithChildren } from "react"; import styles from "./tab.module.less"; +interface TabProps extends PropsWithChildren { + className: string; +} + interface HeaderProps { title: ReactNode; onBack: () => void; } -function Tab({ children }: PropsWithChildren) { +function Tab({ className, children }: TabProps) { return ( -
+
{children}
); diff --git a/contents/read.ts b/contents/read.ts index ce95db7..e9ed1ef 100644 --- a/contents/read.ts +++ b/contents/read.ts @@ -1,3 +1,5 @@ +import { formatHTML } from "~utils/html"; + const getTitle = () => { const title = document.title; const metaTitle = document.querySelector(`meta[name="title"]`); @@ -114,6 +116,92 @@ const getTags = () => { return ""; } +const getHTML = () => { + // 微信 + if (location.host === "mp.weixin.qq.com") { + console.log("微信"); + + return document.querySelector(".rich_media_content").innerHTML; + } + + // 腾讯云开发者社区 + if (location.host === "cloud.tencent.com") { + console.log("腾讯云"); + + return formatHTML(document.querySelector(".rno-markdown").innerHTML, (doc) => { + // 替换掉腾讯云自己的链接 + const qcloudLinkEl = doc.querySelectorAll("a"); + qcloudLinkEl.forEach((el) => { + if (el.href.includes("qcloud")) { + el.parentNode.replaceChild(document.createTextNode(el.innerText), el); + } + }); + + const mdTextEl = doc.querySelectorAll(".rno-markdown__textlink-new") as NodeListOf; + mdTextEl.forEach((el) => { + el.parentNode.replaceChild(document.createTextNode(el.innerText), el); + }); + }); + } + + // 掘金 + const itemPropArticle = document.querySelector("[itemprop=articleBody] .markdown-body"); + + if (itemPropArticle) { + console.log("掘金"); + + return formatHTML(itemPropArticle.innerHTML); + } + + // CSDN + const contentViews = document.getElementById("content_views"); + + if (contentViews) { + console.log("CSDN"); + + return formatHTML(contentViews.innerHTML); + } + + // 通用模式 + const articleEl = document.querySelector("article"); + + if (articleEl) { + console.log("文章标签"); + + return formatHTML(articleEl.innerHTML); + } + + // 尝试匹配到文章 + const firstParagraph = document.querySelector("p"); + + let continueFindCorrectParent = true; + let parentEl; + let limitCount = 0; + + if (firstParagraph) { + while (continueFindCorrectParent) { + if (limitCount > 20) { + continueFindCorrectParent = false; + throw new Error("DOM 层级检测遍历次数过多"); + } + + limitCount += 1; + + parentEl = parentEl ? parentEl.parentElement : firstParagraph.parentElement; + + // 这个父下面有多个疑似正文的元素 + if (Array.from(parentEl.querySelectorAll("h2, h3, h4, p")).length > 1) { + console.log(parentEl, `匹配结束,向上遍历 ${limitCount} 次`); + continueFindCorrectParent = false; + } + } + + return formatHTML(parentEl.innerHTML); + } + + return ""; +} + chrome.runtime.onMessage.addListener((req, sender, send) => { if (req.type === "toolbox:getInfo") { send({ @@ -124,6 +212,7 @@ chrome.runtime.onMessage.addListener((req, sender, send) => { image: getImage(), sitename: getSiteName(), tags: getTags(), + raw_content: getHTML(), }); } }); diff --git a/utils/html.ts b/utils/html.ts new file mode 100644 index 0000000..a8f5595 --- /dev/null +++ b/utils/html.ts @@ -0,0 +1,100 @@ +// 删除标题里面没用的内容 +const pickTextNode = (el: HTMLElement) => { + const fragment = document.createDocumentFragment(); + + while (el.firstChild) { + // 检查当前子节点是否是文本节点 + if (el.firstChild.nodeType === Node.TEXT_NODE) { + // 如果是文本节点,将其移动到 DocumentFragment 中 + fragment.appendChild(el.firstChild); + } + else { + el.removeChild(el.firstChild); + } + } + + return fragment.textContent; +} + +// 删除里面的属性 +const removeAttributes = (el: HTMLElement) => { + const attrs = el.attributes; + + Array.from(attrs).forEach((attr) => { + el.removeAttribute(attr.name); + }); +} + +// 格式化 HTML 内容 +export const formatHTML = (html: string, extraFormatter?: (doc: Document) => void) => { + const nextDocument = document.implementation.createHTMLDocument(); + + nextDocument.documentElement.innerHTML = html; + + // 删除标题内无效元素 + const titleEl = nextDocument.querySelectorAll("h2, h3, h4, h5, h6") as NodeListOf; + titleEl.forEach((el) => { + // 遍历并移除所有属性 + removeAttributes(el); + + el.innerHTML = pickTextNode(el); + }); + + // 优化 p 标签 + const paraEl = nextDocument.querySelectorAll("p") as NodeListOf; + paraEl.forEach((el) => { + // 删除空的 p 标签 + if (!el.innerHTML.trim()) { + el.remove(); + } + + // 遍历并移除所有属性 + removeAttributes(el); + + // 删除 p 里面的 span 替换成普通 Text,应该没用的 + const spanEl = el.querySelectorAll("span"); + spanEl.forEach((el) => { + el.parentNode.replaceChild(document.createTextNode(el.innerText), el); + }); + }); + + // 删除 figure 标签 + const figureEl = nextDocument.querySelectorAll("figure") as NodeListOf; + figureEl.forEach((el) => { + const img = el.querySelector("img"); + + if (img) { + el.innerHTML = ""; + el.appendChild(img); + } + }); + + // 删除 style 标签 + const stylesEl = nextDocument.querySelectorAll("style"); + stylesEl.forEach((el) => { + el.remove(); + }); + + // 提取 pre 下面的内容 + const preEl = nextDocument.querySelectorAll("pre"); + preEl.forEach((el) => { + removeAttributes(el); + + // hljs / prism + const codeEl = el.querySelector("code") as HTMLElement; + + if (codeEl) { + const nextCodeEl = document.createElement("code"); + nextCodeEl.innerText = codeEl.innerText; + + el.innerHTML = null; + el.appendChild(nextCodeEl); + } + }); + + if (extraFormatter) { + extraFormatter(nextDocument); + } + + return nextDocument.documentElement.innerHTML; +}