Everything between these two comments will be replaced
Last active
September 22, 2023 12:27
-
-
Save ebullient/9574764676b96530659e4f65945d3392 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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`. | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment