Integrate flowershow packages (#923)

* [packages][m]: mv @flowershow/core package here

* [packages/core][xs]: rename to @portaljs/core

* [package.json][xs]: setup npm workspaces

* [packages/core][xs]:replace deprecated rollup executor

* [core/package.json][s]: fix mermaid versions

* [core/tsconfig][xs]: rm extends

* [core/jest.config][xs]: rm coverageDirectory

* [core/package.json][xs]: install core-js

* [packages.json][s]:use same version for all nrwl packages

* [core/.eslintrc][xs]: adjust ignorePatterns

* [core/project.json][xs]: rm publish targets

* [packages][m]: mv @flowershow/remark-wiki-link here

* [packages][m]: mv @flowershow/remark-wiki-link here

* [packages][m]: mv @flowershow/remark-embed here

* [remark-callouts/project.json][xs]: adjst test pattern

* [package.json][s]: install missing deps

* [remark-callouts][xs]: adjst fields in package.json

* [remark-callouts][s]: rm pubish targets and adjst build executor

* [remark-embed/jest.config][xs]: rm unknown option coverageDirectory

* [remark-embed][xs]: rm publish targets

* [remark-embed][s]: rename to @portaljs/remark-embed

* [remark-wiki-link/eslintrc][xs]:adjst ignorePatterns

* [package.json][xs]: install missing deps

* [remark-wiki-link/test][xs]:specify format

- also temporarily force any type on htmlExtension

* [remark-wiki-link/README][xs]: replace @flowershow with @portaljs

* [remark-wiki-link][xs]:rm old changelog

* [remark-wiki-link][xs]: adjst package.json

* [remark-wiki-link/project.json][xs]: rm publish targets

* [core][s]: rm old changelog

* [core/README][xs]:correct scope name

* [remark-callouts/README][xs]: add @portaljs to pckg name

* [remark-embed/README][xs]: add @portaljs to pckg name

* [package-lock.json][xs]: refresh after rebasing on main
This commit is contained in:
Ola Rubaj
2023-06-07 12:21:00 +02:00
committed by GitHub
parent 0b8c56bcac
commit af134cac8b
139 changed files with 10264 additions and 2303 deletions

View File

@@ -0,0 +1,4 @@
{
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime"]
}

View File

@@ -0,0 +1,7 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "dist/**/*"],
"env": {
"jest": true
}
}

View File

@@ -0,0 +1,2 @@
src
test

View File

@@ -0,0 +1,132 @@
# remark-wiki-link
Parse and render wiki-style links in markdown especially Obsidian style links.
## What is this ?
Using obsidian, when we type in wiki link syntax for eg. `[[wiki_link]]` it would parse them as anchors.
## Features supported
- [x] Support `[[Internal link]]`
- [x] Support `[[Internal link|With custom text]]`
- [x] Support `[[Internal link#heading]]`
- [x] Support `[[Internal link#heading|With custom text]]`
- [x] Support `![[Document.pdf]]`
- [x] Support `![[Image.png]]`
* Supported image formats are jpg, jpeg, png, apng, webp, gif, svg, bmp, ico
* Unsupported image formats will display a raw wiki link string, e.g. `[[Image.xyz]]`.
Future support:
- [ ] Support `![[Audio.mp3]]`
- [ ] Support `![[Video.mp4]]`
- [ ] Support `![[Embed note]]`
- [ ] Support `![[Embed note#heading]]`
## Installation
```bash
npm install @portaljs/remark-wiki-link
```
## Usage
```javascript
import unified from "unified";
import markdown from "remark-parse";
import wikiLinkPlugin from "@portaljs/remark-wiki-link";
const processor = unified().use(markdown).use(wikiLinkPlugin);
```
## Configuration options
### `pathFormat`
Type: `"raw" | "obisidan-absolute" | "obsidian-short"`
Default: `"raw"`
- `"raw"`: use this option for regular relative or absolute paths (or Obsidian relative paths), e.g. `[[../some/folder/file]]` or `[[[/some/folder/file]]]`,
- `"obsidian-absolute"`: use this option for Obsidian absolute paths, i.e. paths with no leading `/`, e.g. `[[some/folder/file]]`
- `"obsidian-short"`: use this option for Obsidian shortened paths, e.g. `[[file]]` to resolve them to absolute paths. Note that apart from setting this value, you will also need to pass a list of paths to files in your content folder, and pass it as `permalinks` option. You can generate this list yourself or use our util function `getPermalinks`. See below for more info.
> [!note]
> Wiki link format in Obsidian can be configured in Settings -> Files & Links -> New link format.
### `aliasDivider`
Type: single character string
Default: `"|"`
Alias divider character used in your wiki links. E.g. `[[/some/folder/file|Alias]]`
### `permalinks`
Type: `Array<string>`
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.
### `wikiLinkResolver`
Type: `(name: string) => Array<string>`
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"` 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 `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`
Type: `string`
Default: `"new"`
Class name added to nodes created for wiki links for which no matching permalink (passed in `permalinks` option) was found.
### `wikiLinkClassName`
Type: `string`
Default: `"internal"`
Class name added to all wiki link nodes.
### `hrefTemplate`
Type: `(permalink: string) => string`
Default: `(permalink: string) => permalink`
A function that will be used to convert a matched permalink of the wiki link to `href` (or `src` for images).
### `markdownFolder` ❌ (deprecated as of version 1.1.0)
A string that points to the content folder, that will be used to resolve Obsidian shortened wiki link path format.
Instead of using this option, use e.g. `getPermalinks` util function exported from this package to generate a list of permalinks from your content folder, and pass it explicitly as `permalinks` option.
## Generating list of permalinks from content folder with `getPermalinks`
If you're using shortened path format for your Obsidian wiki links, in order to resolve them correctly to paths they point to, you need to set `option.pathFormat: "obsidian-short"` but also provide the plugin with a list of permalinks that point to files in your content folder as `option.permalinks`. You can use your own script to generate this list or use our util function `getPermalinks` like so:
```javascript {4,6,11-12}
import unified from "unified";
import markdown from "remark-parse";
import wikiLinkPlugin from "@portaljs/remark-wiki-link";
import { getPermalinks } from "@portaljs/remark-wiki-link";
const permalinks = await getPermalinks("path-to-your-content-folder");
const processor = unified().use(markdown).use(wikiLinkPlugin, {
pathFormat: "obsidian-short",
permalinks,
});
```
## Running tests
```bash
pnpm nx test remark-wiki-link
```

