diff --git a/.gitignore b/.gitignore index 44c4618..a7b9445 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.log -index.js -node_modules \ No newline at end of file +node_modules +*.d.ts +*.js \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..daf0d8d --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Gatsby Remark Enhanced Wikilink + +A Gatsby Remark plugin that adds support to Enhanced Wikilink Syntax (e.g., Obsidian Internal Links) for Gatsby ^4.0.0 + +- To support file linking and image embedding, use with `gatsby-remark-images`, `gatsby-remark-copy-linked-files` and `gatsby-source-filesystem` +- To be compatible with `gatsby-remark-autolink-headers`, the default implementation uses [Github Slugger](https://github.com/Flet/github-slugger) to slugify filenames and headings. This can be configured via setting a new `wikilinkToUrl` function. For more information, please see `Options` section below. + +## Supported Syntax + +- [x] Linking to MD files via `[[Internal Link#Heading | Alias]]` +- [x] Linking to other files via `[[../path/document.pdf]]` +- [x] Embed Images `![[../images/Hello.png]]` +- [ ] Embed Notes `![[Internal Notes]]` + +## Installation + +```bash +yarn add gatsby-remark-enhanced-wikilink +``` + +## Usage + +Add the plugin to your Gatsby config: + +```js +// gatsby-config.js +plugins: [ + { + resolve: "gatsby-transformer-remark", + options: { + plugins: [ + { + resolve: 'gatsby-remark-obsidian', + options: { + stripBrackets: true, + imageExtensions: ['png', 'jpg', 'jpeg'], + fileFileExtensions: ['png', 'jpg', 'jpeg', 'pdf'] + // see other options below + }, + }, + ] + } + }, +], +``` + +## Options + +```ts +type WikilinkArgs = { + fileName?: string; + heading?: string; + alias?: string; +}; + +type Options = { + stripBrackets?: boolean; + wikilinkToUrl?: (args: WikilinkArgs) => string; + wikilinkToLinkText?: (args: WikilinkArgs) => string; + imageExtensions?: Array; + linkFileExtensions?: Array; +}; +``` diff --git a/package.json b/package.json index fb2bdbc..71a7e46 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,32 @@ { - "name": "gatsby-remark-wikilink", + "name": "gatsby-remark-enhanced-wikilink", "version": "1.0.0", - "description": "Add wikilinks to Gatsby Remark", + "description": "A Gatsby Remark plugin that adds support to Enhanced Wikilink Syntax", "main": "index.js", + "types": "index.d.ts", "author": "Anakorn Kyavatanakij ", "license": "MIT", + "keywords": [ + "gatsby", + "remark", + "wikilink", + "obsidian" + ], + "files": [ + "index.js", + "index.d.ts" + ], + "scripts": { + "build": "tsc", + "watch": "tsc --watch", + "prepack": "tsc" + }, + "dependencies": { + "github-slugger": "^1.4.0", + "unist-util-visit": "^2.0.2" + }, "devDependencies": { + "@types/github-slugger": "^1.3.0", "@types/mdast": "^3.0.10", "@types/node": "^17.0.5", "typescript": "^4.5.4" @@ -13,12 +34,9 @@ "peerDependencies": { "gatsby": "^4.0.0" }, - "scripts": { - "build": "tsc", - "watch": "tsc --watch" - }, - "dependencies": { - "unist-util-visit": "^2.0.2", - "mdast-util-to-string": "^2.0.0" + "homepage": "https://github.com/anakornk/gatsby-remark-enhanced-wikilink#readme", + "repository": { + "type": "git", + "url": "https://github.com/anakornk/gatsby-remark-enhanced-wikilink.git" } } diff --git a/src/index.ts b/src/index.ts index e94d416..c31fc2b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,162 @@ import visit from 'unist-util-visit'; -import toString from 'mdast-util-to-string'; - -export = async ({ markdownAST }: any) => { - visit(markdownAST, 'heading', (node: any) => { - let { depth } = node; - // Skip if not an h1 - if (depth !== 1) return; - // Grab the innerText of the heading node - let text = toString(node); - const html = ` -

- ${text} -

- `; - node.type = 'html'; - node.children = undefined; - node.value = html; +import { Node } from 'unist'; +import GithubSlugger from 'github-slugger'; +import type { Text } from 'mdast'; + +type WikilinkArgs = { + fileName?: string; + heading?: string; + alias?: string; +}; + +type Options = { + stripBrackets?: boolean; + wikilinkToUrl?: (args: WikilinkArgs) => string; + wikilinkToLinkText?: (args: WikilinkArgs) => string; + imageExtensions?: Array; + linkFileExtensions?: Array; +}; + +const slugify = GithubSlugger.slug; + +const defaultWikilinkToUrl: Options['wikilinkToUrl'] = ({ + fileName, + heading, +}) => { + return `${slugify(fileName)}${heading && `#${slugify(heading)}`}` || '#'; +}; + +const defaultWikilinkToLinkText: Options['wikilinkToLinkText'] = ({ + fileName, + heading, + alias, +}) => { + return ( + alias || + (fileName && heading && `${fileName} > ${heading}`) || + fileName || + `> ${heading}` + ); +}; + +const wrapWithBrackets = (text: string, isEmbed: boolean = false) => { + return isEmbed ? `![[${text}]]` : `[[${text}]]`; +}; + +const transformWikilink = ( + { markdownAST }: { markdownAST: Node }, + { + stripBrackets = true, + wikilinkToUrl = defaultWikilinkToUrl, + wikilinkToLinkText = defaultWikilinkToLinkText, + imageExtensions = ['png', 'jpg', 'jpeg'], + linkFileExtensions = ['png', 'jpg', 'jpeg', 'pdf'], + }: Options +) => { + visit(markdownAST, 'text', (node, index, parent) => { + if (!parent) return; + + const { value: text } = node; + + const wikilinkRegExp = /(!?)\[\[(.+?)\]\]/g; + + const result = []; + let start = 0; + + let match = wikilinkRegExp.exec(text); + + // loop each match and transform wikilinks + while (match) { + const position = match.index; + const fullMatchedString = match[0]; + const isEmbed = match[1] === '!'; + const wikilinkText = match[2]; + + if (start !== position) { + result.push({ + type: 'text', + value: text.slice(start, position), + }); + } + + if (isEmbed) { + const imageRegExp = new RegExp( + `([^\\/]*)\\.(?:${imageExtensions.join('|')})$`, + 'i' + ); + const imageMatch = wikilinkText.match(imageRegExp); + if (imageMatch) { + result.push({ + type: 'image', + url: wikilinkText, + alt: imageMatch[1] || wikilinkText, + }); + } else { + // TODO: Support embed MD + result.push({ + type: 'text', + value: wrapWithBrackets(wikilinkText, true), + }); + } + } else { + const linkFileRegExp = new RegExp( + `\\.(?:${linkFileExtensions.join('|')})$`, + 'i' + ); + if (wikilinkText.match(linkFileRegExp)) { + result.push({ + type: 'link', + url: wikilinkText, + children: [ + { + type: 'text', + value: stripBrackets + ? wikilinkText + : wrapWithBrackets(wikilinkText), + }, + ], + }); + } else { + // get file name, heading and alias + const splitRegex = /([^#\|]*)(?:#?)([^\|]*)(?:\|?)(.*)/; // split wikilink text to fileName, heading and alias + let [_, fileName, heading, alias] = wikilinkText.match( + splitRegex + ) || [,]; + fileName = fileName?.replace(/\.md$/, '').trim(); // support filename with md ext + heading = heading?.trim(); + alias = alias?.trim(); + + const url = wikilinkToUrl({ fileName, heading, alias }); + const linkText = wikilinkToLinkText({ fileName, heading, alias }); + + result.push({ + type: 'link', + url: url, + children: [ + { + type: 'text', + value: stripBrackets ? linkText : wrapWithBrackets(linkText), + }, + ], + }); + } + } + + start = position + fullMatchedString.length; + match = wikilinkRegExp.exec(text); + } + + // if there is at least one match + if (result.length > 0) { + // add the rest of the text that hasn't been matched + if (start < text.length) { + result.push({ type: 'text', value: text.slice(start) }); + } + // add siblings + parent.children.splice(index, 1, ...result); + return index + result.length; + } }); - return markdownAST; }; + +export = transformWikilink; diff --git a/tsconfig.json b/tsconfig.json index b64ad60..f7e2840 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,8 @@ "noUnusedLocals": true, "noUnusedParameters": true, "rootDir": "src", - "outDir": "./" + "outDir": "./", + "declaration": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules/**"] diff --git a/yarn.lock b/yarn.lock index 8b21643..00d0125 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@types/github-slugger@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/github-slugger/-/github-slugger-1.3.0.tgz#16ab393b30d8ae2a111ac748a015ac05a1fc5524" + integrity sha512-J/rMZa7RqiH/rT29TEVZO4nBoDP9XJOjnbbIofg7GQKs4JIduEO3WLpte+6WeUz/TcrXKlY+bM7FYrp8yFB+3g== + "@types/mdast@^3.0.10": version "3.0.10" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" @@ -19,10 +24,10 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== -mdast-util-to-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b" - integrity sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w== +github-slugger@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.4.0.tgz#206eb96cdb22ee56fdc53a28d5a302338463444e" + integrity sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ== typescript@^4.5.4: version "4.5.4"