// Create sitemap.xml file from HTML files in output directory. Obviously, this // needs to go after the processor that creates the HTML files. // directory: information about the directory to process, e.g., input directory, // output directory and the processor (transformation) to use. public static void ProcessSitemap(DirectoryToProcess directory) { directory = directory ?? throw new ArgumentNullException(nameof(directory)); var myConfig = (from p in directory.Processors where p.Class == $"{typeof(Sitemap)}" && p.Method == nameof(ProcessSitemap) select p).FirstOrDefault() ?? new Processor(); if (Directory.Exists(directory.OutputPath)) { var normalizedPath = Path.GetFullPath(directory.OutputPath); var outputFileName = Path.Combine(normalizedPath, "sitemap.xml"); var sitemap = new StringBuilder(@"<?xml version='1.0' encoding='UTF-8'?> <urlset xmlns='http://www.sitemaps.org/schemas/sitemap/0.9'> "); myConfig.Wildcards.ForEach(wc => { Directory.EnumerateFiles(normalizedPath, wc, SearchOption.AllDirectories) .Except(myConfig.Exclusions.Select(e => Path.Combine(normalizedPath, e))) .OrderBy(p => p) .ToList() .ForEach(htmlFileName => { // Do the bare minimum escaping, because & the likeliest problematic // file or path name character we're going to find. sitemap.Append($"\t<url><loc>{directory.TargetURL.Replace("&", "&")}/{Path.GetFileName(htmlFileName).Replace("&", "&")}</loc></url>\n"); }); }); sitemap.Append("</urlset>"); File.WriteAllText(outputFileName, sitemap.ToString()); } }
// Recursively copy files by wildcard from input to output directory. These are done // in parallel and hence should be autonomous from each other. // directory: information about the directory to process, e.g., input directory, // output directory and the processor (transformation) to use. public static void ProcessStaticFiles(DirectoryToProcess directory) { directory = directory ?? throw new ArgumentNullException(nameof(directory)); var myConfig = (from p in directory.Processors where p.Class == $"{typeof(BasicProcessors)}" && p.Method == nameof(ProcessStaticFiles) select p).FirstOrDefault() ?? new Processor(); if (Directory.Exists(directory.InputPath)) { var normalizedPath = Path.GetFullPath(directory.InputPath); Parallel.ForEach(myConfig.Wildcards, wc => { Parallel.ForEach(Directory.EnumerateFiles(normalizedPath, wc, SearchOption.AllDirectories) .Except(myConfig.Exclusions.Select(e => Path.Combine(normalizedPath, e))), inputFileName => { var subdirectory = GetSubdirectory(normalizedPath, Path.GetDirectoryName(inputFileName)); var outputDirectory = Path.Combine(directory.OutputPath, subdirectory); Directory.CreateDirectory(outputDirectory); var outputFileName = Path.Combine(outputDirectory, Path.GetFileName(inputFileName)); // Only process file if the output file doesn't exist or exists and is older than the input file. if ((File.Exists(outputFileName) && File.GetLastWriteTimeUtc(outputFileName) < File.GetLastWriteTimeUtc(inputFileName)) || !File.Exists(outputFileName)) { File.Copy(inputFileName, outputFileName, true); } }); }); } }
// Add watermark to images. These are done in parallel and hence should be // autonomous from each other. // Note that this processes files in the output directory, and does NOT update // the original input files. // directory: information about the directory to process, e.g., input directory, // output directory and the processor (transformation) to use. public static void AddWatermark(DirectoryToProcess directory) { directory = directory ?? throw new ArgumentNullException(nameof(directory)); var myConfig = (from p in directory.Processors where p.Class == $"{typeof(ImageProcessor)}" && p.Method == nameof(AddWatermark) select p).FirstOrDefault() ?? new Processor(); var procConfig = GetConfiguration <swingor_image.ProcessorConfiguration>(myConfig.ConfigFilePath, "ProcessorConfiguration"); var font = SystemFonts.CreateFont(procConfig.Font, procConfig.FontSize); if (Directory.Exists(directory.OutputPath)) { var normalizedPath = Path.GetFullPath(directory.OutputPath); Parallel.ForEach(myConfig.Wildcards, wc => { Parallel.ForEach(Directory.EnumerateFiles(normalizedPath, wc, SearchOption.AllDirectories) .Except(myConfig.Exclusions.Select(e => Path.Combine(normalizedPath, e))), imgFileName => { using (var img = Image.Load(imgFileName)) { if (img.MetaData.ExifProfile != null && img.MetaData.ExifProfile.GetValue(ExifTag.Copyright) != null) { using (var img2 = img.Clone(ctx => ctx.ApplyScalingWaterMarkSimple(font, Rgba32.HotPink, 5, procConfig.ScalingFactor, $"© {img.MetaData.ExifProfile.GetValue(ExifTag.Copyright).Value.ToString()}"))) { img2.Save(imgFileName); }; } } }); }); } }
// Recursively add copyright info to the EXIF metadata. These are done // in parallel and hence should be autonomous from each other. // Note that this processes files in the output directory, and does NOT update // the original input files. // directory: information about the directory to process, e.g., input directory, // output directory and the processor (transformation) to use. public static void ProcessImageExifs(DirectoryToProcess directory) { directory = directory ?? throw new ArgumentNullException(nameof(directory)); var myConfig = (from p in directory.Processors where p.Class == $"{typeof(ImageProcessor)}" && p.Method == nameof(ProcessImageExifs) select p).FirstOrDefault() ?? new Processor(); if (Directory.Exists(directory.OutputPath)) { var normalizedPath = Path.GetFullPath(directory.OutputPath); Parallel.ForEach(myConfig.Wildcards, wc => { Parallel.ForEach(Directory.EnumerateFiles(normalizedPath, wc, SearchOption.AllDirectories) .Except(myConfig.Exclusions.Select(e => Path.Combine(normalizedPath, e))), imgFileName => { using (var img = Image.Load(imgFileName)) { img.MetaData.ExifProfile = img.MetaData.ExifProfile ?? new ExifProfile(); var changed = false; changed = img.AddExifTagIfMissing(ExifTag.Artist, directory.DefaultAuthor); changed = img.AddExifTagIfMissing(ExifTag.Copyright, $"{directory.DefaultAuthor} - {DateTime.Now.Year}"); if (changed) { img.Save(imgFileName); } } }); }); } }
// Process Markdown files. These are done in parallel and hence should be autonomous from // each other. // directory: information about the directory to process, e.g., input directory, // output directory and the processor (transformation) to use. public static void ProcessMarkdownFiles(DirectoryToProcess directory) { directory = directory ?? throw new ArgumentNullException(nameof(directory)); var myConfig = (from p in directory.Processors where p.Class == $"{typeof(BasicProcessors)}" && p.Method == nameof(ProcessMarkdownFiles) select p).FirstOrDefault() ?? new Processor(); if (Directory.Exists(directory.InputPath)) { var normalizedPath = Path.GetFullPath(directory.InputPath); var pipeline = new MarkdownPipelineBuilder().UseYamlFrontMatter().UseAdvancedExtensions().Build(); Parallel.ForEach(myConfig.Wildcards, wc => { Parallel.ForEach(Directory.EnumerateFiles(normalizedPath, wc, SearchOption.AllDirectories) .Except(myConfig.Exclusions.Select(e => Path.Combine(normalizedPath, e))), inputFileName => { var subdirectory = GetSubdirectory(normalizedPath, Path.GetDirectoryName(inputFileName)); var outputDirectory = Path.Combine(directory.OutputPath, subdirectory); Directory.CreateDirectory(outputDirectory); var outputFileName = Path.Combine(outputDirectory, $"{Path.GetFileNameWithoutExtension(inputFileName)}.html"); // Only process file if the output file doesn't exist or exists and is older than the input file. if ((File.Exists(outputFileName) && File.GetLastWriteTimeUtc(outputFileName) < File.GetLastWriteTimeUtc(inputFileName)) || !File.Exists(outputFileName)) { var outputText = new StringBuilder(); myConfig.Prepends.ForEach(file => outputText.Append(File.ReadAllText(Path.Combine(directory.InputPath, file)))); var fileText = File.ReadAllText(inputFileName); var meta = ParseYAML(fileText, directory.DefaultAuthor, directory.DefaultTitle); outputText.Append(Markdown.ToHtml(fileText, pipeline)); myConfig.Postpends.ForEach(file => outputText.Append(File.ReadAllText(Path.Combine(directory.InputPath, file)))); ReplaceTemplatesWithMetadata(outputText, meta); File.WriteAllText(outputFileName, outputText.ToString()); } }); }); } }
// Create rss.xml file from HTML files in output directory. This should typically // go after the processor that creates the HTML files, although it works off the // input files and just makes assumptions about the output. // directory: information about the directory to process, e.g., input directory, // output directory and the processor (transformation) to use. public static void ProcessRSSFeed(DirectoryToProcess directory) { directory = directory ?? throw new ArgumentNullException(nameof(directory)); var myConfig = (from p in directory.Processors where p.Class == $"{typeof(RSS)}" && p.Method == nameof(ProcessRSSFeed) select p).FirstOrDefault() ?? new Processor(); var stopAfter = myConfig.StopAfter; if (Directory.Exists(directory.OutputPath)) { var normalizedPath = Path.GetFullPath(directory.OutputPath); var outputFileName = Path.Combine(normalizedPath, "rss.xml"); var rss = new StringBuilder($@"<?xml version='1.0' encoding='UTF-8'?> <rss version='2.0'> <channel> <title>{directory.SiteTitle.Replace("&", "&")}</title> <description>{directory.SiteDescription.Replace("&", "&")}</description> <link>{directory.TargetURL.Replace("&", "&")}</link> <lastBuildDate>{DateTime.Now.ToString("R")}</lastBuildDate> <pubDate>{DateTime.Now.ToString("R")}</pubDate> <ttl>1800</ttl>' "); var inputDir = new DirectoryInfo(directory.InputPath); myConfig.Wildcards.ForEach(wc => { (from fi in inputDir.EnumerateFileSystemInfos(wc, SearchOption.AllDirectories) where !myConfig.Exclusions.Contains(fi.Name) select fi) .OrderByDescending(fi => fi.LastWriteTimeUtc) .Take(stopAfter.HasValue && stopAfter.Value > 0 ? stopAfter.Value : int.MaxValue) .ToList() .ForEach(fi => { using (var hasher = MD5.Create()) { // A bit of a hack - the RSS spec says the Guid should be static for any given // article, even if it changes. So, using the input file name to compute a hash and // using that for the article identifying Guid. If the file name changes, the output // file name will also change, and IMHO, then the Guid should, too. var guid = new Guid(hasher.ComputeHash(Encoding.UTF8.GetBytes(fi.Name))).ToString(); // Could use the ParseYAML method from the main program, but our needs here are // simple (for now), and a bit specialized. var lines = File.ReadAllLines(fi.FullName).ToList(); var title = lines.Where(l => l.StartsWith("title:")).FirstOrDefault(); // Look for YAML front matter for the title. If don't find it, look for first // level-1 header. Both of these are a bit "fragile." if (!string.IsNullOrEmpty(title)) { title = title.Replace("title:", "").Trim(); } else { title = lines.Where(l => l.StartsWith("# ")).FirstOrDefault(); } if (!string.IsNullOrEmpty(title)) { title = title.Replace("# ", "").Trim(); } else { // Otherwise use the base file name. title = Path.GetFileNameWithoutExtension(fi.Name); } var date = lines.Where(l => l.StartsWith("date:")).FirstOrDefault(); if (!string.IsNullOrEmpty(date)) { date = date.Replace("date:", "").Trim(); } else { date = fi.LastWriteTimeUtc.ToString("R"); } rss.Append("\t\t<item>\n") .Append($"\t\t\t<title>{title.Replace("&", "&")}</title>\n") .Append($"\t\t\t<link>{directory.TargetURL.Replace("&", "&")}/{Path.GetFileNameWithoutExtension(fi.Name).Replace("&", "&")}.html</link>\n") .Append($"\t\t\t<guid isPermaLink='false'>{guid}</guid>\n") .Append($"\t\t\t<pubDate>{date}</pubDate>\n") .Append("\t\t</item>\n"); } }); }); rss.Append("\t</channel>\n</rss>"); File.WriteAllText(outputFileName, rss.ToString()); } }