View File

@@ -0,0 +1,14 @@
import type { JestConfigWithTsJest } from "ts-jest";
const jestConfig: JestConfigWithTsJest = {
displayName: "remark-wiki-link",
preset: "ts-jest",
testEnvironment: "node",
transform: {
"^.+\\.[tj]s?$": "ts-jest",
},
moduleFileExtensions: ["ts", "js"],
transformIgnorePatterns: ["<rootDir>/node_modules/(?!remark-parse)"],
};
export default jestConfig;

View File

@@ -0,0 +1,43 @@
{
"name": "@portaljs/remark-wiki-link",
"version": "1.0.0",
"description": "Parse and render wiki-style links in markdown especially Obsidian style links.",
"repository": {
"type": "git",
"url": "git+https://github.com/datopian/portaljs.git",
"directory": "packages/remark-wiki-link"
},
"keywords": [
"remark",
"remark-plugin",
"markdown",
"gfm",
"obsidian"
],
"author": "Rufus Pollock",
"license": "MIT",
"bugs": {
"url": "https://github.com/datopian/portaljs/issues"
},
"homepage": "https://github.com/datopian/portaljs#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"mdast-util-to-markdown": "^1.5.0",
"mdast-util-wiki-link": "^0.0.2",
"micromark-util-symbol": "^1.0.1"
},
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"devDependencies": {
"micromark": "^3.1.0",
"remark-gfm": "^3.0.1"
}
}

View File

@@ -0,0 +1,45 @@
{
"name": "remark-wiki-link",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/remark-wiki-link/src",
"projectType": "library",
"targets": {
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["packages/remark-wiki-link/**/*.{ts,js}"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "packages/remark-wiki-link/jest.config.ts",
"passWithNoTests": true
}
},
"build": {
"executor": "@nrwl/rollup:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"entryFile": "packages/remark-wiki-link/src/index.ts",
"outputPath": "packages/remark-wiki-link/dist",
"compiler": "babel",
"tsConfig": "packages/remark-wiki-link/tsconfig.lib.json",
"project": "packages/remark-wiki-link/package.json",
"format": ["esm", "cjs"],
"external": ["mdast-util-wiki-link"],
"generateExportsField": true,
"assets": [
{
"glob": "packages/remark-wiki-link/README.md",
"input": ".",
"output": "."
}
]
}
}
},
"tags": []
}

View File

@@ -0,0 +1,3 @@
export * from "./lib/remarkWikiLink";
export { default } from "./lib/remarkWikiLink";
export { getPermalinks } from "./utils";

View File

@@ -0,0 +1,172 @@
import { isSupportedFileFormat } from "./isSupportedFileFormat";
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 (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 wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver;
const newClassName = opts.newClassName || "new";
const wikiLinkClassName = opts.wikiLinkClassName || "internal";
const defaultHrefTemplate = (permalink: string) => permalink;
const hrefTemplate = opts.hrefTemplate || defaultHrefTemplate;
function top(stack) {
return stack[stack.length - 1];
}
function enterWikiLink(token) {
this.enter(
{
type: "wikiLink",
data: {
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"?
exists: null, // TODO is this even needed here?
// fields for mdast-util-to-hast (used e.g. by remark-rehype)
hName: null,
hProperties: null,
hChildren: null,
},
},
token
);
}
function exitWikiLinkTarget(token) {
const target = this.sliceSerialize(token);
const current = top(this.stack);
current.data.target = target;
}
function exitWikiLinkAlias(token) {
const alias = this.sliceSerialize(token);
const current = top(this.stack);
current.data.alias = alias;
}
function exitWikiLink(token) {
const wikiLink = this.exit(token);
const {
data: { isEmbed, target, alias },
} = wikiLink;
// eslint-disable-next-line no-useless-escape
const wikiLinkWithHeadingPattern = /([\w\s\/\.-]*)(#.*)?/;
const [, path, heading = ""] = target.match(wikiLinkWithHeadingPattern);
const possibleWikiLinkPermalinks = wikiLinkResolver(path);
const matchingPermalink = permalinks.find((e) => {
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;
}
} else {
if (e === p) {
return true;
}
}
return false;
});
});
// TODO this is ugly
const link =
matchingPermalink ||
(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+/, "-").toLowerCase();
let classNames = wikiLinkClassName;
if (!matchingPermalink) {
classNames += " " + newClassName;
}
if (isEmbed) {
const [isSupportedFormat, format] = isSupportedFileFormat(target);
if (!isSupportedFormat) {
wikiLink.data.hName = "p";
wikiLink.data.hChildren = [
{
type: "text",
value: `![[${target}]]`,
},
];
} else if (format === "pdf") {
wikiLink.data.hName = "iframe";
wikiLink.data.hProperties = {
className: classNames,
width: "100%",
src: `${hrefTemplate(link)}#toolbar=0`,
};
} else {
wikiLink.data.hName = "img";
wikiLink.data.hProperties = {
className: classNames,
src: hrefTemplate(link),
alt: displayName,
};
}
} else {
wikiLink.data.hName = "a";
wikiLink.data.hProperties = {
className: classNames,
href: hrefTemplate(link) + headingId,
};
wikiLink.data.hChildren = [{ type: "text", value: displayName }];
}
}
return {
enter: {
wikiLink: enterWikiLink,
},
exit: {
wikiLinkTarget: exitWikiLinkTarget,
wikiLinkAlias: exitWikiLinkAlias,
wikiLink: exitWikiLink,
},
};
}
export { fromMarkdown };

