Add Read "raw_content" contentEditable Element & Edit

This commit is contained in:
奇趣保罗 2024-08-04 18:26:05 +08:00
parent 99d63d4912
commit 945f8ce431
9 changed files with 295 additions and 7 deletions

View File

@ -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<HTMLFormElement>();
const [htmlContent, setHtmlContent] = useState("");
const { bindInput, setValues, onSubmit } = useForm<FormValue>({
initialValues: {
@ -60,13 +65,14 @@ function Read() {
}
setValues(res);
res.raw_content && setHtmlContent(res.raw_content);
})
}, []);
return (
<Tab>
<Tab className={styles.read}>
<Tab.Body>
<Form ref={formRef} onSubmit={onSubmit(submitForm)}>
<Form ref={formRef} onSubmit={onSubmit((values) => submitForm({ ...values, raw_content: htmlContent }))}>
<input {...bindInput("title")} required placeholder="标题" />
<input {...bindInput("link")} required placeholder="链接" />
<div>
@ -94,6 +100,7 @@ function Read() {
<input {...bindInput("tags")} placeholder="标签" />
</div>
</Form>
<Article className={styles.article} value={htmlContent} onChange={setHtmlContent} />
</Tab.Body>
<Tab.Footer>
<button onClick={() => formRef.current?.requestSubmit()}></button>

View File

@ -0,0 +1,8 @@
.read {
.article {
padding: .75em;
margin-top: 1em;
border-radius: .5em;
border: 1px solid #eee;
}
}

View File

@ -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";

View File

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

View File

@ -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<HTMLElement>(null);
useEffect(() => {
if (!articleRef.current) {
return;
}
if (articleRef.current.innerHTML !== String(value)) {
articleRef.current.innerHTML = String(value);
}
}, [value]);
return (
<article
ref={articleRef}
className={clsn(styles.article, className)}
contentEditable
onInput={(ev) => onChange(articleRef.current.innerHTML)}
/>
);
}
export default Article;

View File

@ -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 (
<div className={styles.tab}>
<div className={clsn(styles.tab, className)}>
{children}
</div>
);

View File

@ -1,3 +1,5 @@
import { formatHTML } from "~utils/html";
const getTitle = () => {
const title = document.title;
const metaTitle = document.querySelector<HTMLMetaElement>(`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<HTMLElement>;
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(),
});
}
});

100
utils/html.ts Normal file
View File

@ -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<HTMLElement>;
titleEl.forEach((el) => {
// 遍历并移除所有属性
removeAttributes(el);
el.innerHTML = pickTextNode(el);
});
// 优化 p 标签
const paraEl = nextDocument.querySelectorAll("p") as NodeListOf<HTMLElement>;
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<HTMLElement>;
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;
}