If you have a bug/issue with the commenting system, please send me an email (my email is in the "About" section).
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | {% else %}
89 |
90 |
Comments are disabled for this article
91 |
92 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject commentator "0.18.0"
2 | :description "A Free commenting system"
3 | :url "https://github.com/mcorbin/commentator"
4 | :license {:name "EPL-2.0"
5 | :url "https://www.eclipse.org/legal/epl-2.0/"}
6 | :dependencies [[amazonica "0.3.156"
7 | :exclusions
8 | [com.amazonaws/aws-java-sdk
9 | com.amazonaws/amazon-kinesis-client]]
10 | [com.amazonaws/aws-java-sdk-core "1.12.128"]
11 | [com.amazonaws/aws-java-sdk-s3 "1.12.128"]
12 | [fr.mcorbin/corbihttp "0.30.0"]
13 | [org.clojure/clojure "1.10.3"]
14 | [org.clojure/core.cache "1.0.225"]]
15 | :main ^:skip-aot commentator.core
16 | :target-path "target/%s"
17 | :source-paths ["src"]
18 | :profiles {:dev {:dependencies [[org.clojure/tools.namespace "1.2.0"]
19 | [pjstadig/humane-test-output "0.11.0"]
20 | [tortue/spy "2.9.0"]
21 | [ring/ring-mock "0.4.0"]]
22 | :global-vars {*assert* true}
23 | :env {:commentator-configuration "dev/resources/config.edn"}
24 | :plugins [[lein-environ "1.1.0"]
25 | [lein-cloverage "1.1.1"]
26 | [lein-ancient "0.6.15"]
27 | [lein-cljfmt "0.6.6"]]
28 | :injections [(require 'pjstadig.humane-test-output)
29 | (pjstadig.humane-test-output/activate!)]
30 | :repl-options {:init-ns user}
31 | :source-paths ["dev"]}
32 | :uberjar {:aot :all}}
33 | :test-selectors {:default (fn [x] (not (:integration x)))
34 | :integration :integration})
35 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | set -e
2 | tag=$1
3 | lein test
4 |
5 | git add .
6 | git commit -m "release ${tag}"
7 | git tag -a "${tag}" -m "release ${tag}"
8 | docker build -t mcorbin/commentator:${tag} .
9 | docker push mcorbin/commentator:${tag}
10 | git push --tags
11 | git push
12 |
--------------------------------------------------------------------------------
/site/commentator/.hugo_build.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcorbin/commentator/25093372506d2f0e598f689e3f9e393e49f16732/site/commentator/.hugo_build.lock
--------------------------------------------------------------------------------
/site/commentator/archetypes/default.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "{{ replace .Name "-" " " | title }}"
3 | date: {{ .Date }}
4 | draft: true
5 | ---
6 |
7 |
--------------------------------------------------------------------------------
/site/commentator/config.toml:
--------------------------------------------------------------------------------
1 | baseURL = "https://www.commentator.mcorbin.fr"
2 | languageCode = "en-us"
3 | title = "Commentator"
4 | theme = "hugo-theme-learn"
5 |
6 | [params]
7 | themeVariant = "commentator"
8 | disableInlineCopyToClipBoard = true
9 |
10 |
11 | [[menu.shortcuts]]
12 | name = " Github repo"
13 | identifier = "ds"
14 | url = "https://github.com/mcorbin/commentator"
15 | weight = 10
16 |
17 | [[menu.shortcuts]]
18 | name = " Site theme"
19 | identifier = "learntheme"
20 | url = "https://github.com/matcornic/hugo-theme-learn/"
21 | weight = 20
--------------------------------------------------------------------------------
/site/commentator/content/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Commentator
3 | weight: 30
4 | chapter: false
5 | ---
6 |
7 | # Commentator, a free commenting system for your blog
8 |
9 | Commentator is a simple application which provide all you need to have a powerful commenting system on your blogs or websites.
10 |
11 | Commentator uses json files (one per article) stored on any S3-compatible storage to store your comment. The rest of the application is completely stateless.
12 |
13 | Using a S3 compatible store as a database provide several advantages:
14 |
15 | - Easy to use: you don't have to setup a SQL database for example.
16 | - Highly available storage.
17 | - You can use any S3 tools (s3cmd for example) to interact directly with your comments if you want to.
18 |
19 | Commentator also provides:
20 |
21 | - Multi site support: one instance of Commentator can manage comments for multiple websites.
22 | - An easy-to-use API to manage comments and events. A public API allows users to create or retrieve approved comments, and a admin API allows you to administrate comments (approve them or delete them for example).
23 | Everytime a comment is added an event is generated into a dedicated file on S3. The API allows you to read and delete the events. You can use this file to be notified when a new comment is added.
24 | - Rate limiting, either by IP or using the requests `x-forwarded-for` header.
25 | - A "challenge" system to avoid spammers.
26 | - An in-memory cache for comments, for performances and to avoid hitting S3 too much.
27 | - Metrics about the applications exposed using the Prometheus format.
28 |
29 | 
30 |
--------------------------------------------------------------------------------
/site/commentator/content/api/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: API
3 | weight: 20
4 | chapter: false
5 | ---
6 |
7 | The Commentator API is used to manage comments and events.
8 |
9 | Some API calls (to approve or delete comments for exemple) are only availables for admin users.
10 |
11 | Admin users should provide the token (defined in the [configuration](/howto/configuration/) in the `Authorization` header.
12 |
13 | New ways of authenticating admin users may be added in the future.
14 |
--------------------------------------------------------------------------------
/site/commentator/content/api/comments/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Comments
3 | weight: 1
4 | disableToc: false
5 | ---
6 |
7 | # Public API
8 |
9 | ## Add a comment
10 |
11 | - **POST** `/api/v1/comment//`
12 |
13 |
14 | | Field | Type | Description |
15 | | ------ | ----------- | ----------- |
16 | | author | string | The author's name |
17 | | author-website | string | The author's website (optional) |
18 | | content | string | The comment content |
19 | | signature | string | The challenge signature |
20 | | timestamp | string | The challenge timestamp |
21 | | answer | string | The challenge answer |
22 |
23 | ---
24 |
25 | ```
26 | curl -X POST --header "Content-Type: application/json" --data \ '{"author":"mcorbin", "author-website": "mcorbin.fr", "content":"My comment","challenge":"c1","answer":"5", "signature": "", "timestamp": 1639688441946}' http://localhost:8787/api/v1/comment/mcorbin-fr/foo
27 |
28 | {"message":"Comment added"}
29 | ```
30 |
31 | ## List comments for an article
32 |
33 | - **GET** `/api/v1/comment//`
34 |
35 | ---
36 |
37 | ```
38 | curl http://localhost:8787/api/v1/comment/mcorbin-fr/foo
39 |
40 | [
41 | {
42 | "content": "My comment",
43 | "author": "mcorbin",
44 | "author-website": "mcorbin.fr",
45 | "id": "0bfe788b-3a06-47fe-8a75-4a3ec6a3b8d9",
46 | "approved": true,
47 | "timestamp": 1629101319712
48 | }
49 | ]
50 |
51 | ```
52 |
53 | ## Get a random challenge
54 |
55 | - **GET** `/api/v1/challenge/`
56 |
57 | ---
58 |
59 | ```
60 | curl http://localhost:8787/api/v1/challenge/mcorbin-fr/foo
61 | {"timestamp": 1629101319712,"question":"1 + 9 = ?", "signature": ""}
62 | ```
63 |
64 | # Admin API
65 |
66 | ## Get a comment
67 |
68 | - **GET** `/api/admin/comment///`
69 |
70 | ---
71 |
72 | ```
73 | curl -H "Authorization: my-super-token" http://localhost:8787/api/admin/comment/mcorbin-fr/foo/0bfe788b-3a06-47fe-8a75-4a3ec6a3b8d9
74 |
75 | {
76 | "content": "My comment",
77 | "author": "mcorbin",
78 | "author-website": "mcorbin.fr",
79 | "id": "0bfe788b-3a06-47fe-8a75-4a3ec6a3b8d9",
80 | "approved": false,
81 | "timestamp": 1629101319712
82 | }
83 | ```
84 |
85 | ## List comments for an article
86 |
87 | - **GET** `/api/admin/comment//`
88 |
89 | ---
90 |
91 | ```
92 | curl -H "Authorization: my-super-token" http://localhost:8787/api/admin/comment/mcorbin-fr/foo
93 | [
94 | {
95 | "content": "My comment",
96 | "author": "mcorbin",
97 | "author-website": "mcorbin.fr",
98 | "id": "0bfe788b-3a06-47fe-8a75-4a3ec6a3b8d9",
99 | "approved": false,
100 | "timestamp": 1629101319712
101 | }
102 | ]
103 | ```
104 |
105 | ## Approve a comment
106 |
107 | - **POST** `/api/admin/comment///`
108 |
109 | ---
110 |
111 | ```
112 | curl -X POST -H "Authorization: my-super-token" http://localhost:8787/api/admin/comment/mcorbin-fr/foo/0bfe788b-3a06-47fe-8a75-4a3ec6a3b8d9
113 |
114 | {
115 | "message": "Comment approved"
116 | }
117 | ```
118 |
119 | ## Delete a comment
120 |
121 | - **DELETE** `/api/admin/comment///`
122 |
123 | ---
124 |
125 | ```
126 | curl -X DELETE -H "Authorization: my-super-token" http://localhost:8787/api/admin/comment/mcorbin-fr/foo/0bfe788b-3a06-47fe-8a75-4a3ec6a3b8d9
127 |
128 | {
129 | "message":"Comment deleted"
130 | }
131 | ```
132 |
133 | ## Delete all comments for an article
134 |
135 | - **DELETE** `/api/admin/comment//`
136 |
137 | ---
138 |
139 | ```
140 | curl -X DELETE -H "Authorization: my-super-token" http://localhost:8787/api/admin/comment/mcorbin-fr/foo
141 |
142 | {
143 | "message":"Comments deleted"
144 | }
145 | ```
146 |
--------------------------------------------------------------------------------
/site/commentator/content/api/events/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Events
3 | weight: 1
4 | disableToc: false
5 | ---
6 |
7 | **Admin API**
8 |
9 | ## List events
10 |
11 | - **GET** `/api/admin/event/`
12 |
13 | ---
14 |
15 | ```
16 | curl -H "Authorization: my-super-token" http://localhost:8787/api/admin/event/mcorbin-fr
17 |
18 | [
19 | {
20 | "timestamp": 1628364816474,
21 | "id": "01970a27-eb5f-4ee5-97af-a5a11a427824",
22 | "article": "foo",
23 | "message": "New comment 67f99f31-9724-4288-94d7-4fc860dab744 on article foo",
24 | "comment-id": "67f99f31-9724-4288-94d7-4fc860dab744",
25 | "type": "new-comment"
26 | }
27 | ]
28 | ```
29 |
30 | ## Delete an event
31 |
32 | - **DELETE** `/api/admin/event/`
33 |
34 | ---
35 |
36 | ```
37 | curl -H "Authorization: my-super-token" http://localhost:8787/api/admin/event/mcorbin-fr
38 |
39 | [
40 | {
41 | "timestamp": 1628364816474,
42 | "id": "01970a27-eb5f-4ee5-97af-a5a11a427824",
43 | "article": "foo",
44 | "message": "New comment 67f99f31-9724-4288-94d7-4fc860dab744 on article foo",
45 | "comment-id": "67f99f31-9724-4288-94d7-4fc860dab744",
46 | "type": "new-comment"
47 | }
48 | ]
49 | ```
50 |
--------------------------------------------------------------------------------
/site/commentator/content/api/monitoring/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Monitoring
3 | weight: 30
4 | disableToc: false
5 | ---
6 |
7 | ## Healthz
8 |
9 | - **GET** `/healthz`
10 |
11 | ---
12 |
13 | ```
14 | curl localhost:8787/healthz
15 |
16 | {"message":"ok"}
17 | ```
18 |
19 | ## Metrics
20 |
21 | You can [configure](howto/configuration/) Commentator to expose metrics using the Prometheus format on a dedicated port.
22 |
--------------------------------------------------------------------------------
/site/commentator/content/howto/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: How to
3 | weight: 1
4 | chapter: true
5 | ---
6 |
7 | # How to use Commentator
8 |
--------------------------------------------------------------------------------
/site/commentator/content/howto/configuration/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Configuration
3 | weight: 10
4 | disableToc: false
5 | ---
6 |
7 | The Commentator configuration is defined in [EDN](https://github.com/edn-format/edn).
8 | Here is a commented example configuration file:
9 |
10 | ```clojure
11 | {;; The Mirabelle HTTP Server.
12 | ;; The key, cert, and cacert optional parameters can be set to enable TLS (the
13 | ;; parameters are path to the cert files)
14 | :http {:host "127.0.0.1"
15 | :port 8787}
16 | ;; The list of allowed origins for the public API.
17 | :allow-origin ["https://www.mcorbin.fr"]
18 | ;; The admin token for the admin API calls
19 | :admin {:token #secret "my-super-token"}
20 | ;; The number of minutes for an user before being able to publish another comment
21 | ;; on Commentator.
22 | ;; The `x-forwarded-for` header is first used to get the user IP
23 | ;; If the header is not set it fallbacks to the request source IP.
24 | :rate-limit-minutes 5
25 | :store {;; Your s3 access/secret keys
26 | ;; In this example the #env reader is used but you can also
27 | ;; specify the values without using environment variables if you
28 | ;; want to
29 | :access-key #secret #env ACCESS_KEY
30 | :secret-key #secret #env SECRET_KEY
31 | ;; the prefix for the buckets used by Commentator to store comments
32 | ;; and events
33 | :bucket-prefix "commentator-dev-"
34 | ;; The S3 endpoint
35 | :endpoint "https://sos-ch-gva-2.exo.io"}
36 | :comment {;; Set to true if you want to have comments automatically approved once
37 | ;; created
38 | :auto-approve false
39 | ;; A map containing for each website a list of articles which can receive
40 | ;; comments. You can use this map to disable comments for an article
41 | ;; for example.
42 | :allowed-articles {"mcorbin-fr" ["foo"
43 | "bar"]}}
44 | ;; Logging configuration (https://github.com/pyr/unilog)
45 | :logging {:level "info"
46 | :console {:encoder "json"}
47 | :overrides {:org.eclipse.jetty "info"
48 | :org.apache.http "error"}}
49 | ;; The prometheus configuration to expose the metrics.
50 | ;; The key, cert, and cacert optional parameters can be set to enable TLS (the
51 | ;; parameters are path to the cert files)
52 | :prometheus {:host "127.0.0.1"
53 | :port 8788}
54 | ;; Challenges configurations to avoid spammers
55 | ;; See the different modes to configure challenges on https://www.commentator.mcorbin.fr/howto/use-it/
56 | :challenges {:type :math
57 | :ttl 120
58 | :secret #secret "azizjiuzarhuaizhaiuzr"}}
59 | ```
60 |
61 | Commentator can be used to store comments for multiple websites.
62 |
63 | The `:allowed-articles` key should contain the list of articles open for comments for each website. The website names and articles should match the ones used in the [API](/api/comments/). You can also check the [use it](/howto/use-it/) section of the documentation for more information about this.
64 |
65 | Each website will have a dedicated bucket to store its comments and events.
66 |
67 | The bucket name will be ``. The bucket prefix should be a string between 1 and 19 characters, and the website a string between 1 and 39 characters. Allowed characters are letters (both uppercase and lowercase), number, `_` and `-`.
68 |
--------------------------------------------------------------------------------
/site/commentator/content/howto/get-build/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Get and launch Commentator
3 | weight: 1
4 | disableToc: false
5 | ---
6 |
7 | ## Get Commentator jar file
8 |
9 | You can download the Commentator jar from [Github](https://github.com/mcorbin/commentator/releases). Commentator is tested under Java 17 (LTS).
10 |
11 | You can also build Commentator yourself. You will need to do that:
12 |
13 | - [Leiningen](https://leiningen.org/), the Clojure build tool.
14 | - Java 11.
15 |
16 | Then, clone the [Git repository](https://github.com/mcorbin/commentator). You can now build the project with `lein uberjar`.
17 |
18 | The resulting jar will be in `target/uberjar/commentator--standalone.jar`
19 |
20 | ## Launch Commentator
21 |
22 | Commentator needs a [configuration file](/howto/configuration/). You need to set the environment variable `COMMENTATOR_CONFIGURATION` to the path of this file.
23 |
24 | ### Using Docker
25 |
26 | The Docker image is available on the [Docker Hub](https://hub.docker.com/r/mcorbin/commentator).
27 | You will need to mount the configuration file as a volume (or inject it into the running container somehow) in order to make it work.
28 |
--------------------------------------------------------------------------------
/site/commentator/content/howto/use-it/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Use it
3 | weight: 50
4 | disableToc: false
5 | ---
6 |
7 | Commentator stores the articles comments on a S3 compatible store. +
8 | Let's say we want to store comments for the blog `mcorbin.fr` for example.
9 |
10 | The first thing to do is to write the Commentator [configuration file](/howto/configuration/).
11 |
12 | As explained in the linked section of the documentation, several things are important.
13 |
14 | **Allow origin**
15 |
16 | The same Commentator instance can serve comments for several websites. You need to configure the list of websites in the `:allow-origin` section.
17 |
18 | **Rate limit**
19 |
20 | The `:rate-limit-minutes` option prevents users to create more than one comment every N minutes.
21 |
22 | The `x-forwarded-for` header is first used to get the user IP. If the header is not set it fallbacks to the request source IP.
23 |
24 | **Comment**
25 |
26 | Comments can be automatically approved by setting the `:auto-approve` value to `true`. By default, comments not approved by an administrator are not displayed.
27 |
28 | You also need to configure the `allowed-articles` key. It contains a map containing for each website the list of articles allowed to receive comments. For example:
29 |
30 | ```clojure
31 | {"mcorbin-fr" ["my-first-article"
32 | "my-second-article"]}
33 | ```
34 |
35 | With this setup, only requests to create comments on the `/api/v1/comment//` path will be allowed, where the possible value for `` is "mcorbin-fr" and the possible values for `` are "my-first-article" and "my-second-article".
36 |
37 | You can read the [API documentation](/api/comments/) to understand how it works. It's important to keep in mind that the `allowed-articles` key should match how the API is used later to store and retrieve comments for articles.
38 |
39 | **Challenges**
40 |
41 | Commentator supports a basic challenge system to avoid spammers.
42 |
43 | Commentator provides an [API endpoint](/api/comments/) to return a random challenge. This API endpoint can be used to integrate Commentator on your website. What is returned by the endpoint is a json payload containing:
44 |
45 | - A `question`, a text containing a question
46 | - A `timestamp`
47 | - A `signature`
48 |
49 | When users want to create a comment, they should provide:
50 |
51 | - The `timestamp` and the `signature` provided by the previous endpoint.
52 | - An `answer` field containing the expected answer (case insensitive).
53 |
54 | The `:challenges` key in the configuration can be used to configure challenges. It currently supports two modes.
55 |
56 | ### questions
57 |
58 | ```clojure
59 | {:type :questions
60 | :ttl 120
61 | :secret #secret "azizjiuzarhuaizhaiuzr"
62 | :questions [{:question "1 + 4 = ?"
63 | :answer "5"}
64 | {:question "1 + 9 = ?"
65 | :answer "10"}]}
66 | ```
67 |
68 | We have here two challenges with simple questions about mathematical operations. The questions and answers are totally free, it's up to you to choose what you want to ask.
69 |
70 | You can also easily generate a lot of challenges programmatically if you want to.
71 |
72 | When users want to create a comment, they should provide:
73 |
74 | challenge name and the correct answer. The case of the letters in the answer is not important, everything is compared after being converted to lower case.
75 |
76 | The TTL is the validity duration of the challenge.
77 |
78 | ### math
79 |
80 | ```clojure
81 | {:type :math
82 | :ttl 120
83 | :secret #secret "azizjiuzarhuaizhaiuzr"}
84 | ```
85 |
86 | This challenge will automatically generate simple mathematical challenges. It could for example return a `:question` containing `"what is the result of: 10 + 6"`
87 |
88 | The user should, like in the `questions` challenge, provide the answer, timestamp and signature when creating a comment.
89 |
90 | **Store**
91 |
92 | You should put the store configuration (to access the S3 storage) in the `:store` key.
93 |
94 | The `:bucket-prefix` value will be used as prefix for the buckets storing the comments. For example, if `:bucket-prefix` is set to "commentator-", the bucket will be "commentator-mcorbin-fr" for the `mcorbin-fr` website.
95 |
96 | The buckets should already exist, Commentator will not create them automatically.
97 |
98 | ## Events
99 |
100 | Every time a comment is published, an event is pushed in the website bucket in a file named `events.json`.
101 |
102 | The [API](/api/events/) let you retrieve and delete these events. You can use this file or these endpoints to be notified when a new comment is created.
103 |
104 | ## Integration
105 |
106 | The code I use to integrate Commentator on my personal blog is available on [Github](https://github.com/mcorbin/commentator/blob/master/integration/mcorbin/page.html).
107 |
108 | I'm not a frontend developer. If you have frontend skills and want to write a nice integration, please reach to me on Github.
109 |
--------------------------------------------------------------------------------
/site/commentator/content/support/_index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Support and contributions
3 | weight: 30
4 | chapter: false
5 | ---
6 |
7 |
8 | ## Need help ?
9 |
10 | If you need help, please open issues on [Github](https://github.com/mcorbin/commentator).
11 |
--------------------------------------------------------------------------------
/site/commentator/layouts/partials/logo.html:
--------------------------------------------------------------------------------
1 |
2 |
"},onSelect:function(){}};for(var c in e)e.hasOwnProperty(c)&&(l[c]=e[c]);for(var a="object"==typeof l.selector?[l.selector]:document.querySelectorAll(l.selector),u=0;u0?i.sc.scrollTop=n+i.sc.suggestionHeight+s-i.sc.maxHeight:0>n&&(i.sc.scrollTop=n+s)}else i.sc.scrollTop=0},o(window,"resize",i.updateSC),document.body.appendChild(i.sc),n("autocomplete-suggestion","mouseleave",function(){var e=i.sc.querySelector(".autocomplete-suggestion.selected");e&&setTimeout(function(){e.className=e.className.replace("selected","")},20)},i.sc),n("autocomplete-suggestion","mouseover",function(){var e=i.sc.querySelector(".autocomplete-suggestion.selected");e&&(e.className=e.className.replace("selected","")),this.className+=" selected"},i.sc),n("autocomplete-suggestion","mousedown",function(e){if(t(this,"autocomplete-suggestion")){var o=this.getAttribute("data-val");i.value=o,l.onSelect(e,o,this),i.sc.style.display="none"}},i.sc),i.blurHandler=function(){try{var e=document.querySelector(".autocomplete-suggestions:hover")}catch(t){var e=0}e?i!==document.activeElement&&setTimeout(function(){i.focus()},20):(i.last_val=i.value,i.sc.style.display="none",setTimeout(function(){i.sc.style.display="none"},350))},o(i,"blur",i.blurHandler);var r=function(e){var t=i.value;if(i.cache[t]=e,e.length&&t.length>=l.minChars){for(var o="",s=0;st||t>40)&&13!=t&&27!=t){var o=i.value;if(o.length>=l.minChars){if(o!=i.last_val){if(i.last_val=o,clearTimeout(i.timer),l.cache){if(o in i.cache)return void r(i.cache[o]);for(var s=1;s https://github.com/noelboss/featherlight/issues/317
9 | !function(u){"use strict";if(void 0!==u)if(u.fn.jquery.match(/-ajax/))"console"in window&&window.console.info("Featherlight needs regular jQuery, not the slim version.");else{var r=[],i=function(t){return r=u.grep(r,function(e){return e!==t&&0','
','",'
'+n.loading+"
","
",""].join("")),o="."+n.namespace+"-close"+(n.otherClose?","+n.otherClose:"");return n.$instance=i.clone().addClass(n.variant),n.$instance.on(n.closeTrigger+"."+n.namespace,function(e){if(!e.isDefaultPrevented()){var t=u(e.target);("background"===n.closeOnClick&&t.is("."+n.namespace)||"anywhere"===n.closeOnClick||t.closest(o).length)&&(n.close(e),e.preventDefault())}}),this},getContent:function(){if(!1!==this.persist&&this.$content)return this.$content;var t=this,e=this.constructor.contentFilters,n=function(e){return t.$currentTarget&&t.$currentTarget.attr(e)},r=n(t.targetAttr),i=t.target||r||"",o=e[t.type];if(!o&&i in e&&(o=e[i],i=t.target&&r),i=i||n("href")||"",!o)for(var a in e)t[a]&&(o=e[a],i=t[a]);if(!o){var s=i;if(i=null,u.each(t.contentFilters,function(){return(o=e[this]).test&&(i=o.test(s)),!i&&o.regex&&s.match&&s.match(o.regex)&&(i=s),!i}),!i)return"console"in window&&window.console.error("Featherlight: no content filter found "+(s?' for "'+s+'"':" (no target specified)")),!1}return o.process.call(t,i)},setContent:function(e){return this.$instance.removeClass(this.namespace+"-loading"),this.$instance.toggleClass(this.namespace+"-iframe",e.is("iframe")),this.$instance.find("."+this.namespace+"-inner").not(e).slice(1).remove().end().replaceWith(u.contains(this.$instance[0],e[0])?"":e),this.$content=e.addClass(this.namespace+"-inner"),this},open:function(t){var n=this;if(n.$instance.hide().appendTo(n.root),!(t&&t.isDefaultPrevented()||!1===n.beforeOpen(t))){t&&t.preventDefault();var e=n.getContent();if(e)return r.push(n),s(!0),n.$instance.fadeIn(n.openSpeed),n.beforeContent(t),u.when(e).always(function(e){n.setContent(e),n.afterContent(t)}).then(n.$instance.promise()).done(function(){n.afterOpen(t)})}return n.$instance.detach(),u.Deferred().reject().promise()},close:function(e){var t=this,n=u.Deferred();return!1===t.beforeClose(e)?n.reject():(0===i(t).length&&s(!1),t.$instance.fadeOut(t.closeSpeed,function(){t.$instance.detach(),t.afterClose(e),n.resolve()})),n.promise()},resize:function(e,t){if(e&&t&&(this.$content.css("width","").css("height",""),this.$content.parent().width()');return n.onload=function(){r.naturalWidth=n.width,r.naturalHeight=n.height,t.resolve(r)},n.onerror=function(){t.reject(r)},n.src=e,t.promise()}},html:{regex:/^\s*<[\w!][^<]*>/,process:function(e){return u(e)}},ajax:{regex:/./,process:function(e){var n=u.Deferred(),r=u("").load(e,function(e,t){"error"!==t&&n.resolve(r.contents()),n.fail()});return n.promise()}},iframe:{process:function(e){var t=new u.Deferred,n=u(""),r=function(e,t){var n={},r=new RegExp("^"+t+"([A-Z])(.*)");for(var i in e){var o=i.match(r);o&&(n[(o[1]+o[2].replace(/([A-Z])/g,"-$1")).toLowerCase()]=e[i])}return n}(this,"iframe"),i=function(e,t){var n={};for(var r in e)r in t&&(n[r]=e[r],delete e[r]);return n}(r,o);return n.hide().attr("src",e).attr(i).css(r).on("load",function(){t.resolve(n.show())}).appendTo(this.$instance.find("."+this.namespace+"-content")),t.promise()}},text:{process:function(e){return u("
",{text:e})}}},functionAttributes:["beforeOpen","afterOpen","beforeContent","afterContent","beforeClose","afterClose"],readElementConfig:function(e,t){var r=this,i=new RegExp("^data-"+t+"-(.*)"),o={};return e&&e.attributes&&u.each(e.attributes,function(){var e=this.name.match(i);if(e){var t=this.value,n=u.camelCase(e[1]);if(0<=u.inArray(n,r.functionAttributes))t=new Function(t);else try{t=JSON.parse(t)}catch(e){}o[n]=t}}),o},extend:function(e,t){var n=function(){this.constructor=e};return n.prototype=this.prototype,e.prototype=new n,e.__super__=this.prototype,u.extend(e,this,t),e.defaults=e.prototype,e},attach:function(i,o,a){var s=this;"object"!=typeof o||o instanceof u!=!1||a||(a=o,o=void 0);var c,e=(a=u.extend({},a)).namespace||s.defaults.namespace,l=u.extend({},s.defaults,s.readElementConfig(i[0],e),a),t=function(e){var t=u(e.currentTarget),n=u.extend({$source:i,$currentTarget:t},s.readElementConfig(i[0],l.namespace),s.readElementConfig(e.currentTarget,l.namespace),a),r=c||t.data("featherlight-persisted")||new s(o,n);"shared"===r.persist?c=r:!1!==r.persist&&t.data("featherlight-persisted",r),n.$currentTarget.blur&&n.$currentTarget.blur(),r.open(e)};return i.on(l.openTrigger+"."+l.namespace,l.filter,t),{filter:l.filter,handler:t}},current:function(){var e=this.opened();return e[e.length-1]||null},opened:function(){var t=this;return i(),u.grep(r,function(e){return e instanceof t})},close:function(e){var t=this.current();if(t)return t.close(e)},_onReady:function(){var r=this;if(r.autoBind){var i=u(r.autoBind);i.each(function(){r.attach(u(this))}),u(document).on("click",r.autoBind,function(e){if(!e.isDefaultPrevented()){var t=u(e.currentTarget);if(i.length!==(i=i.add(t)).length){var n=r.attach(t);(!n.filter||0";
28 | }
29 | }
30 | });
31 |
32 | // Change styles, depending on parameters set to the image
33 | images.each(function(index){
34 | var image = $(this)
35 | var o = getUrlParameter(image[0].src);
36 | if (typeof o !== "undefined") {
37 | var h = o["height"];
38 | var w = o["width"];
39 | var c = o["classes"];
40 | image.css("width", function() {
41 | if (typeof w !== "undefined") {
42 | return w;
43 | } else {
44 | return "auto";
45 | }
46 | });
47 | image.css("height", function() {
48 | if (typeof h !== "undefined") {
49 | return h;
50 | } else {
51 | return "auto";
52 | }
53 | });
54 | if (typeof c !== "undefined") {
55 | var classes = c.split(',');
56 | for (i = 0; i < classes.length; i++) {
57 | image.addClass(classes[i]);
58 | }
59 | }
60 | }
61 | });
62 |
63 | // Stick the top to the top of the screen when scrolling
64 | $(document).ready(function(){
65 | $("#top-bar").sticky({topSpacing:0, zIndex: 1000});
66 | });
67 |
68 |
69 | jQuery(document).ready(function() {
70 | // Add link button for every
71 | var text, clip = new ClipboardJS('.anchor');
72 | $("h1~h2,h1~h3,h1~h4,h1~h5,h1~h6").append(function(index, html){
73 | var element = $(this);
74 | var url = encodeURI(document.location.origin + document.location.pathname);
75 | var link = url + "#"+element[0].id;
76 | return " " +
77 | "" +
78 | ""
79 | ;
80 | });
81 |
82 | $(".anchor").on('mouseleave', function(e) {
83 | $(this).attr('aria-label', null).removeClass('tooltipped tooltipped-s tooltipped-w');
84 | });
85 |
86 | clip.on('success', function(e) {
87 | e.clearSelection();
88 | $(e.trigger).attr('aria-label', 'Link copied to clipboard!').addClass('tooltipped tooltipped-s');
89 | });
90 | $('code.language-mermaid').each(function(index, element) {
91 | var content = $(element).html().replace(/&/g, '&');
92 | $(element).parent().replaceWith('