Full Code of crisp-oss/chappe for AI

master a269f1e3a96c cached
102 files
424.3 KB
107.3k tokens
59 symbols
1 requests
Download .txt
Showing preview only (454K chars total). Download the full file or copy to clipboard to get everything.
Repository: crisp-oss/chappe
Branch: master
Commit: a269f1e3a96c
Files: 102
Total size: 424.3 KB

Directory structure:
gitextract_j_mdcei_/

├── .babelrc
├── .banner
├── .bowerrc
├── .github/
│   └── workflows/
│       ├── build.yml
│       └── test.yml
├── .gitignore
├── .jscsrc
├── .jshintrc
├── .npmignore
├── .pug-lintrc
├── .stylelintrc.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin/
│   └── chappe.js
├── bower.json
├── examples/
│   └── acme-docs/
│       ├── config.json
│       ├── data/
│       │   ├── changes/
│       │   │   ├── 2020.json
│       │   │   └── 2021.json
│       │   ├── guides/
│       │   │   ├── hello-world/
│       │   │   │   ├── index.md
│       │   │   │   └── quickstart/
│       │   │   │       └── index.md
│       │   │   ├── index.md
│       │   │   └── markdown-syntax/
│       │   │       ├── index.md
│       │   │       ├── section-example/
│       │   │       │   ├── index.md
│       │   │       │   ├── sub-section-one/
│       │   │       │   │   └── index.md
│       │   │       │   └── sub-section-two/
│       │   │       │       └── index.md
│       │   │       └── syntax/
│       │   │           └── index.md
│       │   └── references/
│       │       ├── rest-api/
│       │       │   ├── _private.md
│       │       │   └── v1.md
│       │       └── rtm-api/
│       │           └── v1.md
│       └── package.json
├── gulpfile.js
├── package.json
├── res/
│   ├── config/
│   │   ├── common.json
│   │   └── user.json
│   └── plugins/
│       ├── gulp/
│       │   ├── minisearch.js
│       │   └── pug-templates.js
│       └── marked/
│           ├── extensions/
│           │   ├── embed.js
│           │   ├── emphasis.js
│           │   ├── figcaption.js
│           │   ├── navigation-item.js
│           │   └── navigation.js
│           └── renderers/
│               ├── code.js
│               └── heading.js
└── src/
    ├── javascripts/
    │   └── common/
    │       └── common.js
    ├── locales/
    │   └── en.json
    ├── stylesheets/
    │   ├── _colors.scss
    │   ├── _config.scss
    │   ├── _functions.scss
    │   ├── _globals.scss
    │   ├── _mixins.scss
    │   ├── _variables.scss
    │   ├── changes/
    │   │   └── changes.scss
    │   ├── common/
    │   │   ├── _appearance.scss
    │   │   ├── _badges.scss
    │   │   ├── _base.scss
    │   │   ├── _buttons.scss
    │   │   ├── _code.scss
    │   │   ├── _content.scss
    │   │   ├── _fonts.scss
    │   │   ├── _footer.scss
    │   │   ├── _header.scss
    │   │   ├── _highlight.scss
    │   │   ├── _markdown.scss
    │   │   ├── _search.scss
    │   │   ├── _viewer.scss
    │   │   └── common.scss
    │   ├── guides/
    │   │   ├── _common.scss
    │   │   ├── _content.scss
    │   │   ├── _sidebar_left.scss
    │   │   └── guides.scss
    │   ├── home/
    │   │   └── home.scss
    │   ├── not_found/
    │   │   └── not_found.scss
    │   └── references/
    │       ├── _content.scss
    │       ├── _sidebar_left.scss
    │       └── references.scss
    └── templates/
        ├── __base.pug
        ├── _body_footer.pug
        ├── _body_header.pug
        ├── _body_search.pug
        ├── _head_favicon.pug
        ├── _head_http.pug
        ├── _head_includes.pug
        ├── _head_metas.pug
        ├── _head_screen.pug
        ├── _head_theme.pug
        ├── _mixins.pug
        ├── changes/
        │   └── index.pug
        ├── guides/
        │   ├── _content.pug
        │   ├── _sidebar_left.pug
        │   └── index.pug
        ├── home/
        │   └── index.pug
        ├── not_found/
        │   └── index.pug
        └── references/
            ├── _blueprint.pug
            ├── _blueprint_content.pug
            ├── _blueprint_sidebar_left.pug
            ├── _markdown.pug
            ├── _markdown_content.pug
            ├── _markdown_sidebar_left.pug
            ├── _mixins_blueprint.pug
            ├── _mixins_common.pug
            └── index.pug

================================================
FILE CONTENTS
================================================

================================================
FILE: .babelrc
================================================
{
  presets: ["es2015"]
}


================================================
FILE: .banner
================================================
      ___          ___          ___          ___       ___       ___
     /  /\        /__/\        /  /\        /  /\     /  /\     /  /\
    /  /:/        \  \:\      /  /::\      /  /::\   /  /::\   /  /:/_
   /  /:/          \__\:\    /  /:/\:\    /  /:/\:\ /  /:/\:\ /  /:/ /\
  /  /:/  ___  ___ /  /::\  /  /:/~/::\  /  /:/~/://  /:/~/://  /:/ /:/_
 /__/:/  /  /\/__/\  /:/\:\/__/:/ /:/\:\/__/:/ /://__/:/ /://__/:/ /:/ /\
 \  \:\ /  /:/\  \:\/:/__\/\  \:\/:/__\/\  \:\/:/ \  \:\/:/ \  \:\/:/ /:/
  \  \:\  /:/  \  \::/      \  \::/      \  \::/   \  \::/   \  \::/ /:/
   \  \:\/:/    \  \:\       \  \:\       \  \:\    \  \:\    \  \:\/:/
    \  \::/      \  \:\       \  \:\       \  \:\    \  \:\    \  \::/
     \__\/        \__\/        \__\/        \__\/     \__\/     \__\/

 — {{bundle}} by Crisp


================================================
FILE: .bowerrc
================================================
{
  "registry": "https://registry.bower.io"
}


================================================
FILE: .github/workflows/build.yml
================================================
on:
  push:
    tags:
      - "v*.*.*"

permissions:
  id-token: write

name: Build and Release

jobs:
  release:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Install NodeJS
        uses: actions/setup-node@v1
        with:
          node-version: 24.x
          registry-url: https://registry.npmjs.org

      - name: Verify versions
        run: node --version && npm --version && node -p process.versions.v8

      - name: Release package
        run: npm publish --ignore-scripts --provenance


================================================
FILE: .github/workflows/test.yml
================================================
on: [push, pull_request]

