diff --git a/packages/remark-wiki-link/src/lib/remarkWikiLink.ts b/packages/remark-wiki-link/src/lib/remarkWikiLink.ts index 6dac7d54..7aab22de 100644 --- a/packages/remark-wiki-link/src/lib/remarkWikiLink.ts +++ b/packages/remark-wiki-link/src/lib/remarkWikiLink.ts @@ -1,42 +1,203 @@ -import { toMarkdown } from "mdast-util-wiki-link"; -import { syntax, SyntaxOptions } from "./syntax"; -import { fromMarkdown, FromMarkdownOptions } from "./fromMarkdown"; +import { isSupportedFileFormat } from './isSupportedFileFormat'; -let warningIssued = false; +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]; +}; -type RemarkWikiLinkOptions = FromMarkdownOptions & SyntaxOptions; +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 +} -function remarkWikiLink(opts: RemarkWikiLinkOptions = {}) { - const data = this.data(); // this is a reference to the processor +export function getImageSize(size: string) { + // eslint-disable-next-line prefer-const + let [width, height] = size.split('x'); - function add(field, value) { - if (data[field]) data[field].push(value); - else data[field] = [value]; + 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 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]; } - 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" + 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 ); } - // 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)); + 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 = top(this.stack) + const { + data: {isEmbed, target, alias}, + } = wikiLink; + this.exit(token); + // eslint-disable-next-line no-useless-escape + const wikiLinkWithHeadingPattern = /^(.*?)(#.*)?$/u; + 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+/g, '-').toLowerCase(); + let classNames = wikiLinkClassName; + if (!matchingPermalink) { + classNames += ' ' + newClassName; + } + + if (isEmbed) { + const [isSupportedFormat, format] = isSupportedFileFormat(target); + if (!isSupportedFormat) { + // Temporarily render note transclusion as a regular wiki link + if (!format) { + wikiLink.data.hName = 'a'; + wikiLink.data.hProperties = { + className: classNames + ' ' + 'transclusion', + href: hrefTemplate(link) + headingId, + }; + wikiLink.data.hChildren = [{ type: 'text', value: displayName }]; + } else { + 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 { + 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.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 default remarkWikiLink; -export { remarkWikiLink }; +export { fromMarkdown };