Let's edit a text block in Figma as shown in the image:
Figma Plugin API gives the following segments for this text block:
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 }
]
Since the list is long, let's simplify it a bit:
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 }
]
I'm trying to take this segments data and convert it into an HTML tree with Javascript. The output should be as follows:
<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>typesettting </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 countries,</span></li>
</ol>
</ul>
I tried:
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")
}

<ul>and<ol>elements still need to be surrounded by<li>elements.segmentsarray of the above provided example code where an item has the form of ...{ ind: 2, list: "OL", chars: "It has survived\nnot only\nfive centuries,", bold: false, link: null }... already is not a valid transformation result of the array where asegmentsitem has the form of ...{ "characters": "not only\nfive centuries,", "fontWeight": 400, "listOptions": { "type": "ORDERED" }, "indentation": 2, "hyperlink": null }. Not only is text formatted differently but also the indentation varies, one form featuring indentation levels of 3, whereas the other only has 2