View File

@@ -0,0 +1,146 @@
import { isSupportedFileFormat } from "./isSupportedFileFormat";
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 (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 wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver;
const newClassName = opts.newClassName || "new";
const wikiLinkClassName = opts.wikiLinkClassName || "internal";
const defaultHrefTemplate = (permalink: string) => permalink;
const hrefTemplate = opts.hrefTemplate || defaultHrefTemplate;
function top(stack) {
return stack[stack.length - 1];
}
function enterWikiLink() {
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"));
current.target = target;
}
function exitWikiLinkAlias(token) {
const alias = this.sliceSerialize(token);
const current = top(this.getData("wikiLinkStack"));
current.alias = alias;
}
function exitWikiLink(token) {
const wikiLink = this.getData("wikiLinkStack").pop();
const { target, alias } = wikiLink;
const isEmbed = token.isType === "embed";
// eslint-disable-next-line no-useless-escape
const wikiLinkWithHeadingPattern = /([\w\s\/\.-]*)(#.*)?/;
const [, path, heading = ""] = target.match(wikiLinkWithHeadingPattern);
const possibleWikiLinkPermalinks = wikiLinkResolver(path);
const matchingPermalink = permalinks.find((e) => {
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;
}
} else {
if (e === p) {
return true;
}
}
return false;
});
});
// 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;
}
if (isEmbed) {
const [isSupportedFormat, format] = isSupportedFileFormat(target);
if (!isSupportedFormat) {
this.raw(`![[${target}]]`);
} else if (format === "pdf") {
this.tag(
`<iframe width="100%" src="${hrefTemplate(
link
)}#toolbar=0" class="${classNames}" />`
);
} else {
this.tag(
`<img src="${hrefTemplate(
link
)}" alt="${displayName}" class="${classNames}" />`
);
}
} else {
this.tag(
`<a href="${hrefTemplate(link + headingId)}" class="${classNames}">`
);
this.raw(displayName);
this.tag("</a>");
}
}
return {
enter: {
wikiLink: enterWikiLink,
},
exit: {
wikiLinkTarget: exitWikiLinkTarget,
wikiLinkAlias: exitWikiLinkAlias,
wikiLink: exitWikiLink,
},
};
}
export { html };

View File

@@ -0,0 +1,28 @@
// TODO why only these?
export const supportedFileFormats = [
"webp",
"jpg",
"jpeg",
"gif",
"bmp",
"svg",
"apng",
"png",
"avif",
"ico",
"pdf",
];
export const isSupportedFileFormat = (filePath: string): [boolean, string] => {
const fileExtensionPattern = /\.([0-9a-z]{1,4})$/i;
const match = filePath.match(fileExtensionPattern);
if (!match) {
return [false, null];
}
const [, extension] = match;
const isSupported = supportedFileFormats.includes(extension);
return [isSupported, extension];
};

View File

@@ -0,0 +1,42 @@
import { toMarkdown } from "mdast-util-wiki-link";
import { syntax, SyntaxOptions } from "./syntax";
import { fromMarkdown, FromMarkdownOptions } from "./fromMarkdown";
let warningIssued = false;
type RemarkWikiLinkOptions = FromMarkdownOptions & SyntaxOptions;
function remarkWikiLink(opts: RemarkWikiLinkOptions = {}) {
const data = this.data(); // this is a reference to the processor
function add(field, value) {
if (data[field]) data[field].push(value);
else data[field] = [value];
}
if (
!warningIssued &&
((this.Parser &&
this.Parser.prototype &&
this.Parser.prototype.blockTokenizers) ||
(this.Compiler &&
this.Compiler.prototype &&
this.Compiler.prototype.visitors))
) {
warningIssued = true;
console.warn(
"[remark-wiki-link] Warning: please upgrade to remark 13 to use this plugin"
);
}
// add extensions to packages used by remark-parse
// micromark extensions
add("micromarkExtensions", syntax(opts));
// mdast-util-from-markdown extensions
add("fromMarkdownExtensions", fromMarkdown(opts));
// mdast-util-to-markdown extensions
add("toMarkdownExtensions", toMarkdown(opts));
}
export default remarkWikiLink;
export { remarkWikiLink };

View File

