Skip to content

Instantly share code, notes, and snippets.

@ebullient
Last active September 22, 2023 12:27
Show Gist options
  • Save ebullient/9574764676b96530659e4f65945d3392 to your computer and use it in GitHub Desktop.
Save ebullient/9574764676b96530659e4f65945d3392 to your computer and use it in GitHub Desktop.
# 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;
}
}

Missing things

Everything between these two comments will be replaced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment