npf2html
Version:
Converts Tumblr's Neue Post Format to plain HTML
146 lines • 5.74 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderInlineFormat = renderInlineFormat;
exports.formatText = formatText;
/**
* Pre-processes {@link formatting} to combine any adjacent identical formats.
* Tumblr sometimes splits these up when other formatting is also present, so
* this produces cleaner HTML.
**/
function mergeAdjacentFormats(formatting) {
// A map from format types to the index in mergedFormats of the most recent
// occurance of those formats.
const lastFormatOfType = {};
const mergedFormats = [];
for (const format of formatting) {
// Never merge links or mentions.
if (format.type === 'link' || format.type === 'mention') {
mergedFormats.push(format);
continue;
}
const lastIndex = lastFormatOfType[format.type];
if (lastIndex !== undefined) {
const last = mergedFormats[lastIndex];
if (last && canMerge(last, format)) {
mergedFormats[lastIndex] = { ...last, end: format.end };
continue;
}
}
lastFormatOfType[format.type] = mergedFormats.length;
mergedFormats.push(format);
}
return mergedFormats;
}
/** Returns whether two {@link InlineFormat}s can be safely merged. */
function canMerge(format1, format2) {
if (format1.end !== format2.start)
return false;
if (format1.type !== format2.type)
return false;
switch (format1.type) {
case 'bold':
case 'italic':
case 'strikethrough':
case 'small':
return true;
case 'link':
case 'mention':
return false;
case 'color':
return format1.hex === format2.hex;
}
}
/** Builds the list of {@link InlineFormatSpan}s for {@link block}. */
function buildFormatSpans(text, formatting) {
var _a;
// Sort formats first by start (earliest to latest), then by length (longest
// to shortest). This ensures that earlier formats are never nested within
// later ones.
const formats = [...formatting].sort((a, b) => a.start === b.start ? b.end - a.end : a.start - b.start);
// A stack of open spans. Because formats is sorted by start, this will be as
// well.
const open = [];
// The fully-closed spans of formatted text.
const spans = [];
let codeUnitIndex = 0;
let codePointIndex = 0;
const end = Math.max(...formats.map(format => format.end));
while (codeUnitIndex < end) {
while (codePointIndex === ((_a = formats[0]) === null || _a === void 0 ? void 0 : _a.start)) {
open.push({ format: formats.shift(), start: codeUnitIndex, children: [] });
}
const outermostClosed = open.findIndex(span => span.format.end === codePointIndex + 1);
const codePointLength = text.charCodeAt(codeUnitIndex) >> 10 === 0x36 ? 2 : 1;
if (outermostClosed !== -1) {
// Tumblr allows inline formats to overlap without being subsets of one
// another. To handle this in HTML, we track the formats that aren't
// closed yet and add them back into `open` afterwards.
const stillOpen = [];
for (let j = outermostClosed; j < open.length; j++) {
const span = open[j];
(j === 0 ? spans : open[j - 1].children).push({
...span,
end: codeUnitIndex + codePointLength,
});
if (span.format.end > codePointIndex + 1) {
stillOpen.push({ ...span, start: codeUnitIndex + codePointLength });
}
}
-open.splice(outermostClosed);
open.push(...stillOpen);
}
codeUnitIndex += codePointLength;
codePointIndex++;
}
if (open.length > 0)
spans.push({ ...open[0], end });
for (let i = 1; i < open.length; i++) {
spans.at(-1).children.push({ ...open[i], end });
}
return spans;
}
/**
* Applies the formatting specified by {@link format} to {@link html}, which may
* already include nested formatting.
*
* @category Inline
*/
function renderInlineFormat(renderer, html, format) {
switch (format.type) {
case 'bold':
return `<strong>${html}</strong>`;
case 'italic':
return `<em>${html}</em>`;
case 'strikethrough':
return `<s>${html}</s>`;
case 'small':
return `<small>${html}</small>`;
case 'link':
return `<a href="${renderer.escape(format.url)}">${html}</a>`;
case 'mention':
return (`<a class="${renderer.prefix}-inline-mention"` +
` href="${renderer.escape(format.blog.url)}">${html}</a>`);
case 'color':
return (`<span style="color: ${renderer.escape(format.hex)}">` +
html +
'</span>');
}
}
/** HTML-escapes {@link text} and formats it according to {@link formatting}. */
function formatText(renderer, text, formatting) {
if (!formatting)
return renderer.escape(text);
const renderSpans = (start, end, children) => {
let result = '';
let i = start;
for (const child of children) {
result +=
renderer.escape(text.substring(i, child.start)) +
renderer.renderInlineFormat(renderSpans(child.start, child.end, child.children), child.format);
i = child.end;
}
return result + renderer.escape(text.substring(i, end));
};
return renderSpans(0, text.length, buildFormatSpans(text, mergeAdjacentFormats(formatting)));
}
//# sourceMappingURL=inline-format.js.map