name: Test and Build

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest]
        node-version: [20.x, 22.x, 24.x]
      fail-fast: false

    runs-on: ${{ matrix.os }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Install NodeJS
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      - name: Verify versions
        run: node --version && npm --version && node -p process.versions.v8

      - name: Cache build artifacts
        id: cache-node
        uses: actions/cache@v4
        with:
          path: |
            ~/.npm
            .chappe
            node_modules
          key: test-${{ runner.os }}-node-${{ matrix.node-version }}

      - name: Install dependencies
        run: npm install

      - name: Run tests
        run: npm test

      - name: Build 'acme-docs' example
        run: npm run build


================================================
FILE: .gitignore
================================================
.DS_Store
Thumbs.db
npm-debug.log

package-lock.json

node_modules/
.chappe/

dist/


================================================
FILE: .jscsrc
================================================
{
  "maximumLineLength": 80,

  "validateIndentation": 2,
  "validateQuoteMarks": "\"",

  "disallowTrailingComma": true,
  "disallowTrailingWhitespace": true,
  "disallowNewlineBeforeBlockStatements": true,
  "disallowMixedSpacesAndTabs": true,
  "disallowMultipleLineStrings": true,
  "disallowNamedUnassignedFunctions": true,
  "disallowQuotedKeysInObjects": true,
  "disallowSpacesInsideParentheses": true,
  "disallowKeywordsOnNewLine": ["else"],
  "disallowIdentifierNames": ["console"],

  "requireCurlyBraces": true,
  "requireDotNotation": true,
  "requireSemicolons": true,
  "requireSpaceBeforeObjectValues": true,
  "requireSpaceBetweenArguments": true,
  "requireSpacesInForStatement": true,
  "requireSpaceAfterObjectKeys": true,
  "requireSpaceBeforeBinaryOperators": true,
  "requireSpaceBeforeBlockStatements": true,
  "requireSpacesInConditionalExpression": true,
  "requireBlocksOnNewline": true,
  "requireCommaBeforeLineBreak": true,
  "requireLineFeedAtFileEnd": true,
  "requirePaddingNewLineAfterVariableDeclaration": true,
  "requirePaddingNewLinesAfterUseStrict": true,
  "requirePaddingNewLinesBeforeExport": true,
  "requirePaddingNewLinesInObjects": true
}


================================================
FILE: .jshintrc
================================================
{
  "camelcase": false,
  "esversion": 6,
  "node": true,
  "predef": [
    "require",
    "define",
    "escape",
    "Buffer",
    "module"
  ]
}


================================================
FILE: .npmignore
================================================
package-lock.json

.github/**
.chappe/**
dist/**
examples/**



================================================
FILE: .pug-lintrc
================================================
{
  "requireLowerCaseTags": true,
  "requireLowerCaseAttributes": true,
  "requireLineFeedAtFileEnd": true,

  "requireIdLiteralsBeforeAttributes": true,
  "requireClassLiteralsBeforeIdLiterals": true,
  "requireClassLiteralsBeforeAttributes": true,

  "requireStrictEqualityOperators": true,

  "validateSelfClosingTags": true,
  "validateIndentation": 2,
  "validateDivTags": true,
  "validateAttributeQuoteMarks": "\"",

  "disallowStringConcatenation": true,
  "disallowDuplicateAttributes": true,

  "disallowSpecificAttributes": [
    "xmlns",

    {
      "br": "role"
    },

    {
      "hr": "role"
    },

    {
      "nav": "role"
    }
  ],

  "disallowSpecificTags": [
    "b",
    "hgroup",
    "i",
    "s",
    "u"
  ],

  "requireSpecificAttributes": [
    {
      "html": [
        "lang"
      ]
    },

    {
      "a": [
        "href"
      ]
    },

    {
      "img": [
        "src",
        "alt"
      ]
    },

    {
      "form": [
        "action",
        "method"
      ]
    },

    {
      "input": [
        "type",
        "name"
      ]
    },

    {
      "textarea": [
        "name",
        "cols",
        "rows"
      ]
    }
  ]
}


================================================
FILE: .stylelintrc.yml
================================================
extends:
  - stylelint-config-standard-scss

rules:
  selector-id-pattern: null
  selector-class-pattern: null

  at-rule-empty-line-before: null
  no-descending-specificity: null

  scss/comment-no-empty: null
  scss/load-no-partial-leading-underscore: null
  scss/dollar-variable-empty-line-before: null


================================================
FILE: CHANGELOG.md
================================================
# Changelog

## 1.16.0 (2026-05-03)

### New Features

* Added support for footnotes in guides.

## 1.15.2 (2026-03-31)

### Bug Fixes

* Fixed category icon overflow in guides details section.

## 1.15.1 (2026-02-02)

### Bug Fixes

* Fixed generated slug for anchor links in Markdown articles (in some cases).

## 1.15.0 (2026-01-28)

### Changes

* Migrate `.jade` files to `.pug` syntax (and update the syntax to Pug 3).

## 1.14.0 (2026-01-27)

### Changes

* Migrate `.sass` files to `.scss` syntax.
* Re-instate linting of stylesheet files with `gulp-stylelint-esm`.

## 1.13.0 (2026-01-27)

### Changes

* Migrate Sass syntax away from `@import` to `@use`.

## 1.12.0 (2025-11-11)

### Changes

* Migrate `gulp-cssmin` to `gulp-clean-css` for Node.js 24 compatibility.

## 1.11.0 (2025-10-16)

### Changes

* Migrate to NPMJS OIDC publishing tokens (since Classic Tokens will be removed in November 2025).

## 1.10.2 (2025-07-15)

### Bug Fixes

* Fixed dropped enumeration members with empty values (such as `0` and `false`).
* Fixed enumeration descriptions, that were not showing at all.

## 1.10.1 (2025-06-22)

### New Features

* Added a way to configure navigation link targets (`self` or `blank`, defaults to `self`).

## 1.10.0 (2025-06-22)

### New Features

* Added a way to disable customer support CTAs with the `features.support` feature flag option (enabled by default).

## 1.9.8 (2025-05-17)

### Bug Fixes

* Fixed concurrency issues on the copying of images while the `sass` is also running.

## 1.9.7 (2025-05-17)

### New Features

* Added a way to customize `async` and `defer` attributes in the `includes.scripts.urls` option.

### Bug Fixes

* The warnings thrown by `sass` are now hidden (until they are fixed).

## 1.9.6 (2025-05-16)

### Changes

* Replaced deprecated `node-sass` dependency with `sass`.

## 1.9.5 (2023-11-06)

### Changes

* Added provenance information upon building NPM package over GitHub Actions.

## 1.9.4 (2023-08-02)

### Bug Fixes

* Fixed performance issues when installing Chappe with NPM v9.

## 1.9.3 (2023-01-06)

### New Features

* Automated the package release process via GitHub Actions (ie. `npm publish`).

## 1.9.2 (2022-12-02)

### Changes

* Improved detection of page titles when generating Open Graph previews.

## 1.9.1 (2022-12-02)

### New Features

* Added support for the `twitter:image:src` and `twitter:card` tags.

## 1.9.0 (2022-12-02)

### ⚠️ Breaking Changes

* The Open Graph images are now auto-generated from the page content, using the provided `opengraph` configuration property; since this option already existed before, you will need to make sure to update your Open Graph image so that it can contain inserted text in the foreground (ie. you need to clear any text from your existing Open Graph image).

## 1.8.1 (2022-11-03)

### Changes

* Added a background hover effect on table rows.

## 1.8.0 (2022-06-24)

### New Features

* Added a "copy to clipboard" button in all code blocks.

## 1.7.1 (2022-05-06)

### Changes

* The generated platform changes RSS feed now uses the configured title.

## 1.7.0 (2022-05-06)

### ⚠️ Breaking Changes

* Changed how the theme accent color gets configured with the `theme` configuration property (`light` and `dark` mode variants must now be set).

### New Features

* Support for Crisp Status (aside from Vigil).

## 1.6.4 (2022-02-13)

### Changes

* Moved the search engine opening shortcut from ⌘F to ⌘K (after gathering user feedback).

## 1.6.3 (2022-01-12)

### Bug Fixes

* Fixed an issue with non-highlighted code blocks in Dark Mode, where code text would appear black-on-black.
* Fixed an issue with inline code blocks in Dark Mode, where selected text would appear white-on-white.

## 1.6.2 (2022-01-11)

### Changes

* Improved management of CSS colors.

## 1.6.1 (2022-01-11)

### Changes

* Improved Dark Mode colors.
* Moved logos to embedded images (this prevents visual glitches when loading docs).

## 1.6.0 (2022-01-10)

### New Features

* Implemented Dark Mode.
* Added the ability to configure the theme accent color with the `theme` configuration property.

## 1.5.1 (2022-01-07)

### Changes

* Improved the performance of watching for changes in `data/` while using `chappe serve` or `chappe watch`.

### Bug Fixes

* Fixed Gulp meta-events showing in the Chappe CLI output when using `--verbose` (they are now hidden).

## 1.5.0 (2022-01-06)

### New Features

* The Chappe CLI now embeds a preview server, used to ease with writing and previewing docs (via `chappe serve`).

### Changes

* Improved logging in the Chappe CLI.

### Bug Fixes

* Fixed the abrupt stopping of the Chappe CLI whenever a resource build failed while using `chappe watch`.

## 1.4.1 (2022-01-06)

### Changes

* Refactored the README to add the Chappe logo.

## 1.4.0 (2022-01-05)

### Changes

* Moved the project to a dedicated GitHub organization: [Crisp OSS](https://github.com/crisp-oss).

## 1.3.2 (2022-01-05)

### Changes

* Moved the Chappe CLI from ES5 to ES6.

## 1.3.1 (2022-01-05)

### Changes

* Refactored Chappe CLI terminal outputs.
* Improved the `acme-docs` example.

## 1.3.0 (2022-01-05)

### Changes

* Reworked the lint pipeline to reduce the number of dependencies.

### Bug Fixes

* Fixed an issue where 404 and private pages appeared in the sitemap.
* Fixed the configuration path for the SASS linter, which could not read its configuration file in some cases.

## 1.2.1 (2022-01-05)

### Bug Fixes

* Fixed the normalization of paths passed to the Chappe CLI via `--config`.

## 1.2.0 (2022-01-05)

### New Features

* The Chappe CLI `--config` argument now accepts multiple configuration files (comma-separated, merged together).
* Added the ability to override certain Chappe internal values with the `overrides` configuration property.

### Bug Fixes

* Fixed an issue in Chappe CLI, where a build failure could cause the process to hang indefinitely.

## 1.1.2 (2022-01-04)

### Bug Fixes

* Fixed the NPMJS distribution package, that was missing some more hidden files (such as `.babelrc`).

## 1.1.1 (2022-01-04)

### Bug Fixes

* Fixed the NPMJS distribution package, that was missing the `.banner` file.

## 1.1.0 (2022-01-04)

### Changes

* Moved the build pipeline to Gulp 4.
* Improved the Chappe CLI with terminal spinners.

## 1.0.3 (2022-01-04)

### Changes

* Now showing a Chappe logo when calling the Chappe CLI.
* Better temporary files management.

### Bug Fixes

* All internal paths are now absolute (this fixes some build environments).

## 1.0.2 (2022-01-03)

### Bug Fixes

* Fix dependencies for published `chappe` package.

## 1.0.1 (2022-01-03)

### Bug Fixes

* Fix the path of Chappe CLI.

## 1.0.0 (2022-01-03)

### New Features

* Initial release.


================================================
FILE: LICENSE
================================================
Copyright (c) 2021 Crisp IM SAS

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
<img alt="Chappe" src="https://crisp-oss.github.io/chappe/images/chappe.png" width="300">

[![Test and Build](https://github.com/crisp-oss/chappe/actions/workflows/test.yml/badge.svg)](https://github.com/crisp-oss/chappe/actions/workflows/test.yml) [![Build and Release](https://github.com/crisp-oss/chappe/actions/workflows/build.yml/badge.svg)](https://github.com/crisp-oss/chappe/actions/workflows/build.yml) [![NPM](https://img.shields.io/npm/v/chappe.svg)](https://www.npmjs.com/package/chappe) [![Downloads](https://img.shields.io/npm/dt/chappe.svg)](https://www.npmjs.com/package/chappe)

**Developer Docs builder. Write guides in Markdown and references in API Blueprint. Comes with a built-in search engine.**

Chappe is a Developer Docs builder, that produces static assets. No runtime, just lightweight static files. It was built to address SaaS companies needs, and can serve as a first-class modern alternative to hosted services such as [ReadMe](https://readme.com/).

The reason behind why we made Chappe is the following: while looking for a Developer Docs builder at [Crisp](https://crisp.chat/en/), all that we could find were either outdated open-source projects, or commercial documentation builders. We wanted a modern Developer Docs website hosted on our premises, as pure-static assets. The latter is especially important, as we do not want to rely on a plethora of external services that can go down anytime.

**Using Chappe is as easy as:**

1. Writing all your docs in Markdown;
2. Building your docs in a single command;
3. Finally, deploying static build assets to your Web servers (or GitHub Pages, Cloudflare Pages, etc. — _this can be automated via GitHub Actions_);

**😘 Maintainer**: [@valeriansaliou](https://github.com/valeriansaliou)

## Screenshots & Demo

**👉 See a live demo of Chappe on the [Crisp Developer Hub](https://docs.crisp.chat/).**

1️⃣ Chappe can generate your REST API reference:

[![Chappe References](https://crisp-oss.github.io/chappe/images/screenshot-references.gif)](https://docs.crisp.chat/references/rest-api/v1/)

2️⃣ It also generates Markdown-based developer guides:

[![Chappe Guides](https://crisp-oss.github.io/chappe/images/screenshot-guides.gif)](https://docs.crisp.chat/guides/rest-api/rate-limits/)

3️⃣ Oh, and it also lets your users search anything in your Developer Docs:

[![Chappe Search](https://crisp-oss.github.io/chappe/images/screenshot-search.gif)](https://docs.crisp.chat/)

_👉 Note that the search engine feature is 100% local. This means that it does not run on an external service like [Algolia](https://www.algolia.com/), though it does provides similar search performance and results. The search index is generated at build time as a JSON file, which gets loaded on-demand when the search box gets opened._

## Who uses it?

<table>
<tr>
<td align="center"><a href="https://crisp.chat/"><img src="https://crisp-oss.github.io/chappe/images/logo-crisp.png" width="64" /></a></td>
<td align="center"><a href="https://meowtel.com/"><img src="https://crisp-oss.github.io/chappe/images/logo-meowtel.png" width="64" /></a></td>
</tr>
<tr>
<td align="center">Crisp</td>
<td align="center">Meowtel</td>
</tr>
</table>

_👋 You use Chappe and you want to be listed there? [Open an issue](https://github.com/crisp-oss/chappe/issues)._

## Last changes

The version history can be found in the [CHANGELOG.md](https://github.com/crisp-oss/chappe/blob/master/CHANGELOG.md) file.

## Features

* **Simple & fast**: generate a Developer Docs with optimized static assets. No runtime
* **Guides**: write developer guides in Markdown (rich content support: images, videos, tables, etc.)
* **References**: document your HTTP REST API specification using API Blueprint
* **Changes**: maintain a changelog of your platform (eg. your REST API, your SDKs)
* **RSS feed**: users can subscribe to your changelog over RSS
* **Beautiful Markdown rendering**: all content that you write gets rendered with a clear and modern style
* **Syntax highlighting**: coloring for your code examples in 100+ programming languages
* **Built-in search engine**: the index is generated during build and is hosted locally
* **Fully responsive**: full support of desktop, tablet and phone screens
* **Dark Mode**: read your docs either in light mode or dark mode
* **Customizable theme**: configure an accent color for your docs theme
* **SEO-friendly**: a deep sitemap is generated for search engines
* **Sharing-friendly**: full support of the Open Graph protocol, with the auto-generation of preview images
* **Private pages support**: mark any guide or reference as private or unlisted (prefix its name with `_`)
* **Local preview server**: skip setting up a local Web server to preview your docs while writing them, Chappe embeds a preview server that can be started in a single command

_The following optional features can also be enabled:_

* **Chatbox**: integrate with the [Crisp Chatbox](https://crisp.chat/en/livechat/) to handle tech support and collect user feedback
* **Status page**: integrate with your [Vigil](https://github.com/valeriansaliou/vigil) status page to show live system status (can also be [Crisp Status](https://crisp.chat/en/status/))

## How to use it?

### Installation & Overview

To install and use Chappe, please follow those steps:

1. Create a new, empty Git repository;
2. Copy the `examples/acme-docs/` folder contents from the Chappe repository into your project root;
3. Run: `npm install` (make sure that you have a recent NodeJS version installed);
4. Run: `npx chappe serve` to build the docs and serve them over a local Web server (it will also watch for changes);
5. Open: [http://localhost:8080](http://localhost:8080/) in your Web browser to access your docs;
6. Write your Markdown guides and references in the `data/` directory (changes will be hot-reloaded in your browser);

Please refer to sections below for more details on how to write docs, customize Chappe, and deploy your final docs to your Web server.

_👉 If Chappe fails to install on your Mac with an Apple Silicon chip, please refer to the [Common questions](#-common-questions) section below._

### Configuration

The configuration of your Chappe docs is stored in a single JSON file, usually named `config.json`. Your configuration file will make references to images, such as your docs logo, which are stored in the `assets/` folder.

An empty definition of the Chappe configuration file is available in: [res/config/user.json](https://github.com/crisp-oss/chappe/blob/master/res/config/user.json), although you may rather want to see a filled example: [examples/acme-docs/config.json](https://github.com/crisp-oss/chappe/blob/master/examples/acme-docs/config.json) (if you copy-paste it, **make sure** to change all of its contents).

_👇 Notes on certain configuration rules can be found in the [Advanced settings](#%EF%B8%8F-advanced-settings) section._

### Chappe CLI usage

Chappe provides you with the `chappe` command, that builds your docs.

It supports the following actions, defaulting to `build` if none is specified:

* `build` to build docs
* `clean` to clean `dist/` and all temporary files
* `watch` to watch for changes and re-build (useful while writing docs)
* `serve` to serve built assets on your local/development computer (useful while testing and writing docs, **not used for production**)
* `lint` to run lints on Chappe internal resources

It supports the following parameters, with a default value if not set:

* `--config` (paths to the configuration files, comma-separated, _default value:_ `./config.json`)
* `--assets` (path to the assets directory, _default value:_ `./assets`)
* `--data` (path to the data directory, _default value:_ `./data`)
* `--dist` (path where to write built resources, _default value:_ `./dist`)
* `--temp` (path where to write temporary files, _default value:_ `./.chappe`)
* `--env` (environment, either `development` or `production`, _default value:_ `production`)

If you are running with the `serve` action, it accepts additional parameters:

* `--host` (hostname or IP address to use for the local/development server, _default value:_ `localhost`)
* `--port` (port number to use for the local/development server, _default value:_ `8080`)

Some special parameters are also available:

* `--quiet` (show less output when performing task)
* `--verbose` (show more output when performing task)
* `--example` (name of the Chappe docs example to build, useful for Chappe developers and quick tests, eg. `acme-docs`)

To build your docs, you can call the Chappe CLI as such:

```bash
npx chappe build --config=./config.json --assets=./assets --data=./data --dist=./dist
```

You can also call the Chappe CLI without any argument, in which case defaults will be used:

```bash
npx chappe build
```

By default, docs are built for a `production` target, meaning that all assets produced are optimized for speed and size. In most use cases, you will never need to set it to `development`, unless you are trying to extend or modify the Chappe core and therefore need to see uncompressed assets output.

To create a local development server on [http://localhost:8080](http://localhost:8080/), used to write and preview your docs, use:

```bash
npx chappe serve
```

_👉 If the `chappe` command is not found, make sure to add `chappe` to your `package.json` and call `npm install`._

### Writing docs

Docs can be either: `guides`, `references` or `changes`. The corresponding folders are stored in the `data/` directory, which is passed to the Chappe CLI whenever building your docs.

* `guides` are articles that walk your users through using your systems ([example here](https://docs.crisp.chat/guides/rest-api/rate-limits/)). They are written in Markdown, and are organized in sub-folders if deep nesting of guides in several sections is required. Chappe will auto-generate the navigation sidebar for you, based on this folder hierarchy.
* `references` are formal specifications of your systems (examples of: [API Blueprint](https://docs.crisp.chat/references/rest-api/v1/) and [Markdown](https://docs.crisp.chat/references/rtm-api/v1/)). They are written in [API Blueprint](https://apiblueprint.org/) for your HTTP REST API (a pseudo-Markdown format), or traditional Markdown for other systems (eg. a WebSocket server).
* `changes` is a timeline of updates that you made to your systems ([example here](https://docs.crisp.chat/changes/)). They are defined in a JSON format. In addition to the timeline, an RSS feed also gets generated at the `/changes.rss` URL.

### Deploying your docs

To deploy your docs:

1. First, create a Virtual Host on your Web server, using a dedicated domain, eg. `docs.acme.com`;
2. Then, build Chappe with `npx chappe build` (on your local computer or a CI/CD runner such as GitHub Actions);
3. Finally, copy the contents of the `dist/` folder to your server folder for your docs Virtual Host (eg. `/var/www/docs.acme.com`);

⚠️ Chappe **must** be hosted at the root of your docs domain — it **will not** work if hosted in a sub-directory!

Here is an example configuration file for NGINX on the Virtual Host `docs.acme.com`:

```
server {
  listen 443 ssl http2;
  server_name docs.acme.com;

  root /var/www/docs.acme.com;

  error_page 404 /not_found/;
}
```

_👉 Note that if possible, you should make sure that you have a rule to catch 404 errors and show the `not_found` page (as the NGINX configuration file above shows)._

## Syntax guide

### How to write guides?

Guides are stored within `guides/` in your data directory. A guide is stored as a Markdown file named `index.md` in a sub-directory with the guide name eg. `hello-world`. The sub-directory structure directly maps to the final URL that you get: for instance `guides/hello-world/index.md` results in eg. `http://docs.acme.com/guides/hello-world/`.

#### Structure of a guide file

Each guide Markdown file **must** start with a meta-data header, which holds information on:

* `TITLE`: The guide article name
  * Example: `TITLE: Hello World`
* `INDEX`: Number used to position the article relative to others in the navigation sidebar
  * Example: `INDEX: 1`
* `UPDATED`: The date at which the guide article has been updated
  * Example: `UPDATED: 2021-09-22`
* `LINK`: Additional navigation links to be added in the navigation sidebar
  * _Optional_, _Multiple possible_
  * Example: `LINK: Reference -> /references/rest-api/v1/`

Right after the header is defined, you can start writing Markdown for your guide, as normal.

An example of a full Markdown code for a guide is available at: [examples/acme-docs/data/guides/hello-world/index.md](https://raw.githubusercontent.com/crisp-oss/chappe/master/examples/acme-docs/data/guides/hello-world/index.md)

#### Adding icons to guide sections

Each guide main section can have its icon shown in the navigation sidebar (first-level sections only).

Section icons are defined in the `config.json` configuration file, within `images.categories.guides`. The section folder name, eg. `hello-world`, should be added to the `guides` object, associated to an SVG icon image from your `assets/` folder.

For example:

```json
{
  "images" : {
    "categories" : {
      "guides" : {
        "hello-world" : "images/categories/guides/hello-world.svg"
      }
    }
  }
}
```

#### List of special Markdown syntax

While the [Markdown specification](https://daringfireball.net/projects/markdown/syntax) defines most of the syntax that we need to build a full-featured Developer Docs (text formatting, images, tables, etc.), some non-standard elements had to be defined in Chappe.

---

##### Video embeds

To embed a video in a page, use the following Markdown syntax:

```markdown
${provider}[Video Title](video-id)
```

Supported providers: `youtube`

Example:

```markdown
${youtube}[In-depth Introduction to the Crisp RTM API](vS-h6k2ML6M)
```

---

##### Text emphasis (notice, info or warning blocks)

To insert text in an emphasis block, use one of the following Markdown syntaxes:

```markdown
! This is a notice text.
!! This is an info text.
!!! This is a warning text.
```

---

##### Image with caption

To insert an image with a caption, use the following Markdown syntax:

```markdown
$[Caption Text](![Image Title](image-path.png))
```

Example:

```markdown
$[Copy your Website ID](![](copy-website-id.png))
```

---

##### Navigation links

To insert a navigation block, with one or multiple links to other pages, use the following Markdown syntax:

```markdown
+ Navigation
  | Link Title 1: Link Description -> ./link/target/1/
  | Link Title 2: Link Description -> http://external-url.com/target/page/ [blank]
```

Example:

```markdown
+ Navigation
  | Quickstart: Learn how to use the REST API in minutes. -> ./quickstart/
  | Authentication: Read how to authenticate to the REST API. -> ./authentication/
  | Rate-Limits: Learn about request rate-limits. -> ./rate-limits/
  | API Libraries: Libraries for your programming language. -> ./api-libraries/
```

---

##### Interact with the Crisp Chatbox

If you need to interact with the Crisp Chatbox from your Markdown code, you can include a traditional Markdown link with an URL pointing to special anchors.

The following anchors are available:

* Pop open the chatbox: `#crisp-chat-open`
* Prompt to submit feedback on the current page: `#crisp-chat-feedback`

Example:

```markdown
If you have any question on this guide, please [contact our chat support](#crisp-chat-open).
```

_👉 Note that this only works if you are using the Crisp Chatbox integration, and if the Crisp Chatbox is appearing on your docs._

---

### How to write references?

References are stored within `references/` in your data directory. A reference is stored either as an API Blueprint or Markdown file named for example `v1.md` for the API version, in a sub-directory corresponding to the name of the API, eg. `rest-api`. The sub-directory structure directly maps to the final URL that you get: for instance `references/rest-api/v1.md` results in eg. `http://docs.acme.com/references/rest-api/v1/`.

#### API Blueprint references

API Blueprint-formatted references are used to specify an HTTP REST API.

Each reference written with API Blueprint **must** start with a meta-data header, which holds information on:

* `TYPE`: The type of the reference
  * Value: `API Blueprint`
* `TITLE`: The reference title (with its version number)
  * Example: `TITLE: REST API Reference (V1)`
* `UPDATED`: The date at which the reference has been updated
  * Example: `UPDATED: 2021-12-22`

Immediately following, come API Blueprint meta-datas:

* `FORMAT`: The API Blueprint format (_do not change this_)
  * Value: `1A`
* `HOST`: The HTTP REST API host URL
  * Example: `https://api.crisp.chat/v1`

Then, a main title with the following mandatory content:

```markdown
# Reference
```

After that, you can specify all your HTTP REST API routes in API Blueprint as normal.

Also, note that as done with guides above, reference sections can have their own icon images. Section icons are defined in the `config.json` configuration file, within `images.categories.references`.

An example of a full API Blueprint code for a reference is available at: [examples/acme-docs/data/references/rest-api/v1.md](https://raw.githubusercontent.com/crisp-oss/chappe/master/examples/acme-docs/data/references/rest-api/v1.md)

#### Markdown references

Markdown-formatted references are used to specify anything that is not an HTTP REST API. For instance, a WebSocket endpoint, a network protocol or a programmatic interface.

Each reference Markdown file **must** start with a meta-data header, which holds information on:

* `TYPE`: The type of the reference
  * Value: `Markdown`
* `TITLE`: The reference title (with its version number)
  * Example: `TITLE: RTM API Reference (V1)`
* `UPDATED`: The date at which the reference has been updated
  * Example: `UPDATED: 2021-09-22`

After that, you can write the specification contents in Markdown.

Also, note that as done with guides above, reference sections can have their own icon images. Section icons are defined in the `config.json` configuration file, within `images.categories.references`.

An example of a full Markdown code for a reference is available at: [examples/acme-docs/data/references/rtm-api/v1.md](https://raw.githubusercontent.com/crisp-oss/chappe/master/examples/acme-docs/data/references/rtm-api/v1.md)

### How to write changelogs?

Changes are stored within `changes/` in your data directory. They are organized in JSON files for each year, eg. `2021.json`.

#### Structure of a changelog file

A changelog file for a year contains an array of all individual change entries. Think of it as a yearly feed of all dated changes.

For instance, a `2021.json` file with a single change would contain:

```json
[
  {
    "group" : "rest_api",
    "type"  : "change",
    "date"  : "2021-12-03",
    "text"  : "Markdown-formatted text for this change on the REST API."
  }
]
```

An example of a full changelog file is available at: [examples/acme-docs/data/changes/2021.json](https://raw.githubusercontent.com/crisp-oss/chappe/master/examples/acme-docs/data/changes/2021.json)

#### Allowed values for a change

A change is structured as such:

* `group`: the category of this change — _define your custom categories labels in `texts.changes.groups` and colors in `colors.changes.groups` within your `config.json`_;
* `type`: the type of the change (either: `change` or `deprecation`);
* `date`: a date for the change (formatted as: `YYYY-MM-DD`);
* `text`: the description text for the change, Markdown-formatted — _make sure that any URL you define there is a full URL, as this is also used in RSS feeds_

## ⚛️ Advanced settings

### Available code coloring

Code coloring rules for programming languages must be added manually, for each syntax that you intend use. As the rules are quite heavy for each syntax, Chappe includes none by default.

For instance, if you need to show examples of Java code, you'd need to add the `java` code coloring rule in `plugins.code.syntaxes` in your `config.json`. Chappe runs on [Prism](https://github.com/PrismJS/prism) for code coloring.

Most often used syntaxes are listed below (pick yours!):

```
markup
markup-templating
css
clike
c
javascript
bash
go
java
groovy
json
objectivec
php
python
ruby
rust
swift
objectivec
```

All available Prism rules can be found [here](https://github.com/PrismJS/prism/tree/master/components).

_👉 Note that some rules depend on others. For instance, `objectivec` requires the `c` rule to be also included. If you do not get code coloring for a certain syntax after including it, then it probably means that one of its dependency is missing. Please refer to the list of Prism components for more details._

### Check file sizes during build

Once Chappe is done building your docs, it checks for all built files sizes against maximum build size rules. This is done to ensure that you do not get bad surprises about your Developer Docs users experiencing slow load times, especially when including a lot of heavy images in guides.

In the event a build size rule threshold is reached, the Chappe CLI will error out, informing you which file is over-sized.

To adjust size thresholds or disable this checker rule, open your `config.json` file and refer to the `rules.build_size` property:

* To circumvent build failure when a file is over-sized, set the `fail` property to `false`;
* Maximum sizes can be adjusted where relevant with the `sizes` property (note that sizes are in bytes, so 10KB is about `10000`);

## 🙋 Common questions

### The installation of Chappe fails on my Mac with Apple Silicon

Chappe relies on the `gulp-ogimage` dependency to auto-generate Open Graph images, which itself uses a library named `canvas`. Unfortunately, as of December 2022, `canvas` does not provide any pre-built binary for the `arm64` CPU architecture, leading to Chappe failing to install on Macs with Apple Silicon chips.

In order to install Chappe on `arm64` architectures, you will need to ensure that [Homebrew](https://brew.sh/) is setup on your system, then run:

```bash
brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman
```

Once those tools are installed, try installing Chappe again.

### How can I customize my docs style?

In order to customize your docs style — _ie. override the default Chappe style past what can already be customized in the `config.json` configuration file_ — open `config.json` and look for the `includes` property (that contains `stylesheets`, that contains `urls` and `inline`).

You can easily deploy your own custom stylesheet on your docs domain, along with Chappe-generated `dist/` assets, with CSS classes overriding Chappe default styles:

```json
{
  "includes" : {
    "stylesheets" : {
      "urls"   : [
        "/overrides/style.css"
      ],

      "inline" : []
    }
  }
}
```

### How can I add scripts like Google Tag Manager?

To add inline scripts such as Google Tag Manager, open your `config.json` configuration file for Chappe, and look for the `includes` property (that contains `scripts`, that contains `urls` and `inline`).

Add a new entry to the `urls` and `inline` array, separately, giving eg.:

```json
{
  "includes" : {
    "scripts" : {
      "urls"   : [
        "https://www.googletagmanager.com/gtag/js?id={YOUR_GTM_ID}",

        {
          "src"   : "https://scripts.simpleanalyticscdn.com/latest.js",
          "async" : true
        }
      ],

      "inline" : [
        "window.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\ngtag(\"js\", new Date());\ngtag(\"config\", \"{YOUR_GTM_ID}\");"
      ]
    }
  }
}
```

The `urls` property will include the JavaScript at the provided URL on all pages, while the `inline` property will append the inline JavaScript in a `script` element on all pages.

### How can I deploy my docs to GitHub Pages?

To build your docs to GitHub Pages, you will first need to host your docs project as a GitHub repository. Then, make sure that GitHub Actions is configured and running for your project.

You can then use the [deploy-to-github-pages](https://github.com/marketplace/actions/deploy-to-github-pages) action to proceed with building your docs via `npx chappe build` and then deploying the `dist/` folder to GitHub Pages.

### Where does the Chappe name come from?

Chappe was named after [Claude Chappe](https://en.wikipedia.org/wiki/Claude_Chappe), a French inventor, pioneer in long-distance communications. He invented the [optical telegraph](https://en.wikipedia.org/wiki/Optical_telegraph) (a.k.a. semaphore telegraph), later replaced by the [electrical telegraph](https://en.wikipedia.org/wiki/Electrical_telegraph). Those technologies were the founding blocks of what took over the world next: analog and digital telecommunications.

Quoting from his page on Wikipedia:

> This [the optical telegraph] was the first practical telecommunications system of the industrial age, and was used until the 1850s when electric telegraph systems replaced it.

_Credits to [Baptiste Jamin](https://github.com/baptistejamin) for the name idea._


================================================
FILE: bin/chappe.js
================================================
#!/usr/bin/env node

/*
 * chappe
 *
 * Copyright 2021, Crisp IM SAS
 * Author: Valerian Saliou <valerian@valeriansaliou.name>
 */


"use strict";


// Suppress all warnings from Node, as this is a CLI script
process.removeAllListeners("warning");


var fs       = require("fs");
var path     = require("path");

var ora      = require("ora");

var version  = require("../package.json").version;
var args     = require("yargs").argv;


/**
 * Chappe CLI
 * @class
 * @classdesc Chappe CLI class.
 */
class ChappeCLI {
  /**
   * Constructor
   */
  constructor() {
    // Constants
    this.__context_defaults  = {
      default : {
        config : "./config.json",
        assets : "./assets",
        data   : "./data",
        dist   : "./dist",
        temp   : "./.chappe",
        env    : "production",
        host   : "localhost",
        port   : 8080
      },

      example : {
        config : "./examples/{{target}}/config.json",
        assets : "./examples/{{target}}/assets",
        data   : "./examples/{{target}}/data",
        dist   : "./dist",
        temp   : "./.chappe",
        env    : "development",
        host   : "localhost",
        port   : 8080
      }
    };

    this.__actions_available = [
      "build",
      "clean",
      "watch",
      "serve",
      "lint"
    ];

    this.__actions_logging   = [
      "watch",
      "serve"
    ];

    this.__actions_no_aborts = [
      "watch",
      "serve"
    ];

    this.__spinner_successes = {
      default : {
        method : "succeed",
        text   : "Success!"
      },

      clean   : {
        method : "succeed",
        text   : "Cleaned up."
      },

      build   : {
        method : "succeed",
        text   : "Build done!"
      },

      lint    : {
        method : "succeed",
        text   : "Lint passed."
      },

      serve   : {
        method : "start",
        text   : "Now listening..."
      },

      watch   : {
        method : "start",
        text   : "Watching...\n"
      }
    };

    this.__path_expand_keys  = [
      "config",
      "assets",
      "data",
      "dist",
      "temp"
    ];

    this.__env_available     = [
      "development",
      "production"
    ];

    // Storage
    this.__has_setup_gulp_logging = false;
  }


  // jscs:disable disallowIdentifierNames


  /**
   * Runs the class
   * @public
   * @return {undefined}
   */
  run() {
    // Run help?
    if (args.help) {
      return this.__run_help();
    }

    // Run version?
    if (args.version) {
      return this.__run_version();
    }

    // Run default (clean or build)
    this.__run_default();
  }


  /**
   * Runs help
   * @private
   * @return {undefined}
   */
  __run_help() {
    console.log(
      "Builds given Chappe documentation resources into static assets.\n\n"  +

      "Available actions:\n"                                                 +
        (this.__format_help_actions(this.__actions_available) + "\n\n")      +

      "Available arguments:\n"                                               +
        (this.__format_help_argument("config") + "\n")                       +
        (this.__format_help_argument("assets") + "\n")                       +
        (this.__format_help_argument("data")   + "\n")                       +
        (this.__format_help_argument("dist")   + "\n")                       +
        (this.__format_help_argument("temp")   + "\n")                       +
        (this.__format_help_argument("env")    + "\n")                       +
        (this.__format_help_argument("host")   + "\n")                       +
        (this.__format_help_argument("port")   + "\n\n")                     +

      "Other arguments:\n"                                                   +
        (this.__format_help_argument("quiet") + "\n")                        +
        (this.__format_help_argument("verbose") + "\n")                      +
        this.__format_help_argument("example")
    );

    process.exit(0);
  }


  /**
   * Runs version
   * @private
   * @return {undefined}
   */
  __run_version() {
    console.log("Chappe CLI v" + version);

    process.exit(0);
  }


  /**
   * Runs default
   * @private
   * @return {undefined}
   */
  __run_default() {
    let _has_output = (
      (args.quiet && !args.verbose) ? false : true
    );

    // Acquire task from action
    let _task = this.__acquire_action();

    // Dump banner?
    if (_has_output === true) {
      console.log(this.__dump_banner());
    }

    // Acquire context
    global.CONTEXT = this.__acquire_context(_task);

    if (_has_output === true) {
      console.log(
        ("Chappe will " + _task + " docs with context:\n")  +
          (this.__dump_context(global.CONTEXT) + "\n")
      );
    }

    // Setup spinner
    // Throttle down spinner refresh interval, to ease w/ terminal CPU usage \
    //   when performing long operations eg. 'watch'.
    let _spinner = ora({
      text     : "Working...\n",
      color    : "cyan",
      interval : 350
    });

    // Import Gulp instance
    let _gulp = require("gulp");

    // Setup error traps
    this.__setup_error_traps(_gulp, _spinner, _task);

    // Setup Gulp logging? (pre-task mode, only if verbose)
    if (args.verbose) {
      this.__setup_gulp_logging(_gulp, _spinner);
    }

    // Import the Gulpfile
    let _gulpfile = require("../gulpfile.js");

    // Start spinner
    _spinner.start();

    // Build docs
    _gulpfile[_task]((error) => {
      // Any error occured?
      if (error) {
        // Throw error and stop spinner
        _spinner.fail("Error:");

        console.log(error);

        _spinner.stop();

        process.exit(1);
      } else {
        // Show success spinner (depending on task success rule)
        let _success_rules = (
          this.__spinner_successes[_task] || this.__spinner_successes.default
        );

        _spinner[_success_rules.method](_success_rules.text);

        // Setup Gulp logging? (post-task mode, only for certain actions)
        if (this.__actions_logging.includes(_task) === true) {
          this.__setup_gulp_logging(_gulp, _spinner);
        }
      }
    });
  }


  /**
   * Formats help actions
   * @private
   * @param  {object} actions
   * @return {string} Formatted help actions
   */
  __format_help_actions(actions) {
    let _actions = actions.map((action) => {
      return (" " + action);
    });

    return _actions.join("\n");
  }


  /**
   * Formats help argument
   * @private
   * @param  {string} name
   * @return {string} Formatted help argument
   */
  __format_help_argument(name) {
    let _argument = (" --" + name);

    // Append default value? (if any)
    let _default_value = this.__context_defaults.default[name];

    if (typeof _default_value !== "undefined") {
      _argument += ("  (defaults to: '" + _default_value + "')");
    }

    return _argument;
  }


  /**
   * Dumps the banner
   * @private
   * @return {string} Dumped banner
   */
  __dump_banner() {
    // Generate bundle name
    let _bundle_name = ("Chappe v" + version);

    // Read banner file
    let _buffer = (
      fs.readFileSync(path.join(__dirname, "../.banner"))
    );

    // Convert banner to string and inject bundle name
    let _banner = _buffer.toString().replace("{{bundle}}", _bundle_name);

    return _banner;
  }


  /**
   * Dumps the context
   * @private
   * @param  {object} context
   * @return {string} Dumped context
   */
  __dump_context(context) {
    let _context = Object.keys(context).map((key) => {
      return (" " + key + " -> " + context[key]);
    });

    return _context.join("\n");
  }


  /**
   * Acquires current action
   * @private
   * @return {string} Current action
   */
  __acquire_action() {
    let _action;

    for (let _i = 0; _i < this.__actions_available.length; _i++) {
      let _cur_action = this.__actions_available[_i];

      if (process.argv.includes(_cur_action) === true) {
        _action = _cur_action;

        break;
      }
    }

    return (_action || "build");
  }


  /**
   * Acquires current context
   * @private
   * @param  {string} task
   * @return {object} Current context
   */
  __acquire_context(task) {
    // Acquire defaults
    let _defaults = (
      this.__context_defaults[(args.example ? "example" : "default")]
    );

    // Inject target in defaults?
    if (args.example) {
      _defaults = this.__inject_defaults_target(
        _defaults, args.example
      );
    }

    // Generate context
    // Notice: the values are temporarily represented as arrays, to ease with \
    //   data normalization steps.
    let _context = {
      config : (args.config  || _defaults.config).split(","),
      assets : [(args.assets || _defaults.assets)],
      data   : [(args.data   || _defaults.data)],
      dist   : [(args.dist   || _defaults.dist)],
      temp   : [(args.temp   || _defaults.temp)],
      env    : [(args.env    || _defaults.env)]
    };

    // Append serve-related context?
    if (task === "serve") {
      _context.host = [(args.host || _defaults.host)];
      _context.port = [parseInt((args.port || _defaults.port), 10)];
    }

    // Expand context paths
    let _base_path = process.cwd();

    this.__path_expand_keys.forEach((key) => {
      let _context_values = _context[key];

      for (let _i = 0; _i < _context_values.length; _i++) {
        // Path is not already in absolute format? (convert to absolute)
        if (path.isAbsolute(_context_values[_i]) !== true) {
          _context_values[_i] = (
            path.join(_base_path, _context_values[_i])
          );
        }
      }
    });

    // Re-join context values as bare strings (from lists)
    for (let _key in _context) {
      // Notice, this retains non-string types untouched
      if (_context[_key].length === 1) {
        _context[_key] = _context[_key][0];
      } else {
        _context[_key] = _context[_key].join(",");
      }
    }

    // Validate final context
    if (this.__env_available.includes(_context.env) !== true) {
      throw new Error(
        "Environment value not recognized: " + _context.env
      );
    }

    return _context;
  }


  /**
   * Inject target into defaults
   * @private
   * @param  {object} defaults
   * @param  {string} target
   * @return {object} Defaults w/ injections
   */
  __inject_defaults_target(defaults, target) {
    // Important: create a new defaults object, as not to alter the given one, \
    //   which could be re-used later.
    let _injected_defaults = {};

    for (let _key in defaults) {
      let _cur_default = defaults[_key];

      if (typeof _cur_default === "string") {
        _cur_default = _cur_default.replace("{{target}}", target);
      }

      _injected_defaults[_key] = _cur_default;
    }

    return _injected_defaults;
  }


  /**
   * Setups error traps
   * @private
   * @param  {object} gulp
   * @param  {object} spinner
   * @param  {string} task
   * @return {undefined}
   */
  __setup_error_traps(gulp, spinner, task) {
    // Check if should crash on error
    let _crash_on_error = (
      (this.__actions_no_aborts.includes(task) === true) ? false : true
    );

    // Setup process events
    process.once("exit", (code) => {
      if (code > 0) {
        // Self-kill, because apparently even if calling process.exit(1), the \
        //   process stays active and sticky in certain cases (due to \
        //   registered listeners and file descriptors in some Gulp libraries).
        // Warning: this is a bit hacky!
        process.exitCode = code;

        process.kill(process.pid, "SIGKILL");
      }
    });

    process.on("uncaughtException", (error) => {
      // Throw error and stop spinner
      spinner.fail("Unexpected failure:");

      console.log(
        (error && error.context) ? error.context : error
      );

      spinner.stop();

      process.exit(1);
    });

    // Important: setup Gulp error listener, otherwise any error will get \
    //   uncaught and be handled by 'uncaughtException' at the process-level.
    gulp.on("error", (event) => {
      // Freeze spinner w/ failure
      spinner.fail(
        "Error in '" + event.name + "':"
      );

      if (event.error) {
        console.log(event.error);
      }

      // Restart the spinner
      spinner.start();

      if (_crash_on_error === true) {
        process.exit(1);
      }
    });
  }


  /**
   * Setups Gulp logging
   * @private
   * @param  {object} gulp
   * @param  {object} spinner
   * @return {undefined}
   */
  __setup_gulp_logging(gulp, spinner) {
    // Not quiet and not already setup?
    if (!args.quiet && this.__has_setup_gulp_logging !== true) {
      this.__has_setup_gulp_logging = true;

      gulp.on("start", (event) => {
        // Freeze spinner w/ information
        // Notice: do not log meta-events eg. '<series>'
        if (event.name.startsWith("<") !== true) {
          spinner.info(
            "Starting '" + event.name + "'..."
          );
        }
      });

      gulp.on("stop", (event) => {
        // Freeze spinner w/ success
        // Notice: do not log meta-events eg. '<series>'
        if (event.name.startsWith("<") !== true) {
          spinner.succeed(
            "Finished '" + event.name + "'"
          );

          // Restart the spinner
          spinner.start();
        }
      });
    }
  }


  // jscs:enable disallowIdentifierNames
}


(new ChappeCLI()).run();


================================================
FILE: bower.json
================================================
{
  "name": "chappe",

  "dependencies": {
    "reset.css": "https://github.com/shannonmoeller/reset-css.git#d8bfbea7095dd54700fdb7ff05953b27862c5363",
    "console": "https://github.com/valeriansaliou/console.js.git#fe138ee08caba80aadf7f2794e40131f40988755",
    "cookies": "https://github.com/crisp-dev/Cookies.git#ba57f18775e726a80bab9aaad35ee479c6520963",
    "cash": "https://github.com/fabiospampinato/cash.git#f7b4fc2ce0fc02eb367ecc6e5092d27e7c9809ba",
    "minisearch": "https://github.com/lucaong/minisearch.git#917cf84b2ff79f3ba5612f4a5d5f542b74f78bbf",
    "prism.js": "https://github.com/PrismJS/prism.git#61221218f1773ef5d4d8c60b02ed02c2d2e976ac"
  }
}


================================================
FILE: examples/acme-docs/config.json
================================================
{
  "identity" : {
    "title"     : "Acme Developer Hub",
    "copyright" : "Acme Inc."
  },

  "theme" : {
    "accent" : {
      "light" : {
        "base"   : "#2275f1",
        "active" : "#0f69ef"
      },

      "dark" : {
        "base"   : "#e0e7ed",
        "active" : "#d5dde4"
      }
    }
  },

  "urls" : {
    "base" : "https://docs.acme.com"
  },

  "favicons" : {
    "main"  : "favicons/favicon.ico",

    "sizes" : {
      "default" : "favicons/favicon.png",
      "512x512" : "favicons/favicon-512x512.png",
      "256x256" : "favicons/favicon-256x256.png",
      "128x128" : "favicons/favicon-128x128.png",
      "32x32"   : "favicons/favicon-32x32.png"
    }
  },

  "images" : {
    "illustrations" : {
      "home" : "images/illustrations/home.png"
    },

    "logos" : {
      "header_full"  : "images/logos/logo-header-full.svg",
      "header_short" : "images/logos/logo-header-short.svg",
      "footer"       : "images/logos/logo-footer.svg"
    },

    "metas" : {
      "opengraph" : "images/metas/opengraph.png"
    },

    "categories" : {
      "guides" : {
        "hello-world"     : "images/categories/guides/hello-world.svg",
        "markdown-syntax" : "images/categories/guides/markdown-syntax.svg"
      },

      "references" : {
        "rest-api" : "images/categories/references/rest-api.svg",
        "rtm-api"  : "images/categories/references/rtm-api.svg",
        "team"     : "images/categories/references/team.svg"
      }
    }
  },

  "dimensions" : {
    "logos" : {
      "header_full"  : {
        "width" : 136
      },

      "header_short" : {
        "width" : 46
      },

      "footer"       : {
        "width" : 84
      }
    }
  },

  "colors" : {
    "changes" : {
      "groups" : {
        "rest_api"  : "#d34040",
        "rtm_api"   : "#dc9b37",
        "web_hooks" : "#1a71f5"
      }
    }
  },

  "texts" : {
    "home" : {
      "title" : "Welcome to the Acme Developer Hub",
      "label" : "Build apps for 10M+ Acme app users."
    },

    "changes" : {
      "groups" : {
        "rest_api"  : "REST API",
        "rtm_api"   : "RTM API",
        "web_hooks" : "Web Hooks"
      },

      "titles" : {
        "feed"   : "Acme Platform Changes",
        "latest" : "Latest Platform Changes",
        "year"   : "Platform Changes In"
      },

      "notice" : "**The Acme Platform is updated every day with improvements and new features.** This page is a ledger of all notable changes that occured over time, and which may impact your integrations.\n\n**Note that breaking changes appear in red.**\n\nIf you would like to monitor those changes, an RSS feed is available at: [`changes.rss`](/changes.rss)"
    }
  },

  "links" : {
    "header" : {
      "navigation" : [
        {
          "route" : ["guides"],
          "label" : "Guides"
        },

        {
          "route"    : ["references"],
          "label"    : "References",

          "dropdown" : [
            {
              "route" : ["rest-api", "v1"],
              "label" : "REST API Reference"
            },

            {
              "route" : ["rtm-api", "v1"],
              "label" : "RTM API Reference"
            }
          ]
        }
      ],

      "actions" : [
        {
          "label"    : "Sign Up",

          "dropdown" : [
            {
              "title"    : "Sign Up to Acme",
              "subtitle" : "Create your Acme user account.",
              "target"   : "https://dashboard.acme.com/"
            },

            {
              "title"    : "Sign Up to Developer Center",
              "subtitle" : "Start building Acme integrations.",
              "target"   : "https://developers.acme.com/"
            }
          ]
        }
      ]
    },

    "footer" : {
      "navigation" : [
        {
          "label"  : "Acme Website",
          "target" : "https://acme.com/"
        },

        {
          "label"  : "Terms of Use",
          "target" : "https://acme.com/terms/"
        },

        {
          "label"  : "Privacy Policy",
          "target" : "https://acme.com/privacy/"
        },

        {
          "label"  : "About Us",
          "target" : "https://acme.com/about/"
        },

        {
          "label"  : "Read our Blog",
          "target" : "https://blog.acme.com/"
        }
      ]
    }
  },

  "actions" : {
    "home" : [
      {
        "route" : ["guides"],
        "label" : "Build your First Acme App"
      }
    ]
  },

  "bulletpoints" : {
    "home" : {
      "quickstart" : {
        "description" : "Start building on the top of the Acme Platform in a matter of minutes.",

        "actions"     : [
          {
            "route" : ["guides"],
            "label" : "Start building"
          }
        ]
      },

      "guides" : {
        "description" : "Guides & tutorials on how to integrate Acme with your systems.",

        "actions"     : [
          {
            "route" : ["guides", "hello-world"],
            "label" : "Introduction"
          },

          {
            "route" : ["guides", "markdown-syntax", "syntax"],
            "label" : "Markdown Syntax"
          }
        ]
      },

      "references" : {
        "description" : "In-depth technical references of all available Acme APIs.",

        "actions"     : [
          {
            "route" : ["references", "rest-api", "v1"],
            "label" : "REST API"
          },

          {
            "route" : ["references", "rtm-api", "v1"],
            "label" : "RTM API"
          }
        ]
      }
    }
  },

  "includes" : {
    "scripts" : {
      "urls"   : [],
      "inline" : []
    },

    "stylesheets" : {
      "urls"   : [],
      "inline" : []
    }
  },

  "tokens" : {
    "crisp_website_id" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  },

  "plugins" : {
    "code" : {
      "syntaxes" : [
        "markup",
        "markup-templating",
        "css",
        "clike",
        "c",
        "javascript",
        "bash",
        "go",
        "java",
        "groovy",
        "json",
        "php",
        "python",
        "ruby",
        "rust",
        "swift",
        "objectivec"
      ]
    }
  },

  "features" : {
    "support" : true
  },

  "rules" : {
    "build_size" : {
      "fail"  : true,

      "sizes" : {
        "references" : {
          "pages" : {
            "gzip_maximum" : 80000
          }
        },

        "guides" : {
          "pages" : {
            "gzip_maximum" : 25000
          },

          "images" : {
            "maximum" : 500000
          }
        },

        "data" : {
          "objects" : {
            "maximum"      : 140000,
            "gzip_maximum" : 25000
          }
        }
      }
    }
  }
}


================================================
FILE: examples/acme-docs/data/changes/2020.json
================================================
[
  {
    "group" : "rest_api",
    "type"  : "change",
    "date"  : "2020-12-08",

    "text"  : "The REST API now rejects all requests without an `application/json` `Content-Type` header. Please update your apps!"
  },

  {
    "group" : "web_hooks",
    "type"  : "change",
    "date"  : "2020-05-28",

    "text"  : "The Web Hooks delivery user-agent has been updated to: `Acme-Hooks-Deliver (1.0)`"
  }
]


================================================
FILE: examples/acme-docs/data/changes/2021.json
================================================
[
  {
    "group" : "rest_api",
    "type"  : "change",
    "date"  : "2021-12-03",

    "text"  : "New official REST API libraries have been added for Golang and Java."
  },

  {
    "group" : "web_hooks",
    "type"  : "deprecation",
    "date"  : "2021-05-23",

    "text"  : "The Web Hooks retry system **has been phased out**. If your server does not handle Web Hooks from us immediately, we will not attempt to deliver them again anymore."
  },

  {
    "group" : "rtm_api",
    "type"  : "change",
    "date"  : "2021-01-04",

    "text"  : "The RTM API now **auto-closes the socket channel** if the authentication payload is not received in a timely manner (20 seconds maximum)."
  }
]


================================================
FILE: examples/acme-docs/data/guides/hello-world/index.md
================================================
TITLE: Hello World
INDEX: 1
UPDATED: 2021-09-22
LINK: REST API Reference -> /references/rest-api/v1/
LINK: RTM API Reference -> /references/rtm-api/v1/

Hello World! We advise that you start with the [Quickstart guide](./quickstart/), which will get you running in a few minutes.

+ Navigation
  | Quickstart: Learn how to use our systems in minutes. -> ./quickstart/


================================================
FILE: examples/acme-docs/data/guides/hello-world/quickstart/index.md
================================================
TITLE: Quickstart
INDEX: 1
UPDATED: 2021-09-22

**To retrieve and send data on behalf of Acme teams, you need to use the REST API. The REST API can be connected to over HTTPS.**

This guide walks you through using the REST API. Note that the REST API can be used aside to the RTM API which lets you receive real-time events associated to any action that you take with the REST API.

# Overview Schematic

![Overview schematic](schematic-overview.png)

_👉 The REST API lets your integration access features that regular Acme operator users have access to, such as sending messages. Note that the RTM API, not covered in this guide, lets you receive real-time events, and can be used side by side with the REST API._

---

# In-Depth Video Tutorial

${youtube}[In-depth Introduction to the Acme REST API](4A37k4KAHfo)

---

# How To Use

## Use a library

The most convenient way to use the REST API is to use a REST API-compatible library that we provide.

Let's provide an example on how you can make requests to the REST API in your code.

#### 1. Import and configure the library

```javascript
var Acme = require("acme-api");

// Create the Acme client (it lets you access the REST API and RTM API at once)
var AcmeClient = new Acme();

// Configure your Acme authentication tokens ('app' token)
AcmeClient.authenticateTier("app", "<token_identifier>", "<token_key>");
```

#### 2. Make requests to the API

```javascript
// <previous code eluded>

// Send text message for `team_id` and `session_id`
// Note: replace '<team_id>' and '<session_id>' with your values
AcmeClient.team.sendMessageInConversation(
  "<team_id>", "<session_id>",

  {
    type    : "text",
    from    : "operator",
    origin  : "chat",
    content : "This was sent from the REST API!"
  }
)
  .then(() => {
    console.info("Message has been sent.");
  })
  .catch((error) => {
    console.error("Message could not be sent, because:", error);
  });
```

---

## Direct usage (no library)

**In case no library is available, it is still easy to use the REST API using an HTTP library** available for your programming language. As long as you are able to make requests in all available HTTP methods, set body content, and configure request headers, then you are good to go.

**The HTTP endpoint base you will need to use is:** `https://api.acme.com/v1/`. Note that all requests must be done via HTTPS (HTTP over TLS), we do not support HTTP (plain text HTTP).

**The full list of HTTP routes available to you**, as well as the data input that they expect, is available on the [REST API Reference](/references/rest-api/v1/).

! The examples that follow **will use cURL over the command-line**, as API users that do not rely on a library are most-likely using the command-line instead.

!! Make sure to replace `{identifier}:{key}` with your token keypair (keep the middle `:` separator). Also, replace `{team_id}` with your team identifier, and any other value with `{brackets}`.

#### Example 1: Get team details

```bash
curl https://api.acme.com/v1/team/{team_id} \
  --get \
  --user "{identifier}:{key}" \
  --header "X-Acme-Tier: app"
```

#### Example 2: Send a text message

```bash
curl https://api.acme.com/v1/team/{team_id}/conversation/{session_id}/message \
  --user "{identifier}:{key}" \
  --header "Content-Type: application/json" \
  --header "X-Acme-Tier: app" \
  --data '{ "type": "text", "from": "operator", "origin": "chat", "content": "This was sent with cURL!" }'
```

_Note that `--post` does not need to be passed there, as cURL assumes it implicitly if `--data` is being used._

---

# Usage Considerations

## Rate-limits (quotas)

**The REST API is subject to rate-limits and daily quotas** to protect our systems against abuse. It guarantees that the REST API stays as fast and responsive as possible for all Acme users.

---

## Submitted data

**Whenever you submit data to the API using `POST`, `PUT` or `PATCH`, it gets validated against a data schema.**

The [REST API Reference](/references/rest-api/v1/) specifies the data format that schemas enforce, within the Data Structure section, for each request.

Whenever you submit data to the REST API, if it gets rejected by schema validators, **you will receive the following response error: `invalid_data`**. A message may also be provided, with explanations of what you did wrong.


================================================
FILE: examples/acme-docs/data/guides/index.md
================================================
TITLE: Guides
INDEX: 1
UPDATED: 2021-09-22

**👋 Welcome to the Acme Developer Hub!** We have written extensive guides to help you build powerful integrations with Acme. This ranges from our REST API to our RTM API.

Guides are classified in categories, for each system available to you. Aside from guides, you may also find references. References are more technical, as they serve as full specifications of our systems. You may want to use guides and references aside, switching between both of them.

+ Navigation
  | Hello World: Guides on how to start using our systems. -> ./hello-world/ [blank]
  | Markdown Syntax: Guides on the Markdown syntax you can use. -> ./markdown-syntax/

! If you have any technical question while building your integration with Acme, feel free to [chat with our support team](#crisp-chat-open). Our technical support team will gladly help you fix any issue you encounter.


================================================
FILE: examples/acme-docs/data/guides/markdown-syntax/index.md
================================================
TITLE: Markdown Syntax
INDEX: 2
UPDATED: 2022-01-05

This is the Markdown syntax guide, it contains two sections:

+ Navigation
  | Syntax: Guide of all available Markdown elements. -> ./syntax/
  | Section Example: Examples of sub-sections. -> ./section-example/


================================================
FILE: examples/acme-docs/data/guides/markdown-syntax/section-example/index.md
================================================
TITLE: Section Example
INDEX: 2
UPDATED: 2022-01-05

This section contains two example sub-sections. **Pick one in the sidebar**, or the navigation menu below.

+ Navigation
  | Sub-Section One: First example sub-section. -> ./sub-section-one/
  | Sub-Section Two: Second example sub-section. -> ./sub-section-two/


================================================
FILE: examples/acme-docs/data/guides/markdown-syntax/section-example/sub-section-one/index.md
================================================
TITLE: Sub-Section One
INDEX: 1
UPDATED: 2022-01-05

! This is an example of a sub-section! **(number one)**


================================================
FILE: examples/acme-docs/data/guides/markdown-syntax/section-example/sub-section-two/index.md
================================================
TITLE: Sub-Section Two
INDEX: 1
UPDATED: 2022-01-05

!! This is an example of a sub-section! **(number two)**


================================================
FILE: examples/acme-docs/data/guides/markdown-syntax/syntax/index.md
================================================
TITLE: Syntax
INDEX: 1
UPDATED: 2022-01-05

# Navigation Links

You can easily insert navigation links anywhere as such:

+ Navigation
  | Hello World Quickstart: Read the Hello World guide. -> /guides/hello-world/quickstart/
  | Example Sub-Section One: First example sub-section. -> /guides/markdown-syntax/section-example/sub-section-one/
  | Example Sub-Section Two: Second example sub-section. -> /guides/markdown-syntax/section-example/sub-section-two/

---

# Text Samples

Sem integer vitae justo eget magna. Euismod lacinia at quis risus. Tellus cras adipiscing enim eu turpis egestas. Fringilla urna porttitor rhoncus dolor purus non. Commodo viverra maecenas accumsan lacus vel. **Feugiat in fermentum posuere urna nec tincidunt praesent semper feugiat**.

**Turpis egestas integer eget aliquet nibh.** Pharetra convallis posuere morbi leo urna molestie at. Metus aliquam eleifend mi in. Scelerisque varius morbi enim nunc faucibus a. Condimentum id venenatis a condimentum vitae sapien. Neque [ornare aenean](#) euismod elementum nisi quis. Lacus laoreet non curabitur gravida arcu ac tortor. Vestibulum lectus mauris ultrices eros in!

Leo urna molestie at _elementum_ eu **_facilisis_** sed, eg. `team:conversation:messages` Nam aliquam sem et tortor consequat id porta nibh.

Sagittis vitae et leo duis ut. **Suspendisse faucibus interdum** posuere lorem ipsum dolor sit amet. Nisl tincidunt eget nullam non nisi est sit amet. Urna nunc id cursus metus aliquam eleifend. 😀

Turpis massa tincidunt dui ut ornare lectus sit. Eget dolor morbi non arcu risus quis varius quam quisque. Maecenas sed enim ut sem viverra. Etiam tempor orci eu lobortis elementum nibh. **Tristique senectus et netus et malesuada fames.**

!!! Commodo viverra maecenas accumsan lacus vel! Eget egestas purus viverra accumsan in nisl nisi. Blandit aliquam etiam erat velit scelerisque in dictum non consectetur. Nam at lectus urna duis convallis. 😅

!! Mattis molestie a **iaculis at erat** pellentesque adipiscing. Posuere lorem ipsum dolor sit amet. Nec tincidunt praesent semper feugiat nibh sed. Vestibulum lorem sed risus ultricies tristique nulla aliquet enim.

! Id diam maecenas ultricies mi eget mauris pharetra et ultrices. Pretium lectus quam id leo in vitae. Posuere ac ut consequat semper viverra. Massa massa ultricies mi quis hendrerit. Nullam ac tortor vitae purus faucibus.

In tellus integer feugiat scelerisque varius morbi enim nunc:

> “Id diam maecenas ultricies mi eget mauris pharetra et ultrices. Pretium lectus quam id leo in vitae. Posuere ac ut consequat semper viverra. Massa massa ultricies mi quis hendrerit. Nullam ac tortor vitae purus faucibus.”
>
> Valerian.

---

# Footnote Samples

This sentence has two[^foo] footnotes[^bar].

---

# List Samples

Nunc lobortis mattis aliquam faucibus purus in massa. Risus feugiat in ante metus dictum at tempor. Felis eget velit aliquet sagittis id consectetur.

**Vestibulum lectus mauris ultrices eros in:**

* Access to buckets: `bucket:url`
* Access to availability information: `team:availability`
* Access to conversation initiate: `team:conversation:initiate`
* Access to conversation sessions: `team:conversation:sessions`
* Access to conversation messages: `team:conversation:messages`
* Access to a people profiles: `team:people:profiles`
* Access to a people conversations: `team:people:conversations`
* Access to a people events: `team:people:events`

**Vestibulum lectus mauris ultrices eros in:**

1. Access to buckets: `bucket:url`
2. Access to availability information: `team:availability`
3. Access to conversation initiate: `team:conversation:initiate`

---

# Code Sample

Inline code: `echo "hello world"`

Block code:

```javascript
var dump = {
  error : false
};

console.log("Hello World", dump);
```

---

# Video Embed Sample

YouTube videos can be included in Markdown, as such:

${youtube}[An Architect's Own House Situated on a Remote Beach](LildjJAG0fk)

---

# Full-Width Image Sample

Full-width images can be included as such:

![](image-caption.jpg)

---

# Caption Image Sample

Images can be included with a caption, as such:

$[Sample Mountain Image](![](image-caption.jpg))

---

# Table Sample

Est ullamcorper eget nulla facilisi etiam dignissim diam quis enim. **Sit amet dictum sit amet justo donec enim diam vulputate.**

Elit eget gravida cum sociis natoque penatibus et. Ut faucibus pulvinar elementum integer enim neque:

| Route Name | API Reference | Associated Scopes | Comments |
| --- | --- | --- | --- |
| Check If Conversation Exists | [Read reference](/references/rest-api/v1/) | `team:conversation:sessions` | — |
| Initiate Conversation With Session | [Read reference](/references/rest-api/v1/) | `team:conversation:initiate` | Write permission required |
| Send Message In Conversation | [Read reference](/references/rest-api/v1/) | `team:conversation:messages` | Write permission required |
| Assign Conversation Routing | [Read reference](/references/rest-api/v1/) | `team:conversation:routing` | Write permission required |
| List Conversation Pages | [Read reference](/references/rest-api/v1/) | `team:conversation:pages` | — |
| Get Conversation State | [Read reference](/references/rest-api/v1/) | `team:conversation:states` | — |
| Request Email Transcript | [Read reference](/references/rest-api/v1/) | `team:conversation:actions` | Write permission required |

Ac tortor vitae purus faucibus ornare suspendisse. Egestas sed tempus urna et pharetra. Euismod nisi porta lorem mollis aliquam ut porttitor! 👏

---

# Title Samples

## Level 2 Title

### Level 3 Title

#### Level 4 Title

##### Level 5 Title

###### Level 6 Title

---

[^foo]: This is an example footnote.
[^bar]: Note that it has to be defined **on a single line**, but using `<br/>`<br/>you can still make it multiline if needed.


================================================
FILE: examples/acme-docs/data/references/rest-api/_private.md
================================================
TYPE: API Blueprint
TITLE: REST API Reference (Private)
UPDATED: 2021-11-03
FORMAT: 1A
HOST: https://api.acme.com/v1

# Reference

This reference documents private Acme API routes.

☢️ **Those routes are solely used by Acme systems to perform eg. account management and other tasks. This may not be shared to public users, although routes are still available for them to access through the same API endpoint than the public one.**

# Group Email

Manages Acme emails.

## Subscription [/email/{email_hash}/subscription]

Manages email subscriptions. Used to subscribe or unsubscribe from Acme notification emails.

### Get Subscription Status [GET /email/{email_hash}/subscription/{key}{?team_id}]

Resolves current subscription status (subscribed or unsubscribed).

+ Attributes
    + error (boolean)
    + reason (string)
    + data (object)
        + subscribed (boolean) - Whether email is subscribed or not

+ Parameters
    + email_hash (string) - Email secure hash
    + key (string) - Private security for given email
    + team_id (string, optional) - Team identifier for email

+ Request Get Subscription Status (application/json)

    + Body

+ Response 200 (application/json)

    + Body

        ```
        {
            "error": false,
            "reason": "resolved",

            "data": {
                "subscribed": true
            }
        }
        ```

+ Response 404 (application/json)

    + Body

        ```
        {
            "error": true,
            "reason": "email_not_found",
            "data": {}
        }
        ```


================================================
FILE: examples/acme-docs/data/references/rest-api/v1.md
================================================
TYPE: API Blueprint
TITLE: REST API Reference (V1)
UPDATED: 2021-12-22
FORMAT: 1A
HOST: https://api.acme.com/v1

# Reference

The Acme REST API offers access and control over all Acme data.

All resources that you will most likely use are prefixed with a star symbol (⭐).

**While integrating the REST API, you may be interested in the following guides:**

+ Navigation
  | Quickstart: Get started in minutes. -> /guides/hello-world/quickstart/

# Group Team

Manages Acme teams.

## Base [/team]

Manages teams.

### Check If Team Exists [HEAD /team{?domain}]

Checks if given team exists (by domain).

+ Parameters
    + domain (string, required) - The team domain to check against

+ Request Check If Team Exists (application/json)

    + Tiers: `user` `app`

    + Body

+ Response 200 (application/json)

+ Response 404 (application/json)

### Create Team [POST /team]

Creates a new team.

+ Attributes
    + name (string, required) - Team name
    + domain (string, required) - Team domain

+ Request Create Team (application/json)

    + Tiers: `user`

    + Body

        ```
        {
            "name": "Acme, Inc.",
            "domain": "acme-inc.com"
        }
        ```

+ Response 201 (application/json)

    + Body

        ```
        {
            "error": false,
            "reason": "added",

            "data": {
                "team_id": "e2efddb0-d1ce-47fd-99f5-d3a5b69f1def"
            }
        }
        ```

+ Response 423 (application/json)

    + Body

        ```
        {
            "error": true,
            "reason": "quota_limit_exceeded",
            "data": {}
        }
        ```

### Get A Team [GET /team/{team_id}]

Resolves an existing team information.

+ Attributes
    + error (boolean)
    + reason (string)
    + data (object)
        + team_id (string) - Team identifier
        + name (string) - Team name
        + domain (string) - Team domain

+ Parameters
    + team_id (string) - The team identifier

+ Request Get Team Information (application/json)

    + Tiers: `user` `app`

    + Body

+ Response 200 (application/json)

    + Body

        ```
        {
            "error": false,
            "reason": "resolved",

            "data": {
                "team_id": "8c842203-7ed8-4e29-a608-7cf78a7d2fcc",
                "name": "Acme",
                "domain": "acme.com"
            }
        }
        ```

+ Response 403 (application/json)

    + Body

        ```
        {
            "error": true,
            "reason": "not_allowed",
            "data": {}
        }
        ```

+ Response 404 (application/json)

    + Body

        ```
        {
            "error": true,
            "reason": "not_subscribed",
            "data": {}
        }
        ```

### Delete A Team [DELETE /team/{team_id}]

Deletes an existing team.

+ Attributes
    + verify (string, required) - User password (used to double-authenticate deletion)

+ Parameters
    + team_id (string) - The team identifier

+ Request Delete A Team (application/json)

    + Tiers: `user`

    + Body

        ```
        {
            "verify": "MySuperSecurePassword"
        }
        ```

+ Response 200 (application/json)

    + Body

        ```
        {
            "error": false,
            "reason": "deleted",
            "data": {}
        }
        ```

+ Response 403 (application/json)

    + Body

        ```
        {
            "error": true,
            "reason": "not_allowed",
            "data": {}
        }
        ```

+ Response 404 (application/json)

    + Body

        ```
        {
            "error": true,
            "reason": "team_not_found",
            "data": {}
        }
        ```

+ Response 423 (application/json)

    + Body

        ```
        {
            "error": true,
            "reason": "password_unverified",
            "data": {}
        }
        ```

## Conversations [/team/{team_id}/conversations]

Manages multiple team conversations.

### ⭐ List Conversations [GET /team/{team_id}/conversations/{page_number}{?search_query}{&search_type}{&search_operator}]

Lists conversations for team.

+ Attributes
    + error (boolean)
    + reason (string)
    + data (array)
        + (object)
            + session_id (string) - Session identifier
            + team_id (string) - Team identifier
            + people_id (string) - People identifier
            + state (enum[string]) - Conversation state
                + Members
                    + `pending`
                    + `unresolved`
                    + `resolved`
            + status (enum[number]) - Conversation status (an alias of state; useful for sorting conversations)
                + Members
                    + `0` - Numeric code for pending status
                    + `1` - Numeric code for unresolved status
                    + `2` - Numeric code for resolved status
            + is_verified (boolean) - Whether session is verified or not (user email ownership is authenticated)
            + is_blocked (boolean) - Whether session is blocked or not (block messages from visitor)
            + availability (enum[string]) - Visitor availability
                + Members
                    + `online`
                    + `offline`
            + active (object) - User activity statistics
                + now (boolean) - Whether user is considered active right now or not
                + last (number) - Timestamp at which the user was last active
            + last_message (string) - Last message excerpt
            + participants (array) - External participants for this conversation
                + (object)
                    + type (enum[string]) - External participant type
                        + Members
                            + `email`
                    + target (string) - External participant target (ie. email address, identifier, etc.)
            + mentions (array[string]) - Mentioned user identifiers (from conversation messages)
            + created_at (number) - Conversation creation timestamp
            + updated_at (number) - Conversation update timestamp
            + compose (object) - Compose states
                + operator (object) - Compose state for operator
                    + type (enum[string]) - Compose state type
                        + Members
                            + `start`
                            + `stop`
                    + excerpt (string) - Message excerpt for compose state
                    + timestamp (number) - Timestamp for compose state
                    + user (object) - Compose user information
                        + user_id (string) - Compose user identifier
                        + nickname (string) - Compose user nickname
                        + avatar (string) - Compose user avatar
                + visitor (object) - Compose state for visitor
                    + type (enum[string]) - Compose state type
                        + Members
                            + `start`
                            + `stop`
                    + excerpt (string) - Message excerpt for compose state
                    + timestamp (number) - Timestamp for compose state
            + unread (object) - Unread messages counters
                + operator (number) - Unread messages counter for operator
                + visitor (number) - Unread messages counter for visitor
            + assigned (object) - Assigned operator (if any)
                + user_id (string) - Operator user identifier
            + meta (object) - Meta-data for conversation
                + nickname (string) - Visitor nickname
                + email (string) - Visitor email
                + phone (string) - Visitor phone
                + address (string) - Visitor address
                + ip (string) - Visitor IP address
                + data (object) - Visitor data
                + avatar (string) - Visitor avatar
                + device (object) - Device information
                    + capabilities (array) - Visitor device capabilities
                        + (enum[string])
                            + Members
                                + `browsing`
                                + `call`
                    + geolocation (object) - Geolocation information for visitor device
                        + country (string) - Country code
                        + region (string) - Region code
                        + city (string) - City name
                        + coordinates (object) - Location coordinates
                            + latitude (number) - Latitude coordinate
                            + longitude (number) - Longitude coordinate
                    + system (object) - Visitor device system information
                        + os (object) - Operating system information
                            + version (string) - OS version
                            + name (string) - OS name
                        + engine (object) - Rendering engine information
                            + version (string) - Engine version
                            + name (string) - Engine name
                        + browser (object) - Browser information
                            + major (string) - Browser major version (eg: version 8.1 has a major of 8)
                            + version (string) - Browser version
                            + name (string) - Browser name
                        + useragent (string) - Visitor user agent
                    + timezone (number) - Visitor device timezone offset (UTC)
                    + locales (array[string]) - Visitor device locales
                + segments (array[string]) - Segments attributed to conversation

+ Parameters
    + team_id (string) - The team identifier
    + page_number (number, optional) - Page number for conversations paging
    + search_query (string, optional) - Search query in all conversations (text if type is `text` or `segment`, filter if type is `filter`)
    + search_type (string, optional) - Search type (either `text`, `segment` or `filter`)
    + search_operator (string, optional) - Search operator if search type is `filter` (`or` or `and` respectful to boolean algebra, defaults to `and` if not set)

+ Request List Conversations (application/json)

    + Tiers: `user` `app`
    + Scopes: `team:conversation:sessions`

    + Body

+ Response 206 (application/json)

    + Body

        ```
        {
            "error": false,
            "reason": "listed",

            "data": [
                {
                    "session_id": "session_aaea8e1d-d6e3-4238-9252-d8c2e5579f5c",
                    "team_id": "8c842203-7ed8-4e29-a608-7cf78a7d2fcc",
                    "people_id": "0cb89450-34fb-4d51-8905-040c1d14a594",
                    "status": 1,
                    "state": "unresolved",
                    "is_verified": false,
                    "is_blocked": false,
                    "availability": "offline",

                    "active": {
                        "now": false
                    },

                    "last_message": "All right, thanks.",
                    "mentions": [],

                    "participants": [
                        {
                            "type": "email",
                            "target": "jane.doe@acme-inc.com"
                        }
                    ],

                    "updated_at": 1468401603070,
                    "created_at": 1468341857826,

                    "unread": {
                        "operator": 0,
                        "visitor": 1
                    },

                    "assigned": {
                        "user_id": "a4c32c68-be91-4e29-8a05-976e93abbe3f"
                    },

                    "meta": {
                        "nickname": "Dan Boy",
                        "email": "dan.boy@acme-inc.com",
                        "ip": "104.236.186.68",
                        "avatar": null,

                        "device": {
                            "capabilities": [
                                "call"
                            ],

                            "geolocation": {
                                "country": "US",
                                "region": "CA",
                                "city": "San Francisco",

                                "coordinates": {
                                    "latitude": 37.7749,
                                    "longitude": -122.4194
                                }
                            },

                            "system": {
                                "os": {
                                    "version": "10.11.5",
                                    "name": "Mac OS"
                                },

                                "engine": {
                                    "name": "WebKit",
                                    "version": "537.36"
                                },

                                "browser": {
                                    "major": "51",
                                    "version": "51.0.2683.0",
                                    "name": "Chrome"
                                },

                                "useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2683.0 Safari/537.36"
                            },

                            "timezone": -120,

                            "locales": [
                                "en",
                                "fr"
                            ]
                        },

                        "segments": [
                            "customer",
                            "friend"
                        ]
                    }
                }
            ]
        }
        ```

+ Response 400 (application/json)

    + Body

        ```
        {
            "error": true,
            "reason": "search_query_too_long",
            "data": {}
        }
        ```

+ Response 402 (application/json)

    + Body

        ```
        {
            "error": true,
            "reason": "subscription_upgrade_required",
            "data": {}
        }
        ```

+ Response 403 (application/json)

    + Body

        ```
        {
            "error": true,
            "reason": "not_allowed",
            "data": {}
        }
        ```

+ Response 404 (application/json)

    + Body

        ```
        {
            "error": true,
            "reason": "not_subscribed",
            "data": {}
        }
        ```

+ Response 423 (application/json)

    + Body

        ```
        {
            "error": true,
            "reason": "quota_limit_exceeded",
            "data": {}
        }
        ```


================================================
FILE: examples/acme-docs/data/references/rtm-api/v1.md
================================================
TYPE: Markdown
TITLE: RTM API Reference (V1)
UPDATED: 2021-09-22

Events are sent on the RTM Events API WebSocket channel that you can open alongside your REST API channel, which allows you to receive asynchronous replies and events for some of your actions via the REST API.

+ Navigation
  | Quickstart: Get started in minutes. -> /guides/hello-world/quickstart/

# Endpoint

You may subscribe to events by opening a [Socket.IO](https://socket.io/) connection to the WebSocket endpoint.

**The RTM API endpoint URL is:** `wss://rtm.acme.com/`

! **There is a limit on the maximum number of connections that can be simultaneously open** with the RTM API (per-token, per-team and per-IP). This limit is quite high and can be revised at any time. Please make sure to teardown any unused connection before opening a new one. _Most use cases require a single RTM API connection._

---

# Namespaces

Available RTM event namespaces are listed below.

!! Note that if your API token is a `app` tier token, then you will **only have access to the RTM events that your token scopes allow**, based on the API routes that you have access to. Required scopes and tiers are listed for each event below.

# Events

## Session Events

### Session Update Availability

* **Event**: `session:update_availability`
* **Description:** session availability changed _(eg. online to offline)_
* **Tiers:** `user` `app`
* **Scopes:** `team:conversation:sessions` + `read`

```json
{
  "team_id": "42286ab3-b29a-4fde-8538-da0ae501d825",
  "session_id": "session_36ba3566-9651-4790-afc8-ffedbccc317f",
  "availability": "online"
}
```

### Session Update Verify

* **Event**: `session:update_verify`
* **Description:** session verification status changed
* **Tiers:** `user` `app`
* **Scopes:** `team:conversation:sessions` + `read`

```json
{
  "team_id": "42286ab3-b29a-4fde-8538-da0ae501d825",
  "session_id": "session_36ba3566-9651-4790-afc8-ffedbccc317f",
  "is_verified": true
}
```

---

## Message Events

### Message Updates

* **Event**: `message:updated`
* **Description:** message has been updated
* **Tiers:** `user` `app`
* **Scopes:** `team:conversation:messages` + `read`

```json
{
  "team_id": "42286ab3-b29a-4fde-8538-da0ae501d825",
  "session_id": "session_36ba3566-9651-4790-afc8-ffedbccc317f",
  "fingerprint": 163240180126629,
  "content": "This is an edited message!"
}
```


================================================
FILE: examples/acme-docs/package.json
================================================
{
  "dependencies": {
    "chappe": "1.x.x"
  }
}


================================================
FILE: gulpfile.js
================================================
/*
 * chappe
 *
 * Copyright 2021, Crisp IM SAS
 * Author: Valerian Saliou <valerian@valeriansaliou.name>
 */


var fs                     = require("fs");
var path                   = require("path");

var lodash                 = require("lodash");
var del                    = require("del");
var glob                   = require("glob");
var merge                  = require("merge-stream");
var marked                 = require("marked").marked;
var marked_footnote        = require("marked-footnote");
var marked_gfm_heading_id  = require("marked-gfm-heading-id").gfmHeadingId;
var marked_mangle          = require("marked-mangle").mangle;
var remove_markdown        = require("@tommoor/remove-markdown");
var MiniSearch             = require("minisearch");
var Feed                   = require("feed").Feed;

var gulp                   = require("gulp");

var gulp_connect           = require("gulp-connect");
var gulp_file              = require("gulp-file");
var gulp_bower             = require("gulp-bower");
var gulp_pug               = require("gulp-pug");
var gulp_sass              = require("gulp-sass")(require("sass"));
var gulp_inline_image      = require("gulp-inline-image");
var gulp_ogimage           = require("gulp-ogimage");
var gulp_sass_variables    = require("gulp-sass-variables");
var gulp_concat            = require("gulp-concat");
var gulp_babel             = require("gulp-babel");
var gulp_clean_css         = require("gulp-clean-css");
var gulp_uglify            = require("gulp-uglify");
var gulp_rename            = require("gulp-rename");
var gulp_replace           = require("gulp-replace");
var gulp_header            = require("gulp-header");
var gulp_pug_lint          = require("gulp-pug-lint");
var gulp_stylelint         = require("gulp-stylelint-esm").default;
var gulp_jshint            = require("gulp-jshint");
var gulp_jscs              = require("gulp-jscs");
var gulp_sizereport        = require("gulp-sizereport");
var gulp_sitemap           = require("gulp-sitemap");
var gulp_notify            = require("gulp-notify");
var gulp_noop              = require("gulp-noop");

var package                = require("./package.json");

var gulp_pug_templates     = require("./res/plugins/gulp/pug-templates");
var gulp_minisearch        = require("./res/plugins/gulp/minisearch");

var marked_renderers       = {
  heading : require("./res/plugins/marked/renderers/heading"),
  code    : require("./res/plugins/marked/renderers/code")
};

var marked_extensions      = {
  navigation      : require("./res/plugins/marked/extensions/navigation"),
  navigation_item : require("./res/plugins/marked/extensions/navigation-item"),
  emphasis        : require("./res/plugins/marked/extensions/emphasis"),
  figcaption      : require("./res/plugins/marked/extensions/figcaption"),
  embed           : require("./res/plugins/marked/extensions/embed")
};


// Global context

var PARENT_CONTEXT = (
  (typeof global.CONTEXT !== "undefined") ? global.CONTEXT : {}
);

var CONTEXT        = {
  // Data
  CONFIG        : {},
  SEARCH_INDEX  : null,

  // Configurations
  PATH_CONFIG       : (
    PARENT_CONTEXT.config ? PARENT_CONTEXT.config.split(",") : null
  ),

  PATH_ASSETS       : (PARENT_CONTEXT.assets || null),
  PATH_DATA         : (PARENT_CONTEXT.data   || null),
  PATH_TEMP         : (PARENT_CONTEXT.temp   || null),
  PATH_DIST         : (PARENT_CONTEXT.dist   || null),

  SERVE_HOST        : (PARENT_CONTEXT.host || null),
  SERVE_PORT        : (PARENT_CONTEXT.port || null),

  PATH_CHAPPE       : null,
  PATH_SOURCES      : null,
  PATH_LIBRARIES    : null,
  PATH_BUILD_PAGES  : null,
  PATH_BUILD_ASSETS : null,

  IS_PRODUCTION     : ((PARENT_CONTEXT.env === "production") ? true : false),
  IS_WATCH          : false
};


// Initializers

marked.use({
  renderer   : {
    heading : marked_renderers.heading,
    code    : marked_renderers.code
  },

  extensions : [
    marked_extensions.navigation,
    marked_extensions.navigation_item,
    marked_extensions.emphasis,
    marked_extensions.figcaption,
    marked_extensions.embed
  ]
});

marked.use(
  marked_footnote(),
  marked_gfm_heading_id(),
  marked_mangle()
);


// Tasks

/*
  Acquires the configuration
*/
var get_configuration = function(next) {
  // Assert that all paths are set
  if (CONTEXT.PATH_CONFIG === null || CONTEXT.PATH_ASSETS === null  ||
        CONTEXT.PATH_DATA === null || CONTEXT.PATH_TEMP === null    ||
        CONTEXT.PATH_DIST === null) {
    throw new Error(
      "A build path was not passed properly, please pass them as globals"
    );
  }

  // Make sure that the config and data paths exist
  // Notice: do not check the 'temp' and 'dist' path, as they will get \
  //   auto-created.
  if (fs.existsSync(CONTEXT.PATH_ASSETS) !== true) {
    throw new Error("The assets path provided does not exist!");
  }
  if (fs.existsSync(CONTEXT.PATH_DATA) !== true) {
    throw new Error("The data path provided does not exist!");
  }

  CONTEXT.PATH_CONFIG.forEach(function(config_path) {
    if (fs.existsSync(config_path) !== true) {
      throw new Error(
        "One of the configuration path provided does not exist!"
      );
    }
  });

  // Initialize final source paths
  CONTEXT.PATH_CHAPPE    = __dirname;
  CONTEXT.PATH_SOURCES   = path.join(CONTEXT.PATH_CHAPPE, "./src");
  CONTEXT.PATH_LIBRARIES = path.join(CONTEXT.PATH_TEMP, "./lib");

  // Initialize final build paths
  CONTEXT.PATH_BUILD_PAGES  = CONTEXT.PATH_DIST;
  CONTEXT.PATH_BUILD_ASSETS = (CONTEXT.PATH_DIST + "/static");

  // Read atomic configurations (merge them together)
  var _merge_pipeline = [
    // #1: Common configuration (project static)
    require("./res/config/common.json"),

    // #2: Site configuration (project defaults)
    {
      SITE : require("./res/config/user.json")
    }
  ];

  // Read vector configurations
  CONTEXT.PATH_CONFIG.forEach(function(config_path) {
    // #3: Site configuration (user-provided)
    _merge_pipeline.push({
      SITE : require(config_path)
    });
  });

  // Unwind merge pipeline
  _merge_pipeline.forEach(function(merge_object) {
    CONTEXT.CONFIG = lodash.merge(CONTEXT.CONFIG, merge_object);
  });

  // Assign contextual values
  CONTEXT.CONFIG.ENVIRONMENT = (
    (CONTEXT.IS_PRODUCTION === true) ? "production" : "development"
  );

  CONTEXT.CONFIG.REVISION = (
    "v" + (package.version || "0.0.0")
  );

  next();
};


/*
  Installs bower packages
*/
var bower = function() {
  return gulp_bower({
    directory : CONTEXT.PATH_LIBRARIES,
    cwd       : CONTEXT.PATH_CHAPPE,
    verbosity : 1
  });
};


/*
  Copies all user assets
*/
var copy_user_assets = function() {
  return gulp.src(
    CONTEXT.PATH_ASSETS + "/**/*"
  )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/user"
      )
    );
};


/*
  Copies images (base images)
*/
var copy_images_base = function() {
  return gulp.src(
    CONTEXT.PATH_SOURCES + "/images/**/*"
  )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/images"
      )
    )
    .pipe(
      gulp_connect.reload()
    );
};


/*
  Copies images (guides images)
*/
var copy_images_guides = function() {
  return gulp.src(
    CONTEXT.PATH_DATA + "/guides/**/*.{jpg,jpeg,png,gif}"
  )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/images/guides/content"
      )
    )
    .pipe(
      gulp_connect.reload()
    );
};