@@ -0,0 +1,168 @@
// Adjusted copy of https://github.com/landakram/micromark-extension-wiki-link/blob/master/src/index.js
import { codes } from "micromark-util-symbol/codes.js";
export interface SyntaxOptions {
aliasDivider?: string;
}
function isEndOfLineOrFile(code: number) {
return (
code === codes.carriageReturnLineFeed ||
code === codes.carriageReturn ||
code === codes.lineFeed ||
code === codes.eof
);
}
/**
* Token types:
* - `wikiLink`:
* - `wikiLinkMarker`: The opening and closing brackets
* - `wikiLinkData`: The data between the brackets
* - `wikiLinkTarget`: The target of the link (the part before the alias divider)
* - `wikiLinkAliasMarker`: The alias divider
* - `wikiLinkAlias`: The alias of the link (the part after the alias divider)
* */
function wikiLink(opts: SyntaxOptions = {}) {
const aliasDivider = opts.aliasDivider || "|";
const aliasMarker = aliasDivider.charCodeAt(0);
const startMarker = codes.leftSquareBracket;
const embedStartMarker = codes.exclamationMark;
const endMarker = codes.rightSquareBracket;
function tokenize(effects, ok, nok) {
let data = false;
let alias = false;
let startMarkerCount = 0;
let endMarkerCount = 0;
return start;
// recognize the start of a wiki link
function start(code: number) {
if (code === startMarker) {
effects.enter("wikiLink");
effects.enter("wikiLinkMarker");
return consumeStart(code);
} else if (code === embedStartMarker) {
effects.enter("wikiLink", { isType: "embed" });
effects.enter("wikiLinkMarker", { isType: "embed" });
return consumeStart(code);
} else {
return nok(code);
}
}
function consumeStart(code: number) {
// when coursor is at the first character after the start marker `[[`
if (startMarkerCount === 2) {
effects.exit("wikiLinkMarker");
return consumeData(code);
}
if (code === startMarker || code === embedStartMarker) {
if (code === startMarker) {
startMarkerCount++;
}
effects.consume(code);
return consumeStart;
} else {
return nok(code);
}
}
function consumeData(code: number) {
if (isEndOfLineOrFile(code)) {
return nok(code);
}
effects.enter("wikiLinkData");
effects.enter("wikiLinkTarget");
return consumeTarget(code);
}
function consumeTarget(code: number) {
if (code === aliasMarker) {
if (!data) return nok(code);
effects.exit("wikiLinkTarget");
effects.enter("wikiLinkAliasMarker");
return consumeAliasMarker(code);
}
if (code === endMarker) {
if (!data) return nok(code);
effects.exit("wikiLinkTarget");
effects.exit("wikiLinkData");
effects.enter("wikiLinkMarker");
return consumeEnd(code);
}
if (isEndOfLineOrFile(code)) {
return nok(code);
}
data = true;
effects.consume(code);
return consumeTarget;
}
function consumeAliasMarker(code) {
effects.consume(code);
effects.exit("wikiLinkAliasMarker");
effects.enter("wikiLinkAlias");
return consumeAlias(code);
}
function consumeAlias(code: number) {
if (code === endMarker) {
if (!alias) return nok(code);
effects.exit("wikiLinkAlias");
effects.exit("wikiLinkData");
effects.enter("wikiLinkMarker");
return consumeEnd(code);
}
if (isEndOfLineOrFile(code)) {
return nok(code);
}
alias = true;
effects.consume(code);
return consumeAlias;
}
function consumeEnd(code: number) {
if (endMarkerCount === 2) {
effects.exit("wikiLinkMarker");
effects.exit("wikiLink");
return ok(code);
}
if (code !== endMarker) {
return nok(code);
}
effects.consume(code);
endMarkerCount++;
return consumeEnd;
}
}
const wikiLinkConstruct = { tokenize };
return {
text: {
[codes.leftSquareBracket]: wikiLinkConstruct,
[codes.exclamationMark]: wikiLinkConstruct,
},
};
}
export { wikiLink as syntax };

View File

@@ -0,0 +1,43 @@
import fs from "fs";
import path from "path";
// 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())
.map((dirent) => path.join(dir, dirent.name));
const dirs = dirents
.filter((dirent) => dirent.isDirectory())
.map((dirent) => path.join(dir, dirent.name));
for (const d of dirs) {
files.push(...recursiveGetFiles(d));
}
return files;
};
export const getPermalinks = (
markdownFolder: string,
ignorePatterns: Array<RegExp> = [],
func: (str: any, ...args: any[]) => string = defaultPathToPermalinkFunc
) => {
const files = recursiveGetFiles(markdownFolder);
const filesFiltered = files.filter((file) => {
return !ignorePatterns.some((pattern) => file.match(pattern));
});
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
};

View File

@@ -0,0 +1 @@
export { getPermalinks } from "./getPermalinks";

View File

@@ -0,0 +1,2 @@
# Document Title

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,2 @@
# Document Title

View File

@@ -0,0 +1,2 @@
# Document Title

View File

@@ -0,0 +1,2 @@
# Document Title

View File

@@ -0,0 +1,2 @@
# Document Title

View File

@@ -0,0 +1,2 @@
# Document Title

View File

@@ -0,0 +1,2 @@
# Document Title

View File

