import { resolveAttachmentPath } from "$sb/lib/resolve.ts"; import { Client } from "../client.ts"; import { Decoration, syntaxTree } from "../deps.ts"; import { decoratorStateField, invisibleDecoration, isCursorInRange, } from "./util.ts"; export function linkPlugin(client: Client) { return decoratorStateField((state) => { const widgets: any[] = []; syntaxTree(state).iterate({ enter: ({ type, from, to }) => { if (type.name !== "Link") { return; } // Adding 2 on each side due to [[ and ]] that are outside the WikiLinkPage node if (isCursorInRange(state, [from, to])) { return; } const text = state.sliceDoc(from, to); // Links are of the form [hell](https://example.com) const [anchorPart, linkPart] = text.split("]("); // Not pretty if (anchorPart.substring(1).trim() === "") { // Empty link text, let's not do live preview (because it would make it disappear) return; } if (!linkPart) { // Invalid link return; } const cleanAnchor = anchorPart.substring(1); // cut off the initial [ let cleanLink = linkPart.substring(0, linkPart.length - 1); // cut off the final ) if (!cleanLink.includes("://")) { cleanLink = resolveAttachmentPath( client.currentPage, decodeURI(cleanLink), ); } // Hide the start [ widgets.push( invisibleDecoration.range( from, from + 1, ), ); // Wrap the link in a href widgets.push( Decoration.mark({ tagName: "a", class: "sb-link", attributes: { href: cleanLink, title: `Click to visit ${cleanLink}`, }, }).range(from + 1, from + cleanAnchor.length + 1), ); // Hide the tail end of the link widgets.push( invisibleDecoration.range( from + cleanAnchor.length + 1, to, ), ); }, }); return Decoration.set(widgets, true); }); }