让我们在 Figma 中编辑一个文本块,如图所示:
Figma 插件 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: 2, list: "OL", chars: "It has survived\nnot only\nfive centuries,", bold: false, link: null }
]
我正在尝试获取此段数据并使用 Javascript 将其转换为 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>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>
我尝试过:
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")
}
以下是通过创建递归函数来处理嵌套列表的方法。这也处理了粗体和超链接功能:
function convertSegmentToHTML(segment) {
let html = segment.chars.replace(/\n/g, '<br>');
if (segment.bold) {
html = `<strong>${html}</strong>`;
}
if (segment.link) {
html = `<a href="${segment.link}">${html}</a>`;
}
return html;
}
function processSegments(segments) {
let html = '';
let listTag = null;
let listIndent = null;
while (segments.length > 0) {
const segment = segments[0];
if (segment.ind !== listIndent) {
if (listIndent !== null) {
html += `</${listTag}>`;
if (segment.ind < listIndent) {
return html;
}
}
listIndent = segment.ind;
listTag = segment.list ? (segment.list === "UL" ? "ul" : "ol") : null;
if (listTag) {
html += `<${listTag}>`;
}
}
segments.shift();
if (listTag) {
html += `<li>${convertSegmentToHTML(segment)}</li>`;
if (segments.length > 0 && segments[0].ind > segment.ind) {
html += processSegments(segments);
}
} else {
html += convertSegmentToHTML(segment);
}
}
if (listIndent !== null) {
html += `</${listTag}>`;
}
return html;
}
const html = processSegments(segments);
console.log(html);
此脚本将按顺序处理每个段,并跟踪当前列表缩进级别和类型。当遇到缩进级别较高的段时,它会递归调用自身来处理嵌套列表。当遇到缩进级别较低的段时,它返回到目前为止构建的 HTML,调用函数将关闭列表标签并继续处理。
convertSegmentToHTML 函数处理各个片段到 HTML 的转换,并处理粗体文本和超链接。
通过 OP 计算的 reduce
数组上基于单个
segments
的迭代循环,已经可以实现可靠的解决方案。
标记被聚合,而
reduce
通过为每个segments
项调用reducer函数来消耗数组。因此,reducer 函数需要以一种可以跟踪聚合标记的打开/关闭嵌套列表标签的方式来实现。实现它的一种方法是提供一个 collector
对象作为 reduce
方法 初始值 ,除了例如数据之外,它还携带所有必要的数据。它的 result
属性。
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: 2, list: "OL", chars: "It has survived\nnot only\nfive centuries,", bold: false, link: null }
];
const markup = segments
.reduce(aggregateMarkup, { result: '' })
.result;
document
.querySelector('#test')
.innerHTML = markup;
console.log('markup ...', markup);
body { margin: 0; }
#test { width: 50%; }
.as-console-wrapper { left: auto!important; width: 50%; min-height: 100%; }
<script>
function aggregateMarkup(collector, segment, idx, segmentArray) {
let { openTagNames = [], result = '' } = collector;
const {
ind: indentation, list,
bold: isBold, link, chars = '',
} = segment;
const isLink = !!link;
const isListItem = !!list;
const isOpeningListItem =
isListItem && ((segmentArray[idx - 1]?.ind ?? 0) < indentation);
const isTerminatingListItem =
isListItem && ((segmentArray[idx + 1]?.ind ?? 0) < indentation);
let markup = chars
.trim()
.replace(/(?:^[\n\s]+)|(?:[\n\s]+$)/g, '')
.replace(/\n/g, '<br\/>');
if (!isBold && !isLink && !isListItem) {
result = `${ result } <span>${ markup }<\/span> `;
} else {
const listTagName = String(list ?? '').toLowerCase();
if (isOpeningListItem) {
// - keep track of opening list tag-names
// by pushing the currently opened tag-name
// into the tracking-list for every opened list-tag.
openTagNames.push(listTagName);
result = result + `<${ listTagName }>`;
}
if (isBold) {
markup = `<strong>${ markup }<\/strong>`;
}
if (isLink) {
markup = `<a href="${ link }">${ markup }<\/a>`;
}
if (isListItem) {
markup = `<li>${ markup }<\/li>`;
}
result = [result, markup].join(' ');
if (isTerminatingListItem) {
// - keep track of opening list tag-names
// by removing the last tag-name from the
// tracking-list for every closed list-tag.
openTagNames.pop();
result = `${ result }<\/${ listTagName }>`;
}
}
if ((idx >= segmentArray.length - 1) && !!openTagNames.length) {
// - at the time, the entire segments array has been iterated
// make sure to terminate every tracked, still unclosed tag.
result = openTagNames
.reduce((markup, tagName) => `${ markup }<\/${ tagName }>`, result);
}
return { openTagNames, result };
}
</script>
<div id="test"></div>