/*
  Copies fonts
*/
var copy_fonts = function() {
  return gulp.src(
    CONTEXT.PATH_SOURCES + "/fonts/**/*"
  )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/fonts"
      )
    )
    .pipe(
      gulp_connect.reload()
    );
};


/*
  Concats javascript libraries
*/
var concat_libraries_javascripts = function() {
  // Acquire targets
  // Notice: append user-defined syntaxes for code coloring (from 'prism.js')
  var _targets = CONTEXT.CONFIG.LIBRARIES.JAVASCRIPTS.map(function(library) {
    return (CONTEXT.PATH_LIBRARIES + "/" + library);
  });

  CONTEXT.CONFIG.SITE.plugins.code.syntaxes.forEach(function(syntax) {
    var _syntax_path = (
      CONTEXT.PATH_LIBRARIES + "/prism.js/components/prism-" + syntax + ".js"
    );

    if (fs.existsSync(_syntax_path) !== true) {
      throw new Error(
        "Unsupported code syntax plugin provided: " + syntax + " at path: "  +
          _syntax_path
      );
    }

    _targets.push(_syntax_path);
  });

  return gulp.src(_targets)
    .pipe(
      gulp_concat("common/libs.js")
    )
    .pipe(
      gulp_replace(
        "manual: _self.Prism && _self.Prism.manual,", "manual: true,"
      )
    )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/javascripts"
      )
    );
};