@@ -0,0 +1,62 @@
import * as path from "path";
// import * as url from "url";
import { getPermalinks } from "../src/utils";
// const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
// const markdownFolder = path.join(__dirname, "/fixtures/content");
const markdownFolder = path.join(
".",
"/packages/remark-wiki-link/test/fixtures/content"
);
describe("getPermalinks", () => {
test("should return an array of permalinks", () => {
const expectedPermalinks = [
"/", // /index.md
"/abc",
"/blog/first-post",
"/blog/Second Post",
"/blog/third-post",
"/blog", // /blog/index.md
"/blog/tutorials/first-tutorial",
"/assets/Pasted Image 123.png",
];
const permalinks = getPermalinks(markdownFolder, [/\.DS_Store/]);
expect(permalinks).toHaveLength(expectedPermalinks.length);
permalinks.forEach((permalink) => {
expect(expectedPermalinks).toContain(permalink);
});
});
test("should return an array of permalinks with custom path -> permalink converter function", () => {
const expectedPermalinks = [
"/", // /index.md
"/abc",
"/blog/first-post",
"/blog/second-post",
"/blog/third-post",
"/blog", // /blog/index.md
"/blog/tutorials/first-tutorial",
"/assets/pasted-image-123.png",
];
const func = (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
.replace(/ /g, "-") // replace spaces with hyphens
.toLowerCase(); // convert to lowercase
return permalink.length > 0 ? permalink : "/"; // for home page
};
const permalinks = getPermalinks(markdownFolder, [/\.DS_Store/], func);
expect(permalinks).toHaveLength(expectedPermalinks.length);
permalinks.forEach((permalink) => {
expect(expectedPermalinks).toContain(permalink);
});
});
});

View File

@@ -0,0 +1,23 @@
import {
isSupportedFileFormat,
supportedFileFormats,
} from "../src/lib/isSupportedFileFormat";
describe("isSupportedFileFormat", () => {
test("should return [false, null] for a path with no file extension", () => {
const filePath = "/content/some/markdown/page";
expect(isSupportedFileFormat(filePath)).toStrictEqual([false, null]);
});
test("should return [true, <extension>] for a path with supported file extension", () => {
supportedFileFormats.forEach((fileFormat) => {
const filePath = `image.${fileFormat}`;
expect(isSupportedFileFormat(filePath)).toStrictEqual([true, fileFormat]);
});
});
test("should return [false, <extension>] for a path with unsupported file extension", () => {
const filePath = "image.xyz";
expect(isSupportedFileFormat(filePath)).toStrictEqual([false, "xyz"]);
});
});

View File

