Skip to content

Commit

Permalink
Wiki links fixes (datopian#504)
Browse files Browse the repository at this point in the history
## Changes
- don't hyphentate or lowercase src/hrefs by default
- add ignore patterns option to `getPermalinks` util function and allow passing custom paths -> permalinks converter function
- fix regex patterns
- adjust and extend tests
  • Loading branch information
olayway authored May 16, 2023
1 parent 2acab13 commit 5ef0262
Show file tree
Hide file tree
Showing 12 changed files with 395 additions and 266 deletions.
5 changes: 5 additions & 0 deletions .changeset/moody-hornets-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@flowershow/remark-wiki-link": minor
---

Don't hyphentate or lowercase src/hrefs by default. Add ignore patterns option to `getPermalinks` util function and allow passing custom paths -> permalinks converter function.
10 changes: 6 additions & 4 deletions packages/remark-wiki-link/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,16 @@ Default: `[]`

A list of permalinks you want to match your wiki link paths with. Wiki links with matched permalinks will have `node.data.exists` property set to `true`. Wiki links with no matching permalinks will also have additional class `new` set.

### `pageResolver`
### `wikiLinkResolver`

Type: `(name: string) => Array<string>`
Default: `(name: string) => name.replace(/ /g, "-").toLowerCase();` (simplified; see source code for full version)
Default: `(name: string) => name.replace(/\/index$/, "")` (simplified; see source code for full version)

A function that will take the wiki link target page (e.g. `"/some/folder/file"` for `[[/some/folder/file#Some Heading|Some Alias]]` wiki link) and return an array of possible permalinks, that should be matched against an array of permalinks passed in `permalinks` config option.
A function that will take the wiki link target page (e.g. `"/some/folder/file"` in `[[/some/folder/file#Some Heading|Some Alias]]` wiki link) and return an array of pages to which the wiki link **can** be resolved (one of them will be used, depending on wheather `pemalinks` are passed, and if match is found).

If no matching permalink is found, the first item from the array returned by this function will be used as a node's permalink.
If `permalinks` are passed, the resulting array will be matched against these permalinks to find the match. The matching pemalink will be used as node's `href` (or `src` for images).

If no matching permalink is found, the first item from the array returned by this function will be used as a node's `href` (or `src` for images). So, if you want to write a custom wiki link -> url

### `newClassName`

Expand Down
91 changes: 51 additions & 40 deletions packages/remark-wiki-link/src/lib/fromMarkdown.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import { isSupportedFileFormat } from "./isSupportedFileFormat";
import { pageResolver as defaultPageResolver } from "./pageResolver";

const defaultWikiLinkResolver = (target: string) => {
// for [[#heading]] links
if (!target) {
return [];
}
let permalink = target.replace(/\/index$/, "");
// TODO what to do with [[index]] link?
if (permalink.length === 0) {
permalink = "/";
}
return [permalink];
};

export interface FromMarkdownOptions {
pathFormat?:
| "raw" // default; use for regular relative or absolute paths
| "obsidian-absolute" // use for Obsidian-style absolute paths, i.e. with no leading slash
| "obsidian-short"; // use for Obsidian-style shortened paths
permalinks?: string[];
pageResolver?: (name: string) => string[];
newClassName?: string;
wikiLinkClassName?: string;
hrefTemplate?: (permalink: string) => string;
| "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
wikiLinkClassName?: string; // class name to add to all wiki links
hrefTemplate?: (permalink: string) => string; // function to generate the href attribute of a link
}

// 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 permalinks = opts.permalinks || [];
const pageResolver = opts.pageResolver || defaultPageResolver;
const wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver;
const newClassName = opts.newClassName || "new";
const wikiLinkClassName = opts.wikiLinkClassName || "internal";
const defaultHrefTemplate = (permalink: string) => permalink;
Expand All @@ -35,10 +47,10 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
type: "wikiLink",
data: {
isEmbed: token.isType === "embed",
target: null,
alias: null,
permalink: null,
exists: null,
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"?
exists: null, // TODO is this even needed here?
// fields for mdast-util-to-hast (used e.g. by remark-rehype)
hName: null,
hProperties: null,
Expand Down Expand Up @@ -66,46 +78,45 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
const {
data: { isEmbed, target, alias },
} = wikiLink;

const resolveShortenedPaths = pathFormat === "obsidian-short";
const prefix = pathFormat === "obsidian-absolute" ? "/" : "";
const pagePermalinks = pageResolver(target, isEmbed, prefix);

// eslint-disable-next-line no-useless-escape
const pathWithOptionalHeadingPattern = /([a-z0-9\.\/_-]*)(#.*)?/;
let targetHeading = "";
const wikiLinkWithHeadingPattern = /([\w\s\/\.-]*)(#.*)?/;
const [, path, heading = ""] = target.match(wikiLinkWithHeadingPattern);

const possibleWikiLinkPermalinks = wikiLinkResolver(path);

const matchingPermalink = permalinks.find((e) => {
return pagePermalinks.find((p) => {
const [, pagePath, heading] = p.match(pathWithOptionalHeadingPattern);
if (!pagePath.length) {
return false;
}
if (resolveShortenedPaths) {
if (e === pagePath || e.endsWith(pagePath)) {
targetHeading = heading ?? "";
return possibleWikiLinkPermalinks.find((p) => {
if (pathFormat === "obsidian-short") {
if (e === p || e.endsWith(p)) {
return true;
}
} else if (pathFormat === "obsidian-absolute") {
if (e === "/" + p) {
return true;
}
return false;
} else {
if (e === pagePath) {
targetHeading = heading ?? "";
if (e === p) {
return true;
}
return false;
}
return false;
});
});

wikiLink.data.exists = !!matchingPermalink;

const permalink = matchingPermalink || pagePermalinks[0];
// TODO this is ugly
const link =
matchingPermalink ||
(pathFormat === "obsidian-absolute"
? "/" + possibleWikiLinkPermalinks[0]
: possibleWikiLinkPermalinks[0]) ||
"";

wikiLink.data.permalink = permalink;
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+/, "-").toLowerCase();
let classNames = wikiLinkClassName;
if (!matchingPermalink) {
classNames += " " + newClassName;
Expand All @@ -126,21 +137,21 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
wikiLink.data.hProperties = {
className: classNames,
width: "100%",
src: `${hrefTemplate(permalink)}#toolbar=0`,
src: `${hrefTemplate(link)}#toolbar=0`,
};
} else {
wikiLink.data.hName = "img";
wikiLink.data.hProperties = {
className: classNames,
src: hrefTemplate(permalink),
src: hrefTemplate(link),
alt: displayName,
};
}
} else {
wikiLink.data.hName = "a";
wikiLink.data.hProperties = {
className: classNames,
href: hrefTemplate(permalink) + targetHeading,
href: hrefTemplate(link) + headingId,
};
wikiLink.data.hChildren = [{ type: "text", value: displayName }];
}
Expand Down
81 changes: 46 additions & 35 deletions packages/remark-wiki-link/src/lib/html.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
import { isSupportedFileFormat } from "./isSupportedFileFormat";
import { pageResolver as defaultPageResolver } from "./pageResolver";

const defaultWikiLinkResolver = (target: string) => {
// for [[#heading]] links
if (!target) {
return [];
}
let permalink = target.replace(/\/index$/, "");
// TODO what to do with [[index]] link?
if (permalink.length === 0) {
permalink = "/";
}
return [permalink];
};

export interface HtmlOptions {
pathFormat?:
| "raw" // default; use for regular relative or absolute paths
| "obsidian-absolute" // use for Obsidian-style absolute paths, i.e. with no leading slash
| "obsidian-short"; // use for Obsidian-style shortened paths
permalinks?: string[];
pageResolver?: (name: string) => string[];
newClassName?: string;
wikiLinkClassName?: string;
hrefTemplate?: (permalink: string) => string;
| "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
wikiLinkClassName?: string; // class name to add to all wiki links
hrefTemplate?: (permalink: string) => string; // function to generate the href attribute of a link
}

// Micromark HtmlExtension
// https://github.com/micromark/micromark#htmlextension
function html(opts: HtmlOptions = {}) {
const pathFormat = opts.pathFormat || "raw";
const permalinks = opts.permalinks || [];
const pageResolver = opts.pageResolver || defaultPageResolver;
const wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver;
const newClassName = opts.newClassName || "new";
const wikiLinkClassName = opts.wikiLinkClassName || "internal";
const defaultHrefTemplate = (permalink: string) => permalink;
Expand Down Expand Up @@ -51,42 +63,43 @@ function html(opts: HtmlOptions = {}) {
const wikiLink = this.getData("wikiLinkStack").pop();
const { target, alias } = wikiLink;
const isEmbed = token.isType === "embed";

const resolveShortenedPaths = pathFormat === "obsidian-short";
const prefix = pathFormat === "obsidian-absolute" ? "/" : "";
const pagePermalinks = pageResolver(target, isEmbed, prefix);

// eslint-disable-next-line no-useless-escape
const pathWithOptionalHeadingPattern = /([a-z0-9\.\/_-]*)(#.*)?/;
let targetHeading = "";
const wikiLinkWithHeadingPattern = /([\w\s\/\.-]*)(#.*)?/;
const [, path, heading = ""] = target.match(wikiLinkWithHeadingPattern);

const possibleWikiLinkPermalinks = wikiLinkResolver(path);

const matchingPermalink = permalinks.find((e) => {
return pagePermalinks.find((p) => {
const [, pagePath, heading] = p.match(pathWithOptionalHeadingPattern);
if (!pagePath.length) {
return false;
}
if (resolveShortenedPaths) {
if (e === pagePath || e.endsWith(pagePath)) {
targetHeading = heading ?? "";
return possibleWikiLinkPermalinks.find((p) => {
if (pathFormat === "obsidian-short") {
if (e === p || e.endsWith(p)) {
return true;
}
} else if (pathFormat === "obsidian-absolute") {
if (e === "/" + p) {
return true;
}
return false;
} else {
if (e === pagePath) {
targetHeading = heading ?? "";
if (e === p) {
return true;
}
return false;
}
return false;
});
});

const permalink = matchingPermalink || pagePermalinks[0];
// TODO this is ugly
const link =
matchingPermalink ||
(pathFormat === "obsidian-absolute"
? "/" + possibleWikiLinkPermalinks[0]
: possibleWikiLinkPermalinks[0]) ||
"";

// remove leading # if the target is a heading on the same page
const displayName = alias || target.replace(/^#/, "");

// replace spaces with dashes and lowercase headings
const headingId = heading.replace(/\s+/, "-").toLowerCase();
let classNames = wikiLinkClassName;
if (!matchingPermalink) {
classNames += " " + newClassName;
Expand All @@ -99,21 +112,19 @@ function html(opts: HtmlOptions = {}) {
} else if (format === "pdf") {
this.tag(
`<iframe width="100%" src="${hrefTemplate(
permalink
link
)}#toolbar=0" class="${classNames}" />`
);
} else {
this.tag(
`<img src="${hrefTemplate(
permalink
link
)}" alt="${displayName}" class="${classNames}" />`
);
}
} else {
this.tag(
`<a href="${hrefTemplate(
permalink + targetHeading
)}" class="${classNames}">`
`<a href="${hrefTemplate(link + headingId)}" class="${classNames}">`
);
this.raw(displayName);
this.tag("</a>");
Expand Down
19 changes: 0 additions & 19 deletions packages/remark-wiki-link/src/lib/pageResolver.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/remark-wiki-link/src/lib/remarkWikiLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ function remarkWikiLink(opts: RemarkWikiLinkOptions = {}) {
// micromark extensions
add("micromarkExtensions", syntax(opts));
// mdast-util-from-markdown extensions
add("fromMarkdownExtensions", fromMarkdown(opts)); // TODO: not sure if this is needed
add("fromMarkdownExtensions", fromMarkdown(opts));
// mdast-util-to-markdown extensions
add("toMarkdownExtensions", toMarkdown(opts)); // TODO: not sure if this is needed
add("toMarkdownExtensions", toMarkdown(opts));
}

export default remarkWikiLink;
Expand Down
35 changes: 23 additions & 12 deletions packages/remark-wiki-link/src/utils/getPermalinks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import fs from "fs";
import path from "path";

// recursive function to get all files in a directory
const recursiveGetFiles = (dir) => {
// recursively get all files in a directory
const recursiveGetFiles = (dir: string) => {
const dirents = fs.readdirSync(dir, { withFileTypes: true });
const files = dirents
.filter((dirent) => dirent.isFile())
Expand All @@ -17,16 +17,27 @@ const recursiveGetFiles = (dir) => {
return files;
};

// TODO slugify
export const getPermalinks = (markdownFolder) => {
export const getPermalinks = (
markdownFolder: string,
ignorePatterns: Array<RegExp> = [],
func: (str: any, ...args: any[]) => string = defaultPathToPermalinkFunc
) => {
const files = recursiveGetFiles(markdownFolder);
const permalinks = files.map((file) => {
const permalink = file
.replace(markdownFolder, "") // make the permalink relative to the markdown folder
.replace(/\.(mdx|md)/, "")
.replace(/\\/g, "/") // replace windows backslash with forward slash
.replace(/\/index$/, ""); // remove index from the end of the permalink
return permalink.length > 0 ? permalink : "/"; // for home page
const filesFiltered = files.filter((file) => {
return !ignorePatterns.some((pattern) => file.match(pattern));
});
return permalinks;

return filesFiltered.map((file) => func(file, markdownFolder));
};

const defaultPathToPermalinkFunc = (
filePath: string,
markdownFolder: string
) => {
const permalink = filePath
.replace(markdownFolder, "") // make the permalink relative to the markdown folder
.replace(/\.(mdx|md)/, "")
.replace(/\\/g, "/") // replace windows backslash with forward slash
.replace(/\/index$/, ""); // remove index from the end of the permalink
return permalink.length > 0 ? permalink : "/"; // for home page
};
Loading

0 comments on commit 5ef0262

Please sign in to comment.