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 itn", "fontWeight": 400, "listOptions": { "type": "ORDERED" }, "indentation": 1, "hyperlink": null },
{ "characters": "nto make a typenn", "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 survivedn", "fontWeight": 400, "listOptions": { "type": "ORDERED" }, "indentation": 3, "hyperlink": null },
{ "characters": "not onlynfive 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 itn", bold: false, link: null },
{ ind: 0, list: null, chars: "nto make a typenn", bold: false, link: null },
{ ind: 1, list: "UL", chars: "specimen book.n", bold: false, link: null },
{ ind: 2, list: "OL", chars: "It has survivednnot onlynfive 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")
}
2
Answers
Here’s how you can do it by creating a recursive function to handle the nested lists. This also takes care of the bold and hyperlink features:
This script will process each segment in order, keeping track of the current list indentation level and type. When it encounters a segment with a higher indentation level, it calls itself recursively to process the nested list. When it encounters a segment with a lower indentation level, it returns the HTML built up to this point, and the calling function will close the list tag and continue processing.
The convertSegmentToHTML function handles the conversion of individual segments to HTML, taking care of bold text and hyperlinks.
A reliable solution can be achieved already by a single
reduce
based iteration cycle over the OP’s computedsegments
array.The markup gets aggregated while
reduce
consumes the array by invoking the reducer function for eachsegments
-item. Thus, the reducer function needs to be implemented in a way where one can keep track of the aggregated markup’s opened/closed nested list-tags. One way of achieving it, is to provide acollector
object as thereduce
methods initial value, which does carry all the necessary data in addition to e.g. itsresult
property.