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;