diff --git a/.changeset/lovely-laws-return.md b/.changeset/lovely-laws-return.md new file mode 100644 index 000000000..374a1105a --- /dev/null +++ b/.changeset/lovely-laws-return.md @@ -0,0 +1,5 @@ +--- +'@portaljs/remark-wiki-link': minor +--- + +Add image resize feature diff --git a/package-lock.json b/package-lock.json index 8690c7e5b..2c0a090b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49897,7 +49897,7 @@ }, "packages/components": { "name": "@portaljs/components", - "version": "0.5.10", + "version": "0.6.0", "dependencies": { "@githubocto/flat-ui": "^0.14.1", "@heroicons/react": "^2.0.17", @@ -50383,7 +50383,7 @@ }, "packages/remark-wiki-link": { "name": "@portaljs/remark-wiki-link", - "version": "1.1.2", + "version": "1.1.3", "license": "MIT", "dependencies": { "mdast-util-to-markdown": "^1.5.0", diff --git a/packages/remark-wiki-link/package.json b/packages/remark-wiki-link/package.json index 2d6ec07c9..ce91ee51f 100644 --- a/packages/remark-wiki-link/package.json +++ b/packages/remark-wiki-link/package.json @@ -1,6 +1,6 @@ { "name": "@portaljs/remark-wiki-link", - "version": "1.1.2", + "version": "1.1.3", "description": "Parse and render wiki-style links in markdown especially Obsidian style links.", "repository": { "type": "git", diff --git a/packages/remark-wiki-link/src/lib/fromMarkdown.ts b/packages/remark-wiki-link/src/lib/fromMarkdown.ts index 5b5f7b9c8..088142248 100644 --- a/packages/remark-wiki-link/src/lib/fromMarkdown.ts +++ b/packages/remark-wiki-link/src/lib/fromMarkdown.ts @@ -1,23 +1,23 @@ -import { isSupportedFileFormat } from "./isSupportedFileFormat"; +import { isSupportedFileFormat } from './isSupportedFileFormat'; const defaultWikiLinkResolver = (target: string) => { // for [[#heading]] links if (!target) { return []; } - let permalink = target.replace(/\/index$/, ""); + let permalink = target.replace(/\/index$/, ''); // TODO what to do with [[index]] link? if (permalink.length === 0) { - permalink = "/"; + permalink = '/'; } return [permalink]; }; export interface FromMarkdownOptions { pathFormat?: - | "raw" // default; use for regular relative or absolute paths - | "obsidian-absolute" // use for Obsidian-style absolute paths (with no leading slash) - | "obsidian-short"; // use for Obsidian-style shortened paths (shortest path possible) + | 'raw' // default; use for regular relative or absolute paths + | 'obsidian-absolute' // use for Obsidian-style absolute paths (with no leading slash) + | 'obsidian-short'; // use for Obsidian-style shortened paths (shortest path possible) permalinks?: string[]; // list of permalinks to match possible permalinks of a wiki link against wikiLinkResolver?: (name: string) => string[]; // function to resolve wiki links to an array of possible permalinks newClassName?: string; // class name to add to links that don't have a matching permalink @@ -25,14 +25,23 @@ export interface FromMarkdownOptions { hrefTemplate?: (permalink: string) => string; // function to generate the href attribute of a link } +export function getImageSize(size: string) { + // eslint-disable-next-line prefer-const + let [width, height] = size.split('x'); + + if (!height) height = width; + + return { width, height }; +} + // mdas-util-from-markdown extension // https://github.com/syntax-tree/mdast-util-from-markdown#extension function fromMarkdown(opts: FromMarkdownOptions = {}) { - const pathFormat = opts.pathFormat || "raw"; + const pathFormat = opts.pathFormat || 'raw'; const permalinks = opts.permalinks || []; const wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver; - const newClassName = opts.newClassName || "new"; - const wikiLinkClassName = opts.wikiLinkClassName || "internal"; + const newClassName = opts.newClassName || 'new'; + const wikiLinkClassName = opts.wikiLinkClassName || 'internal'; const defaultHrefTemplate = (permalink: string) => permalink; const hrefTemplate = opts.hrefTemplate || defaultHrefTemplate; @@ -44,9 +53,9 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) { function enterWikiLink(token) { this.enter( { - type: "wikiLink", + type: 'wikiLink', data: { - isEmbed: token.isType === "embed", + isEmbed: token.isType === 'embed', target: null, // the target of the link, e.g. "Foo Bar#Heading" in "[[Foo Bar#Heading]]" alias: null, // the alias of the link, e.g. "Foo" in "[[Foo Bar|Foo]]" permalink: null, // TODO shouldn't this be named just "link"? @@ -80,18 +89,18 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) { } = wikiLink; // eslint-disable-next-line no-useless-escape const wikiLinkWithHeadingPattern = /^(.*?)(#.*)?$/u; - const [, path, heading = ""] = target.match(wikiLinkWithHeadingPattern); + const [, path, heading = ''] = target.match(wikiLinkWithHeadingPattern); const possibleWikiLinkPermalinks = wikiLinkResolver(path); const matchingPermalink = permalinks.find((e) => { return possibleWikiLinkPermalinks.find((p) => { - if (pathFormat === "obsidian-short") { + if (pathFormat === 'obsidian-short') { if (e === p || e.endsWith(p)) { return true; } - } else if (pathFormat === "obsidian-absolute") { - if (e === "/" + p) { + } else if (pathFormat === 'obsidian-absolute') { + if (e === '/' + p) { return true; } } else { @@ -106,20 +115,19 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) { // TODO this is ugly const link = matchingPermalink || - (pathFormat === "obsidian-absolute" - ? "/" + possibleWikiLinkPermalinks[0] + (pathFormat === 'obsidian-absolute' + ? '/' + possibleWikiLinkPermalinks[0] : possibleWikiLinkPermalinks[0]) || - ""; + ''; wikiLink.data.exists = !!matchingPermalink; wikiLink.data.permalink = link; - // remove leading # if the target is a heading on the same page - const displayName = alias || target.replace(/^#/, ""); - const headingId = heading.replace(/\s+/g, "-").toLowerCase(); + const displayName = alias || target.replace(/^#/, ''); + const headingId = heading.replace(/\s+/g, '-').toLowerCase(); let classNames = wikiLinkClassName; if (!matchingPermalink) { - classNames += " " + newClassName; + classNames += ' ' + newClassName; } if (isEmbed) { @@ -127,44 +135,55 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) { if (!isSupportedFormat) { // Temporarily render note transclusion as a regular wiki link if (!format) { - wikiLink.data.hName = "a"; + wikiLink.data.hName = 'a'; wikiLink.data.hProperties = { - className: classNames + " " + "transclusion", + className: classNames + ' ' + 'transclusion', href: hrefTemplate(link) + headingId, }; - wikiLink.data.hChildren = [{ type: "text", value: displayName }]; - + wikiLink.data.hChildren = [{ type: 'text', value: displayName }]; } else { - wikiLink.data.hName = "p"; + wikiLink.data.hName = 'p'; wikiLink.data.hChildren = [ { - type: "text", + type: 'text', value: `![[${target}]]`, }, ]; } - } else if (format === "pdf") { - wikiLink.data.hName = "iframe"; + } else if (format === 'pdf') { + wikiLink.data.hName = 'iframe'; wikiLink.data.hProperties = { className: classNames, - width: "100%", + width: '100%', src: `${hrefTemplate(link)}#toolbar=0`, }; } else { - wikiLink.data.hName = "img"; - wikiLink.data.hProperties = { - className: classNames, - src: hrefTemplate(link), - alt: displayName, - }; + const hasDimensions = alias && /^\d+(x\d+)?$/.test(alias); + // Take the target as alt text except if alt name was provided [[target|alt text]] + const altText = hasDimensions || !alias ? target : alias; + + wikiLink.data.hName = 'img'; + wikiLink.data.hProperties = { + className: classNames, + src: hrefTemplate(link), + alt: altText + }; + + if (hasDimensions) { + const { width, height } = getImageSize(alias as string); + Object.assign(wikiLink.data.hProperties, { + width, + height, + }); + } } } else { - wikiLink.data.hName = "a"; + wikiLink.data.hName = 'a'; wikiLink.data.hProperties = { className: classNames, href: hrefTemplate(link) + headingId, }; - wikiLink.data.hChildren = [{ type: "text", value: displayName }]; + wikiLink.data.hChildren = [{ type: 'text', value: displayName }]; } } diff --git a/packages/remark-wiki-link/src/lib/html.ts b/packages/remark-wiki-link/src/lib/html.ts index 1058652f6..a95c0c6a8 100644 --- a/packages/remark-wiki-link/src/lib/html.ts +++ b/packages/remark-wiki-link/src/lib/html.ts @@ -1,23 +1,24 @@ -import { isSupportedFileFormat } from "./isSupportedFileFormat"; +import { getImageSize } from './fromMarkdown'; +import { isSupportedFileFormat } from './isSupportedFileFormat'; const defaultWikiLinkResolver = (target: string) => { // for [[#heading]] links if (!target) { return []; } - let permalink = target.replace(/\/index$/, ""); + let permalink = target.replace(/\/index$/, ''); // TODO what to do with [[index]] link? if (permalink.length === 0) { - permalink = "/"; + permalink = '/'; } return [permalink]; }; export interface HtmlOptions { pathFormat?: - | "raw" // default; use for regular relative or absolute paths - | "obsidian-absolute" // use for Obsidian-style absolute paths (with no leading slash) - | "obsidian-short"; // use for Obsidian-style shortened paths (shortest path possible) + | 'raw' // default; use for regular relative or absolute paths + | 'obsidian-absolute' // use for Obsidian-style absolute paths (with no leading slash) + | 'obsidian-short'; // use for Obsidian-style shortened paths (shortest path possible) permalinks?: string[]; // list of permalinks to match possible permalinks of a wiki link against wikiLinkResolver?: (name: string) => string[]; // function to resolve wiki links to an array of possible permalinks newClassName?: string; // class name to add to links that don't have a matching permalink @@ -28,11 +29,11 @@ export interface HtmlOptions { // Micromark HtmlExtension // https://github.com/micromark/micromark#htmlextension function html(opts: HtmlOptions = {}) { - const pathFormat = opts.pathFormat || "raw"; + const pathFormat = opts.pathFormat || 'raw'; const permalinks = opts.permalinks || []; const wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver; - const newClassName = opts.newClassName || "new"; - const wikiLinkClassName = opts.wikiLinkClassName || "internal"; + const newClassName = opts.newClassName || 'new'; + const wikiLinkClassName = opts.wikiLinkClassName || 'internal'; const defaultHrefTemplate = (permalink: string) => permalink; const hrefTemplate = opts.hrefTemplate || defaultHrefTemplate; @@ -41,21 +42,21 @@ function html(opts: HtmlOptions = {}) { } function enterWikiLink() { - let stack = this.getData("wikiLinkStack"); - if (!stack) this.setData("wikiLinkStack", (stack = [])); + let stack = this.getData('wikiLinkStack'); + if (!stack) this.setData('wikiLinkStack', (stack = [])); stack.push({}); } function exitWikiLinkTarget(token) { const target = this.sliceSerialize(token); - const current = top(this.getData("wikiLinkStack")); + const current = top(this.getData('wikiLinkStack')); current.target = target; } function exitWikiLinkAlias(token) { const alias = this.sliceSerialize(token); - const current = top(this.getData("wikiLinkStack")); + const current = top(this.getData('wikiLinkStack')); current.alias = alias; } @@ -111,7 +112,9 @@ function html(opts: HtmlOptions = {}) { // Temporarily render note transclusion as a regular wiki link if (!format) { this.tag( - `` + `` ); this.raw(displayName); this.tag(""); @@ -125,11 +128,18 @@ function html(opts: HtmlOptions = {}) { )}#toolbar=0" class="${classNames}" />` ); } else { - this.tag( - `${displayName}` - ); + const hasDimensions = alias && /^\d+(x\d+)?$/.test(alias); + // Take the target as alt text except if alt name was provided [[target|alt text]] + const altText = hasDimensions || !alias ? target : alias; + let imgAttributes = `src="${hrefTemplate( + link + )}" alt="${altText}" class="${classNames}"`; + + if (hasDimensions) { + const { width, height } = getImageSize(alias as string); + imgAttributes += ` width="${width}" height="${height}"`; + } + this.tag(``); } } else { this.tag( diff --git a/packages/remark-wiki-link/test/micromarkExtensionWikiLink.spec.ts b/packages/remark-wiki-link/test/micromarkExtensionWikiLink.spec.ts index 663b885c1..5f2f9e8e7 100644 --- a/packages/remark-wiki-link/test/micromarkExtensionWikiLink.spec.ts +++ b/packages/remark-wiki-link/test/micromarkExtensionWikiLink.spec.ts @@ -48,7 +48,7 @@ describe("micromark-extension-wiki-link", () => { html({ permalinks: ["/some/folder/Wiki Link"], pathFormat: "obsidian-short", - }) as any // TODO type fix + }) as any, // TODO type fix ], }); expect(serialized).toBe( @@ -75,7 +75,7 @@ describe("micromark-extension-wiki-link", () => { html({ permalinks: ["/some/folder/Wiki Link"], pathFormat: "obsidian-absolute", - }) as any // TODO type fix + }) as any, // TODO type fix ], }); expect(serialized).toBe( @@ -97,10 +97,14 @@ describe("micromark-extension-wiki-link", () => { }); test("parses a wiki link with heading and alias", () => { - const serialized = micromark("[[Wiki Link#Some Heading|Alias]]", "ascii", { - extensions: [syntax()], - htmlExtensions: [html() as any], // TODO type fix - }); + const serialized = micromark( + "[[Wiki Link#Some Heading|Alias]]", + "ascii", + { + extensions: [syntax()], + htmlExtensions: [html() as any], // TODO type fix + } + ); // note: lowercased and hyphenated heading expect(serialized).toBe( '

Alias

' @@ -134,7 +138,7 @@ describe("micromark-extension-wiki-link", () => { extensions: [syntax()], htmlExtensions: [html() as any], // TODO type fix }); - expect(serialized).toBe("

![[My Image.xyz]]

"); + expect(serialized).toBe('

![[My Image.xyz]]

'); }); test("parses and image ambed with a matching permalink", () => { @@ -147,6 +151,28 @@ describe("micromark-extension-wiki-link", () => { ); }); + // TODO: Fix alt attribute + test("Can identify the dimensions of the image if exists", () => { + const serialized = micromark("![[My Image.jpg|200]]", "ascii", { + extensions: [syntax()], + htmlExtensions: [html({ permalinks: ["My Image.jpg"] }) as any], // TODO type fix + }); + expect(serialized).toBe( + '

My Image.jpg

' + ); + }); + + // TODO: Fix alt attribute + test("Can identify the dimensions of the image if exists", () => { + const serialized = micromark("![[My Image.jpg|200x200]]", "ascii", { + extensions: [syntax()], + htmlExtensions: [html({ permalinks: ["My Image.jpg"] }) as any], // TODO type fix + }); + expect(serialized).toBe( + '

My Image.jpg

' + ); + }); + test("parses an image embed with a matching permalink and Obsidian-style shortedned path", () => { const serialized = micromark("![[My Image.jpg]]", { extensions: [syntax()], @@ -154,7 +180,7 @@ describe("micromark-extension-wiki-link", () => { html({ permalinks: ["/assets/My Image.jpg"], pathFormat: "obsidian-short", - }) as any // TODO type fix + }) as any, // TODO type fix ], }); expect(serialized).toBe( @@ -189,7 +215,7 @@ describe("micromark-extension-wiki-link", () => { extensions: [syntax()], htmlExtensions: [html() as any], // TODO type fix }); - expect(serialized).toBe("

[[Wiki Link

"); + expect(serialized).toBe('

[[Wiki Link

'); }); test("doesn't parse a wiki link with one missing closing bracket", () => { @@ -197,7 +223,7 @@ describe("micromark-extension-wiki-link", () => { extensions: [syntax()], htmlExtensions: [html() as any], // TODO type fix }); - expect(serialized).toBe("

[[Wiki Link]

"); + expect(serialized).toBe('

[[Wiki Link]

'); }); test("doesn't parse a wiki link with a missing opening bracket", () => { @@ -205,7 +231,7 @@ describe("micromark-extension-wiki-link", () => { extensions: [syntax()], htmlExtensions: [html() as any], // TODO type fix }); - expect(serialized).toBe("

[Wiki Link]]

"); + expect(serialized).toBe('

[Wiki Link]]

'); }); test("doesn't parse a wiki link in single brackets", () => { @@ -213,7 +239,7 @@ describe("micromark-extension-wiki-link", () => { extensions: [syntax()], htmlExtensions: [html() as any], // TODO type fix }); - expect(serialized).toBe("

[Wiki Link]

"); + expect(serialized).toBe('

[Wiki Link]

'); }); }); @@ -225,7 +251,7 @@ describe("micromark-extension-wiki-link", () => { html({ newClassName: "test-new", wikiLinkClassName: "test-wiki-link", - }) as any // TODO type fix + }) as any, // TODO type fix ], }); expect(serialized).toBe( @@ -251,7 +277,7 @@ describe("micromark-extension-wiki-link", () => { wikiLinkResolver: (page) => [ page.replace(/\s+/, "-").toLowerCase(), ], - }) as any // TODO type fix + }) as any, // TODO type fix ], }); expect(serialized).toBe( @@ -330,5 +356,5 @@ describe("micromark-extension-wiki-link", () => { }); expect(serialized).toBe(`

li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#LI NK-W(i)th-àcèô íã_a(n)d_uNdErlinE!:ª%@'*º$ °~./\\

`); }); - }) + }); }); diff --git a/packages/remark-wiki-link/test/remarkWikiLink.spec.ts b/packages/remark-wiki-link/test/remarkWikiLink.spec.ts index 276038fc6..051cc42c3 100644 --- a/packages/remark-wiki-link/test/remarkWikiLink.spec.ts +++ b/packages/remark-wiki-link/test/remarkWikiLink.spec.ts @@ -246,6 +246,28 @@ describe("remark-wiki-link", () => { expect(node.data?.hName).toEqual("img"); expect((node.data?.hProperties as any).src).toEqual("My Image.png"); expect((node.data?.hProperties as any).alt).toEqual("My Image.png"); + expect((node.data?.hProperties as any).width).toBeUndefined(); + expect((node.data?.hProperties as any).height).toBeUndefined(); + }); + }); + + test("Can identify the dimensions of the image if exists", () => { + const processor = unified().use(markdown).use(wikiLinkPlugin); + + let ast = processor.parse("![[My Image.png|132x612]]"); + ast = processor.runSync(ast); + + expect(select("wikiLink", ast)).not.toEqual(null); + + visit(ast, "wikiLink", (node: Node) => { + expect(node.data?.isEmbed).toEqual(true); + expect(node.data?.target).toEqual("My Image.png"); + expect(node.data?.permalink).toEqual("My Image.png"); + expect(node.data?.hName).toEqual("img"); + expect((node.data?.hProperties as any).src).toEqual("My Image.png"); + expect((node.data?.hProperties as any).alt).toEqual("My Image.png"); + expect((node.data?.hProperties as any).width).toBe("132"); + expect((node.data?.hProperties as any).height).toBe("612"); }); }); @@ -365,13 +387,17 @@ describe("remark-wiki-link", () => { test("parses a link with special characters and symbols", () => { const processor = unified().use(markdown).use(wikiLinkPlugin); - let ast = processor.parse("[[li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô íã_a(n)D_UNDERLINE!:ª%@'*º$ °~./\\]]"); + let ast = processor.parse( + "[[li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô íã_a(n)D_UNDERLINE!:ª%@'*º$ °~./\\]]" + ); ast = processor.runSync(ast); expect(select("wikiLink", ast)).not.toEqual(null); visit(ast, "wikiLink", (node: Node) => { expect(node.data?.exists).toEqual(false); - expect(node.data?.permalink).toEqual("li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\"); + expect(node.data?.permalink).toEqual( + "li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\" + ); expect(node.data?.alias).toEqual(null); expect(node.data?.hName).toEqual("a"); expect((node.data?.hProperties as any).className).toEqual( @@ -383,9 +409,9 @@ describe("remark-wiki-link", () => { expect((node.data?.hChildren as any)[0].value).toEqual( "li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô íã_a(n)D_UNDERLINE!:ª%@'*º$ °~./\\" ); - }) + }); }); - }) + }); describe("invalid wiki links", () => { test("doesn't parse a wiki link with two missing closing brackets", () => { @@ -586,4 +612,3 @@ describe("remark-wiki-link", () => { }); }); }); -