♻️ 公告解析重构完成

This commit is contained in:
目棃
2024-08-17 18:49:18 +08:00
parent 9e9ba53d99
commit 6bfa1b3cb4
9 changed files with 234 additions and 228 deletions

View File

@@ -1,75 +0,0 @@
/*
* @file assets/css/post-parser.css
* @description 游戏公告解析 css
* @since Beta v0.5.0
*/
.anno-body {
width: 800px;
margin: 0 auto;
font-family: var(--font-text);
}
.anno-title,
.anno-subtitle {
color: var(--common-text-title);
font-family: var(--font-title);
}
.anno-title {
font-size: 20px;
}
.anno-subtitle {
font-size: 16px;
opacity: 0.6;
}
.anno-img {
width: 800px;
max-width: 100%;
height: auto;
border-radius: 10px;
margin: 10px auto;
}
.anno-content {
line-height: 2;
}
.anno-content :deep(a) {
color: #00c3ff;
cursor: pointer;
text-decoration: underline solid #00c3ff;
text-underline-position: under;
transform: translateY(2px);
}
.anno-content :deep(p) {
line-height: 2;
}
.anno-content :deep(details) {
padding: 10px;
border: 1px var(--common-shadow-2) solid;
border-radius: 10px;
margin: 10px;
}
.anno-content :deep(details) div {
margin-left: 10px;
}
.anno-content :deep(details) ::marker {
color: var(--tgc-pink-1);
content: "✧";
}
.anno-content :deep(ol) {
list-style: "✧";
}
.anno-content :deep(td) {
padding-left: 10px;
line-height: 2;
}

View File

@@ -14,6 +14,8 @@ import TpText from "../post/tp-text.vue";
import TpTexts from "../post/tp-texts.vue";
import TpUnknown from "../post/tp-unknown.vue";
import TaTable from "./ta-table.vue";
interface TaParserProps {
data: TGApp.BBS.Announcement.ContentItem;
}
@@ -25,6 +27,7 @@ function getTaName(ta: TGApp.Plugins.Mys.SctPost.Base) {
if (typeof ta.insert === "string") return TpText;
if ("image" in ta.insert) return TpImage;
if ("backup_text" in ta.insert) return TpBackupText;
if ("table" in ta.insert) return TaTable;
return TpUnknown;
}
</script>

View File

@@ -0,0 +1,16 @@
<template>
<div v-html="props.data.insert.table"></div>
</template>
<script lang="ts" setup>
interface TaTableType {
insert: {
table: string;
};
}
interface TaTableProps {
data: TaTableType;
}
const props = defineProps<TaTableProps>();
</script>

View File