/*
  Concats stylesheet libraries
*/
var concat_libraries_stylesheets = function() {
  return gulp.src(
    CONTEXT.CONFIG.LIBRARIES.STYLESHEETS.map(function(library) {
      return (CONTEXT.PATH_LIBRARIES + "/" + library);
    })
  )
    .pipe(
      gulp_concat("common/libs.css")
    )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/stylesheets"
      )
    );
};


/*
  Prepares Minisearch search index (before it gets populated)
*/
var minisearch_prepare = function(next) {
  // Initialize search index
  CONTEXT.SEARCH_INDEX = new MiniSearch(
    CONTEXT.CONFIG.SEARCH.OPTIONS.BASE
  );

  next();
};


/*
  Consolidates Minisearch search index (after it was populated)
*/
var minisearch_consolidate = function() {
  // Generate search index data
  return gulp_file(
    ("./" + CONTEXT.CONFIG.SEARCH.INDEX),
    JSON.stringify(CONTEXT.SEARCH_INDEX),

    {
      src : true
    }
  )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/data"
      )
    );
};


/*
  Compiles Pug templates (base templates)
*/
var pug_templates_base = function() {
  var _stream = merge();

  gulp_pug_templates.list_config_locales(CONTEXT, package, marked)
    .forEach(function(pug_data) {
      _stream.add(
        gulp_pug_templates.pipe_commons(CONTEXT, pug_data.locale, function() {
          return gulp.src(
            CONTEXT.CONFIG.SOURCES.TEMPLATES.map(function(template) {
              return (CONTEXT.PATH_SOURCES + "/templates/" + template);
            }),

            {
              base : (CONTEXT.PATH_SOURCES + "/templates")
            }
          )
            .pipe(
              gulp_pug(pug_data.config)
            );
        })
          .pipe(
            gulp.dest(
              CONTEXT.PATH_BUILD_PAGES
            )
          )
          .pipe(
            gulp_connect.reload()
          )
      );
    });

  return _stream;
};


/*
  Compiles Pug templates (guides templates)
*/
var pug_templates_guides = function() {
  var _stream = merge();

  // Acquire the guides meta-data
  // Format: { \
  //   segments<array>, id<string>, title<string>, index<int>, links<array>, \
  //   updated<date>, markdown<string>, parent<object>, subtrees<array> \
  // }
  // Output: [tree<array>, linear<array>]
  var _guide_meta_data = (
    gulp_pug_templates.traverse_guides_tree(CONTEXT.PATH_DATA + "/guides")
  );

  var _guide_meta_tree   = _guide_meta_data[0],
      _guide_meta_linear = _guide_meta_data[1];

  // Append all guides to search index
  var _categories = _guide_meta_tree[0];

  if (_categories) {
    gulp_minisearch.insert_categories(
      CONTEXT, "guides", [_categories.title], (_categories.subtrees || [])
    );
  }

  // Compile all guide pages
  gulp_pug_templates.list_config_locales(CONTEXT, package, marked)
    .forEach(function(pug_data) {
      _guide_meta_linear.forEach(function(guide_level) {
        // Generate current guide data (for locale)
        var pug_level_config = lodash.cloneDeep(pug_data.config);

        pug_level_config.data.guides = _guide_meta_tree;
        pug_level_config.data.guide  = guide_level;

        // Forbid indexing? (if any entry segment starts with an underscore)
        var _segment_private_index = guide_level.segments.findIndex(
          function(segment) {
            return ((segment[0] === "_") ? true : false);
          }
        );

        if (_segment_private_index !== -1) {
          pug_level_config.data.INDEXING = false;
        }

        _stream.add(
          gulp_pug_templates.pipe_commons(
            CONTEXT, pug_data.locale, function() {
              return gulp.src(
                (CONTEXT.PATH_SOURCES + "/templates/guides/index.pug"),

                {
                  base : (CONTEXT.PATH_SOURCES + "/templates")
                }
              )
                .pipe(
                  gulp_pug(pug_level_config)
                )
                .pipe(
                  gulp_rename(function(file_path) {
                    file_path.basename = path.join.apply(this, [].concat(
                      guide_level.segments, ["index"]
                    ));
                  })
                );
            }
          )
            .pipe(
              gulp.dest(
                CONTEXT.PATH_BUILD_PAGES
              )
            )
        );
      });
    });

  return _stream;
};


/*
  Compiles Pug templates (references templates)
*/
var pug_templates_references = function() {
  var _stream = merge();

  // Acquire the parsed references content
  // Format: {segments<array>, type<string>, data<object>}
  // Output: linear<array>
  var _reference_meta_data = (
    gulp_pug_templates.traverse_references_linear(
      CONTEXT, (CONTEXT.PATH_DATA + "/references")
    )
  );

  gulp_pug_templates.list_config_locales(CONTEXT, package, marked)
    .forEach(function(pug_data) {
      _reference_meta_data.forEach(function(reference_entry) {
        // Generate current reference data (for locale)
        var pug_entry_config = lodash.cloneDeep(pug_data.config);

        pug_entry_config.data.reference = reference_entry;

        // Forbid indexing? (if any entry segment starts with an underscore)
        var _segment_private_index = reference_entry.segments.findIndex(
          function(segment) {
            return ((segment[0] === "_") ? true : false);
          }
        );

        if (_segment_private_index !== -1) {
          pug_entry_config.data.INDEXING = false;
        }

        // Append all reference anchors to search index
        // Important: only if reference indexing is not forbidden
        if (reference_entry.categories  &&
              pug_entry_config.data.INDEXING !== false) {
          // Acquire reference path, as well as reference path title (with \
          //   its common suffix extracted)
          // Notice: 'REST API Reference (V1)' becomes 'REST API' + 'V1'
          var _reference_path    = ["References"],
              _title_split_regex = /^(.+) Reference \(([^\(\)]+)\)$/;

          var _title_matcher = (
            reference_entry.title.match(_title_split_regex)
          );

          var _reference_title_path = (
            (_title_matcher && _title_matcher[1] && _title_matcher[2]) ?
              [_title_matcher[1], _title_matcher[2]] : [reference_entry.title]
          );

          // Insert reference base
          gulp_minisearch.insert_categories(
            CONTEXT, "references", _reference_path, [reference_entry]
          );

          // Insert reference sub-categories
          gulp_minisearch.insert_categories(
            CONTEXT, "references",
              [].concat(_reference_path, _reference_title_path),
              reference_entry.categories, reference_entry.segments
          );
        }

        _stream.add(
          gulp_pug_templates.pipe_commons(
            CONTEXT, pug_data.locale, function() {
              return gulp.src(
                (CONTEXT.PATH_SOURCES + "/templates/references/index.pug"),

                {
                  base : (CONTEXT.PATH_SOURCES + "/templates")
                }
              )
                .pipe(
                  gulp_pug(pug_entry_config)
                )
                .pipe(
                  gulp_rename(function(file_path) {
                    file_path.basename = path.join.apply(this, [].concat(
                      reference_entry.segments, ["index"]
                    ));
                  })
                );
            }
          )
            .pipe(
              gulp.dest(
                CONTEXT.PATH_BUILD_PAGES
              )
            )
            .pipe(
              gulp_connect.reload()
            )
        );
      });
    });

  return _stream;
};


