From f29b956339d44dc22c6c911797d43bd8415ec1c9 Mon Sep 17 00:00:00 2001
From: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Date: Wed, 18 Jan 2023 12:36:16 +0100
Subject: [PATCH] DEV: introduces documentation for chat (#19772)

Note this commit also slightly changes internal API: channel instead of getChannel and updateCurrentUserChannelNotificationsSettings instead of updateCurrentUserChatChannelNotificationsSettings.

Also destroyChannel takes a second param which is the name confirmation instead of an optional object containing this confirmation. This is to enforce the fact that it's required.

In the future a top level jsdoc config file could be used instead of the hack tempfile, but while it's only an experiment for chat, it's probably good enough.
---
 .github/workflows/documentation.yml           |  76 +++
 .jsdoc                                        |   7 +
 package.json                                  |   1 +
 .../chat-channel-delete-modal-inner.js        |   4 +-
 .../components/chat-channel-settings-view.js  |   5 +-
 .../javascripts/discourse/lib/collection.js   | 128 +++++
 .../discourse/services/chat-api.js            | 257 +++++----
 .../services/chat-channels-manager.js         |   2 +-
 plugins/chat/docs/FRONTEND.md                 | 352 ++++++++++++
 plugins/chat/lib/tasks/chat_doc.rake          |  13 +
 yarn.lock                                     | 531 +++++++++++++++++-
 11 files changed, 1252 insertions(+), 124 deletions(-)
 create mode 100644 .github/workflows/documentation.yml
 create mode 100644 .jsdoc
 create mode 100644 plugins/chat/assets/javascripts/discourse/lib/collection.js
 create mode 100644 plugins/chat/docs/FRONTEND.md
 create mode 100644 plugins/chat/lib/tasks/chat_doc.rake

diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
new file mode 100644
index 00000000000..aec086257ae
--- /dev/null
+++ b/.github/workflows/documentation.yml
@@ -0,0 +1,76 @@
+name: Documentation
+
+on:
+  pull_request:
+  push:
+    branches:
+      - main
+
+permissions:
+  contents: read
+
+jobs:
+  build:
+    if: "!(github.event_name == 'push' && github.repository == 'discourse/discourse-private-mirror')"
+    name: run
+    runs-on: ubuntu-latest
+    container: discourse/discourse_test:slim
+    timeout-minutes: 30
+
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 1
+
+      - name: Setup Git
+        run: |
+          git config --global user.email "ci@ci.invalid"
+          git config --global user.name "Discourse CI"
+
+      - name: Bundler cache
+        uses: actions/cache@v3
+        with:
+          path: vendor/bundle
+          key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
+          restore-keys: |
+            ${{ runner.os }}-gem-
+
+      - name: Setup gems
+        run: |
+          gem install bundler --conservative -v $(awk '/BUNDLED WITH/ { getline; gsub(/ /,""); print $0 }' Gemfile.lock)
+          bundle config --local path vendor/bundle
+          bundle config --local deployment true
+          bundle config --local without development
+          bundle install --jobs 4
+          bundle clean
+
+      - name: Get yarn cache directory
+        id: yarn-cache-dir
+        run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
+
+      - name: Yarn cache
+        uses: actions/cache@v3
+        id: yarn-cache
+        with:
+          path: ${{ steps.yarn-cache-dir.outputs.dir }}
+          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+          restore-keys: |
+            ${{ runner.os }}-yarn-
+
+      - name: Yarn install
+        run: yarn install
+
+      - name: Check Chat documentation
+        run: |
+          LOAD_PLUGINS=1 bin/rake chat:doc
+
+          if [ ! -z "$(git status --porcelain plugins/chat/docs/)" ]; then
+            echo "Chat documentation is not up to date. To resolve, run:"
+            echo "  LOAD_PLUGINS=1 bin/rake chat:doc"
+            echo
+            echo "Or manually apply the diff printed below:"
+            echo "---------------------------------------------"
+            git -c color.ui=always diff plugins/chat/docs/
+            exit 1
+          fi
+        timeout-minutes: 30
diff --git a/.jsdoc b/.jsdoc
new file mode 100644
index 00000000000..92345fda22c
--- /dev/null
+++ b/.jsdoc
@@ -0,0 +1,7 @@
+// jsdoc doesn't accept paths starting with _ (which is the case on github runners)
+// so we need to alter the default config
+{
+  "source": {
+    "excludePattern": ""
+  }
+}
diff --git a/package.json b/package.json
index d39655d49c4..922906ef928 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
     "chrome-launcher": "^0.15.1",
     "chrome-remote-interface": "^0.31.3",
     "eslint-config-discourse": "^3.3.0",
+    "jsdoc-to-markdown": "^8.0.0",
     "lefthook": "^1.2.0",
     "puppeteer-core": "^13.7.0"
   },
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js
index 4943ae1e34b..9a34dd80e53 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js
@@ -40,9 +40,7 @@ export default Component.extend(ModalFunctionality, {
     this.set("deleting", true);
 
     return this.chatApi
-      .destroyChannel(this.chatChannel.id, {
-        name_confirmation: this.channelNameConfirmation,
-      })
+      .destroyChannel(this.chatChannel.id, this.channelNameConfirmation)
       .then(() => {
         this.set("confirmed", true);
         this.flash(I18n.t("chat.channel_delete.process_started"), "success");
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.js
index 5367f4af96e..7ce1a4b4975 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.js
@@ -87,10 +87,7 @@ export default class ChatChannelSettingsView extends Component {
     const settings = {};
     settings[key] = value;
     return this.chatApi
-      .updateCurrentUserChatChannelNotificationsSettings(
-        this.channel.id,
-        settings
-      )
+      .updateCurrentUserChannelNotificationsSettings(this.channel.id, settings)
       .then((result) => {
         [
           "muted",
diff --git a/plugins/chat/assets/javascripts/discourse/lib/collection.js b/plugins/chat/assets/javascripts/discourse/lib/collection.js
new file mode 100644
index 00000000000..a001121e2d6
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/lib/collection.js
@@ -0,0 +1,128 @@
+/** @module Collection */
+
+import { ajax } from "discourse/lib/ajax";
+import { tracked } from "@glimmer/tracking";
+import { bind } from "discourse-common/utils/decorators";
+import { Promise } from "rsvp";
+
+/**
+ * Handles a paginated API response.
+ *
+ * @class
+ */
+export default class Collection {
+  @tracked items = [];
+  @tracked meta = {};
+  @tracked loading = false;
+
+  /**
+   * Create a Collection instance
+   * @param {string} resourceURL - the API endpoint to call
+   * @param {callback} handler - anonymous function used to handle the response
+   */
+  constructor(resourceURL, handler) {
+    this._resourceURL = resourceURL;
+    this._handler = handler;
+    this._fetchedAll = false;
+  }
+
+  get loadMoreURL() {
+    return this.meta.load_more_url;
+  }
+
+  get totalRows() {
+    return this.meta.total_rows;
+  }
+
+  get length() {
+    return this.items.length;
+  }
+
+  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
+  [Symbol.iterator]() {
+    let index = 0;
+
+    return {
+      next: () => {
+        if (index < this.items.length) {
+          return { value: this.items[index++], done: false };
+        } else {
+          return { done: true };
+        }
+      },
+    };
+  }
+
+  /**
+   * Loads first batch of results
+   * @returns {Promise}
+   */
+  @bind
+  load(params = {}) {
+    this._fetchedAll = false;
+
+    if (this.loading) {
+      return Promise.resolve();
+    }
+
+    this.loading = true;
+
+    const filteredQueryParams = Object.entries(params).filter(
+      ([, v]) => v !== undefined
+    );
+    const queryString = new URLSearchParams(filteredQueryParams).toString();
+
+    const endpoint = this._resourceURL + (queryString ? `?${queryString}` : "");
+    return this.#fetch(endpoint)
+      .then((result) => {
+        this.items = this._handler(result);
+        this.meta = result.meta;
+      })
+      .finally(() => {
+        this.loading = false;
+      });
+  }
+
+  /**
+   * Attempts to load more results
+   * @returns {Promise}
+   */
+  @bind
+  loadMore() {
+    let promise = Promise.resolve();
+
+    if (this.loading) {
+      return promise;
+    }
+
+    if (
+      this._fetchedAll ||
+      (this.totalRows && this.items.length >= this.totalRows)
+    ) {
+      return promise;
+    }
+
+    this.loading = true;
+
+    if (this.loadMoreURL) {
+      promise = this.#fetch(this.loadMoreURL).then((result) => {
+        const newItems = this._handler(result);
+
+        if (newItems.length) {
+          this.items = this.items.concat(newItems);
+        } else {
+          this._fetchedAll = true;
+        }
+        this.meta = result.meta;
+      });
+    }
+
+    return promise.finally(() => {
+      this.loading = false;
+    });
+  }
+
+  #fetch(url) {
+    return ajax(url, { type: "GET" });
+  }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-api.js b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
index e88cb954cc9..e0157f0f617 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-api.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
@@ -1,123 +1,42 @@
+/** @module ChatApi */
+
 import Service, { inject as service } from "@ember/service";
 import { ajax } from "discourse/lib/ajax";
 import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership";
-import { tracked } from "@glimmer/tracking";
-import { bind } from "discourse-common/utils/decorators";
-import { Promise } from "rsvp";
-
-class Collection {
-  @tracked items = [];
-  @tracked meta = {};
-  @tracked loading = false;
-
-  constructor(resourceURL, handler) {
-    this._resourceURL = resourceURL;
-    this._handler = handler;
-    this._fetchedAll = false;
-  }
-
-  get loadMoreURL() {
-    return this.meta.load_more_url;
-  }
-
-  get totalRows() {
-    return this.meta.total_rows;
-  }
-
-  get length() {
-    return this.items.length;
-  }
-
-  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
-  [Symbol.iterator]() {
-    let index = 0;
-
-    return {
-      next: () => {
-        if (index < this.items.length) {
-          return { value: this.items[index++], done: false };
-        } else {
-          return { done: true };
-        }
-      },
-    };
-  }
-
-  @bind
-  load(params = {}) {
-    this._fetchedAll = false;
-
-    if (this.loading) {
-      return;
-    }
-
-    this.loading = true;
-
-    const filteredQueryParams = Object.entries(params).filter(
-      ([, v]) => v !== undefined
-    );
-    const queryString = new URLSearchParams(filteredQueryParams).toString();
-
-    const endpoint = this._resourceURL + (queryString ? `?${queryString}` : "");
-    return this.#fetch(endpoint)
-      .then((result) => {
-        this.items = this._handler(result);
-        this.meta = result.meta;
-      })
-      .finally(() => {
-        this.loading = false;
-      });
-  }
-
-  @bind
-  loadMore() {
-    let promise = Promise.resolve();
-
-    if (this.loading) {
-      return promise;
-    }
-
-    if (
-      this._fetchedAll ||
-      (this.totalRows && this.items.length >= this.totalRows)
-    ) {
-      return promise;
-    }
-
-    this.loading = true;
-
-    if (this.loadMoreURL) {
-      promise = this.#fetch(this.loadMoreURL).then((result) => {
-        const newItems = this._handler(result);
-
-        if (newItems.length) {
-          this.items = this.items.concat(newItems);
-        } else {
-          this._fetchedAll = true;
-        }
-        this.meta = result.meta;
-      });
-    }
-
-    return promise.finally(() => {
-      this.loading = false;
-    });
-  }
-
-  #fetch(url) {
-    return ajax(url, { type: "GET" });
-  }
-}
+import Collection from "../lib/collection";
 
+/**
+ * Chat API service. Provides methods to interact with the chat API.
+ *
+ * @class
+ * @implements {@ember/service}
+ */
 export default class ChatApi extends Service {
   @service chatChannelsManager;
 
-  getChannel(channelId) {
+  /**
+   * Get a channel by its ID.
+   * @param {number} channelId - The ID of the channel.
+   * @returns {Promise}
+   *
+   * @example
+   *
+   *    this.chatApi.channel(1).then(channel => { ... })
+   */
+  channel(channelId) {
     return this.#getRequest(`/channels/${channelId}`).then((result) =>
       this.chatChannelsManager.store(result.channel)
     );
   }
 
+  /**
+   * List all accessible category channels of the current user.
+   * @returns {module:Collection}
+   *
+   * @example
+   *
+   *    this.chatApi.channels.then(channels => { ... })
+   */
   channels() {
     return new Collection(`${this.#basePath}/channels`, (response) => {
       return response.channels.map((channel) =>
@@ -126,26 +45,85 @@ export default class ChatApi extends Service {
     });
   }
 
+  /**
+   * Moves messages from one channel to another.
+   * @param {number} channelId - The ID of the original channel.
+   * @param {object} data - Params of the move.
+   * @param {Array.<number>} data.message_ids - IDs of the moved messages.
+   * @param {number} data.destination_channel_id - ID of the channel where the messages are moved to.
+   * @returns {Promise}
+   *
+   * @example
+   *
+   *   this.chatApi
+   *     .moveChannelMessages(1, {
+   *       message_ids: [2, 3],
+   *       destination_channel_id: 4,
+   *     }).then(() => { ... })
+   */
   moveChannelMessages(channelId, data = {}) {
     return this.#postRequest(`/channels/${channelId}/messages/moves`, {
       move: data,
     });
   }
 
-  destroyChannel(channelId, data = {}) {
-    return this.#deleteRequest(`/channels/${channelId}`, { channel: data });
+  /**
+   * Destroys a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @param {string} channelName - The name of the channel to be destroyed, used as confirmation.
+   * @returns {Promise}
+   *
+   * @example
+   *
+   *    this.chatApi.destroyChannel(1, "foo").then(() => { ... })
+   */
+  destroyChannel(channelId, channelName) {
+    return this.#deleteRequest(`/channels/${channelId}`, {
+      channel: { name_confirmation: channelName },
+    });
   }
 
+  /**
+   * Creates a channel.
+   * @param {object} data - Params of the channel.
+   * @param {string} data.name - The name of the channel.
+   * @param {string} data.chatable_id - The category of the channel.
+   * @param {string} data.description - The description of the channel.
+   * @param {boolean} [data.auto_join_users] - Should users join this channel automatically.
+   * @returns {Promise}
+   *
+   * @example
+   *
+   *    this.chatApi
+   *      .createChannel({ name: "foo", chatable_id: 1, description "bar" })
+   *      .then((channel) => { ... })
+   */
   createChannel(data = {}) {
     return this.#postRequest("/channels", { channel: data }).then((response) =>
       this.chatChannelsManager.store(response.channel)
     );
   }
 
+  /**
+   * Lists chat permissions for a category.
+   * @param {number} categoryId - ID of the category.
+   * @returns {Promise}
+   */
   categoryPermissions(categoryId) {
-    return ajax(`/chat/api/category-chatables/${categoryId}/permissions`);
+    return this.#getRequest(`/category-chatables/${categoryId}/permissions`);
   }
 
+  /**
+   * Sends a message.
+   * @param {number} channelId - ID of the channel.
+   * @param {object} data - Params of the message.
+   * @param {string} data.message - The raw content of the message in markdown.
+   * @param {string} data.cooked - The cooked content of the message.
+   * @param {number} [data.in_reply_to_id] - The ID of the replied-to message.
+   * @param {number} [data.staged_id] - The staged ID of the message before it was persisted.
+   * @param {Array.<number>} [data.upload_ids] - Array of upload ids linked to the message.
+   * @returns {Promise}
+   */
   sendMessage(channelId, data = {}) {
     return ajax(`/chat/${channelId}`, {
       ignoreUnsent: false,
@@ -154,20 +132,50 @@ export default class ChatApi extends Service {
     });
   }
 
+  /**
+   * Creates a channel archive.
+   * @param {number} channelId - The ID of the channel.
+   * @param {object} data - Params of the archive.
+   * @param {string} data.selection - "new_topic" or "existing_topic".
+   * @param {string} [data.title] - Title of the topic when creating a new topic.
+   * @param {string} [data.category_id] - ID of the category used when creating a new topic.
+   * @param {Array.<string>} [data.tags] - tags used when creating a new topic.
+   * @param {string} [data.topic_id] - ID of the topic when using an existing topic.
+   * @returns {Promise}
+   */
   createChannelArchive(channelId, data = {}) {
     return this.#postRequest(`/channels/${channelId}/archives`, {
       archive: data,
     });
   }
 
+  /**
+   * Updates a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @param {object} data - Params of the archive.
+   * @param {string} [data.description] - Description of the channel.
+   * @param {string} [data.name] - Name of the channel.
+   * @returns {Promise}
+   */
   updateChannel(channelId, data = {}) {
     return this.#putRequest(`/channels/${channelId}`, { channel: data });
   }
 
+  /**
+   * Updates the status of a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @param {string} status - The new status, can be "open" or "closed".
+   * @returns {Promise}
+   */
   updateChannelStatus(channelId, status) {
     return this.#putRequest(`/channels/${channelId}/status`, { status });
   }
 
+  /**
+   * Lists members of a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @returns {module:Collection}
+   */
   listChannelMemberships(channelId) {
     return new Collection(
       `${this.#basePath}/channels/${channelId}/memberships`,
@@ -179,27 +187,50 @@ export default class ChatApi extends Service {
     );
   }
 
+  /**
+   * Lists public and direct message channels of the current user.
+   * @returns {Promise}
+   */
   listCurrentUserChannels() {
-    return this.#getRequest(`/channels/me`).then((result) => {
+    return this.#getRequest("/channels/me").then((result) => {
       return (result?.channels || []).map((channel) =>
         this.chatChannelsManager.store(channel)
       );
     });
   }
 
