- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡 - 药品百科: 搜索 + 分类筛选 + 20种药品静态数据 - 惠教中心: 4条药品使用指南课程 - 我的: 家庭成员信息管理 - 自定义TabBar + navigation-bar组件 - SVG药品分类插图
210 lines
5.4 KiB
TypeScript
210 lines
5.4 KiB
TypeScript
// html-to-wx-nodes.ts
|
|
declare global {
|
|
interface DOMParser {
|
|
new (): DOMParser;
|
|
parseFromString(source: string, mimeType: string): Document;
|
|
}
|
|
|
|
interface Document {
|
|
body: HTMLElement;
|
|
}
|
|
|
|
interface HTMLElement extends Element {
|
|
innerHTML: string;
|
|
}
|
|
|
|
interface Element {
|
|
tagName: string;
|
|
attributes: NamedNodeMap;
|
|
childNodes: ChildNode[];
|
|
}
|
|
|
|
interface ChildNode {
|
|
nodeType: number;
|
|
textContent: string | null;
|
|
}
|
|
|
|
interface NamedNodeMap {
|
|
length: number;
|
|
item(index: number): Attr | null;
|
|
}
|
|
|
|
interface Attr {
|
|
name: string;
|
|
value: string;
|
|
}
|
|
|
|
const Node: {
|
|
TEXT_NODE: number;
|
|
ELEMENT_NODE: number;
|
|
};
|
|
|
|
var DOMParser: {
|
|
prototype: DOMParser;
|
|
new (): DOMParser;
|
|
};
|
|
}
|
|
|
|
type WxNode = {
|
|
name?: string; // 元素节点:标签名,如 'div','img'
|
|
attrs?: Record<string, string>; // 属性集合
|
|
children?: WxNode[]; // 子节点(元素或文本)
|
|
type?: "text"; // 文本节点标记
|
|
text?: string; // 文本节点内容
|
|
};
|
|
|
|
function decodeHtmlEntities(str: string) {
|
|
return str.replace(/<|>|&|"|'/g, (s) => {
|
|
switch (s) {
|
|
case "<":
|
|
return "<";
|
|
case ">":
|
|
return ">";
|
|
case "&":
|
|
return "&";
|
|
case """:
|
|
return '"';
|
|
case "'":
|
|
return "'";
|
|
default:
|
|
return s;
|
|
}
|
|
});
|
|
}
|
|
|
|
function elementToNode(el: Element): WxNode {
|
|
const node: WxNode = {
|
|
name: el.tagName.toLowerCase(),
|
|
attrs: {},
|
|
children: [],
|
|
};
|
|
|
|
// copy attributes
|
|
for (let i = 0; i < el.attributes.length; i++) {
|
|
const a = el.attributes.item(i)!;
|
|
node.attrs![a.name] = a.value;
|
|
}
|
|
|
|
// map special: if data-align present, try to map to style (optional)
|
|
if (node.attrs!["data-align"] && !node.attrs!["style"]) {
|
|
const align = node.attrs!["data-align"];
|
|
if (node.name === "img") {
|
|
if (align === "center") {
|
|
node.attrs!["style"] = "display:block;margin:0 auto;";
|
|
} else if (align === "left") {
|
|
node.attrs!["style"] = "float:left;margin-right:8px;";
|
|
} else if (align === "right") {
|
|
node.attrs!["style"] = "float:right;margin-left:8px;";
|
|
}
|
|
} else {
|
|
// for block elements like div, p
|
|
node.attrs!["style"] = `text-align:${align};`;
|
|
}
|
|
}
|
|
|
|
// children: process childNodes
|
|
el.childNodes.forEach((child: ChildNode) => {
|
|
if (child.nodeType === Node.TEXT_NODE) {
|
|
const txt = (child.textContent || "").trim();
|
|
if (txt.length > 0) {
|
|
node.children!.push({ type: "text", text: decodeHtmlEntities(txt) });
|
|
}
|
|
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
node.children!.push(elementToNode(child as Element & ChildNode));
|
|
}
|
|
});
|
|
|
|
// if no children for img, remove children prop (not required)
|
|
if (node.name === "img") {
|
|
delete node.children;
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
export function htmlToWxNodes(html: string): WxNode[] {
|
|
html = (html || "").trim();
|
|
if (!html) return [];
|
|
|
|
// try DOMParser (browser / devtools)
|
|
if (typeof DOMParser !== "undefined") {
|
|
try {
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, "text/html");
|
|
|
|
const body = doc.body;
|
|
const nodes: WxNode[] = [];
|
|
body.childNodes.forEach((child) => {
|
|
if (child.nodeType === Node.TEXT_NODE) {
|
|
const txt = (child.textContent || "").trim();
|
|
if (txt) nodes.push({ type: "text", text: decodeHtmlEntities(txt) });
|
|
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
nodes.push(elementToNode(child as Element & ChildNode));
|
|
}
|
|
});
|
|
return nodes;
|
|
} catch (e) {
|
|
// fallthrough to fallback parser
|
|
console.warn("DOMParser failed, fallback to simple parse", e);
|
|
}
|
|
}
|
|
|
|
// Fallback: very small parser for simple HTML (img, div, p, plain text)
|
|
const fallbackNodes: WxNode[] = [];
|
|
const imgRegex = /<img\b([^>]+?)\/?>/gi;
|
|
let lastIndex = 0;
|
|
let m;
|
|
while ((m = imgRegex.exec(html)) !== null) {
|
|
// push text before this img as text node
|
|
if (m.index > lastIndex) {
|
|
const txt = html
|
|
.slice(lastIndex, m.index)
|
|
.replace(/<[^>]+>/g, "")
|
|
.trim();
|
|
if (txt)
|
|
fallbackNodes.push({ type: "text", text: decodeHtmlEntities(txt) });
|
|
}
|
|
const attrsStr = m[1];
|
|
const attrs: Record<string, string> = {};
|
|
attrsStr.replace(
|
|
/([^\s=]+)(?:="([^"]*)")?/g,
|
|
(_: string, k: string, v: string) => {
|
|
attrs[k] = v || "";
|
|
return "";
|
|
}
|
|
);
|
|
// normalize src, alt
|
|
const node: WxNode = {
|
|
name: "img",
|
|
attrs: {
|
|
src: attrs.src || attrs["data-src"] || "",
|
|
alt: attrs.alt || "",
|
|
},
|
|
};
|
|
if (attrs["data-align"]) {
|
|
const al = attrs["data-align"];
|
|
if (al === "center")
|
|
node.attrs!["style"] =
|
|
"display:block;margin:0 auto;max-width:100%;height:auto;";
|
|
if (al === "left")
|
|
node.attrs!["style"] =
|
|
"float:left;margin-right:8px;max-width:100%;height:auto;";
|
|
if (al === "right")
|
|
node.attrs!["style"] =
|
|
"float:right;margin-left:8px;max-width:100%;height:auto;";
|
|
}
|
|
fallbackNodes.push(node);
|
|
lastIndex = imgRegex.lastIndex;
|
|
}
|
|
// trailing text
|
|
if (lastIndex < html.length) {
|
|
const trailing = html
|
|
.slice(lastIndex)
|
|
.replace(/<[^>]+>/g, "")
|
|
.trim();
|
|
if (trailing)
|
|
fallbackNodes.push({ type: "text", text: decodeHtmlEntities(trailing) });
|
|
}
|
|
return fallbackNodes;
|
|
}
|