/*
  Compiles Pug templates (changes templates)
*/
var pug_templates_changes = function() {
  var _stream = merge();

  // Acquire the change years data
  // Format: [year<int>, data<object>] (ordered by most recent year first)
  var _change_years = (
    glob.sync(
      (CONTEXT.PATH_DATA + "/changes/*.json")
    )
      .sort()
      .reverse()
      .map(function(file_path) {
        var _file_path_parts = path.parse(file_path);

        return [
          parseInt(_file_path_parts.name, 10),
          JSON.parse(fs.readFileSync(file_path))
        ];
      })
      .map(function(year_data) {
        // Classify each change in its month (in an orderly manner)
        // Notice: the Map object is used as it retains natural ordering, \
        //   where last months come first.
        var _year_months = new Map();

        year_data[1].forEach(function(entry) {
          // Validate entry data
          if (!entry.group || !entry.type || !entry.date || !entry.text) {
            throw new Error(
              "Invalid entry data in changes for year: " + year_data[0]
            );
          }

          // Acquire entry date
          var _entry_date = (new Date(entry.date));

          if (!_entry_date || isNaN(_entry_date.getTime())) {
            throw new Error(
              "Invalid date in changes for year: " + year_data[0]
            );
          }

          // Acquire entry month key
          var _entry_month_key = ("" + (_entry_date.getMonth() + 1));

          if (_entry_month_key.length === 1) {
            _entry_month_key = ("0" + _entry_month_key);
          }

          // Make sure entries object for month is initialized
          if (_year_months.has(_entry_month_key) === false) {
            _year_months.set(_entry_month_key, []);
          }

          // Append entry to month entries object
          _year_months.get(_entry_month_key).push(entry);
        });

        // Assign new per-month data
        // Important: convert back all ordered map entries to an array, which \
        //   we can iterate on right away from templates.
        year_data[1] = Array.from(
          _year_months.entries()
        );

        return year_data;
      })
  );

  // Map available change years (as bare numbers)
  // Important: before prepending latest year data
  var _available_bare_years = _change_years.map(function(change_year) {
    return change_year[0];
  });

  // Prepend list of years with the 'latest' year (ie. corresponding to the \
  //   'index'), and push only the first 3 months (ie. latest)
  var _first_change_year = (_change_years[0] || []);

  _change_years.unshift([
    (_first_change_year[0] || -1),
    (_first_change_year[1] || []).slice(0, 3),
    "index"
  ]);

  gulp_pug_templates.list_config_locales(CONTEXT, package, marked)
    .forEach(function(pug_data) {
      _change_years.forEach(function(change_year) {
        // Generate current year data (for locale)
        var pug_year_config = lodash.cloneDeep(pug_data.config);

        pug_year_config.data.years   = _available_bare_years;

        pug_year_config.data.changes = {
          year     : change_year[0],

          current  : (
            (change_year[2] === "index") ? "latest" : change_year[0]
          ),

          timeline : change_year[1]
        };

        _stream.add(
          gulp_pug_templates.pipe_commons(
            CONTEXT, pug_data.locale, function() {
              return gulp.src(
                (CONTEXT.PATH_SOURCES + "/templates/changes/index.pug"),

                {
                  base : (CONTEXT.PATH_SOURCES + "/templates")
                }
              )
                .pipe(
                  gulp_pug(pug_year_config)
                )
                .pipe(
                  gulp_rename(function(file_path) {
                    file_path.basename = (
                      change_year[2] ? change_year[2] : (
                        path.join(("" + change_year[0]), "index")
                      )
                    );
                  })
                );
            }
          )
            .pipe(
              gulp.dest(
                CONTEXT.PATH_BUILD_PAGES
              )
            )
            .pipe(
              gulp_connect.reload()
            )
        );
      });
    });

  return _stream;
};


/*
  Compiles all Pug templates (shell task to aggregate sub-tasks)
*/
var pug_templates_all = function() {
  return gulp.parallel(
    pug_templates_base,
    pug_templates_guides,
    pug_templates_references,
    pug_templates_changes
  );
}();


/*
  Compiles SCSS stylesheets
*/
var scss = function() {
  return gulp.src(
    CONTEXT.CONFIG.SOURCES.STYLESHEETS.map(function(stylesheet) {
      return (CONTEXT.PATH_SOURCES + "/stylesheets/" + stylesheet);
    }),

    {
      base : (CONTEXT.PATH_SOURCES + "/stylesheets")
    }
  )
    .pipe(
      gulp_sass_variables({
        "$use-inline-images" : CONTEXT.IS_PRODUCTION
      })
    )
    .pipe(
      gulp_sass({
        outputStyle : "expanded"
      })
    )
    .on("error", gulp_notify.onError({
      title     : "scss",
      message   : "Error compiling",
      emitError : true
    }))
    .on("error", function(error) {
      if (CONTEXT.IS_WATCH === true) {
        // Handle compile errors (used for development ease w/ `gulp watch`)
        console.error(error.toString());
      } else {
        throw error;
      }
    })
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/stylesheets"
      )
    );
};


/*
  Imports inline images in stylesheets
*/
var css_inline_images = function() {
  return gulp.src((
    CONTEXT.PATH_BUILD_ASSETS + "/stylesheets/**/*.css"
  ), {
    base : (CONTEXT.PATH_BUILD_ASSETS + "/images")
  })
    .pipe(
      gulp_inline_image()
    )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/stylesheets"
      )
    )
    .pipe(
      gulp_connect.reload()
    );
};


/*
  Compiles Babel templates
*/
var babel = function() {
  return gulp.src(
    CONTEXT.CONFIG.SOURCES.JAVASCRIPTS.map(function(javascript) {
      return (CONTEXT.PATH_SOURCES + "/javascripts/" + javascript);
    }),

    {
      base : (CONTEXT.PATH_SOURCES + "/javascripts")
    }
  )
    .pipe(
      gulp_babel()
    )
    .on("error", gulp_notify.onError({
      title     : "babel",
      message   : "Error compiling",
      emitError : true
    }))
    .on("error", function(error) {
      if (CONTEXT.IS_WATCH === true) {
        // Handle compile errors (used for development ease w/ `gulp watch`)
        console.error(error.toString());
      } else {
        throw error;
      }
    })
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/javascripts"
      )
    );
};


/*
  Replaces values from template files (guides templates)
*/
var replace_templates_guides = function() {
  return gulp.src(
    CONTEXT.PATH_BUILD_PAGES + "/guides/**/*.html"
  )
    .pipe(
      gulp_replace(
        /src="(?:\.\/)?([^\.\/"']+\.(?:jpg|jpeg|png|gif))"/g,

        function(_, file_name) {
          // Acquire base path segments
          var _image_base_segments = this.file.relative.split("/");

          _image_base_segments.pop();

          // Build final path segments
          var _image_final_segments = (
            [].concat(
              ["", "static", "images", "guides", "content"],
              _image_base_segments,
              [file_name]
            )
          );

          // Replace with final path to image file within static assets
          return (
            "src=\"" + _image_final_segments.join("/") + "\""
          );
        }
      )
    )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_PAGES + "/guides"
      )
    )
    .pipe(
      gulp_connect.reload()
    );
};


/*
  Replaces values from javascript files
*/
var replace_javascripts = function() {
  return gulp.src(
    CONTEXT.PATH_BUILD_ASSETS + "/javascripts/**/*.js"
  )
    .pipe(
      gulp_replace(
        "@:revision", CONTEXT.CONFIG.REVISION
      )
    )
    .pipe(
      gulp_replace(
        "\"@:url_status\"",

        JSON.stringify({
          provider : (
            CONTEXT.CONFIG.SITE.urls.vigil ? "vigil" : (
              CONTEXT.CONFIG.SITE.urls.crisp_status ? "crisp" : null
            )
          ),

          target   : (
            CONTEXT.CONFIG.SITE.urls.vigil  ||
              CONTEXT.CONFIG.SITE.urls.crisp_status || null
          )
        })
      )
    )
    .pipe(
      gulp_replace(
        "@:search_index", CONTEXT.CONFIG.SEARCH.INDEX
      )
    )
    .pipe(
      gulp_replace(
        "\"@:search_options_base\"",
        JSON.stringify(CONTEXT.CONFIG.SEARCH.OPTIONS.BASE)
      )
    )
    .pipe(
      gulp_replace(
        "\"@:search_options_query\"",
        JSON.stringify(CONTEXT.CONFIG.SEARCH.OPTIONS.QUERY)
      )
    )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/javascripts"
      )
    )
    .pipe(
      gulp_connect.reload()
    );
};


/*
  Replaces values from stylesheet files
*/
const replace_stylesheets = function() {
  return gulp.src(
    CONTEXT.PATH_BUILD_ASSETS + "/stylesheets/**/*.css"
  )
    .pipe(
      gulp_replace(
        "@@revision",
        CONTEXT.CONFIG.REVISION
      )
    )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/stylesheets"
      )
    );
};


/*
  Compiles feeds (changes templates)
*/
var feed_changes = function() {
  var _title_maximum_length = 80;

  // Acquire paths for the last 3 years (sort by last year first)
  var _feed_years_back = 3;

  var _last_year_paths = glob.sync(
    (CONTEXT.PATH_DATA + "/changes/*.json")
  )
    .sort()
    .reverse()
    .splice(0, _feed_years_back);

  // Acquire the change years data
  var _change_years = (
    _last_year_paths.map(function(file_path) {
      return JSON.parse(
        fs.readFileSync(file_path)
      );
    })
  );

  // Concatenate all changes together
  var _changes_flat = [].concat.apply(
    [], _change_years
  );

  // Acquire feed title
  var _feed_title = (
    CONTEXT.CONFIG.SITE.texts.changes.titles.feed || "Platform Changes"
  );

  // Construct feed object
  var _feed = new Feed({
    title       : _feed_title,
    description : ("Feed of " + _feed_title + "."),

    link        : CONTEXT.CONFIG.SITE.urls.base,
    language    : "en",
    generator   : package.name,

    author      : {
      name : package.author.name
    },

    favicon     : (
      CONTEXT.CONFIG.SITE.urls.base + "/favicon.ico"
    ),

    updated     : (new Date())
  });

  _changes_flat.forEach(function(change) {
    // Strip Markdown from text
    var _text_barebone = (
      remove_markdown(change.text || "")
    );

    // Split text into a title
    var _title;

    if (_text_barebone.length > _title_maximum_length) {
      _title = (
        _text_barebone.substr(0, _title_maximum_length) + " (..)"
      );
    } else {
      _title = _text_barebone;
    }

    // Add entry
    _feed.addItem({
      title   : _title,
      content : marked(change.text),
      link    : (CONTEXT.CONFIG.SITE.urls.base + "/changes/"),
      date    : (new Date(change.date))
    });
  });

  // Generate feed data
  return gulp_file(
    "changes.rss", _feed.rss2(), {
      src : true
    }
  )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_PAGES
      )
    );
};


/*
  Generates sitemap
*/
var sitemap = function() {
  // Scan all templates, while ignoring the 'not_found' page and all private \
  //   pages (ie. those starting w/ '_')
  return gulp.src([
    (CONTEXT.PATH_BUILD_PAGES + "/**/*.html"),

    ("!" + CONTEXT.PATH_BUILD_PAGES + "/not_found/index.html"),
    ("!" + CONTEXT.PATH_BUILD_PAGES + "/**/_*/index.html")
  ])
    .pipe(
      gulp_sitemap({
        siteUrl : CONTEXT.CONFIG.SITE.urls.base,

        getLoc  : function(site_url, location) {
          // Nuke file name and extension from URL? (if found)
          // Notice: built files are stored in directories, corresponding to \
          //   their path, ending in a file named eg. 'index.html'.
          var _file_index = location.lastIndexOf("index.html");

          // Rewrite location?
          if (_file_index > 0) {
            location = location.substring(0, _file_index);
          }

          return location;
        }
      })
    )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_PAGES
      )
    );
};


/*
  Generates robots
*/
var robots = function() {
  return gulp_file(
    "robots.txt",

    (
      "User-Agent: *\n"  +
      "Allow: /\n"       +
      ("Sitemap: " + CONTEXT.CONFIG.SITE.urls.base + "/sitemap.xml\n")
    ),

    {
      src : true
    }
  )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_PAGES
      )
    );
};


/*
  Generates Open Graph images
*/
var ogimage = function() {
  // Generate Open Graph images (if any background is configured)
  return gulp.src(
    CONTEXT.PATH_BUILD_PAGES + "/**/*.html"
  )
    .pipe(function() {
      if (CONTEXT.CONFIG.SITE.images.metas.opengraph) {
        return gulp_ogimage({
          base            : function(file) {
            // Acquire base path segments
            // Notice: pop the last two items
            var _page_base_segments = file.relative.split("/");

            _page_base_segments.splice(
              (_page_base_segments.length - 2), 2
            );

            if (_page_base_segments.length > 0) {
              _page_base_segments.unshift("");
            }

            // Generate base path for generated image, which will be output to \
            //   the final HTML file.
            return (
              CONTEXT.CONFIG.SITE.urls.base + "/static/images/opengraph"  +
                _page_base_segments.join("/")
            );
          },

          directory       : function(file) {
            // Acquire base path segments
            // Notice: pop the last two items
            var _page_base_segments = file.relative.split("/");

            _page_base_segments.splice(
              (_page_base_segments.length - 2), 2
            );

            // Build final path segments
            var _page_final_segments = (
              [].concat(
                ["", "images", "opengraph"],
                _page_base_segments
              )
            );

            // Return full directory path (except from the first directory, \
            //   which becomes the image name); this will get used to output \
            //   the generated image file on the file system.
            return (
              CONTEXT.PATH_BUILD_ASSETS + _page_final_segments.join("/")
            );
          },

          name            : function(file) {
            // Acquire base path segments
            // Notice: pop the last item
            var _page_base_segments = file.relative.split("/");

            _page_base_segments.pop();

            // Return last directory name (if there is none, this means this \
            //   is the 'index' page); this will get used to output the \
            //   generated image file on the file system.
            return (
              _page_base_segments[(_page_base_segments.length - 1)] || "index"
            );
          },

          backgroundImage : function() {
            // Return base background image, which will get used as a baseline \
            //   to generate all Open Graph images (ie. as their background \
            //   image).
            return (
              CONTEXT.PATH_ASSETS + "/"  +
                CONTEXT.CONFIG.SITE.images.metas.opengraph
            );
          },

          title           : function(_, $) {
            return (
              $("head title").first().text() || ""
            );
          },

          description     : function(_, $) {
            return (
              $("head meta[name=\"description\"]").first().attr("content") || ""
            );
          }
        })
      }

      // No Open Graph background image configured, warn and skip
      console.warn(
        "⚠️  No Open Graph background image configured, skipping the "  +
          "auto-generation of 'og:image' metadata.\n   "                +
          "You can configure this with the 'images.metas.opengraph' "   +
          "configuration namespace.\n"
      );

      return gulp_noop();
    }())
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_PAGES
      )
    );
};


/*
  Minifies stylesheet files
*/
var cssmin = function() {
  return gulp.src(
    CONTEXT.PATH_BUILD_ASSETS + "/stylesheets/**/*.css"
  )
    .pipe(
      gulp_clean_css()
    )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/stylesheets"
      )
    );
};


/*
  Minifies javascript files
*/
var uglify = function() {
  return gulp.src(
    CONTEXT.PATH_BUILD_ASSETS + "/javascripts/**/*.js"
  )
    .pipe(
      gulp_uglify()
    )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS + "/javascripts"
      )
    );
};


/*
  Insert banner in build files
*/
var build_banner = function() {
  var date   = new Date();

  var today  = (
    (date.getMonth() + 1) + "/" + date.getDate() + "/" + date.getFullYear()
  );

  var banner = [
    "/**",
    " * <%= pkg.name %> - <%= pkg.description %>",
    " * @version v<%= pkg.version %>",
    " * @author <%= pkg.author.name %> <%= pkg.author.url %>",
    " * @date <%= today %>",
    " */",
    ""
  ].join("\n");

  return gulp.src(
    CONTEXT.PATH_BUILD_ASSETS + "/**/*.{css,js}"
  )
    .pipe(
      gulp_header(banner, {
        pkg   : package,
        today : today
      })
    )
    .pipe(
      gulp.dest(
        CONTEXT.PATH_BUILD_ASSETS
      )
    );
};


/*
  Shows final build size
*/
var build_size = function() {
  var _stream = merge();

  // Acquire sizes
  var _fail  = (CONTEXT.CONFIG.SITE.rules.build_size.fail || false),
      _sizes = CONTEXT.CONFIG.SITE.rules.build_size.sizes;

  // Map all sources and options
  var _sources = [
    {
      glob    : (
        CONTEXT.PATH_BUILD_PAGES + "/references/**/*.html"
      ),

      options : {
        production : {
          gzip  : true,
          total : true,
          fail  : _fail,

          "*"   : {
            maxGzippedSize : _sizes.references.pages.gzip_maximum
          }
        }
      }
    },

    {
      glob    : (
        CONTEXT.PATH_BUILD_PAGES + "/guides/**/*.html"
      ),

      options : {
        production : {
          gzip  : true,
          total : true,
          fail  : _fail,

          "*"   : {
            maxGzippedSize : _sizes.guides.pages.gzip_maximum
          }
        }
      }
    },

    {
      glob    : (
        CONTEXT.PATH_BUILD_ASSETS + "/images/guides/content/**/"  +
          "*.{jpg,jpeg,png,gif}"
      ),

      options : {
        production : {
          total : true,
          fail  : _fail,

          "*"   : {
            maxSize : _sizes.guides.images.maximum
          }
        }
      }
    },

    {
      glob    : (
        CONTEXT.PATH_BUILD_ASSETS + "/data/**/*.json"
      ),

      options : {
        production : {
          gzip  : true,
          total : true,
          fail  : _fail,

          "*"   : {
            maxSize        : _sizes.data.objects.maximum,
            maxGzippedSize : _sizes.data.objects.gzip_maximum
          }
        }
      }
    }
  ];

  // Compute size report for each source
  _sources.forEach(function(source) {
    _stream.add(
      gulp.src(source.glob)
        .pipe(
          gulp_sizereport(
            (CONTEXT.IS_PRODUCTION === true) ? source.options.production : (
              source.options.production.default || {
                total : true
              }
            )
          )
        )
    );
  });

  return _stream;
};


/*
  Builds all resources
*/
var build_resources = function() {
  // Generate main series
  var _series = [
    gulp.parallel(
      robots,
      feed_changes,

      copy_user_assets,
      copy_images_guides,

      gulp.series(
        bower,

        gulp.parallel(
          concat_libraries_stylesheets,

          gulp.series(
            gulp.parallel(
              concat_libraries_javascripts,
              babel
            ),

            replace_javascripts
          )
        )
      ),

      gulp.series(
        gulp.parallel(
          copy_images_base,
          copy_fonts
        ),

        scss,
        replace_stylesheets,
        css_inline_images
      ),

      gulp.series(
        pug_templates_all,

        gulp.parallel(
          replace_templates_guides,
          minisearch_consolidate,
          sitemap
        ),

        ogimage
      )
    )
  ];

  // Append production final tasks?
  if (CONTEXT.IS_PRODUCTION === true) {
    _series.push(
      gulp.parallel(
        cssmin,
        uglify
      ),

      build_banner,
      build_size
    );
  }

  return gulp.series(_series);
}();


/*
  Cleans all build files
*/
var build_clean = function() {
  return del([
    (CONTEXT.PATH_BUILD_ASSETS + "/*"),
    (CONTEXT.PATH_BUILD_PAGES + "/*")
  ]);
};


/*
  Starts connect server
*/
var connect_server = function(next) {
  gulp_connect.server(
    {
      name       : "Server",
      root       : CONTEXT.PATH_DIST,
      host       : CONTEXT.SERVE_HOST,
      port       : CONTEXT.SERVE_PORT,
      silent     : true,

      livereload : {
        hostname : CONTEXT.SERVE_HOST,
        port     : (CONTEXT.SERVE_PORT + 1),
      }
    },

    function() {
      console.log(
        "\n🔥 Preview server started on: http://" + CONTEXT.SERVE_HOST + ":"  +
          CONTEXT.SERVE_PORT + "\n"
      );
    }
  );

  next();
};


/*
  Watches for changes on resources
*/
var watch_resources = function(next) {
  CONTEXT.IS_WATCH = true;

  // Internal files (Chappe files)
  // Notice: only if not 'production', ie. in 'development' mode, as this is \
  //   used by Chappe developers only.
  if (CONTEXT.IS_PRODUCTION !== true) {
    gulp.watch("bower.json", bower);
    gulp.watch("res/config/*", get_configuration);
    gulp.watch("src/images/**/*", copy_images_base);
    gulp.watch("src/fonts/**/*", copy_fonts);

    gulp.watch(
      "src/locales/**/*",

      gulp.series(
        pug_templates_all,
        replace_templates_guides
      )
    );

    gulp.watch(
      "src/templates/**/*",

      gulp.series(
        pug_templates_all,

        gulp.parallel(
          replace_templates_guides,
          sitemap
        )
      )
    );

    gulp.watch(
      "src/stylesheets/**/*",

      gulp.series(
        scss,
        replace_stylesheets,
        css_inline_images
      )
    );

    gulp.watch(
      "src/javascripts/**/*",

      gulp.series(
        babel,
        replace_javascripts
      )
    );
  }

  // External files (user files)
  CONTEXT.PATH_CONFIG.forEach(function(config_path) {
    gulp.watch(config_path, get_configuration);
  });

  gulp.watch(
    (CONTEXT.PATH_DATA + "/guides/**/*.{jpg,jpeg,png,gif}"),
    copy_images_guides
  );

  gulp.watch(
    (CONTEXT.PATH_DATA + "/guides/**/*.md"),

    gulp.series(
      pug_templates_guides,
      replace_templates_guides
    )
  );

  gulp.watch(
    (CONTEXT.PATH_DATA + "/references/**/*.md"),
    pug_templates_references
  );

  gulp.watch(
    (CONTEXT.PATH_DATA + "/changes/**/*.json"),

    gulp.parallel(
      pug_templates_changes,
      feed_changes
    )
  );

  next();
};


/*
  Lints Pug templates
*/
var lint_pug_templates = function() {
  return gulp.src(
    CONTEXT.PATH_SOURCES + "/templates/**/*.pug"
  )
    .pipe(
      gulp_pug_lint({
        defaultFile : path.join(
          CONTEXT.PATH_CHAPPE, ".pug-lintrc"
        )
      })
    );
};


/*
  Lints SCSS stylesheets
*/
var lint_scss_stylesheets = function() {
  return gulp.src(
    CONTEXT.PATH_SOURCES + "/stylesheets/**/*.scss"
  )
    .pipe(
      gulp_stylelint({
        failAfterError : true,

        configFile     : path.join(
          CONTEXT.PATH_CHAPPE, ".stylelintrc.yml"
        ),

        reporters      : [
          {
            formatter : "verbose",
            console   : true
          }
        ]
      })
    );
};


/*
  Lints JS scripts (with JSHint)
*/
var lint_js_scripts_jshint = function() {
  return gulp.src([
    CONTEXT.PATH_SOURCES + "/javascripts/**/*.js",
    CONTEXT.PATH_CHAPPE + "/bin/*.js"
  ])
    .pipe(
      gulp_jshint({
        defaultFile : path.join(
          CONTEXT.PATH_CHAPPE, ".jshintrc"
        )
      })
    )
    .pipe(
      gulp_jshint.reporter("fail")
    );
};


/*
  Lints JS scripts (with JSCS)
*/
var lint_js_scripts_jscs = function() {
  return gulp.src([
    CONTEXT.PATH_SOURCES + "/javascripts/**/*.js",
    CONTEXT.PATH_CHAPPE + "/bin/*.js"
  ])
    .pipe(
      gulp_jscs({
        configPath : path.join(
          CONTEXT.PATH_CHAPPE, ".jscsrc"
        )
      })
    )
    .pipe(
      gulp_jscs.reporter("fail")
    );
};


/*
  Lints project built code
*/
var lint = function() {
  return gulp.series(
    get_configuration,

    gulp.parallel(
      lint_pug_templates,
      lint_scss_stylesheets,
      lint_js_scripts_jshint,
      lint_js_scripts_jscs
    )
  );
}();


/*
  Serves project
*/
var serve = function() {
  return gulp.series(
    get_configuration,
    minisearch_prepare,
    build_clean,
    build_resources,
    watch_resources,
    connect_server
  );
}();


/*
  Cleans all build files
*/
var clean = function() {
  return gulp.series(
    get_configuration,
    build_clean
  )
}();


/*
  Builds project
*/
var build = function() {
  return gulp.series(
    get_configuration,
    minisearch_prepare,
    build_resources
  );
}();


/*
  Watches for project changes
*/
var watch = function() {
  return gulp.series(
    get_configuration,
    minisearch_prepare,
    watch_resources
  )
}();


/*
  Export all public tasks
*/
exports.lint    = lint;
exports.serve   = serve;
exports.clean   = clean;
exports.watch   = watch;
exports.build   = build;
exports.default = build;


================================================
FILE: package.json
================================================
{
  "name": "chappe",
  "description": "Developer Docs builder. Write guides in Markdown and references in API Blueprint. Comes with a built-in search engine.",
  "version": "1.16.0",
  "homepage": "https://github.com/crisp-oss/chappe",
  "license": "MIT",
  "bin": {
    "chappe": "bin/chappe.js"
  },
  "author": {
    "name": "Valerian Saliou",
    "email": "valerian@valeriansaliou.name",
    "url": "https://valeriansaliou.name/"
  },
  "contributors": [
    {
      "name": "Baptiste Jamin",
      "url": "https://jam.in/"
    }
  ],
  "repository": {
    "type": "git",
    "url": "git://github.com/crisp-oss/chappe.git"
  },
  "bugs": {
    "url": "https://github.com/crisp-oss/chappe/issues"
  },
  "licenses": [
    {
      "type": "MIT",
      "url": "https://github.com/crisp-oss/chappe/blob/master/LICENSE"
    }
  ],
  "engines": {
    "node": ">= 20.0.0"
  },
  "scripts": {
    "dev": "./bin/chappe.js serve --example=acme-docs",
    "build": "./bin/chappe.js build --example=acme-docs",
    "test": "./bin/chappe.js lint --example=acme-docs"
  },
  "dependencies": {
    "@tommoor/remove-markdown": "0.3.x",
    "babel-preset-es2015": "6.9.x",
    "del": "6.0.x",
    "drafter.js": "3.2.x",
    "escape-html": "1.0.x",
    "feed": "4.2.x",
    "glob": "7.2.x",
    "gulp": "4.0.x",
    "gulp-babel": "6.1.x",
    "gulp-bower": "0.0.15",
    "gulp-clean-css": "4.3.x",
    "gulp-concat": "2.6.x",
    "gulp-connect": "5.7.x",
    "gulp-file": "0.4.x",
    "gulp-header": "2.0.x",
    "gulp-inline-image": "1.0.x",
    "gulp-jscs": "4.1.x",
    "gulp-jshint": "2.1.x",
    "gulp-noop": "1.0.x",
    "gulp-notify": "4.0.x",
    "gulp-ogimage": "2.0.x",
    "gulp-pug": "5.0.x",
    "gulp-pug-lint": "0.1.x",
    "gulp-rename": "2.0.x",
    "gulp-replace": "1.1.x",
    "gulp-sass": "6.0.x",
    "gulp-sass-variables": "1.2.x",
    "gulp-sitemap": "8.0.x",
    "gulp-sizereport": "1.2.x",
    "gulp-stylelint-esm": "3.0.x",
    "gulp-uglify": "3.0.x",
    "http-status-codes": "2.2.x",
    "jshint": "2.13.x",
    "lodash": "4.17.x",
    "markdown-toc": "1.2.x",
    "marked": "7.0.x",
    "marked-footnote": "1.4.x",
    "marked-gfm-heading-id": "3.2.x",
    "marked-mangle": "1.1.x",
    "merge-stream": "2.0.x",
    "minisearch": "3.1.x",
    "ora": "5.4.x",
    "sass": "1.89.x",
    "slug": "5.2.x",
    "stylelint-config-standard-scss": "16.0.x",
    "trunc-text": "1.0.x",
    "yargs": "4.8.x"
  },
  "keywords": [
    "docs",
    "documentation",
    "api-documentation",
    "documentation-tool",
    "docs-generator",
    "developer-documentation",
    "developer-tool",
    "markdown",
    "blueprint",
    "developer",
    "api-rest",
    "github-pages",
    "cloudflare-pages"
  ]
}


