Add a visible caption (<figcaption>) under images, editable from the image bubble-menu and persisted across all formats: native Yjs/JSON, HTML export, and Markdown. - image node: new plain-text `caption` attribute (parse/render `data-caption` on <img>, emitted only when set) + `setImageCaption` command. The node stays an atom; the schema shape is unchanged, so the server's generateHTML/generateJSON path round-trips it for free. - resize node-view: re-parent the resizable wrapper into a <figure> and render the caption in a <figcaption> BELOW it, outside nodeView.wrapper (so onCommit's offsetHeight measurement and the left/right resize handles still cover the image only). This path also drives read-only / share rendering. React placeholder view renders the caption too. - bubble-menu: new useCaptionControl panel modeled on useAltTextControl (own icon, Caption strings, softer sanitizer, ~500 char limit). - markdown lossless round-trip: a captioned image is emitted as a raw <img data-caption> wrapped in a block <div> (same trick as <video>) in both the editor-ext turndown rule and the MCP converter; caption-less images stay clean . Import restores the caption via the shared markdownToHtml + parseHTML. - styles + i18n keys; tests for the schema attr round-trip, markdown round-trip (editor-ext) and the MCP converter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
47 lines
2.2 KiB
TypeScript
47 lines
2.2 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { htmlToMarkdown } from "../markdown/utils/turndown.utils";
|
|
import { markdownToHtml } from "../markdown/utils/marked.utils";
|
|
|
|
// Lossless markdown round-trip for image captions (issue #221). An image WITH a
|
|
// caption can't be expressed as ``, so it is emitted as a raw <img>
|
|
// (carrying data-caption) wrapped in a block <div>, the same trick the <video>
|
|
// rule uses. marked passes the raw HTML through, so markdownToHtml keeps the
|
|
// data-caption, and the image extension's parseHTML restores the attribute.
|
|
describe("image caption markdown round-trip", () => {
|
|
it("HTML -> Markdown emits a raw <img data-caption> for captioned images", () => {
|
|
const html = `<p><img src="/files/a.png" alt="cat" data-caption="A grey cat"></p>`;
|
|
const md = htmlToMarkdown(html);
|
|
expect(md).toContain("data-caption=\"A grey cat\"");
|
|
expect(md).toContain('src="/files/a.png"');
|
|
expect(md).toContain('alt="cat"');
|
|
// It must NOT degrade to the lossy ![]() form.
|
|
expect(md).not.toContain("![cat]");
|
|
});
|
|
|
|
it("Markdown -> HTML restores data-caption on the <img>", async () => {
|
|
const html = `<p><img src="/files/a.png" alt="cat" data-caption="A grey cat"></p>`;
|
|
const md = htmlToMarkdown(html);
|
|
const back = await markdownToHtml(md);
|
|
expect(back).toContain('data-caption="A grey cat"');
|
|
expect(back).toContain('src="/files/a.png"');
|
|
});
|
|
|
|
it("special characters in the caption survive the round-trip (escaped)", async () => {
|
|
const html = `<p><img src="/files/a.png" data-caption='Tom & "Jerry"'></p>`;
|
|
const md = htmlToMarkdown(html);
|
|
const back = await markdownToHtml(md);
|
|
// parse5 keeps the entity-encoded form inside the attribute value.
|
|
expect(back).toContain("data-caption=");
|
|
expect(back).toContain("Jerry");
|
|
expect(back).toContain("Tom");
|
|
});
|
|
|
|
it("caption-less images stay a clean  with no raw HTML", () => {
|
|
const html = `<p><img src="/files/a.png" alt="cat"></p>`;
|
|
const md = htmlToMarkdown(html);
|
|
expect(md).toContain("");
|
|
expect(md).not.toContain("data-caption");
|
|
expect(md).not.toContain("<img");
|
|
});
|
|
});
|