Repository: tldr-pages/tldr-node-client (js)

Issue:
Rendered emails become URL-encoded
## Actual behavior

When the TLDR is rendered, emails are URL-encoded. For example using `tldr gpg`:

```
  - Export public key for mailto:&#x61;&#108;&#x69;&#99;&#x65;&#x40;&#x65;&#x78;&#x61;&#x6d;&#112;&#x6c;&#x65;&#x2e;&#x63;&#111;&#x6d; (output to stdout):
    gpg --export --armor alice@example.com
```

## Expected behavior

Using the Python client and the same example, the output is correct:

```
  - Export public key for alice@example.com (output to stdout):
    gpg --export --armor alice@example.com
```

## Environment

- Node.js v18.12.1
- tldr v3.3.8

Accidentally made [this issue in tldr](https://github.com/tldr-pages/tldr/issues/9560) first. (^: 

Pull request description:
feat (3.4.0): add platform option, Android and BSD platforms
## Description

This PR updates the `--os` and `-o` option to `--platform` and `-p`. This change would make the official client comply with the last client specification and #420 is expected to add support for the recent client specification 2.0. (Supersedes #337, thanks to the original authors and reviews for initial work on this transition)

This PR also adds support for Android and other BSD platforms (~there might be another platform `openbsd` in future, subscribe to https://github.com/tldr-pages/tldr/pull/10698 to stay tuned~ Edit. added it too).

## Checklist

Please review this checklist before submitting a pull request.

- [x] Code compiles correctly
- [x] Created tests, if possible
- [x] All tests passing (`npm run test:all`)
- [x] Extended the README / documentation, if necessary

## Related PRs

Closes #337
Fixes #395

Patch:
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..c999433
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,24 @@
+# This workflow automatically publishes the package to NPM when a new release is created.
+# Before, creating a new release, make sure to update the package version in package.json
+# and add a Granular Access Token (with read and write packages scope)
+# to the repository secrets with the name NPM_TOKEN.
+# Once, the release has been published remove it from the repository secrets.
+
+name: Publish Package to NPM
+on:
+  release:
+    types: [published]
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      # Setup .npmrc file to publish to npm
+      - uses: actions/setup-node@v4
+        with:
+          node-version: '20.x'
+          registry-url: 'https://registry.npmjs.org'
+      - run: npm ci
+      - run: npm publish
+        env:
+          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/README.md b/README.md
index 935135a..a06260a 100644
--- a/README.md
+++ b/README.md
@@ -21,12 +21,19 @@ npm install -g tldr
 To see tldr pages:
 
 - `tldr <command>` show examples for this command
-- `tldr <command> --os=<platform>` show command page for the given platform (`linux`, `osx`, `sunos`, `windows`)
-- `tldr --search "<query>"` search all pages for the query
+- `tldr <command> --platform=<osx, darwin, macos, freebsd, linux, netbsd, openbsd, sunos, android, windows, win32>` show command page for the given platform
+- `tldr --android <command>` show command page for Android
+- `tldr --darwin <command>` show command page for darwin (macOS)
+- `tldr --freebsd <command>` show command page for FreeBSD
 - `tldr --linux <command>` show command page for Linux
-- `tldr --osx <command>` show command page for OSX
+- `tldr --macos <command>` show command page for macOS
+- `tldr --netbsd <command>` show command page for NetBSD
+- `tldr --openbsd <command>` show command page for OpenBSD
+- `tldr --osx <command>` show command page for osx (macOS)
 - `tldr --sunos <command>` show command page for SunOS
+- `tldr --win32 <command>` show command page for win32 (Windows)
 - `tldr --windows <command>` show command page for Windows
+- `tldr --search "<query>"` search all pages for the query
 - `tldr --list` show all pages for current platform
 - `tldr --list-all` show all available pages
 - `tldr --random` show a page at random
@@ -91,7 +98,7 @@ you can put it in the config file:
 The default platform value can be overwritten with command-line option:
 
 ```shell
-tldr du --os=osx
+tldr du --platform=<osx>
 ```
 
 As a contributor, you can also point to your own fork containing the `tldr.zip` file. The file is just a zipped version of the entire tldr repo:
@@ -122,7 +129,7 @@ It's easiest for
 [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh)
 users, so let's start with that.
 
-```
+```zsh
 mkdir -p $ZSH_CUSTOM/plugins/tldr
 ln -s bin/completion/zsh/_tldr $ZSH_CUSTOM/plugins/tldr/_tldr
 ```
@@ -131,13 +138,13 @@ Then add tldr to your oh-my-zsh plugins,
 usually defined in `~/.zshrc`,
 resulting in something looking like this:
 
-```
+```zsh
 plugins=(git tmux tldr)
 ```
 
 Alternatively, using [zplug](https://github.com/zplug/zplug)
 
-```
+```zsh
 zplug "tldr-pages/tldr-node-client", use:bin/completion/zsh
 ```
 
@@ -147,29 +154,29 @@ Copy or symlink `bin/completion/zsh/_tldr` to
 (note the filename).
 Then add the containing directory to your fpath:
 
-```
+```zsh
 fpath=(my/completions $fpath)
 ```
 
 ### Bash
 
-```
+```bash
 ln -s bin/completion/bash/tldr ~/.tldr-completion.bash
 ```
 
 Now add the following line to our bashrc file:
 
-```
+```bash
 source ~/.tldr-completion.bash
 ```
 
 ## FAQ
 
-#### Installation Issues
+### Installation Issues
 
 - If you are trying to install as non-root user (`npm install -g tldr`) and get something like:
 
-```
+```text
 Error: EACCES: permission denied, access '/usr/local/lib/node_modules/tldr'
 ```
 
@@ -177,7 +184,7 @@ Then most probably your npm's default installation directory has improper permis
 
 - If you are trying to install as a root user (`sudo npm install -g tldr`) and get something like:
 
-```
+```shell
 as root ->
 gyp WARN EACCES attempting to reinstall using temporary dev dir "/usr/local/lib/node_modules/tldr/node_modules/webworker-threads/.node-gyp"
 gyp WARN EACCES user "root" does not have permission to access the dev dir "/usr/local/lib/node_modules/tldr/node_modules/webworker-threads/.node-gyp/8.9.1"
@@ -187,14 +194,14 @@ You need to add the option `--unsafe-perm` to your command. This is because when
 
 - If you see an error related to `webworker-threads` like:
 
-```
+```text
 /usr/local/lib/node_modules/tldr/node_modules/natural/lib/natural/classifiers/classifier.js:32
     if (e.code !== 'MODULE_NOT_FOUND') throw e;
 ```
 
 Most probably you need to reinstall `node-gyp` and `webworker-threads`. Try this -
 
-```
+```shell
 sudo -H npm uninstall -g tldr
 sudo -H npm uninstall -g webworker-threads
 npm install -g node-gyp
@@ -236,10 +243,6 @@ for a few rough guidelines.
 [npm-url]: https://www.npmjs.com/package/tldr
 [npm-image]: https://img.shields.io/npm/v/tldr.svg
 [gh-actions-url]: https://github.com/tldr-pages/tldr-node-client/actions?query=workflow%3ATest+branch%3Amaster
-[gh-actions-image]: https://img.shields.io/github/actions/workflow/status/tldr-pages/tldr-node-client/test.yml?branch=master
-[dep-url]: https://david-dm.org/tldr-pages/tldr-node-client
-[dep-image]: https://david-dm.org/tldr-pages/tldr-node-client.svg?theme=shields.io
-[dev-dep-url]: https://david-dm.org/tldr-pages/tldr-node-client#info=devDependencies
-[dev-dep-image]: https://david-dm.org/tldr-pages/tldr-node-client/dev-status.svg?theme=shields.io
+[gh-actions-image]: https://img.shields.io/github/actions/workflow/status/tldr-pages/tldr-node-client/test.yml?branch=main
 [matrix-url]: https://matrix.to/#/#tldr-pages:matrix.org
 [matrix-image]: https://img.shields.io/matrix/tldr-pages:matrix.org?label=chat+on+matrix
diff --git a/bin/completion/bash/tldr b/bin/completion/bash/tldr
index d47a23c..860b478 100644
--- a/bin/completion/bash/tldr
+++ b/bin/completion/bash/tldr
@@ -2,7 +2,7 @@
 
 BUILTIN_THEMES="single base16 ocean"
 
-OS_TYPES="linux osx sunos windows"
+PLATFORM_TYPES="android freebsd linux netbsd openbsd osx sunos windows"
 
 OPTIONS='-v
 --version
@@ -20,10 +20,17 @@ OPTIONS='-v
 --render
 -m
 --markdown
--o
+-p
+--android
+--darwin
+--freebsd
 --linux
+--macos
+--netbsd
+--openbsd
 --osx
 --sunos
+--win32
 --windows
 -t
 --theme
@@ -56,8 +63,8 @@ function _tldr_autocomplete {
         COMPREPLY=(`compgen -f $cur`)
         ;;
 
-      -o|--os)
-        COMPREPLY=(`compgen -W "$OS_TYPES" $cur`)
+      -p|--platform)
+        COMPREPLY=(`compgen -W "$PLATFORM_TYPES" $cur`)
         ;;
 
       -t|--theme)
diff --git a/bin/completion/zsh/_tldr b/bin/completion/zsh/_tldr
index 027e13e..ad1e9ba 100644
--- a/bin/completion/zsh/_tldr
+++ b/bin/completion/zsh/_tldr
@@ -1,8 +1,8 @@
 #compdef tldr
 
-local -a pages oses
+local -a pages platforms
 pages=$(tldr -a1)
-oses='( linux osx sunos windows )'
+platforms='( android freebsd linux netbsd openbsd osx sunos windows )'
 
 _arguments \
   '(- *)'{-h,--help}'[show help]' \
@@ -15,11 +15,18 @@ _arguments \
   '(- *)'{-e,--random-example}'[show a random example]' \
   '(- *)'{-m,--markdown}'[show the original markdown format page]' \
   '(-f --render)'{-f,--render}'[render a specific markdown file]:markdown file:_files -/' \
-  '(-o --os)'{-o,--os}"[override operating system]:os:${oses}" \
+  '(-p --platform)'{-p,--platform}"[override platform]:platform:(${(j:|:)platforms})" \
+  '(- *)'{-u,--update}'[update local cache]' \
+  '--android[override operating system with Android]' \
+  '--darwin[override operating system with macOS]' \
+  '--freebsd[override operating system with FreeBSD]' \
   '--linux[override operating system with Linux]' \
-  '--osx[override operating system with OSX]' \
+  '--macos[override operating system with macOS]' \
+  '--netbsd[override operating system with NetBSD]' \
+  '--openbsd[override operating system with OpenBSD]' \
+  '--osx[override operating system with macOS]' \
   '--sunos[override operating system with SunOS]' \
+  '--win32[override operating system with Windows]' \
   '--windows[override operating system with Windows]' \
-  '(- *)'{-u,--update}'[update local cache]' \
   '(- *)'{-c,--clear-cache}'[clear local cache]' \
   "*:page:(${(b)pages})" && return 0
diff --git a/bin/tldr b/bin/tldr
index f0f5c74..705d6eb 100755
--- a/bin/tldr
+++ b/bin/tldr
@@ -4,9 +4,11 @@ const program = require('commander');
 const pkg = require('../package');
 const Tldr = require('../lib/tldr');
 const config = require('../lib/config');
-const platform = require('../lib/platform');
+const platforms = require('../lib/platforms');
 const { TldrError } = require('../lib/errors');
 
+pkg.version = `v${pkg.version}\nClient Specification: 2.0`;
+
 program
   .version(pkg.version, '-v, --version', 'Display version')
   .helpOption('-h, --help', 'Show this help message')
@@ -22,11 +24,13 @@ program
   .option('-e, --random-example', 'Show a random example')
   .option('-f, --render [file]', 'Render a specific markdown [file]')
   .option('-m, --markdown', 'Output in markdown format')
-  .option('-o, --os [type]', 'Override the operating system [linux, osx, sunos, windows]')
-  .option('--linux', 'Override the operating system with Linux')
-  .option('--osx', 'Override the operating system with OSX')
-  .option('--sunos', 'Override the operating system with SunOS')
-  .option('--windows', 'Override the operating system with Windows')
+  .option('-p, --platform [type]', `Override the current platform [${platforms.supportedPlatforms.join(', ')}]`);
+
+for (const platform of platforms.supportedPlatforms) {
+  program.option(`--${platform}`, `Override the platform with ${platform}`);
+}
+
+program
   .option('-t, --theme [theme]', 'Color theme (simple, base16, ocean)')
   .option('-s, --search [keywords]', 'Search pages using keywords')
   //
@@ -39,7 +43,7 @@ const help = `
   Examples:
 
     $ tldr tar
-    $ tldr du --os=linux
+    $ tldr du --platform=linux
     $ tldr --search "create symbolic link to file"
     $ tldr --list
     $ tldr --list-all
@@ -62,27 +66,15 @@ program.on('--help', () => {
 
 program.parse(process.argv);
 
-if (program.linux) {
-  program.os = 'linux';
-}
-
-if (program.osx) {
-  program.os = 'osx';
-}
-
-if (program.sunos) {
-  program.os = 'sunos';
-}
-
-if (program.windows) {
-  program.os = 'windows';
+for (const platform of platforms.supportedPlatforms) {
+  if (program[platform]) {
+    program.platform = platform;
+  }
 }
 
 let cfg = config.get();
-if (program.os) {
-  if (platform.isSupported(program.os)) {
-    cfg.platform = program.os;
-  }
+if (program.platform && platforms.isSupported(program.platform)) {
+  cfg.platform = program.platform;
 }
 
 if (program.theme) {
diff --git a/lib/cache.js b/lib/cache.js
index f5bbefd..fab2148 100644
--- a/lib/cache.js
+++ b/lib/cache.js
@@ -4,14 +4,12 @@ const fs = require('fs-extra');
 const os = require('os');
 const path = require('path');
 const remote = require('./remote');
-const platform = require('./platform');
+const platforms = require('./platforms');
 const index = require('./index');
 const utils = require('./utils');
 
 class Cache {
   constructor(config) {
-    // TODO: replace this with a private field when it reaches enough maturity
-    // https://github.com/tc39/proposal-class-fields#private-fields
     this.config = config;
     this.cacheFolder = path.join(config.cache, 'cache');
   }
@@ -21,7 +19,7 @@ class Cache {
   }
 
   getPage(page) {
-    let preferredPlatform = platform.getPreferredPlatformFolder(this.config);
+    let preferredPlatform = platforms.getPreferredPlatformFolder(this.config);
     const preferredLanguage = process.env.LANG || 'en';
     return index.findPage(page, preferredPlatform, preferredLanguage)
       .then((folder) => {
diff --git a/lib/config.js b/lib/config.js
index 0f2eb52..34c7627 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -5,6 +5,7 @@ const fs = require('fs');
 const path = require('path');
 const utils = require('./utils');
 const osHomedir = require('os').homedir;
+const platforms = require('./platforms');
 
 exports.get = () => {
   const DEFAULT = path.join(__dirname, '..', 'config.json');
@@ -55,10 +56,9 @@ exports.get = () => {
   return merged;
 };
 
-function validatePlatform(os) {
-  let platform = require('./platform');
-  if (os && !platform.isSupported(os)) {
-    return 'Unsupported platform : ' + os;
+function validatePlatform(platform) {
+  if (platform && !platforms.isSupported(platform)) {
+    return 'Unsupported platform : ' + platform;
   }
   return null;
 }
@@ -118,4 +118,4 @@ function validateThemeItem(field, key) {
     return null;
   }
   return errMsg.join('\n');
-}
\ No newline at end of file
+}
diff --git a/lib/index.js b/lib/index.js
index d116b5b..74c9fac 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -59,12 +59,12 @@ function findPage(page, preferredPlatform, preferredLanguage) {
         targetPlatform = 'common';
       } else if (targets.length > 0 && hasLang(targets, preferredLanguage)) {
         targetLanguage = preferredLanguage;
-        targetPlatform = targets[0].os;
-        console.log(`Command ${page} does not exist for the host platform. Displaying the page from ${targets[0].os} platform`);
+        targetPlatform = targets[0].platform;
+        console.log(`Command ${page} does not exist for the host platform. Displaying the page from ${targetPlatform} platform`);
       } else if (targets.length > 0 && hasLang(targets, 'en')) {
         targetLanguage = 'en';
-        targetPlatform = targets[0].os;
-        console.log(`Command ${page} does not exist for the host platform. Displaying the page from ${targets[0].os} platform`);
+        targetPlatform = targets[0].platform;
+        console.log(`Command ${page} does not exist for the host platform. Displaying the page from ${targetPlatform} platform`);
       }
 
       if (!targetLanguage && !targetPlatform) {
@@ -81,7 +81,7 @@ function findPage(page, preferredPlatform, preferredLanguage) {
 
 function hasPlatformLang(targets, preferredPlatform, preferredLanguage) {
   return targets.some((t) => {
-    return t.os === preferredPlatform && t.language === preferredLanguage;
+    return t.platform === preferredPlatform && t.language === preferredLanguage;
   });
 }
 
@@ -116,7 +116,7 @@ function commandsFor(platform) {
       let commands = Object.keys(idx)
         .filter((cmd) => {
           let targets = idx[cmd].targets;
-          let platforms = targets.map((t) => {return t.os;});
+          let platforms = targets.map((t) => {return t.platform;});
           return platforms.indexOf(platform) !== -1 || platforms.indexOf('common') !== -1;
         })
         .sort();
@@ -187,24 +187,24 @@ function buildShortPagesIndex() {
     .then((files) => {
       files = files.filter(utils.isPage);
       let reducer = (index, file) => {
-        let os = utils.parsePlatform(file);
+        let platform = utils.parsePlatform(file);
         let page = utils.parsePagename(file);
         let language = utils.parseLanguage(file);
         if (index[page]) {
           let targets = index[page].targets;
           let needsPush = true;
           for (const target of targets) {
-            if (target.platform === os && target.language === language) {
+            if (target.platform === platform && target.language === language) {
               needsPush = false;
               continue;
             }
           }
           if (needsPush) {
-            targets.push({'os': os, 'language': language});
+            targets.push({ platform, language });
             index[page].targets = targets;
           }
         } else {
-          index[page] = {targets: [{'os': os, 'language': language}]};
+          index[page] = {targets: [{ platform, language }]};
         }
 
         return index;
diff --git a/lib/platform.js b/lib/platforms.js
similarity index 70%
rename from lib/platform.js
rename to lib/platforms.js
index 6401eb9..2e5b8e5 100644
--- a/lib/platform.js
+++ b/lib/platforms.js
@@ -3,17 +3,24 @@
 const os = require('os');
 
 const folders = {
-  'osx': 'osx',
+  'android': 'android',
   'darwin': 'osx',
+  'freebsd': 'freebsd',
   'linux': 'linux',
+  'macos': 'osx',
+  'netbsd': 'netbsd',
+  'openbsd': 'openbsd',
+  'osx': 'osx',
   'sunos': 'sunos',
-  'windows': 'windows',
-  'win32': 'windows'
+  'win32': 'windows',
+  'windows': 'windows'
 };
 
+const supportedPlatforms = Object.keys(folders);
+
 // Check if the platform is there in the list of platforms or not
 function isSupported(platform) {
-  return Object.prototype.hasOwnProperty.call(folders, platform);
+  return supportedPlatforms.includes(platform);
 }
 
 // If the platform given in config is present, return that.
@@ -35,5 +42,6 @@ function getPreferredPlatformFolder(config) {
 module.exports = {
   isSupported,
   getPreferredPlatform,
-  getPreferredPlatformFolder
+  getPreferredPlatformFolder,
+  supportedPlatforms
 };
diff --git a/lib/render.js b/lib/render.js
index b26b029..aa0dd85 100644
--- a/lib/render.js
+++ b/lib/render.js
@@ -1,6 +1,7 @@
 'use strict';
 
 const Theme = require('./theme');
+const he = require('he'); // Import the 'he' library
 
 // The page structure is passed to this function, and then the theme is applied
 // to different parts of the page and rendered to the console
@@ -24,18 +25,23 @@ exports.toANSI = (page, config) => {
     }, '');
   }
 
+  function decodeEntities(text) {
+    return he.decode(text); // Decode HTML entities
+  }
+
   // Creating an array where each line is an element in it
   let output = [];
 
   // Pushing each line by extracting the page parts and applying the theme to it
   output.push('  ' + theme.renderCommandName(page.name));
   output.push('');
-  output.push('  ' + theme.renderMainDescription(page.description.replace(/\n/g, '\n  ')));
+  output.push('  ' + theme.renderMainDescription(decodeEntities(page.description.replace(/mailto:/g, '')).replace(/\n/g, '\n  '))); // Decode entities and remove "mailto:" prefix in the description
   output.push('');
 
   page.examples.forEach((example) => {
-    output.push(theme.renderExampleDescription('  - ' + example.description));
-    output.push(highlight(example.code));
+    // Decode entities and remove "mailto:" prefix in the description and code
+    output.push(theme.renderExampleDescription('  - ' + decodeEntities(example.description.replace(/mailto:/g, ''))));
+    output.push(highlight(decodeEntities(example.code.replace(/mailto:/g, ''))));
     output.push('');
   });
 
diff --git a/lib/search.js b/lib/search.js
index 1290054..ef4b65a 100644
--- a/lib/search.js
+++ b/lib/search.js
@@ -7,7 +7,7 @@ const natural = require('natural');
 const config = require('./config');
 const utils = require('./utils');
 const index = require('./index');
-const platform = require('./platform');
+const platforms = require('./platforms');
 
 const CACHE_FOLDER = path.join(config.get().cache, 'cache');
 
@@ -189,16 +189,16 @@ exports.printResults = (results, config) => {
   // Prints the passed results to the console.
   // If the command is not available for the current platform,
   // it lists the available platforms instead.
-  // Example: tldr --search print directory tree --os sunos prints:
+  // Example: tldr --search print directory tree --platform sunos prints:
   //              $ tree (Available on: linux, osx)
   index.getShortIndex().then((shortIndex) => {
     let outputs = new Set();
-    let preferredPlatform = platform.getPreferredPlatform(config);
+    let preferredPlatform = platforms.getPreferredPlatform(config);
     results.forEach((elem) => {
       let cmdname = utils.parsePagename(elem.file);
       let output = '    $ ' + cmdname;
       let targets = shortIndex[cmdname]['targets'];
-      let platforms = targets.map((t) => {return t.os;});
+      let platforms = targets.map((t) => {return t.platform;});
       if (platforms.indexOf('common') === -1 && platforms.indexOf(preferredPlatform) === -1) {
         output += ' (Available on: ' + platforms.join(', ') + ')';
       }
diff --git a/lib/tldr.js b/lib/tldr.js
index 27b1adb..5b06ed3 100644
--- a/lib/tldr.js
+++ b/lib/tldr.js
@@ -7,7 +7,7 @@ const ora = require('ora');
 const { EmptyCacheError, MissingPageError, MissingRenderPathError } = require('./errors');
 const Cache = require('./cache');
 const search = require('./search');
-const platform = require('./platform');
+const platforms = require('./platforms');
 const parser = require('./parser');
 const render = require('./render');
 const index = require('./index');
@@ -15,15 +15,13 @@ const index = require('./index');
 
 class Tldr {
   constructor(config) {
-    // TODO: replace this with a private field when it reaches enough maturity
-    // https://github.com/tc39/proposal-class-fields#private-fields
     this.config = config;
     this.cache = new Cache(this.config);
   }
 
   list(singleColumn) {
-    let os = platform.getPreferredPlatformFolder(this.config);
-    return index.commandsFor(os)
+    let platform = platforms.getPreferredPlatformFolder(this.config);
+    return index.commandsFor(platform)
       .then((commands) => {
         return this.printPages(commands, singleColumn);
       });
@@ -41,8 +39,8 @@ class Tldr {
   }
 
   random(options) {
-    let os = platform.getPreferredPlatformFolder(this.config);
-    return index.commandsFor(os)
+    let platform = platforms.getPreferredPlatformFolder(this.config);
+    return index.commandsFor(platform)
       .then((pages) => {
         if (pages.length === 0) {
           throw new EmptyCacheError();
@@ -57,8 +55,8 @@ class Tldr {
   }
 
   randomExample() {
-    let os = platform.getPreferredPlatformFolder(this.config);
-    return index.commandsFor(os)
+    let platform = platforms.getPreferredPlatformFolder(this.config);
+    return index.commandsFor(platform)
       .then((pages) => {
         if (pages.length === 0) {
           throw new EmptyCacheError();
diff --git a/package-lock.json b/package-lock.json
index b796926..8360c74 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "tldr",
-  "version": "3.3.8",
+  "version": "3.4.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "tldr",
-      "version": "3.3.8",
+      "version": "3.4.0",
       "license": "MIT",
       "dependencies": {
         "adm-zip": "^0.5.10",
@@ -15,6 +15,7 @@
         "commander": "^6.1.0",
         "fs-extra": "^9.0.1",
         "glob": "^7.1.6",
+        "he": "^1.2.0",
         "lodash": "^4.17.20",
         "marked": "^4.0.10",
         "ms": "^2.1.2",
@@ -3253,7 +3254,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
       "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
-      "dev": true,
       "bin": {
         "he": "bin/he"
       }
@@ -10085,8 +10085,7 @@
     "he": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
-      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
-      "dev": true
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
     },
     "hmac-drbg": {
       "version": "1.0.1",
diff --git a/package.json b/package.json
index 8341597..a75de6d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "tldr",
-  "version": "3.3.8",
+  "version": "3.4.0",
   "description": "Simplified and community-driven man pages",
   "author": "Romain Prieto",
   "license": "MIT",
@@ -14,15 +14,18 @@
     "unix",
     "linux",
     "osx",
+    "openbsd",
+    "freebsd",
+    "netbsd",
     "commands",
     "command-line",
     "shell",
     "bash",
     "zsh"
   ],
-  "homepage": "http://tldr-pages.github.io",
+  "homepage": "https://tldr.sh",
   "engines": {
-    "node": ">=14"
+    "node": ">=16"
   },
   "main": "bin/tldr",
   "files": [
@@ -56,6 +59,7 @@
     "commander": "^6.1.0",
     "fs-extra": "^9.0.1",
     "glob": "^7.1.6",
+    "he": "^1.2.0",
     "lodash": "^4.17.20",
     "marked": "^4.0.10",
     "ms": "^2.1.2",


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 0081cbd..ea02464 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -34,7 +34,7 @@ jobs:
           ${{ matrix.os }}-npm-
 
     - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v3
+      uses: actions/setup-node@v4
       with:
         node-version: ${{ matrix.node-version }}
 
diff --git a/test/cache.spec.js b/test/cache.spec.js
index 2ab8270..77eff24 100644
--- a/test/cache.spec.js
+++ b/test/cache.spec.js
@@ -8,7 +8,7 @@ const path = require('path');
 const fs = require('fs-extra');
 const index = require('../lib/index');
 const remote = require('../lib/remote');
-const platform = require('../lib/platform');
+const platforms = require('../lib/platforms');
 
 
 describe('Cache', () => {
@@ -69,7 +69,9 @@ describe('Cache', () => {
         dd: ['linux', 'osx', 'sunos'],
         du: ['linux', 'osx', 'sunos'],
         top: ['linux', 'osx'],
-        svcs: ['sunos']
+        svcs: ['sunos'],
+        pkg: ['android', 'freebsd', 'openbsd'],
+        pkgin: ['netbsd']
       });
     });
 
@@ -79,7 +81,7 @@ describe('Cache', () => {
 
     it('should return page contents for ls', () => {
       sinon.stub(fs, 'readFile').resolves('# ls\n> ls page');
-      sinon.stub(platform, 'getPreferredPlatformFolder').returns('osx');
+      sinon.stub(platforms, 'getPreferredPlatformFolder').returns('osx');
       sinon.stub(index, 'findPage').resolves('osx');
       const cache = new Cache(config.get());
       return cache.getPage('ls')
@@ -87,28 +89,28 @@ describe('Cache', () => {
           should.exist(content);
           content.should.startWith('# ls');
           fs.readFile.restore();
-          platform.getPreferredPlatformFolder.restore();
+          platforms.getPreferredPlatformFolder.restore();
           index.findPage.restore();
         });
     });
 
     it('should return empty contents for svcs on OSX', () =>{
       sinon.stub(fs, 'readFile').resolves('# svcs\n> svcs');
-      sinon.stub(platform, 'getPreferredPlatformFolder').returns('osx');
+      sinon.stub(platforms, 'getPreferredPlatformFolder').returns('osx');
       sinon.stub(index, 'findPage').resolves(null);
       const cache = new Cache(config.get());
       return cache.getPage('svc')
         .then((content) => {
           should.not.exist(content);
           fs.readFile.restore();
-          platform.getPreferredPlatformFolder.restore();
+          platforms.getPreferredPlatformFolder.restore();
           index.findPage.restore();
         });
     });
 
     it('should return page contents for svcs on SunOS', () => {
       sinon.stub(fs, 'readFile').resolves('# svcs\n> svcs');
-      sinon.stub(platform, 'getPreferredPlatformFolder').returns('sunos');
+      sinon.stub(platforms, 'getPreferredPlatformFolder').returns('sunos');
       sinon.stub(index, 'findPage').resolves('svcs');
       const cache = new Cache(config.get());
       return cache.getPage('svcs')
@@ -116,7 +118,22 @@ describe('Cache', () => {
           should.exist(content);
           content.should.startWith('# svcs');
           fs.readFile.restore();
-          platform.getPreferredPlatformFolder.restore();
+          platforms.getPreferredPlatformFolder.restore();
+          index.findPage.restore();
+        });
+    });
+
+    it('should return page contents for pkg on Android', () => {
+      sinon.stub(fs, 'readFile').resolves('# pkg\n> pkg');
+      sinon.stub(platforms, 'getPreferredPlatformFolder').returns('android');
+      sinon.stub(index, 'findPage').resolves('pkg');
+      const cache = new Cache(config.get());
+      return cache.getPage('pkg')
+        .then((content) => {
+          should.exist(content);
+          content.should.startWith('# pkg');
+          fs.readFile.restore();
+          platforms.getPreferredPlatformFolder.restore();
           index.findPage.restore();
         });
     });
diff --git a/test/functional-test.sh b/test/functional-test.sh
index 919f88d..17a96ec 100755
--- a/test/functional-test.sh
+++ b/test/functional-test.sh
@@ -6,11 +6,11 @@ alias tldr="node bin/tldr"
 
 function tldr-render-pages {
   tldr zip && \
-  tldr du --os=linux && \
-  tldr du --os=osx && \
-  tldr du --os=linux --markdown && \
-  tldr du --os=osx --markdown && \
-  tldr du --os=windows --markdown && \
+  tldr du --platform=linux && \
+  tldr du --platform=osx && \
+  tldr du --platform=linux --markdown && \
+  tldr du --platform=osx --markdown && \
+  tldr du --platform=windows --markdown && \
   LANG= tldr --random && \
   LANG= tldr --random-example && \
   tldr --list && \
diff --git a/test/index.spec.js b/test/index.spec.js
index f2cb7dd..5961783 100644
--- a/test/index.spec.js
+++ b/test/index.spec.js
@@ -17,15 +17,19 @@ const pages = [
   ['/pages', 'common', 'git.md'],
   ['/pages', 'common', 'ln.md'],
   ['/pages', 'common', 'ls.md'],
+  ['/pages', 'freebsd', 'pkg.md'],
   ['/pages', 'linux', 'dd.md'],
   ['/pages', 'linux', 'du.md'],
   ['/pages', 'linux', 'top.md'],
+  ['/pages', 'netbsd', 'pkgin.md'],
+  ['/pages', 'openbsd', 'pkg.md'],
   ['/pages', 'osx', 'dd.md'],
   ['/pages', 'osx', 'du.md'],
   ['/pages', 'osx', 'top.md'],
   ['/pages', 'sunos', 'dd.md'],
   ['/pages', 'sunos', 'du.md'],
-  ['/pages', 'sunos', 'svcs.md']
+  ['/pages', 'sunos', 'svcs.md'],
+  ['/pages', 'android', 'pkg.md'],
 ].map((x) => {
   return path.join(...x);
 });
@@ -84,139 +88,170 @@ describe('Index', () => {
     it('should find Linux platform for apk command for Chinese', () => {
       return index.findPage('apk', 'linux', 'zh')
         .then((folder) => {
-          return folder.should.equal(path.join('pages.zh', 'linux'));
+          should.equal(folder, path.join('pages.zh', 'linux'));
+        });
+    });
+
+    it('should find platform android for pkg command for English', () => {
+      return index.findPage('pkg', 'android', 'en')
+        .then((folder) => {
+          should.equal(folder, path.join('pages', 'android'));
+        });
+    });
+
+    it('should find platform freebsd for pkg command for English', () => {
+      return index.findPage('pkg', 'freebsd', 'en')
+        .then((folder) => {
+          should.equal(folder, path.join('pages', 'freebsd'));
+        });
+    });
+
+    it('should find platform openbsd for pkg command for English', () => {
+      return index.findPage('pkg', 'openbsd', 'en')
+        .then((folder) => {
+          should.equal(folder, path.join('pages', 'openbsd'));
+        });
+    });
+
+    it('should find platform netbsd for pkgin command for English', () => {
+      return index.findPage('pkgin', 'netbsd', 'en')
+        .then((folder) => {
+          should.equal(folder, path.join('pages', 'netbsd'));
         });
     });
 
     it('should find Linux platform for apk command for Chinese given Windows', () => {
       return index.findPage('apk', 'windows', 'zh')
         .then((folder) => {
-          return folder.should.equal(path.join('pages.zh', 'linux'));
+          should.equal(folder, path.join('pages.zh', 'linux'));
         });
     });
 
     it('should find Linux platform for dd command', () => {
       return index.findPage('dd', 'linux', 'en')
         .then((folder) => {
-          return folder.should.equal(path.join('pages', 'linux'));
+          should.equal(folder, path.join('pages', 'linux'));
         });
     });
 
     it('should find platform common for cp command for English', () => {
       return index.findPage('cp', 'linux', 'en')
         .then((folder) => {
-          return folder.should.equal(path.join('pages', 'common'));
+          should.equal(folder, path.join('pages', 'common'));
         });
     });
 
     it('should find platform common for cp command for Tamil', () => {
       return index.findPage('cp', 'linux', 'ta')
         .then((folder) => {
-          return folder.should.equal(path.join('pages.ta', 'common'));
+          should.equal(folder, path.join('pages.ta', 'common'));
         });
     });
 
     it('should find platform common for cp command for Italian', () => {
       return index.findPage('cp', 'linux', 'it')
         .then((folder) => {
-          return folder.should.equal(path.join('pages.it', 'common'));
+          should.equal(folder, path.join('pages.it', 'common'));
         });
     });
 
     it('should find platform common for cp command for Italian given Windows', () => {
       return index.findPage('cp', 'windows', 'it')
         .then((folder) => {
-          return folder.should.equal(path.join('pages.it', 'common'));
+          should.equal(folder, path.join('pages.it', 'common'));
         });
     });
 
     it('should find platform common for ls command for Italian', () => {
       return index.findPage('ls', 'linux', 'it')
         .then((folder) => {
-          return folder.should.equal(path.join('pages', 'common'));
+          should.equal(folder, path.join('pages', 'common'));
         });
     });
 
+
     it('should find platform common for cp command for Italian given common platform', () => {
       return index.findPage('cp', 'common', 'it')
         .then((folder) => {
-          return folder.should.equal(path.join('pages.it', 'common'));
+          should.equal(folder, path.join('pages.it', 'common'));
         });
     });
 
     it('should find platform common for cp command for English given a bad language', () => {
       return index.findPage('cp', 'linux', 'notexist')
         .then((folder) => {
-          return folder.should.equal(path.join('pages', 'common'));
+          should.equal(folder, path.join('pages', 'common'));
         });
     });
 
     it('should find platform for svcs command on Linux', () => {
       return index.findPage('svcs', 'linux', 'en')
         .then((folder) => {
-          return folder.should.equal(path.join('pages', 'sunos'));
+          should.equal(folder, path.join('pages', 'sunos'));
         });
     });
 
     it('should not find platform for non-existing command', () => {
       return index.findPage('qwerty', 'linux', 'en')
         .then((folder) => {
-          return should.not.exist(folder);
+          should.not.exist(folder);
         });
     });
   });
 
-  it('should return correct list of all pages', () => {
+  it('should return the correct list of all pages', () => {
     return index.commands()
       .then((commands) => {
-        commands.should.deepEqual([
-          'apk', 'cp', 'dd', 'du', 'git', 'ln', 'ls', 'svcs', 'top'
+        should.deepEqual(commands, [
+          'apk', 'cp', 'dd', 'du', 'git', 'ln', 'ls', 'pkg', 'pkgin', 'svcs', 'top'
         ]);
       });
   });
 
   describe('commandsFor()', () => {
-    it('should return correct list of pages for Linux', () => {
+    it('should return the correct list of pages for Linux', () => {
       return index.commandsFor('linux')
         .then((commands) => {
-          commands.should.deepEqual([
+          should.deepEqual(commands, [
             'apk', 'cp', 'dd', 'du', 'git', 'ln', 'ls', 'top'
           ]);
         });
     });
 
-    it('should return correct list of pages for OSX', () => {
+    it('should return the correct list of pages for OSX', () => {
       return index.commandsFor('osx')
         .then((commands) => {
-          commands.should.deepEqual([
+          should.deepEqual(commands, [
             'cp', 'dd', 'du', 'git', 'ln', 'ls', 'top'
           ]);
         });
     });
 
-    it('should return correct list of pages for SunOS', () => {
+    it('should return the correct list of pages for SunOS', () => {
       return index.commandsFor('sunos')
         .then((commands) => {
-          commands.should.deepEqual([
+          should.deepEqual(commands, [
             'cp', 'dd', 'du', 'git', 'ln', 'ls', 'svcs'
           ]);
         });
     });
   });
 
-  it('should return correct short index on getShortIndex()', () => {
+  it('should return the correct short index on getShortIndex()', () => {
     return index.getShortIndex()
       .then((idx) => {
-        idx.should.deepEqual({
-          apk: {targets: [{language: 'en', os: 'linux'}, {language: 'zh', os: 'linux'}]},
-          cp: {targets: [{language: 'en', os: 'common'}, {language: 'it', os: 'common'}, {language: 'ta', os: 'common'}]},
-          dd: {targets: [{language: 'en', os: 'linux'}, {language: 'en', os: 'osx'}, {language: 'en', os: 'sunos'}]},
-          du: {targets: [{language: 'en', os: 'linux'}, {language: 'en', os: 'osx'}, {language: 'en', os: 'sunos'}]},
-          git: {targets: [{language: 'en', os: 'common'}]},
-          ln: {targets: [{language: 'en', os: 'common'}]},
-          ls: {targets: [{language: 'en', os: 'common'}]},
-          svcs: {targets: [{language: 'en', os: 'sunos'}]},
-          top: {targets: [{language: 'en', os: 'linux'}, {language: 'en', os: 'osx'}]},
+        should.deepEqual(idx, {
+          apk: { targets: [{ language: 'en', platform: 'linux' }, { language: 'zh', platform: 'linux' }] },
+          cp: { targets: [{ language: 'en', platform: 'common' }, { language: 'it', platform: 'common' }, { language: 'ta', platform: 'common' }] },
+          dd: { targets: [{ language: 'en', platform: 'linux' }, { language: 'en', platform: 'osx' }, { language: 'en', platform: 'sunos' }] },
+          du: { targets: [{ language: 'en', platform: 'linux' }, { language: 'en', platform: 'osx' }, { language: 'en', platform: 'sunos' }] },
+          git: { targets: [{ language: 'en', platform: 'common' }] },
+          ln: { targets: [{ language: 'en', platform: 'common' }] },
+          ls: { targets: [{ language: 'en', platform: 'common' }] },
+          pkg: { targets: [{ language: 'en', platform: 'freebsd' }, { language: 'en', platform: 'openbsd' }, { language: 'en', platform: 'android' }] },
+          pkgin: { targets: [{ language: 'en', platform: 'netbsd' }] },
+          svcs: { targets: [{ language: 'en', platform: 'sunos' }] },
+          top: { targets: [{ language: 'en', platform: 'linux' }, { language: 'en', platform: 'osx' }] },
         });
       });
   });
diff --git a/test/platform.spec.js b/test/platform.spec.js
index aa0842d..0ed7723 100644
--- a/test/platform.spec.js
+++ b/test/platform.spec.js
@@ -3,7 +3,7 @@
 const os = require('os');
 const config = require('../lib/config');
 const sinon = require('sinon');
-const platform = require('../lib/platform');
+const platforms = require('../lib/platforms');
 
 describe('Platform', () => {
 
@@ -20,7 +20,7 @@ describe('Platform', () => {
     it('should return the running platform with no configuration', () => {
       os.platform.onCall(0).returns('darwin');
       this.config = {};
-      platform.getPreferredPlatform(this.config).should.eql('darwin');
+      platforms.getPreferredPlatform(this.config).should.eql('darwin');
     });
 
     it('should overwrite the running platform if configured', () => {
@@ -28,7 +28,7 @@ describe('Platform', () => {
       this.config = {
         platform: 'linux'
       };
-      platform.getPreferredPlatform(this.config).should.eql('linux');
+      platforms.getPreferredPlatform(this.config).should.eql('linux');
     });
 
     it('should return current system platform if configuration is wrong', () => {
@@ -36,17 +36,21 @@ describe('Platform', () => {
       this.config = {
         platform: 'there_is_no_such_platform'
       };
-      platform.getPreferredPlatform(this.config).should.eql('darwin');
+      platforms.getPreferredPlatform(this.config).should.eql('darwin');
     });
   });
 
   describe('isSupported', () => {
-    it('should tell that Linux, OSX, SunOS and Win32 are supported', () => {
-      platform.isSupported('osx').should.eql(true);
-      platform.isSupported('linux').should.eql(true);
-      platform.isSupported('sunos').should.eql(true);
-      platform.isSupported('windows').should.eql(true);
-      platform.isSupported('ios').should.eql(false);
+    it('should tell that Android, FreeBSD, Linux, NetBSD, OpenBSD, OSX, SunOS and Win32 are supported', () => {
+      platforms.isSupported('android').should.eql(true);
+      platforms.isSupported('osx').should.eql(true);
+      platforms.isSupported('freebsd').should.eql(true);
+      platforms.isSupported('linux').should.eql(true);
+      platforms.isSupported('netbsd').should.eql(true);
+      platforms.isSupported('openbsd').should.eql(true);
+      platforms.isSupported('sunos').should.eql(true);
+      platforms.isSupported('windows').should.eql(true);
+      platforms.isSupported('ios').should.eql(false);
     });
   });
 });