================================================
FILE: res/config/common.json
================================================
{
  "LANGS" : [
    "en"
  ],

  "URLS" : {
    "CRISP_WEB" : "https://crisp.chat/"
  },

  "LIBRARIES" : {
    "JAVASCRIPTS" : [
      "console/console.js",
      "cookies/src/cookies.js",
      "cash/dist/cash.js",
      "minisearch/dist/umd/index.js",
      "prism.js/prism.js"
    ],

    "STYLESHEETS" : [
      "reset.css/reset.css"
    ]
  },

  "SOURCES" : {
    "TEMPLATES" : [
      "home/index.pug",
      "not_found/index.pug"
    ],

    "JAVASCRIPTS" : [
      "common/common.js"
    ],

    "STYLESHEETS" : [
      "common/common.scss",
      "home/home.scss",
      "guides/guides.scss",
      "references/references.scss",
      "changes/changes.scss",
      "not_found/not_found.scss"
    ]
  },

  "FORMAT" : {
    "DESCRIPTION" : {
      "TRUNCATE" : 140
    },

    "DATES" : {
      "LOCALE_DATE_STRING" : {
        "AREA"    : "en-US",

        "OPTIONS" : {
          "year"  : "numeric",
          "month" : "long",
          "day"   : "numeric"
        }
      }
    }
  },

  "COLORS" : {
    "BADGES" : {
      "HTTP" : {
        "head"   : "blue",
        "get"    : "blue",
        "post"   : "green",
        "put"    : "yellow",
        "patch"  : "yellow",
        "delete" : "red"
      }
    }
  },

  "SEARCH" : {
    "INDEX" : "search/index.json",

    "OPTIONS" : {
      "BASE" : {
        "fields"      : [
          "title",
          "path",
          "summary"
        ],

        "storeFields" : [
          "id",
          "path",
          "title",
          "summary"
        ]
      },

      "QUERY" : {
        "searchOptions" : {
          "prefix"  : true,
          "fuzzy"   : 0.15,

          "boost"   : {
            "title"   : 6,
            "path"    : 4,
            "summary" : 1
          },

          "weights" : {
            "fuzzy"  : 0.5,
            "prefix" : 0.25
          }
        }
      }
    }
  }
}


================================================
FILE: res/config/user.json
================================================
{
  "identity" : {
    "title"     : "",
    "copyright" : ""
  },

  "theme" : {
    "accent" : {
      "light" : {
        "base"   : "#2275f1",
        "active" : "#0f69ef"
      },

      "dark" : {
        "base"   : "#e0e7ed",
        "active" : "#d5dde4"
      }
    }
  },

  "urls" : {
    "base"         : "",
    "vigil"        : "",
    "crisp_status" : ""
  },

  "favicons" : {
    "main"  : "",

    "sizes" : {
      "default" : "",
      "512x512" : "",
      "256x256" : "",
      "128x128" : "",
      "32x32"   : ""
    }
  },

  "images" : {
    "illustrations" : {
      "home"      : "",
      "not_found" : ""
    },

    "logos" : {
      "header_full"  : "",
      "header_short" : "",
      "footer"       : ""
    },

    "metas" : {
      "opengraph" : ""
    },

    "categories" : {
      "guides"     : {},
      "references" : {}
    }
  },

  "dimensions" : {
    "logos" : {
      "header_full"  : {
        "width" : 160
      },

      "header_short" : {
        "width" : 50
      },

      "footer"       : {
        "width" : 120
      }
    }
  },

  "colors" : {
    "changes" : {
      "groups" : {}
    }
  },

  "texts" : {
    "home" : {
      "title" : "",
      "label" : ""
    },

    "changes" : {
      "groups" : {},

      "titles" : {
        "feed"   : "",
        "latest" : "",
        "year"   : ""
      },

      "notice" : ""
    }
  },

  "links" : {
    "header" : {
      "navigation" : [],
      "actions"    : []
    },

    "footer" : {
      "navigation" : []
    }
  },

  "actions" : {
    "home" : []
  },

  "bulletpoints" : {
    "home" : {
      "quickstart" : {
        "description" : "",
        "actions"     : []
      },

      "guides" : {
        "description" : "",
        "actions"     : []
      },

      "references" : {
        "description" : "",
        "actions"     : []
      }
    }
  },

  "includes" : {
    "scripts" : {
      "urls"   : [],
      "inline" : []
    },

    "stylesheets" : {
      "urls"   : [],
      "inline" : []
    }
  },

  "tokens" : {
    "crisp_website_id" : ""
  },

  "plugins" : {
    "code" : {
      "syntaxes" : []
    }
  },

  "features" : {
    "support" : true
  },

  "rules" : {
    "build_size" : {
      "fail"  : true,

      "sizes" : {
        "references" : {
          "pages" : {
            "gzip_maximum" : 100000
          }
        },

        "guides" : {
          "pages" : {
            "gzip_maximum" : 50000
          },

          "images" : {
            "maximum" : 1000000
          }
        },

        "data" : {
          "objects" : {
            "maximum"      : 200000,
            "gzip_maximum" : 50000
          }
        }
      }
    }
  },

  "overrides" : {
    "crisp_chatbox_url" : ""
  }
}


================================================
FILE: res/plugins/gulp/minisearch.js
================================================
/*
 * chappe
 *
 * Copyright 2021, Crisp IM SAS
 * Author: Valerian Saliou <valerian@valeriansaliou.name>
 */


var remove_markdown  = require("@tommoor/remove-markdown");


module.exports = {
  insert_categories : function(
    context, type, path_origin, categories, override_segments
  ) {
    var _self = this;

    // Define a maximum length for summaries
    var _summary_maximum_length = 100;

    categories.forEach(function(category) {
      // Generate target page URL
      var _target_segments = (
        override_segments || category.segments || []
      );

      var _target_url = (
        "/" + type + "/" + _target_segments.join("/")  +
          ((_target_segments.length > 0) ? "/" : "")
      );

      // Append page anchor? (if not a category with segments)
      if (!category.segments && category.id) {
        _target_url += ("#" + category.id);
      }

      // Acquire summary (based on available data)
      var _summary_full = "";

      if (category.data) {
        switch (category.type) {
          case "API Blueprint": {
            if (category.data.content) {
              var _blueprint_first_line;

              // Acquire first line from content
              for (var _i = 0; _i < category.data.content.length; _i++) {
                var _entry = category.data.content[_i];

                if (_entry.element === "copy" && _entry.content) {
                  _blueprint_first_line = _entry.content;

                  break;
                }
              }

              // Strip Markdown from first line?
              if (_blueprint_first_line) {
                _summary_full = remove_markdown(_blueprint_first_line);
              }
            }

            break;
          }

          case "Markdown": {
            // Strip Markdown from data
            _summary_full = remove_markdown(category.data);

            break;
          }
        }

        // Use title as summary? (fallback)
        if (!_summary_full) {
          _summary_full = category.title;
        }
      } else if (category.markdown) {
        // Strip Markdown from text
        _summary_full = remove_markdown(category.markdown);
      }

      // Generate short summary
      var _summary;

      if (_summary_full.length > _summary_maximum_length) {
        _summary = (
          _summary_full.substr(0, _summary_maximum_length) + "."
        );
      } else {
        _summary = _summary_full;
      }

      // Insert entry to index
      context.SEARCH_INDEX.add({
        id      : _target_url,
        path    : path_origin.join(" > "),
        title   : category.title,
        summary : (_summary || "")
      });

      // Recurse to subtrees?
      if ((category.subtrees || []).length > 0) {
        // Generate current path origin
        var _parent_path_origin = [].concat(path_origin);

        _parent_path_origin.push(category.title);

        // Insert all child categories to the index
        _self.insert_categories(
          context, type, _parent_path_origin, category.subtrees,
            override_segments
        );
      }
    });
  }
};


================================================
FILE: res/plugins/gulp/pug-templates.js
================================================
/*
 * chappe
 *
 * Copyright 2021, Crisp IM SAS
 * Author: Valerian Saliou <valerian@valeriansaliou.name>
 */


var fs                 = require("fs");
var path               = require("path");
var url                = require("url");

var lodash             = require("lodash");
var glob               = require("glob");
var slug               = require("slug");
var drafter            = require("drafter.js");
var toc                = require("markdown-toc");
var truncate_text      = require("trunc-text");
var remove_markdown    = require("@tommoor/remove-markdown");
var http_status_codes  = require("http-status-codes");

var gulp_notify        = require("gulp-notify");
var gulp_rename        = require("gulp-rename");


