Files
outline/shared/utils/markdown.ts
T
000f2f98fb feat: Add TextPack (.textpack) support (#13235)
* feat: Add TextPack (.textpack) import support

Adds a single-document importer for TextPack, the zipped variant of
TextBundle used by Bear, Ulysses, iA Writer and others. Bare .textbundle
directories are not supported, as a directory cannot be delivered through
a browser file input.

The bundle's text entry may use any extension, per the spec, with
info.json's type deciding whether it can be read as markdown. Assets are
inlined as data URIs for the existing attachment pipeline to pick up,
bounded by the attachment size limit and a memory ceiling, and only for
media types markdown-it accepts as a link destination.

Also fixes an existing bug where a file embedded in an HTML or email
import as a data URI was stored in the document as base64 rather than
being uploaded as an attachment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Refactor to individual converters

* refactor

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 20:42:05 -04:00

95 lines
2.4 KiB
TypeScript

const escapes: [RegExp, string][] = [
[/\\/g, "\\\\"],
[/\*/g, "\\*"],
[/^-/g, "\\-"],
[/^\+ /g, "\\+ "],
[/^(=+)/g, "\\$1"],
[/^(#{1,6}) /g, "\\$1 "],
[/`/g, "\\`"],
[/^~~~/g, "\\~~~"],
[/\[/g, "\\["],
[/\]/g, "\\]"],
[/\(/g, "\\("], // OLN-91
[/\)/g, "\\)"], // OLN-91
[/^>/g, "\\>"],
[/_/g, "\\_"],
[/^(\d+)\. /g, "$1\\. "],
[/\$/g, "\\$"],
];
/**
* Escape markdown characters in a string
*
* @param text - The text to escape
* @returns The escaped text
*/
export const escape = function (text: string) {
return escapes.reduce(function (accumulator, esc) {
return accumulator.replace(esc[0], esc[1]);
}, text);
};
/**
* Unescape markdown characters in a string
*
* @param text - The text to unescape
* @returns The unescaped text
*/
export const unescape = function (text: string) {
return text.replace(/\\([\\*+-\d.])/g, "$1");
};
/**
* Matches a markdown link or image, capturing the text between its
* parentheses. A destination containing an unescaped closing parenthesis is
* not matched.
*/
const linkRegex = /(!?\[[^\]]*\]\()([^)]*)(\))/g;
/**
* Replaces the destination of every markdown link and image in a string.
*
* Understands the angle-bracket form used when a destination contains spaces,
* and preserves any link title.
*
* @param text The markdown text to rewrite.
* @param replace Called with each destination; return the replacement, or
* undefined to leave the link as it was written.
* @returns The markdown with replaced destinations.
*/
export function replaceMarkdownLinks(
text: string,
replace: (href: string) => string | undefined
): string {
return text.replace(
linkRegex,
(match, prefix: string, target: string, suffix: string) => {
const leading = target.length - target.trimStart().length;
const trimmed = target.trim();
let href: string;
let title: string;
if (trimmed.startsWith("<")) {
const end = trimmed.indexOf(">");
if (end === -1) {
return match;
}
href = trimmed.slice(1, end);
title = target.slice(leading + end + 1);
} else {
[href] = trimmed.split(/\s/, 1);
if (!href) {
return match;
}
title = target.slice(leading + href.length);
}
const replacement = replace(href);
return replacement === undefined
? match
: `${prefix}${replacement}${title}${suffix}`;
}
);
}