Trilium widget to turn file paths into links, with windows path normalization. Add as "JS Frontend" note and make sure it has the #widget property.

November 12, 2025 ยท View on GitHub

/* License: MIT https://opensource.org/licenses/MIT *

  • Usage: Any file paths, except those in code blocks, will become links.
  • File paths with spaces can work by first creating a link without a space,
  • and then adding the parts after the space. */ function normalizePathToUrl(path) { if (/^[A-Z]:/i.test(path)) { return 'file:///' + path.replace(/\/g, '/'); } return 'file://' + path; }

function isInCodeBlock(item) { let element = item.parent; while (element) { if (element.is('element', 'codeBlock')) { return true; } element = element.parent; }

if (item.hasAttribute && item.hasAttribute('code')) {
    return true;
}

return false;

}

class PathLinkerWidget extends api.NoteContextAwareWidget { constructor(...args) { super(...args); this.register }

get position() { 
    // higher value means position towards the bottom/right
    return 100; 
} 

get parentWidget() { return 'center-pane'; }

async refreshWithNote(note) {
    api.getActiveContextTextEditor().then(editor => {
        if (!editor) {
            return
        }
        const path_re = /(?<=^|\s)([A-Z]:[\\/]{1,2}[^\s]+|\/[^\s]+)/g;
        const path_start_re = /^([A-Z]:[\\/]{1,2}|\/)/;

        editor.model.change(writer => {
            const root = editor.model.document.getRoot();
            const linksToUpdate = [];

            const range = editor.model.createRangeIn(root);
            for (const { item, previousPosition } of range.getWalker({ ignoreElementEnd: true })) {
                if (item.is('$textProxy')) {
                    if (isInCodeBlock(item)) {
                        continue;
                    }
                    const text = item.data;

                    if (item.hasAttribute('linkHref')) {
                        if (path_start_re.test(text)) {
                            const startPos = previousPosition;
                            const endPos = startPos.getShiftedBy(text.length);

                            linksToUpdate.push({
                                range: writer.createRange(startPos, endPos),
                                url: normalizePathToUrl(text)
                            });
                        }
                    } else {
                        let match;
                        path_re.lastIndex = 0;

                        while ((match = path_re.exec(text)) !== null) {
                            const pathText = match[1];
                            const pathStartIndex = match.index + (match[0].length - pathText.length);

                            const startPos = previousPosition.getShiftedBy(pathStartIndex);
                            const endPos = startPos.getShiftedBy(pathText.length);

                            linksToUpdate.push({
                                range: writer.createRange(startPos, endPos),
                                url: normalizePathToUrl(pathText)
                            });
                        }
                    }
                }
            }

            for (const link of linksToUpdate) {
                writer.setAttribute('linkHref', link.url, link.range);
            }
        });
    });
}
async entitiesReloadedEvent({loadResults}) {
    if (loadResults.isNoteContentReloaded(this.noteId)) {
        this.refresh();
    }
}
doRender() {
    this.$widget = $(``);
    return this.$widget;
}

}

let widget = new PathLinkerWidget(); console.log("Loaded PathLinkerWidget"); module.exports = widget;