module.exports = {
  list_config_locales : function(context, package, marked) {
    var pug_config = {
      data : lodash.merge(
        lodash.clone(context.CONFIG),

        {
          PACKAGE : package,

          DATE    : {
            YEAR : (new Date()).getUTCFullYear()
          },

          METHODS : {
            marked          : marked,
            remove_markdown : remove_markdown,
            truncate_text   : truncate_text,
            slug            : slug
          }
        }
      )
    };

    if (context.IS_PRODUCTION) {
      pug_config.pretty = false;
    } else {
      pug_config.pretty = true;
    }

    return context.CONFIG.LANGS.map(function(lang) {
      var pug_config_lang = lodash.cloneDeep(pug_config);

      // Assign locale code
      pug_config_lang.data.LOCALE = {
        CODE      : lang,
        DIRECTION : "ltr"
      };

      // Assign locale strings
      pug_config_lang.data.$_ = JSON.parse(
        fs.readFileSync(context.PATH_SOURCES + "/locales/" + lang + ".json")
      );

      return {
        locale : lang,
        config : pug_config_lang
      };
    });
  },

  pipe_commons : function(context, locale, fn_pipeline) {
    return fn_pipeline()
      .on("error", gulp_notify.onError({
        title     : "pug_templates_base",
        message   : "Error compiling",
        emitError : true
      }))
      .on("error", function(error) {
        if (context.IS_WATCH === true) {
          // Handle compile errors (used for development ease w/ `gulp watch`)
          console.error(error.toString());  // jscs:ignore
        } else {
          throw error;
        }
      })
      .pipe(
        gulp_rename(function(file_path) {
          // Append locale code to base name (only if not default locale, ie. \
          //   'en')
          if (locale !== context.CONFIG.LANGS[0]) {
            file_path.basename += ("." + locale);
          }

          // Map home directory to base directory? (as 'home' must be set at \
          //   the root, ie. as the entry 'index.html')
          if (file_path.dirname === "home") {
            file_path.dirname = "";
          }
        })
      );
  },

  traverse_guides_tree : function(
    root_path, base_path, linear_output, parent_tree
  ) {
    var _self = this;

    // Apply defaults
    base_path     = (base_path     || root_path);
    linear_output = (linear_output || []);
    parent_tree   = (parent_tree   || null);

    var _tree_output = glob.sync("./index.md", {
      cwd : base_path
    })
      .map(function(file_path) {
        // Acquire file paths (full paths from build root, and page paths)
        var _file_path_full  = path.join(base_path, file_path),
            _file_path_pages = path.relative(root_path, _file_path_full);

        // Split file path into its final URL segments
        var _path_segments = (
          _file_path_pages.split("/")
            .filter(function(file_path_segment) {
              // Ignore segment?
              if (file_path_segment === "." || file_path_segment === "index.md") {
                return false;
              }

              // Accept segment
              return true;
            })
        );

        // Read guide metas: title, order and others (and then, Markdown)
        var _file_data  = fs.readFileSync(_file_path_full, "utf-8"),
            _file_lines = _file_data.split(/\r?\n/);

        var _guide_title    = null,
            _guide_index    = null,
            _guide_updated  = null,
            _guide_links    = [],
            _guide_markdown = "";

        // Read content line-by-line
        var _has_scanned_metas = false;

        for (var _i = 0; _i < _file_lines.length; _i++) {
          // Match on meta line? (if has not already scanned all metas)
          if (_has_scanned_metas !== true) {
            var _match_meta_line = _file_lines[_i].match(/^([A-Z]+):(.+)$/);

            if (_match_meta_line) {
              var _match_meta_value = (
                (_match_meta_line[2] || "").trim() || null
              );

              switch (_match_meta_line[1]) {
                case "TITLE": {
                  _guide_title = _match_meta_value;

                  break;
                }

                case "INDEX": {
                  if (!isNaN(_match_meta_value)) {
                    _guide_index = parseInt(_match_meta_value, 10);
                  }

                  break;
                }

                case "UPDATED": {
                  _guide_updated = (new Date(_match_meta_value));

                  break;
                }

                case "LINK": {
                  var _link_parts = _match_meta_value.split("->"),
                      _link_name  = (_link_parts[0] || "").trim(),
                      _link_url   = (_link_parts[1] || "").trim();

                  if (!_link_name || !_link_url || _link_parts.length !== 2) {
                    throw new Error(
                      "Guide link value is invalid: " + _match_meta_value
                    );
                  }

                  _guide_links.push({
                    name : _link_name,
                    url  : _link_url
                  });

                  break;
                }

                default: {
                  throw new Error(
                    "Guide meta-data value is unsupported: " + _match_meta_line[1]
                  );
                }
              }

              continue;
            }

            // Non-meta line found, consider as having scanned them all.
            _has_scanned_metas = true;
          }

          // Handle Markdown line
          _guide_markdown += _file_lines[_i];
          _guide_markdown += "\n";
        }

        // Perform a final trim of the parsed Markdown (as extra end-lines may \
        //   be held)
        _guide_markdown = _guide_markdown.trim();

        // Validate acquired values
        if (!_guide_title || !_guide_updated                          ||
              isNaN(_guide_updated.getTime()) || isNaN(_guide_index)  ||
              _guide_index < 1) {
          throw new Error("Guide meta-data is invalid at: " + _file_path_full);
        }

        // Generate tree entry
        var _entry = {
          segments : _path_segments,
          id       : _path_segments.join("_"),
          title    : _guide_title,
          index    : _guide_index,
          links    : _guide_links,
          updated  : _guide_updated,
          markdown : _guide_markdown,
          parent   : parent_tree,
          subtrees : []
        };

        // List sub-trees (traverse tree recursively)
        var _sub_tree_parts = (
          glob.sync("./*/", {
            cwd : base_path
          })
            .map(function(directory_path) {
              var _sub_tree_data = _self.traverse_guides_tree(
                root_path, path.join(base_path, directory_path), linear_output,
                  _entry
              );

              // Take tree output, drop the linear output aggregator
              return _sub_tree_data[0];
            })
            .filter(function(sub_tree) {
              // Filter-out empty sub-trees
              return (sub_tree.length > 0 ? true : false);
            })
        );

        // Reduce sub-trees into a single array
        _sub_tree_parts.forEach(function(sub_tree_part) {
          _entry.subtrees = _entry.subtrees.concat(sub_tree_part);
        });

        // Sort sub-trees by lower index first
        _entry.subtrees = _entry.subtrees.sort(function(previous, next) {
          return (previous.index - next.index);
        });

        // Append this entry to the linear output (w/ the same object reference \
        //   as the tree-based output)
        linear_output.push(_entry);

        return _entry;
      })
      .sort(function(previous, next) {
        // Sort main-tree by lower index first
        return (previous.index - next.index);
      });

    return [_tree_output, linear_output];
  },

  traverse_references_linear : function(context, root_path) {
    var _self = this;

    return glob.sync("./**/*.md", {
      cwd : root_path
    })
      .map(function(file_path) {
        // Acquire file paths (full paths from build root, and page paths)
        var _file_path_full = path.join(root_path, file_path);

        // Split file path into its final URL segments
        var _path_segments = (
          file_path.split("/")
            .filter(function(file_path_segment) {
              // Ignore segment?
              if (file_path_segment === ".") {
                return false;
              }

              // Accept segment
              return true;
            })
            .map(function(file_path_segment) {
              // Remove all file extensions in all segments
              return file_path_segment.split(".")[0];
            })
        );

        // Read reference metas: type and others (and then, content)
        var _file_data  = fs.readFileSync(_file_path_full, "utf-8"),
            _file_lines = _file_data.split(/\r?\n/);

        var _reference_type    = null,
            _reference_title   = null,
            _reference_updated = null,
            _reference_content = "";

        // Read content line-by-line
        var _has_scanned_metas = false;

        for (var _i = 0; _i < _file_lines.length; _i++) {
          // Match on meta line? (if has not already scanned all metas)
          if (_has_scanned_metas !== true) {
            // Only scan on certain meta keys, as some keys may be used for \
            //   eg. API Blueprint.
            var _match_meta_line = (
              _file_lines[_i].match(/^(TYPE|TITLE|UPDATED):(.+)$/)
            );

            if (_match_meta_line) {
              var _match_meta_value = (
                (_match_meta_line[2] || "").trim() || null
              );

              switch (_match_meta_line[1]) {
                case "TYPE": {
                  _reference_type = _match_meta_value;

                  break;
                }

                case "TITLE": {
                  _reference_title = _match_meta_value;

                  break;
                }

                case "UPDATED": {
                  _reference_updated = (new Date(_match_meta_value));

                  break;
                }

                default: {
                  throw new Error(
                    "Reference meta-data value is unsupported: "  +
                      _match_meta_line[1]
                  );
                }
              }

              continue;
            }

            // Non-meta line found, consider as having scanned them all.
            _has_scanned_metas = true;
          }

          // Handle content line
          _reference_content += _file_lines[_i];
          _reference_content += "\n";
        }

        // Perform a final trim of the parsed content (as extra end-lines may \
        //   be held)
        _reference_content = _reference_content.trim();

        // Validate acquired values
        if (!_reference_type || !_reference_title || !_reference_updated  ||
              isNaN(_reference_updated.getTime())) {
          throw new Error(
            "Reference meta-data is invalid at: " + _file_path_full
          );
        }

        // Return entry data
        return [
          {
            segments : _path_segments,
            id       : _path_segments.join("_"),
            type     : _reference_type,
            title    : _reference_title,
            updated  : _reference_updated,
          },

          _reference_content
        ];
      })
      .map(function(entry_items) {
        var _entry = entry_items[0];

        switch (_entry.type) {
          case "API Blueprint": {
            var _parse_result = drafter.parseSync(entry_items[1], {
              requireBlueprintName      : true,
              generateSourceMap         : false,
              generateMessageBody       : false,
              generateMessageBodySchema : false
            });

            // Blueprint has error? (likely invalid)
            if (!_parse_result || _parse_result.content.length === 0  ||
                  _parse_result.content[0].element !== "category") {
              throw new Error(
                "Reference API Blueprint could not be parsed for: "  +
                  _entry.segments.join("/")
              );
            }

            // Blueprint has warnings? (we want to be strict there and abort \
            //   build, as ignored warnings can result in unseen issues in the \
            //   final built reference document)
            // Notice: render errors in a readable format.
            var _warns_count = (_parse_result.content.length - 1);

            if (_warns_count > 0) {
              var _warns_limit = 50;

              throw new Error(
                "Reference API Blueprint has warnings for: "  +
                  _entry.segments.join("/")                   +
                  "\n\n"                                      +
                  "Warnings to be resolved:"                  +
                  "\n"                                        +

                  (
                    _parse_result.content.splice(1, _warns_limit)
                      .map(function(warning) {
                        var _text = (
                          (typeof warning.content === "string") ?
                            (warning.content || "[no text]") : "[wrong type]"
                        );

                        return (" |- (" + warning.element + ") -> " + _text);
                      })
                      .join("\n")
                  )                                           +

                  (
                    (_warns_count <= _warns_limit) ? "" : (
                      "\n" + (
                        " \\+ (" + (_warns_count - _warns_limit) + " more)"
                      )
                    )
                  )
              );
            }

            // Acquire final entry data
            var _data = _parse_result.content[0];

            // Find API host URL from parent attributes
            var _host_url = null;

            (_data.attributes.metadata.content || []).forEach(
              function(metadata) {
                if (!_host_url && metadata.element === "member"  &&
                      metadata.content                           &&
                      metadata.content.key.content === "HOST") {
                  _host_url = metadata.content.value.content;
                }
              }
            );

            // Still did not find host URL? This is unexpected.
            if (!_host_url) {
              throw new Error(
                "Reference API Blueprint HOST value could not be found"
              );
            }

            // Assign parsed Blueprint result tree (first entry contains the \
            //   whole result tree)
            _entry.data       = _data;

            _entry.baseline   = (
              _self.map_references_blueprint_baseline(_host_url)
            );
            _entry.categories = (
              _self.map_references_blueprint_categories(context, _data.content)
            );
            _entry.examples   = (
              _self.map_references_blueprint_examples(_data.content)
            );

            break;
          }

          case "Markdown": {
            // Acquire final entry data
            var _data = entry_items[1];

            // Assign parsed Markdown data
            _entry.data       = _data;

            _entry.categories = (
              _self.map_references_markdown_categories(_data)
            );

            break;
          }

          default: {
            throw new Error(
              "Reference type: " + _entry.type + " is not recognized on: "  +
                _entry.segments.join("/")
            );
          }
        }

        return _entry;
      });
  },

  map_references_blueprint_baseline : function(host_url) {
    var _baseline = {};

    // Map host URL parts
    _baseline.host = {
      url  : host_url,
      path : (url.parse(host_url).pathname || "/")
    };

    return _baseline;
  },

  map_references_blueprint_categories : function(context, entries, parent_id) {
    var _self = this;

    var _categories = [];

    entries.forEach(function(entry) {
      // Match on category groups only
      if (entry.element === "category" || entry.element === "resource"  ||
            entry.element === "transition") {
        // Acquire HTTP method associated to 'transition' element (ie. request)
        var _badge = null;

        if (entry.element === "transition") {
          var _http_method = (
            _self.find_references_blueprint_http_method(entry)
          );

          if (_http_method) {
            // Acquire badge color for HTTP method
            var _badge_color = (
              context.CONFIG.COLORS.BADGES.HTTP[_http_method] || null
            );

            // Create final badge object
            _badge = [_http_method, _badge_color];
          }
        }

        // Generate category identifier (prepend parent identifier, if any)
        var _id = slug(entry.meta.title.content);

        if (parent_id) {
          _id = (parent_id + "-" + _id);
        }

        // Push current category to the level of categories being scanned
        _categories.push({
          id       : _id,
          title    : entry.meta.title.content,
          badge    : _badge,

          subtrees : (
            _self.map_references_blueprint_categories(
              context, entry.content,

              ((entry.element === "category") ? _id : null)  //-[parent_id]
            )
          )
        });
      }
    });

    return _categories;
  },

  map_references_blueprint_examples : function(entries, parent_map) {
    var _self = this;

    parent_map = (parent_map || {});

    entries.forEach(function(entry) {
      switch (entry.element) {
        case "category":
        case "resource": {
          // Recurse on children (parent groups of transition group)
          _self.map_references_blueprint_examples(
            entry.content, parent_map
          );

          break;
        }

        case "transition": {
          // Acquire HTTP method
          var _http_method = (
            _self.find_references_blueprint_http_method(entry)
          );

          // Acquire restrictions
          var _restrictions = (
            _self.find_references_blueprint_restrictions(entry)
          );

          // Parse URL parts (if any)
          var _url_parts = (
            _self.find_references_blueprint_url_parts(entry)
          );

          // Parse available request-response flows
          var _flows = (
            _self.find_references_blueprint_flows(entry)
          );

          // Validate acquired data
          if (!_http_method || Object.keys(_flows).length === 0) {
            throw new Error(
              "Reference API Blueprint transition entry has no example request"
            );
          }

          // Generate example data
          var _example_data = {
            method : _http_method,

            url    : {
              parts : _url_parts
            },

            tiers  : (_restrictions.tiers  || []),
            scopes : (_restrictions.scopes || []),

            flows  : _flows
          };

          // Assign example data to parent map
          var _id = slug(entry.meta.title.content);

          parent_map[_id] = _example_data;

          break;
        }
      }
    });

    return parent_map;
  },

  map_references_markdown_categories : function(data) {
    var _self = this;

    // Generate navigation menu from Markdown
    var _navigation = toc(data, {
      maxdepth : 3,
      firsth1  : true
    });

    // Generate navigation tree (based on indent levels)
    return (
      _self.transduce_references_markdown_categories_tree(_navigation.json)
    );
  },

  transduce_references_markdown_categories_tree : function(items, level) {
    var _self = this;

    level = (level || 1);

    // Append entries to tree
    var _tree           = [],
        _current_entry  = null,
        _children_stack = [];

    for (var _i = 0; _i < (items.length + 1); _i++) {
      var _item = (items[_i] || null);

      // Scanning is finished as we have overflown? Or are we still scanning?
      if (_item === null || _item.lvl === level) {
        // Push last current entry before, if any
        if (_current_entry !== null) {
          // Transduce eventual children from the stack
          _current_entry.subtrees = (
            _self.transduce_references_markdown_categories_tree(
              _children_stack, (level + 1)
            )
          );

          // Append current entry to tree (with its eventual children)
          _tree.push(_current_entry);

          // Reset children stack back to empty state
          _children_stack = [];
        }

        // Start a new current entry?
        if (_item !== null) {
          _current_entry = {
            id       : slug(_item.content),
            title    : _item.content,
            subtrees : []
          };
        }
      } else {
        // Append raw child entry to children stack
        _children_stack.push(_item);
      }
    }

    return _tree;
  },

  find_references_blueprint_http_method : function(transition) {
    var _http_method = null;

    transition.content.forEach(function(entry) {
      if (_http_method === null && entry.element === "httpTransaction") {
        entry.content.forEach(function(transaction) {
          if (_http_method === null && transaction.element === "httpRequest") {
            if (transaction.attributes && transaction.attributes.method) {
              // Read HTTP method
              _http_method = (
                transaction.attributes.method.content.toLowerCase()
              );
            }
          }
        });
      }
    });

    return _http_method;
  },

  find_references_blueprint_restrictions : function(transition) {
    var _restrictions = null;

    // Iterate on each 'copy' text line found
    transition.content.forEach(function(entry) {
      if (_restrictions === null && entry.element === "httpTransaction") {
        entry.content.forEach(function(transaction) {
          if (_restrictions === null && transaction.element === "httpRequest") {
            transaction.content.forEach(function(sub_entry) {
              if (sub_entry.element === "copy") {
                sub_entry.content.split("\n").forEach(function(line) {
                  var _match = line.trim().match(/^\+ (Tiers|Scopes):(.+)$/);

                  if (_match && _match[1] && _match[2]) {
                    var _key = _match[1].toLowerCase();

                    // Initialize restrictions map or key array (if needed)
                    _restrictions       = (_restrictions       || {});
                    _restrictions[_key] = (_restrictions[_key] || []);

                    // Iterate on parts sub-matches
                    var _part_regex = /(?:^| )`([^`]+)`/g,
                        _part_match;

                    while (_part_match = _part_regex.exec(_match[2])) {
                      // Assign found restriction type and values
                      _restrictions[_key].push(
                        _part_match[1].trim()
                      );
                    }
                  }
                });
              }
            });
          }
        });
      }
    });

    return (_restrictions || {});
  },

  find_references_blueprint_url_parts : function(transition) {
    // Notice: this is a simple and (relatively) hacky way to parse URL \
    //   parts into tokens, w/o using more complex yet complete ways \
    //   eg. backtracking. This does the job in our use-case.
    var _url_parts = [];

    if (transition.attributes && transition.attributes.href) {
      var _href         = (transition.attributes.href.content || ""),
          _current_part = null;

      for (var _i = 0; _i < _href.length; _i++) {
        var _slice = _href[_i];

        // Initialize current part?
        var _was_initiated = false;

        if (_current_part === null) {
          _current_part  = {
            type  : ((_slice === "{") ? "parameter" : "segment"),
            value : ""
          };

          _was_initiated = true;
        }

        // Push current character? (ignore '{' and '}' enclosure characters)
        if (_slice !== "{" && _slice !== "}") {
          _current_part.value += _slice;
        }

        // Push current part? (closure or opener character detected, or \
        //   end-of-string, or next character opens a different enclosed \
        //   type)
        if (_current_part !== null && ((_i === (_href.length - 1))   ||
              ((_slice === "{" || _slice === "}" || _slice === "/")  &&
                (_was_initiated !== true || _href[_i + 1] === "{")))) {
          var _first_slice = _current_part.value[0];

          // Ignore argument parameters
          if (_first_slice !== "?" && _first_slice !== "&") {
            _url_parts.push(_current_part);
          }

          _current_part = null;
        }
      }
    }

    return _url_parts;
  },

  find_references_blueprint_flows : function(transition) {
    var _self = this;

    var _flows_map = {};

    transition.content.forEach(function(entry) {
      if (entry.element === "httpTransaction") {
        // Map entry contents as a direct-access map
        var _entry_contents = {};

        entry.content.forEach(function(sub_entry) {
          if (sub_entry.element.startsWith("http") === true) {
            _entry_contents[sub_entry.element] = sub_entry;
          }
        });

        if (_entry_contents.httpRequest && _entry_contents.httpResponse) {
          var _request_name = (
            ((_entry_contents.httpRequest.meta || {}).title || {}).content || "?"
          );

          var _request_id   = slug(_request_name);

          // Assign first flow request data? (if not already set)
          if (!_flows_map[_request_id]) {
            var _request_params = (
              _self.parse_references_blueprint_flow_direction(
                _request_name, _entry_contents.httpRequest
              )
            );

            // Assign new flow data
            _flows_map[_request_id] = {
              request   : _request_params,
              responses : []
            };
          }

          // Assign current flow response data
          var _http_status_name = (
            _entry_contents.httpResponse.attributes.statusCode.content
          );

          // Attempt to resolve reason text from status code?
          if (!isNaN(_http_status_name)) {
            // Notice: 'http_status_codes.getReasonPhrase()' throws if it \
            //   cannot find the reason for a status code, therefore we need \
            //   to wrap this in a try/catch block.
            try {
              var _http_status_reason = http_status_codes.getReasonPhrase(
                parseInt(_http_status_name, 10)
              );

              if (_http_status_reason) {
                _http_status_name += (" " + _http_status_reason);
              }
            } catch (error) {
              // Ignore error (this will skip appending the reason to the \
              //   status name)
            }
          }

          var _response_params = (
            _self.parse_references_blueprint_flow_direction(
              _http_status_name, _entry_contents.httpResponse
            )
          );

          _flows_map[_request_id].responses.push(_response_params);
        }
      }
    });

    return _flows_map;
  },

  parse_references_blueprint_flow_direction : function(name, entry) {
    // Look for direction content type and data
    var _direction_type = null,
        _direction_data = null;

    entry.content.forEach(function(direction_entry) {
      if (!_direction_type && direction_entry.element === "asset"  &&
            (direction_entry.meta.classes.content[0].content  ===
              "messageBody")) {
        // Read type
        // Notice: if type is a MIME? Extract last chunk
        _direction_type = direction_entry.attributes.contentType.content;

        if (_direction_type.includes("/") === true) {
          var _type_chunks = _direction_type.split("/");

          _direction_type    = (
            (_type_chunks[_type_chunks.length - 1] || "").toUpperCase()
          );
        }

        // Read data
        _direction_data = (direction_entry.content || "").trim();

        // Collapse tabulations from 4 spaces to 2 spaces (this improves \
        //   legibility)
        _direction_data = _direction_data.replace(/^( +)\1/gm, "$1");
      }
    });

    return {
      name : name,
      type : (_direction_type || null),
      data : (_direction_data || null)
    };
  }
};


================================================
FILE: res/plugins/marked/extensions/embed.js
================================================
/*
 * chappe
 *
 * Copyright 2021, Crisp IM SAS
 * Author: Valerian Saliou <valerian@valeriansaliou.name>
 */


var _s  = require("escape-html");


// Format: `${type}[title](target)`

module.exports = {
  name  : "embed",
  level : "inline",

  start : function(source) {
    var _match = source.match(
      /^\${(?:[^{}]+)}\[(?:[^\[\]]*)\]\((?:[^\(\)]+)\)/
    );

    if (_match) {
      return _match.index;
    }
  },

  tokenizer : function(source) {
    var _match = /^\${([^{}]+)}\[([^\[\]]*)\]\(([^\(\)]+)\)$/.exec(source);

    if (_match) {
      return {
        type     : "embed",
        raw      : _match[0],
        injector : _match[1],
        title    : _match[2],
        target   : _match[3]
      };
    }
  },

  renderer : function(token) {
    // Generate preview code
    var _preview_url;

    switch (token.injector) {
      case "youtube": {
        // Generate YouTube preview image URL
        _preview_url = (
          "https://img.youtube.com/vi/" + token.target + "/maxresdefault.jpg"
        );

        break;
      }

      default: {
        throw new Error(
          "Unsupported embed injector: " + token.injector
        );
      }
    }

    // Generate caption code
    var _caption_code = (
      !token.title ? "" : (
        "<div class=\"embed-caption\">" + _s(token.title) + "</div>"
      )
    );

    // Generate final embed code
    return (
      "<div "                                                              +
          "class=\"embed\" "                                               +
          "data-injector=\"" + token.injector + "\" "                      +
          "data-target=\"" + token.target + "\" "                          +
          "data-loaded=\"false\">"                                         +
        "<div class=\"embed-wrap\">"                                       +
          "<div class=\"embed-frame\"></div>"                              +

          "<div "                                                          +
              "class=\"embed-preview\" "                                   +
              "style=\"background-image: url('" + _preview_url + "');\">"  +
          "</div>"                                                         +
        "</div>"                                                           +

        _caption_code                                                      +
      "</div>"
    );
  }
};


================================================
FILE: res/plugins/marked/extensions/emphasis.js
================================================
/*
 * chappe
 *
 * Copyright 2021, Crisp IM SAS
 * Author: Valerian Saliou <valerian@valeriansaliou.name>
 */

// Format: `!!! text`, `!! text` or `! text`

module.exports = {
  name  : "emphasis",
  level : "block",

  start : function(source) {
    var _match = source.match(/^(?:[!]{1,3})(?:[ ]{1,})(?:[^\n]*)/);

    if (_match) {
      return _match.index;
    }
  },

  tokenizer : function(source) {
    var _match = /^([!]{1,3})(?:[ ]{1,})([^\n]+)/.exec(source);

    if (_match) {
      // Parse level
      var _level;

      switch (_match[1].trim()) {
        case "!!!": {
          _level = "warning";

          break;
        }

        case "!!": {
          _level = "info";

          break;
        }

        default: {
          _level = "notice";
        }
      }

      return {
        type  : "emphasis",
        raw   : _match[0],
        level : _level,
        text  : this.lexer.inlineTokens(_match[2].trim())
      };
    }
  },

  renderer : function(token) {
    return (
      "<div class=\"emphasis emphasis--" + token.level + "\">"  +
        this.parser.parseInline(token.text)                     +
      "</div>"
    );
  }
};


================================================
FILE: res/plugins/marked/extensions/figcaption.js
================================================
/*
 * chappe
 *
 * Copyright 2021, Crisp IM SAS
 * Author: Valerian Saliou <valerian@valeriansaliou.name>
 */


var _s  = require("escape-html");


// Format: `$[caption](<code>)`

module.exports = {
  name  : "figcaption",
  level : "block",

  start : function(source) {
    var _match = source.match(/^(?:\$\[[^\[\]\n]+\]\([^\n]+)\)/);

    if (_match) {
      return _match.index;
    }
  },

  tokenizer : function(source) {
    var _match = /^\$\[([^\[\]\n]+)\]\(([^\n]+)\)/.exec(source);

    if (_match) {
      return {
        type    : "figcaption",
        raw     : _match[0],
        caption : _match[1],
        code    : this.lexer.inlineTokens(_match[2].trim())
      };
    }
  },

  renderer : function(token) {
    return (
      "<figure>"                                              +
        this.parser.parseInline(token.code)                   +
        "<figcaption>" + _s(token.caption) + "</figcaption>"  +
      "</figure>"
    );
  }
};


================================================
FILE: res/plugins/marked/extensions/navigation-item.js
================================================
/*
 * chappe
 *
 * Copyright 2021, Crisp IM SAS
 * Author: Valerian Saliou <valerian@valeriansaliou.name>
 */


var _s  = require("escape-html");


// Format: `* Title: Description -> URL` for child

var RULE = (
  /^(?:[ ]*\|[ ]?([^:\n]+):[ ]?([^:\n]+)[ ]?->[ ]([^>\n\[\]]+)(?: \[(blank|self)\])?(?:\n|$))/
);

module.exports = {
  name  : "navigation-item",
  level : "inline",

  start : function(source) {
    var _match = source.match(RULE);

    if (_match) {
      return _match.index;
    }
  },

  tokenizer : function(source) {
    var _match = RULE.exec(source);

    if (_match) {
      return {
        type        : "navigation-item",
        raw         : _match[0],
        title       : _match[1].trim(),
        description : _match[2].trim(),
        url         : _match[3].trim(),
        target      : (_match[4] || "self").trim()
      };
    }
  },

  renderer : function(token) {
    return (
      "<li class=\"navigation-item\">"                                        +
        "<a "                                                                 +
            "class=\"navigation-link\" "                                      +
            "href=\"" + _s(token.url) + "\" "                                 +
            "target=\"_" + _s(token.target) + "\">"                           +
          "<span class=\"navigation-text\">"                                  +
            "<span class=\"navigation-title font-sans-semibold\">"            +
              _s(token.title)                                                 +
            "</span>"                                                         +

            "<span class=\"navigation-label font-sans-regular\">"             +
              _s(token.description)                                           +
            "</span>"                                                         +
          "</span>"                                                           +

          "<span class=\"navigation-action font-sans-semibold\">Read</span>"  +
        "</a>"                                                                +
      "</li>"
    );
  }
};


================================================
FILE: res/plugins/marked/extensions/navigation.js
================================================
/*
 * chappe
 *
 * Copyright 2021, Crisp IM SAS
 * Author: Valerian Saliou <valerian@valeriansaliou.name>
 */

// Format: `+ Navigation` for parent

var RULE = (
  /^(?:\+ Navigation[ ]*\n{1,2})((?:(?:[ ]*\|[ ]?(?:[^:\n]+):[ ]?(?:[^:\n]+)[ ]?->[ ](?:[^>\n]+))(?:\n|$))+)/
);

module.exports = {
  name  : "navigation",
  level : "block",

  start : function(source) {
    var _match = source.match(RULE);

    if (_match) {
      return _match.index;
    }
  },

  tokenizer : function(source) {
    var _match = RULE.exec(source);

    if (_match) {
      var _token = {
        type   : "navigation",
        raw    : _match[0],
        text   : _match[1],
        tokens : []
      };

      this.lexer.inline(
        _token.text, _token.tokens
      );

      return _token;
    }
  },

  renderer : function(token) {
    return (
      "<ul class=\"navigation\">"              +
        this.parser.parseInline(token.tokens)  +
      "</ul>"
    );
  }
};


================================================
FILE: res/plugins/marked/renderers/code.js
================================================
/*
 * chappe
 *
 * Copyright 2022, Crisp IM SAS
 * Author: Valerian Saliou <valerian@valeriansaliou.name>
 */


var _s  = require("escape-html");


module.exports = function(code, infostring, escaped) {
  // Acquire language name and its associated class (if any)
  var _language = (infostring || "").trim();

  var _class    = (
    _language ? (this.options.langPrefix + _s(_language)) : null
  );

  return (
    "<pre" + (code ? " class=\"copy\"" : "") + ">"                         +
      "<span class=\"code-clipboard copy-button\"></span>"                 +

      "<code class=\"copy-value" + (_class ? (" " + _class) : "") + "\">"  +
        (escaped ? code : _s(code))                                        +
      "</code>"                                                            +
    "</pre>"                                                               +
    "\n"
  );
};


================================================
FILE: res/plugins/marked/renderers/heading.js
================================================
/*
 * chappe
 *
 * Copyright 2021, Crisp IM SAS
 * Author: Valerian Saliou <valerian@valeriansaliou.name>
 */


var slug  = require("slug");


module.exports = function(text, level) {
  var _id = slug(text);

  return (
    "<h" + level + " id=\"" + _id + "\">"                 +
      "<a class=\"title-anchor\" href=\"#" + _id + "\">"  +
        text                                              +
      "</a>"                                              +
    "</h" + level + ">"
  );
};


================================================
FILE: src/javascripts/common/common.js
================================================
/*
 * chappe
 *
 * Copyright 2021, Crisp IM SAS
 * Author: Valerian Saliou <valerian@valeriansaliou.name>
 */


/**
 * Common
 * @class
 * @classdesc Common class.
 */
class Common {
  /**
   * Constructor
   */
  constructor() {
    let fn = "constructor";

    try {
      this.ns = "Common";
      this._$ = $;

      // Selectors
      this._document_sel = this._$(document);

      // Configuration
      this.__revision                    = "@:revision";
      this.__url_status                  = "@:url_status";

      this.__search_index_path           = "@:search_index";
      this.__search_index_options_base   = "@:search_options_base";
      this.__search_index_options_query  = "@:search_options_query";

      this.__second_in_milliseconds      = 1000;   // 1 second

      this.__copy_state_confirm_delay    = 2000;   // 2 seconds

      this.__content_anchor_viewed_delay = 100;    // 1/10 second

      this.__status_poll_interval        = 5000;   // 5 seconds
      this.__status_poll_refresh         = 90000;  // 90 seconds

      this.__chatbox_z_index             = 120;
      this.__search_results_limit        = 12;

      this.__cookie_prefix               = "chappe/";

      this.__status_known_health         = [
        "healthy",
        "sick",
        "dead"
      ];

      // Instances
      this.__escape_html_text_rules = {
        "&amp;"  : /&/g,
        "&lt;"   : /</g,
        "&gt;"   : />/g,
        "&quot;" : /"/g
      };

      // Storage
      this.__crisp_chat_feedback_shown     = false;
      this.__search_opened                 = false;
      this.__appearance_mode               = "light";

      this.__content_anchor_viewed_timeout = null;
      this.__status_poll_scheduler         = null;

      this.__search_index_responder        = null;
      this.__search_index_load_pending     = null;

      this.__search_field_value_last       = "";

      this.__status_poll_health            = null;
      this.__status_poll_last_check        = 0;
    } catch (error) {
      Console.error(`${this.ns}.${fn}`, error);
    }
  }


  /**
   * Initializes the class
   * @public
   * @return {undefined}
   */
  init() {
    let fn = "init";

    try {
      this.__chatbox();
      this.__options();
      this.__events();
      this.__schedules();
    } catch (error) {
      Console.error(`${this.ns}.${fn}`, error);
    }
  }


  /**
   * Configures the chatbox
   * @private
   * @return {undefined}
   */
  __chatbox() {
    let fn = "__chatbox";

    try {
      // Adjust chatbox z-index? (if chatbox is included)
      if (typeof window.$crisp !== "undefined") {
        window.$crisp.push([
          "config", "container:index", this.__chatbox_z_index
        ]);
      }
    } catch (error) {
      Console.error(`${this.ns}.${fn}`, error);
    }
  }


  /**
   * Setups all user options
   * @private
   * @return {undefined}
   */
  __options() {
    let fn = "__options";

    try {
      // Restore appearance options
      this.__toggle_appearance(
        this.__detect_appearance_preference()  //-[mode]
      );
    } catch (error) {
      Console.error(`${this.ns}.${fn}`, error);
    }
  }


  /**
   * Binds all events
   * @private
   * @return {undefined}
   */
  __events() {
    let fn = "__events";

    try {
      // Bind common events
      this.__bind_copy_click();

      // Bind search events
      this.__bind_search_open_keydown();
      this.__bind_search_open_click();
      this.__bind_search_close_click();
      this.__bind_search_field_keyup();

      // Bind appearance events
      this.__bind_appearance_toggle_click();

      // Bind sidebar events
      this.__bind_sidebar_toggler_click();
      this.__bind_sidebar_nest_level_toggle_click();

      // Bind content events
      this.__bind_content_anchor_viewed();

      // Bind code events
      this.__bind_code_metas_picker_change();
      this.__bind_code_block_viewed();

      // Bind Markdown events
      this.__bind_markdown_embed_click();

      // Bind chatbox events
      this.__bind_crisp_chat_open_click();
      this.__bind_crisp_chat_feedback_click();
    } catch (error) {
      Console.error(`${this.ns}.${fn}`, error);
    }
  }


  /**
   * Binds all schedules
   * @private
   * @return {undefined}
   */
  __schedules() {
    let fn = "__schedules";

    try {
      // Bind all schedules
      this.__bind_status_poll_schedule();
    } catch (error) {
      Console.error(`${this.ns}.${fn}`, error);
    }
  }


  /**
   * Reads cookie
   * @private
   * @param  {string} cookie_key
   * @return {string} Cookie value (if any)
   */
  __read_cookie(cookie_key) {
    let fn = "__read_cookie";

    let _cookie_value;

    try {
      _cookie_value = Cookies.get(
        (this.__cookie_prefix + cookie_key)
      );
    } catch (error) {
      Console.error(`${this.ns}.${fn}`, error);
    } finally {
      return _cookie_value;
    }
  }


  /**
   * Writes cookie
   * @private
   * @param  {string} cookie_key
   * @param  {string} cookie_value
   * @return {undefined}
   */
  __write_cookie(cookie_key, cookie_value) {
    let fn = "__write_cookie";

    try {
      Cookies.set(
        (this.__cookie_prefix + cookie_key), cookie_value,

        {
          domain   : location.hostname,
          expires  : Infinity,
          sameSite : "strict"
        }
      );
    } catch (error) {
      Console.error(`${this.ns}.${fn}`, error);
    }
  }


  /**
   * Copies text (from element)
   * @private
   * @param  {object} text_el
   * @return {undefined}
   */
  __copy_text(text_el) {
    let fn = "__copy_text";

    let _was_copied = false;

    try {
      // Select text (clear out existing selections first)
      let _range = document.createRange();

      _range.selectNode(text_el);

      window.getSelection().removeAllRanges();
      window.getSelection().addRange(_range);

      // Copy text
      document.execCommand("copy");

      window.getSelection().removeAllRanges();

      _was_copied = true;
    } catch (error) {
      Console.error(`${this.ns}.${fn}`, error);
    } finally {
      return _was_copied;
    }
  }


  /**
   * Escapes text (to be included in HTML)
   * @private
   * @param  {string} text_raw
   * @return {string} Escaped text
   */
  __escape_html_text(text_raw) {
    let fn = "__escape_html_text";

    let _text_safe = "";

    try {
      let _text_buffer = text_raw;

      // Apply all escape rules to text
      for (let _value in this.__escape_html_text_rules) {
        let _rule = this.__escape_html_text_rules[_value];

        _text_buffer = _text_buffer.replace(_rule, _value);
      }

      _text_safe = _text_buffer;
    } catch (error) {
      Console.error(`${this.ns}.${fn}`, error);
    } finally {
      return _text_safe;
    }
  }


  /**
   * Opens up search
   * @private
   * @return {undefined}
   */
  __open_search() {
    let fn = "__open_search";

    try {
      if (this.__search_opened !== true) {
        this.__search_opened = true;

        // Open search
        let _search_sel = this._$("#search"),
            _input_sel  = _search_sel.find(".spotlight-input");

        _search_sel.css("display", "block");

        // Focus on search input?
        if (_input_sel.length > 0) {
          _input_sel[0].focus();
        }

        // Ensure that search index is loaded? (if not already loaded, or not \
        //   already loading)
        this.__ensure_load_search_index(_search_sel);
      }
    } catch (error) {
      Console.error(`${this.ns}.${fn}`, error);
    }
  }


  /**
   * Closes search
   * @private
   * @param  {boolean} [do_reset]
   * @return {undefined}
   */
  __close_search(do_reset=false) {
    let fn = "__close_search";

    try {
      if (this.__search_opened === true) {
        this.__search_opened = false;

        // Close search
        let _search_sel = this._$("#search");

        _search_sel.css("display", "none");

        // Reset search value and results?
        if (do_reset === true) {
          let _spotlight_sel = _search_sel.find(".spotlight"),
              _input_sel     = _spotlight_sel.find(".spotlight-input"),
              _entries_sel   = _spotlight_sel.find(".spotlight-entries");

          // Reset input value
          this.__search_field_value_last = "";

          _input_sel.val("");

          // Reset search results
          _entries_sel.empty();

          _spotlight_sel.attr("data-has-results", "false");
        }
      }
    } catch (error) {
      Console.error(`${this.ns}.${fn}`, error);
    }
  }


  /**
   * Toggles selected search result
   * @private
   * @param  {object} results_all_sel
   * @param  {number} [increment]
   * @return {undefined}
   */
  __toggle_search_result_selected(results_all_sel, increment=1) {
    let fn = "__toggle_search_result_selected";

    try {
      // Any result?
      if (results_all_sel.length > 0) {
        // Find the index of the active result
        let _index = -1;

        for (let _i = 0; _i < results_all_sel.length; _i++) {
          if (results_all_sel[_i].getAttribute("data-selected") === "true") {
            _index = _i;

            break;
          }
        }

        // Process the index of the next element to activate
        _index += increment;

        if (_index < 0) {
          _index = (results_all_sel.length - 1);
        } else if (_index >= results_all_sel.length) {
          _index = 0;
        }

        // Select target element?
        if (_index > -1) {
          let _result_selected_el = results_all_sel[_index];

          // Set selected state to selected element
          results_all_sel.removeAttr("data-selected");

          this._$(_result_selected_el).attr("data-selected", "true");

          // Scroll to selected element
          _result_selected_el.scrollIntoView({
            behavior : "smooth",
            block    : "nearest"
          });
        }
      }
    } catch (error) {
      Console.error(`${this.ns}.${fn}`, error);
    }
  }


  /**
   * Proceeds search query
   * @private
   * @param  {object} spotlight_sel
   * @param  {object} results_sel
   * @param  {object} entries_sel
   * @param  {string} [search_query]
   * @return {undefined}
   */
  __proceed_search_query(
    spotlight_sel, results_sel, entries_sel, search_query=""
  ) {
    let fn = "__proceed_search_query";

    try {
      // Initialize 'has results' state
      let _has_results = false;

      // Acquire search results
      if (search_query) {
        // Responder is not available? Throw an error, as this should \
        //   never happen.
        if (this.__search_index_responder === null) {
          throw new Error(
            "Search index responder is not available (yet?)"
          );
        }

        // Obtain search results from index responder
        let _search_results = (
       
Download .txt
gitextract_j_mdcei_/

├── .babelrc
├── .banner
├── .bowerrc
├── .github/
│   └── workflows/
│       ├── build.yml
│       └── test.yml
├── .gitignore
├── .jscsrc
├── .jshintrc
├── .npmignore
├── .pug-lintrc
├── .stylelintrc.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin/
│   └── chappe.js
├── bower.json
├── examples/
│   └── acme-docs/
│       ├── config.json
│       ├── data/
│       │   ├── changes/
│       │   │   ├── 2020.json
│       │   │   └── 2021.json
│       │   ├── guides/
│       │   │   ├── hello-world/
│       │   │   │   ├── index.md
│       │   │   │   └── quickstart/
│       │   │   │       └── index.md
│       │   │   ├── index.md
│       │   │   └── markdown-syntax/
│       │   │       ├── index.md
│       │   │       ├── section-example/
│       │   │       │   ├── index.md
│       │   │       │   ├── sub-section-one/
│       │   │       │   │   └── index.md
│       │   │       │   └── sub-section-two/
│       │   │       │       └── index.md
│       │   │       └── syntax/
│       │   │           └── index.md
│       │   └── references/
│       │       ├── rest-api/
│       │       │   ├── _private.md
│       │       │   └── v1.md
│       │       └── rtm-api/
│       │           └── v1.md
│       └── package.json
├── gulpfile.js
├── package.json
├── res/
│   ├── config/
│   │   ├── common.json
│   │   └── user.json
│   └── plugins/
│       ├── gulp/
│       │   ├── minisearch.js
│       │   └── pug-templates.js
│       └── marked/
│           ├── extensions/
│           │   ├── embed.js
│           │   ├── emphasis.js
│           │   ├── figcaption.js
│           │   ├── navigation-item.js
│           │   └── navigation.js
│           └── renderers/
│               ├── code.js
│               └── heading.js
└── src/
    ├── javascripts/
    │   └── common/
    │       └── common.js
    ├── locales/
    │   └── en.json
    ├── stylesheets/
    │   ├── _colors.scss
    │   ├── _config.scss
    │   ├── _functions.scss
    │   ├── _globals.scss
    │   ├── _mixins.scss
    │   ├── _variables.scss
    │   ├── changes/
    │   │   └── changes.scss
    │   ├── common/
    │   │   ├── _appearance.scss
    │   │   ├── _badges.scss
    │   │   ├── _base.scss
    │   │   ├── _buttons.scss
    │   │   ├── _code.scss
    │   │   ├── _content.scss
    │   │   ├── _fonts.scss
    │   │   ├── _footer.scss
    │   │   ├── _header.scss
    │   │   ├── _highlight.scss
    │   │   ├── _markdown.scss
    │   │   ├── _search.scss
    │   │   ├── _viewer.scss
    │   │   └── common.scss
    │   ├── guides/
    │   │   ├── _common.scss
    │   │   ├── _content.scss
    │   │   ├── _sidebar_left.scss
    │   │   └── guides.scss
    │   ├── home/
    │   │   └── home.scss
    │   ├── not_found/
    │   │   └── not_found.scss
    │   └── references/
    │       ├── _content.scss
    │       ├── _sidebar_left.scss
    │       └── references.scss
    └── templates/
        ├── __base.pug
        ├── _body_footer.pug
        ├── _body_header.pug
        ├── _body_search.pug
        ├── _head_favicon.pug
        ├── _head_http.pug
        ├── _head_includes.pug
        ├── _head_metas.pug
        ├── _head_screen.pug
        ├── _head_theme.pug
        ├── _mixins.pug
        ├── changes/
        │   └── index.pug
        ├── guides/
        │   ├── _content.pug
        │   ├── _sidebar_left.pug
        │   └── index.pug
        ├── home/
        │   └── index.pug
        ├── not_found/
        │   └── index.pug
        └── references/
            ├── _blueprint.pug
            ├── _blueprint_content.pug
            ├── _blueprint_sidebar_left.pug
            ├── _markdown.pug
            ├── _markdown_content.pug
            ├── _markdown_sidebar_left.pug
            ├── _mixins_blueprint.pug
            ├── _mixins_common.pug
            └── index.pug
Download .txt
SYMBOL INDEX (59 symbols across 2 files)

FILE: bin/chappe.js
  class ChappeCLI (line 32) | class ChappeCLI {
    method constructor (line 36) | constructor() {
    method run (line 138) | run() {
    method __run_help (line 159) | __run_help() {
    method __run_version (line 191) | __run_version() {
    method __run_default (line 203) | __run_default() {
    method __format_help_actions (line 287) | __format_help_actions(actions) {
    method __format_help_argument (line 302) | __format_help_argument(name) {
    method __dump_banner (line 321) | __dump_banner() {
    method __dump_context (line 343) | __dump_context(context) {
    method __acquire_action (line 357) | __acquire_action() {
    method __acquire_context (line 380) | __acquire_context(task) {
    method __inject_defaults_target (line 455) | __inject_defaults_target(defaults, target) {
    method __setup_error_traps (line 482) | __setup_error_traps(gulp, spinner, task) {
    method __setup_gulp_logging (line 543) | __setup_gulp_logging(gulp, spinner) {

FILE: src/javascripts/common/common.js
  class Common (line 14) | class Common {
    method constructor (line 18) | constructor() {
    method init (line 90) | init() {
    method __chatbox (line 109) | __chatbox() {
    method __options (line 130) | __options() {
    method __events (line 149) | __events() {
    method __schedules (line 193) | __schedules() {
    method __read_cookie (line 211) | __read_cookie(cookie_key) {
    method __write_cookie (line 235) | __write_cookie(cookie_key, cookie_value) {
    method __copy_text (line 260) | __copy_text(text_el) {
    method __escape_html_text (line 294) | __escape_html_text(text_raw) {
    method __open_search (line 323) | __open_search() {
    method __close_search (line 357) | __close_search(do_reset=false) {
    method __toggle_search_result_selected (line 399) | __toggle_search_result_selected(results_all_sel, increment=1) {
    method __proceed_search_query (line 456) | __proceed_search_query(
    method __ensure_load_search_index (line 513) | __ensure_load_search_index(search_sel) {
    method __toggle_sidebar_nest_level (line 594) | __toggle_sidebar_nest_level(target_sel, is_anchor=false) {
    method __toggle_appearance (line 637) | __toggle_appearance(new_mode=null) {
    method __detect_appearance_preference (line 662) | __detect_appearance_preference() {
    method __refresh_code_metas_picker_section (line 707) | __refresh_code_metas_picker_section(
    method __schedule_handle_content_anchor_viewed (line 783) | __schedule_handle_content_anchor_viewed(
    method __handle_content_anchor_viewed (line 818) | __handle_content_anchor_viewed(
    method __handle_code_metas_picker_change (line 887) | __handle_code_metas_picker_change(code_box_sel) {
    method __handle_code_block_viewed (line 947) | __handle_code_block_viewed(target_sel) {
    method __handle_crisp_chat_open_click (line 990) | __handle_crisp_chat_open_click() {
    method __handle_crisp_chat_feedback_click (line 1010) | __handle_crisp_chat_feedback_click() {
    method __inject_search_results (line 1055) | __inject_search_results(results_sel, entries_sel, results) {
    method __fetch_status_health (line 1161) | __fetch_status_health(provider, target) {
    method __refresh_status_indicator (line 1214) | __refresh_status_indicator(status_sel, seconds_sel) {
    method __bind_search_open_keydown (line 1238) | __bind_search_open_keydown() {
    method __bind_search_open_click (line 1370) | __bind_search_open_click() {
    method __bind_search_close_click (line 1388) | __bind_search_close_click() {
    method __bind_search_field_keyup (line 1417) | __bind_search_field_keyup() {
    method __bind_copy_click (line 1474) | __bind_copy_click() {
    method __bind_appearance_toggle_click (line 1539) | __bind_appearance_toggle_click() {
    method __bind_sidebar_toggler_click (line 1572) | __bind_sidebar_toggler_click() {
    method __bind_sidebar_nest_level_toggle_click (line 1628) | __bind_sidebar_nest_level_toggle_click() {
    method __bind_content_anchor_viewed (line 1655) | __bind_content_anchor_viewed() {
    method __bind_code_metas_picker_change (line 1773) | __bind_code_metas_picker_change() {
    method __bind_code_block_viewed (line 1814) | __bind_code_block_viewed() {
    method __bind_markdown_embed_click (line 1885) | __bind_markdown_embed_click() {
    method __bind_crisp_chat_open_click (line 1962) | __bind_crisp_chat_open_click() {
    method __bind_crisp_chat_feedback_click (line 1990) | __bind_crisp_chat_feedback_click() {
    method __bind_status_poll_schedule (line 2018) | __bind_status_poll_schedule() {
Condensed preview — 102 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (463K chars).
[
  {
    "path": ".babelrc",
    "chars": 26,
    "preview": "{\n  presets: [\"es2015\"]\n}\n"
  },
  {
    "path": ".banner",
    "chars": 813,
    "preview": "      ___          ___          ___          ___       ___       ___\n     /  /\\        /__/\\        /  /\\        /  /\\  "
  },
  {
    "path": ".bowerrc",
    "chars": 46,
    "preview": "{\n  \"registry\": \"https://registry.bower.io\"\n}\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 568,
    "preview": "on:\n  push:\n    tags:\n      - \"v*.*.*\"\n\npermissions:\n  id-token: write\n\nname: Build and Release\n\njobs:\n  release:\n    ru"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 957,
    "preview": "on: [push, pull_request]\n\nname: Test and Build\n\njobs:\n  test:\n    strategy:\n      matrix:\n        os: [ubuntu-latest]\n  "
  },
  {
    "path": ".gitignore",
    "chars": 84,
    "preview": ".DS_Store\nThumbs.db\nnpm-debug.log\n\npackage-lock.json\n\nnode_modules/\n.chappe/\n\ndist/\n"
  },
  {
    "path": ".jscsrc",
    "chars": 1186,
    "preview": "{\n  \"maximumLineLength\": 80,\n\n  \"validateIndentation\": 2,\n  \"validateQuoteMarks\": \"\\\"\",\n\n  \"disallowTrailingComma\": true"
  },
  {
    "path": ".jshintrc",
    "chars": 148,
    "preview": "{\n  \"camelcase\": false,\n  \"esversion\": 6,\n  \"node\": true,\n  \"predef\": [\n    \"require\",\n    \"define\",\n    \"escape\",\n    \""
  },
  {
    "path": ".npmignore",
    "chars": 62,
    "preview": "package-lock.json\n\n.github/**\n.chappe/**\ndist/**\nexamples/**\n\n"
  },
  {
    "path": ".pug-lintrc",
    "chars": 1176,
    "preview": "{\n  \"requireLowerCaseTags\": true,\n  \"requireLowerCaseAttributes\": true,\n  \"requireLineFeedAtFileEnd\": true,\n\n  \"requireI"
  },
  {
    "path": ".stylelintrc.yml",
    "chars": 306,
    "preview": "extends:\n  - stylelint-config-standard-scss\n\nrules:\n  selector-id-pattern: null\n  selector-class-pattern: null\n\n  at-rul"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 6752,
    "preview": "# Changelog\n\n## 1.16.0 (2026-05-03)\n\n### New Features\n\n* Added support for footnotes in guides.\n\n## 1.15.2 (2026-03-31)\n"
  },
  {
    "path": "LICENSE",
    "chars": 1056,
    "preview": "Copyright (c) 2021 Crisp IM SAS\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this so"
  },
  {
    "path": "README.md",
    "chars": 25240,
    "preview": "<img alt=\"Chappe\" src=\"https://crisp-oss.github.io/chappe/images/chappe.png\" width=\"300\">\n\n[![Test and Build](https://gi"
  },
  {
    "path": "bin/chappe.js",
    "chars": 13450,
    "preview": "#!/usr/bin/env node\n\n/*\n * chappe\n *\n * Copyright 2021, Crisp IM SAS\n * Author: Valerian Saliou <valerian@valeriansaliou"
  },
  {
    "path": "bower.json",
    "chars": 666,
    "preview": "{\n  \"name\": \"chappe\",\n\n  \"dependencies\": {\n    \"reset.css\": \"https://github.com/shannonmoeller/reset-css.git#d8bfbea7095"
  },
  {
    "path": "examples/acme-docs/config.json",
    "chars": 6711,
    "preview": "{\n  \"identity\" : {\n    \"title\"     : \"Acme Developer Hub\",\n    \"copyright\" : \"Acme Inc.\"\n  },\n\n  \"theme\" : {\n    \"accent"
  },
  {
    "path": "examples/acme-docs/data/changes/2020.json",
    "chars": 411,
    "preview": "[\n  {\n    \"group\" : \"rest_api\",\n    \"type\"  : \"change\",\n    \"date\"  : \"2020-12-08\",\n\n    \"text\"  : \"The REST API now rej"
  },
  {
    "path": "examples/acme-docs/data/changes/2021.json",
    "chars": 694,
    "preview": "[\n  {\n    \"group\" : \"rest_api\",\n    \"type\"  : \"change\",\n    \"date\"  : \"2021-12-03\",\n\n    \"text\"  : \"New official REST AP"
  },
  {
    "path": "examples/acme-docs/data/guides/hello-world/index.md",
    "chars": 368,
    "preview": "TITLE: Hello World\nINDEX: 1\nUPDATED: 2021-09-22\nLINK: REST API Reference -> /references/rest-api/v1/\nLINK: RTM API Refer"
  },
  {
    "path": "examples/acme-docs/data/guides/hello-world/quickstart/index.md",
    "chars": 4346,
    "preview": "TITLE: Quickstart\nINDEX: 1\nUPDATED: 2021-09-22\n\n**To retrieve and send data on behalf of Acme teams, you need to use the"
  },
  {
    "path": "examples/acme-docs/data/guides/index.md",
    "chars": 905,
    "preview": "TITLE: Guides\nINDEX: 1\nUPDATED: 2021-09-22\n\n**👋 Welcome to the Acme Developer Hub!** We have written extensive guides to"
  },
  {
    "path": "examples/acme-docs/data/guides/markdown-syntax/index.md",
    "chars": 264,
    "preview": "TITLE: Markdown Syntax\nINDEX: 2\nUPDATED: 2022-01-05\n\nThis is the Markdown syntax guide, it contains two sections:\n\n+ Nav"
  },
  {
    "path": "examples/acme-docs/data/guides/markdown-syntax/section-example/index.md",
    "chars": 315,
    "preview": "TITLE: Section Example\nINDEX: 2\nUPDATED: 2022-01-05\n\nThis section contains two example sub-sections. **Pick one in the s"
  },
  {
    "path": "examples/acme-docs/data/guides/markdown-syntax/section-example/sub-section-one/index.md",
    "chars": 109,
    "preview": "TITLE: Sub-Section One\nINDEX: 1\nUPDATED: 2022-01-05\n\n! This is an example of a sub-section! **(number one)**\n"
  },
  {
    "path": "examples/acme-docs/data/guides/markdown-syntax/section-example/sub-section-two/index.md",
    "chars": 110,
    "preview": "TITLE: Sub-Section Two\nINDEX: 1\nUPDATED: 2022-01-05\n\n!! This is an example of a sub-section! **(number two)**\n"
  },
  {
    "path": "examples/acme-docs/data/guides/markdown-syntax/syntax/index.md",
    "chars": 5825,
    "preview": "TITLE: Syntax\nINDEX: 1\nUPDATED: 2022-01-05\n\n# Navigation Links\n\nYou can easily insert navigation links anywhere as such:"
  },
  {
    "path": "examples/acme-docs/data/references/rest-api/_private.md",
    "chars": 1562,
    "preview": "TYPE: API Blueprint\nTITLE: REST API Reference (Private)\nUPDATED: 2021-11-03\nFORMAT: 1A\nHOST: https://api.acme.com/v1\n\n# "
  },
  {
    "path": "examples/acme-docs/data/references/rest-api/v1.md",
    "chars": 14794,
    "preview": "TYPE: API Blueprint\nTITLE: REST API Reference (V1)\nUPDATED: 2021-12-22\nFORMAT: 1A\nHOST: https://api.acme.com/v1\n\n# Refer"
  },
  {
    "path": "examples/acme-docs/data/references/rtm-api/v1.md",
    "chars": 2374,
    "preview": "TYPE: Markdown\nTITLE: RTM API Reference (V1)\nUPDATED: 2021-09-22\n\nEvents are sent on the RTM Events API WebSocket channe"
  },
  {
    "path": "examples/acme-docs/package.json",
    "chars": 50,
    "preview": "{\n  \"dependencies\": {\n    \"chappe\": \"1.x.x\"\n  }\n}\n"
  },
  {
    "path": "gulpfile.js",
    "chars": 42303,
    "preview": "/*\n * chappe\n *\n * Copyright 2021, Crisp IM SAS\n * Author: Valerian Saliou <valerian@valeriansaliou.name>\n */\n\n\nvar fs  "
  },
  {
    "path": "package.json",
    "chars": 2709,
    "preview": "{\n  \"name\": \"chappe\",\n  \"description\": \"Developer Docs builder. Write guides in Markdown and references in API Blueprint"
  },
  {
    "path": "res/config/common.json",
    "chars": 1878,
    "preview": "{\n  \"LANGS\" : [\n    \"en\"\n  ],\n\n  \"URLS\" : {\n    \"CRISP_WEB\" : \"https://crisp.chat/\"\n  },\n\n  \"LIBRARIES\" : {\n    \"JAVASCR"
  },
  {
    "path": "res/config/user.json",
    "chars": 2766,
    "preview": "{\n  \"identity\" : {\n    \"title\"     : \"\",\n    \"copyright\" : \"\"\n  },\n\n  \"theme\" : {\n    \"accent\" : {\n      \"light\" : {\n   "
  },
  {
    "path": "res/plugins/gulp/minisearch.js",
    "chars": 3105,
    "preview": "/*\n * chappe\n *\n * Copyright 2021, Crisp IM SAS\n * Author: Valerian Saliou <valerian@valeriansaliou.name>\n */\n\n\nvar remo"
  },
  {
    "path": "res/plugins/gulp/pug-templates.js",
    "chars": 29697,
    "preview": "/*\n * chappe\n *\n * Copyright 2021, Crisp IM SAS\n * Author: Valerian Saliou <valerian@valeriansaliou.name>\n */\n\n\nvar fs  "
  },
  {
    "path": "res/plugins/marked/extensions/embed.js",
    "chars": 2430,
    "preview": "/*\n * chappe\n *\n * Copyright 2021, Crisp IM SAS\n * Author: Valerian Saliou <valerian@valeriansaliou.name>\n */\n\n\nvar _s  "
  },
  {
    "path": "res/plugins/marked/extensions/emphasis.js",
    "chars": 1167,
    "preview": "/*\n * chappe\n *\n * Copyright 2021, Crisp IM SAS\n * Author: Valerian Saliou <valerian@valeriansaliou.name>\n */\n\n// Format"
  },
  {
    "path": "res/plugins/marked/extensions/figcaption.js",
    "chars": 968,
    "preview": "/*\n * chappe\n *\n * Copyright 2021, Crisp IM SAS\n * Author: Valerian Saliou <valerian@valeriansaliou.name>\n */\n\n\nvar _s  "
  },
  {
    "path": "res/plugins/marked/extensions/navigation-item.js",
    "chars": 2148,
    "preview": "/*\n * chappe\n *\n * Copyright 2021, Crisp IM SAS\n * Author: Valerian Saliou <valerian@valeriansaliou.name>\n */\n\n\nvar _s  "
  },
  {
    "path": "res/plugins/marked/extensions/navigation.js",
    "chars": 962,
    "preview": "/*\n * chappe\n *\n * Copyright 2021, Crisp IM SAS\n * Author: Valerian Saliou <valerian@valeriansaliou.name>\n */\n\n// Format"
  },
  {
    "path": "res/plugins/marked/renderers/code.js",
    "chars": 892,
    "preview": "/*\n * chappe\n *\n * Copyright 2022, Crisp IM SAS\n * Author: Valerian Saliou <valerian@valeriansaliou.name>\n */\n\n\nvar _s  "
  },
  {
    "path": "res/plugins/marked/renderers/heading.js",
    "chars": 492,
    "preview": "/*\n * chappe\n *\n * Copyright 2021, Crisp IM SAS\n * Author: Valerian Saliou <valerian@valeriansaliou.name>\n */\n\n\nvar slug"
  },
  {
    "path": "src/javascripts/common/common.js",
    "chars": 59556,
    "preview": "/*\n * chappe\n *\n * Copyright 2021, Crisp IM SAS\n * Author: Valerian Saliou <valerian@valeriansaliou.name>\n */\n\n\n/**\n * C"
  },
  {
    "path": "src/locales/en.json",
    "chars": 3576,
    "preview": "{\n  \"COMMON\" : {\n    \"HEADER\" : {\n      \"EXTRAS\" : {\n        \"CHANGES\" : \"Last Changes\"\n      },\n\n      \"SEARCH\" : {\n   "
  },
  {
    "path": "src/stylesheets/_colors.scss",
    "chars": 3277,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n// Palette\n$colo"
  },
  {
    "path": "src/stylesheets/_config.scss",
    "chars": 130,
    "preview": "// chappe\n//\n// Copyright 2026, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n$inlining: false"
  },
  {
    "path": "src/stylesheets/_functions.scss",
    "chars": 1124,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"sass:math\""
  },
  {
    "path": "src/stylesheets/_globals.scss",
    "chars": 189,
    "preview": "// chappe\n//\n// Copyright 2026, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@forward \"_mixin"
  },
  {
    "path": "src/stylesheets/_mixins.scss",
    "chars": 3861,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"_config\" a"
  },
  {
    "path": "src/stylesheets/_variables.scss",
    "chars": 991,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n$cache-buster-ha"
  },
  {
    "path": "src/stylesheets/changes/changes.scss",
    "chars": 6343,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_config"
  },
  {
    "path": "src/stylesheets/common/_appearance.scss",
    "chars": 25690,
    "preview": "// chappe\n//\n// Copyright 2022, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"sass:color"
  },
  {
    "path": "src/stylesheets/common/_badges.scss",
    "chars": 1492,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/common/_base.scss",
    "chars": 1679,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/common/_buttons.scss",
    "chars": 1522,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/common/_code.scss",
    "chars": 4700,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/common/_content.scss",
    "chars": 21301,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/common/_fonts.scss",
    "chars": 1311,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/common/_footer.scss",
    "chars": 6063,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/common/_header.scss",
    "chars": 11521,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/common/_highlight.scss",
    "chars": 1845,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\npre {\n  code {\n "
  },
  {
    "path": "src/stylesheets/common/_markdown.scss",
    "chars": 11080,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/common/_search.scss",
    "chars": 10897,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/common/_viewer.scss",
    "chars": 2200,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/common/common.scss",
    "chars": 380,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_config"
  },
  {
    "path": "src/stylesheets/guides/_common.scss",
    "chars": 356,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/guides/_content.scss",
    "chars": 2263,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/guides/_sidebar_left.scss",
    "chars": 738,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/guides/guides.scss",
    "chars": 219,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_config"
  },
  {
    "path": "src/stylesheets/home/home.scss",
    "chars": 3590,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_config"
  },
  {
    "path": "src/stylesheets/not_found/not_found.scss",
    "chars": 1418,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_config"
  },
  {
    "path": "src/stylesheets/references/_content.scss",
    "chars": 20615,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/references/_sidebar_left.scss",
    "chars": 986,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_global"
  },
  {
    "path": "src/stylesheets/references/references.scss",
    "chars": 203,
    "preview": "// chappe\n//\n// Copyright 2021, Crisp IM SAS\n// Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n@use \"../_config"
  },
  {
    "path": "src/templates/__base.pug",
    "chars": 1253,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\ninclude _mix"
  },
  {
    "path": "src/templates/_body_footer.pug",
    "chars": 2689,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n#footer\n  .i"
  },
  {
    "path": "src/templates/_body_header.pug",
    "chars": 2775,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n#header\n  .w"
  },
  {
    "path": "src/templates/_body_search.pug",
    "chars": 956,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n#search\n  .s"
  },
  {
    "path": "src/templates/_head_favicon.pug",
    "chars": 445,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\nif SITE.favi"
  },
  {
    "path": "src/templates/_head_http.pug",
    "chars": 294,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\nmeta(\n  http"
  },
  {
    "path": "src/templates/_head_includes.pug",
    "chars": 1143,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\nif SITE.toke"
  },
  {
    "path": "src/templates/_head_metas.pug",
    "chars": 709,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\nmeta(\n  name"
  },
  {
    "path": "src/templates/_head_screen.pug",
    "chars": 183,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\nmeta(\n  name"
  },
  {
    "path": "src/templates/_head_theme.pug",
    "chars": 418,
    "preview": "//- chappe\n//-\n//- Copyright 2022, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\nstyle.\n  .ap"
  },
  {
    "path": "src/templates/_mixins.pug",
    "chars": 5134,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n\nmixin text-"
  },
  {
    "path": "src/templates/changes/index.pug",
    "chars": 3359,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\nextends ../_"
  },
  {
    "path": "src/templates/guides/_content.pug",
    "chars": 1168,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n.content.con"
  },
  {
    "path": "src/templates/guides/_sidebar_left.pug",
    "chars": 331,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\ninclude ../_"
  },
  {
    "path": "src/templates/guides/index.pug",
    "chars": 1157,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\nextends ../_"
  },
  {
    "path": "src/templates/home/index.pug",
    "chars": 3587,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\nextends ../_"
  },
  {
    "path": "src/templates/not_found/index.pug",
    "chars": 906,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\nextends ../_"
  },
  {
    "path": "src/templates/references/_blueprint.pug",
    "chars": 217,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\ninclude _mix"
  },
  {
    "path": "src/templates/references/_blueprint_content.pug",
    "chars": 485,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n.content\n  ."
  },
  {
    "path": "src/templates/references/_blueprint_sidebar_left.pug",
    "chars": 317,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\ninclude ../_"
  },
  {
    "path": "src/templates/references/_markdown.pug",
    "chars": 189,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\ninclude _mix"
  },
  {
    "path": "src/templates/references/_markdown_content.pug",
    "chars": 240,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n.content.con"
  },
  {
    "path": "src/templates/references/_markdown_sidebar_left.pug",
    "chars": 317,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\ninclude ../_"
  },
  {
    "path": "src/templates/references/_mixins_blueprint.pug",
    "chars": 10374,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n\nmixin refer"
  },
  {
    "path": "src/templates/references/_mixins_common.pug",
    "chars": 826,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\n\nmixin refer"
  },
  {
    "path": "src/templates/references/index.pug",
    "chars": 991,
    "preview": "//- chappe\n//-\n//- Copyright 2021, Crisp IM SAS\n//- Author: Valerian Saliou <valerian@valeriansaliou.name>\n\nextends ../_"
  }
]

About this extraction

This page contains the full source code of the crisp-oss/chappe GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 102 files (424.3 KB), approximately 107.3k tokens, and a symbol index with 59 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!