mirror of
https://github.com/outline/outline.git
synced 2026-08-03 13:27:25 +03:00
* 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>
147 lines
4.6 KiB
TypeScript
147 lines
4.6 KiB
TypeScript
import {
|
|
rewriteAttachmentPaths,
|
|
rewriteInternalLinks,
|
|
} from "./MarkdownAPIImportTask";
|
|
|
|
describe("rewriteAttachmentPaths", () => {
|
|
it("replaces a direct encoded path with the placeholder", () => {
|
|
const out = rewriteAttachmentPaths(
|
|
"",
|
|
[{ id: "att-1", pathInZip: "My Collection/attachments/foo.png" }]
|
|
);
|
|
expect(out).toBe("");
|
|
});
|
|
|
|
it("normalizes legacy `uploads/` bucket layout", () => {
|
|
const out = rewriteAttachmentPaths("", [
|
|
{
|
|
id: "att-2",
|
|
pathInZip: "Some Collection/uploads/abc/file.png",
|
|
},
|
|
]);
|
|
expect(out).toBe("");
|
|
});
|
|
|
|
it("normalizes legacy `public/` bucket layout", () => {
|
|
const out = rewriteAttachmentPaths("", [
|
|
{
|
|
id: "att-3",
|
|
pathInZip: "Some Collection/public/abc/file.png",
|
|
},
|
|
]);
|
|
expect(out).toBe("");
|
|
});
|
|
|
|
it("handles arbitrary folder names like 'attachments/'", () => {
|
|
const out = rewriteAttachmentPaths("", [
|
|
{ id: "att-4", pathInZip: "Collection/attachments/foo.png" },
|
|
]);
|
|
expect(out).toBe("");
|
|
});
|
|
|
|
it("matches nested attachments folders", () => {
|
|
const out = rewriteAttachmentPaths("", [
|
|
{
|
|
id: "att-5",
|
|
pathInZip: "Collection/Doc/attachments/sub/bar.png",
|
|
},
|
|
]);
|
|
expect(out).toBe("");
|
|
});
|
|
|
|
it("substitutes multiple references in the same document", () => {
|
|
const out = rewriteAttachmentPaths(
|
|
" and ",
|
|
[
|
|
{ id: "id-a", pathInZip: "C/attachments/a.png" },
|
|
{ id: "id-b", pathInZip: "C/attachments/b.png" },
|
|
]
|
|
);
|
|
expect(out).toBe(" and ");
|
|
});
|
|
|
|
it("is a no-op when no attachments match", () => {
|
|
const out = rewriteAttachmentPaths("", [
|
|
{ id: "id-a", pathInZip: "C/attachments/a.png" },
|
|
]);
|
|
expect(out).toBe("");
|
|
});
|
|
|
|
it("leaves remote signed URLs untouched so the base task can download them", () => {
|
|
// Slab exports reference images as remote signed URLs rather than files
|
|
// in the zip; these aren't in the manifest and must survive rewriting so
|
|
// the per-page attachment upload step can fetch and re-host them.
|
|
const signedUrl =
|
|
"https://uploads.slab.com/posts/abc/image.png?Signature=xyz&Expires=123";
|
|
const out = rewriteAttachmentPaths(``, [
|
|
{ id: "id-a", pathInZip: "C/attachments/local.png" },
|
|
]);
|
|
expect(out).toBe(``);
|
|
});
|
|
});
|
|
|
|
describe("rewriteInternalLinks", () => {
|
|
it("rewrites a sibling .md link to a placeholder", () => {
|
|
const out = rewriteInternalLinks(
|
|
"see [other](./other.md)",
|
|
"Collection/parent.md",
|
|
{ "Collection/other.md": "doc-1" }
|
|
);
|
|
expect(out).toBe("see [other](<<doc-1>>)");
|
|
});
|
|
|
|
it("rewrites a nested .md link", () => {
|
|
const out = rewriteInternalLinks(
|
|
"see [child](./sub/child.md)",
|
|
"Collection/parent.md",
|
|
{ "Collection/sub/child.md": "doc-2" }
|
|
);
|
|
expect(out).toBe("see [child](<<doc-2>>)");
|
|
});
|
|
|
|
it("leaves unresolved .md links untouched", () => {
|
|
const out = rewriteInternalLinks(
|
|
"see [missing](./missing.md)",
|
|
"Collection/parent.md",
|
|
{}
|
|
);
|
|
expect(out).toBe("see [missing](./missing.md)");
|
|
});
|
|
|
|
it("ignores non-md links", () => {
|
|
const out = rewriteInternalLinks(
|
|
"see [site](https://example.com)",
|
|
"Collection/parent.md",
|
|
{ "Collection/parent.md": "doc-self" }
|
|
);
|
|
expect(out).toBe("see [site](https://example.com)");
|
|
});
|
|
|
|
it("decodes encoded path segments before lookup", () => {
|
|
const out = rewriteInternalLinks(
|
|
"see [other](./My%20Doc.md)",
|
|
"Collection/parent.md",
|
|
{ "Collection/My Doc.md": "doc-3" }
|
|
);
|
|
expect(out).toBe("see [other](<<doc-3>>)");
|
|
});
|
|
|
|
it("rewrites an angle bracketed link to a document with spaces", () => {
|
|
const out = rewriteInternalLinks(
|
|
"see [other](<./My Doc.md>)",
|
|
"Collection/parent.md",
|
|
{ "Collection/My Doc.md": "doc-4" }
|
|
);
|
|
expect(out).toBe("see [other](<<doc-4>>)");
|
|
});
|
|
|
|
it("rewrites a link carrying a title, keeping the title", () => {
|
|
const out = rewriteInternalLinks(
|
|
'see [other](./other.md "The other one")',
|
|
"Collection/parent.md",
|
|
{ "Collection/other.md": "doc-5" }
|
|
);
|
|
expect(out).toBe('see [other](<<doc-5>> "The other one")');
|
|
});
|
|
});
|