[remark-wiki-link][m]: Add image size adjustment in remark-wiki-link (#1084)

* [remark-wiki-link][m]: Add image size adjustment in `remark-wiki-link`

* [remark-wiki-links][m]: Add image size feature to images
This commit is contained in:
mohamed yahia 2024-04-15 18:39:27 +02:00 committed by GitHub
parent 852cf60abc
commit 57952e0817
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 166 additions and 81 deletions

View File

@ -0,0 +1,5 @@
---
'@portaljs/remark-wiki-link': minor
---
Add image resize feature

4
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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 }];
}
}

View File

@ -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(
`<a href="${hrefTemplate(link + headingId)}" class="${classNames} transclusion">`
`<a href="${hrefTemplate(
link + headingId
)}" class="${classNames} transclusion">`
);
this.raw(displayName);
this.tag("</a>");
@ -125,11 +128,18 @@ function html(opts: HtmlOptions = {}) {
)}#toolbar=0" class="${classNames}" />`
);
} else {
this.tag(
`<img src="${hrefTemplate(
link
)}" alt="${displayName}" class="${classNames}" />`
);
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(`<img ${imgAttributes} />`);
}
} else {
this.tag(

View File

@ -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(
'<p><a href="Wiki Link#some-heading" class="internal new">Alias</a></p>'
@ -134,7 +138,7 @@ describe("micromark-extension-wiki-link", () => {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe("<p>![[My Image.xyz]]</p>");
expect(serialized).toBe('<p>![[My Image.xyz]]</p>');
});
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(
'<p><img src="My Image.jpg" alt="My Image.jpg" class="internal" width="200" height="200" /></p>'
);
});
// 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(
'<p><img src="My Image.jpg" alt="My Image.jpg" class="internal" width="200" height="200" /></p>'
);
});
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("<p>[[Wiki Link</p>");
expect(serialized).toBe('<p>[[Wiki Link</p>');
});
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("<p>[[Wiki Link]</p>");
expect(serialized).toBe('<p>[[Wiki Link]</p>');
});
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("<p>[Wiki Link]]</p>");
expect(serialized).toBe('<p>[Wiki Link]]</p>');
});
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("<p>[Wiki Link]</p>");
expect(serialized).toBe('<p>[Wiki Link]</p>');
});
});
@ -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(`<p><a href="li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô-íã_a(n)d_underline!:ª%@'*º$-°~./\\" class="internal new">li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#LI NK-W(i)th-àcèô íã_a(n)d_uNdErlinE!:ª%@'*º$ °~./\\</a></p>`);
});
})
});
});

View File

@ -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", () => {
});
});
});