From be2dbefaaef0c0962d9130b29f96e42effcb45d9 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 3 Oct 2026 16:16:14 +1000 Subject: [PATCH 1/3] Add Map category with empty geospatial files Adds .shp, .shx, .gpkg, .mbtiles and .pmtiles templates under a new Map category, exposed via AllFiles.Maps, MapPaths and MapExtensions. --- claude.md | 4 ++-- files/map/empty.gpkg | Bin 0 -> 5120 bytes files/map/empty.mbtiles | Bin 0 -> 2048 bytes files/map/empty.pmtiles | Bin 0 -> 130 bytes files/map/empty.shp | Bin 0 -> 100 bytes files/map/empty.shx | Bin 0 -> 100 bytes index/empty.gpkg | Bin 0 -> 5120 bytes index/empty.mbtiles | Bin 0 -> 2048 bytes index/empty.pmtiles | Bin 0 -> 130 bytes index/empty.shp | Bin 0 -> 100 bytes index/empty.shx | Bin 0 -> 100 bytes readme.md | 8 ++++++++ src/EmptyFiles/AllFiles.cs | 24 ++++++++++++++++++++++++ src/EmptyFiles/Category.cs | 3 ++- src/EmptyFiles/EmptyFiles.csproj | 3 +++ src/Tests/Tests.cs | 1 + src/extensions.include.md | 8 ++++++++ 17 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 files/map/empty.gpkg create mode 100644 files/map/empty.mbtiles create mode 100644 files/map/empty.pmtiles create mode 100644 files/map/empty.shp create mode 100644 files/map/empty.shx create mode 100644 index/empty.gpkg create mode 100644 index/empty.mbtiles create mode 100644 index/empty.pmtiles create mode 100644 index/empty.shp create mode 100644 index/empty.shx diff --git a/claude.md b/claude.md index 29f6f00..f057e2a 100644 --- a/claude.md +++ b/claude.md @@ -23,14 +23,14 @@ dotnet test src/Tests --configuration Release --filter "FullyQualifiedName~Tests - **src/EmptyFiles.Tool/** — CLI global tool (`emptyfile` command). Targets net10.0. - **src/Tests/** — NUnit tests with Verify snapshot testing. Targets net48 + net8.0 through net11.0. - **src/EmptyFiles.AotTests/** — AOT/trimming compatibility tests (net9.0, PublishAot). -- **files/** — The actual empty template files organized by category: `archive/`, `document/`, `image/`, `sheet/`, `slide/`, `binary/`. +- **files/** — The actual empty template files organized by category: `archive/`, `document/`, `image/`, `map/`, `sheet/`, `slide/`, `binary/`. - **buildTransitive/EmptyFiles.targets** — MSBuild targets included in the NuGet package; copies empty files to output directory on build. ## Architecture Single namespace `EmptyFiles` with four public static API classes: -- **AllFiles** — Main facade. Discovers and loads empty template files from the `files/` directory at static init. Provides file creation (`CreateFile`/`TryCreateFile`), lookup (`GetPathFor`/`TryGetPathFor`), validation (`IsEmptyFile`), and registration (`UseFile`). Files are stored in `FrozenDictionary` per `Category` enum (Archive, Document, Image, Sheet, Slide, Binary). +- **AllFiles** — Main facade. Discovers and loads empty template files from the `files/` directory at static init. Provides file creation (`CreateFile`/`TryCreateFile`), lookup (`GetPathFor`/`TryGetPathFor`), validation (`IsEmptyFile`), and registration (`UseFile`). Files are stored in `FrozenDictionary` per `Category` enum (Archive, Document, Image, Map, Sheet, Slide, Binary). - **FileExtensions** — Text file detection. Maintains a set of ~443 known text extensions and supports custom conventions via `AddTextFileConvention(Func)`. - **ContentTypes** — MIME type mapping between extensions and media types (200+ mappings). - **EmptyFile** — Data class holding `Path`, `LastWriteTime`, and `Category` for each template file. diff --git a/files/map/empty.gpkg b/files/map/empty.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..2ceddeb3e7afaf8afed0291036d805f7b006940e GIT binary patch literal 5120 zcmeHLUvJws5GS3qP74&+!-4<@yH{OcSq5`lm4W|BSu_5 z7+DcfPzqs;9uk5O+JO5u+*4YE%KDUnCq^j$rQp@W$Ld1+Vs!)Uy#DwGw50H#qJ0Cu znN${q5UQ=;+-m1A;-16K!taN$B-1*L{FpcU&Jh6-DR zjp)TWPH&aV_|;})eK%yWP&l?CZVk< z^lO83x^VYeElD(Uhcr6|RjEb}wPw4n9g)ZMNb-ZeRU_<;i}TbyS$ zzr$Sa@q{TH7TN(HgVE^}6gRPm4aRSvM7JjoQC-nHdEi@=et<=Jev^+Ys5y!Z>N<00&U{ECvzdl@*a|DUmL7-?_*ML`W$cB~tN35r(8q?0^B?1N1lMoq zC-_~V_l?c2D1e6dt>bT?;|l*>;J+C%+>YaqY$UFKiC%q@N^j+I_{VS#ucv5!36QT{ z1=LeHl8?tGVCg0B>{yXyM(5+pc$+(rI&hb>U;?Roo#vAsl`eaO?9u&tuVs*2P8wQa zWZDDE>oc+kFyCml=?uWn1oL4OxDg+*d@g%*FFU%Ib&Tx6{cQVw*8L_Yd)*#n3Ndp&W301<9$;oY%Q=G7=yr5Nt3%2_+!M2Jn}K;DvES8bQ|*ZCVUlog zXPanrpK7$zpn76v5+^1g7so$^UnBfG6f63G9{4b4pm_gN_yNMdK(V5K=7Ilr{ud{i zlk@*)gy8z`&`bAM_ts}ZDo{NwdQuZqq|u{pb&scGZS{oSAfHBsijb z?U*%-wQ4CN>4yhY>o)i1BD+eZT2v~MT&wQL<%%pTcF zq!>&vL+uE0bnO;Fr0F+O2n`~)OM!LSSg9wufWt3zy045B22LJ#7 literal 0 HcmV?d00001 diff --git a/files/map/empty.pmtiles b/files/map/empty.pmtiles new file mode 100644 index 0000000000000000000000000000000000000000..27f790ce698e61042f75f73eacc3ad370d5e6640 GIT binary patch literal 130 ucmWIW4av+&EoQD~fB;4)-2kPTpmY@yH{OcSq5`lm4W|BSu_5 z7+DcfPzqs;9uk5O+JO5u+*4YE%KDUnCq^j$rQp@W$Ld1+Vs!)Uy#DwGw50H#qJ0Cu znN${q5UQ=;+-m1A;-16K!taN$B-1*L{FpcU&Jh6-DR zjp)TWPH&aV_|;})eK%yWP&l?CZVk< z^lO83x^VYeElD(Uhcr6|RjEb}wPw4n9g)ZMNb-ZeRU_<;i}TbyS$ zzr$Sa@q{TH7TN(HgVE^}6gRPm4aRSvM7JjoQC-nHdEi@=et<=Jev^+Ys5y!Z>N<00&U{ECvzdl@*a|DUmL7-?_*ML`W$cB~tN35r(8q?0^B?1N1lMoq zC-_~V_l?c2D1e6dt>bT?;|l*>;J+C%+>YaqY$UFKiC%q@N^j+I_{VS#ucv5!36QT{ z1=LeHl8?tGVCg0B>{yXyM(5+pc$+(rI&hb>U;?Roo#vAsl`eaO?9u&tuVs*2P8wQa zWZDDE>oc+kFyCml=?uWn1oL4OxDg+*d@g%*FFU%Ib&Tx6{cQVw*8L_Yd)*#n3Ndp&W301<9$;oY%Q=G7=yr5Nt3%2_+!M2Jn}K;DvES8bQ|*ZCVUlog zXPanrpK7$zpn76v5+^1g7so$^UnBfG6f63G9{4b4pm_gN_yNMdK(V5K=7Ilr{ud{i zlk@*)gy8z`&`bAM_ts}ZDo{NwdQuZqq|u{pb&scGZS{oSAfHBsijb z?U*%-wQ4CN>4yhY>o)i1BD+eZT2v~MT&wQL<%%pTcF zq!>&vL+uE0bnO;Fr0F+O2n`~)OM!LSSg9wufWt3zy045B22LJ#7 literal 0 HcmV?d00001 diff --git a/index/empty.pmtiles b/index/empty.pmtiles new file mode 100644 index 0000000000000000000000000000000000000000..27f790ce698e61042f75f73eacc3ad370d5e6640 GIT binary patch literal 130 ucmWIW4av+&EoQD~fB;4)-2kPTpmY images; + public static FrozenDictionary Maps => maps; + + static FrozenDictionary maps; + public static FrozenDictionary Sheets => sheets; static FrozenDictionary sheets; @@ -35,6 +39,7 @@ static AllFiles() archives = AddCategory(archiveExtensions, Category.Archive); documents = AddCategory(documentExtensions, Category.Document); images = AddCategory(imageExtensions, Category.Image); + maps = AddCategory(mapExtensions, Category.Map); sheets = AddCategory(sheetExtensions, Category.Sheet); slides = AddCategory(slideExtensions, Category.Slide); binary = AddCategory(binaryExtensions, Category.Binary); @@ -42,6 +47,7 @@ static AllFiles() Append(archives); Append(documents); Append(images); + Append(maps); Append(sheets); Append(slides); Append(binary); @@ -101,6 +107,7 @@ static void ExtractAll(string directory) ExtractCategory(directory, "binary", binary); ExtractCategory(directory, "document", documents); ExtractCategory(directory, "image", images); + ExtractCategory(directory, "map", maps); ExtractCategory(directory, "sheet", sheets); ExtractCategory(directory, "slide", slides); } @@ -147,6 +154,9 @@ public static void UseFile(Category category, string file) case Category.Image: Init(ref images, ref imageExtensions); break; + case Category.Map: + Init(ref maps, ref mapExtensions); + break; case Category.Sheet: Init(ref sheets, ref sheetExtensions); break; @@ -261,6 +271,20 @@ void Init(ref FrozenDictionary emptyFiles, ref FrozenSet MapPaths => maps.Values.Select(_ => _.Path); + + public static FrozenSet MapExtensions => mapExtensions; + + static FrozenSet mapExtensions = FrozenSet.ToFrozenSet( + [ + ".gpkg", + ".mbtiles", + ".pmtiles", + ".shp", + ".shx" + ], + StringComparer.OrdinalIgnoreCase); + public static IEnumerable SheetPaths => sheets.Values.Select(_ => _.Path); public static FrozenSet SheetExtensions => sheetExtensions; diff --git a/src/EmptyFiles/Category.cs b/src/EmptyFiles/Category.cs index 5ce978b..a1ca274 100644 --- a/src/EmptyFiles/Category.cs +++ b/src/EmptyFiles/Category.cs @@ -7,5 +7,6 @@ public enum Category Image, Sheet, Slide, - Binary + Binary, + Map } \ No newline at end of file diff --git a/src/EmptyFiles/EmptyFiles.csproj b/src/EmptyFiles/EmptyFiles.csproj index c5f5253..2c0d510 100644 --- a/src/EmptyFiles/EmptyFiles.csproj +++ b/src/EmptyFiles/EmptyFiles.csproj @@ -27,6 +27,9 @@ EmptyFiles.image.%(Filename)%(Extension) + + EmptyFiles.map.%(Filename)%(Extension) + EmptyFiles.sheet.%(Filename)%(Extension) diff --git a/src/Tests/Tests.cs b/src/Tests/Tests.cs index e690a05..eac8bb7 100644 --- a/src/Tests/Tests.cs +++ b/src/Tests/Tests.cs @@ -366,6 +366,7 @@ public async Task WriteExtensions() await WriteCategory(writer, "Archive", AllFiles.Archives); await WriteCategory(writer, "Document", AllFiles.Documents); await WriteCategory(writer, "Image", AllFiles.Images); + await WriteCategory(writer, "Map", AllFiles.Maps); await WriteCategory(writer, "Sheet", AllFiles.Sheets); await WriteCategory(writer, "Slide", AllFiles.Slides); await WriteCategory(writer, "Binary", AllFiles.Binary); diff --git a/src/extensions.include.md b/src/extensions.include.md index f19582f..4a01cd1 100644 --- a/src/extensions.include.md +++ b/src/extensions.include.md @@ -53,6 +53,14 @@ * .webp (26 bytes) * .wmp (48 bytes) +### Map + + * .gpkg (5 KB) + * .mbtiles (2 KB) + * .pmtiles (130 bytes) + * .shp (100 bytes) + * .shx (100 bytes) + ### Sheet * .ods (683 bytes) From 4549091a350569540970e72440a4f0eb9d8854e5 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 3 Oct 2026 16:24:13 +1000 Subject: [PATCH 2/3] Treat .geojson, .gpx and .kml as text extensions --- src/EmptyFiles/FileExtensions.cs | 3 +++ src/Tests/ExtensionsTests.cs | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/EmptyFiles/FileExtensions.cs b/src/EmptyFiles/FileExtensions.cs index fa4e849..7d5c2a0 100644 --- a/src/EmptyFiles/FileExtensions.cs +++ b/src/EmptyFiles/FileExtensions.cs @@ -235,6 +235,7 @@ public static void AddTextFileConvention(IsTextFile convention) => ".ftn", ".gemrc", ".gemspec", + ".geojson", ".gitattributes", ".gitconfig", ".gitignore", @@ -242,6 +243,7 @@ public static void AddTextFileConvention(IsTextFile convention) => ".gitmodules", ".go", ".gpp", + ".gpx", ".gradle", ".graphql", ".groovy", @@ -295,6 +297,7 @@ public static void AddTextFileConvention(IsTextFile convention) => ".jsp", ".jspx", ".jsx", + ".kml", ".ksh", ".less", ".lhs", diff --git a/src/Tests/ExtensionsTests.cs b/src/Tests/ExtensionsTests.cs index b3fc309..acb822f 100644 --- a/src/Tests/ExtensionsTests.cs +++ b/src/Tests/ExtensionsTests.cs @@ -137,4 +137,19 @@ public async Task BicepParametersAreText(string extension) await Assert.That(FileExtensions.IsTextFile($"main.dev.{extension}")).IsTrue(); await Assert.That(FileExtensions.IsTextFile($"main.dev.{extension}".AsSpan())).IsTrue(); } + + [Test] + [Arguments("geojson")] + [Arguments("gpx")] + [Arguments("kml")] + public async Task MapFormatsAreText(string extension) + { + await Assert.That(FileExtensions.IsTextExtension(extension)).IsTrue(); + await Assert.That(FileExtensions.IsTextExtension(extension.AsSpan())).IsTrue(); + await Assert.That(FileExtensions.IsTextExtension($".{extension}")).IsTrue(); + await Assert.That(FileExtensions.IsTextExtension($".{extension}".AsSpan())).IsTrue(); + await Assert.That(FileExtensions.IsTextFile($"route.{extension}")).IsTrue(); + await Assert.That(AllFiles.TryCreateFile($"route.{extension}", useEmptyStringForTextFiles: true)).IsTrue(); + File.Delete($"route.{extension}"); + } } \ No newline at end of file From ad4f852c2f73a2e437246616dd30774548a3c593 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 3 Oct 2026 16:25:25 +1000 Subject: [PATCH 3/3] Update Tests.cs --- src/Tests/Tests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Tests/Tests.cs b/src/Tests/Tests.cs index eac8bb7..5337701 100644 --- a/src/Tests/Tests.cs +++ b/src/Tests/Tests.cs @@ -377,7 +377,11 @@ static async Task WriteCategory(StreamWriter writer, string category, IReadOnlyD await writer.WriteLineAsync(""); await writer.WriteLineAsync($"### {category}"); await writer.WriteLineAsync(""); - foreach (var file in files.OrderBy(_ => _.Key)) + // Exclude the file registered by RegisterUseFile; it is a test fixture, not a shipped template. + var shipped = files + .Where(_ => _.Value.Path != useFileTarget) + .OrderBy(_ => _.Key); + foreach (var file in shipped) { var size = Size.Suffix(new FileInfo(file.Value.Path).Length); await writer.WriteLineAsync($" * {file.Key} ({size})");