+  /**
+   * Makes current user follow a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @returns {Promise}
+   */
   followChannel(channelId) {
     return this.#postRequest(`/channels/${channelId}/memberships/me`).then(
       (result) => UserChatChannelMembership.create(result.membership)
     );
   }
 
+  /**
+   * Makes current user unfollow a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @returns {Promise}
+   */
   unfollowChannel(channelId) {
     return this.#deleteRequest(`/channels/${channelId}/memberships/me`).then(
       (result) => UserChatChannelMembership.create(result.membership)
     );
   }
 
-  updateCurrentUserChatChannelNotificationsSettings(channelId, data = {}) {
+  /**
+   * Update notifications settings of current user for a channel.
+   * @param {number} channelId - The ID of the channel.
+   * @param {object} data - The settings to modify.
+   * @param {boolean} [data.muted] - Mutes the channel.
+   * @param {string} [data.desktop_notification_level] - Notifications level on desktop: never, mention or always.
+   * @param {string} [data.mobile_notification_level] - Notifications level on mobile: never, mention or always.
+   * @returns {Promise}
+   */
+  updateCurrentUserChannelNotificationsSettings(channelId, data = {}) {
     return this.#putRequest(
       `/channels/${channelId}/notifications-settings/me`,
       { notifications_settings: data }
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js
index 1a73752a07c..9d2cae55392 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-channels-manager.js
@@ -117,7 +117,7 @@ export default class ChatChannelsManager extends Service {
 
   async #find(id) {
     return this.chatApi
-      .getChannel(id)
+      .channel(id)
       .catch(popupAjaxError)
       .then((channel) => {
         this.#cache(channel);
diff --git a/plugins/chat/docs/FRONTEND.md b/plugins/chat/docs/FRONTEND.md
new file mode 100644
index 00000000000..8d81aafc507
--- /dev/null
+++ b/plugins/chat/docs/FRONTEND.md
@@ -0,0 +1,352 @@
+## Modules
+
+<dl>
+<dt><a href="#module_Collection">Collection</a></dt>
+<dd></dd>
+<dt><a href="#module_ChatApi">ChatApi</a></dt>
+<dd></dd>
+</dl>
+
+<a name="module_Collection"></a>
+
+## Collection
+
+* [Collection](#module_Collection)
+    * [module.exports](#exp_module_Collection--module.exports) ⏏
+        * [new module.exports(resourceURL, handler)](#new_module_Collection--module.exports_new)
+        * [.load()](#module_Collection--module.exports+load) ⇒ <code>Promise</code>
+        * [.loadMore()](#module_Collection--module.exports+loadMore) ⇒ <code>Promise</code>
+
+
+* * *
+
+<a name="exp_module_Collection--module.exports"></a>
+
+### module.exports ⏏
+Handles a paginated API response.
+
+**Kind**: Exported class  
+
+* * *
+
+<a name="new_module_Collection--module.exports_new"></a>
+
+#### new module.exports(resourceURL, handler)
+Create a Collection instance
+
+
+| Param | Type | Description |
+| --- | --- | --- |
+| resourceURL | <code>string</code> | the API endpoint to call |
+| handler | <code>callback</code> | anonymous function used to handle the response |
+
+
+* * *
+
+<a name="module_Collection--module.exports+load"></a>
+
+#### module.exports.load() ⇒ <code>Promise</code>
+Loads first batch of results
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_Collection--module.exports)  
+
+* * *
+
+<a name="module_Collection--module.exports+loadMore"></a>
+
+#### module.exports.loadMore() ⇒ <code>Promise</code>
+Attempts to load more results
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_Collection--module.exports)  
+
+* * *
+
+<a name="module_ChatApi"></a>
+
+## ChatApi
+
+* [ChatApi](#module_ChatApi)
+    * [module.exports](#exp_module_ChatApi--module.exports) ⏏
+        * [.channel(channelId)](#module_ChatApi--module.exports+channel) ⇒ <code>Promise</code>
+        * [.channels()](#module_ChatApi--module.exports+channels) ⇒ [<code>module.exports</code>](#exp_module_Collection--module.exports)
+        * [.moveChannelMessages(channelId, data)](#module_ChatApi--module.exports+moveChannelMessages) ⇒ <code>Promise</code>
+        * [.destroyChannel(channelId, channelName)](#module_ChatApi--module.exports+destroyChannel) ⇒ <code>Promise</code>
+        * [.createChannel(data)](#module_ChatApi--module.exports+createChannel) ⇒ <code>Promise</code>
+        * [.categoryPermissions(categoryId)](#module_ChatApi--module.exports+categoryPermissions) ⇒ <code>Promise</code>
+        * [.sendMessage(channelId, data)](#module_ChatApi--module.exports+sendMessage) ⇒ <code>Promise</code>
+        * [.createChannelArchive(channelId, data)](#module_ChatApi--module.exports+createChannelArchive) ⇒ <code>Promise</code>
+        * [.updateChannel(channelId, data)](#module_ChatApi--module.exports+updateChannel) ⇒ <code>Promise</code>
+        * [.updateChannelStatus(channelId, status)](#module_ChatApi--module.exports+updateChannelStatus) ⇒ <code>Promise</code>
+        * [.listChannelMemberships(channelId)](#module_ChatApi--module.exports+listChannelMemberships) ⇒ [<code>module.exports</code>](#exp_module_Collection--module.exports)
+        * [.listCurrentUserChannels()](#module_ChatApi--module.exports+listCurrentUserChannels) ⇒ <code>Promise</code>
+        * [.followChannel(channelId)](#module_ChatApi--module.exports+followChannel) ⇒ <code>Promise</code>
+        * [.unfollowChannel(channelId)](#module_ChatApi--module.exports+unfollowChannel) ⇒ <code>Promise</code>
+        * [.updateCurrentUserChannelNotificationsSettings(channelId, data)](#module_ChatApi--module.exports+updateCurrentUserChannelNotificationsSettings) ⇒ <code>Promise</code>
+
+
+* * *
+
+<a name="exp_module_ChatApi--module.exports"></a>
+
+### module.exports ⏏
+Chat API service. Provides methods to interact with the chat API.
+
+**Kind**: Exported class  
+**Implements**: <code>{@ember/service}</code>  
+
+* * *
+
+<a name="module_ChatApi--module.exports+channel"></a>
+
+#### module.exports.channel(channelId) ⇒ <code>Promise</code>
+Get a channel by its ID.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| channelId | <code>number</code> | The ID of the channel. |
+
+**Example**  
+```js
+this.chatApi.channel(1).then(channel => { ... })
+```
+
+* * *
+
+<a name="module_ChatApi--module.exports+channels"></a>
+
+#### module.exports.channels() ⇒ [<code>module.exports</code>](#exp_module_Collection--module.exports)
+List all accessible category channels of the current user.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+**Example**  
+```js
+this.chatApi.channels.then(channels => { ... })
+```
+
+* * *
+
+<a name="module_ChatApi--module.exports+moveChannelMessages"></a>
+
+#### module.exports.moveChannelMessages(channelId, data) ⇒ <code>Promise</code>
+Moves messages from one channel to another.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| channelId | <code>number</code> | The ID of the original channel. |
+| data | <code>object</code> | Params of the move. |
+| data.message_ids | <code>Array.&lt;number&gt;</code> | IDs of the moved messages. |
+| data.destination_channel_id | <code>number</code> | ID of the channel where the messages are moved to. |
+
+**Example**  
+```js
+this.chatApi
+    .moveChannelMessages(1, {
+      message_ids: [2, 3],
+      destination_channel_id: 4,
+    }).then(() => { ... })
+```
+
+* * *
+
+<a name="module_ChatApi--module.exports+destroyChannel"></a>
+
+#### module.exports.destroyChannel(channelId, channelName) ⇒ <code>Promise</code>
+Destroys a channel.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| channelId | <code>number</code> | The ID of the channel. |
+| channelName | <code>string</code> | The name of the channel to be destroyed, used as confirmation. |
+
+**Example**  
+```js
+this.chatApi.destroyChannel(1, "foo").then(() => { ... })
+```
+
+* * *
+
+<a name="module_ChatApi--module.exports+createChannel"></a>
+
+#### module.exports.createChannel(data) ⇒ <code>Promise</code>
+Creates a channel.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| data | <code>object</code> | Params of the channel. |
+| data.name | <code>string</code> | The name of the channel. |
+| data.chatable_id | <code>string</code> | The category of the channel. |
+| data.description | <code>string</code> | The description of the channel. |
+| [data.auto_join_users] | <code>boolean</code> | Should users join this channel automatically. |
+
+**Example**  
+```js
+this.chatApi
+     .createChannel({ name: "foo", chatable_id: 1, description "bar" })
+     .then((channel) => { ... })
+```
+
+* * *
+
+<a name="module_ChatApi--module.exports+categoryPermissions"></a>
+
+#### module.exports.categoryPermissions(categoryId) ⇒ <code>Promise</code>
+Lists chat permissions for a category.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| categoryId | <code>number</code> | ID of the category. |
+
+
+* * *
+
+<a name="module_ChatApi--module.exports+sendMessage"></a>
+
+#### module.exports.sendMessage(channelId, data) ⇒ <code>Promise</code>
+Sends a message.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| channelId | <code>number</code> | ID of the channel. |
+| data | <code>object</code> | Params of the message. |
+| data.message | <code>string</code> | The raw content of the message in markdown. |
+| data.cooked | <code>string</code> | The cooked content of the message. |
+| [data.in_reply_to_id] | <code>number</code> | The ID of the replied-to message. |
+| [data.staged_id] | <code>number</code> | The staged ID of the message before it was persisted. |
+| [data.upload_ids] | <code>Array.&lt;number&gt;</code> | Array of upload ids linked to the message. |
+
+
+* * *
+
+<a name="module_ChatApi--module.exports+createChannelArchive"></a>
+
+#### module.exports.createChannelArchive(channelId, data) ⇒ <code>Promise</code>
+Creates a channel archive.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| channelId | <code>number</code> | The ID of the channel. |
+| data | <code>object</code> | Params of the archive. |
+| data.selection | <code>string</code> | "new_topic" or "existing_topic". |
+| [data.title] | <code>string</code> | Title of the topic when creating a new topic. |
+| [data.category_id] | <code>string</code> | ID of the category used when creating a new topic. |
+| [data.tags] | <code>Array.&lt;string&gt;</code> | tags used when creating a new topic. |
+| [data.topic_id] | <code>string</code> | ID of the topic when using an existing topic. |
+
+
+* * *
+
+<a name="module_ChatApi--module.exports+updateChannel"></a>
+
+#### module.exports.updateChannel(channelId, data) ⇒ <code>Promise</code>
+Updates a channel.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| channelId | <code>number</code> | The ID of the channel. |
+| data | <code>object</code> | Params of the archive. |
+| [data.description] | <code>string</code> | Description of the channel. |
+| [data.name] | <code>string</code> | Name of the channel. |
+
+
+* * *
+
+<a name="module_ChatApi--module.exports+updateChannelStatus"></a>
+
+#### module.exports.updateChannelStatus(channelId, status) ⇒ <code>Promise</code>
+Updates the status of a channel.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| channelId | <code>number</code> | The ID of the channel. |
+| status | <code>string</code> | The new status, can be "open" or "closed". |
+
+
+* * *
+
+<a name="module_ChatApi--module.exports+listChannelMemberships"></a>
+
+#### module.exports.listChannelMemberships(channelId) ⇒ [<code>module.exports</code>](#exp_module_Collection--module.exports)
+Lists members of a channel.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| channelId | <code>number</code> | The ID of the channel. |
+
+
+* * *
+
+<a name="module_ChatApi--module.exports+listCurrentUserChannels"></a>
+
+#### module.exports.listCurrentUserChannels() ⇒ <code>Promise</code>
+Lists public and direct message channels of the current user.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+* * *
+
+<a name="module_ChatApi--module.exports+followChannel"></a>
+
+#### module.exports.followChannel(channelId) ⇒ <code>Promise</code>
+Makes current user follow a channel.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| channelId | <code>number</code> | The ID of the channel. |
+
+
+* * *
+
+<a name="module_ChatApi--module.exports+unfollowChannel"></a>
+
+#### module.exports.unfollowChannel(channelId) ⇒ <code>Promise</code>
+Makes current user unfollow a channel.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| channelId | <code>number</code> | The ID of the channel. |
+
+
+* * *
+
+<a name="module_ChatApi--module.exports+updateCurrentUserChannelNotificationsSettings"></a>
+
+#### module.exports.updateCurrentUserChannelNotificationsSettings(channelId, data) ⇒ <code>Promise</code>
+Update notifications settings of current user for a channel.
+
+**Kind**: instance method of [<code>module.exports</code>](#exp_module_ChatApi--module.exports)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| channelId | <code>number</code> | The ID of the channel. |
+| data | <code>object</code> | The settings to modify. |
+| [data.muted] | <code>boolean</code> | Mutes the channel. |
+| [data.desktop_notification_level] | <code>string</code> | Notifications level on desktop: never, mention or always. |
+| [data.mobile_notification_level] | <code>string</code> | Notifications level on mobile: never, mention or always. |
+
+
+* * *
+
diff --git a/plugins/chat/lib/tasks/chat_doc.rake b/plugins/chat/lib/tasks/chat_doc.rake
new file mode 100644
index 00000000000..98fb9553e3b
--- /dev/null
+++ b/plugins/chat/lib/tasks/chat_doc.rake
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+task "chat:doc" do
+  destination = File.join(Rails.root, "plugins/chat/docs/FRONTEND.md")
+  config = File.join(Rails.root, ".jsdoc")
+
+  files = %w[
+    plugins/chat/assets/javascripts/discourse/lib/collection.js
+    plugins/chat/assets/javascripts/discourse/services/chat-api.js
+  ]
+
+  `yarn --silent jsdoc2md --separators -c #{config} -f #{files.join(" ")} > #{destination}`
+end
diff --git a/yarn.lock b/yarn.lock
index 40febf39c5b..402409e17cc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -209,6 +209,11 @@
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.6.tgz#845338edecad65ebffef058d3be851f1d28a63bc"
   integrity sha512-uQVSa9jJUe/G/304lXspfWVpKpK4euFLgGiMQFOCpM/bgcAdeoHwi/OQz23O9GK2osz26ZiXRRV9aV+Yl1O8tw==
 
+"@babel/parser@^7.9.4":
+  version "7.20.7"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.7.tgz#66fe23b3c8569220817d5feb8b9dcdc95bb4f71b"
+  integrity sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==
+
 "@babel/plugin-proposal-decorators@^7.18.2":
   version "7.18.6"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.18.6.tgz#68e9fd0f022b944f84a8824bb28bfaee724d2595"
@@ -411,6 +416,13 @@
     "@jridgewell/resolve-uri" "^3.0.3"
     "@jridgewell/sourcemap-codec" "^1.4.10"
 
+"@jsdoc/salty@^0.2.1":
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/@jsdoc/salty/-/salty-0.2.2.tgz#567017ddda2048c5ff921aeffd38564a0578fdca"
+  integrity sha512-A1FrVnc7L9qI2gUGsfN0trTiJNK72Y0CL/VAyrmYEmeKI3pnHDawP64CEev31XLyAAOx2xmDo3tbadPxC0CSbw==
+  dependencies:
+    lodash "^4.17.21"
+
 "@json-editor/json-editor@^2.6.1":
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/@json-editor/json-editor/-/json-editor-2.6.1.tgz#169e8b88305d71ccac391c3ae22d4145bc63c9f7"
@@ -493,6 +505,24 @@
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
   integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
 
+"@types/linkify-it@*":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9"
+  integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==
+
+"@types/markdown-it@^12.2.3":
+  version "12.2.3"
+  resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.2.3.tgz#0d6f6e5e413f8daaa26522904597be3d6cd93b51"
+  integrity sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==
+  dependencies:
+    "@types/linkify-it" "*"
+    "@types/mdurl" "*"
+
+"@types/mdurl@*":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
+  integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
+
 "@types/node@*":
   version "14.11.2"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256"
@@ -537,6 +567,13 @@ ajv@^6.10.0, ajv@^6.12.4:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
+ansi-escape-sequences@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/ansi-escape-sequences/-/ansi-escape-sequences-4.1.0.tgz#2483c8773f50dd9174dd9557e92b1718f1816097"
+  integrity sha512-dzW9kHxH011uBsidTXd14JXgzye/YLb2LzeKZ4bsgl/Knwx8AtbSFkkGxagdNOoh0DlqHCmfiEjWKBaqjOanVw==
+  dependencies:
+    array-back "^3.0.1"
+
 ansi-regex@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
@@ -567,6 +604,40 @@ aria-query@^5.0.0:
   resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c"
   integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==
 
+array-back@^1.0.2, array-back@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/array-back/-/array-back-1.0.4.tgz#644ba7f095f7ffcf7c43b5f0dc39d3c1f03c063b"
+  integrity sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw==
+  dependencies:
+    typical "^2.6.0"
+
+array-back@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/array-back/-/array-back-2.0.0.tgz#6877471d51ecc9c9bfa6136fb6c7d5fe69748022"
+  integrity sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==
+  dependencies:
+    typical "^2.6.1"
+
+array-back@^3.0.1, array-back@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0"
+  integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==
+
+array-back@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e"
+  integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==
+
+array-back@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/array-back/-/array-back-5.0.0.tgz#e196609edcec48376236d163958df76e659a0d36"
+  integrity sha512-kgVWwJReZWmVuWOQKEOohXKJX+nD02JAZ54D1RRWlv8L0NebauKAaFxACKzB74RTclt1+WNz5KHaLRDAPZbDEw==
+
+array-back@^6.2.2:
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/array-back/-/array-back-6.2.2.tgz#f567d99e9af88a6d3d2f9dfcc21db6f9ba9fd157"
+  integrity sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==
+
 array-union@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
@@ -611,6 +682,11 @@ bl@^4.0.3, bl@^4.1.0:
     inherits "^2.0.4"
     readable-stream "^3.4.0"
 
+bluebird@^3.7.2:
+  version "3.7.2"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
 brace-expansion@^1.1.7:
   version "1.1.11"
   resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -649,6 +725,15 @@ buffer@^5.2.1, buffer@^5.5.0:
     base64-js "^1.3.1"
     ieee754 "^1.1.13"
 
+cache-point@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/cache-point/-/cache-point-2.0.0.tgz#91e03c38da9cfba9d95ac6a34d24cfe6eff8920f"
+  integrity sha512-4gkeHlFpSKgm3vm2gJN5sPqfmijYRFYCQ6tv5cLw0xVmT6r1z1vd4FNnpuOREco3cBs1G709sZ72LdgddKvL5w==
+  dependencies:
+    array-back "^4.0.1"
+    fs-then-native "^2.0.0"
+    mkdirp2 "^1.0.4"
+
 callsites@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@@ -659,6 +744,13 @@ caniuse-lite@^1.0.30001359:
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001363.tgz#26bec2d606924ba318235944e1193304ea7c4f15"
   integrity sha512-HpQhpzTGGPVMnCjIomjt+jvyUu8vNFo3TaDiZ/RcoTrlOq/5+tC8zHdsbgFB6MxmaY+jCpsH09aD80Bb4Ow3Sg==
 
+catharsis@^0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.9.0.tgz#40382a168be0e6da308c277d3a2b3eb40c7d2121"
+  integrity sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==
+  dependencies:
+    lodash "^4.17.15"
+
 chalk@^2.0.0:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@@ -740,6 +832,14 @@ clone@^1.0.2:
   resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
   integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
 
+collect-all@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/collect-all/-/collect-all-1.0.4.tgz#50cd7119ac24b8e12a661f0f8c3aa0ea7222ddfc"
+  integrity sha512-RKZhRwJtJEP5FWul+gkSMEnaK6H3AGPTTWOiRimCcs+rc/OmQE3Yhy1Q7A7KsdkG3ZXVdZq68Y6ONSdvkeEcKA==
+  dependencies:
+    stream-connect "^1.0.2"
+    stream-via "^1.0.4"
+
 color-convert@^1.9.0:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -769,6 +869,37 @@ colors@^1.4.0:
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
   integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
 
+command-line-args@^5.0.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e"
+  integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==
+  dependencies:
+    array-back "^3.1.0"
+    find-replace "^3.0.0"
+    lodash.camelcase "^4.3.0"
+    typical "^4.0.0"
+
+command-line-tool@^0.8.0:
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/command-line-tool/-/command-line-tool-0.8.0.tgz#b00290ef1dfc11cc731dd1f43a92cfa5f21e715b"
+  integrity sha512-Xw18HVx/QzQV3Sc5k1vy3kgtOeGmsKIqwtFFoyjI4bbcpSgnw2CWVULvtakyw4s6fhyAdI6soQQhXc2OzJy62g==
+  dependencies:
+    ansi-escape-sequences "^4.0.0"
+    array-back "^2.0.0"
+    command-line-args "^5.0.0"
+    command-line-usage "^4.1.0"
+    typical "^2.6.1"
+
+command-line-usage@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-4.1.0.tgz#a6b3b2e2703b4dcf8bd46ae19e118a9a52972882"
+  integrity sha512-MxS8Ad995KpdAC0Jopo/ovGIroV/m0KHwzKfXxKag6FHOkGsH8/lv5yjgablcRxCJJC0oJeUMuO/gmaq+Wq46g==
+  dependencies:
+    ansi-escape-sequences "^4.0.0"
+    array-back "^2.0.0"
+    table-layout "^0.4.2"
+    typical "^2.6.1"
+
 commander@2.11.x:
   version "2.11.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
@@ -784,11 +915,23 @@ commander@^8.3.0:
   resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
   integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
 
+common-sequence@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/common-sequence/-/common-sequence-2.0.2.tgz#accc76bdc5876a1fcd92b73484d4285fff99d838"
+  integrity sha512-jAg09gkdkrDO9EWTdXfv80WWH3yeZl5oT69fGfedBNS9pXUKYInVJ1bJ+/ht2+Moeei48TmSbQDYMc8EOx9G0g==
+
 concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
   integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
 
+config-master@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/config-master/-/config-master-3.1.0.tgz#667663590505a283bf26a484d68489d74c5485da"
+  integrity sha512-n7LBL1zBzYdTpF1mx5DNcZnZn05CWIdsdvtPL4MosvqbBUK3Rq6VWEtGUuF3Y0s9/CIhMejezqlSkP6TnCJ/9g==
+  dependencies:
+    walk-back "^2.0.1"
+
 convert-source-map@^1.7.0:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
@@ -849,6 +992,11 @@ debug@^2.6.8:
   dependencies:
     ms "2.0.0"
 
+deep-extend@~0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+  integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
 deep-is@^0.1.3:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
@@ -878,6 +1026,24 @@ dir-glob@^3.0.1:
   dependencies:
     path-type "^4.0.0"
 
+dmd@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/dmd/-/dmd-6.2.0.tgz#d267a9fb1ce62b74edca8bf5bcbd3b8e08574fe7"
+  integrity sha512-uXWxLF1H7TkUAuoHK59/h/ts5cKavm2LnhrIgJWisip4BVzPoXavlwyoprFFn2CzcahKYgvkfaebS6oxzgflkg==
+  dependencies:
+    array-back "^6.2.2"
+    cache-point "^2.0.0"
+    common-sequence "^2.0.2"
+    file-set "^4.0.2"
+    handlebars "^4.7.7"
+    marked "^4.2.3"
+    object-get "^2.1.1"
+    reduce-flatten "^3.0.1"
+    reduce-unique "^2.0.1"
+    reduce-without "^1.0.1"
+    test-value "^3.0.0"
+    walk-back "^5.1.0"
+
 doctrine@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
@@ -969,6 +1135,11 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1:
   dependencies:
     once "^1.4.0"
 
+entities@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
+  integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
+
 escalade@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -979,6 +1150,11 @@ escape-string-regexp@^1.0.5:
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
+escape-string-regexp@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
+  integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
+
 escape-string-regexp@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
@@ -1240,6 +1416,14 @@ file-entry-cache@^6.0.1:
   dependencies:
     flat-cache "^3.0.4"
 
+file-set@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/file-set/-/file-set-4.0.2.tgz#8d67c92a864202c2085ac9f03f1c9909c7e27030"
+  integrity sha512-fuxEgzk4L8waGXaAkd8cMr73Pm0FxOVkn8hztzUW7BAHhOGH90viQNXbiOsnecCWmfInqU6YmAMwxRMdKETceQ==
+  dependencies:
+    array-back "^5.0.0"
+    glob "^7.1.6"
+
 fill-range@^7.0.1:
   version "7.0.1"
   resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -1247,6 +1431,13 @@ fill-range@^7.0.1:
   dependencies:
     to-regex-range "^5.0.1"
 
+find-replace@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38"
+  integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==
+  dependencies:
+    array-back "^3.0.1"
+
 find-up@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
@@ -1299,6 +1490,11 @@ fs-extra@^9.1.0:
     jsonfile "^6.0.1"
     universalify "^2.0.0"
 
+fs-then-native@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/fs-then-native/-/fs-then-native-2.0.0.tgz#19a124d94d90c22c8e045f2e8dd6ebea36d48c67"
+  integrity sha512-X712jAOaWXkemQCAmWeg5rOT2i+KOpWz1Z/txk/cW0qlOu2oQ9H61vc5w3X/iyuUEfq/OyaFJ78/cZAQD1/bgA==
+
 fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -1371,7 +1567,7 @@ glob-stream@^7.0.0:
     to-absolute-glob "^2.0.2"
     unique-stream "^2.3.1"
 
-glob@^7.1.3, glob@^7.2.0:
+glob@^7.1.3, glob@^7.1.6, glob@^7.2.0:
   version "7.2.3"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
   integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@@ -1418,11 +1614,23 @@ globby@^13.1.2:
     merge2 "^1.4.1"
     slash "^4.0.0"
 
-graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4:
+graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4:
   version "4.2.10"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
   integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
 
+handlebars@^4.7.7:
+  version "4.7.7"
+  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
+  integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==
+  dependencies:
+    minimist "^1.2.5"
+    neo-async "^2.6.0"
+    source-map "^0.6.1"
+    wordwrap "^1.0.0"
+  optionalDependencies:
+    uglify-js "^3.1.4"
+
 has-flag@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -1589,6 +1797,74 @@ js-yaml@^4.1.0:
   dependencies:
     argparse "^2.0.1"
 
+js2xmlparser@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-4.0.2.tgz#2a1fdf01e90585ef2ae872a01bc169c6a8d5e60a"
+  integrity sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==
+  dependencies:
+    xmlcreate "^2.0.4"
+
+jsdoc-api@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/jsdoc-api/-/jsdoc-api-8.0.0.tgz#4b2c25ff60f91b80da51b6cd33943acc7b2cab74"
+  integrity sha512-Rnhor0suB1Ds1abjmFkFfKeD+kSMRN9oHMTMZoJVUrmtCGDwXty+sWMA9sa4xbe4UyxuPjhC7tavZ40mDKK6QQ==
+  dependencies:
+    array-back "^6.2.2"
+    cache-point "^2.0.0"
+    collect-all "^1.0.4"
+    file-set "^4.0.2"
+    fs-then-native "^2.0.0"
+    jsdoc "^4.0.0"
+    object-to-spawn-args "^2.0.1"
+    temp-path "^1.0.0"
+    walk-back "^5.1.0"
+
+jsdoc-parse@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/jsdoc-parse/-/jsdoc-parse-6.2.0.tgz#2b71d3925acfc4badc72526f2470766e0561f6b5"
+  integrity sha512-Afu1fQBEb7QHt6QWX/6eUWvYHJofB90Fjx7FuJYF7mnG9z5BkAIpms1wsnvYLytfmqpEENHs/fax9p8gvMj7dw==
+  dependencies:
+    array-back "^6.2.2"
+    lodash.omit "^4.5.0"
+    lodash.pick "^4.4.0"
+    reduce-extract "^1.0.0"
+    sort-array "^4.1.5"
+    test-value "^3.0.0"
+
+jsdoc-to-markdown@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/jsdoc-to-markdown/-/jsdoc-to-markdown-8.0.0.tgz#27f32ed200d3b84dbf22a49beed485790f93b3ce"
+  integrity sha512-2FQvYkg491+FP6s15eFlgSSWs69CvQrpbABGYBtvAvGWy/lWo8IKKToarT283w59rQFrpcjHl3YdhHCa3l7gXg==
+  dependencies:
+    array-back "^6.2.2"
+    command-line-tool "^0.8.0"
+    config-master "^3.1.0"
+    dmd "^6.2.0"
+    jsdoc-api "^8.0.0"
+    jsdoc-parse "^6.2.0"
+    walk-back "^5.1.0"
+
+jsdoc@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-4.0.0.tgz#9569f79ea5b14ba4bc726da1a48fe6a241ad7893"
+  integrity sha512-tzTgkklbWKrlaQL2+e3NNgLcZu3NaK2vsHRx7tyHQ+H5jcB9Gx0txSd2eJWlMC/xU1+7LQu4s58Ry0RkuaEQVg==
+  dependencies:
+    "@babel/parser" "^7.9.4"
+    "@jsdoc/salty" "^0.2.1"
+    "@types/markdown-it" "^12.2.3"
+    bluebird "^3.7.2"
+    catharsis "^0.9.0"
+    escape-string-regexp "^2.0.0"
+    js2xmlparser "^4.0.2"
+    klaw "^3.0.0"
+    markdown-it "^12.3.2"
+    markdown-it-anchor "^8.4.1"
+    marked "^4.0.10"
+    mkdirp "^1.0.4"
+    requizzle "^0.2.3"
+    strip-json-comments "^3.1.0"
+    underscore "~1.13.2"
+
 jsesc@^2.5.1:
   version "2.5.2"
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
@@ -1618,6 +1894,13 @@ jsonfile@^6.0.1:
   optionalDependencies:
     graceful-fs "^4.1.6"
 
+klaw@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/klaw/-/klaw-3.0.0.tgz#b11bec9cf2492f06756d6e809ab73a2910259146"
+  integrity sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==
+  dependencies:
+    graceful-fs "^4.1.9"
+
 language-subtag-registry@~0.3.2:
   version "0.3.22"
   resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d"
@@ -1688,6 +1971,13 @@ lighthouse-logger@^1.0.0:
     debug "^2.6.8"
     marky "^1.2.0"
 
+linkify-it@^3.0.1:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e"
+  integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==
+  dependencies:
+    uc.micro "^1.0.1"
+
 locate-path@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
@@ -1709,6 +1999,11 @@ locate-path@^7.1.0:
   dependencies:
     p-locate "^6.0.0"
 
+lodash.camelcase@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
+  integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==
+
 lodash.kebabcase@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
@@ -1719,7 +2014,22 @@ lodash.merge@^4.6.2:
   resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
   integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
 
-lodash@^4.17.14:
+lodash.omit@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60"
+  integrity sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==
+
+lodash.padend@^4.6.1:
+  version "4.6.1"
+  resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e"
+  integrity sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw==
+
+lodash.pick@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
+  integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==
+
+lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -1744,6 +2054,27 @@ magnific-popup@1.1.0:
   resolved "https://registry.yarnpkg.com/magnific-popup/-/magnific-popup-1.1.0.tgz#3e7362c5bd18f6785fe99e59d013e20af33d3049"
   integrity sha1-PnNixb0Y9nhf6Z5Z0BPiCvM9MEk=
 
+markdown-it-anchor@^8.4.1:
+  version "8.6.6"
+  resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-8.6.6.tgz#4a12e358c9c2167ee28cb7a5f10e29d6f1ffd7ca"
+  integrity sha512-jRW30YGywD2ESXDc+l17AiritL0uVaSnWsb26f+68qaW9zgbIIr1f4v2Nsvc0+s0Z2N3uX6t/yAw7BwCQ1wMsA==
+
+markdown-it@^12.3.2:
+  version "12.3.2"
+  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90"
+  integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==
+  dependencies:
+    argparse "^2.0.1"
+    entities "~2.1.0"
+    linkify-it "^3.0.1"
+    mdurl "^1.0.1"
+    uc.micro "^1.0.5"
+
+marked@^4.0.10, marked@^4.2.3:
+  version "4.2.5"
+  resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.5.tgz#979813dfc1252cc123a79b71b095759a32f42a5d"
+  integrity sha512-jPueVhumq7idETHkb203WDD4fMA3yV9emQ5vLwop58lu8bTclMghBWcYAavlDqIEMaisADinV1TooIFCfqOsYQ==
+
 marky@^1.2.0:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.1.tgz#a3fcf82ffd357756b8b8affec9fdbf3a30dc1b02"
@@ -1754,6 +2085,11 @@ mdn-data@2.0.27:
   resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.27.tgz#1710baa7b0db8176d3b3d565ccb7915fc69525ab"
   integrity sha512-kwqO0I0jtWr25KcfLm9pia8vLZ8qoAKhWZuZMbneJq3jjBD3gl5nZs8l8Tu3ZBlBAHVQtDur9rdDGyvtfVraHQ==
 
+mdurl@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
+  integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==
+
 merge2@^1.3.0, merge2@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
@@ -1779,11 +2115,26 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
   dependencies:
     brace-expansion "^1.1.7"
 
+minimist@^1.2.5:
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
+  integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
+
 mkdirp-classic@^0.5.2:
   version "0.5.3"
   resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
   integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
 
+mkdirp2@^1.0.4:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/mkdirp2/-/mkdirp2-1.0.5.tgz#68bbe61defefafce4b48948608ec0bac942512c2"
+  integrity sha512-xOE9xbICroUDmG1ye2h4bZ8WBie9EGmACaco8K8cx6RlkJJrxGIqjGqztAI+NMhexXBcdGbSEzI6N3EJPevxZw==
+
+mkdirp@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
+  integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+
 moment-timezone@0.5.39:
   version "0.5.39"
   resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.39.tgz#342625a3b98810f04c8f4ea917e448d3525e600b"
@@ -1811,6 +2162,11 @@ natural-compare@^1.4.0:
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
   integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
 
+neo-async@^2.6.0:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+  integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
 no-case@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
@@ -1831,6 +2187,16 @@ node-releases@^2.0.5:
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666"
   integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==
 
+object-get@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/object-get/-/object-get-2.1.1.tgz#1dad63baf6d94df184d1c58756cc9be55b174dac"
+  integrity sha512-7n4IpLMzGGcLEMiQKsNR7vCe+N5E9LORFrtNUVy4sO3dj9a3HedZCxEL2T7QuLhcHN1NBuBsMOKaOsAYI9IIvg==
+
+object-to-spawn-args@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/object-to-spawn-args/-/object-to-spawn-args-2.0.1.tgz#cf8b8e3c9b3589137a469cac90391f44870144a5"
+  integrity sha512-6FuKFQ39cOID+BMZ3QaphcC8Y4cw6LXBLyIgPU+OhIYwviJamPAn+4mITapnSBQrejB+NNp+FMskhD8Cq+Ys3w==
+
 once@^1.3.0, once@^1.3.1, once@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -2086,6 +2452,35 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
     string_decoder "^1.1.1"
     util-deprecate "^1.0.1"
 
+reduce-extract@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/reduce-extract/-/reduce-extract-1.0.0.tgz#67f2385beda65061b5f5f4312662e8b080ca1525"
+  integrity sha512-QF8vjWx3wnRSL5uFMyCjDeDc5EBMiryoT9tz94VvgjKfzecHAVnqmXAwQDcr7X4JmLc2cjkjFGCVzhMqDjgR9g==
+  dependencies:
+    test-value "^1.0.1"
+
+reduce-flatten@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327"
+  integrity sha512-j5WfFJfc9CoXv/WbwVLHq74i/hdTUpy+iNC534LxczMRP67vJeK3V9JOdnL0N1cIRbn9mYhE2yVjvvKXDxvNXQ==
+
+reduce-flatten@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-3.0.1.tgz#3db6b48ced1f4dbe4f4f5e31e422aa9ff0cd21ba"
+  integrity sha512-bYo+97BmUUOzg09XwfkwALt4PQH1M5L0wzKerBt6WLm3Fhdd43mMS89HiT1B9pJIqko/6lWx3OnV4J9f2Kqp5Q==
+
+reduce-unique@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/reduce-unique/-/reduce-unique-2.0.1.tgz#fb34b90e89297c1e08d75dcf17e9a6443ea71081"
+  integrity sha512-x4jH/8L1eyZGR785WY+ePtyMNhycl1N2XOLxhCbzZFaqF4AXjLzqSxa2UHgJ2ZVR/HHyPOvl1L7xRnW8ye5MdA==
+
+reduce-without@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/reduce-without/-/reduce-without-1.0.1.tgz#68ad0ead11855c9a37d4e8256c15bbf87972fc8c"
+  integrity sha512-zQv5y/cf85sxvdrKPlfcRzlDn/OqKFThNimYmsS3flmkioKvkUGn2Qg9cJVoQiEvdxFGLE0MQER/9fZ9sUqdxg==
+  dependencies:
+    test-value "^2.0.0"
+
 regexpp@^3.0.0, regexpp@^3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
@@ -2111,6 +2506,13 @@ requireindex@~1.1.0:
   resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.1.0.tgz#e5404b81557ef75db6e49c5a72004893fe03e162"
   integrity sha1-5UBLgVV+91225JxacgBIk/4D4WI=
 
+requizzle@^0.2.3:
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.4.tgz#319eb658b28c370f0c20f968fa8ceab98c13d27c"
+  integrity sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==
+  dependencies:
+    lodash "^4.17.21"
+
 resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@@ -2217,22 +2619,47 @@ snake-case@^3.0.3:
     dot-case "^3.0.4"
     tslib "^2.0.3"
 
+sort-array@^4.1.5:
+  version "4.1.5"
+  resolved "https://registry.yarnpkg.com/sort-array/-/sort-array-4.1.5.tgz#64b92aaba222aec606786f4df28ae4e3e3e68313"
+  integrity sha512-Ya4peoS1fgFN42RN1REk2FgdNOeLIEMKFGJvs7VTP3OklF8+kl2SkpVliZ4tk/PurWsrWRsdNdU+tgyOBkB9sA==
+  dependencies:
+    array-back "^5.0.0"
+    typical "^6.0.1"
+
 source-map-js@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
   integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
 
+source-map@^0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
 squoosh@discourse/squoosh#dc9649d:
   version "2.0.0"
   resolved "https://codeload.github.com/discourse/squoosh/tar.gz/dc9649d0a4d396d1251c22291b17d99f1716da44"
   dependencies:
     wasm-feature-detect "^1.2.11"
 
+stream-connect@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/stream-connect/-/stream-connect-1.0.2.tgz#18bc81f2edb35b8b5d9a8009200a985314428a97"
+  integrity sha512-68Kl+79cE0RGKemKkhxTSg8+6AGrqBt+cbZAXevg2iJ6Y3zX4JhA/sZeGzLpxW9cXhmqAcE7KnJCisUmIUfnFQ==
+  dependencies:
+    array-back "^1.0.2"
+
 stream-shift@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
   integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
 
+stream-via@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/stream-via/-/stream-via-1.0.4.tgz#8dccbb0ac909328eb8bc8e2a4bd3934afdaf606c"
+  integrity sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==
+
 string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@@ -2287,6 +2714,17 @@ supports-preserve-symlinks-flag@^1.0.0:
   resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
   integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 
+table-layout@^0.4.2:
+  version "0.4.5"
+  resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.4.5.tgz#d906de6a25fa09c0c90d1d08ecd833ecedcb7378"
+  integrity sha512-zTvf0mcggrGeTe/2jJ6ECkJHAQPIYEwDoqsiqBjI24mvRmQbInK5jq33fyypaCBxX08hMkfmdOqj6haT33EqWw==
+  dependencies:
+    array-back "^2.0.0"
+    deep-extend "~0.6.0"
+    lodash.padend "^4.6.1"
+    typical "^2.6.1"
+    wordwrapjs "^3.0.0"
+
 tar-fs@2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
@@ -2308,6 +2746,35 @@ tar-stream@^2.1.4:
     inherits "^2.0.3"
     readable-stream "^3.1.1"
 
+temp-path@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/temp-path/-/temp-path-1.0.0.tgz#24b1543973ab442896d9ad367dd9cbdbfafe918b"
+  integrity sha512-TvmyH7kC6ZVTYkqCODjJIbgvu0FKiwQpZ4D1aknE7xpcDf/qEOB8KZEK5ef2pfbVoiBhNWs3yx4y+ESMtNYmlg==
+
+test-value@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/test-value/-/test-value-1.1.0.tgz#a09136f72ec043d27c893707c2b159bfad7de93f"
+  integrity sha512-wrsbRo7qP+2Je8x8DsK8ovCGyxe3sYfQwOraIY/09A2gFXU9DYKiTF14W4ki/01AEh56kMzAmlj9CaHGDDUBJA==
+  dependencies:
+    array-back "^1.0.2"
+    typical "^2.4.2"
+
+test-value@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/test-value/-/test-value-2.1.0.tgz#11da6ff670f3471a73b625ca4f3fdcf7bb748291"
+  integrity sha512-+1epbAxtKeXttkGFMTX9H42oqzOTufR1ceCF+GYA5aOmvaPq9wd4PUS8329fn2RRLGNeUkgRLnVpycjx8DsO2w==
+  dependencies:
+    array-back "^1.0.3"
+    typical "^2.6.0"
+
+test-value@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/test-value/-/test-value-3.0.0.tgz#9168c062fab11a86b8d444dd968bb4b73851ce92"
+  integrity sha512-sVACdAWcZkSU9x7AOmJo5TqE+GyNJknHaHsMrR6ZnhjVlVN9Yx6FjHrsKZ3BjIpPCT68zYesPWkakrNupwfOTQ==
+  dependencies:
+    array-back "^2.0.0"
+    typical "^2.6.1"
+
 text-table@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -2388,6 +2855,31 @@ type-fest@^0.20.2:
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
   integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
 
+typical@^2.4.2, typical@^2.6.0, typical@^2.6.1:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d"
+  integrity sha512-ofhi8kjIje6npGozTip9Fr8iecmYfEbS06i0JnIg+rh51KakryWF4+jX8lLKZVhy6N+ID45WYSFCxPOdTWCzNg==
+
+typical@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4"
+  integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==
+
+typical@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/typical/-/typical-6.0.1.tgz#89bd1a6aa5e5e96fa907fb6b7579223bff558a06"
+  integrity sha512-+g3NEp7fJLe9DPa1TArHm9QAA7YciZmWnfAqEaFrBihQ7epOv9i99rjtgb6Iz0wh3WuQDjsCTDfgRoGnmHN81A==
+
+uc.micro@^1.0.1, uc.micro@^1.0.5:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
+  integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
+
+uglify-js@^3.1.4:
+  version "3.17.4"
+  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c"
+  integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==
+
 unbzip2-stream@1.4.3:
   version "1.4.3"
   resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
@@ -2401,6 +2893,11 @@ unc-path-regex@^0.1.2:
   resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
   integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo=
 
+underscore@~1.13.2:
+  version "1.13.6"
+  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441"
+  integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==
+
 unique-stream@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac"
@@ -2444,6 +2941,16 @@ v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0:
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
   integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
 
+walk-back@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/walk-back/-/walk-back-2.0.1.tgz#554e2a9d874fac47a8cb006bf44c2f0c4998a0a4"
+  integrity sha512-Nb6GvBR8UWX1D+Le+xUq0+Q1kFmRBIWVrfLnQAOmcpEzA9oAxwJ9gIr36t9TWYfzvWRvuMtjHiVsJYEkXWaTAQ==
+
+walk-back@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/walk-back/-/walk-back-5.1.0.tgz#486d6f29e67f56ab89b952d987028bbb1a4e956c"
+  integrity sha512-Uhxps5yZcVNbLEAnb+xaEEMdgTXl9qAQDzKYejG2AZ7qPwRQ81lozY9ECDbjLPNWm7YsO1IK5rsP1KoQzXAcGA==
+
 wasm-feature-detect@^1.2.11:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/wasm-feature-detect/-/wasm-feature-detect-1.3.0.tgz#fb3fc5dd4a1ba950a429be843daad67fe048bc42"
@@ -2481,6 +2988,19 @@ word-wrap@^1.2.3:
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
   integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
 
+wordwrap@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+  integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
+
+wordwrapjs@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-3.0.0.tgz#c94c372894cadc6feb1a66bff64e1d9af92c5d1e"
+  integrity sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw==
+  dependencies:
+    reduce-flatten "^1.0.1"
+    typical "^2.6.1"
+
 workbox-cacheable-response@^4.3.1:
   version "4.3.1"
   resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-4.3.1.tgz#f53e079179c095a3f19e5313b284975c91428c91"
@@ -2548,6 +3068,11 @@ ws@^7.2.0:
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67"
   integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==
 
+xmlcreate@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-2.0.4.tgz#0c5ab0f99cdd02a81065fa9cd8f8ae87624889be"
+  integrity sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==
+
 xtend@~4.0.0, xtend@~4.0.1:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"