@@ -47,7 +47,6 @@ const props = defineProps<TpTextProps>();
const mode = ref<string>("text");
const localEmojis = ref(localStorage.getItem("emojis"));
const emojis = ref<TpText[]>([]);
const router = useRouter();
console.log("tpText", JSON.stringify(props.data.insert), toRaw(props.data)?.attributes);
@@ -100,7 +99,7 @@ async function toLink() {
const link = props.data.attributes.link;
const isPost = await parsePost(link);
if (isPost !== false) {
await router.push({
await useRouter().push({
name: "帖子详情",
params: {
post_id: isPost,

View File

@@ -12,6 +12,7 @@
import { StyleValue } from "vue";
import type { Component } from "vue";
import TpImage from "./tp-image.vue";
import TpMention, { type TpMention as TpMentionType } from "./tp-mention.vue";
import TpText, { type TpText as TpTextType } from "./tp-text.vue";
@@ -29,6 +30,9 @@ function getComp(text: TpTextType | TpMentionType): Component {
if (typeof text.insert === "string") {
return TpText;
}
if ("image" in text.insert) {
return TpImage;
}
return TpMention;
}

View File

@@ -1,5 +1,3 @@
<!-- eslint-disable vue/no-v-html -->
<!-- todo 优化显示样式 -->
<template>
<TSwitchTheme />
<TShareBtn
@@ -17,7 +15,6 @@
<div class="anno-subtitle">
{{ parseText(annoData.subtitle) }}
</div>
<!-- <div class="anno-content" v-html="annoHtml" />-->
<div class="anno-content">
<TaParser :data="annoData" />
</div>
@@ -137,5 +134,29 @@ onUnmounted(() => {
font-family: var(--font-title);
font-size: 14px;
}
.anno-body {
width: 800px;
margin: 0 auto;
font-family: var(--font-text);
}
.anno-title,
.anno-subtitle {
color: var(--common-text-title);
font-family: var(--font-title);
}
.anno-title {
font-size: 20px;
}
.anno-subtitle {
font-size: 16px;
opacity: 0.6;
}
.anno-content {
line-height: 2;
}
</style>
<style lang="css" src="../assets/css/anno-parser.css" scoped></style>

View File

@@ -6,13 +6,11 @@
import { getAnnoCard } from "./getAnnoCard.js";
import { getRequestHeader } from "./getRequestHeader.js";
import { parseAnnoContent } from "./parseAnno.js";
import { getServerByUid, transCookie } from "./tools.js";
const TGUtils = {
Anno: {
getCard: getAnnoCard,
parseContent: parseAnnoContent,
},
User: {
getHeader: getRequestHeader,

View File

@@ -4,8 +4,85 @@
* @since Beta v0.5.3
*/
import { h, render } from "vue";
import TpText from "../../components/post/tp-text.vue";
import { decodeRegExp } from "./tools.js";
/**
* @description 预处理p
* @since Beta v0.5.2
* @param {HTMLParagraphElement} p p 元素
* @returns {HTMLParagraphElement} 解析后的 p 元素
*/
function handleAnnoP(p: HTMLParagraphElement): HTMLParagraphElement {
if (p.children.length === 0) {
p.innerHTML = decodeRegExp(p.innerHTML);
} else {
p.querySelectorAll("*").forEach((child) => {
child.innerHTML = decodeRegExp(child.innerHTML);
child.querySelectorAll("span").forEach((span) => handleAnnoSpan(span));
});
}
return p;
}
/**
* @description 预处理 span
* @since Beta v0.4.4
* @param {HTMLSpanElement} span span 元素
* @returns {HTMLSpanElement} 解析后的 span 元素
*/
function handleAnnoSpan(span: HTMLSpanElement): HTMLSpanElement {
if (span.style.fontSize) {
span.style.fontSize = "";
}
if (span.children.length === 0) {
span.innerHTML = decodeRegExp(span.innerHTML);
} else {
span.querySelectorAll("*").forEach((child) => {
if (child.children.length === 0) {
child.innerHTML = decodeRegExp(child.innerHTML);
}
if (child.tagName === "T") {
child.outerHTML = child.innerHTML;
}
});
}
return span;
}
/**
* @description 预处理table
* @since Beta v0.4.7
* @param {HTMLTableElement} table table 元素
* @returns {HTMLTableElement} 解析后的 table 元素
*/
function handleAnnoTable(table: HTMLTableElement): HTMLTableElement {
table.style.borderColor = "var(--common-shadow-2)";
table.querySelectorAll("colgroup").forEach((colgroup) => colgroup.remove());
table.querySelectorAll("td").forEach((td) => {
if (td.style.backgroundColor) td.style.backgroundColor = "var(--box-bg-1)";
td.style.textAlign = "center";
});
return table;
}
/**
* @description 预处理公告内容
* @since Beta v0.4.4
* @param {string} data 游戏内公告数据
* @returns {string} 解析后的数据
*/
export function handleAnnoContent(data: string): string {
const htmlBase = new DOMParser().parseFromString(data, "text/html");
htmlBase.querySelectorAll("p").forEach((p) => handleAnnoP(p));
htmlBase.querySelectorAll("span").forEach((span) => handleAnnoSpan(span));
htmlBase.querySelectorAll("table").forEach((table) => handleAnnoTable(table));
return htmlBase.body.innerHTML;
}
/**
* @description 解析公告内容,转换为结构化数据
* @since Beta v0.5.3
@@ -16,14 +93,10 @@ export function parseAnnoContent(
anno: TGApp.BBS.Announcement.ContentItem,
): TGApp.Plugins.Mys.SctPost.Base[] {
const parser = new DOMParser();
const doc = parser.parseFromString(anno.content, "text/html");
const first = handleAnnoContent(anno.content);
const doc = parser.parseFromString(first, "text/html");
const children: TGApp.Plugins.Mys.SctPost.Base[] = [];
const bannerNode: TGApp.Plugins.Mys.SctPost.Base = {
insert: {
image: anno.banner,
},
};
children.push(bannerNode);
if (anno.banner !== "") children.push({ insert: { image: anno.banner } });
doc.body.childNodes.forEach((child) => {
children.push(...parseAnnoNode(child));
});
@@ -34,9 +107,13 @@ export function parseAnnoContent(
* @description 解析公告节点
* @since Beta v0.5.3
* @param {Node} node - 节点
* @param {Record<string, string>} attr - 属性
* @returns {TGApp.Plugins.Mys.SctPost.Base} 结构化数据
*/
function parseAnnoNode(node: Node): TGApp.Plugins.Mys.SctPost.Base[] {
function parseAnnoNode(
node: Node,
attr?: Record<string, string>,
): TGApp.Plugins.Mys.SctPost.Base[] {
let defaultRes: TGApp.Plugins.Mys.SctPost.Base = {
insert: {
tag: node.nodeName,
@@ -48,7 +125,7 @@ function parseAnnoNode(node: Node): TGApp.Plugins.Mys.SctPost.Base[] {
return [defaultRes];
}
if (node.nodeType === Node.TEXT_NODE) {
return [{ insert: node.textContent }];
return [{ insert: node.textContent, attributes: attr }];
}
const element = <HTMLElement>node;
defaultRes = {
@@ -61,42 +138,56 @@ function parseAnnoNode(node: Node): TGApp.Plugins.Mys.SctPost.Base[] {
},
};
if (element.tagName === "P") {
return [parseAnnoParagraph(element)];
element.innerHTML = decodeRegExp(element.innerHTML);
return [parseAnnoParagraph(element, attr)];
}
if (element.tagName === "OL" || element.tagName === "LI" || element.tagName === "UL") {
const res: TGApp.Plugins.Mys.SctPost.Base[] = [];
Array.from(element.children).forEach((child) => {
res.push(...parseAnnoNode(child));
res.push(...parseAnnoNode(child, attr));
});
return res;
}
if (element.tagName === "DETAILS") {
return [parseAnnoDetails(element)];
}
// todo table解析
if (element.tagName === "TABLE") {
return [defaultRes];
return [parseAnnoTable(element)];
}
if (element.tagName === "DIV") {
if (element.childNodes.length > 1) {
const res: TGApp.Plugins.Mys.SctPost.Base[] = [];
element.childNodes.forEach((child) => {
res.push(...parseAnnoNode(child));
res.push(...parseAnnoNode(child, attr));
});
return res;
}
if (element.childNodes.length === 0) return [defaultRes];
const child = element.childNodes[0];
if (child.nodeType !== Node.ELEMENT_NODE) return [defaultRes];
return parseAnnoNode(child);
return parseAnnoNode(child, attr);
}
if (element.tagName === "SUMMARY") {
const p = document.createElement("p");
p.innerHTML = element.innerHTML;
return [parseAnnoParagraph(p)];
return [parseAnnoParagraph(p, attr)];
}
if (element.tagName === "SPAN") {
return [parseAnnoSpan(element)];
return [parseAnnoSpan(element, attr)];
}
if (element.tagName === "STRONG") {
const res: TGApp.Plugins.Mys.SctPost.Base[] = [];
element.childNodes.forEach((child) => {
res.push(...parseAnnoNode(child, { bold: "true" }));
});
return res;
}
if (element.tagName === "T") {
const res: TGApp.Plugins.Mys.SctPost.Base[] = [];
element.childNodes.forEach((child) => {
res.push(...parseAnnoNode(child, attr));
});
return res;
}
return [defaultRes];
}
@@ -105,9 +196,13 @@ function parseAnnoNode(node: Node): TGApp.Plugins.Mys.SctPost.Base[] {
* @description 解析公告段落
* @since Beta v0.5.3
* @param {HTMLElement} p - 段落元素
* @param {Record<string, string>} attr - 属性
* @returns {TGApp.Plugins.Mys.SctPost.Base} 结构化数据
*/
function parseAnnoParagraph(p: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
function parseAnnoParagraph(
p: HTMLElement,
attr?: Record<string, string>,
): TGApp.Plugins.Mys.SctPost.Base {
const defaultRes = {
insert: {
tag: p.tagName,
@@ -135,14 +230,14 @@ function parseAnnoParagraph(p: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
span.innerHTML = p.innerHTML;
return {
insert: "",
children: [parseAnnoSpan(span)],
children: [parseAnnoSpan(span, attr)],
};
}
const child = <HTMLElement>p.childNodes[0];
if (child.tagName === "SPAN") {
return {
insert: "",
children: [parseAnnoSpan(child)],
children: [parseAnnoSpan(child, attr)],
};
}
if (child.tagName === "IMG") {
@@ -156,9 +251,10 @@ function parseAnnoParagraph(p: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
};
}
if (child.tagName === "STRONG") {
const res = parseAnnoNode(child, { bold: "true" });
return {
insert: child.innerHTML,
attributes: { bold: true },
insert: "",
children: res,
};
}
return defaultRes;
@@ -184,6 +280,16 @@ function parseAnnoParagraph(p: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
insert: element.innerHTML,
attributes: { bold: true },
};
} else if (element.tagName === "T") {
element.innerHTML = element.outerHTML;
const resE = parseAnnoNode(element);
if (resE.length > 1) {
childRes = { insert: element.outerHTML };
} else {
childRes = resE[0];
}
} else if (element.tagName === "IMG") {
childRes = parseAnnoImage(element);
} else {
throw new Error(`Unknown node type: ${element.tagName}`);
}
@@ -199,9 +305,13 @@ function parseAnnoParagraph(p: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
* @description 解析公告 span
* @since Beta v0.5.3
* @param {HTMLElement} span - span 元素
* @param {Record<string, string>} attr - 属性
* @returns {TGApp.Plugins.Mys.SctPost.Base} 结构化数据
*/
function parseAnnoSpan(span: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
function parseAnnoSpan(
span: HTMLElement,
attr?: Record<string, string>,
): TGApp.Plugins.Mys.SctPost.Base {
const defaultRes = {
insert: {
tag: span.tagName,
@@ -216,6 +326,7 @@ function parseAnnoSpan(span: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
console.error(span.innerHTML);
return {
insert: span.innerHTML === "" ? "\n" : decodeRegExp(span.innerHTML),
attributes: attr,
};
}
if (span.childNodes.length === 1) {
@@ -230,11 +341,10 @@ function parseAnnoSpan(span: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
if (res.length > 1) return defaultRes;
return res[0];
}
let spanAttrs: Record<string, string> | null = {};
let spanAttrs: Record<string, string> | undefined = attr;
if (!spanAttrs) spanAttrs = {};
if (span.style.color !== "") {
spanAttrs.color = span.style.color;
} else {
spanAttrs = null;
}
const parse = decodeRegExp(span.innerHTML);
if (parse.includes("</t>")) {
@@ -347,3 +457,52 @@ function parseAnnoDetails(details: HTMLElement): TGApp.Plugins.Mys.SctPost.Base
},
};
}
/**
* @description 解析公告表格
* @since Beta v0.5.3
* @param {HTMLElement} table - 表格元素
* @returns {TGApp.Plugins.Mys.SctPost.Base} 结构化数据
*/
function parseAnnoTable(table: HTMLElement): TGApp.Plugins.Mys.SctPost.Base {
const defaultRes = {
insert: {
tag: table.tagName,
text: table.textContent,
style: table.style.cssText,
html: table.innerHTML,
outerHtml: table.outerHTML,
},
};
if (table.tagName !== "TABLE") return defaultRes;
if (table.childNodes.length === 0) return defaultRes;
const tableBody = table.querySelector("tbody");
if (tableBody === null) return defaultRes;
tableBody.childNodes.forEach((tr) => {
tr.childNodes.forEach((td) => {
td.childNodes.forEach((child, index) => {
const cellParsed = parseAnnoNode(child);
const span = document.createElement("div");
span.style.lineHeight = "2";
for (const cell of cellParsed) {
if (cell.children && cell.children.length > 0) {
for (const cellChild of cell.children) {
if (cellChild.attributes && JSON.stringify(cellChild.attributes === "{}")) {
delete cellChild.attributes;
}
const cellSpan = document.createElement("span");
render(h(TpText, { data: cellChild }), cellSpan);
span.appendChild(cellSpan);
}
}
}
td.replaceChild(span, td.childNodes[index]);
});
});
});
return {
insert: {
table: table.outerHTML,
},
};
}

View File

@@ -1,119 +0,0 @@
/**
* @file web/utils/parseAnno.ts
* @description 解析游戏内公告数据
* @since Beta v0.5.2
*/
import { saveImgLocal } from "../../utils/TGShare.js";
import { isColorSimilar } from "../../utils/toolFunc.js";
import { decodeRegExp } from "./tools.js";
/**
* @description 解析 a
* @since Beta v0.5.0
* @param {HTMLAnchorElement} a a 元素
* @returns {HTMLAnchorElement} 解析后的 a 元素
*/
function parseAnnoA(a: HTMLAnchorElement): HTMLAnchorElement {
const regex = /javascript:miHoYoGameJSSDK.openIn(Browser|Webview)\('(.+)'\);/;
if (regex.test(a.getAttribute("href") ?? "")) {
const href = a.getAttribute("href")?.match(regex);
if (href) {
a.setAttribute("href", href[2]);
a.target = "_blank";
a.title = href[2];
}
}
return a;
}
/**
* @description 解析 p
* @since Beta v0.5.2
* @param {HTMLParagraphElement} p p 元素
* @returns {HTMLParagraphElement} 解析后的 p 元素
*/
function parseAnnoP(p: HTMLParagraphElement): HTMLParagraphElement {
if (p.children.length === 0) {
p.innerHTML = decodeRegExp(p.innerHTML);
} else {
p.querySelectorAll("*").forEach((child) => {
child.innerHTML = decodeRegExp(child.innerHTML);
child.querySelectorAll("span").forEach((span) => parseAnnoSpan(span));
});
}
return p;
}
/**
* @description 解析 span
* @since Beta v0.4.4
* @param {HTMLSpanElement} span span 元素
* @returns {HTMLSpanElement} 解析后的 span 元素
*/
function parseAnnoSpan(span: HTMLSpanElement): HTMLSpanElement {
if (span.style.fontSize) {
span.style.fontSize = "";
}
if (span.style.color) {
if (isColorSimilar("#000000", span.style.color)) {
span.style.color = "var(--app-page-content)";
}
}
if (span.children.length === 0) {
span.innerHTML = decodeRegExp(span.innerHTML);
} else {
span.querySelectorAll("*").forEach((child) => {
if (child.children.length === 0) {
child.innerHTML = decodeRegExp(child.innerHTML);
}
if (child.tagName === "T") {
child.outerHTML = child.innerHTML;
}
});
}
return span;
}
/**
* @description 解析 table
* @since Beta v0.4.7
* @param {HTMLTableElement} table table 元素
* @returns {HTMLTableElement} 解析后的 table 元素
*/
function parseAnnoTable(table: HTMLTableElement): HTMLTableElement {
table.style.borderColor = "var(--common-shadow-2)";
table.querySelectorAll("colgroup").forEach((colgroup) => colgroup.remove());
table.querySelectorAll("td").forEach((td) => {
if (td.style.backgroundColor) td.style.backgroundColor = "var(--box-bg-1)";
td.style.textAlign = "center";
});
return table;
}
/**
* @description 解析游戏内公告数据
* @since Beta v0.4.4
* @todo 需要完善
* @param {string} data 游戏内公告数据
* @returns {Promise<string>} 解析后的数据
*/
export async function parseAnnoContent(data: string): Promise<string> {
const htmlBase = new DOMParser().parseFromString(data, "text/html");
htmlBase.querySelectorAll("a").forEach((a) => parseAnnoA(a));
const imgList = Array.from(htmlBase.querySelectorAll("img"));
for (const img of imgList) {
img.style.maxWidth = "100%";
img.style.borderRadius = "10px";
img.style.margin = "10px 0";
const src = img.getAttribute("src");
if (src) {
img.setAttribute("src", await saveImgLocal(src));
}
}
htmlBase.querySelectorAll("p").forEach((p) => parseAnnoP(p));
htmlBase.querySelectorAll("span").forEach((span) => parseAnnoSpan(span));
htmlBase.querySelectorAll("table").forEach((table) => parseAnnoTable(table));
return htmlBase.body.innerHTML;
}