如何将Figma文本段数组转换为DOM树对象?
问题:将Figma文本段转换为HTML结构
在Figma中编辑一个文本块后,Figma Plugin API返回如下文本段(segments)数据:
const segments = [ { "characters": "Lorem ", "fontWeight": 400, "listOptions": { "type": "NONE" }, "indentation": 0, "hyperlink": null }, { "characters": "Ipsum", "fontWeight": 700, "listOptions": { "type": "NONE" }, "indentation": 0, "hyperlink": null }, { "characters": " is \nsimply dummy text of \n", "fontWeight": 400, "listOptions": { "type": "NONE" }, "indentation": 0, "hyperlink": null }, { "characters": "the printing and \n", "fontWeight": 400, "listOptions": { "type": "UNORDERED" }, "indentation": 1, "hyperlink": null }, { "characters": "typesetting \n", "fontWeight": 400, "listOptions": { "type": "UNORDERED" }, "indentation": 2, "hyperlink": null }, { "characters": "industry. \n", "fontWeight": 400, "listOptions": { "type": "UNORDERED" }, "indentation": 1, "hyperlink": null }, { "characters": "Lorem Ipsum has been the ", "fontWeight": 400, "listOptions": { "type": "NONE" }, "indentation": 0, "hyperlink": null }, { "characters": "industry's standard", "fontWeight": 400, "listOptions": { "type": "NONE" }, "indentation": 0, "hyperlink": { "type": "URL", "value": "http://example.com" } }, { "characters": " dummy text ever since the 1500s, \n", "fontWeight": 400, "listOptions": { "type": "NONE" }, "indentation": 0, "hyperlink": null }, { "characters": "when an unknown \n", "fontWeight": 400, "listOptions": { "type": "ORDERED" }, "indentation": 1, "hyperlink": null }, { "characters": "printer took \na galley of \ntype and \n", "fontWeight": 400, "listOptions": { "type": "UNORDERED" }, "indentation": 2, "hyperlink": null }, { "characters": "scrambled it\n", "fontWeight": 400, "listOptions": { "type": "ORDERED" }, "indentation": 1, "hyperlink": null }, { "characters": "\nto make a type\n\n", "fontWeight": 400, "listOptions": { "type": "NONE" }, "indentation": 0, "hyperlink": null }, { "characters": "specimen book.\n", "fontWeight": 400, "listOptions": { "type": "UNORDERED" }, "indentation": 1, "hyperlink": null }, { "characters": "It has survived\n", "fontWeight": 400, "listOptions": { "type": "ORDERED" }, "indentation": 3, "hyperlink": null }, { "characters": "not only\nfive centuries,", "fontWeight": 400, "listOptions": { "type": "ORDERED" }, "indentation": 2, "hyperlink": null } ]
为简化阅读,将数据精简为:
const segments = [ { ind: 0, list: null, chars: "Lorem ", bold: false, link: null }, { ind: 0, list: null, chars: "Ipsum", bold: true, link: null }, { ind: 0, list: null, chars: " is \nsimply dummy text of \n", bold: false, link: null }, { ind: 1, list: "UL", chars: "the printing and \n", bold: false, link: null }, { ind: 2, list: "UL", chars: "typesetting \n", bold: false, link: null }, { ind: 1, list: "UL", chars: "industry. \n", bold: false, link: null }, { ind: 0, list: null, chars: "Lorem Ipsum has been the ", bold: false, link: null }, { ind: 0, list: null, chars: "industry's standard", bold: false, link: "http://example.com" }, { ind: 0, list: null, chars: " dummy text ever since the 1500s, \n", bold: false, link: null }, { ind: 1, list: "OL", chars: "when an unknown \n", bold: false, link: null }, { ind: 2, list: "UL", chars: "printer took \na galley of \ntype and \n", bold: false, link: null }, { ind: 1, list: "OL", chars: "scrambled it\n", bold: false, link: null }, { ind: 0, list: null, chars: "\nto make a type\n\n", bold: false, link: null }, { ind: 1, list: "UL", chars: "specimen book.\n", bold: false, link: null }, { ind: 3, list: "OL", chars: "It has survived\n", bold: false, link: null }, { ind: 2, list: "OL", chars: " not only\nfive centuries,", bold: false, link: null } ]
需要将这些文本段转换为如下HTML结构:
<span>Lorem </span> <strong>Ipsum</strong> <span> is <br>simply dummy text of </span> <ul> <li> <span>the printing and </span> </li> <ul> <li><span>typesetting </span></li> </ul> <li><span>industry. </span></li> </ul> <span>Lorem Ipsum has been the </span> <a href="http://example.com">industry's standard</a> <span> dummy text ever since the 1500s, </span> <ol> <li><span>when an unknown </span></li> <ul> <li><span>printer took </span></li> <li><span>a galley of </span></li> <li><span>type and </span></li> </ul> <li><span>scrambled it</span></li> </ol> <span>to make a type</span> <ul> <li> <span>specimen book.</span> </li> <ol> <ol> <li><span>It has survived</span></li> </ol> <li><span>not only</span></li> <li><span>five centuries,</span></li> </ol> </ul>
以下是尝试编写的代码,但无法生成符合预期的DOM结构:
function getPureSegment(chars: string) { if (chars.endsWith("\n")) chars = chars.slice(0, -1) return ["<span>", chars.replaceAll(/\n/g, "<br>"), "</span>"] } function getOpeningListTag(segment) { const type = segment.listOptions.type if (type === "ORDERED") return "<ol>" if (type === "UNORDERED") return "<ul>" } function getClosingListTag(segment) { const type = segment.listOptions.type if (type === "ORDERED") return "</ol>" if (type === "UNORDERED") return "</ul>" } function getHtml(segments) { let prevSegment = { indentation: 0 } return segments.flatMap((segment, idx) => { const pure = getPureSegment(segment.characters) let line const endsBreakLine = segment.characters.endsWith("\n") const isLastSegment = idx === segments.length - 1 if (segment.indentation == 0) { if (segment.indentation < prevSegment.indentation) { line = [getClosingListTag(prevSegment), ...pure] } else { line = pure } } else if (segment.indentation > 0) { if (segment.indentation > prevSegment.indentation) { line = [getOpeningListTag(segment), "<li>", ...pure, (isLastSegment || segments[idx + 1].indentation < segment.indentation) && "</li>"].filter(Boolean) } else if (segment.indentation == prevSegment.indentation) { line = [segments[idx - 1].characters.endsWith("\n") && "<li>", ...pure, endsBreakLine && "</li>"].filter(Boolean) } else { line = [getClosingListTag(segment), "<li>", ...pure, endsBreakLine && "</li>"].filter(Boolean) } if (isLastSegment) line.push("</ul>".repeat(segment.indentation)) } prevSegment = segment return line }).join("\n") }
解决方案
原代码仅通过前后缩进的简单比较处理列表嵌套,未维护列表层级状态栈,也未正确处理列表类型切换、li标签闭合时机及多行文本拆分逻辑。以下是修正后的代码:
// 处理单个文本段的内容,生成对应HTML标签(包含加粗、链接、换行) function getSegmentContent(segment: { characters: string; fontWeight: number; hyperlink: { value: string } | null }) { let content = segment.characters.replace(/\n/g, "<br>"); // 移除末尾的换行(如果有) if (segment.characters.endsWith("\n")) { content = content.slice(0, -4); // 去掉最后的<br> } // 处理超链接 if (segment.hyperlink) { return `<a href="${segment.hyperlink.value}">${content}</a>`; } // 处理加粗 if (segment.fontWeight >= 700) { return `<strong>${content}</strong>`; } // 默认用span包裹 return `<span>${content}</span>`; } // 获取列表的开始标签 function getListStartTag(type: string) { return type === "ORDERED" ? "<ol>" : "<ul>"; } // 获取列表的结束标签 function getListEndTag(type: string) { return type === "ORDERED" ? "</ol>" : "</ul>"; } function convertSegmentsToHtml(segments: any[]) { const output: string[] = []; // 栈结构,保存当前的列表层级信息:{ indent: 缩进级别, type: 列表类型 } const listStack: { indent: number; type: string }[] = []; let currentIndent = 0; let currentLiOpen = false; // 遍历所有文本段 segments.forEach((segment, idx) => { const { indentation, listOptions, characters } = segment; const listType = listOptions.type; const isList = listType !== "NONE"; const nextSegment = segments[idx + 1]; const nextIndent = nextSegment ? nextSegment.indentation : 0; const endsWithNewLine = characters.endsWith("\n"); // 1. 处理缩进变化:关闭需要闭合的列表 while (currentIndent > indentation) { const closedList = listStack.pop()!; output.push(getListEndTag(closedList.type)); currentIndent--; currentLiOpen = false; } // 2. 处理非列表文本段 if (!isList) { // 如果之前有未闭合的li,先闭合 if (currentLiOpen) { output.push("</li>"); currentLiOpen = false; } output.push(getSegmentContent(segment)); currentIndent = indentation; return; } // 3. 处理列表文本段 // 打开新的列表(当缩进增加时) while (currentIndent < indentation) { output.push(getListStartTag(listType)); listStack.push({ indent: currentIndent + 1, type: listType }); currentIndent++; } // 处理li标签:如果当前没有打开的li,或者需要新建li(换行后) if (!currentLiOpen || endsWithNewLine || (nextSegment && nextIndent === indentation && nextSegment.listOptions.type === listType)) { if (currentLiOpen) { output.push("</li>"); } output.push("<li>"); currentLiOpen = true; } // 添加当前段的内容 output.push(getSegmentContent(segment)); // 如果当前段末尾是换行,且下一段不是同层级同类型的列表,闭合li if (endsWithNewLine && (!nextSegment || nextIndent !== indentation || nextSegment.listOptions.type !== listType)) { output.push("</li>"); currentLiOpen = false; } }); // 遍历结束后,闭合所有剩余的列表 while (listStack.length > 0) { const closedList = listStack.pop()!; if (currentLiOpen) { output.push("</li>"); currentLiOpen = false; } output.push(getListEndTag(closedList.type)); } // 格式化输出(可选,用于对齐缩进) return formatHtml(output.join("")); } // 可选:格式化HTML代码,使其更易读 function formatHtml(html: string): string { let formatted = ""; let indentLevel = 0; const indent = " "; let inTag = false; for (let i = 0; i < html.length; i++) { const char = html[i]; if (char === "<") { inTag = true; // 判断是结束标签 if (html[i + 1] === "/") { indentLevel--; formatted += "\n" + indent.repeat(indentLevel); } else { formatted += "\n" + indent.repeat(indentLevel); } } formatted += char; if (char === ">") { inTag = false; // 判断是开始标签且不是自闭合标签 if (html[i - 1] !== "/" && html[i - 2] !== "/") { if (!html.slice(i + 1).match(/^\s*<\//)) { indentLevel++; } } } } return formatted.trim(); } // 使用示例 const htmlResult = convertSegmentsToHtml(segments); console.log(htmlResult);
代码说明
- 状态栈维护:用
listStack跟踪当前嵌套的列表层级和类型,解决多层列表嵌套的闭合问题。 - 文本段处理:单独处理加粗、超链接、换行,确保每个文本段生成正确的HTML标签。
- 列表与li的逻辑:根据缩进变化、换行符、下一段的状态,精准控制
li标签的打开和闭合时机。 - 格式化输出:可选的
formatHtml函数用于美化输出的HTML结构,便于阅读。
该代码可正确生成符合预期的HTML结构,处理多层嵌套列表、不同列表类型切换、文本样式(加粗、链接)以及换行拆分li的逻辑。
内容的提问来源于stack exchange,提问作者Cihad Paksoy
相关产品推荐
相关产品推荐

