Repository: dbrgn/tealdeer (rust)

Issue:
Custom Pages With `.md` Extension
I really like the custom pages functionality, but it's not clear to me why the custom pages use a `.page` extension when the stock pages use `.md`.

Is the reason simply that the patch files use `.patch`?

The reason I'm interested in this is that I'd like to store my custom pages in an [Obsidian](https://obsidian.md/) vault as notes. This way, I can maintain command usage information in my regular notes app and have it accessible from the command line with tealdeer. Unfortunately, Obsidian expects the files to have a `.md` extension, which complicates things.

It would be fantastic to be able to define a custom extension or use `.md` for the custom pages.
I'm not sure about how the patch files would have to be handled, however. For me, I don't see myself using patches, but it would be great to come to a solution that works for them too.

I'm happy to work on a PR for this, but before doing anything, I want to make sure I understand the decisions that were made, and what the best design would be.

Pull request:
Change custom page files to use a `.md` extension.
My rationale for this PR is in #320. Closes #320.

To summarise, the lack of a `.md` extension causes problems for external tools working with the custom page files, and my solution is to simply add the extension to the end.

e.g.
- `{name}.page.md`
- `{name}.patch.md`

This *will* be a breaking change for anyone using custom pages.

Patch:
diff --git a/RELEASING.md b/RELEASING.md
index 6eb868c..16cb4e2 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -14,6 +14,9 @@ Update version numbers:
     $ vim Cargo.toml
     $ cargo update -p tealdeer
 
+For release 1.7.0: Remove this note and uncomment warning in
+`docs/src/usage_custom_pages.md`.
+
 Update docs:
 
     $ cargo run -- --help > docs/src/usage.txt
diff --git a/docs/src/installing.md b/docs/src/installing.md
index 9d60898..5e51afa 100644
--- a/docs/src/installing.md
+++ b/docs/src/installing.md
@@ -22,7 +22,7 @@ Tealdeer has been added to a few package managers:
 - Homebrew: [`tealdeer`](https://formulae.brew.sh/formula/tealdeer)
 - MacPorts: [`tealdeer`](https://ports.macports.org/port/tealdeer/)
 - NetBSD: [`sysutils/tealdeer`](https://pkgsrc.se/sysutils/tealdeer)
-- Nix: [`tealdeer`](https://nixos.org/nixos/packages.html#tealdeer)
+- Nix: [`tealdeer`](https://search.nixos.org/packages?query=tealdeer)
 - openSUSE: [`tealdeer`](https://software.opensuse.org/package/tealdeer?search_term=tealdeer)
 - Scoop: [`tealdeer`](https://github.com/ScoopInstaller/Main/blob/master/bucket/tealdeer.json)
 - Solus: [`tealdeer`](https://packages.getsol.us/shannon/t/tealdeer/)
diff --git a/docs/src/usage_custom_pages.md b/docs/src/usage_custom_pages.md
index d3d1e81..87344d7 100644
--- a/docs/src/usage_custom_pages.md
+++ b/docs/src/usage_custom_pages.md
@@ -1,5 +1,16 @@
 # Custom Pages and Patches
 
+<!--
+TODO: Uncomment when releasing 1.7.0.
+> ⚠️ **Breaking change in version 1.7.0:** The file name extension for custom
+> pages and patches was changed:
+>
+> - `<name>.page` → `<name>.page.md`
+> - `<name>.patch` → `<name>.patch.md`
+>
+> If you have custom pages or patches, you need to rename them.
+-->
+
 Tealdeer allows creating new custom pages, overriding existing pages as well as
 extending existing pages.
 
@@ -15,28 +26,28 @@ file](config_directories.html).
 
 To document internal command line tools, or if you want to replace an existing
 tldr page with one that's better suited for you, place a file with the name
-`<command>.page` in the custom pages directory. When calling `tldr <command>`,
+`<command>.page.md` in the custom pages directory. When calling `tldr <command>`,
 your custom page will be shown instead of the upstream version in the cache.
 
 Path:
 
-    $CUSTOM_PAGES_DIR/<command>.page
+    $CUSTOM_PAGES_DIR/<command>.page.md
 
 Example:
 
-    ~/.local/share/tealdeer/pages/ufw.page
+    ~/.local/share/tealdeer/pages/ufw.page.md
 
 ## Custom Patches
 
 Sometimes you don't want to fully replace an existing upstream page, but just
 want to extend it with your own examples that you frequently need. In this
-case, use a file called `<command>.patch`, it will be appended to existing
+case, use a file called `<command>.patch.md`, it will be appended to existing
 pages.
 
 Path:
 
-    $CUSTOM_PAGES_DIR/<command>.patch
+    $CUSTOM_PAGES_DIR/<command>.patch.md
 
 Example:
 
-    ~/.local/share/tealdeer/pages/ufw.patch
+    ~/.local/share/tealdeer/pages/ufw.patch.md
diff --git a/src/cache.rs b/src/cache.rs
index aa64f5e..48be75e 100644
--- a/src/cache.rs
+++ b/src/cache.rs
@@ -13,7 +13,7 @@ use reqwest::{blocking::Client, Proxy};
 use walkdir::{DirEntry, WalkDir};
 use zip::ZipArchive;
 
-use crate::types::PlatformType;
+use crate::{types::PlatformType, utils::print_warning};
 
 pub static TLDR_PAGES_DIR: &str = "tldr-pages";
 static TLDR_OLD_PAGES_DIR: &str = "tldr-master";
@@ -21,6 +21,7 @@ static TLDR_OLD_PAGES_DIR: &str = "tldr-master";
 #[derive(Debug)]
 pub struct Cache {
     cache_dir: PathBuf,
+    enable_styles: bool,
 }
 
 #[derive(Debug)]
@@ -85,12 +86,13 @@ pub enum CacheFreshness {
 }
 
 impl Cache {
-    pub fn new<P>(cache_dir: P) -> Self
+    pub fn new<P>(cache_dir: P, enable_styles: bool) -> Self
     where
         P: Into<PathBuf>,
     {
         Self {
             cache_dir: cache_dir.into(),
+            enable_styles,
         }
     }
 
@@ -232,7 +234,7 @@ impl Cache {
             .find(|path| path.exists() && path.is_file())
     }
 
-    /// Look up custom patch (<name>.patch). If it exists, store it in a variable.
+    /// Look up custom patch (<name>.patch.md). If it exists, store it in a variable.
     fn find_patch(patch_name: &str, custom_pages_dir: Option<&Path>) -> Option<PathBuf> {
         custom_pages_dir
             .map(|custom_dir| custom_dir.join(patch_name))
@@ -248,8 +250,8 @@ impl Cache {
         platforms: &[PlatformType],
     ) -> Option<PageLookupResult> {
         let page_filename = format!("{name}.md");
-        let patch_filename = format!("{name}.patch");
-        let custom_filename = format!("{name}.page");
+        let patch_filename = format!("{name}.patch.md");
+        let custom_filename = format!("{name}.page.md");
 
         // Determine directory paths
         let pages_dir = self.pages_dir();
@@ -264,8 +266,11 @@ impl Cache {
             })
             .collect();
 
-        // Look up custom page (<name>.page). If it exists, return it directly
+        // Look up custom page (<name>.page.md). If it exists, return it directly
         if let Some(config_dir) = custom_pages_dir {
+            // TODO: Remove this check 1 year after version 1.7.0 was released
+            self.check_for_old_custom_pages(config_dir);
+
             let custom_page = config_dir.join(custom_filename);
             if custom_page.exists() && custom_page.is_file() {
                 return Some(PageLookupResult::with_page(custom_page));
@@ -326,6 +331,15 @@ impl Cache {
                 .map(str::to_string)
         };
 
+        let to_stem_custom = |entry: DirEntry| -> Option<String> {
+            entry
+                .path()
+                .file_name()
+                .and_then(OsStr::to_str)
+                .and_then(|s| s.strip_suffix(".page.md"))
+                .map(str::to_string)
+        };
+
         // Recursively walk through common and (if applicable) platform specific directory
         let mut pages = WalkDir::new(platforms_dir)
             .min_depth(1) // Skip root directory
@@ -344,8 +358,12 @@ impl Cache {
 
         if let Some(custom_pages_dir) = custom_pages_dir {
             let is_page = |entry: &DirEntry| -> bool {
-                let extension = entry.path().extension().unwrap_or_default();
-                entry.file_type().is_file() && extension == "page"
+                entry.file_type().is_file()
+                    && entry
+                        .path()
+                        .file_name()
+                        .and_then(OsStr::to_str)
+                        .map_or(false, |file_name| file_name.ends_with(".page.md"))
             };
 
             let custom_pages = WalkDir::new(custom_pages_dir)
@@ -354,7 +372,7 @@ impl Cache {
                 .into_iter()
                 .filter_entry(is_page)
                 .filter_map(Result::ok)
-                .filter_map(to_stem);
+                .filter_map(to_stem_custom);
 
             pages.extend(custom_pages);
         }
@@ -395,6 +413,39 @@ impl Cache {
 
         Ok(true)
     }
+
+    /// Check for old custom pages (without .md suffix) and print a warning.
+    fn check_for_old_custom_pages(&self, custom_pages_dir: &Path) {
+        let old_custom_pages_exist = WalkDir::new(custom_pages_dir)
+            .min_depth(1)
+            .max_depth(1)
+            .into_iter()
+            .filter_entry(|entry| entry.file_type().is_file())
+            .any(|entry| {
+                if let Ok(entry) = entry {
+                    let extension = entry.path().extension();
+                    if let Some(extension) = extension {
+                        extension == "page" || extension == "patch"
+                    } else {
+                        false
+                    }
+                } else {
+                    false
+                }
+            });
+        if old_custom_pages_exist {
+            print_warning(
+                self.enable_styles,
+                &format!(
+                    "Custom pages using the old naming convention were found in {}.\n\
+                     Please rename them to follow the new convention:\n\
+                     - `<name>.page` → `<name>.page.md`\n\
+                     - `<name>.patch` → `<name>.patch.md`",
+                    custom_pages_dir.display()
+                ),
+            );
+        }
+    }
 }
 
 /// Unit Tests for cache module
@@ -411,8 +462,8 @@ mod tests {
     fn test_reader_with_patch() {
         // Write test files
         let dir = tempfile::tempdir().unwrap();
-        let page_path = dir.path().join("test.page");
-        let patch_path = dir.path().join("test.patch");
+        let page_path = dir.path().join("test.page.md");
+        let patch_path = dir.path().join("test.patch.md");
         {
             let mut f1 = File::create(&page_path).unwrap();
             f1.write_all(b"Hello\n").unwrap();
@@ -435,7 +486,7 @@ mod tests {
     fn test_reader_without_patch() {
         // Write test file
         let dir = tempfile::tempdir().unwrap();
-        let page_path = dir.path().join("test.page");
+        let page_path = dir.path().join("test.page.md");
         {
             let mut f = File::create(&page_path).unwrap();
             f.write_all(b"Hello\n").unwrap();
diff --git a/src/main.rs b/src/main.rs
index 15110e6..66da7a1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -301,7 +301,7 @@ fn main() {
     }
 
     // Instantiate cache. This will not yet create the cache directory!
-    let cache = Cache::new(&config.directories.cache_dir.path);
+    let cache = Cache::new(&config.directories.cache_dir.path, enable_styles);
 
     // Clear cache, pass through
     if args.clear_cache {


Test patch:
diff --git a/tests/inkscape-v2.patch b/tests/inkscape-v2.patch.md
similarity index 100%
rename from tests/inkscape-v2.patch
rename to tests/inkscape-v2.patch.md
diff --git a/tests/lib.rs b/tests/lib.rs
index 0afac46..fa8d19c 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -84,7 +84,7 @@ impl TestEnv {
     fn add_page_entry(&self, name: &str, contents: &str) {
         let dir = self.custom_pages_dir.path();
         create_dir_all(dir).unwrap();
-        let mut file = File::create(dir.join(format!("{name}.page"))).unwrap();
+        let mut file = File::create(dir.join(format!("{name}.page.md"))).unwrap();
         file.write_all(contents.as_bytes()).unwrap();
     }
 
@@ -92,7 +92,7 @@ impl TestEnv {
     fn add_patch_entry(&self, name: &str, contents: &str) {
         let dir = self.custom_pages_dir.path();
         create_dir_all(dir).unwrap();
-        let mut file = File::create(dir.join(format!("{name}.patch"))).unwrap();
+        let mut file = File::create(dir.join(format!("{name}.patch.md"))).unwrap();
         file.write_all(contents.as_bytes()).unwrap();
     }
 
@@ -798,7 +798,7 @@ fn test_autoupdate_cache() {
     check_cache_updated(false);
 }
 
-/// End-end test to ensure .page files overwrite pages in cache_dir
+/// End-end test to ensure .page.md files overwrite pages in cache_dir
 #[test]
 fn test_custom_page_overwrites() {
     let testenv = TestEnv::new();
@@ -811,7 +811,7 @@ fn test_custom_page_overwrites() {
 
     // Add file that should be ignored to the cache dir
     testenv.add_entry("inkscape-v2", "");
-    // Add .page file to custome_pages_dir
+    // Add .page.md file to custom_pages_dir
     testenv.add_page_entry("inkscape-v2", include_str!("inkscape-v2.md"));
 
     // Load expected output
@@ -825,7 +825,7 @@ fn test_custom_page_overwrites() {
         .stdout(diff(expected));
 }
 
-/// End-End test to ensure that .patch files are appended to pages in the cache_dir
+/// End-End test to ensure that .patch.md files are appended to pages in the cache_dir
 #[test]
 fn test_custom_patch_appends_to_common() {
     let testenv = TestEnv::new();
@@ -838,8 +838,8 @@ fn test_custom_patch_appends_to_common() {
 
     // Add page to the cache dir
     testenv.add_entry("inkscape-v2", include_str!("inkscape-v2.md"));
-    // Add .page file to custome_pages_dir
-    testenv.add_patch_entry("inkscape-v2", include_str!("inkscape-v2.patch"));
+    // Add .page.md file to custom_pages_dir
+    testenv.add_patch_entry("inkscape-v2", include_str!("inkscape-v2.patch.md"));
 
     // Load expected output
     let expected = include_str!("inkscape-patched-no-color.expected");
@@ -852,7 +852,7 @@ fn test_custom_patch_appends_to_common() {
         .stdout(diff(expected));
 }
 
-/// End-End test to ensure that .patch files are not appended to .page files in the custom_pages_dir
+/// End-End test to ensure that .patch.md files are not appended to .page.md files in the custom_pages_dir
 /// Maybe this interaction should change but I put this test here for the coverage
 #[test]
 fn test_custom_patch_does_not_append_to_custom() {
@@ -868,8 +868,8 @@ fn test_custom_patch_does_not_append_to_custom() {
 
     // Add page to the cache dir
     testenv.add_page_entry("inkscape-v2", include_str!("inkscape-v2.md"));
-    // Add .page file to custome_pages_dir
-    testenv.add_patch_entry("inkscape-v2", include_str!("inkscape-v2.patch"));
+    // Add .page.md file to custom_pages_dir
+    testenv.add_patch_entry("inkscape-v2", include_str!("inkscape-v2.patch.md"));
 
     // Load expected output
     let expected = include_str!("inkscape-default-no-color.expected");