Missing things
Everything between these two comments will be replaced
# Missing files with Custom JS | |
There are two parts: missing.js and missing.md | |
- `missing.js` is a CustomJS script (placed where you would store CustomJS scripts in your vault) | |
- `missing.md` is the target file (contents between HTML comments are overwritten) | |
The path to `missing.md` must be specified in `missing.js`. | |
class Missing { | |
RENDER_MISSING = /([\s\S]*?<!--MISSING BEGIN-->)[\s\S]*?(<!--MISSING END-->[\s\S]*?)/i; | |
constructor() { | |
console.log("Creating Missing items renderer"); | |
this.targetFile = "assets/no-sync/missing.md"; | |
// add additional files to ignore here | |
this.ignoreFiles = [ | |
this.targetFile, | |
"${result.lastSession}", | |
"${result}", | |
"assets/attachments/name.jpeg" | |
]; | |
this.ignoreAnchors = [ | |
"callout", "card", "portrait", "symbol", "token", | |
"center", "gallery", "right", | |
"full-width", "tiny-left" | |
]; | |
// Obnoxious regular expression because markdown links are complicated: | |
// Matches: [link text](vaultPath "title") | |
// Negative lookahead for ]( | |
const nextLink = "(?!\\]\\()"; | |
// Link title (non-capturing optional group) | |
const linkTitle = "(?: \".+\")?"; | |
// Any sequence of characters except ]( | |
const linkText = "(?:" + nextLink + ".)*?"; | |
// Any sequence of characters except ]( or space | |
const vaultPath = "(?:" + nextLink + "[^ ])+?"; | |
// Compile the entire pattern: from the string because that (in total) was easier to read | |
this.markdownLinkPattern = new RegExp("\\!?\\[" + linkText + "\\]\\((" + vaultPath + ")" + linkTitle + "\\)", "g"); | |
} | |
pathToMdLink(path) { | |
return `[${path}](${path.replace(/ /g, '%20')})`; | |
} | |
async invoke() { | |
console.log("Finding lost things"); | |
const missing = app.vault.getAbstractFileByPath(this.targetFile); | |
if (!missing) { | |
console.log(`${this.targetFile} file not found`); | |
return; | |
} | |
// create a map of not-markdown/not-canvas files that could be referenced | |
// ignore templates and regex files | |
const fileMap = {}; | |
app.vault.getFiles() | |
.filter(x => !x.path.endsWith('.canvas')) | |
.filter(x => !x.path.endsWith('.md')) | |
.filter(x => !x.path.contains('assets/regex')) | |
.filter(x => !x.path.contains('assets/templates')) | |
.filter(x => !x.path.contains('assets/customjs')) | |
.filter(x => !this.ignoreFiles.includes(x.path)) | |
.forEach((f) => { | |
fileMap[f.path] = 1; | |
}); | |
const init = JSON.parse(JSON.stringify(fileMap)); | |
console.log("Initial fileMap:", init); | |
// Find all markdown files that are not in the ignore list | |
const files = app.vault.getFiles() | |
.filter(x => x.path.endsWith('.md')) | |
.filter(x => !this.ignoreFiles.includes(x.path)); | |
console.log("Finding lost things: reading files"); | |
const leaflet = []; | |
const anchors = []; | |
const rows = []; | |
// read all the files and extract the text | |
const promises = files.map(file => app.vault.cachedRead(file) | |
.then((txt) => this.findReferences(txt, file, leaflet, rows, anchors, fileMap))); | |
await Promise.all(promises); | |
console.log("Finding lost things: writing result"); | |
await this.renderMissing(missing, () => { | |
let result = '\n'; | |
result += '## Missing reference\n'; | |
result += this.renderTable(['Source', 'Target'], rows); | |
result += '\n'; | |
result += '## Missing heading or block reference\n'; | |
result += this.renderTable(['Source', 'Anchor', 'Target'], anchors); | |
result += '\n'; | |
result += '## Missing leaflet reference\n'; | |
result += this.renderTable(['Leaflet Source', 'Missing'], leaflet); | |
result += '\n'; | |
result += '## Unreferenced Things\n'; | |
const keys = Object.keys(fileMap).sort(); | |
keys.filter(x => fileMap[x] != 0) | |
.filter(x => !x.endsWith('.md')) | |
.filter(x => { | |
if (x.contains('excalidraw')) { | |
const file = app.metadataCache.getFirstLinkpathDest(x.replace(/\.(svg|png)/, '.md'), x); | |
// only excalidraw images where drawing is MIA | |
return file == null; | |
} | |
return true; | |
}).forEach(x => { | |
result += `- ${this.pathToMdLink(x)}\n`; | |
}); | |
return result; | |
}); | |
console.log("Finding lost things: Done! 🫶 "); | |
} | |
findReferences = (txt, file, leaflet, rows, anchors, fileMap) => { | |
const fileCache = app.metadataCache.getFileCache(file); | |
if (fileCache.embeds) { | |
fileCache.embeds.forEach((x) => this.findTarget(file, x, rows, anchors, fileMap)); | |
} | |
if (fileCache.links) { | |
fileCache.links.forEach((x) => this.findTarget(file, x, rows, anchors, fileMap)); | |
} | |
// Find links in code blocks | |
if (txt.contains('```ad-')) { | |
const lines = txt.split('\n'); | |
fileCache.sections.forEach((s) => { | |
if (s.type == 'code') { | |
const code = lines.slice(s.position.start.line, s.position.end.line + 1).join('\n'); | |
[...code.matchAll(this.markdownLinkPattern)].forEach((x) => { | |
this.findTarget(file, { link: x[1] }, rows, anchors, fileMap); | |
}); | |
} | |
}); | |
} | |
if (txt.contains('```leaflet')) { | |
// find all lines matching "image: (path to image)" and extract the image name | |
[...txt.matchAll(/image: (.*)/g)].forEach((x) => { | |
const imgName = x[1]; | |
const tgtFile = app.metadataCache.getFirstLinkpathDest(imgName, file.path); | |
if (tgtFile == null) { | |
// The image this leaflet needs is missing | |
leaflet.push([this.pathToMdLink(file.path), imgName]); | |
} else { | |
// We found the image, | |
fileMap[tgtFile.path] = 0; | |
} | |
}); | |
} | |
} | |
findTarget = async (file, x, rows, anchors, fileMap) => { | |
let target = x.link; | |
// remove title: [link text](vaultPath "title") -> [link text](vaultPath) | |
const titlePos = target.indexOf(' "'); | |
if (titlePos >= 0) { | |
target = target.substring(0, titlePos); | |
} | |
// remove anchor: [link text](vaultPath#anchor) -> [link text](vaultPath) | |
const anchorPos = x.link.indexOf('#'); | |
const anchor = (anchorPos < 0 ? '' : target.substring(anchorPos + 1).replace(/%20/g, ' ').trim()); | |
target = (anchorPos < 0 ? target : target.substring(0, anchorPos)).replace(/%20/g, ' ').trim(); | |
// ignore external links and ignored files | |
if (target.startsWith('http') | |
|| target.startsWith('mailto') | |
|| target.startsWith('view-source') | |
|| this.ignoreFiles.includes(target)) { | |
return; | |
} | |
let tgtFile = file; | |
if (target) { | |
// find the target file | |
tgtFile = app.metadataCache.getFirstLinkpathDest(target, file.path); | |
if (tgtFile == null) { | |
console.log("LOST:", target, "from", file.path); | |
rows.push([this.pathToMdLink(file.path), target]); | |
} else { | |
fileMap[tgtFile.path] = 0; | |
} | |
} | |
if (anchor && tgtFile) { | |
if (this.ignoreAnchors.includes(anchor)) { | |
return; | |
} | |
const tgtFileCache = app.metadataCache.getFileCache(tgtFile); | |
if (!tgtFileCache) { | |
console.log("MISSING:", tgtFile.path, "#", anchor, "from", file.path, tgtFileCache); | |
anchors.push([this.pathToMdLink(file.path), `--`, 'missing cache']); | |
} else if (anchor.startsWith('^')) { | |
const blockref = anchor.substring(1); | |
const tgtBlock = tgtFileCache.blocks ? tgtFileCache.blocks[blockref] : ''; | |
if (!tgtBlock) { | |
console.log("MISSING:", tgtFile.path, "#^", blockref, "from", file.path, tgtFileCache.blocks); | |
anchors.push([this.pathToMdLink(file.path), `"#${anchor}"`, `"${x.link}"`]); | |
} | |
} else { | |
const lower = anchor.toLowerCase(); | |
const tgtHeading = tgtFileCache.headings | |
? tgtFileCache.headings.find(x => lower == x.heading.toLowerCase() | |
.replace(/[?:]/g, '') | |
.replace('#', ' ')) | |
: ''; | |
if (!tgtHeading) { | |
console.log("MISSING:", tgtFile.path, "#", anchor, "from", file.path, tgtFileCache.headings); | |
anchors.push([this.pathToMdLink(file.path), `"#${anchor}"`, `"${x.link}"`]); | |
} | |
} | |
} | |
} | |
renderMissing = async (file, renderer) => { | |
await app.vault.process(file, (source) => { | |
let match = this.RENDER_MISSING.exec(source); | |
if (match) { | |
source = match[1]; | |
source += renderer(); | |
source += match[2]; | |
} | |
return source; | |
}); | |
} | |
renderTable = (headers, rows) => { | |
let result = ''; | |
result += '|'; | |
headers.forEach((h) => { | |
result += ` ${h} |`; | |
}); | |
result += '\n'; | |
result += '|'; | |
headers.forEach(() => { | |
result += ' --- |'; | |
}); | |
result += '\n'; | |
rows.forEach((r) => { | |
result += '|'; | |
r.forEach((c) => { | |
result += ` ${c} |`; | |
}); | |
result += '\n'; | |
}); | |
return result; | |
} | |
} |
Everything between these two comments will be replaced