@@ -0,0 +1,312 @@
import { syntax } from "../src/lib/syntax";
import { html } from "../src/lib/html";
import { micromark } from "micromark";
describe("micromark-extension-wiki-link", () => {
describe("parses a wikilink", () => {
test("with 'raw' file format (default) that has no matching permalink", () => {
const serialized = micromark("[[Wiki Link]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
// note: class="internal new"
expect(serialized).toBe(
'<p><a href="Wiki Link" class="internal new">Wiki Link</a></p>'
);
});
test("with 'raw' file format (default) that has a matching permalink", () => {
const serialized = micromark("[[Wiki Link]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html({ permalinks: ["Wiki Link"] }) as any], // TODO type fix
});
// note: class="internal"
expect(serialized).toBe(
'<p><a href="Wiki Link" class="internal">Wiki Link</a></p>'
);
});
test("with shortened Obsidian-style path that has no matching permalink", () => {
const serialized = micromark("[[Wiki Link]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [
html({
pathFormat: "obsidian-short",
}) as any // TODO type fix
],
});
// note: class="internal new"
expect(serialized).toBe(
'<p><a href="Wiki Link" class="internal new">Wiki Link</a></p>'
);
});
test("with shortened Obsidian-style path that has a matching permalink", () => {
const serialized = micromark("[[Wiki Link]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [
html({
permalinks: ["/some/folder/Wiki Link"],
pathFormat: "obsidian-short",
}) as any // TODO type fix
],
});
expect(serialized).toBe(
'<p><a href="/some/folder/Wiki Link" class="internal">Wiki Link</a></p>'
);
});
// Obsidian absolute path doesn't have a leading slash
test("with 'obsidian-absolute' path format that has no matching permalink", () => {
const serialized = micromark("[[some/folder/Wiki Link]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html({ pathFormat: "obsidian-absolute" }) as any], // TODO type fix
});
expect(serialized).toBe(
'<p><a href="/some/folder/Wiki Link" class="internal new">some/folder/Wiki Link</a></p>'
);
});
// Obsidian absolute path doesn't have a leading slash
test("with 'obsidian-absolute' path format that has a matching permalink", () => {
const serialized = micromark("[[some/folder/Wiki Link]]", {
extensions: [syntax()],
htmlExtensions: [
html({
permalinks: ["/some/folder/Wiki Link"],
pathFormat: "obsidian-absolute",
}) as any // TODO type fix
],
});
expect(serialized).toBe(
'<p><a href="/some/folder/Wiki Link" class="internal">some/folder/Wiki Link</a></p>'
);
});
});
describe("aliases and headings", () => {
test("parses a wiki link with heading", () => {
const serialized = micromark("[[Wiki Link#Some Heading]]", "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">Wiki Link#Some Heading</a></p>'
);
});
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
});
// note: lowercased and hyphenated heading
expect(serialized).toBe(
'<p><a href="Wiki Link#some-heading" class="internal new">Alias</a></p>'
);
});
test("parses a wiki link to a heading on the same page", () => {
const serialized = micromark("[[#Some Heading]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe(
'<p><a href="#some-heading" class="internal new">Some Heading</a></p>'
);
});
});
describe("image embeds", () => {
test("parses an image embed of supported file format", () => {
const serialized = micromark("![[My Image.jpg]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe(
'<p><img src="My Image.jpg" alt="My Image.jpg" class="internal new" /></p>'
);
});
test("parses an image embed of unsupported file format", () => {
const serialized = micromark("![[My Image.xyz]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe("<p>![[My Image.xyz]]</p>");
});
test("parses and image ambed with a matching permalink", () => {
const serialized = micromark("![[My Image.jpg]]", "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" /></p>'
);
});
test("parses an image embed with a matching permalink and Obsidian-style shortedned path", () => {
const serialized = micromark("![[My Image.jpg]]", {
extensions: [syntax()],
htmlExtensions: [
html({
permalinks: ["/assets/My Image.jpg"],
pathFormat: "obsidian-short",
}) as any // TODO type fix
],
});
expect(serialized).toBe(
'<p><img src="/assets/My Image.jpg" alt="My Image.jpg" class="internal" /></p>'
);
});
test("parses an image embed with an alt text", () => {
const serialized = micromark("![[My Image.jpg|My Image Alt]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe(
'<p><img src="My Image.jpg" alt="My Image Alt" class="internal new" /></p>'
);
});
test("parses a pdf embed", () => {
const serialized = micromark("![[My Document.pdf]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe(
'<p><iframe width="100%" src="My Document.pdf#toolbar=0" class="internal new" /></p>'
);
});
});
describe("invalid wiki links", () => {
test("doesn't parse a wiki link with two missing closing brackets", () => {
const serialized = micromark("[[Wiki Link", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe("<p>[[Wiki Link</p>");
});
test("doesn't parse a wiki link with one missing closing bracket", () => {
const serialized = micromark("[[Wiki Link]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe("<p>[[Wiki Link]</p>");
});
test("doesn't parse a wiki link with a missing opening bracket", () => {
const serialized = micromark("[Wiki Link]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe("<p>[Wiki Link]]</p>");
});
test("doesn't parse a wiki link in single brackets", () => {
const serialized = micromark("[Wiki Link]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe("<p>[Wiki Link]</p>");
});
});
describe("other options", () => {
test("parses a wiki link with a custom class", () => {
const serialized = micromark("[[Wiki Link]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [
html({
newClassName: "test-new",
wikiLinkClassName: "test-wiki-link",
}) as any // TODO type fix
],
});
expect(serialized).toBe(
'<p><a href="Wiki Link" class="test-wiki-link test-new">Wiki Link</a></p>'
);
});
test("parses a wiki link with a custom divider", () => {
const serialized = micromark("[[Wiki Link:Alias Name]]", "ascii", {
extensions: [syntax({ aliasDivider: ":" })],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe(
'<p><a href="Wiki Link" class="internal new">Alias Name</a></p>'
);
});
test("parses a wiki link with a custom page resolver", () => {
const serialized = micromark("[[Wiki Link]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [
html({
wikiLinkResolver: (page) => [
page.replace(/\s+/, "-").toLowerCase(),
],
}) as any // TODO type fix
],
});
expect(serialized).toBe(
'<p><a href="wiki-link" class="internal new">Wiki Link</a></p>'
);
});
});
test("parses wiki links to index files", () => {
const serialized = micromark("[[/some/folder/index]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe(
'<p><a href="/some/folder" class="internal new">/some/folder/index</a></p>'
);
});
describe("other", () => {
test("parses a wiki link to some index page in a folder with no matching permalink", () => {
const serialized = micromark("[[/some/folder/index]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe(
'<p><a href="/some/folder" class="internal new">/some/folder/index</a></p>'
);
});
test("parses a wiki link to some index page in a folder with a matching permalink", () => {
const serialized = micromark("[[/some/folder/index]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html({ permalinks: ["/some/folder"] }) as any], // TODO type fix
});
expect(serialized).toBe(
'<p><a href="/some/folder" class="internal">/some/folder/index</a></p>'
);
});
test("parses a wiki link to home index page with no matching permalink", () => {
const serialized = micromark("[[/index]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe(
'<p><a href="/" class="internal new">/index</a></p>'
);
});
test("parses a wiki link to home index page with a matching permalink", () => {
const serialized = micromark("[[/index]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html({ permalinks: ["/"] }) as any], // TODO type fix
});
expect(serialized).toBe('<p><a href="/" class="internal">/index</a></p>');
});
});
});

View File

