diff --git a/.gitignore b/.gitignore
index eceebd8..5779ab3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,7 +13,6 @@ develop-eggs/
dist/
docs/_build/
eggs/
-lib/
lib64/
parts/
sdist/
diff --git a/docs/.dockerignore b/docs/.dockerignore
new file mode 100644
index 0000000..f643017
--- /dev/null
+++ b/docs/.dockerignore
@@ -0,0 +1,12 @@
+.git/
+.github/
+build/
+.editorconfig
+.gitattributes
+.gitignore
+CHANGELOG.md
+CODE_OF_CONDUCT.md
+deploy.sh
+font-selection.json
+README.md
+Vagrantfile
\ No newline at end of file
diff --git a/docs/.editorconfig b/docs/.editorconfig
new file mode 100644
index 0000000..1692977
--- /dev/null
+++ b/docs/.editorconfig
@@ -0,0 +1,18 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# Top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+
+[*.rb]
+charset = utf-8
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/docs/.gitattributes b/docs/.gitattributes
new file mode 100644
index 0000000..3069c43
--- /dev/null
+++ b/docs/.gitattributes
@@ -0,0 +1 @@
+source/javascripts/lib/* linguist-vendored
diff --git a/docs/.github/ISSUE_TEMPLATE/bug.md b/docs/.github/ISSUE_TEMPLATE/bug.md
new file mode 100644
index 0000000..43305a2
--- /dev/null
+++ b/docs/.github/ISSUE_TEMPLATE/bug.md
@@ -0,0 +1,22 @@
+---
+name: Report a Bug
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Bug Description**
+A clear and concise description of what the bug is and how to reproduce it.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Browser (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Last upstream Slate commit (run `git log --author="\(Robert Lord\)\|\(Matthew Peveler\)\|\(Mike Ralphson\)" | head -n 1`):**
+Put the commit hash here
diff --git a/docs/.github/ISSUE_TEMPLATE/config.yml b/docs/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..16f4bee
--- /dev/null
+++ b/docs/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Questions, Ideas, Discussions
+ url: https://github.com/slatedocs/slate/discussions
+ about: Ask and answer questions, and propose new features.
diff --git a/docs/.github/PULL_REQUEST_TEMPLATE.md b/docs/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..151e45d
--- /dev/null
+++ b/docs/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/docs/.github/dependabot.yml b/docs/.github/dependabot.yml
new file mode 100644
index 0000000..d17e882
--- /dev/null
+++ b/docs/.github/dependabot.yml
@@ -0,0 +1,9 @@
+version: 2
+updates:
+- package-ecosystem: bundler
+ directory: "/"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
+ target-branch: dev
+ versioning-strategy: increase-if-necessary
diff --git a/docs/.github/workflows/build.yml b/docs/.github/workflows/build.yml
new file mode 100644
index 0000000..add4434
--- /dev/null
+++ b/docs/.github/workflows/build.yml
@@ -0,0 +1,42 @@
+name: Build
+
+on:
+ push:
+ branches: [ '*' ]
+ pull_request:
+ branches: [ '*' ]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ ruby-version: [2.6, 2.7, '3.0', 3.1]
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby-version }}
+
+ - uses: actions/cache@v2
+ with:
+ path: vendor/bundle
+ key: gems-${{ runner.os }}-${{ matrix.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }}
+ restore-keys: |
+ gems-${{ runner.os }}-${{ matrix.ruby-version }}-
+ gems-${{ runner.os }}-
+
+ # necessary to get ruby 2.3 to work nicely with bundler vendor/bundle cache
+ # can remove once ruby 2.3 is no longer supported
+ - run: gem update --system
+
+ - run: bundle config set deployment 'true'
+ - name: bundle install
+ run: |
+ bundle config path vendor/bundle
+ bundle install --jobs 4 --retry 3
+
+ - run: bundle exec middleman build
diff --git a/docs/.github/workflows/deploy.yml b/docs/.github/workflows/deploy.yml
new file mode 100644
index 0000000..3eefbec
--- /dev/null
+++ b/docs/.github/workflows/deploy.yml
@@ -0,0 +1,44 @@
+name: Deploy
+
+on:
+ push:
+ branches: [ 'main' ]
+
+jobs:
+ deploy:
+ permissions:
+ contents: write
+
+ runs-on: ubuntu-latest
+ env:
+ ruby-version: 2.6
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ env.ruby-version }}
+
+ - uses: actions/cache@v2
+ with:
+ path: vendor/bundle
+ key: gems-${{ runner.os }}-${{ env.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }}
+ restore-keys: |
+ gems-${{ runner.os }}-${{ env.ruby-version }}-
+ gems-${{ runner.os }}-
+
+ - run: bundle config set deployment 'true'
+ - name: bundle install
+ run: |
+ bundle config path vendor/bundle
+ bundle install --jobs 4 --retry 3
+
+ - run: bundle exec middleman build
+
+ - name: Deploy
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./build
+ keep_files: true
diff --git a/docs/.github/workflows/dev_deploy.yml b/docs/.github/workflows/dev_deploy.yml
new file mode 100644
index 0000000..85c7f8b
--- /dev/null
+++ b/docs/.github/workflows/dev_deploy.yml
@@ -0,0 +1,85 @@
+name: Dev Deploy
+
+on:
+ push:
+ branches: [ 'dev' ]
+
+jobs:
+ push_to_registry:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v2
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v1
+ with:
+ platforms: all
+
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v3
+ with:
+ images: |
+ slatedocs/slate
+ tags: |
+ type=ref,event=branch
+
+ - name: Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v1
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_ACCESS_KEY }}
+
+ - name: Push to Docker Hub
+ uses: docker/build-push-action@v2
+ with:
+ builder: ${{ steps.buildx.outputs.name }}
+ context: .
+ file: ./Dockerfile
+ platforms: linux/amd64,linux/arm64,linux/ppc64le
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ deploy_gh:
+ permissions:
+ contents: write
+
+ runs-on: ubuntu-latest
+ env:
+ ruby-version: 2.6
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ env.ruby-version }}
+
+ - uses: actions/cache@v2
+ with:
+ path: vendor/bundle
+ key: gems-${{ runner.os }}-${{ env.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }}
+ restore-keys: |
+ gems-${{ runner.os }}-${{ env.ruby-version }}-
+ gems-${{ runner.os }}-
+ - run: bundle config set deployment 'true'
+ - name: bundle install
+ run: |
+ bundle config path vendor/bundle
+ bundle install --jobs 4 --retry 3
+ - run: bundle exec middleman build
+
+ - name: Deploy
+ uses: peaceiris/actions-gh-pages@v3.7.0-8
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ destination_dir: dev
+ publish_dir: ./build
+ keep_files: true
diff --git a/docs/.github/workflows/publish.yml b/docs/.github/workflows/publish.yml
new file mode 100644
index 0000000..6c51180
--- /dev/null
+++ b/docs/.github/workflows/publish.yml
@@ -0,0 +1,47 @@
+name: Publish Docker image
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ push_to_registry:
+ name: Push Docker image to Docker Hub
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v2
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v1
+ with:
+ platforms: all
+
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v3
+ with:
+ images: slatedocs/slate
+ tags: |
+ type=ref,event=tag
+
+ - name: Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v1
+
+ - name: Login to DockerHub
+ uses: docker/login-action@v1
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_ACCESS_KEY }}
+
+ - name: Push to Docker Hub
+ uses: docker/build-push-action@v2
+ with:
+ builder: ${{ steps.buildx.outputs.name }}
+ context: .
+ file: ./Dockerfile
+ platforms: linux/amd64,linux/arm64,linux/ppc64le
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..8e5b019
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,29 @@
+*.gem
+*.rbc
+.bundle
+.config
+coverage
+InstalledFiles
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+test/tmp
+test/version_tmp
+tmp
+*.DS_STORE
+build/
+.cache
+.vagrant
+.sass-cache
+.env
+build/
+
+# YARD artifacts
+.yardoc
+_yardoc
+doc/
+.idea/
+
+# Vagrant artifacts
+ubuntu-*-console.log
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
new file mode 100644
index 0000000..49b83dc
--- /dev/null
+++ b/docs/CHANGELOG.md
@@ -0,0 +1,335 @@
+# Changelog
+
+## Version 2.13.1
+
+*January 31, 2023*
+
+* Fix Vagrantfile gem install for ruby >= 2.6 (thanks @Cyb0rk)
+* Disable file watcher in run_build() for sake of qemu on arm64 (thanks @anapsix)
+* Expand deprecated git.io links to full url in docs (thanks @judge2020)
+* Add margin to paragraph following code-block on phones (thanks @tlhunter)
+* Bump nokogiri from 1.13.4 to 1.13.9
+* Bump rouge from 3.28.0 to 3.30.0
+* Bump redcarpet from 3.5.1 to 3.6.0
+* Bump middleman from 4.4.2 to 4.4.3
+* Bump middleman-syntax from 3.2.0 to 3.3.0
+* Bump webrick from 1.7.0 to 1.8.1
+
+## Version 2.13.0
+
+*April 22, 2022*
+
+* __Drop support for ruby 2.5__
+* Bump rouge from 3.26.1 to 3.28.0
+* Formally support ruby 3.1
+* Bump nokogiri from 1.12.5 to 1.13.4
+* Build docker images for multiple architectures (e.g. `aarch64`)
+* Remove `VOLUME` declaration from Dockerfile (thanks @aemengo)
+
+The security vulnerabilities reported against recent versions of nokogiri should not affect slate users with a regular setup.
+
+## Version 2.12.0
+
+*November 04, 2021*
+
+* Bump nokogiri from 1.12.3 to 1.12.5
+* Bump ffi from 1.15.0 to 1.15.4
+* Bump rouge from 3.26.0 to 3.26.1
+* Bump middleman from 4.4.0 to 4.4.2
+* Remove unnecessary files from docker images
+
+## Version 2.11.0
+
+*August 12, 2021*
+
+* __[Security]__ Bump addressable transitive dependency from 2.7.0 to 2.8.0
+* Support specifying custom meta tags in YAML front-matter
+* Bump nokogiri from 1.11.3 to 1.12.3 (minimum supported version is 1.11.4)
+* Bump middleman-autoprefixer from 2.10.1 to 3.0.0
+* Bump jquery from 3.5.1 to 3.6.0
+* Bump middleman from [`d180ca3`](https://github.com/middleman/middleman/commit/d180ca337202873f2601310c74ba2b6b4cf063ec) to 4.4.0
+
+## Version 2.10.0
+
+*April 13, 2021*
+
+* Add support for Ruby 3.0 (thanks @shaun-scale)
+* Add requirement for Git on installing dependencies
+* Bump nokogiri from 1.11.2 to 1.11.3
+* Bump middleman from 4.3.11 to [`d180ca3`](https://github.com/middleman/middleman/commit/d180ca337202873f2601310c74ba2b6b4cf063ec)
+
+## Version 2.9.2
+
+*March 30, 2021*
+
+* __[Security]__ Bump kramdown from 2.3.0 to 2.3.1
+* Bump nokogiri from 1.11.1 to 1.11.2
+
+## Version 2.9.1
+
+*February 27, 2021*
+
+* Fix Slate language tabs not working if localStorage is disabled
+
+## Version 2.9.0
+
+*January 19, 2021*
+
+* __Drop support for Ruby 2.3 and 2.4__
+* __[Security]__ Bump nokogiri from 1.10.10 to 1.11.1
+* __[Security]__ Bump redcarpet from 3.5.0 to 3.5.1
+* Specify slate is not supported on Ruby 3.x
+* Bump rouge from 3.24.0 to 3.26.0
+
+## Version 2.8.0
+
+*October 27, 2020*
+
+* Remove last trailing newline when using the copy code button
+* Rework docker image and make available at slatedocs/slate
+* Improve Dockerfile layout to improve caching (thanks @micvbang)
+* Bump rouge from 3.20 to 3.24
+* Bump nokogiri from 1.10.9 to 1.10.10
+* Bump middleman from 4.3.8 to 4.3.11
+* Bump lunr.js from 2.3.8 to 2.3.9
+
+## Version 2.7.1
+
+*August 13, 2020*
+
+* __[security]__ Bumped middleman from 4.3.7 to 4.3.8
+
+_Note_: Slate uses redcarpet, not kramdown, for rendering markdown to HTML, and so was unaffected by the security vulnerability in middleman.
+If you have changed slate to use kramdown, and with GFM, you may need to install the `kramdown-parser-gfm` gem.
+
+## Version 2.7.0
+
+*June 21, 2020*
+
+* __[security]__ Bumped rack in Gemfile.lock from 2.2.2 to 2.2.3
+* Bumped bundled jQuery from 3.2.1 to 3.5.1
+* Bumped bundled lunr from 0.5.7 to 2.3.8
+* Bumped imagesloaded from 3.1.8 to 4.1.4
+* Bumped rouge from 3.17.0 to 3.20.0
+* Bumped redcarpet from 3.4.0 to 3.5.0
+* Fix color of highlighted code being unreadable when printing page
+* Add clipboard icon for "Copy to Clipboard" functionality to code boxes (see note below)
+* Fix handling of ToC selectors that contain punctutation (thanks @gruis)
+* Fix language bar truncating languages that overflow screen width
+* Strip HTML tags from ToC title before displaying it in title bar in JS (backup to stripping done in Ruby code) (thanks @atic)
+
+To enable the new clipboard icon, you need to add `code_clipboard: true` to the frontmatter of source/index.html.md.
+See [this line](https://github.com/slatedocs/slate/blame/main/source/index.html.md#L19) for an example of usage.
+
+## Version 2.6.1
+
+*May 30, 2020*
+
+* __[security]__ update child dependency activesupport in Gemfile.lock to 5.4.2.3
+* Update Middleman in Gemfile.lock to 4.3.7
+* Replace Travis-CI with GitHub actions for continuous integration
+* Replace Spectrum with GitHub discussions
+
+## Version 2.6.0
+
+*May 18, 2020*
+
+__Note__: 2.5.0 was "pulled" due to a breaking bug discovered after release. It is recommended to skip it, and move straight to 2.6.0.
+
+* Fix large whitespace gap in middle column for sections with codeblocks
+* Fix highlighted code elements having a different background than rest of code block
+* Change JSON keys to have a different font color than their values
+* Disable asset hashing for woff and woff2 elements due to middleman bug breaking woff2 asset hashing in general
+* Move Dockerfile to Debian from Alpine
+* Converted repo to a [GitHub template](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-template-repository)
+* Update sassc to 2.3.0 in Gemfile.lock
+
+## Version 2.5.0
+
+*May 8, 2020*
+
+* __[security]__ update nokogiri to ~> 1.10.8
+* Update links in example docs to https://github.com/slatedocs/slate from https://github.com/lord/slate
+* Update LICENSE to include full Apache 2.0 text
+* Test slate against Ruby 2.5 and 2.6 on Travis-CI
+* Update Vagrantfile to use Ubuntu 18.04 (thanks @bradthurber)
+* Parse arguments and flags for deploy.sh on script start, instead of potentially after building source files
+* Install nodejs inside Vagrantfile (thanks @fernandoaguilar)
+* Add Dockerfile for running slate (thanks @redhatxl)
+* update middleman-syntax and rouge to ~>3.2
+* update middleman to 4.3.6
+
+## Version 2.4.0
+
+*October 19, 2019*
+
+- Move repository from lord/slate to slatedocs/slate
+- Fix documentation to point at new repo link, thanks to [Arun](https://github.com/slash-arun), [Gustavo Gawryszewski](https://github.com/gawry), and [Daniel Korbit](https://github.com/danielkorbit)
+- Update `nokogiri` to 1.10.4
+- Update `ffi` in `Gemfile.lock` to fix security warnings, thanks to [Grey Baker](https://github.com/greysteil) and [jakemack](https://github.com/jakemack)
+- Update `rack` to 2.0.7 in `Gemfile.lock` to fix security warnings, thanks to [Grey Baker](https://github.com/greysteil) and [jakemack](https://github.com/jakemack)
+- Update middleman to `4.3` and relax constraints on middleman related gems, thanks to [jakemack](https://github.com/jakemack)
+- Add sass gem, thanks to [jakemack](https://github.com/jakemack)
+- Activate `asset_cache` in middleman to improve cacheability of static files, thanks to [Sam Gilman](https://github.com/thenengah)
+- Update to using bundler 2 for `Gemfile.lock`, thanks to [jakemack](https://github.com/jakemack)
+
+## Version 2.3.1
+
+*July 5, 2018*
+
+- Update `sprockets` in `Gemfile.lock` to fix security warnings
+
+## Version 2.3
+
+*July 5, 2018*
+
+- Allows strikethrough in markdown by default.
+- Upgrades jQuery to 3.2.1, thanks to [Tomi Takussaari](https://github.com/TomiTakussaari)
+- Fixes invalid HTML in `layout.erb`, thanks to [Eric Scouten](https://github.com/scouten) for pointing out
+- Hopefully fixes Vagrant memory issues, thanks to [Petter Blomberg](https://github.com/p-blomberg) for the suggestion
+- Cleans HTML in headers before setting `document.title`, thanks to [Dan Levy](https://github.com/justsml)
+- Allows trailing whitespace in markdown files, thanks to [Samuel Cousin](https://github.com/kuzyn)
+- Fixes pushState/replaceState problems with scrolling not changing the document hash, thanks to [Andrey Fedorov](https://github.com/anfedorov)
+- Removes some outdated examples, thanks [@al-tr](https://github.com/al-tr), [Jerome Dahdah](https://github.com/jdahdah), and [Ricardo Castro](https://github.com/mccricardo)
+- Fixes `nav-padding` bug, thanks [Jerome Dahdah](https://github.com/jdahdah)
+- Code style fixes thanks to [Sebastian Zaremba](https://github.com/vassyz)
+- Nokogiri version bump thanks to [Grey Baker](https://github.com/greysteil)
+- Fix to default `index.md` text thanks to [Nick Busey](https://github.com/NickBusey)
+
+Thanks to everyone who contributed to this release!
+
+## Version 2.2
+
+*January 19, 2018*
+
+- Fixes bugs with some non-roman languages not generating unique headers
+- Adds editorconfig, thanks to [Jay Thomas](https://github.com/jaythomas)
+- Adds optional `NestingUniqueHeadCounter`, thanks to [Vladimir Morozov](https://github.com/greenhost87)
+- Small fixes to typos and language, thx [Emir RibiΔ](https://github.com/ribice), [Gregor Martynus](https://github.com/gr2m), and [Martius](https://github.com/martiuslim)!
+- Adds links to Spectrum chat for questions in README and ISSUE_TEMPLATE
+
+## Version 2.1
+
+*October 30, 2017*
+
+- Right-to-left text stylesheet option, thanks to [Mohammad Hossein Rabiee](https://github.com/mhrabiee)
+- Fix for HTML5 history state bug, thanks to [Zach Toolson](https://github.com/ztoolson)
+- Small styling changes, typo fixes, small bug fixes from [Marian Friedmann](https://github.com/rnarian), [Ben Wilhelm](https://github.com/benwilhelm), [Fouad Matin](https://github.com/fouad), [Nicolas Bonduel](https://github.com/NicolasBonduel), [Christian Oliff](https://github.com/coliff)
+
+Thanks to everyone who submitted PRs for this version!
+
+## Version 2.0
+
+*July 17, 2017*
+
+- All-new statically generated table of contents
+ - Should be much faster loading and scrolling for large pages
+ - Smaller Javascript file sizes
+ - Avoids the problem with the last link in the ToC not ever highlighting if the section was shorter than the page
+ - Fixes control-click not opening in a new page
+ - Automatically updates the HTML title as you scroll
+- Updated design
+ - New default colors!
+ - New spacings and sizes!
+ - System-default typefaces, just like GitHub
+- Added search input delay on large corpuses to reduce lag
+- We even bumped the major version cause hey, why not?
+- Various small bug fixes
+
+Thanks to everyone who helped debug or wrote code for this version! It was a serious community effort, and I couldn't have done it alone.
+
+## Version 1.5
+
+*February 23, 2017*
+
+- Add [multiple tabs per programming language](https://github.com/lord/slate/wiki/Multiple-language-tabs-per-programming-language) feature
+- Upgrade Middleman to add Ruby 1.4.0 compatibility
+- Switch default code highlighting color scheme to better highlight JSON
+- Various small typo and bug fixes
+
+## Version 1.4
+
+*November 24, 2016*
+
+- Upgrade Middleman and Rouge gems, should hopefully solve a number of bugs
+- Update some links in README
+- Fix broken Vagrant startup script
+- Fix some problems with deploy.sh help message
+- Fix bug with language tabs not hiding properly if no error
+- Add `!default` to SASS variables
+- Fix bug with logo margin
+- Bump tested Ruby versions in .travis.yml
+
+## Version 1.3.3
+
+*June 11, 2016*
+
+Documentation and example changes.
+
+## Version 1.3.2
+
+*February 3, 2016*
+
+A small bugfix for slightly incorrect background colors on code samples in some cases.
+
+## Version 1.3.1
+
+*January 31, 2016*
+
+A small bugfix for incorrect whitespace in code blocks.
+
+## Version 1.3
+
+*January 27, 2016*
+
+We've upgraded Middleman and a number of other dependencies, which should fix quite a few bugs.
+
+Instead of `rake build` and `rake deploy`, you should now run `bundle exec middleman build --clean` to build your server, and `./deploy.sh` to deploy it to Github Pages.
+
+## Version 1.2
+
+*June 20, 2015*
+
+**Fixes:**
+
+- Remove crash on invalid languages
+- Update Tocify to scroll to the highlighted header in the Table of Contents
+- Fix variable leak and update search algorithms
+- Update Python examples to be valid Python
+- Update gems
+- More misc. bugfixes of Javascript errors
+- Add Dockerfile
+- Remove unused gems
+- Optimize images, fonts, and generated asset files
+- Add chinese font support
+- Remove RedCarpet header ID patch
+- Update language tabs to not disturb existing query strings
+
+## Version 1.1
+
+*July 27, 2014*
+
+**Fixes:**
+
+- Finally, a fix for the redcarpet upgrade bug
+
+## Version 1.0
+
+*July 2, 2014*
+
+[View Issues](https://github.com/tripit/slate/issues?milestone=1&state=closed)
+
+**Features:**
+
+- Responsive designs for phones and tablets
+- Started tagging versions
+
+**Fixes:**
+
+- Fixed 'unrecognized expression' error
+- Fixed #undefined hash bug
+- Fixed bug where the current language tab would be unselected
+- Fixed bug where tocify wouldn't highlight the current section while searching
+- Fixed bug where ids of header tags would have special characters that caused problems
+- Updated layout so that pages with disabled search wouldn't load search.js
+- Cleaned up Javascript
diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..cc17fd9
--- /dev/null
+++ b/docs/CODE_OF_CONDUCT.md
@@ -0,0 +1,46 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@lord.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/docs/Dockerfile b/docs/Dockerfile
new file mode 100644
index 0000000..077ef3b
--- /dev/null
+++ b/docs/Dockerfile
@@ -0,0 +1,26 @@
+FROM ruby:2.6-slim
+
+WORKDIR /srv/slate
+
+EXPOSE 4567
+
+COPY Gemfile .
+COPY Gemfile.lock .
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ build-essential \
+ git \
+ nodejs \
+ && gem install bundler \
+ && bundle install \
+ && apt-get remove -y build-essential git \
+ && apt-get autoremove -y \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY . /srv/slate
+
+RUN chmod +x /srv/slate/slate.sh
+
+ENTRYPOINT ["/srv/slate/slate.sh"]
+CMD ["build"]
diff --git a/docs/Gemfile b/docs/Gemfile
new file mode 100644
index 0000000..f2e8441
--- /dev/null
+++ b/docs/Gemfile
@@ -0,0 +1,13 @@
+ruby '>= 2.6'
+source 'https://rubygems.org'
+
+# Middleman
+gem 'middleman', '~> 4.4'
+gem 'middleman-syntax', '~> 3.2'
+gem 'middleman-autoprefixer', '~> 3.0'
+gem 'middleman-sprockets', '~> 4.1'
+gem 'rouge', '~> 3.21'
+gem 'redcarpet', '~> 3.6.0'
+gem 'nokogiri', '~> 1.13.3'
+gem 'sass'
+gem 'webrick'
diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock
new file mode 100644
index 0000000..79bfbcb
--- /dev/null
+++ b/docs/Gemfile.lock
@@ -0,0 +1,145 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ activesupport (6.1.6.1)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (>= 1.6, < 2)
+ minitest (>= 5.1)
+ tzinfo (~> 2.0)
+ zeitwerk (~> 2.3)
+ addressable (2.8.1)
+ public_suffix (>= 2.0.2, < 6.0)
+ autoprefixer-rails (10.2.5.0)
+ execjs (< 2.8.0)
+ backports (3.23.0)
+ coffee-script (2.4.1)
+ coffee-script-source
+ execjs
+ coffee-script-source (1.12.2)
+ concurrent-ruby (1.2.0)
+ contracts (0.16.1)
+ dotenv (2.8.1)
+ erubis (2.7.0)
+ execjs (2.7.0)
+ fast_blank (1.0.1)
+ fastimage (2.2.6)
+ ffi (1.15.5)
+ haml (5.2.2)
+ temple (>= 0.8.0)
+ tilt
+ hamster (3.0.0)
+ concurrent-ruby (~> 1.0)
+ hashie (3.6.0)
+ i18n (1.6.0)
+ concurrent-ruby (~> 1.0)
+ kramdown (2.4.0)
+ rexml
+ listen (3.8.0)
+ rb-fsevent (~> 0.10, >= 0.10.3)
+ rb-inotify (~> 0.9, >= 0.9.10)
+ memoist (0.16.2)
+ middleman (4.4.3)
+ coffee-script (~> 2.2)
+ haml (>= 4.0.5, < 6.0)
+ kramdown (>= 2.3.0)
+ middleman-cli (= 4.4.3)
+ middleman-core (= 4.4.3)
+ middleman-autoprefixer (3.0.0)
+ autoprefixer-rails (~> 10.0)
+ middleman-core (>= 4.0.0)
+ middleman-cli (4.4.3)
+ thor (>= 0.17.0, < 2.0)
+ middleman-core (4.4.3)
+ activesupport (>= 6.1, < 7.1)
+ addressable (~> 2.4)
+ backports (~> 3.6)
+ bundler (~> 2.0)
+ contracts (~> 0.13)
+ dotenv
+ erubis
+ execjs (~> 2.0)
+ fast_blank
+ fastimage (~> 2.0)
+ hamster (~> 3.0)
+ hashie (~> 3.4)
+ i18n (~> 1.6.0)
+ listen (~> 3.0)
+ memoist (~> 0.14)
+ padrino-helpers (~> 0.15.0)
+ parallel
+ rack (>= 1.4.5, < 3)
+ sassc (~> 2.0)
+ servolux
+ tilt (~> 2.0.9)
+ toml
+ uglifier (~> 3.0)
+ webrick
+ middleman-sprockets (4.1.1)
+ middleman-core (~> 4.0)
+ sprockets (>= 3.0)
+ middleman-syntax (3.3.0)
+ middleman-core (>= 3.2)
+ rouge (~> 3.2)
+ mini_portile2 (2.8.0)
+ minitest (5.17.0)
+ nokogiri (1.13.9)
+ mini_portile2 (~> 2.8.0)
+ racc (~> 1.4)
+ padrino-helpers (0.15.2)
+ i18n (>= 0.6.7, < 2)
+ padrino-support (= 0.15.2)
+ tilt (>= 1.4.1, < 3)
+ padrino-support (0.15.2)
+ parallel (1.22.1)
+ parslet (2.0.0)
+ public_suffix (5.0.1)
+ racc (1.6.0)
+ rack (2.2.6.2)
+ rb-fsevent (0.11.2)
+ rb-inotify (0.10.1)
+ ffi (~> 1.0)
+ redcarpet (3.6.0)
+ rexml (3.2.5)
+ rouge (3.30.0)
+ sass (3.7.4)
+ sass-listen (~> 4.0.0)
+ sass-listen (4.0.0)
+ rb-fsevent (~> 0.9, >= 0.9.4)
+ rb-inotify (~> 0.9, >= 0.9.7)
+ sassc (2.4.0)
+ ffi (~> 1.9)
+ servolux (0.13.0)
+ sprockets (3.7.2)
+ concurrent-ruby (~> 1.0)
+ rack (> 1, < 3)
+ temple (0.10.0)
+ thor (1.2.1)
+ tilt (2.0.11)
+ toml (0.3.0)
+ parslet (>= 1.8.0, < 3.0.0)
+ tzinfo (2.0.6)
+ concurrent-ruby (~> 1.0)
+ uglifier (3.2.0)
+ execjs (>= 0.3.0, < 3)
+ webrick (1.8.1)
+ zeitwerk (2.6.0)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ middleman (~> 4.4)
+ middleman-autoprefixer (~> 3.0)
+ middleman-sprockets (~> 4.1)
+ middleman-syntax (~> 3.2)
+ nokogiri (~> 1.13.3)
+ redcarpet (~> 3.6.0)
+ rouge (~> 3.21)
+ sass
+ webrick
+
+RUBY VERSION
+ ruby 2.7.2p137
+
+BUNDLED WITH
+ 2.2.22
diff --git a/docs/LICENSE b/docs/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/docs/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..1560f21
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
Slate helps you create beautiful, intelligent, responsive API documentation.
+
+Features
+------------
+
+* **Clean, intuitive design** β With Slate, the description of your API is on the left side of your documentation, and all the code examples are on the right side. Inspired by [Stripe's](https://stripe.com/docs/api) and [PayPal's](https://developer.paypal.com/webapps/developer/docs/api/) API docs. Slate is responsive, so it looks great on tablets, phones, and even in print.
+
+* **Everything on a single page** β Gone are the days when your users had to search through a million pages to find what they wanted. Slate puts the entire documentation on a single page. We haven't sacrificed linkability, though. As you scroll, your browser's hash will update to the nearest header, so linking to a particular point in the documentation is still natural and easy.
+
+* **Slate is just Markdown** β When you write docs with Slate, you're just writing Markdown, which makes it simple to edit and understand. Everything is written in Markdown β even the code samples are just Markdown code blocks.
+
+* **Write code samples in multiple languages** β If your API has bindings in multiple programming languages, you can easily put in tabs to switch between them. In your document, you'll distinguish different languages by specifying the language name at the top of each code block, just like with GitHub Flavored Markdown.
+
+* **Out-of-the-box syntax highlighting** for [over 100 languages](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers), no configuration required.
+
+* **Automatic, smoothly scrolling table of contents** on the far left of the page. As you scroll, it displays your current position in the document. It's fast, too. We're using Slate at TripIt to build documentation for our new API, where our table of contents has over 180 entries. We've made sure that the performance remains excellent, even for larger documents.
+
+* **Let your users update your documentation for you** β By default, your Slate-generated documentation is hosted in a public GitHub repository. Not only does this mean you get free hosting for your docs with GitHub Pages, but it also makes it simple for other developers to make pull requests to your docs if they find typos or other problems. Of course, if you don't want to use GitHub, you're also welcome to host your docs elsewhere.
+
+* **RTL Support** Full right-to-left layout for RTL languages such as Arabic, Persian (Farsi), Hebrew etc.
+
+Getting started with Slate is super easy! Simply press the green "use this template" button above and follow the instructions below. Or, if you'd like to check out what Slate is capable of, take a look at the [sample docs](https://slatedocs.github.io/slate/).
+
+Getting Started with Slate
+------------------------------
+
+To get started with Slate, please check out the [Getting Started](https://github.com/slatedocs/slate/wiki#getting-started)
+section in our [wiki](https://github.com/slatedocs/slate/wiki).
+
+We support running Slate in three different ways:
+* [Natively](https://github.com/slatedocs/slate/wiki/Using-Slate-Natively)
+* [Using Vagrant](https://github.com/slatedocs/slate/wiki/Using-Slate-in-Vagrant)
+* [Using Docker](https://github.com/slatedocs/slate/wiki/Using-Slate-in-Docker)
+
+Companies Using Slate
+---------------------------------
+
+* [NASA](https://api.nasa.gov)
+* [Sony](http://developers.cimediacloud.com)
+* [Best Buy](https://bestbuyapis.github.io/api-documentation/)
+* [Travis-CI](https://docs.travis-ci.com/api/)
+* [Greenhouse](https://developers.greenhouse.io/harvest.html)
+* [WooCommerce](http://woocommerce.github.io/woocommerce-rest-api-docs/)
+* [Dwolla](https://docs.dwolla.com/)
+* [Clearbit](https://clearbit.com/docs)
+* [Coinbase](https://developers.coinbase.com/api)
+* [Parrot Drones](http://developer.parrot.com/docs/bebop/)
+* [CoinAPI](https://docs.coinapi.io/)
+
+You can view more in [the list on the wiki](https://github.com/slatedocs/slate/wiki/Slate-in-the-Wild).
+
+Questions? Need Help? Found a bug?
+--------------------
+
+If you've got questions about setup, deploying, special feature implementation in your fork, or just want to chat with the developer, please feel free to [start a thread in our Discussions tab](https://github.com/slatedocs/slate/discussions)!
+
+Found a bug with upstream Slate? Go ahead and [submit an issue](https://github.com/slatedocs/slate/issues). And, of course, feel free to submit pull requests with bug fixes or changes to the `dev` branch.
+
+Contributors
+--------------------
+
+Slate was built by [Robert Lord](https://lord.io) while at [TripIt](https://www.tripit.com/). The project is now maintained by [Matthew Peveler](https://github.com/MasterOdin) and [Mike Ralphson](https://github.com/MikeRalphson).
+
+Thanks to the following people who have submitted major pull requests:
+
+- [@chrissrogers](https://github.com/chrissrogers)
+- [@bootstraponline](https://github.com/bootstraponline)
+- [@realityking](https://github.com/realityking)
+- [@cvkef](https://github.com/cvkef)
+
+Also, thanks to [Sauce Labs](http://saucelabs.com) for sponsoring the development of the responsive styles.
diff --git a/docs/Vagrantfile b/docs/Vagrantfile
new file mode 100644
index 0000000..8a9981b
--- /dev/null
+++ b/docs/Vagrantfile
@@ -0,0 +1,47 @@
+Vagrant.configure(2) do |config|
+ config.vm.box = "ubuntu/focal64"
+ config.vm.network :forwarded_port, guest: 4567, host: 4567
+ config.vm.provider "virtualbox" do |vb|
+ vb.memory = "2048"
+ end
+
+ config.vm.provision "bootstrap",
+ type: "shell",
+ inline: <<-SHELL
+ # add nodejs v12 repository
+ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
+
+ sudo apt-get update
+ sudo apt-get install -yq ruby ruby-dev
+ sudo apt-get install -yq pkg-config build-essential nodejs git libxml2-dev libxslt-dev
+ sudo apt-get autoremove -yq
+ gem install --no-document bundler
+ SHELL
+
+ # add the local user git config to the vm
+ config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig"
+
+ config.vm.provision "install",
+ type: "shell",
+ privileged: false,
+ inline: <<-SHELL
+ echo "=============================================="
+ echo "Installing app dependencies"
+ cd /vagrant
+ sudo gem install bundler -v "$(grep -A 1 "BUNDLED WITH" Gemfile.lock | tail -n 1)"
+ bundle config build.nokogiri --use-system-libraries
+ bundle install
+ SHELL
+
+ config.vm.provision "run",
+ type: "shell",
+ privileged: false,
+ run: "always",
+ inline: <<-SHELL
+ echo "=============================================="
+ echo "Starting up middleman at http://localhost:4567"
+ echo "If it does not come up, check the ~/middleman.log file for any error messages"
+ cd /vagrant
+ bundle exec middleman server --watcher-force-polling --watcher-latency=1 &> ~/middleman.log &
+ SHELL
+end
diff --git a/docs/config.rb b/docs/config.rb
new file mode 100644
index 0000000..6f8b677
--- /dev/null
+++ b/docs/config.rb
@@ -0,0 +1,63 @@
+# Unique header generation
+require './lib/unique_head.rb'
+
+# Markdown
+set :markdown_engine, :redcarpet
+set :markdown,
+ fenced_code_blocks: true,
+ smartypants: true,
+ disable_indented_code_blocks: true,
+ prettify: true,
+ strikethrough: true,
+ tables: true,
+ with_toc_data: true,
+ no_intra_emphasis: true,
+ renderer: UniqueHeadCounter
+
+# Assets
+set :css_dir, 'stylesheets'
+set :js_dir, 'javascripts'
+set :images_dir, 'images'
+set :fonts_dir, 'fonts'
+
+# Activate the syntax highlighter
+activate :syntax
+ready do
+ require './lib/monokai_sublime_slate.rb'
+ require './lib/multilang.rb'
+end
+
+activate :sprockets
+
+activate :autoprefixer do |config|
+ config.browsers = ['last 2 version', 'Firefox ESR']
+ config.cascade = false
+ config.inline = true
+end
+
+# Github pages require relative links
+activate :relative_assets
+set :relative_links, true
+
+# Build Configuration
+configure :build do
+ # We do want to hash woff and woff2 as there's a bug where woff2 will use
+ # woff asset hash which breaks things. Trying to use a combination of ignore and
+ # rewrite_ignore does not work as it conflicts weirdly with relative_assets. Disabling
+ # the .woff2 extension only does not work as .woff will still activate it so have to
+ # have both. See https://github.com/slatedocs/slate/issues/1171 for more details.
+ activate :asset_hash, :exts => app.config[:asset_extensions] - %w[.woff .woff2]
+ # If you're having trouble with Middleman hanging, commenting
+ # out the following two lines has been known to help
+ activate :minify_css
+ activate :minify_javascript
+ # activate :gzip
+end
+
+# Deploy Configuration
+# If you want Middleman to listen on a different port, you can set that below
+set :port, 4567
+
+helpers do
+ require './lib/toc_data.rb'
+end
diff --git a/docs/deploy.sh b/docs/deploy.sh
new file mode 100755
index 0000000..15133d2
--- /dev/null
+++ b/docs/deploy.sh
@@ -0,0 +1,226 @@
+#!/usr/bin/env bash
+set -o errexit #abort if any command fails
+me=$(basename "$0")
+
+help_message="\
+Usage: $me [-c FILE] []
+Deploy generated files to a git branch.
+
+Options:
+
+ -h, --help Show this help information.
+ -v, --verbose Increase verbosity. Useful for debugging.
+ -e, --allow-empty Allow deployment of an empty directory.
+ -m, --message MESSAGE Specify the message used when committing on the
+ deploy branch.
+ -n, --no-hash Don't append the source commit's hash to the deploy
+ commit's message.
+ --source-only Only build but not push
+ --push-only Only push but not build
+"
+
+
+run_build() {
+ bundle exec middleman build --clean
+}
+
+parse_args() {
+ # Set args from a local environment file.
+ if [ -e ".env" ]; then
+ source .env
+ fi
+
+ # Parse arg flags
+ # If something is exposed as an environment variable, set/overwrite it
+ # here. Otherwise, set/overwrite the internal variable instead.
+ while : ; do
+ if [[ $1 = "-h" || $1 = "--help" ]]; then
+ echo "$help_message"
+ exit 0
+ elif [[ $1 = "-v" || $1 = "--verbose" ]]; then
+ verbose=true
+ shift
+ elif [[ $1 = "-e" || $1 = "--allow-empty" ]]; then
+ allow_empty=true
+ shift
+ elif [[ ( $1 = "-m" || $1 = "--message" ) && -n $2 ]]; then
+ commit_message=$2
+ shift 2
+ elif [[ $1 = "-n" || $1 = "--no-hash" ]]; then
+ GIT_DEPLOY_APPEND_HASH=false
+ shift
+ elif [[ $1 = "--source-only" ]]; then
+ source_only=true
+ shift
+ elif [[ $1 = "--push-only" ]]; then
+ push_only=true
+ shift
+ else
+ break
+ fi
+ done
+
+ if [ ${source_only} ] && [ ${push_only} ]; then
+ >&2 echo "You can only specify one of --source-only or --push-only"
+ exit 1
+ fi
+
+ # Set internal option vars from the environment and arg flags. All internal
+ # vars should be declared here, with sane defaults if applicable.
+
+ # Source directory & target branch.
+ deploy_directory=build
+ deploy_branch=gh-pages
+
+ #if no user identity is already set in the current git environment, use this:
+ default_username=${GIT_DEPLOY_USERNAME:-deploy.sh}
+ default_email=${GIT_DEPLOY_EMAIL:-}
+
+ #repository to deploy to. must be readable and writable.
+ repo=https://github.com/tarsil/django-messages-drf
+
+ #append commit hash to the end of message by default
+ append_hash=${GIT_DEPLOY_APPEND_HASH:-true}
+}
+
+main() {
+ enable_expanded_output
+
+ if ! git diff --exit-code --quiet --cached; then
+ echo Aborting due to uncommitted changes in the index >&2
+ return 1
+ fi
+
+ commit_title=`git log -n 1 --format="%s" HEAD`
+ commit_hash=` git log -n 1 --format="%H" HEAD`
+
+ #default commit message uses last title if a custom one is not supplied
+ if [[ -z $commit_message ]]; then
+ commit_message="publish: $commit_title"
+ fi
+
+ #append hash to commit message unless no hash flag was found
+ if [ $append_hash = true ]; then
+ commit_message="$commit_message"$'\n\n'"generated from commit $commit_hash"
+ fi
+
+ previous_branch=`git rev-parse --abbrev-ref HEAD`
+
+ if [ ! -d "$deploy_directory" ]; then
+ echo "Deploy directory '$deploy_directory' does not exist. Aborting." >&2
+ return 1
+ fi
+
+ # must use short form of flag in ls for compatibility with macOS and BSD
+ if [[ -z `ls -A "$deploy_directory" 2> /dev/null` && -z $allow_empty ]]; then
+ echo "Deploy directory '$deploy_directory' is empty. Aborting. If you're sure you want to deploy an empty tree, use the --allow-empty / -e flag." >&2
+ return 1
+ fi
+
+ if git ls-remote --exit-code $repo "refs/heads/$deploy_branch" ; then
+ # deploy_branch exists in $repo; make sure we have the latest version
+
+ disable_expanded_output
+ git fetch --force $repo $deploy_branch:$deploy_branch
+ enable_expanded_output
+ fi
+
+ # check if deploy_branch exists locally
+ if git show-ref --verify --quiet "refs/heads/$deploy_branch"
+ then incremental_deploy
+ else initial_deploy
+ fi
+
+ restore_head
+}
+
+initial_deploy() {
+ git --work-tree "$deploy_directory" checkout --orphan $deploy_branch
+ git --work-tree "$deploy_directory" add --all
+ commit+push
+}
+
+incremental_deploy() {
+ #make deploy_branch the current branch
+ git symbolic-ref HEAD refs/heads/$deploy_branch
+ #put the previously committed contents of deploy_branch into the index
+ git --work-tree "$deploy_directory" reset --mixed --quiet
+ git --work-tree "$deploy_directory" add --all
+
+ set +o errexit
+ diff=$(git --work-tree "$deploy_directory" diff --exit-code --quiet HEAD --)$?
+ set -o errexit
+ case $diff in
+ 0) echo No changes to files in $deploy_directory. Skipping commit.;;
+ 1) commit+push;;
+ *)
+ echo git diff exited with code $diff. Aborting. Staying on branch $deploy_branch so you can debug. To switch back to main, use: git symbolic-ref HEAD refs/heads/main && git reset --mixed >&2
+ return $diff
+ ;;
+ esac
+}
+
+commit+push() {
+ set_user_id
+ git --work-tree "$deploy_directory" commit -m "$commit_message"
+
+ disable_expanded_output
+ #--quiet is important here to avoid outputting the repo URL, which may contain a secret token
+ git push --quiet $repo $deploy_branch
+ enable_expanded_output
+}
+
+#echo expanded commands as they are executed (for debugging)
+enable_expanded_output() {
+ if [ $verbose ]; then
+ set -o xtrace
+ set +o verbose
+ fi
+}
+
+#this is used to avoid outputting the repo URL, which may contain a secret token
+disable_expanded_output() {
+ if [ $verbose ]; then
+ set +o xtrace
+ set -o verbose
+ fi
+}
+
+set_user_id() {
+ if [[ -z `git config user.name` ]]; then
+ git config user.name "$default_username"
+ fi
+ if [[ -z `git config user.email` ]]; then
+ git config user.email "$default_email"
+ fi
+}
+
+restore_head() {
+ if [[ $previous_branch = "HEAD" ]]; then
+ #we weren't on any branch before, so just set HEAD back to the commit it was on
+ git update-ref --no-deref HEAD $commit_hash $deploy_branch
+ else
+ git symbolic-ref HEAD refs/heads/$previous_branch
+ fi
+
+ git reset --mixed
+}
+
+filter() {
+ sed -e "s|$repo|\$repo|g"
+}
+
+sanitize() {
+ "$@" 2> >(filter 1>&2) | filter
+}
+
+parse_args "$@"
+
+if [[ ${source_only} ]]; then
+ run_build
+elif [[ ${push_only} ]]; then
+ main "$@"
+else
+ run_build
+ main "$@"
+fi
diff --git a/docs/font-selection.json b/docs/font-selection.json
new file mode 100755
index 0000000..5e78f5d
--- /dev/null
+++ b/docs/font-selection.json
@@ -0,0 +1,148 @@
+{
+ "IcoMoonType": "selection",
+ "icons": [
+ {
+ "icon": {
+ "paths": [
+ "M438.857 73.143q119.429 0 220.286 58.857t159.714 159.714 58.857 220.286-58.857 220.286-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857zM512 785.714v-108.571q0-8-5.143-13.429t-12.571-5.429h-109.714q-7.429 0-13.143 5.714t-5.714 13.143v108.571q0 7.429 5.714 13.143t13.143 5.714h109.714q7.429 0 12.571-5.429t5.143-13.429zM510.857 589.143l10.286-354.857q0-6.857-5.714-10.286-5.714-4.571-13.714-4.571h-125.714q-8 0-13.714 4.571-5.714 3.429-5.714 10.286l9.714 354.857q0 5.714 5.714 10t13.714 4.286h105.714q8 0 13.429-4.286t6-10z"
+ ],
+ "attrs": [],
+ "isMulticolor": false,
+ "tags": [
+ "exclamation-circle"
+ ],
+ "defaultCode": 61546,
+ "grid": 14
+ },
+ "attrs": [],
+ "properties": {
+ "id": 100,
+ "order": 4,
+ "prevSize": 28,
+ "code": 58880,
+ "name": "exclamation-sign",
+ "ligatures": ""
+ },
+ "setIdx": 0,
+ "iconIdx": 0
+ },
+ {
+ "icon": {
+ "paths": [
+ "M585.143 786.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-54.857v-292.571q0-8-5.143-13.143t-13.143-5.143h-182.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h54.857v182.857h-54.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h256q8 0 13.143-5.143t5.143-13.143zM512 274.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-109.714q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h109.714q8 0 13.143-5.143t5.143-13.143zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z"
+ ],
+ "attrs": [],
+ "isMulticolor": false,
+ "tags": [
+ "info-circle"
+ ],
+ "defaultCode": 61530,
+ "grid": 14
+ },
+ "attrs": [],
+ "properties": {
+ "id": 85,
+ "order": 3,
+ "name": "info-sign",
+ "prevSize": 28,
+ "code": 58882
+ },
+ "setIdx": 0,
+ "iconIdx": 2
+ },
+ {
+ "icon": {
+ "paths": [
+ "M733.714 419.429q0-16-10.286-26.286l-52-51.429q-10.857-10.857-25.714-10.857t-25.714 10.857l-233.143 232.571-129.143-129.143q-10.857-10.857-25.714-10.857t-25.714 10.857l-52 51.429q-10.286 10.286-10.286 26.286 0 15.429 10.286 25.714l206.857 206.857q10.857 10.857 25.714 10.857 15.429 0 26.286-10.857l310.286-310.286q10.286-10.286 10.286-25.714zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z"
+ ],
+ "attrs": [],
+ "isMulticolor": false,
+ "tags": [
+ "check-circle"
+ ],
+ "defaultCode": 61528,
+ "grid": 14
+ },
+ "attrs": [],
+ "properties": {
+ "id": 83,
+ "order": 9,
+ "prevSize": 28,
+ "code": 58886,
+ "name": "ok-sign"
+ },
+ "setIdx": 0,
+ "iconIdx": 6
+ },
+ {
+ "icon": {
+ "paths": [
+ "M658.286 475.429q0-105.714-75.143-180.857t-180.857-75.143-180.857 75.143-75.143 180.857 75.143 180.857 180.857 75.143 180.857-75.143 75.143-180.857zM950.857 950.857q0 29.714-21.714 51.429t-51.429 21.714q-30.857 0-51.429-21.714l-196-195.429q-102.286 70.857-228 70.857-81.714 0-156.286-31.714t-128.571-85.714-85.714-128.571-31.714-156.286 31.714-156.286 85.714-128.571 128.571-85.714 156.286-31.714 156.286 31.714 128.571 85.714 85.714 128.571 31.714 156.286q0 125.714-70.857 228l196 196q21.143 21.143 21.143 51.429z"
+ ],
+ "width": 951,
+ "attrs": [],
+ "isMulticolor": false,
+ "tags": [
+ "search"
+ ],
+ "defaultCode": 61442,
+ "grid": 14
+ },
+ "attrs": [],
+ "properties": {
+ "id": 2,
+ "order": 1,
+ "prevSize": 28,
+ "code": 58887,
+ "name": "icon-search"
+ },
+ "setIdx": 0,
+ "iconIdx": 7
+ }
+ ],
+ "height": 1024,
+ "metadata": {
+ "name": "slate",
+ "license": "SIL OFL 1.1"
+ },
+ "preferences": {
+ "showGlyphs": true,
+ "showQuickUse": true,
+ "showQuickUse2": true,
+ "showSVGs": true,
+ "fontPref": {
+ "prefix": "icon-",
+ "metadata": {
+ "fontFamily": "slate",
+ "majorVersion": 1,
+ "minorVersion": 0,
+ "description": "Based on FontAwesome",
+ "license": "SIL OFL 1.1"
+ },
+ "metrics": {
+ "emSize": 1024,
+ "baseline": 6.25,
+ "whitespace": 50
+ },
+ "resetPoint": 58880,
+ "showSelector": false,
+ "selector": "class",
+ "classSelector": ".icon",
+ "showMetrics": false,
+ "showMetadata": true,
+ "showVersion": true,
+ "ie7": false
+ },
+ "imagePref": {
+ "prefix": "icon-",
+ "png": true,
+ "useClassSelector": true,
+ "color": 4473924,
+ "bgColor": 16777215
+ },
+ "historySize": 100,
+ "showCodes": true,
+ "gridSize": 16,
+ "showLiga": false
+ }
+}
diff --git a/docs/index.md b/docs/index.md
deleted file mode 100644
index 2b6f7e4..0000000
--- a/docs/index.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Introduction
-
-[](https://circleci.com/gh/tarsil/django-messages-drf)
-[](https://codecov.io/gh/tarsil/django-messages-drf)
-
-Django Messages DRF is an alternative and based on pinax-messages but using
-Django Rest Framework by making it easier to integrate with your existing project.
-
-A special thanks to pinax for inspiring me to do this and use some ideas.
-
-## Overview
-
-`django-messages-drf` is an app for providing private user-to-user threaded
-messaging.
-
-## Requirements
-
-Python 3.7+
-
-## Supported Django and Python Versions
-
-| Django / Python | 3.6 | 3.7 | 3.8 | 3.9 | 3.10 |
-| --------------- | --- | --- | --- | --- | ---- |
-| 2.2 | Yes | Yes | Yes | Yes | Yes |
-| 3.0 | Yes | Yes | Yes | Yes | Yes |
-| 3.1 | Yes | Yes | Yes | Yes | Yes |
-| 3.2 | Yes | Yes | Yes | Yes | Yes |
-| 4.0 | Yes | Yes | Yes | Yes | Yes |
diff --git a/docs/installation.md b/docs/installation.md
deleted file mode 100644
index e388630..0000000
--- a/docs/installation.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# Installation
-
-```shell
-$ pip install django-messages-drf
-```
-
-Add `django_messages_drf` to your `INSTALLED_APPS`:
-
-```python
-INSTALLED_APPS = [
- ...
- "django_messages_drf",
- ...
-]
-```
-
-Run Django migrations to create `django-messages-drf` database tables:
-
-```shell
-$ python manage.py migrate
-```
-
-You'll also want to add ``django_messages_drf.urls` into your main urlpatterns.
-
-```python
-urlpatterns = [
- # other urls
- path("messages-drf/", include("django_messages_drf.urls", namespace="django_messages_drf")),
-]
-```
diff --git a/docs/lib/monokai_sublime_slate.rb b/docs/lib/monokai_sublime_slate.rb
new file mode 100644
index 0000000..cd2de33
--- /dev/null
+++ b/docs/lib/monokai_sublime_slate.rb
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*- #
+# frozen_string_literal: true
+
+# this is based on https://github.com/rouge-ruby/rouge/blob/master/lib/rouge/themes/monokai_sublime.rb
+# but without the added background, and changed styling for JSON keys to be soft_yellow instead of white
+
+module Rouge
+ module Themes
+ class MonokaiSublimeSlate < CSSTheme
+ name 'monokai.sublime.slate'
+
+ palette :black => '#000000'
+ palette :bright_green => '#a6e22e'
+ palette :bright_pink => '#f92672'
+ palette :carmine => '#960050'
+ palette :dark => '#49483e'
+ palette :dark_grey => '#888888'
+ palette :dark_red => '#aa0000'
+ palette :dimgrey => '#75715e'
+ palette :emperor => '#555555'
+ palette :grey => '#999999'
+ palette :light_grey => '#aaaaaa'
+ palette :light_violet => '#ae81ff'
+ palette :soft_cyan => '#66d9ef'
+ palette :soft_yellow => '#e6db74'
+ palette :very_dark => '#1e0010'
+ palette :whitish => '#f8f8f2'
+ palette :orange => '#f6aa11'
+ palette :white => '#ffffff'
+
+ style Generic::Heading, :fg => :grey
+ style Literal::String::Regex, :fg => :orange
+ style Generic::Output, :fg => :dark_grey
+ style Generic::Prompt, :fg => :emperor
+ style Generic::Strong, :bold => false
+ style Generic::Subheading, :fg => :light_grey
+ style Name::Builtin, :fg => :orange
+ style Comment::Multiline,
+ Comment::Preproc,
+ Comment::Single,
+ Comment::Special,
+ Comment, :fg => :dimgrey
+ style Error,
+ Generic::Error,
+ Generic::Traceback, :fg => :carmine
+ style Generic::Deleted,
+ Generic::Inserted,
+ Generic::Emph, :fg => :dark
+ style Keyword::Constant,
+ Keyword::Declaration,
+ Keyword::Reserved,
+ Name::Constant,
+ Keyword::Type, :fg => :soft_cyan
+ style Literal::Number::Float,
+ Literal::Number::Hex,
+ Literal::Number::Integer::Long,
+ Literal::Number::Integer,
+ Literal::Number::Oct,
+ Literal::Number,
+ Literal::String::Char,
+ Literal::String::Escape,
+ Literal::String::Symbol, :fg => :light_violet
+ style Literal::String::Doc,
+ Literal::String::Double,
+ Literal::String::Backtick,
+ Literal::String::Heredoc,
+ Literal::String::Interpol,
+ Literal::String::Other,
+ Literal::String::Single,
+ Literal::String, :fg => :soft_yellow
+ style Name::Attribute,
+ Name::Class,
+ Name::Decorator,
+ Name::Exception,
+ Name::Function, :fg => :bright_green
+ style Name::Variable::Class,
+ Name::Namespace,
+ Name::Entity,
+ Name::Builtin::Pseudo,
+ Name::Variable::Global,
+ Name::Variable::Instance,
+ Name::Variable,
+ Text::Whitespace,
+ Text,
+ Name, :fg => :white
+ style Name::Label, :fg => :bright_pink
+ style Operator::Word,
+ Name::Tag,
+ Keyword,
+ Keyword::Namespace,
+ Keyword::Pseudo,
+ Operator, :fg => :bright_pink
+ end
+ end
+ end
diff --git a/docs/lib/multilang.rb b/docs/lib/multilang.rb
new file mode 100644
index 0000000..36fbe5b
--- /dev/null
+++ b/docs/lib/multilang.rb
@@ -0,0 +1,16 @@
+module Multilang
+ def block_code(code, full_lang_name)
+ if full_lang_name
+ parts = full_lang_name.split('--')
+ rouge_lang_name = (parts) ? parts[0] : "" # just parts[0] here causes null ref exception when no language specified
+ super(code, rouge_lang_name).sub("highlight #{rouge_lang_name}") do |match|
+ match + " tab-" + full_lang_name
+ end
+ else
+ super(code, full_lang_name)
+ end
+ end
+end
+
+require 'middleman-core/renderers/redcarpet'
+Middleman::Renderers::MiddlemanRedcarpetHTML.send :include, Multilang
diff --git a/docs/lib/nesting_unique_head.rb b/docs/lib/nesting_unique_head.rb
new file mode 100644
index 0000000..0127837
--- /dev/null
+++ b/docs/lib/nesting_unique_head.rb
@@ -0,0 +1,22 @@
+# Nested unique header generation
+require 'middleman-core/renderers/redcarpet'
+
+class NestingUniqueHeadCounter < Middleman::Renderers::MiddlemanRedcarpetHTML
+ def initialize
+ super
+ @@headers_history = {} if !defined?(@@headers_history)
+ end
+
+ def header(text, header_level)
+ friendly_text = text.gsub(/<[^>]*>/,"").parameterize
+ @@headers_history[header_level] = text.parameterize
+
+ if header_level > 1
+ for i in (header_level - 1).downto(1)
+ friendly_text.prepend("#{@@headers_history[i]}-") if @@headers_history.key?(i)
+ end
+ end
+
+ return "#{text}"
+ end
+end
diff --git a/docs/lib/toc_data.rb b/docs/lib/toc_data.rb
new file mode 100644
index 0000000..4a04efe
--- /dev/null
+++ b/docs/lib/toc_data.rb
@@ -0,0 +1,31 @@
+require 'nokogiri'
+
+def toc_data(page_content)
+ html_doc = Nokogiri::HTML::DocumentFragment.parse(page_content)
+
+ # get a flat list of headers
+ headers = []
+ html_doc.css('h1, h2, h3').each do |header|
+ headers.push({
+ id: header.attribute('id').to_s,
+ content: header.children,
+ title: header.children.to_s.gsub(/<[^>]*>/, ''),
+ level: header.name[1].to_i,
+ children: []
+ })
+ end
+
+ [3,2].each do |header_level|
+ header_to_nest = nil
+ headers = headers.reject do |header|
+ if header[:level] == header_level
+ header_to_nest[:children].push header if header_to_nest
+ true
+ else
+ header_to_nest = header if header[:level] < header_level
+ false
+ end
+ end
+ end
+ headers
+end
diff --git a/docs/lib/unique_head.rb b/docs/lib/unique_head.rb
new file mode 100644
index 0000000..d42bab2
--- /dev/null
+++ b/docs/lib/unique_head.rb
@@ -0,0 +1,24 @@
+# Unique header generation
+require 'middleman-core/renderers/redcarpet'
+require 'digest'
+class UniqueHeadCounter < Middleman::Renderers::MiddlemanRedcarpetHTML
+ def initialize
+ super
+ @head_count = {}
+ end
+ def header(text, header_level)
+ friendly_text = text.gsub(/<[^>]*>/,"").parameterize
+ if friendly_text.strip.length == 0
+ # Looks like parameterize removed the whole thing! It removes many unicode
+ # characters like Chinese and Russian. To get a unique URL, let's just
+ # URI escape the whole header
+ friendly_text = Digest::SHA1.hexdigest(text)[0,10]
+ end
+ @head_count[friendly_text] ||= 0
+ @head_count[friendly_text] += 1
+ if @head_count[friendly_text] > 1
+ friendly_text += "-#{@head_count[friendly_text]}"
+ end
+ return "#{text}"
+ end
+end
diff --git a/docs/mixins.md b/docs/mixins.md
deleted file mode 100644
index c37281d..0000000
--- a/docs/mixins.md
+++ /dev/null
@@ -1,106 +0,0 @@
-# Mixins
-
-Mixins are a super useful tool when it comes to apply the DRY principles or share functionalities
-across the platform.
-
----
-
-1. [RequireUserContextView](#RequireUserContextView)
-2. [ThreadMixin](#ThreadMixin)
-3. [CurrentThreadDefault](#CurrentThreadDefault)
-
----
-
-## RequireUserContextView
-
-A simplification of a `get_serializer_context` that can be applied on every serializer that needs
-the user in the `context`.
-
-```python hl_lines="18"
-class RequireUserContextView(GenericAPIView):
- """
- Handles with Generics of views
- """
- def get_serializer(self, *args, **kwargs):
- """
- Return the serializer instance that should be used for validating and
- deserializing input, and for serializing output.
- """
- serializer_class = self.get_serializer_class()
- kwargs['context'] = self.get_serializer_context()
- return serializer_class(*args, **kwargs)
-
- def get_serializer_context(self):
- context = super().get_serializer_context()
- context.update({
- 'request': self.request,
- 'user': self.request.user,
- })
- return context
-```
-
-## ThreadMixin
-
-All things common to a thread.
-
-```python
-class ThreadMixin:
- """
- Everything related with a thread, is placed here.
- """
- def get_thread(self):
- """Gets the thread"""
- try:
- return Thread.objects.get(uuid=self.kwargs.get('uuid'))
- except Thread.DoesNotExist:
- return
-
- def get_user(self):
- """Gets a User to whom which the message will be sent"""
- try:
- return get_user_model().objects.get(pk=self.kwargs.get('user_id'))
- except get_user_model().DoesNotExist:
- return
-
- def get_thead_by_id(self):
- """Gets a thread by id"""
- try:
- return Thread.objects.get(id=self.kwargs.get('thread_id'))
- except Thread.DoesNotExist:
- return
-```
-
-## CurrentThreadDefault
-
-Similar to `CurrentThreadDefault`, this mixin allows a similar behaviour to be injected into the
-serializer fields as long as the `thread` is passed into the context.
-
-```python hl_lines="5"
-class CurrentThreadDefault:
- requires_context = True
-
- def __call__(self, serializer_field):
- return serializer_field.context['thread']
-
- def __repr__(self):
- return '%s()' % self.__class__.__name__
-```
-
-### Examples
-
-``` python hl_lines="2 14"
-# serializers.py
-from django_messages.drf.mixins import CurrentThreadDefault
-
-
-class MessageSerializer(serializers.ModelSerializer):
- uuid = serializers.UUIDField(required=True)
- subject = serializers.CharField(required=True)
- content = serializers.CharField(
- required=True, allow_null=False, allow_blank=False, error_messages={
- 'blank': _("The message cannot be empty"),
- }
- )
- sender = serializers.HiddenField(default=serializers.CurrentUserDefault())
- thread = serializers.HiddenField(default=CurrentThreadDefault())
-```
diff --git a/docs/models.md b/docs/models.md
deleted file mode 100644
index fcc2d6b..0000000
--- a/docs/models.md
+++ /dev/null
@@ -1,225 +0,0 @@
-# Models
-
-We decided to use UUIDs to make harder to make associations by using it but not using as primary
-key.
-
----
-
-## List of Models
-
-1. [Thread](#thread)
-2. [UserThread](#userthread)
-3. [Message](#message)
-
----
-
-## Thread
-
-```python
-class Thread(AuditModel):
- """Main model where a thread is created. This model only contains a subject
- and a ManyToMany relationship with the users.
-
- Django by default creates an 'invisible' model when ManyToMany is declared
- but we can override the default and point to our own model.
-
- A `uuid` field is declared as a way to
- """
- uuid = models.UUIDField(blank=False, null=False, editable=False, default=uuid4)
- subject = models.CharField(max_length=150)
- users = models.ManyToManyField(settings.AUTH_USER_MODEL, through="UserThread")
-```
-
-Thread is the main model and some sort of source of truth.
-
-### Functions
-
-```python
- @classmethod
- def inbox(cls, user):
- """Returns the inbox of a given user"""
- return cls.objects.filter(userthread__user=user, userthread__deleted=False)
-
- @classmethod
- def deleted(cls, user):
- """Returns the deleted messages of a given user"""
- return cls.objects.filter(userthread__user=user, userthread__deleted=True)
-
- @classmethod
- def unread(cls, user):
- """Returns all the unread messages of a given user"""
- return cls.objects.filter(
- userthread__user=user,
- userthread__deleted=False,
- userthread__unread=True
- )
-
- @property
- def first_message(self):
- """Returns the first message"""
- return self.messages.all()[0]
-
- @property
- def latest_message(self):
- """Returs the last message"""
- return self.messages.order_by("-sent_at")[0]
-
- @classmethod
- def ordered(cls, objs):
- """
- Returns the iterable ordered the correct way, this is a class method
- because we don"t know what the type of the iterable will be.
- """
- objs = list(objs)
- objs.sort(key=lambda o: o.latest_message.sent_at, reverse=True)
- return objs
-
- @classmethod
- def get_thread_users(cls):
- """Returns all the users from the thread"""
- return cls.users.all()
-
- def earliest_message(self, user_to_exclude=None):
- """
- Returns the earliest message of the thread
-
- :param user_to_exclude: Returns a list of the messages excluding a given user. This is
- particulary useful for showing the earliest message sent in a thread between two different
- users
- """
- try:
- return self.messages.exclude(sender=user_to_exclude).earliest('sent_at')
- except Message.DoesNotExist:
- return
-
- def last_message(self):
- """
- Returns the latest message of the thread. Is the reverse of the `earliest_message`
- """
- try:
- return self.messages.all().latest('sent_at')
- except Message.DoesNotExist:
- return
-
- def last_message_excluding_user(self, user_to_exclude=None):
- """
- Returns the latest message of the thread. Is the reverse of the `earliest_message`
-
- :param user_to_exclude: Returns a list of the messages excluding a given user. This is
- particulary useful for showing the latest message sent in a thread between two different
- users.
- """
- queryset = self.messages.all()
- try:
- if user_to_exclude:
- queryset = queryset.exclude(sender=user_to_exclude)
- return queryset.latest('sent_at')
- except Message.DoesNotExist:
- return
-
- def unread_messages(self, user):
- """
- Gets the unread messages from User in a given Thread.
-
- Example:
- '''
- t = Thread.objects.first()
- user = User.objects.first()
- unread = t.uread_messages(user)
- '''
- """
- return self.userthread_set.filter(user=user, deleted=False, unread=True, thread=self)
-
- def is_user_first_message(self, user):
- """
- Checks if the user started the thread
- :return: Bool
- """
- try:
- message = self.messages.earliest('sent_at')
- except Message.DoesNotExist:
- return False
- return bool(message.sender.pk == user.pk)
-```
-
-## UserThread
-
-```python
-class UserThread(models.Model):
- """Maps the user and the thread. This model was used to override the default ManyToMany
- relationship table generated by django.
- """
- uuid = models.UUIDField(blank=False, null=False, default=uuid4, editable=False,)
-
- thread = models.ForeignKey(Thread, on_delete=models.CASCADE)
- user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
-
- unread = models.BooleanField()
- deleted = models.BooleanField()
-```
-
-This model is a substitution of the default generated by ManyToMany of Django.
-
-## Message
-
-```python
-class Message(models.Model):
- """
- Message model where creates threads, user threads and mapping between them.
- """
- uuid = models.UUIDField(blank=False, null=False, default=uuid4, editable=False)
- thread = models.ForeignKey(Thread, related_name="messages", on_delete=models.CASCADE)
- sender = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="sent_messages", on_delete=models.CASCADE)
- sent_at = models.DateTimeField(default=timezone.now)
- content = models.TextField()
-```
-
-### Functions
-
-```python
- @classmethod
- def new_reply(cls, thread, user, content):
- """
- Create a new reply for an existing Thread. Mark thread as unread for all other participants,
- and mark thread as read by replier. We want an atomic operation as we can't afford having
- lost data between tables and causing problems with data integrity.
- """
- with transaction.atomic():
- try:
- msg = cls.objects.create(thread=thread, sender=user, content=content)
- thread.userthread_set.exclude(user=user).update(deleted=False, unread=True)
- thread.userthread_set.filter(user=user).update(deleted=False, unread=False)
- message_sent.send(sender=cls, message=msg, thread=thread, reply=True)
- except OperationalError as e:
- log.exception(e)
- return
- return msg
-
- @classmethod
- def new_message(cls, from_user, to_users, subject, content):
- """
- Create a new Message and Thread. Mark thread as unread for all recipients, and
- mark thread as read and deleted from inbox by creator. We want an atomic operation as we
- also can't afford having lost data between tables and causing problems with data integrity.
- """
- with transaction.atomic():
- try:
- thread = Thread.objects.create(subject=subject)
- for user in to_users:
- thread.userthread_set.create(user=user, deleted=False, unread=True)
- thread.userthread_set.create(user=from_user, deleted=True, unread=False)
- msg = cls.objects.create(thread=thread, sender=from_user, content=content)
- message_sent.send(sender=cls, message=msg, thread=thread, reply=False)
- except OperationalError as e:
- log.exception(e)
- return
- return msg
-
- def get_absolute_url(self):
- return self.thread.get_absolute_url()
-```
-
-## Tips
-
-When creating a new message, the default behavior is calling the `new_message` or `reply_message`,
-depending of the type.
diff --git a/docs/overview/reference-guide.md b/docs/overview/reference-guide.md
deleted file mode 100644
index 0be34e4..0000000
--- a/docs/overview/reference-guide.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Reference Guide
-
-To make it a little bit clear how the project works, we provide a reference
-guide that can be updated at any time.
-
-## URLβViewβTemplate Matrix
-
-| URL Name | View |
-| :-------- | :----- |
-| __django_messages_drf:inbox__ | InboxListApiView |
-| __django_messages_drf:thread__ | ThreadListApiView |
-| __django_messages_drf:thread-create__ | ThreadCRUDApiView |
-| __django_messages_drf:thread-send__ | ThreadCRUDApiView |
-| __django_messages_drf:thread-delete__ | ThreadCRUDApiView |
-| __django_messages_drf:message-edit__ | EditMessageApiView |
diff --git a/docs/overview/signals.md b/docs/overview/signals.md
deleted file mode 100644
index 4f983c6..0000000
--- a/docs/overview/signals.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Signals
-
-We only provide one signal at the moment.
-
-```python
-message_sent = Signal(providing_args=["message", "thread", "reply"])
-```
diff --git a/docs/overview/url-names.md b/docs/overview/url-names.md
deleted file mode 100644
index c3baa95..0000000
--- a/docs/overview/url-names.md
+++ /dev/null
@@ -1,64 +0,0 @@
-# URLs
-
-## Names
-
-These URL names are available when using django_messages_drf `urls.py`:
-
-| View | Description |
-| :-------- | :----- |
-| __django_messages_drf:inbox__ | Inbox view. |
-| __django_messages_drf:thread__ | Lists the details of a tread of a User. Requires a UUID of a thread. |
-| __django_messages_drf:thread-create__ | Create new message to specific user. Requires a User PK (user to send). |
-| __django_messages_drf:thread-send__ | Replies to a thread. Requires thread UUID. |
-| __django_messages_drf:thread-delete__ | Delete message thread, requires thread UUID. |
-| __django_messages_drf:message-edit__ | Edits a message sent in a thread. |
-
-## django_messages_drf:inbox
-
-It doesn't require parameters
-
-## __django_messages_drf:thread__
-
-| Parameter | Description | Method
-| :-------- | :----- | :----- |
-| uuid | The UUID of a thread | GET |
-
-## __django_messages_drf:thread-create__
-
-Creates a thread.
-
-| Parameter | Description | Method |
-| :-------- | :----- | :----- |
-| user_id | The user id | GET |
-| message | The content of the message | POST |
-| subject | The subject of the message | POST |
-
-## __django_messages_drf:thread-send__
-
-Replies to the thread.
-
-| Parameter | Description | Method |
-| :-------- | :----- | :----- |
-| uuid | The UUID of a thread | GET |
-| user_id | The user id | GET |
-| message | The content of the message | POST |
-| subject | The subject of the message | POST |
-
-## __django_messages_drf:thread-delete__
-
-Replies to the thread.
-
-| Parameter | Description | Method |
-| :-------- | :----- | :----- |
-| uuid | The UUID of a thread | DELETE |
-
-## __django_messages_drf:message-edit__
-
-Edits a message sent by a given user.
-
-| Parameter | Description | Method |
-| :-------- | :----- | :----- |
-| user_id | The UUID of a thread | GET |
-| thread_id | The UUID of a thread | GET |
-| uuid | The UUID of the message to edit | PUT |
-| content | The content of the message | PUT |
diff --git a/docs/overview/views.md b/docs/overview/views.md
deleted file mode 100644
index 37f4d0d..0000000
--- a/docs/overview/views.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# Views
-
-The available views of the package, below.
-
-| Name | Description |
-| :-------- | :----- |
-| __InboxListApiView__ | Display all message threads. |
-| __ThreadCRUDApiView__ | Create a new message thread/Reply to Thread. Also Deletes a message. |
-| __ThreadListApiView__ | View specific message thread. |
-| __EditMessageApiView__ | Edits a specific message in a thread. |
diff --git a/docs/pagination.md b/docs/pagination.md
deleted file mode 100644
index a1d7a81..0000000
--- a/docs/pagination.md
+++ /dev/null
@@ -1,63 +0,0 @@
-# Pagination
-
-Two custom pagination classes are provided for the application. The information was gathered from
-[here](https://gist.github.com/tarsil/6255492c273b682bb329ba3f8d623754).
-
----
-
-1. [Pagination](#pagination)
-2. [SimplePagination](#simplepagination)
-
----
-
-## Pagination
-
-```python
-class Pagination(pagination.PageNumberPagination):
- """
- Custom paginator for REST API responses
- 'links': {
- 'next': next page url,
- 'previous': previous page url
- },
- 'count': number of records fetched,
- 'total_pages': total number of pages,
- 'next': bool has next page,
- 'previous': bool has previous page,
- 'results': result set
- })
- """
-
- def get_paginated_response(self, data):
- return Response({
- 'links': {
- 'next': self.get_next_link(),
- 'previous': self.get_previous_link()
- },
- 'pagination': {
- 'previous_page': self.page.number - 1 if self.page.number != 1 else None,
- 'current_page': self.page.number,
- 'next_page': self.page.number + 1 if self.page.has_next() else None,
- 'page_size': self.page_size
- },
- 'count': self.page.paginator.count,
- 'total_pages': self.page.paginator.num_pages,
- 'next': self.page.has_next(),
- 'previous': self.page.has_previous(),
- 'results': data
- })
-```
-
-## SimplePagination
-
-```python
-class SimplePagination(pagination.PageNumberPagination):
- """
- Custom paginator for REST API responses
- """
- def get_paginated_response(self, data):
- return Response({
- 'records_filtered': self.page.paginator.count,
- 'data': data
- })
-```
diff --git a/docs/permissions.md b/docs/permissions.md
deleted file mode 100644
index 9f19f35..0000000
--- a/docs/permissions.md
+++ /dev/null
@@ -1,114 +0,0 @@
-# Permissions
-
-A small set of permissions are set in the app to make sure the data is safer and secure and those
-can be also extended.
-
----
-
-1. [AccessMixin](#accessmixin)
-1. [DjangoMessageDRFAuthMixin](#djangomessagedrfauthmixin)
-
----
-
-## AccessMixin
-
-Base class of all permission mixins of Django Messages DRF. Adds an extension for the permissions of
-Django Rest Framework where you can now append into a list instead of repeating on every class.
-
-```python
-class AccessMixin(metaclass=DjangoMessageDRFAuthMeta):
- """
- Django rest framework doesn't append permission_classes on inherited models which can cause
- issues when it comes to call an API programmatically, this way we create a metaclass that will
- read from a property custom from our subclasses and will append to the default
- `permission_classes` on the subclasses of AccessMixin.
- """
- pass
-```
-
-## DjangoMessageDRFAuthMixin
-
-Base class of all views of the application and sets the principle that every view inheriting from
-this will validate the user authentication.
-
-```python
-class DjangoMessageDRFAuthMixin(AccessMixin, APIView):
- """
- Base APIView requiring login credentials to access it from the inside of the platform
- Or via request (if known)
- """
- permissions = [IsAuthenticated]
- pagination_class = None
-
- def __init__(self, *args, **kwargs) -> None:
- """
- Checks if the views contain the `permissions` attribute and overrides the
- `permission_classes`.
- """
- super().__init__(*args, **kwargs)
- self.permission_classes = self.permissions
- if self.pagination_class:
- try:
- rest_settings = settings.REST_FRAMEWORK
- except AttributeError:
- rest_settings = {}
- page_size = rest_settings.get('PAGE_SIZE', 50)
- self.pagination_class.page_size = page_size
-
-```
-
-## Examples
-
-Using the **`DjangoMessageDRFAuthMixin`** as a base we can now start creating our own views without
-thinking about replicating the `permission_classes`.
-
-### With DjangoMessageDRFAuthMixin
-
-```python
-from rest_framework.views import APIView
-
-from django_messages_drf.permissions import DjangoMessageDRFAuthMixin
-from my_app.permissions import MyPermission
-
-
-class MyCustomView(DjangoMessageDRFAuthMixin, APiView):
- """
- My Custom view that will do things
- """
- permissions = [MyPermission]
-
-```
-
-Importing the `APIView` is optional since the `DjangoMessageDRFAuthMixin` already implements it.
-
-Behind the scenes, Django Messages DRF is appending the `permissions` to `permission_classes` of
-Django Rest Framework, which means that if we query for the `permission_classes` we would have:
-
-```shell
-permission_classes = [IsAuthenticated, MyPermission]
-```
-
-### Without DjangoMessageDRFAuthMixin
-
-
-```python
-from rest_framework.views import APIView
-
-from rest_framework.permissions import IsAuthenticated
-from my_app.permissions import MyPermission
-
-
-class BaseView(APiView):
- permission_classes = [IsAuthenticated]
-
-
-class MyCustomView(BaseView):
- """
- My Custom view that will do things
- """
- permission_classes = [MyPermission]
-
-```
-
-This won't have the same result as the `DjangoMessageDRFAuthMixin` because what is doing is actually
-reassigning the `permission_classes` from the `BaseView` to the `MyCustomView`.
diff --git a/docs/release-notes.md b/docs/release-notes.md
deleted file mode 100644
index e734c33..0000000
--- a/docs/release-notes.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# Release Notes
-
-## 1.0.6
-
-- Preparing to drop support for python 3.6.
-- Fix `providing_args` from signals as it is deprecated in Django 4.
-
-## 1.0.5
-
-- Added `id` field to the ThreadSerializer.
-
-## 1.0.4
-
-- [Bugfix #10](https://github.com/tarsil/django-messages-drf/pull/10). Thank you [kamikaz1k](https://github.com/kamikaz1k)
-- [Bugfix #9](https://github.com/tarsil/django-messages-drf/pull/9). Thank you [kamikaz1k](https://github.com/kamikaz1k)
-
-## 1.0.3
-
-### Added
-
-- Settings to override the serializers on the views by using a custom.
-- `EditMessageApiView` allowing editing a message sent from a user of a given thread.
-- `CurrentThreadDefault` similar to `CurrentUserDefault` from Django Rest Framework but for threads.
-
-### Fixed
-
-- Show sender when a message sent is from the same sender and receiver - [Issue](https://github.com/tarsil/django-messages-drf/issues/5)
-- Issue with `display_name` for InboxSerializer - [Issue](https://github.com/tarsil/django-messages-drf/issues/4).
-- `ThreadCRUDApiView` `post` where wasn't using the data from the serializer.
-
-## 1.0.2
-
-### Added
-
-- Support for python 3.9
-- CircleCI config
-
-### Fixed
-
-- Tests naming conflicts.
-- Migration issues.
-
-### Updated
-
-- README.
-
-## 1.0.0
-
-- Initial release
-
-## License
-
-Copyright (c) 2020-present Tiago Silva and contributors under the [MIT license](https://opensource.org/licenses/MIT).
diff --git a/docs/serializers.md b/docs/serializers.md
deleted file mode 100644
index 5c0b7d9..0000000
--- a/docs/serializers.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Serializers
-
-Django Messages DRF like with the views, also comes with a set of serializers that allows you
-to apply in your project but you can and should build your own with your own use cases.
-
-The way the serializers are built are the default ones from Django Rest Framework.
-
-## Examples
-
-### Inbox
-
-A simple example for an inbox serializer.
-
-```python
-class InboxSerializer(serializers.ModelSerializer):
- """
- Serializer for the inbox.
- """
- sent_at = serializers.DateTimeField(source='first_message.sent_at')
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.useruser = self.context.get('user')
-
- class Meta:
- model = Thread
- fields = ('uuid', 'subject', 'sent_at')
-
-```
-
-### Sender
-
-A sender for Django Messages DRF is a Django **`user`** and can be whatever you decided that u.
-
-```python
-class SenderSerializer(serializers.ModelSerializer):
- class Meta:
- model = get_user_model()
- fields = ('first_name', 'last_name', 'email')
-```
diff --git a/docs/settings.md b/docs/settings.md
deleted file mode 100644
index 57e2d15..0000000
--- a/docs/settings.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Serializer Settings
-
-Django Messages DRF allows overriding some settings for the views, which means, instead of creating
-a new view just to apply your own serializer, you can simply override the setting and
-Django Messages DRF will apply it on the current views.
-
-None of the below settings are required to be added to your `settings.py`. This is only if
-you wish to override the current defaults.
-
-## Overriding
-
-In your **`settings.py`**.
-
-| Setting Name | View | Default |
-| :-------- | :----- | :----- |
-| __DJANGO_MESSAGES_DRF_INBOX_SERIALIZER__ | InboxListApiView | InboxSerializer |
-| __DJANGO_MESSAGES_DRF_THREAD_SERIALIZER__ | ThreadListApiView | ThreadSerializer |
-| __DJANGO_MESSAGES_DRF_MESSAGE_SERIALIZER__ | ThreadCRUDApiView | ThreadReplySerializer |
-| __DJANGO_MESSAGES_DRF_EDIT_MESSAGE_SERIALIZER__ | EditMessageApiView | EditMessageSerializer |
-
-## Usage
-
-Overriding is based on `import_string` from your **`settings.py`**.
-
-### Examples
-
-```python
-# settings.py
-
-DJANGO_MESSAGES_DRF_INBOX_SERIALIZER = 'myapp.serializers.MyCustomInboxSerializer'
-DJANGO_MESSAGES_DRF_THREAD_SERIALIZER = 'myapp.serializers.MyCustomThreadSerializer'
-```
-
-If none of the settings is overridden or is **`None`** , then it will default to the original.
-
-# Behaviour Settings
-
-Django Messages DRF allows overriding some behaviours.
-
-## Overriding
-
-In your **`settings.py`**.
-
-| Setting Name | Behaviour | Type | Default |
-| :-------- | :----- | :----- | :----- |
-| __DJANGO_MESSAGES_MARK_NEW_THREAD_MESSAGE_AS_DELETED__ | Mark the first message sent as deleted | Boolean | True |
diff --git a/docs/slate.sh b/docs/slate.sh
new file mode 100755
index 0000000..d6f1bd9
--- /dev/null
+++ b/docs/slate.sh
@@ -0,0 +1,248 @@
+#!/usr/bin/env bash
+set -o errexit #abort if any command fails
+
+me=$(basename "$0")
+
+help_message="\
+Usage: $me [] []
+Run commands related to the slate process.
+
+Commands:
+
+ serve Run the middleman server process, useful for
+ development.
+ build Run the build process.
+ deploy Will build and deploy files to branch. Use
+ --no-build to only deploy.
+
+Global Options:
+
+ -h, --help Show this help information.
+ -v, --verbose Increase verbosity. Useful for debugging.
+
+Deploy options:
+ -e, --allow-empty Allow deployment of an empty directory.
+ -m, --message MESSAGE Specify the message used when committing on the
+ deploy branch.
+ -n, --no-hash Don't append the source commit's hash to the deploy
+ commit's message.
+ --no-build Do not build the source files.
+"
+
+
+run_serve() {
+ exec bundle exec middleman serve --watcher-force-polling
+}
+
+run_build() {
+ bundle exec middleman build --clean --watcher-disable
+}
+
+parse_args() {
+ # Set args from a local environment file.
+ if [ -e ".env" ]; then
+ source .env
+ fi
+
+ command=
+
+ # Parse arg flags
+ # If something is exposed as an environment variable, set/overwrite it
+ # here. Otherwise, set/overwrite the internal variable instead.
+ while : ; do
+ if [[ $1 = "-h" || $1 = "--help" ]]; then
+ echo "$help_message"
+ exit 0
+ elif [[ $1 = "-v" || $1 = "--verbose" ]]; then
+ verbose=true
+ shift
+ elif [[ $1 = "-e" || $1 = "--allow-empty" ]]; then
+ allow_empty=true
+ shift
+ elif [[ ( $1 = "-m" || $1 = "--message" ) && -n $2 ]]; then
+ commit_message=$2
+ shift 2
+ elif [[ $1 = "-n" || $1 = "--no-hash" ]]; then
+ GIT_DEPLOY_APPEND_HASH=false
+ shift
+ elif [[ $1 = "--no-build" ]]; then
+ no_build=true
+ shift
+ elif [[ $1 = "serve" || $1 = "build" || $1 = "deploy" ]]; then
+ if [ ! -z "${command}" ]; then
+ >&2 echo "You can only specify one command."
+ exit 1
+ fi
+ command=$1
+ shift
+ elif [ -z $1 ]; then
+ break
+ fi
+ done
+
+ if [ -z "${command}" ]; then
+ >&2 echo "Command not specified."
+ exit 1
+ fi
+
+ # Set internal option vars from the environment and arg flags. All internal
+ # vars should be declared here, with sane defaults if applicable.
+
+ # Source directory & target branch.
+ deploy_directory=build
+ deploy_branch=gh-pages
+
+ #if no user identity is already set in the current git environment, use this:
+ default_username=${GIT_DEPLOY_USERNAME:-deploy.sh}
+ default_email=${GIT_DEPLOY_EMAIL:-}
+
+ #repository to deploy to. must be readable and writable.
+ repo=origin
+
+ #append commit hash to the end of message by default
+ append_hash=${GIT_DEPLOY_APPEND_HASH:-true}
+}
+
+main() {
+ enable_expanded_output
+
+ if ! git diff --exit-code --quiet --cached; then
+ echo Aborting due to uncommitted changes in the index >&2
+ return 1
+ fi
+
+ commit_title=`git log -n 1 --format="%s" HEAD`
+ commit_hash=` git log -n 1 --format="%H" HEAD`
+
+ #default commit message uses last title if a custom one is not supplied
+ if [[ -z $commit_message ]]; then
+ commit_message="publish: $commit_title"
+ fi
+
+ #append hash to commit message unless no hash flag was found
+ if [ $append_hash = true ]; then
+ commit_message="$commit_message"$'\n\n'"generated from commit $commit_hash"
+ fi
+
+ previous_branch=`git rev-parse --abbrev-ref HEAD`
+
+ if [ ! -d "$deploy_directory" ]; then
+ echo "Deploy directory '$deploy_directory' does not exist. Aborting." >&2
+ return 1
+ fi
+
+ # must use short form of flag in ls for compatibility with macOS and BSD
+ if [[ -z `ls -A "$deploy_directory" 2> /dev/null` && -z $allow_empty ]]; then
+ echo "Deploy directory '$deploy_directory' is empty. Aborting. If you're sure you want to deploy an empty tree, use the --allow-empty / -e flag." >&2
+ return 1
+ fi
+
+ if git ls-remote --exit-code $repo "refs/heads/$deploy_branch" ; then
+ # deploy_branch exists in $repo; make sure we have the latest version
+
+ disable_expanded_output
+ git fetch --force $repo $deploy_branch:$deploy_branch
+ enable_expanded_output
+ fi
+
+ # check if deploy_branch exists locally
+ if git show-ref --verify --quiet "refs/heads/$deploy_branch"
+ then incremental_deploy
+ else initial_deploy
+ fi
+
+ restore_head
+}
+
+initial_deploy() {
+ git --work-tree "$deploy_directory" checkout --orphan $deploy_branch
+ git --work-tree "$deploy_directory" add --all
+ commit+push
+}
+
+incremental_deploy() {
+ #make deploy_branch the current branch
+ git symbolic-ref HEAD refs/heads/$deploy_branch
+ #put the previously committed contents of deploy_branch into the index
+ git --work-tree "$deploy_directory" reset --mixed --quiet
+ git --work-tree "$deploy_directory" add --all
+
+ set +o errexit
+ diff=$(git --work-tree "$deploy_directory" diff --exit-code --quiet HEAD --)$?
+ set -o errexit
+ case $diff in
+ 0) echo No changes to files in $deploy_directory. Skipping commit.;;
+ 1) commit+push;;
+ *)
+ echo git diff exited with code $diff. Aborting. Staying on branch $deploy_branch so you can debug. To switch back to main, use: git symbolic-ref HEAD refs/heads/main && git reset --mixed >&2
+ return $diff
+ ;;
+ esac
+}
+
+commit+push() {
+ set_user_id
+ git --work-tree "$deploy_directory" commit -m "$commit_message"
+
+ disable_expanded_output
+ #--quiet is important here to avoid outputting the repo URL, which may contain a secret token
+ git push --quiet $repo $deploy_branch
+ enable_expanded_output
+}
+
+#echo expanded commands as they are executed (for debugging)
+enable_expanded_output() {
+ if [ $verbose ]; then
+ set -o xtrace
+ set +o verbose
+ fi
+}
+
+#this is used to avoid outputting the repo URL, which may contain a secret token
+disable_expanded_output() {
+ if [ $verbose ]; then
+ set +o xtrace
+ set -o verbose
+ fi
+}
+
+set_user_id() {
+ if [[ -z `git config user.name` ]]; then
+ git config user.name "$default_username"
+ fi
+ if [[ -z `git config user.email` ]]; then
+ git config user.email "$default_email"
+ fi
+}
+
+restore_head() {
+ if [[ $previous_branch = "HEAD" ]]; then
+ #we weren't on any branch before, so just set HEAD back to the commit it was on
+ git update-ref --no-deref HEAD $commit_hash $deploy_branch
+ else
+ git symbolic-ref HEAD refs/heads/$previous_branch
+ fi
+
+ git reset --mixed
+}
+
+filter() {
+ sed -e "s|$repo|\$repo|g"
+}
+
+sanitize() {
+ "$@" 2> >(filter 1>&2) | filter
+}
+
+parse_args "$@"
+
+if [ "${command}" = "serve" ]; then
+ run_serve
+elif [[ "${command}" = "build" ]]; then
+ run_build
+elif [[ ${command} = "deploy" ]]; then
+ if [[ ${no_build} != true ]]; then
+ run_build
+ fi
+ main "$@"
+fi
diff --git a/docs/source/fonts/slate.eot b/docs/source/fonts/slate.eot
new file mode 100644
index 0000000..13c4839
Binary files /dev/null and b/docs/source/fonts/slate.eot differ
diff --git a/docs/source/fonts/slate.svg b/docs/source/fonts/slate.svg
new file mode 100644
index 0000000..5f34982
--- /dev/null
+++ b/docs/source/fonts/slate.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/docs/source/fonts/slate.ttf b/docs/source/fonts/slate.ttf
new file mode 100644
index 0000000..ace9a46
Binary files /dev/null and b/docs/source/fonts/slate.ttf differ
diff --git a/docs/source/fonts/slate.woff b/docs/source/fonts/slate.woff
new file mode 100644
index 0000000..1e72e0e
Binary files /dev/null and b/docs/source/fonts/slate.woff differ
diff --git a/docs/source/fonts/slate.woff2 b/docs/source/fonts/slate.woff2
new file mode 100644
index 0000000..7c585a7
Binary files /dev/null and b/docs/source/fonts/slate.woff2 differ
diff --git a/docs/source/images/logo.png b/docs/source/images/logo.png
new file mode 100644
index 0000000..68a478f
Binary files /dev/null and b/docs/source/images/logo.png differ
diff --git a/docs/source/images/navbar.png b/docs/source/images/navbar.png
new file mode 100644
index 0000000..df38e90
Binary files /dev/null and b/docs/source/images/navbar.png differ
diff --git a/docs/source/includes/_errors.md b/docs/source/includes/_errors.md
new file mode 100644
index 0000000..2a2589e
--- /dev/null
+++ b/docs/source/includes/_errors.md
@@ -0,0 +1,9 @@
+# Errors
+
+
+
+Mainterners:
+
+Tarsil
diff --git a/docs/source/index.html.md b/docs/source/index.html.md
new file mode 100644
index 0000000..e2654a6
--- /dev/null
+++ b/docs/source/index.html.md
@@ -0,0 +1,1144 @@
+---
+title: Django Messages DRF
+
+language_tabs: # must be one of https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers
+ - python
+
+toc_footers:
+ - Can we improve it? Tell us
+
+includes:
+ - errors
+
+search: true
+
+code_clipboard: true
+
+meta:
+ - name: description
+ content: Documentation for the Django Messages DRF API
+---
+
+# Introduction
+
+[](https://circleci.com/gh/tarsil/django-messages-drf)
+[](https://codecov.io/gh/tarsil/django-messages-drf)
+
+Django Messages DRF is an alternative and based on pinax-messages but using Django Rest Framework by making it easier to integrate with your existing project. It allows CRUD messages along with inbox and creating threads. Users can reply to messages and mark them as read.
+
+A special thanks to pinax for inspiring me to do this and use some ideas.
+
+Tested, easy to customize, up-to-date with Python 3.10 app that provided private user-to-user threaded messaging.
+
+## Supported Django and Python Versions
+
+| Django / Python | 3.6 | 3.7 | 3.8 | 3.9 | 3.10 |
+| --------------- | --- | --- | --- | --- | ---- |
+| 2.2 | Yes | Yes | Yes | Yes | Yes |
+| 3.0 | Yes | Yes | Yes | Yes | Yes |
+| 3.1 | Yes | Yes | Yes | Yes | Yes |
+| 3.2 | Yes | Yes | Yes | Yes | Yes |
+| 4.0 | Yes | Yes | Yes | Yes | Yes |
+
+# Installing
+
+> In order to install run pip:
+
+```shell
+pip install django_messages_drf
+```
+
+> Then add `django_messages_drf` to your `INSTALLED_APPS`:
+
+```python
+INSTALLED_APPS = [
+ # ...
+ "django_messages_drf",
+ # ...
+]
+```
+
+> Run Django migrations to create `django-messages-drf` database tables:
+
+```shell
+python manage.py migrate
+```
+
+> You'll also want to add `django_messages_drf.urls` into your main urlpatterns.
+
+```python
+urlpatterns = [
+ # other urls
+ path("api/messages-drf/", include("django_messages_drf.urls", namespace="django_messages_drf")),
+]
+```
+
+> Remember to use at least Python 3.6
+
+Process of installing uses default pip procedure like other django apps.
+
+# URLs
+
+## Overview
+
+```python
+from django.urls import path
+from . import views
+
+# Change app_name when customizing endpoints in your app
+app_name = "django_messages_drf"
+
+urlpatterns = [
+ path('inbox/', views.InboxListApiView.as_view(), name='inbox'),
+ path('message/thread//', views.ThreadListApiView.as_view(), name='thread'),
+ path('message/thread//send/', views.ThreadCRUDApiView.as_view(), name='thread-create'),
+ path('message/thread///send/', views.ThreadCRUDApiView.as_view(), name='thread-send'),
+ path('message/thread///edit/', views.EditMessageApiView.as_view(), name='message-edit'),
+ path('thread//delete', views.ThreadCRUDApiView.as_view(), name='thread-delete'),
+]
+
+```
+
+App provides 6 endpoints with CRUD functionalities.
+
+## Inbox
+
+```python
+path('inbox/', views.InboxListApiView.as_view(), name='inbox'),
+```
+
+This endpoint retrieves all threads that have been sent to current user (initiated by other users).
+
+### HTTP Request
+
+`GET http://localhost:8000/api/messages-drf/inbox/`
+
+
+
+## List Messages
+
+```python
+path('message/thread//', views.ThreadListApiView.as_view(), name='thread'),
+```
+
+This endpoint retrieves all messages that are within a thread.
+
+### Route Parameters
+
+| Parameter | Required | Description |
+| --------- | -------- | ----------------------- |
+| uuid | true | The UUID of the thread. |
+
+### HTTP Request
+
+`GET http://localhost:8000/api/message/thread//`
+
+
+
+## Send First Message
+
+```python
+path('message/thread//send/', views.ThreadCRUDApiView.as_view(), name='thread-create'),
+```
+
+> This View can also take another url parameter - `thread_uuid` (see below)
+
+This endpoint sends a new message to a user by initiating new thread.
+
+### Route Parameters
+
+| Parameter | Required | Description |
+| --------- | -------- | ---------------------------------------------- |
+| user_id | true | The id of a user we want to send a message to. |
+
+### Body Parameters
+
+| Parameter | Description | Method |
+| :-------- | :------------------------- | :----- |
+| message | The content of the message | POST |
+| subject | The subject of the message | POST |
+
+### HTTP Request
+
+`GET http://localhost:8000/api/messages-drf/message/thread//send/`
+
+
+
+## Expand on thread
+
+```python
+path('message/thread///send/', views.ThreadCRUDApiView.as_view(), name='thread-send'),
+```
+
+> This is the same View that initiates a thread (see above).
+
+This endpoint sends a reply to an existing message.
+
+### Route Parameters
+
+| Parameter | Required | Description |
+| --------- | -------- | ---------------------------------------------- |
+| uuid | true | The id of a thread we want to send a reply to. |
+| user_id | true | The id of a user we want to send a message to. |
+
+### Body Parameters
+
+| Parameter | Description | Method |
+| :-------- | :------------------------- | :----- |
+| message | The content of the message | POST |
+| subject | The subject of the message | POST |
+
+### HTTP Request
+
+`GET http://localhost:8000/api/messages-drf/message/thread///send/`
+
+
+
+## Edit message
+
+```python
+path('message/thread///edit/', views.EditMessageApiView.as_view(), name='message-edit'),
+```
+
+This endpoint edits a message from within a thread.
+
+
+
+### Route Parameters
+
+| Parameter | Required | Description |
+| --------- | -------- | ---------------------------------------------- |
+| thread_id | true | The id of a message we want to edit. |
+| user_id | true | The id of a user we want to send a message to. |
+
+
+
+### Body Parameters
+
+| Parameter | Description | Method |
+| :-------- | :------------------------- | :----- |
+| message | The content of the message | POST |
+| subject | The subject of the message | POST |
+
+### HTTP Request
+
+`GET http://localhost:8000/api/messages-drf/message/thread///edit/`
+
+# Views
+
+Django Messages DRF comes initially with a set of views that allows you
+to apply in your projects. All the views are in Django Rest Framework and allowing customization
+up to a certain level.
+
+All of the serializers are provided by the settings and allows overriding from there.
+
+## InboxListApiView
+
+The main view for an inbox of a **`user`** where return an ordered list from the latest received to
+the first.
+
+```python
+class InboxListApiView(DjangoMessageDRFAuthMixin, RequireUserContextView, ListAPIView):
+ """
+ Returns the Inbox the logged in User
+ """
+ serializer_class = InboxSerializer
+ pagination_class = Pagination
+
+ def get_queryset(self):
+ queryset = Thread.inbox(self.request.user)
+ return Thread.ordered(queryset)
+```
+
+### Tips
+
+We use a custom **`Pagination`** object that adds some more details to the default Django
+Pagination. You can have your own pagination object and override the default.
+
+```python
+# Custom Pagination Applied to the view
+
+from rest_framework import pagination
+
+from django_messages_drf.views import InboxListApiView
+
+class MyCustomPagination(pagination.PageNumberPagination):
+ # Add custom pagination logic
+
+
+class MyInboxListApiView(InboxListApiView):
+ pagination_class = MyCustomPagination
+
+```
+
+You can also override the serializer_class default using the same principle.
+
+```python
+# Custom Pagination Applied to the view
+
+from rest_framework import serializers
+
+from django_messages_drf.views import InboxListApiView
+
+class MyCustomSerializer(serializers.ModelSerializer):
+ # Add custom serializer logic
+
+
+class MyInboxListApiView(InboxListApiView):
+ serializer_class = MyCustomSerializer
+```
+
+Or combining both **`pagination`** and **`serializer_class`** in one.
+
+```python
+# Custom Pagination Applied to the view
+
+from rest_framework import pagination
+from rest_framework import serializers
+
+from django_messages_drf.views import InboxListApiView
+
+class MyCustomPagination(pagination.PageNumberPagination):
+ # Add custom pagination logic
+
+class MyCustomSerializer(serializers.ModelSerializer):
+ # Add custom serializer logic
+
+
+class MyInboxListApiView(InboxListApiView):
+ serializer_class = MyCustomSerializer
+ pagination_class = MyCustomPagination
+```
+
+## ThreadListApiView
+
+```python
+class ThreadListApiView(DjangoMessageDRFAuthMixin, ThreadMixin, RequireUserContextView, ListAPIView):
+ """
+ Gets all the messages from a given thread
+ """
+ serializer_class = ThreadSerializer
+
+ def get(self, request, *args, **kwargs):
+ instance = self.get_thread()
+ if not instance:
+ raise NotFound(code=status.HTTP_404_NOT_FOUND)
+
+ serializer = self.serializer_class(instance, context=self.get_serializer_context())
+ return Response(serializer.data, status=status.HTTP_200_OK)
+```
+
+### Tips
+
+The same logic for **ThreadListApiView** is the same applied for [InboxListApiView](#tips) by
+overriding the default **`serializer_class`**.
+
+```python
+# Custom Pagination Applied to the view
+
+from rest_framework import serializers
+
+from django_messages_drf.views import ThreadListApiView
+
+class MyCustomSerializer(serializers.ModelSerializer):
+ # Add custom serializer logic
+
+
+class MyThreadListApiView(ThreadListApiView):
+ serializer_class = MyCustomSerializer
+```
+
+## ThreadCRUDApiView
+
+```python
+class ThreadCRUDApiView(DjangoMessageDRFAuthMixin, ThreadMixin, RequireUserContextView, APIView):
+ """
+ View that allows the reply of a specific message as well as the
+ We will apply some pagination to return a list for the results and therefore
+
+ 1. This API gets or creates the Thread
+ 2. If a UUID is passed, then a Thread is validated and created but if only a user_id is
+ passed, then it will create a new thread and start a conversation.
+ """
+ serializer_class = ThreadReplySerializer
+
+ def post(self, request, uuid=None, *args, **kwargs):
+ """
+ Replies a mensage in given thread
+ """
+ thread = self.get_thread() if uuid else None
+ user = self.get_user()
+
+ if not user:
+ raise NotFound(code=status.HTTP_404_NOT_FOUND)
+
+ serializer = self.serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ subject = request.data.get('subject') or thread.subject
+ if not thread:
+ msg = Message.new_message(
+ from_user=self.request.user, to_users=[user], subject=subject,
+ content=request.data.get('message')
+ )
+
+ else:
+ msg = Message.new_reply(thread, self.request.user, request.data.get('message'))
+ thread.subject = subject
+ thread.save()
+
+ message = MessageSerializer(msg, context=self.get_serializer_context())
+ return Response(message.data, status=status.HTTP_200_OK)
+
+ def delete(self, request, *args, **kwargs):
+ """
+ Flags a thread as deleted a thread from the system.
+ To remove completely, another permanent view can be added to execute the action.
+ """
+ thread = self.get_thread()
+ if not thread:
+ raise NotFound(code=status.HTTP_404_NOT_FOUND)
+
+ thread.userthread_set.filter(user=request.user).update(deleted=True)
+ return Response(status=status.HTTP_200_OK)
+```
+
+### Tips
+
+The same logic for **ThreadCRUDApiView** is the same applied for [InboxListApiView](#tips) by
+overriding the default **`serializer_class`**.
+
+```python
+# Custom Pagination Applied to the view
+
+from rest_framework import serializers
+
+from django_messages_drf.views import ThreadCRUDApiView
+
+class MyCustomSerializer(serializers.ModelSerializer):
+ # Add custom serializer logic
+
+
+class MyThreadCRUDApiView(ThreadCRUDApiView):
+ serializer_class = MyCustomSerializer
+```
+
+## EditMessageApiView
+
+```python
+class EditMessageApiView(DjangoMessageDRFAuthMixin, ThreadMixin, RequireUserContextView, APIView):
+ """
+ Edits a message sent from a user in a given thread
+ """
+ serializer_class = EDIT_MESSAGE_SERIALIZER
+
+ def get_instance(self, user, message_uuid):
+ """
+ Checks of the message exists
+ """
+ try:
+ return Message.objects.get(sender=user, uuid=message_uuid)
+ except Message.DoesNotExist:
+ return
+
+ def get_serializer_context(self):
+ context = super().get_serializer_context()
+ context.update({
+ 'thread': self.get_thead_by_id(),
+ })
+ return context
+
+ def put(self, request, user_id, thread_id, *args, **kwargs):
+ """
+ Edits a mensage in given thread.
+
+ 1. Gets the user_id from the URL.
+ 2. From the request.data gets the uuid of the message
+ 3. Validates
+ 4. Saves and returns
+ """
+ user = self.get_user()
+
+ if not user:
+ raise NotFound()
+
+ if (not user.pk == request.user.pk):
+ raise PermissionDenied()
+
+ # Get the instance of the message for a given user
+ instance = self.get_instance(user, request.data.get('uuid'))
+ if not instance:
+ raise NotFound()
+
+ serializer = self.serializer_class(instance, data=request.data, context=self.get_serializer_context())
+ serializer.is_valid(raise_exception=True)
+ instance = serializer.save()
+
+ message = MessageSerializer(instance, context=self.get_serializer_context())
+ return Response(message.data, status=status.HTTP_200_OK)
+
+```
+
+### General Tip
+
+1. The views follow a similar structure and design everywhere but they can also be overwritten in a normal Django way.
+2. Checkout the settings page to see how to override the variables.
+
+# Mixins
+
+Mixins are a super useful tool when it comes to apply the DRY principles or share functionalities
+across the platform.
+
+## RequireUserContextView
+
+A simplification of a `get_serializer_context` that can be applied on every serializer that needs
+the user in the `context`.
+
+```python
+class RequireUserContextView(GenericAPIView):
+ """
+ Handles with Generics of views
+ """
+ def get_serializer(self, *args, **kwargs):
+ """
+ Return the serializer instance that should be used for validating and
+ deserializing input, and for serializing output.
+ """
+ serializer_class = self.get_serializer_class()
+ kwargs['context'] = self.get_serializer_context()
+ return serializer_class(*args, **kwargs)
+
+ def get_serializer_context(self):
+ context = super().get_serializer_context()
+ context.update({
+ 'request': self.request,
+ 'user': self.request.user,
+ })
+ return context
+```
+
+## ThreadMixin
+
+All things common to a thread.
+
+```python
+class ThreadMixin:
+ """
+ Everything related with a thread, is placed here.
+ """
+ def get_thread(self):
+ """Gets the thread"""
+ try:
+ return Thread.objects.get(uuid=self.kwargs.get('uuid'))
+ except Thread.DoesNotExist:
+ return
+
+ def get_user(self):
+ """Gets a User to whom which the message will be sent"""
+ try:
+ return get_user_model().objects.get(pk=self.kwargs.get('user_id'))
+ except get_user_model().DoesNotExist:
+ return
+
+ def get_thead_by_id(self):
+ """Gets a thread by id"""
+ try:
+ return Thread.objects.get(id=self.kwargs.get('thread_id'))
+ except Thread.DoesNotExist:
+ return
+```
+
+## CurrentThreadDefault
+
+Similar to `CurrentThreadDefault`, this mixin allows a similar behaviour to be injected into the
+serializer fields as long as the `thread` is passed into the context.
+
+```python
+class CurrentThreadDefault:
+ requires_context = True
+
+ def __call__(self, serializer_field):
+ return serializer_field.context['thread']
+
+ def __repr__(self):
+ return '%s()' % self.__class__.__name__
+```
+
+## Examples
+
+```python
+# serializers.py
+from django_messages.drf.mixins import CurrentThreadDefault
+
+
+class MessageSerializer(serializers.ModelSerializer):
+ uuid = serializers.UUIDField(required=True)
+ subject = serializers.CharField(required=True)
+ content = serializers.CharField(
+ required=True, allow_null=False, allow_blank=False, error_messages={
+ 'blank': _("The message cannot be empty"),
+ }
+ )
+ sender = serializers.HiddenField(default=serializers.CurrentUserDefault())
+ thread = serializers.HiddenField(default=CurrentThreadDefault())
+```
+
+# Models
+
+We decided to use UUIDs to make harder to make associations by using it but not using as primary
+key.
+
+## Thread
+
+```python
+class Thread(AuditModel):
+ """Main model where a thread is created. This model only contains a subject
+ and a ManyToMany relationship with the users.
+
+ Django by default creates an 'invisible' model when ManyToMany is declared
+ but we can override the default and point to our own model.
+
+ A `uuid` field is declared as a way to
+ """
+ uuid = models.UUIDField(blank=False, null=False, editable=False, default=uuid4)
+ subject = models.CharField(max_length=150)
+ users = models.ManyToManyField(settings.AUTH_USER_MODEL, through="UserThread")
+```
+
+Thread is the main model and some sort of source of truth.
+
+### Functions
+
+```python
+ @classmethod
+ def inbox(cls, user):
+ """Returns the inbox of a given user"""
+ return cls.objects.filter(userthread__user=user, userthread__deleted=False)
+
+ @classmethod
+ def deleted(cls, user):
+ """Returns the deleted messages of a given user"""
+ return cls.objects.filter(userthread__user=user, userthread__deleted=True)
+
+ @classmethod
+ def unread(cls, user):
+ """Returns all the unread messages of a given user"""
+ return cls.objects.filter(
+ userthread__user=user,
+ userthread__deleted=False,
+ userthread__unread=True
+ )
+
+ @property
+ def first_message(self):
+ """Returns the first message"""
+ return self.messages.all()[0]
+
+ @property
+ def latest_message(self):
+ """Returs the last message"""
+ return self.messages.order_by("-sent_at")[0]
+
+ @classmethod
+ def ordered(cls, objs):
+ """
+ Returns the iterable ordered the correct way, this is a class method
+ because we don"t know what the type of the iterable will be.
+ """
+ objs = list(objs)
+ objs.sort(key=lambda o: o.latest_message.sent_at, reverse=True)
+ return objs
+
+ @classmethod
+ def get_thread_users(cls):
+ """Returns all the users from the thread"""
+ return cls.users.all()
+
+ def earliest_message(self, user_to_exclude=None):
+ """
+ Returns the earliest message of the thread
+
+ :param user_to_exclude: Returns a list of the messages excluding a given user. This is
+ particulary useful for showing the earliest message sent in a thread between two different
+ users
+ """
+ try:
+ return self.messages.exclude(sender=user_to_exclude).earliest('sent_at')
+ except Message.DoesNotExist:
+ return
+
+ def last_message(self):
+ """
+ Returns the latest message of the thread. Is the reverse of the `earliest_message`
+ """
+ try:
+ return self.messages.all().latest('sent_at')
+ except Message.DoesNotExist:
+ return
+
+ def last_message_excluding_user(self, user_to_exclude=None):
+ """
+ Returns the latest message of the thread. Is the reverse of the `earliest_message`
+
+ :param user_to_exclude: Returns a list of the messages excluding a given user. This is
+ particulary useful for showing the latest message sent in a thread between two different
+ users.
+ """
+ queryset = self.messages.all()
+ try:
+ if user_to_exclude:
+ queryset = queryset.exclude(sender=user_to_exclude)
+ return queryset.latest('sent_at')
+ except Message.DoesNotExist:
+ return
+
+ def unread_messages(self, user):
+ """
+ Gets the unread messages from User in a given Thread.
+
+ Example:
+ '''
+ t = Thread.objects.first()
+ user = User.objects.first()
+ unread = t.uread_messages(user)
+ '''
+ """
+ return self.userthread_set.filter(user=user, deleted=False, unread=True, thread=self)
+
+ def is_user_first_message(self, user):
+ """
+ Checks if the user started the thread
+ :return: Bool
+ """
+ try:
+ message = self.messages.earliest('sent_at')
+ except Message.DoesNotExist:
+ return False
+ return bool(message.sender.pk == user.pk)
+```
+
+## UserThread
+
+```python
+class UserThread(models.Model):
+ """Maps the user and the thread. This model was used to override the default ManyToMany
+ relationship table generated by django.
+ """
+ uuid = models.UUIDField(blank=False, null=False, default=uuid4, editable=False,)
+
+ thread = models.ForeignKey(Thread, on_delete=models.CASCADE)
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
+
+ unread = models.BooleanField()
+ deleted = models.BooleanField()
+```
+
+This model is a substitution of the default generated by ManyToMany of Django.
+
+## Message
+
+```python
+class Message(models.Model):
+ """
+ Message model where creates threads, user threads and mapping between them.
+ """
+ uuid = models.UUIDField(blank=False, null=False, default=uuid4, editable=False)
+ thread = models.ForeignKey(Thread, related_name="messages", on_delete=models.CASCADE)
+ sender = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="sent_messages", on_delete=models.CASCADE)
+ sent_at = models.DateTimeField(default=timezone.now)
+ content = models.TextField()
+```
+
+### Functions
+
+```python
+ @classmethod
+ def new_reply(cls, thread, user, content):
+ """
+ Create a new reply for an existing Thread. Mark thread as unread for all other participants,
+ and mark thread as read by replier. We want an atomic operation as we can't afford having
+ lost data between tables and causing problems with data integrity.
+ """
+ with transaction.atomic():
+ try:
+ msg = cls.objects.create(thread=thread, sender=user, content=content)
+ thread.userthread_set.exclude(user=user).update(deleted=False, unread=True)
+ thread.userthread_set.filter(user=user).update(deleted=False, unread=False)
+ message_sent.send(sender=cls, message=msg, thread=thread, reply=True)
+ except OperationalError as e:
+ log.exception(e)
+ return
+ return msg
+
+ @classmethod
+ def new_message(cls, from_user, to_users, subject, content):
+ """
+ Create a new Message and Thread. Mark thread as unread for all recipients, and
+ mark thread as read and deleted from inbox by creator. We want an atomic operation as we
+ also can't afford having lost data between tables and causing problems with data integrity.
+ """
+ with transaction.atomic():
+ try:
+ thread = Thread.objects.create(subject=subject)
+ for user in to_users:
+ thread.userthread_set.create(user=user, deleted=False, unread=True)
+ thread.userthread_set.create(user=from_user, deleted=True, unread=False)
+ msg = cls.objects.create(thread=thread, sender=from_user, content=content)
+ message_sent.send(sender=cls, message=msg, thread=thread, reply=False)
+ except OperationalError as e:
+ log.exception(e)
+ return
+ return msg
+
+ def get_absolute_url(self):
+ return self.thread.get_absolute_url()
+```
+
+## Tips
+
+When creating a new message, the default behavior is calling the `new_message` or `reply_message`,
+depending of the type.
+
+# Pagination
+
+Two custom pagination classes are provided for the application. The information was gathered from
+[here](https://gist.github.com/tarsil/6255492c273b682bb329ba3f8d623754).
+
+## Pagination class
+
+```python
+class Pagination(pagination.PageNumberPagination):
+ """
+ Custom paginator for REST API responses
+ 'links': {
+ 'next': next page url,
+ 'previous': previous page url
+ },
+ 'count': number of records fetched,
+ 'total_pages': total number of pages,
+ 'next': bool has next page,
+ 'previous': bool has previous page,
+ 'results': result set
+ })
+ """
+
+ def get_paginated_response(self, data):
+ return Response({
+ 'links': {
+ 'next': self.get_next_link(),
+ 'previous': self.get_previous_link()
+ },
+ 'pagination': {
+ 'previous_page': self.page.number - 1 if self.page.number != 1 else None,
+ 'current_page': self.page.number,
+ 'next_page': self.page.number + 1 if self.page.has_next() else None,
+ 'page_size': self.page_size
+ },
+ 'count': self.page.paginator.count,
+ 'total_pages': self.page.paginator.num_pages,
+ 'next': self.page.has_next(),
+ 'previous': self.page.has_previous(),
+ 'results': data
+ })
+```
+
+## SimplePagination
+
+```python
+class SimplePagination(pagination.PageNumberPagination):
+ """
+ Custom paginator for REST API responses
+ """
+ def get_paginated_response(self, data):
+ return Response({
+ 'records_filtered': self.page.paginator.count,
+ 'data': data
+ })
+```
+
+# Permissions
+
+A small set of permissions are set in the app to make sure the data is safer and secure and those
+can be also extended.
+
+## AccessMixin
+
+Base class of all permission mixins of Django Messages DRF. Adds an extension for the permissions of
+Django Rest Framework where you can now append into a list instead of repeating on every class.
+
+```python
+class AccessMixin(metaclass=DjangoMessageDRFAuthMeta):
+ """
+ Django rest framework doesn't append permission_classes on inherited models which can cause
+ issues when it comes to call an API programmatically, this way we create a metaclass that will
+ read from a property custom from our subclasses and will append to the default
+ `permission_classes` on the subclasses of AccessMixin.
+ """
+ pass
+```
+
+## DjangoMessageDRFAuthMixin
+
+Base class of all views of the application and sets the principle that every view inheriting from
+this will validate the user authentication.
+
+```python
+class DjangoMessageDRFAuthMixin(AccessMixin, APIView):
+ """
+ Base APIView requiring login credentials to access it from the inside of the platform
+ Or via request (if known)
+ """
+ permissions = [IsAuthenticated]
+ pagination_class = None
+
+ def __init__(self, *args, **kwargs) -> None:
+ """
+ Checks if the views contain the `permissions` attribute and overrides the
+ `permission_classes`.
+ """
+ super().__init__(*args, **kwargs)
+ self.permission_classes = self.permissions
+ if self.pagination_class:
+ try:
+ rest_settings = settings.REST_FRAMEWORK
+ except AttributeError:
+ rest_settings = {}
+ page_size = rest_settings.get('PAGE_SIZE', 50)
+ self.pagination_class.page_size = page_size
+
+```
+
+## Examples
+
+Using the **`DjangoMessageDRFAuthMixin`** as a base we can now start creating our own views without
+thinking about replicating the `permission_classes`.
+
+### With DjangoMessageDRFAuthMixin
+
+```python
+from rest_framework.views import APIView
+
+from django_messages_drf.permissions import DjangoMessageDRFAuthMixin
+from my_app.permissions import MyPermission
+
+
+class MyCustomView(DjangoMessageDRFAuthMixin, APiView):
+ """
+ My Custom view that will do things
+ """
+ permissions = [MyPermission]
+
+```
+
+Importing the `APIView` is optional since the `DjangoMessageDRFAuthMixin` already implements it.
+
+Behind the scenes, Django Messages DRF is appending the `permissions` to `permission_classes` of
+Django Rest Framework, which means that if we query for the `permission_classes` we would have:
+
+```shell
+permission_classes = [IsAuthenticated, MyPermission]
+```
+
+### Without DjangoMessageDRFAuthMixin
+
+```python
+from rest_framework.views import APIView
+
+from rest_framework.permissions import IsAuthenticated
+from my_app.permissions import MyPermission
+
+
+class BaseView(APiView):
+ permission_classes = [IsAuthenticated]
+
+
+class MyCustomView(BaseView):
+ """
+ My Custom view that will do things
+ """
+ permission_classes = [MyPermission]
+
+```
+
+This won't have the same result as the `DjangoMessageDRFAuthMixin` because what is doing is actually
+reassigning the `permission_classes` from the `BaseView` to the `MyCustomView`.
+
+# Serializers
+
+Django Messages DRF like with the views, also comes with a set of serializers that allows you
+to apply in your project but you can and should build your own with your own use cases.
+
+The way the serializers are built are the default ones from Django Rest Framework.
+
+## Inbox
+
+A simple example for an inbox serializer.
+
+```python
+class InboxSerializer(serializers.ModelSerializer):
+ """
+ Serializer for the inbox.
+ """
+ sent_at = serializers.DateTimeField(source='first_message.sent_at')
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.useruser = self.context.get('user')
+
+ class Meta:
+ model = Thread
+ fields = ('uuid', 'subject', 'sent_at')
+
+```
+
+## Sender
+
+A sender for Django Messages DRF is a Django **`user`** and can be whatever you decided that u.
+
+```python
+class SenderSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = get_user_model()
+ fields = ('first_name', 'last_name', 'email')
+```
+
+# Serializer Settings
+
+Django Messages DRF allows overriding some settings for the views, which means, instead of creating
+a new view just to apply your own serializer, you can simply override the setting and
+Django Messages DRF will apply it on the current views.
+
+None of the below settings are required to be added to your `settings.py`. This is only if
+you wish to override the current defaults.
+
+## Overriding
+
+In your **`settings.py`**.
+
+| Setting Name | View | Default |
+| :---------------------------------------------- | :----------------- | :-------------------- |
+| **DJANGO_MESSAGES_DRF_INBOX_SERIALIZER** | InboxListApiView | InboxSerializer |
+| **DJANGO_MESSAGES_DRF_THREAD_SERIALIZER** | ThreadListApiView | ThreadSerializer |
+| **DJANGO_MESSAGES_DRF_MESSAGE_SERIALIZER** | ThreadCRUDApiView | ThreadReplySerializer |
+| **DJANGO_MESSAGES_DRF_EDIT_MESSAGE_SERIALIZER** | EditMessageApiView | EditMessageSerializer |
+
+## Usage
+
+Overriding is based on `import_string` from your **`settings.py`**.
+
+### Examples
+
+```python
+# settings.py
+
+DJANGO_MESSAGES_DRF_INBOX_SERIALIZER = 'myapp.serializers.MyCustomInboxSerializer'
+DJANGO_MESSAGES_DRF_THREAD_SERIALIZER = 'myapp.serializers.MyCustomThreadSerializer'
+```
+
+If none of the settings is overridden or is **`None`** , then it will default to the original.
+
+# Behaviour Settings
+
+Django Messages DRF allows overriding some behaviours.
+
+## Overriding
+
+In your **`settings.py`**.
+
+| Setting Name | Behaviour | Type | Default |
+| :----------------------------------------------------- | :------------------------------------- | :------ | :------ |
+| **DJANGO_MESSAGES_MARK_NEW_THREAD_MESSAGE_AS_DELETED** | Mark the first message sent as deleted | Boolean | True |
+
+# Utils
+
+Some useful utils are provided with the project to make it easier to reuse across.
+
+## AuditModel
+
+```python
+class AuditModel(models.Model):
+ """A common audit model for tracking"""
+ created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True)
+ modified_at = models.DateTimeField(null=False, blank=False, auto_now=True)
+
+```
+
+Adding the **`AuditModel`** to a model will add an audit trailing to it making it easier
+to filter by dates.
+
+This can be extended and add more information such as **`created_by`** or **`modified_by`**
+where those are users of the application.
+
+# Signals
+
+## Message sent
+
+We only provide one signal at the moment. This signal fires off after every message.
+
+```python
+message_sent = Signal(providing_args=["message", "thread", "reply"])
+```
+
+
+
+# Release Notes
+
+## 1.0.6
+
+- Preparing to drop support for python 3.6.
+- Fix `providing_args` from signals as it is deprecated in Django 4.
+
+## 1.0.5
+
+- Added `id` field to the ThreadSerializer.
+
+## 1.0.4
+
+- [Bugfix #10](https://github.com/tarsil/django-messages-drf/pull/10). Thank you [kamikaz1k](https://github.com/kamikaz1k)
+- [Bugfix #9](https://github.com/tarsil/django-messages-drf/pull/9). Thank you [kamikaz1k](https://github.com/kamikaz1k)
+
+## 1.0.3
+
+### Added
+
+- Settings to override the serializers on the views by using a custom.
+- `EditMessageApiView` allowing editing a message sent from a user of a given thread.
+- `CurrentThreadDefault` similar to `CurrentUserDefault` from Django Rest Framework but for threads.
+
+### Fixed
+
+- Show sender when a message sent is from the same sender and receiver - [Issue](https://github.com/tarsil/django-messages-drf/issues/5)
+- Issue with `display_name` for InboxSerializer - [Issue](https://github.com/tarsil/django-messages-drf/issues/4).
+- `ThreadCRUDApiView` `post` where wasn't using the data from the serializer.
+
+## 1.0.2
+
+### Added
+
+- Support for python 3.9
+- CircleCI config
+
+### Fixed
+
+- Tests naming conflicts.
+- Migration issues.
+
+### Updated
+
+- README.
+
+## 1.0.0
+
+- Initial release
+
+## License
+
+Copyright (c) 2020-present Tiago Silva and contributors under the [MIT license](https://opensource.org/licenses/MIT).
diff --git a/docs/source/javascripts/all.js b/docs/source/javascripts/all.js
new file mode 100644
index 0000000..5f5d406
--- /dev/null
+++ b/docs/source/javascripts/all.js
@@ -0,0 +1,2 @@
+//= require ./all_nosearch
+//= require ./app/_search
diff --git a/docs/source/javascripts/all_nosearch.js b/docs/source/javascripts/all_nosearch.js
new file mode 100644
index 0000000..026e5a2
--- /dev/null
+++ b/docs/source/javascripts/all_nosearch.js
@@ -0,0 +1,27 @@
+//= require ./lib/_energize
+//= require ./app/_copy
+//= require ./app/_toc
+//= require ./app/_lang
+
+function adjustLanguageSelectorWidth() {
+ const elem = $('.dark-box > .lang-selector');
+ elem.width(elem.parent().width());
+}
+
+$(function() {
+ loadToc($('#toc'), '.toc-link', '.toc-list-h2', 10);
+ setupLanguages($('body').data('languages'));
+ $('.content').imagesLoaded( function() {
+ window.recacheHeights();
+ window.refreshToc();
+ });
+
+ $(window).resize(function() {
+ adjustLanguageSelectorWidth();
+ });
+ adjustLanguageSelectorWidth();
+});
+
+window.onpopstate = function() {
+ activateLanguage(getLanguageFromQueryString());
+};
diff --git a/docs/source/javascripts/app/_copy.js b/docs/source/javascripts/app/_copy.js
new file mode 100644
index 0000000..4dfbbb6
--- /dev/null
+++ b/docs/source/javascripts/app/_copy.js
@@ -0,0 +1,15 @@
+function copyToClipboard(container) {
+ const el = document.createElement('textarea');
+ el.value = container.textContent.replace(/\n$/, '');
+ document.body.appendChild(el);
+ el.select();
+ document.execCommand('copy');
+ document.body.removeChild(el);
+}
+
+function setupCodeCopy() {
+ $('pre.highlight').prepend('');
+ $('.copy-clipboard').on('click', function() {
+ copyToClipboard(this.parentNode.children[1]);
+ });
+}
diff --git a/docs/source/javascripts/app/_lang.js b/docs/source/javascripts/app/_lang.js
new file mode 100644
index 0000000..cc5ac8b
--- /dev/null
+++ b/docs/source/javascripts/app/_lang.js
@@ -0,0 +1,171 @@
+//= require ../lib/_jquery
+
+/*
+Copyright 2008-2013 Concur Technologies, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may
+not use this file except in compliance with the License. You may obtain
+a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations
+under the License.
+*/
+;(function () {
+ 'use strict';
+
+ var languages = [];
+
+ window.setupLanguages = setupLanguages;
+ window.activateLanguage = activateLanguage;
+ window.getLanguageFromQueryString = getLanguageFromQueryString;
+
+ function activateLanguage(language) {
+ if (!language) return;
+ if (language === "") return;
+
+ $(".lang-selector a").removeClass('active');
+ $(".lang-selector a[data-language-name='" + language + "']").addClass('active');
+ for (var i=0; i < languages.length; i++) {
+ $(".highlight.tab-" + languages[i]).hide();
+ $(".lang-specific." + languages[i]).hide();
+ }
+ $(".highlight.tab-" + language).show();
+ $(".lang-specific." + language).show();
+
+ window.recacheHeights();
+
+ // scroll to the new location of the position
+ if ($(window.location.hash).get(0)) {
+ $(window.location.hash).get(0).scrollIntoView(true);
+ }
+ }
+
+ // parseURL and stringifyURL are from https://github.com/sindresorhus/query-string
+ // MIT licensed
+ // https://github.com/sindresorhus/query-string/blob/7bee64c16f2da1a326579e96977b9227bf6da9e6/license
+ function parseURL(str) {
+ if (typeof str !== 'string') {
+ return {};
+ }
+
+ str = str.trim().replace(/^(\?|#|&)/, '');
+
+ if (!str) {
+ return {};
+ }
+
+ return str.split('&').reduce(function (ret, param) {
+ var parts = param.replace(/\+/g, ' ').split('=');
+ var key = parts[0];
+ var val = parts[1];
+
+ key = decodeURIComponent(key);
+ // missing `=` should be `null`:
+ // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
+ val = val === undefined ? null : decodeURIComponent(val);
+
+ if (!ret.hasOwnProperty(key)) {
+ ret[key] = val;
+ } else if (Array.isArray(ret[key])) {
+ ret[key].push(val);
+ } else {
+ ret[key] = [ret[key], val];
+ }
+
+ return ret;
+ }, {});
+ };
+
+ function stringifyURL(obj) {
+ return obj ? Object.keys(obj).sort().map(function (key) {
+ var val = obj[key];
+
+ if (Array.isArray(val)) {
+ return val.sort().map(function (val2) {
+ return encodeURIComponent(key) + '=' + encodeURIComponent(val2);
+ }).join('&');
+ }
+
+ return encodeURIComponent(key) + '=' + encodeURIComponent(val);
+ }).join('&') : '';
+ };
+
+ // gets the language set in the query string
+ function getLanguageFromQueryString() {
+ if (location.search.length >= 1) {
+ var language = parseURL(location.search).language;
+ if (language) {
+ return language;
+ } else if (jQuery.inArray(location.search.substr(1), languages) != -1) {
+ return location.search.substr(1);
+ }
+ }
+
+ return false;
+ }
+
+ // returns a new query string with the new language in it
+ function generateNewQueryString(language) {
+ var url = parseURL(location.search);
+ if (url.language) {
+ url.language = language;
+ return stringifyURL(url);
+ }
+ return language;
+ }
+
+ // if a button is clicked, add the state to the history
+ function pushURL(language) {
+ if (!history) { return; }
+ var hash = window.location.hash;
+ if (hash) {
+ hash = hash.replace(/^#+/, '');
+ }
+ history.pushState({}, '', '?' + generateNewQueryString(language) + '#' + hash);
+
+ // save language as next default
+ if (localStorage) {
+ localStorage.setItem("language", language);
+ }
+ }
+
+ function setupLanguages(l) {
+ var defaultLanguage = null;
+ if (localStorage) {
+ defaultLanguage = localStorage.getItem("language");
+ }
+
+ languages = l;
+
+ var presetLanguage = getLanguageFromQueryString();
+ if (presetLanguage) {
+ // the language is in the URL, so use that language!
+ activateLanguage(presetLanguage);
+
+ if (localStorage) {
+ localStorage.setItem("language", presetLanguage);
+ }
+ } else if ((defaultLanguage !== null) && (jQuery.inArray(defaultLanguage, languages) != -1)) {
+ // the language was the last selected one saved in localstorage, so use that language!
+ activateLanguage(defaultLanguage);
+ } else {
+ // no language selected, so use the default
+ activateLanguage(languages[0]);
+ }
+ }
+
+ // if we click on a language tab, activate that language
+ $(function() {
+ $(".lang-selector a").on("click", function() {
+ var language = $(this).data("language-name");
+ pushURL(language);
+ activateLanguage(language);
+ return false;
+ });
+ });
+})();
diff --git a/docs/source/javascripts/app/_search.js b/docs/source/javascripts/app/_search.js
new file mode 100644
index 0000000..0b0ccd9
--- /dev/null
+++ b/docs/source/javascripts/app/_search.js
@@ -0,0 +1,102 @@
+//= require ../lib/_lunr
+//= require ../lib/_jquery
+//= require ../lib/_jquery.highlight
+;(function () {
+ 'use strict';
+
+ var content, searchResults;
+ var highlightOpts = { element: 'span', className: 'search-highlight' };
+ var searchDelay = 0;
+ var timeoutHandle = 0;
+ var index;
+
+ function populate() {
+ index = lunr(function(){
+
+ this.ref('id');
+ this.field('title', { boost: 10 });
+ this.field('body');
+ this.pipeline.add(lunr.trimmer, lunr.stopWordFilter);
+ var lunrConfig = this;
+
+ $('h1, h2').each(function() {
+ var title = $(this);
+ var body = title.nextUntil('h1, h2');
+ lunrConfig.add({
+ id: title.prop('id'),
+ title: title.text(),
+ body: body.text()
+ });
+ });
+
+ });
+ determineSearchDelay();
+ }
+
+ $(populate);
+ $(bind);
+
+ function determineSearchDelay() {
+ if (index.tokenSet.toArray().length>5000) {
+ searchDelay = 300;
+ }
+ }
+
+ function bind() {
+ content = $('.content');
+ searchResults = $('.search-results');
+
+ $('#input-search').on('keyup',function(e) {
+ var wait = function() {
+ return function(executingFunction, waitTime){
+ clearTimeout(timeoutHandle);
+ timeoutHandle = setTimeout(executingFunction, waitTime);
+ };
+ }();
+ wait(function(){
+ search(e);
+ }, searchDelay);
+ });
+ }
+
+ function search(event) {
+
+ var searchInput = $('#input-search')[0];
+
+ unhighlight();
+ searchResults.addClass('visible');
+
+ // ESC clears the field
+ if (event.keyCode === 27) searchInput.value = '';
+
+ if (searchInput.value) {
+ var results = index.search(searchInput.value).filter(function(r) {
+ return r.score > 0.0001;
+ });
+
+ if (results.length) {
+ searchResults.empty();
+ $.each(results, function (index, result) {
+ var elem = document.getElementById(result.ref);
+ searchResults.append("