Files
陈誉午 994b267f5c feat: 电子药箱小程序 - 4Tab药品管理
- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡
- 药品百科: 搜索 + 分类筛选 + 20种药品静态数据
- 惠教中心: 4条药品使用指南课程
- 我的: 家庭成员信息管理
- 自定义TabBar + navigation-bar组件
- SVG药品分类插图
2026-07-29 11:20:52 +08:00

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(/&lt;|&gt;|&amp;|&quot;|&#39;/g, (s) => {
switch (s) {
case "&lt;":
return "<";
case "&gt;":
return ">";
case "&amp;":
return "&";
case "&quot;":
return '"';
case "&#39;":
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;
}