function generateSite(inputDir, outputDir, deleteOutput, filter) {
    if (deleteOutput) {
        deleteDirectory(outputDir);
    }
    if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir);

    const files = fs.readdirSync(inputDir);
    files.forEach((file) => {
        const filePath = path.join(inputDir, file);
        const isDir = fs.statSync(filePath).isDirectory();
        if (file.startsWith("_")) return;

        if (isDir) {
            if (filter(filePath, true, null)) return;
            if (path.resolve(filePath) == path.resolve(outputDir)) return;
            const subOutputDir = path.join(outputDir, file);
            fs.mkdirSync(subOutputDir, { recursive: true });
            generateSite(filePath, subOutputDir, deleteOutput, filter);
        } else {
            const inputFile = filePath;
            const outputFile = path.join(outputDir, file);
            processFile(inputFile, outputFile, filter);
        }
    });
}
@param {string} inputDir path to the input directory, traversed recursively, outputDir and files/directories starting with _ are automatically skipped
@param {string} outputDir path to the output directory
@param {boolean} deleteOutput whether the contents of output directory should be deleted first
@param {function(string, boolean, string):boolean} filter takes path, whether it is a directory and data (or null if directory), returns true if the file should be left out