diff --git a/media/d952123032e51b0f.svg b/media/d952123032e51b0f.svg deleted file mode 100644 index 6ba5bc6..0000000 --- a/media/d952123032e51b0f.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/package.json b/package.json index 83ba6ef..50aa5dd 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "build": "./bin/til --build", "install": "mkdir -p \"$HOME/.local/bin\"; ln -sf \"$PWD/bin/til\" \"$HOME/.local/bin/til\"" }, + "prettier": { + "semi": false + }, "dependencies": { "context-eval": "^0.1.0", "hyperjsx": "^1.0.12", diff --git a/src/til.mjs b/src/til.mjs index aaefd40..a44ab38 100644 --- a/src/til.mjs +++ b/src/til.mjs @@ -14,6 +14,7 @@ import { exec, fileExists, spin, + confirm, } from './util.mjs' export default async function til(args) { @@ -38,7 +39,8 @@ Other commands: \x1B[1mtil --help\x1B[0m this lovely message right here \x1B[1mtil --build\x1B[0m builds all documents to HTML files for distribution - \x1B[1mtil --sync\x1B[0m ensures the local environment has all known changes + \x1B[1mtil --sync\x1B[0m ensures the local environment has all known changes + \x1B[1mtil --gc\x1B[0m removes unused files Examples: @@ -70,6 +72,11 @@ open all in fzf.`) case '--sync': await syncRepo() return + + case 'gc': + case '--gc': + await garbageCollect() + return } // Almost certainly a mistake @@ -100,7 +107,7 @@ open all in fzf.`) const title = args.join(' ') // Make sure the repo is up to date before making any changes. - await syncRepo() + // await syncRepo() // Either coerce the promptname to a filename, or show a fzf. let filename = title @@ -180,30 +187,7 @@ open all in fzf.`) await exec(`mkdir -p ${ENTRIES_PATH}`) // Get all existing tags - const allTags = await spin( - 'Tagging', - async () => - new Set( - ( - await Promise.all( - ( - await fs.readdir(ENTRIES_PATH) - ).map(async filename => - yamlTags( - yaml.parse( - ( - await fs.readFile( - path.resolve(ENTRIES_PATH, filename), - 'utf8' - ) - ).match(/---\n(.+?)\n---\n/s)?.[1] || '' - )?.tags - ) - ) - ) - ).flat() - ) - ) + const allTags = await spin('Tagging', getAllTags) // Write a template to a tmp file and fill it into the editor buffer. // This way you can quit without saving and not alter the repo state. @@ -258,6 +242,9 @@ open all in fzf.`) break } + // Clean up any unreferenced files + await garbageCollect() + // Update the repo await spin('Publish', async () => { // Ensure the file is named correctly after editing @@ -358,6 +345,47 @@ async function syncRepo() { }) } +async function garbageCollect() { + const mediaRefs = new Set( + (await getAllEntries()).flatMap(({ contents }) => + contents.match(/[0-9a-f]{8,32}\.[a-z]+/g) || [] + ) + ) + + const medias = new Set(await fs.readdir(MEDIA_PATH)) + + // Remove all medias which have a reference leaving only unreferenced media + // to remain. + for (const ref of mediaRefs) { + medias.delete(ref) + } + + for (const filename of medias) { + if (await confirm(`Remove unused media ${filename}?`)) { + await fs.unlink(path.resolve(MEDIA_PATH, filename)) + } + } +} + +async function getAllEntries() { + return await Promise.all( + (await fs.readdir(ENTRIES_PATH)).map(async filename => ({ + filename, + contents: await fs.readFile(path.resolve(ENTRIES_PATH, filename), 'utf8') + })) + ) +} + +async function getAllTags() { + return new Set( + (await getAllEntries()).flatMap(({ contents }) => + yamlTags( + yaml.parse(contents.match(/---\n(.+?)\n---\n/s)?.[1] || '')?.tags + ) + ) + ) +} + function sanitizeFilename(name) { return name .toLowerCase() diff --git a/src/util.mjs b/src/util.mjs index 4ae8449..e2dbb2a 100644 --- a/src/util.mjs +++ b/src/util.mjs @@ -1,6 +1,7 @@ import * as child_process from 'child_process' import * as fs from 'fs/promises' import * as path from 'path' +import * as readline from 'readline/promises' import * as url from 'url' import * as util from 'util' import { DateTime } from 'luxon' @@ -52,7 +53,7 @@ export const fileExists = exists(stats => stats.isFile()) export const directoryExists = exists(stats => stats.isDirectory()) // Show a spinner while running an async `doing` function. -export const spin = async (name, doing) => { +export async function spin(name, doing) { if (!doing) [name, doing] = [doing, name] let frame = 0 const SPINNER = [ @@ -82,6 +83,17 @@ export const spin = async (name, doing) => { } } +// Ask for confirmation before continuing +export async function confirm(query) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + const response = (await rl.question(query + ' (Y/n): ')).trim().toLowerCase() + rl.close() + return response === 'y' || response === '' +} + function handleInterrupt(event, code) { process.exit(code) }