@@ -0,0 +1,538 @@
import markdown from "remark-parse";
import { unified } from "unified";
import { select } from "unist-util-select";
import { visit } from "unist-util-visit";
import { Node } from "unist";
import wikiLinkPlugin from "../src/lib/remarkWikiLink";
describe("remark-wiki-link", () => {
describe("parses a wikilink", () => {
test("with 'raw' file format (default) that has no matching permalink", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("[[Wiki Link]]");
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("Wiki Link");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual(
"internal new"
);
expect((node.data?.hProperties as any).href).toEqual("Wiki Link");
expect((node.data?.hChildren as any)[0].value).toEqual("Wiki Link");
});
});
test("with 'raw' file format (default) that has a matching permalink", () => {
const processor = unified()
.use(markdown)
.use(wikiLinkPlugin, {
permalinks: ["Wiki Link"],
});
let ast = processor.parse("[[Wiki Link]]");
ast = processor.runSync(ast);
expect(select("wikiLink", ast)).not.toEqual(null);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(true);
expect(node.data?.permalink).toEqual("Wiki Link");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual("internal");
expect((node.data?.hProperties as any).href).toEqual("Wiki Link");
expect((node.data?.hChildren as any)[0].value).toEqual("Wiki Link");
});
});
test("with shortened Obsidian-style path that has no matching permalink", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin, {
pathFormat: "obsidian-short",
});
let ast = processor.parse("[[Wiki Link]]");
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("Wiki Link");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual(
"internal new"
);
expect((node.data?.hProperties as any).href).toEqual("Wiki Link");
expect((node.data?.hChildren as any)[0].value).toEqual("Wiki Link");
});
});
test("with shortened Obsidian-style path that has a matching permalink", () => {
const processor = unified()
.use(markdown)
.use(wikiLinkPlugin, {
permalinks: ["/some/folder/Wiki Link"],
pathFormat: "obsidian-short",
});
let ast = processor.parse("[[Wiki Link]]");
ast = processor.runSync(ast);
expect(select("wikiLink", ast)).not.toEqual(null);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(true);
expect(node.data?.permalink).toEqual("/some/folder/Wiki Link");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual("internal");
expect((node.data?.hProperties as any).href).toEqual(
"/some/folder/Wiki Link"
);
expect((node.data?.hChildren as any)[0].value).toEqual("Wiki Link");
});
});
// Obsidian absolute path doesn't have a leading slash
test("with 'obsidian-absolute' path format that has no matching permalink", () => {
const processor = unified()
.use(markdown)
.use(wikiLinkPlugin, { pathFormat: "obsidian-absolute" });
let ast = processor.parse("[[some/folder/Wiki Link]]");
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("/some/folder/Wiki Link");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual(
"internal new"
);
expect((node.data?.hProperties as any).href).toEqual(
"/some/folder/Wiki Link"
);
expect((node.data?.hChildren as any)[0].value).toEqual(
"some/folder/Wiki Link"
);
});
});
// Obsidian absolute path doesn't have a leading slash
test("with 'obsidian-absolute' path format that has a matching permalink", () => {
const processor = unified()
.use(markdown)
.use(wikiLinkPlugin, {
permalinks: ["/some/folder/Wiki Link"],
pathFormat: "obsidian-absolute",
});
let ast = processor.parse("[[some/folder/Wiki Link]]");
ast = processor.runSync(ast);
expect(select("wikiLink", ast)).not.toEqual(null);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(true);
expect(node.data?.permalink).toEqual("/some/folder/Wiki Link");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual("internal");
expect((node.data?.hProperties as any).href).toEqual(
"/some/folder/Wiki Link"
);
expect((node.data?.hChildren as any)[0].value).toEqual(
"some/folder/Wiki Link"
);
});
});
});
describe("aliases and headings", () => {
test("parses a wiki link with heading", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("[[Wiki Link#Some Heading]]");
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("Wiki Link");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual(
"internal new"
);
expect((node.data?.hProperties as any).href).toEqual(
"Wiki Link#some-heading"
);
expect((node.data?.hChildren as any)[0].value).toEqual(
"Wiki Link#Some Heading"
);
});
});
test("parses a wiki link with heading and alias", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("[[Wiki Link#Some Heading|Alias]]");
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("Wiki Link");
expect(node.data?.alias).toEqual("Alias");
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual(
"internal new"
);
expect((node.data?.hProperties as any).href).toEqual(
"Wiki Link#some-heading"
);
expect((node.data?.hChildren as any)[0].value).toEqual("Alias");
});
});
test("parses a wiki link to a heading on the same page", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("[[#Some Heading]]");
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(""); // TODO should this be null?
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual(
"internal new"
);
expect((node.data?.hProperties as any).href).toEqual("#some-heading");
expect((node.data?.hChildren as any)[0].value).toEqual("Some Heading");
});
});
});
describe("image embeds", () => {
test("parses an image embed of supported file format", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("![[My Image.png]]");
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");
});
});
test("parses an image embed of unsupported file format", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("![[My Image.xyz]]");
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.xyz");
expect(node.data?.permalink).toEqual("My Image.xyz");
expect(node.data?.hName).toEqual("p");
expect((node.data?.hChildren as any)[0].value).toEqual(
"![[My Image.xyz]]"
);
});
});
test("parses an image embed with a matching permalink", () => {
const processor = unified()
.use(markdown)
.use(wikiLinkPlugin, {
permalinks: ["Pasted Image 123.png"],
});
let ast = processor.parse("![[Pasted Image 123.png]]");
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("Pasted Image 123.png");
expect(node.data?.exists).toEqual(true);
expect(node.data?.permalink).toEqual("Pasted Image 123.png");
expect(node.data?.hName).toEqual("img");
expect((node.data?.hProperties as any).src).toEqual(
"Pasted Image 123.png"
);
expect((node.data?.hProperties as any).alt).toEqual(
"Pasted Image 123.png"
);
});
});
test("parses an image embed with a matching permalink and Obsidian-style shortedned path", () => {
const processor = unified()
.use(markdown)
.use(wikiLinkPlugin, {
pathFormat: "obsidian-short",
permalinks: ["/assets/Pasted Image 123.png"],
});
let ast = processor.parse("![[Pasted Image 123.png]]");
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("Pasted Image 123.png");
expect(node.data?.exists).toEqual(true);
expect(node.data?.permalink).toEqual("/assets/Pasted Image 123.png");
expect(node.data?.hName).toEqual("img");
expect((node.data?.hProperties as any).src).toEqual(
"/assets/Pasted Image 123.png"
);
expect((node.data?.hProperties as any).alt).toEqual(
"Pasted Image 123.png"
);
});
});
test("parses an image embed with an alt text", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("![[My Image.png|Alt Text]]");
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("Alt Text");
});
});
test("parses a pdf embed", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("![[My Document.pdf]]");
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 Document.pdf");
expect(node.data?.permalink).toEqual("My Document.pdf");
expect(node.data?.hName).toEqual("iframe");
expect((node.data?.hProperties as any).src).toEqual(
"My Document.pdf#toolbar=0"
);
});
});
});
describe("invalid wiki links", () => {
test("doesn't parse a wiki link with two missing closing brackets", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("[[Wiki Link");
ast = processor.runSync(ast);
expect(select("wikiLink", ast)).toEqual(null);
});
test("doesn't parse a wiki link with one missing closing bracket", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("[[Wiki Link]");
ast = processor.runSync(ast);
expect(select("wikiLink", ast)).toEqual(null);
});
test("doesn't parse a wiki link with a missing opening bracket", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("Wiki Link]]");
ast = processor.runSync(ast);
expect(select("wikiLink", ast)).toEqual(null);
});
test("doesn't parse a wiki link in single brackets", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("[Wiki Link]");
ast = processor.runSync(ast);
expect(select("wikiLink", ast)).toEqual(null);
});
});
test("supports different config options", () => {
const processor = unified()
.use(markdown)
.use(wikiLinkPlugin, {
aliasDivider: ":",
pathFormat: "obsidian-short",
permalinks: ["/some/folder/123/real-page"],
wikiLinkResolver: (pageName: string) => [
`123/${pageName.replace(/ /g, "-").toLowerCase()}`,
],
wikiLinkClassName: "my-wiki-link-class",
hrefTemplate: (permalink: string) => `https://my-site.com${permalink}`,
});
let ast = processor.parse("[[Real Page#Some Heading:Page Alias]]");
ast = processor.runSync(ast);
expect(select("wikiLink", ast)).not.toEqual(null);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(true);
expect(node.data?.permalink).toEqual("/some/folder/123/real-page");
expect(node.data?.alias).toEqual("Page Alias");
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual(
"my-wiki-link-class"
);
expect((node.data?.hProperties as any).href).toEqual(
"https://my-site.com/some/folder/123/real-page#some-heading"
);
expect((node.data?.hChildren as any)[0].value).toEqual("Page Alias");
});
});
test("parses wiki links to index files", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("[[/some/folder/index]]");
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("/some/folder");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual("internal new");
expect((node.data?.hProperties as any).href).toEqual("/some/folder");
expect((node.data?.hChildren as any)[0].value).toEqual(
"/some/folder/index"
);
});
});
describe("other", () => {
test("parses a wiki link to some index page in a folder with no matching permalink", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("[[/some/folder/index]]");
ast = processor.runSync(ast);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(false);
expect(node.data?.permalink).toEqual("/some/folder");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual(
"internal new"
);
expect((node.data?.hProperties as any).href).toEqual("/some/folder");
expect((node.data?.hChildren as any)[0].value).toEqual(
"/some/folder/index"
);
});
});
test("parses a wiki link to some index page in a folder with a matching permalink", () => {
const processor = unified()
.use(markdown)
.use(wikiLinkPlugin, { permalinks: ["/some/folder"] });
let ast = processor.parse("[[/some/folder/index]]");
ast = processor.runSync(ast);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(true);
expect(node.data?.permalink).toEqual("/some/folder");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual("internal");
expect((node.data?.hProperties as any).href).toEqual("/some/folder");
expect((node.data?.hChildren as any)[0].value).toEqual(
"/some/folder/index"
);
});
});
test("parses a wiki link to home index page with no matching permalink", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("[[/index]]");
ast = processor.runSync(ast);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(false);
expect(node.data?.permalink).toEqual("/");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual(
"internal new"
);
expect((node.data?.hProperties as any).href).toEqual("/");
expect((node.data?.hChildren as any)[0].value).toEqual("/index");
});
});
test("parses a wiki link to home index page with a matching permalink", () => {
const processor = unified()
.use(markdown)
.use(wikiLinkPlugin, { permalinks: ["/"] });
let ast = processor.parse("[[/index]]");
ast = processor.runSync(ast);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(true);
expect(node.data?.permalink).toEqual("/");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual("internal");
expect((node.data?.hProperties as any).href).toEqual("/");
expect((node.data?.hChildren as any)[0].value).toEqual("/index");
});
});
});
});

View File

@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"allowJs": true,
"target": "es2020",
"module": "es2020",
"moduleResolution": "node",
"esModuleInterop": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["node"],
"outDir": "dist",
"forceConsistentCasingInFileNames": true
},
"exclude": ["**/*.spec.ts"],
"include": ["**/*.ts", "**/*.js"]
}

View File

@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["node", "jest"]
},
"include": ["**/*.spec.ts", "**/*.d.ts"]
}