├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── Makefile ├── README.md ├── config.json ├── config.preview.json ├── doc └── api.md ├── frontend ├── index.html ├── index.js ├── style.css └── tos.md ├── package.json ├── scripts ├── README.md ├── _pb ├── deploy-static.sh ├── md2html.sh ├── pb ├── pb.fish └── render.js ├── src ├── auth.js ├── common.js ├── cors.js ├── highlight.js ├── index.js ├── markdown.js ├── parseFormdata.js └── staticPages.js ├── test ├── integration.test.js ├── test.env └── test.sh ├── webpack.config.js ├── wrangler.toml └── yarn.lock /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: 5 | - ci-test 6 | - master 7 | workflow_dispatch: 8 | inputs: 9 | logLevel: 10 | description: 'Log level' 11 | required: true 12 | default: 'warning' 13 | 14 | jobs: 15 | deploy: 16 | name: deploy 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: checkout code 20 | uses: actions/checkout@v2 21 | 22 | - name: Get yarn cache directory path 23 | id: yarn-cache-dir-path 24 | run: echo "::set-output name=dir::$(yarn cache dir)" 25 | 26 | - uses: actions/cache@v2 27 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 28 | with: 29 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 30 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 31 | restore-keys: | 32 | ${{ runner.os }}-yarn- 33 | 34 | - name: setup 35 | run: yarn 36 | 37 | - name: build 38 | env: 39 | CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }} 40 | CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} 41 | run: | 42 | mkdir dist 43 | make deploy 44 | 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /dist 3 | **/*.rs.bk 4 | Cargo.lock 5 | bin/ 6 | pkg/ 7 | wasm-pack.log 8 | worker/ 9 | node_modules/ 10 | .cargo-ok 11 | 12 | .idea -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "semi": false, 4 | "trailingComma": "all", 5 | "tabWidth": 2, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Sharzy L 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CONF = config.json 2 | CONF_PREVIEW = config.preview.json 3 | BUILD_DIR = dist 4 | JS_DIR = src 5 | JS_LOCK = yarn.lock 6 | 7 | target_html_files = index.html tos.html 8 | 9 | # script path 10 | html_render_script = scripts/render.js 11 | md2html = scripts/md2html.sh 12 | deploy_static_script = scripts/deploy-static.sh 13 | 14 | # stub directories to record when files are uploaded 15 | RENDERED_DIR = dist/rendered 16 | RENDERED_PREVIEW_DIR = dist/rendered_preview 17 | DEPLOY_DIR = dist/preview 18 | DEPLOY_PREVIEW_DIR = dist/deploy_preview 19 | 20 | rendered_html = $(addprefix $(RENDERED_DIR)/,$(target_html_files)) 21 | rendered_preview_html = $(addprefix $(RENDERED_PREVIEW_DIR)/,$(target_html_files)) 22 | deploy_html = $(addprefix $(DEPLOY_DIR)/, $(target_html_files)) 23 | deploy_preview_html = $(addprefix $(DEPLOY_PREVIEW_DIR)/, $(target_html_files)) 24 | 25 | html: $(all_html) 26 | 27 | test: 28 | ./test/test.sh 29 | 30 | deploy: $(deploy_html) $(source_js_files) $(JS_LOCK) 31 | yarn wrangler publish 32 | 33 | preview: $(deploy_preview_html) $(source_js_files) $(JS_LOCK) 34 | yarn wrangler publish --env preview 35 | 36 | clean: 37 | rm -f $(all_html) $(all_html_deploy) $(all_html_preview) $(js_deploy) $(js_preview) 38 | 39 | $(BUILD_DIR)/tos.html.liquid: frontend/tos.md $(md2html) 40 | mkdir -p $(BUILD_DIR) 41 | $(md2html) $< $@ "Terms and Conditions" 42 | 43 | $(BUILD_DIR)/index.html.liquid: frontend/index.html frontend/index.js frontend/style.css 44 | @# no generation needed, simply copy 45 | mkdir -p $(BUILD_DIR) 46 | cp $< $@ 47 | 48 | # convert liquid template to html file 49 | $(rendered_html): $(RENDERED_DIR)/%.html: $(BUILD_DIR)/%.html.liquid $(CONF) $(html_render_script) 50 | mkdir -p $(dir $@) 51 | node $(html_render_script) -c $(CONF) -o $@ $< 52 | @# remove indents to reduce size 53 | perl -pi -e 's/^\s+//g' $@ 54 | 55 | # convert liquid template to html file 56 | $(rendered_preview_html): $(RENDERED_PREVIEW_DIR)/%.html: $(BUILD_DIR)/%.html.liquid $(CONF_PREVIEW) $(html_render_script) 57 | mkdir -p $(dir $@) 58 | node $(html_render_script) -c $(CONF_PREVIEW) -o $@ $< 59 | @# remove indents to reduce size 60 | perl -pi -e 's/^\s+//g' $@ 61 | 62 | # deploy html file to Cloudflare 63 | $(deploy_html): $(DEPLOY_DIR)/%.html: $(RENDERED_DIR)/%.html $(deploy_static_script) 64 | $(deploy_static_script) $< 65 | @mkdir -p $(dir $@) 66 | @touch $@ 67 | 68 | # deploy html file to Cloudflare preview env 69 | $(deploy_preview_html): $(DEPLOY_PREVIEW_DIR)/%.html: $(RENDERED_PREVIEW_DIR)/%.html $(deploy_static_script) 70 | $(deploy_static_script) --preview $< 71 | @mkdir -p $(dir $@) 72 | @touch $@ 73 | 74 | .PHONY: html test deploy preview clean 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pastebin-worker 2 | 3 | This is a pastebin that can be deployed on Cloudflare workers. It forks from [shz.al](https://github.com/SharzyL/pastebin-worker), thanks for this project. 4 | 5 | Demo: [igdu](https://igdu.cloudns.org/) or [shz.al](https://shz.al) 6 | 7 | 8 | [shz.al](https://github.com/SharzyL/pastebin-worker)'s original guideline should change some command line with the relates app changing, such as wrangler publish do not support or recommend now. You should change the two "wrangler publish" to "wrangler deploy" in makefile. Maybe others should remember, but I forget. 9 | 10 | **Philosophy**: effortless deployment, friendly CLI usage, rich functionality. 11 | 12 | **Features**: 13 | 14 | 1. Share your paste with as short as 4 characters 15 | 2. Customize the paste URL 16 | 4. **Update** and **delete** your paste as you want 17 | 5. **Expire** your paste after a period of time 18 | 6. **Syntax highlighting** powered by PrismJS 19 | 7. Display **markdown** file as HTML 20 | 8. Used as a URL shortener 21 | 9. Customize returned mimetype 22 | 23 | ## Usage methods 24 | 25 | 1. You can post, update, delete your paste directly on the website (such as [shz.al](https://shz.al)). 26 | 27 | 2. It also provides a convenient HTTP API to use. See [API reference](doc/api.md) for details. You can easily call API via command line (using `curl` or similar tools). 28 | 29 | 3. [pb](/scripts) is a bash script to make it easier to use on command line. 30 | 31 | ## Limitations 32 | 33 | 1. If deployed on Cloudflare Worker free-tier plan, the service allows at most 100,000 reads and 1000 writes, 1000 deletes per day. 34 | 2. Due to the size limit of Cloudflare KV storage, the size of each paste is bounded under 25 MB. 35 | 36 | ## Deploy 37 | 38 | You are free to deploy the pastebin on your own domain if you host your domain on Cloudflare. 39 | 40 | 1.Requirements: 41 | 1.1. \*nix environment with bash and basic cli programs. If you are using Windows, try cygwin, WSL or something. 42 | 1.2. GNU make. 43 | 1.3. `node` and `yarn`. 44 | This part maybe be more important than you think in windows system. Recomman setting: git windows, nodejs(ltsl), cygwin(for install other dependecy app),yarn. 45 | 46 | 2.Create two KV namespaces on Cloudflare workers dashboard (one for production, one for test). Remember their IDs. If you do not need testing, simply create one,Remember the KV ID. Don't create a workers, just KV. Workers will be created by the command make deploy in later setting. 47 | 48 | 3.Clone the repository to your local file path and enter the directory. 49 | 50 | 4.Login to your Cloudflare account with comand `wrangler login`. Before that your should install the newest wrangler, the old wrangler do not support some command. 51 | Wrangler login will ask you to authenticate your cloudflare account could make change throuhg wrangler. 52 | 53 | 5.Modify entries in `wrangler.toml` according to your own account information (`account_id`, routes, kv namespace ids are what you need to modify). The `env.preview` section can be safely removed if you do not need a testing deployment. Refer to [Cloudflare doc](https://developers.cloudflare.com/workers/cli-wrangler/configuration) on how to find out these parameters. 54 | 55 | 6.Modify the contents in `config.json` (which controls the generation of static pages): `BASE_URL` is the URL of your site (no trailing slash); `FAVICON` is the URL to the favicon you want to use on your site. If you need testing, also modify `config.preview.json`. 56 | 57 | 7. Deploy and enjoy! Before that, check the nodejs's version, the old will not support by wrangler; and you should check whether you has installed yarn before you deploy by cloudflare wrangler. Then you could make deploy. Then you could find your pastebin service will be ok through your custom domain. 58 | 59 | 60 | 61 | ## Auth 62 | 63 | If you want a private deployment (only you can upload paste, but everyone can read the paste), add the following entry to your `config.json` (other configurations also contained in the outmost brace): 64 | 65 | ```json 66 | { 67 | "basicAuth": { 68 | "enabled": true, 69 | "passwd": { 70 | "admin1": "this-is-passwd-1", 71 | "admin2": "this-is-passwd-2" 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | Now every access to PUT or POST request, and every access to the index page, requires an HTTP basic auth with the user-password pair listed above. For example: 78 | 79 | ```shell 80 | $ curl example-pb.com 81 | HTTP basic auth is required 82 | 83 | $ curl -Fc=@/path/to/file example-pb.com 84 | HTTP basic auth is required 85 | 86 | $ curl -u admin1:wrong-passwd -Fc=@/path/to/file example-pb.com 87 | Error 401: incorrect passwd for basic auth 88 | 89 | $ curl -u admin1:this-is-passwd-1 -Fc=@/path/to/file example-pb.com 90 | { 91 | "url": "https://example-pb.com/YCDX", 92 | "suggestUrl": null, 93 | "admin": "https://example-pb.com/YCDX:Sij23HwbMjeZwKznY3K5trG8", 94 | "isPrivate": false 95 | } 96 | ``` 97 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "BASE_URL": "https://shz.al", 3 | "REPO": "https://github.com/SharzyL/pastebin-worker", 4 | "FAVICON": "https://sharzy.in/favicon-32x32.png" 5 | } 6 | -------------------------------------------------------------------------------- /config.preview.json: -------------------------------------------------------------------------------- 1 | { 2 | "BASE_URL": "https://pb-preview.shz.al", 3 | "REPO": "https://github.com/SharzyL/pastebin-worker", 4 | "FAVICON": "https://sharzy.in/favicon-32x32.png" 5 | } 6 | -------------------------------------------------------------------------------- /doc/api.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ## GET `/` 4 | 5 | Return the index page. 6 | 7 | ## **GET** `/[.]` 8 | 9 | Fetch the paste with name ``. By default, it will return the raw content of the paste. The `Content-Type` header is set to `text/plain;charset=UTF-8`. If `` is given, the worker will infer mime-type from `` and change `Content-Type`. This method accepts the following query string parameters: 10 | 11 | - `?lang=`: optional. returns a web page with syntax highlight powered by prism.js. 12 | 13 | - `?mime=`: optional. specify the mime-type, suppressing the effect of ``. No effect if `lang` is specified (in which case the mime-type is always `text/html`). 14 | 15 | Examples: `GET /abcd?lang=js`, `GET /abcd?mime=application/json`. 16 | 17 | If error occurs, the worker returns status code different from `200`: 18 | 19 | - `404`: the paste of given name is not found. 20 | - `500`: unexpected exception. You may report this to the author to give it a fix. 21 | 22 | Usage example: 23 | 24 | ```shell 25 | $ curl https://shz.al/i-p- 26 | https://web.archive.org/web/20210328091143/https://mp.weixin.qq.com/s/5phCQP7i-JpSvzPEMGk56Q 27 | 28 | $ curl https://shz.al/~panty.jpg | feh - 29 | 30 | $ firefox 'https://shz.al/kf7z?lang=nix' 31 | 32 | $ curl 'https://shz.al/~panty.jpg?mime=image/png' -w '%{content_type}' -o /dev/null -sS 33 | image/png;charset=UTF-8 34 | ``` 35 | 36 | ## GET `/:` 37 | 38 | Return the web page to edit the paste of name `` and password ``. 39 | 40 | If error occurs, the worker returns status code different from `200`: 41 | 42 | - `404`: the paste of given name is not found. 43 | - `500`: unexpected exception. You may report this to the author to give it a fix. 44 | 45 | ## GET `/u/` 46 | 47 | Redirect to the URL recorded in the paste of name ``. 48 | 49 | If error occurs, the worker returns status code different from `302`: 50 | 51 | - `404`: the paste of given name is not found. 52 | - `500`: unexpected exception. You may report this to the author to give it a fix. 53 | 54 | Usage example: 55 | 56 | ```shell 57 | $ firefox https://shz.al/u/i-p- 58 | 59 | $ curl -L https://shz.al/u/i-p- 60 | ``` 61 | 62 | ## GET `/a/` 63 | 64 | Return the HTML converted from the markdown file stored in the paste of name ``. The markdown conversion follows GitHub Flavored Markdown (GFM) Spec, supported by [remark-gfm](https://github.com/remarkjs/remark-gfm). 65 | 66 | Syntax highlighting is supported by [prims.js](https://prismjs.com/). LaTeX mathematics is supported by [MathJax](https://www.mathjax.org). 67 | 68 | If error occurs, the worker returns status code different from `200`: 69 | 70 | - `404`: the paste of given name is not found. 71 | - `500`: unexpected exception. You may report this to the author to give it a fix. 72 | 73 | Usage example: 74 | 75 | ```md 76 | # Header 1 77 | This is the content of `test.md` 78 | 79 | 82 | 83 | ## Header 2 84 | 85 | | abc | defghi | 86 | :-: | -----------: 87 | bar | baz 88 | 89 | **Bold**, `Monospace`, *Italics*, ~~Strikethrough~~, [URL](https://github.com) 90 | 91 | - A 92 | - A1 93 | - A2 94 | - B 95 | 96 | ![Panty](https://shz.al/~panty.jpg) 97 | 98 | 1. first 99 | 2. second 100 | 101 | > Quotation 102 | 103 | $$ 104 | \int_{-\infty}^{\infty} e^{-x^2} = \sqrt{\pi} 105 | $$ 106 | 107 | ``` 108 | 109 | ```shell 110 | $ curl -Fc=@test.md -Fn=test-md https://shz.al 111 | 112 | $ firefox https://shz.al/a/~test-md 113 | ``` 114 | 115 | ## **POST** `/` 116 | 117 | Upload your paste. It accept parameters in form-data: 118 | 119 | - `c`: mandatory. The **content** of your paste, text of binary. It should be no larger than 10 MB. 120 | 121 | - `e`: optional. The **expiration** time of the paste. After this period of time, the paste is permanently deleted. It should be an integer or a float point number suffixed with an optional unit (seconds by default). Supported units: `s` (seconds), `m` (minutes), `h` (hours), `d` (days), `M` (months). For example, `360.24` means 360.25 seconds; `25d` is interpreted as 25 days. It should be no smaller than 60 seconds due to the limitation of Cloudflare KV storage. 122 | 123 | - `s`: optional. The **password** which allows you to modify and delete the paste. If not specified, the worker will generate a random string as password. 124 | 125 | - `n`: optional. The customized **name** of your paste. If not specified, the worker will generate a random string (4 characters by default) as the name. You need to prefix the name with `~` when fetching the paste of customized name. The name is at least 3 characters long, consisting of alphabet, digits and characters in `+_-[]*$=@,;/`. 126 | 127 | - `p`: optional. The flag of **private mode**. If specified to any value, the name of the paste is as long as 24 characters. No effect if `n` is used. 128 | 129 | `POST` method returns a JSON string by default, if no error occurs, for example: 130 | 131 | ```json 132 | { 133 | "url": "https://shz.al/abcd", 134 | "admin": "https://shz.al/abcd:w2eHqyZGc@CQzWLN=BiJiQxZ", 135 | "expire": 100, 136 | "isPrivate": false 137 | } 138 | ``` 139 | 140 | Explanation of the fields: 141 | 142 | - `url`: mandatory. The URL to fetch the paste. When using a customized name, it looks like `https//shz.al/~myname`. 143 | - `admin`: mandatory. The URL to update and delete the paste, which is `url` suffixed by `~` and the password. 144 | - `expire`: optional. The expiration seconds. 145 | - `isPrivate`: mandatory. Whether the paste is in private mode. 146 | 147 | If error occurs, the worker returns status code different from `200`: 148 | 149 | - `400`: your request is in bad format. 150 | - `409`: the name is already used. 151 | - `413`: the content is too large. 152 | - `500`: unexpected exception. You may report this to the author to give it a fix. 153 | 154 | Usage example: 155 | 156 | ```shell 157 | $ curl -Fc="kawaii" -Fe=300 -Fn=hitagi https://shz.al # uploading some text 158 | { 159 | "url": "https://shz.al/~hitagi", 160 | "admin": "https://shz.al/~hitagi:22@-OJWcTOH2jprTJWYadmDv", 161 | "isPrivate": false, 162 | "expire": 300 163 | } 164 | 165 | $ curl -Fc=@panty.jpg -Fn=panty -Fs=12345678 https://shz.al # uploading a file 166 | { 167 | "url": "https://shz.al/~panty", 168 | "admin": "https://shz.al/~panty:12345678", 169 | "isPrivate": false 170 | } 171 | 172 | # because `curl` takes some characters as filed separator, the fields should be 173 | # quoted by double-quotes if the field contains semicolon or comma 174 | $ curl -Fc=@panty.jpg -Fn='"hi/hello;g,ood"' -Fs=12345678 https://shz.al 175 | { 176 | "url": "https://shz.al/~hi/hello;g,ood", 177 | "admin": "https://shz.al/~hi/hello;g,ood:QJhMKh5WR6z36QRAAn5Q5GZh", 178 | "isPrivate": false 179 | } 180 | ``` 181 | 182 | ## **PUT** `/:` 183 | 184 | Update you paste of the name `` and password ``. It accept the parameters in form-data: 185 | 186 | - `c`: mandatory. Same as `POST` method. 187 | - `e`: optional. Same as `POST` method. Note that the deletion time is now recalculated. 188 | - `s`: optional. Same as `POST` method. 189 | 190 | The returning of `PUT` method is also the same as `POST` method. 191 | 192 | If error occurs, the worker returns status code different from `200`: 193 | 194 | - `400`: your request is in bad format. 195 | - `403`: your password is not correct. 196 | - `404`: the paste of given name is not found. 197 | - `413`: the content is too large. 198 | - `500`: unexpected exception. You may report this to the author to give it a fix. 199 | 200 | Usage example: 201 | 202 | ```shell 203 | $ curl -X PUT -Fc="kawaii~" -Fe=500 https://shz.al/~hitagi:22@-OJWcTOH2jprTJWYadmDv 204 | { 205 | "url": "https://shz.al/~hitagi", 206 | "admin": "https://shz.al/~hitagi:22@-OJWcTOH2jprTJWYadmDv", 207 | "isPrivate": false, 208 | "expire": 500 209 | } 210 | 211 | $ curl -X PUT -Fc="kawaii~" https://shz.al/~hitagi:22@-OJWcTOH2jprTJWYadmDv 212 | { 213 | "url": "https://shz.al/~hitagi", 214 | "admin": "https://shz.al/~hitagi:22@-OJWcTOH2jprTJWYadmDv", 215 | "isPrivate": false 216 | } 217 | ``` 218 | 219 | ## DELETE `/:` 220 | 221 | Delete the paste of name `` and password ``. It may take seconds to synchronize the deletion globally. 222 | 223 | If error occurs, the worker returns status code different from `200`: 224 | 225 | - `403`: your password is not correct. 226 | - `404`: the paste of given name is not found. 227 | - `500`: unexpected exception. You may report this to the author to give it a fix. 228 | 229 | Usage example: 230 | 231 | ```shell 232 | $ curl -X DELETE https://shz.al/~hitagi:22@-OJWcTOH2jprTJWYadmDv 233 | the paste will be deleted in seconds 234 | 235 | $ curl https://shz.al/~hitagi 236 | not found 237 | ``` 238 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Yet another pastebin 5 | 6 | 7 | 10 | 11 | 12 | 13 |
14 |

Yet Another Pastebin

15 |

This is an open source pastebin deployed on Cloudflare Workers.

16 |

Usage: paste any text here, submit, then share it with URL.

17 |

Refer to GitHub for more details.

18 | 19 |
20 |
21 |
22 |
Edit paste
23 | 27 |
28 |
29 |
30 |
31 | 32 |
33 |
34 |

35 | : 36 |

37 |
38 |
39 | 40 |
41 |

Settings

42 |
43 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 | 57 | 59 |
60 |
61 | 62 | 63 | Example: {{BASE_URL}}/BxWH 64 |
65 |
66 | 67 | 68 | Example: {{BASE_URL}}/5HQWYNmjA4h44SmybeThXXAm 69 |
70 |
71 | 72 | 73 |
74 | {{BASE_URL}}/~ 75 | 76 |
77 | Example: {{BASE_URL}}/~stocking 78 |
79 |
80 | 81 | 82 | 84 |
85 |
86 | 103 | 104 | 105 |
106 | 116 | 117 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /frontend/index.js: -------------------------------------------------------------------------------- 1 | const SEP = ':' 2 | 3 | function parsePath(pathname) { 4 | let role = "", ext = "" 5 | if (pathname[2] === "/") { 6 | role = pathname[1] 7 | pathname = pathname.slice(2) 8 | } 9 | let startOfExt = pathname.indexOf(".") 10 | if (startOfExt >= 0) { 11 | ext = pathname.slice(startOfExt) 12 | pathname = pathname.slice(0, startOfExt) 13 | } 14 | let endOfShort = pathname.indexOf(SEP) 15 | if (endOfShort < 0) endOfShort = pathname.length // when there is no SEP, passwd is left empty 16 | const short = pathname.slice(1, endOfShort) 17 | const passwd = pathname.slice(endOfShort + 1) 18 | return { role, short, passwd, ext } 19 | } 20 | 21 | window.addEventListener('load', () => { 22 | const base_url = '{{BASE_URL}}' 23 | const deploy_date = new Date('{{DEPLOY_DATE}}') 24 | 25 | function getDateString(date) { 26 | const year = date.getFullYear() 27 | const month = (date.getMonth() + 1).toString().padStart(2, '0') 28 | const day = date.getDate().toString().padStart(2, '0') 29 | const hour = date.getHours().toString().padStart(2, '0') 30 | const minute = date.getMinutes().toString().padStart(2, '0') 31 | const second = date.getSeconds().toString().padStart(2, '0') 32 | return `${year}-${month}-${day} ${hour}:${minute}:${second}` 33 | } 34 | 35 | $('#deploy-date').text(getDateString(deploy_date)) 36 | 37 | function copyTextFromInput(input) { 38 | if (input.constructor === String) input = document.getElementById(input) 39 | input.focus() 40 | input.select() 41 | try { 42 | document.execCommand('copy') 43 | } catch (err) { 44 | alert('Failed to copy content') 45 | } 46 | } 47 | 48 | function isAdminUrlLegal(url) { 49 | try { 50 | url = new URL(url) 51 | return url.origin === base_url && url.pathname.indexOf(':') >= 0 52 | } catch (e) { 53 | if (e instanceof TypeError) { 54 | return false 55 | } else { 56 | throw e 57 | } 58 | } 59 | } 60 | 61 | const formatSize = (size) => { 62 | if (!size) return '0' 63 | if (size < 1024) { 64 | return `${size} Bytes` 65 | } else if (size < 1024 * 1024) { 66 | return `${(size / 1024).toFixed(2)} KB` 67 | } else if (size < 1024 * 1024 * 1024) { 68 | return `${(size / 1024 / 1024).toFixed(2)} MB` 69 | } else { 70 | return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB` 71 | } 72 | } 73 | 74 | // monitor input changes and enable/disable submit button 75 | let urlType = 'short', inputType = 'edit', expiration = '', passwd = '' 76 | let customName = '', adminUrl = '', file = null 77 | 78 | const NAME_REGEX = /^[a-zA-Z0-9+_\-\[\]*$=@,;/]{3,}$/ 79 | const submitButton = $('#submit-button') 80 | const deleteButton = $('#delete-button') 81 | const pasteEditArea = $('#paste-textarea') 82 | 83 | function updateButtons() { 84 | const pasteNotEmpty = inputType === 'edit' 85 | ? pasteEditArea.prop('value').length > 0 86 | : file !== null 87 | const expirationNotShort = expiration.length === 0 || parseInt(expiration) >= 60 88 | const nameValid = urlType !== 'custom' || NAME_REGEX.test(customName) 89 | const adminUrlValid = urlType !== 'admin' || isAdminUrlLegal(adminUrl) 90 | 91 | if (pasteNotEmpty && expirationNotShort && nameValid && adminUrlValid) { 92 | submitButton.addClass('enabled') 93 | submitButton.prop('title', '') 94 | } else { 95 | submitButton.removeClass('enabled') 96 | if (!pasteNotEmpty) { 97 | submitButton.prop('title', 'Cannot upload empty paste') 98 | } else if (!expirationNotShort) { 99 | submitButton.prop('title', 'Expiration should be more than 60 seconds') 100 | } else if (!nameValid) { 101 | submitButton.prop('title', `The customized URL should satisfy regex ${NAME_REGEX}`) 102 | } 103 | } 104 | 105 | if (urlType === 'admin') { 106 | submitButton.text('Update') 107 | deleteButton.removeClass('hidden') 108 | } else { 109 | submitButton.text('Submit') 110 | deleteButton.addClass('hidden') 111 | } 112 | 113 | if (adminUrlValid) { 114 | deleteButton.addClass('enabled') 115 | submitButton.prop('title', '') 116 | } else { 117 | deleteButton.removeClass('enabled') 118 | submitButton.prop('title', `The admin URL should start with ${base_url} and contain a colon`) 119 | deleteButton.prop('title', `The admin URL should start with ${base_url} and contain a colon`) 120 | } 121 | } 122 | 123 | updateButtons() 124 | 125 | function updateTabBar() { 126 | if (inputType === 'file') { 127 | $('#paste-tab-edit').removeClass('enabled') 128 | $('#paste-tab-file').addClass('enabled') 129 | $('#paste-file-show').addClass('enabled') 130 | $('#paste-edit').removeClass('enabled') 131 | } else { 132 | $('#paste-tab-file').removeClass('enabled') 133 | $('#paste-tab-edit').addClass('enabled') 134 | $('#paste-edit').addClass('enabled') 135 | $('#paste-file-show').removeClass('enabled') 136 | } 137 | } 138 | 139 | $('#paste-tab-file').on('input', event => { 140 | const files = event.target.files 141 | if (files.length === 0) return 142 | file = files[0] 143 | inputType = 'file' 144 | updateButtons() 145 | updateTabBar() 146 | const fileLine = $('#paste-file-line') 147 | fileLine.children('.file-name').text(file.name) 148 | fileLine.children('.file-size').text(formatSize(file.size)) 149 | }) 150 | 151 | $('#paste-tab-edit').on('click', () => { 152 | inputType = 'edit' 153 | updateButtons() 154 | updateTabBar() 155 | }) 156 | 157 | pasteEditArea.on('input', updateButtons) 158 | 159 | $('#paste-expiration-input').on('input', event => { 160 | expiration = event.target.value 161 | updateButtons() 162 | }) 163 | 164 | $('#paste-passwd-input').on('input', event => { 165 | passwd = event.target.value 166 | }) 167 | 168 | $('input[name="url-type"]').on('input', event => { 169 | urlType = event.target.value 170 | updateButtons() 171 | }) 172 | 173 | $('#paste-custom-url-input').on('input', event => { 174 | customName = event.target.value 175 | updateButtons() 176 | }) 177 | 178 | $('#paste-admin-url-input').on('input', event => { 179 | adminUrl = event.target.value 180 | updateButtons() 181 | }) 182 | 183 | // submit the form 184 | submitButton.on('click', () => { 185 | if (submitButton.hasClass('enabled')) { 186 | if (urlType === 'admin') { 187 | putPaste() 188 | } else { 189 | postPaste() 190 | } 191 | } 192 | }) 193 | 194 | deleteButton.on('click', () => { 195 | if (deleteButton.hasClass('enabled')) { 196 | deletePaste() 197 | } 198 | }) 199 | 200 | function putPaste() { 201 | prepareUploading() 202 | let fd = new FormData() 203 | if (inputType === 'file') { 204 | fd.append('c', file) 205 | } else { 206 | fd.append('c', pasteEditArea.prop('value')) 207 | } 208 | 209 | if (expiration.length > 0) fd.append('e', expiration) 210 | if (passwd.length > 0) fd.append('s', passwd) 211 | 212 | $.ajax({ 213 | method: 'PUT', 214 | url: adminUrl, 215 | data: fd, 216 | processData: false, 217 | contentType: false, 218 | success: (data) => { 219 | renderUploaded(data) 220 | }, 221 | error: handleError, 222 | }) 223 | } 224 | 225 | function postPaste() { 226 | prepareUploading() 227 | let fd = new FormData() 228 | if (inputType === 'file') { 229 | fd.append('c', file) 230 | } else { 231 | fd.append('c', pasteEditArea.prop('value')) 232 | } 233 | 234 | if (expiration.length > 0) fd.append('e', expiration) 235 | if (passwd.length > 0) fd.append('s', passwd) 236 | 237 | if (urlType === 'long') fd.append('p', 'true') 238 | if (urlType === 'custom') fd.append('n', customName) 239 | 240 | $.post({ 241 | url: base_url, 242 | data: fd, 243 | processData: false, 244 | contentType: false, 245 | success: (data) => { 246 | renderUploaded(data) 247 | }, 248 | error: handleError, 249 | }) 250 | } 251 | 252 | function deletePaste() { 253 | prepareUploading() 254 | let fd = new FormData() 255 | $.ajax({ 256 | method: 'DELETE', 257 | url: adminUrl, 258 | data: fd, 259 | processData: false, 260 | success: () => { 261 | alert('Delete successfully') 262 | }, 263 | error: handleError, 264 | }) 265 | } 266 | 267 | function prepareUploading() { 268 | resetCopyButtons() 269 | $('#submit-button').removeClass('enabled') 270 | $('#paste-uploaded-panel input').prop('value', '') 271 | } 272 | 273 | function renderUploaded(uploaded) { 274 | $('#paste-uploaded-panel').removeClass('hidden') 275 | $('#uploaded-url').prop('value', uploaded.url) 276 | $('#uploaded-admin-url').prop('value', uploaded.admin) 277 | if (uploaded.expire) { 278 | $('#uploaded-expiration').prop('value', uploaded.expire) 279 | } 280 | updateButtons() 281 | } 282 | 283 | $('.copy-button').on('click', event => { 284 | const button = event.target 285 | const input = button.parentElement.firstElementChild 286 | input.focus() 287 | input.select() 288 | try { 289 | document.execCommand('copy') 290 | resetCopyButtons() 291 | button.textContent = 'Copied' 292 | } catch (err) { 293 | alert('Failed to copy content') 294 | } 295 | }) 296 | 297 | function resetCopyButtons() { 298 | $('.copy-button').text('Copy') 299 | } 300 | 301 | function handleError(error) { 302 | const status = error.status || '' 303 | let statusText = error.statusText === 'error' ? 'Unknown error' : error.statusText 304 | const responseText = error.responseText || '' 305 | alert(`Error ${status}: ${statusText}\n${responseText}\nView your console for more information`) 306 | $('#submit-button').addClass('enabled') 307 | } 308 | 309 | function initAdmin() { 310 | const { role, short, passwd, ext } = parsePath(location.pathname) 311 | if (passwd.length > 0) { 312 | $('#paste-url-admin-radio').click() 313 | $('#paste-admin-url-input').val(location.href) 314 | urlType = 'admin' 315 | adminUrl = location.href 316 | updateButtons() 317 | $.ajax({ 318 | url: "/" + short, 319 | success: paste => { 320 | pasteEditArea.val(paste) 321 | updateButtons() 322 | }, 323 | error: handleError, 324 | }) 325 | } 326 | } 327 | 328 | initAdmin() 329 | }) 330 | -------------------------------------------------------------------------------- /frontend/style.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress{vertical-align:baseline}template,[hidden]{display:none !important}a{background-color:transparent}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:bold}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:0.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}*{box-sizing:border-box}input,select,textarea,button{font-family:inherit;font-size:inherit;line-height:inherit}body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:14px;line-height:1.5;color:#24292e;background-color:#fff}a{color:#0366d6;text-decoration:none}a:hover{text-decoration:underline}b,strong{font-weight:600}hr,.rule{height:0;margin:15px 0;overflow:hidden;background:transparent;border:0;border-bottom:1px solid #dfe2e5}hr::before,.rule::before{display:table;content:""}hr::after,.rule::after{display:table;clear:both;content:""}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}button{cursor:pointer;border-radius:0}[hidden][hidden]{display:none !important}details summary{cursor:pointer}details:not([open])>*:not(summary){display:none !important}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:0}h1{font-size:32px;font-weight:600}h2{font-size:24px;font-weight:600}h3{font-size:20px;font-weight:600}h4{font-size:16px;font-weight:600}h5{font-size:14px;font-weight:600}h6{font-size:12px;font-weight:600}p{margin-top:0;margin-bottom:10px}small{font-size:90%}blockquote{margin:0}ul,ol{padding-left:0;margin-top:0;margin-bottom:0}ol ol,ul ol{list-style-type:lower-roman}ul ul ol,ul ol ol,ol ul ol,ol ol ol{list-style-type:lower-alpha}dd{margin-left:0}tt,code{font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;font-size:12px}pre{margin-top:0;margin-bottom:0;font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;font-size:12px}.octicon{vertical-align:text-bottom}.anim-fade-in{animation-name:fade-in;animation-duration:1s;animation-timing-function:ease-in-out}.anim-fade-in.fast{animation-duration:300ms}@keyframes fade-in{0%{opacity:0}100%{opacity:1}}.anim-fade-out{animation-name:fade-out;animation-duration:1s;animation-timing-function:ease-out}.anim-fade-out.fast{animation-duration:0.3s}@keyframes fade-out{0%{opacity:1}100%{opacity:0}}.anim-fade-up{opacity:0;animation-name:fade-up;animation-duration:0.3s;animation-fill-mode:forwards;animation-timing-function:ease-out;animation-delay:1s}@keyframes fade-up{0%{opacity:0.8;transform:translateY(100%)}100%{opacity:1;transform:translateY(0)}}.anim-fade-down{animation-name:fade-down;animation-duration:0.3s;animation-fill-mode:forwards;animation-timing-function:ease-in}@keyframes fade-down{0%{opacity:1;transform:translateY(0)}100%{opacity:0.5;transform:translateY(100%)}}.anim-grow-x{width:0%;animation-name:grow-x;animation-duration:0.3s;animation-fill-mode:forwards;animation-timing-function:ease;animation-delay:0.5s}@keyframes grow-x{to{width:100%}}.anim-shrink-x{animation-name:shrink-x;animation-duration:0.3s;animation-fill-mode:forwards;animation-timing-function:ease-in-out;animation-delay:0.5s}@keyframes shrink-x{to{width:0%}}.anim-scale-in{animation-name:scale-in;animation-duration:0.15s;animation-timing-function:cubic-bezier(0.2, 0, 0.13, 1.5)}@keyframes scale-in{0%{opacity:0;transform:scale(0.5)}100%{opacity:1;transform:scale(1)}}.anim-pulse{animation-name:pulse;animation-duration:2s;animation-timing-function:linear;animation-iteration-count:infinite}@keyframes pulse{0%{opacity:0.3}10%{opacity:1}100%{opacity:0.3}}.anim-pulse-in{animation-name:pulse-in;animation-duration:0.5s}@keyframes pulse-in{0%{transform:scale3d(1, 1, 1)}50%{transform:scale3d(1.1, 1.1, 1.1)}100%{transform:scale3d(1, 1, 1)}}.hover-grow{transition:transform 0.3s;backface-visibility:hidden}.hover-grow:hover{transform:scale(1.025)}.border{border:1px #e1e4e8 solid !important}.border-y{border-top:1px #e1e4e8 solid !important;border-bottom:1px #e1e4e8 solid !important}.border-0{border:0 !important}.border-dashed{border-style:dashed !important}.border-blue{border-color:#0366d6 !important}.border-blue-light{border-color:#c8e1ff !important}.border-green{border-color:#34d058 !important}.border-green-light{border-color:#a2cbac !important}.border-red{border-color:#d73a49 !important}.border-red-light{border-color:#cea0a5 !important}.border-purple{border-color:#6f42c1 !important}.border-yellow{border-color:#d9d0a5 !important}.border-gray-light{border-color:#eaecef !important}.border-gray-dark{border-color:#d1d5da !important}.border-black-fade{border-color:rgba(27,31,35,0.15) !important}.border-top{border-top:1px #e1e4e8 solid !important}.border-right{border-right:1px #e1e4e8 solid !important}.border-bottom{border-bottom:1px #e1e4e8 solid !important}.border-left{border-left:1px #e1e4e8 solid !important}.border-top-0{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0{border-bottom:0 !important}.border-left-0{border-left:0 !important}.rounded-0{border-radius:0 !important}.rounded-1{border-radius:3px !important}.rounded-2{border-radius:6px !important}.rounded-top-0{border-top-left-radius:0 !important;border-top-right-radius:0 !important}.rounded-top-1{border-top-left-radius:3px !important;border-top-right-radius:3px !important}.rounded-top-2{border-top-left-radius:6px !important;border-top-right-radius:6px !important}.rounded-right-0{border-top-right-radius:0 !important;border-bottom-right-radius:0 !important}.rounded-right-1{border-top-right-radius:3px !important;border-bottom-right-radius:3px !important}.rounded-right-2{border-top-right-radius:6px !important;border-bottom-right-radius:6px !important}.rounded-bottom-0{border-bottom-right-radius:0 !important;border-bottom-left-radius:0 !important}.rounded-bottom-1{border-bottom-right-radius:3px !important;border-bottom-left-radius:3px !important}.rounded-bottom-2{border-bottom-right-radius:6px !important;border-bottom-left-radius:6px !important}.rounded-left-0{border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}.rounded-left-1{border-bottom-left-radius:3px !important;border-top-left-radius:3px !important}.rounded-left-2{border-bottom-left-radius:6px !important;border-top-left-radius:6px !important}@media (min-width: 544px){.border-sm-top{border-top:1px #e1e4e8 solid !important}.border-sm-right{border-right:1px #e1e4e8 solid !important}.border-sm-bottom{border-bottom:1px #e1e4e8 solid !important}.border-sm-left{border-left:1px #e1e4e8 solid !important}.border-sm-top-0{border-top:0 !important}.border-sm-right-0{border-right:0 !important}.border-sm-bottom-0{border-bottom:0 !important}.border-sm-left-0{border-left:0 !important}.rounded-sm-0{border-radius:0 !important}.rounded-sm-1{border-radius:3px !important}.rounded-sm-2{border-radius:6px !important}.rounded-sm-top-0{border-top-left-radius:0 !important;border-top-right-radius:0 !important}.rounded-sm-top-1{border-top-left-radius:3px !important;border-top-right-radius:3px !important}.rounded-sm-top-2{border-top-left-radius:6px !important;border-top-right-radius:6px !important}.rounded-sm-right-0{border-top-right-radius:0 !important;border-bottom-right-radius:0 !important}.rounded-sm-right-1{border-top-right-radius:3px !important;border-bottom-right-radius:3px !important}.rounded-sm-right-2{border-top-right-radius:6px !important;border-bottom-right-radius:6px !important}.rounded-sm-bottom-0{border-bottom-right-radius:0 !important;border-bottom-left-radius:0 !important}.rounded-sm-bottom-1{border-bottom-right-radius:3px !important;border-bottom-left-radius:3px !important}.rounded-sm-bottom-2{border-bottom-right-radius:6px !important;border-bottom-left-radius:6px !important}.rounded-sm-left-0{border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}.rounded-sm-left-1{border-bottom-left-radius:3px !important;border-top-left-radius:3px !important}.rounded-sm-left-2{border-bottom-left-radius:6px !important;border-top-left-radius:6px !important}}@media (min-width: 768px){.border-md-top{border-top:1px #e1e4e8 solid !important}.border-md-right{border-right:1px #e1e4e8 solid !important}.border-md-bottom{border-bottom:1px #e1e4e8 solid !important}.border-md-left{border-left:1px #e1e4e8 solid !important}.border-md-top-0{border-top:0 !important}.border-md-right-0{border-right:0 !important}.border-md-bottom-0{border-bottom:0 !important}.border-md-left-0{border-left:0 !important}.rounded-md-0{border-radius:0 !important}.rounded-md-1{border-radius:3px !important}.rounded-md-2{border-radius:6px !important}.rounded-md-top-0{border-top-left-radius:0 !important;border-top-right-radius:0 !important}.rounded-md-top-1{border-top-left-radius:3px !important;border-top-right-radius:3px !important}.rounded-md-top-2{border-top-left-radius:6px !important;border-top-right-radius:6px !important}.rounded-md-right-0{border-top-right-radius:0 !important;border-bottom-right-radius:0 !important}.rounded-md-right-1{border-top-right-radius:3px !important;border-bottom-right-radius:3px !important}.rounded-md-right-2{border-top-right-radius:6px !important;border-bottom-right-radius:6px !important}.rounded-md-bottom-0{border-bottom-right-radius:0 !important;border-bottom-left-radius:0 !important}.rounded-md-bottom-1{border-bottom-right-radius:3px !important;border-bottom-left-radius:3px !important}.rounded-md-bottom-2{border-bottom-right-radius:6px !important;border-bottom-left-radius:6px !important}.rounded-md-left-0{border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}.rounded-md-left-1{border-bottom-left-radius:3px !important;border-top-left-radius:3px !important}.rounded-md-left-2{border-bottom-left-radius:6px !important;border-top-left-radius:6px !important}}@media (min-width: 1012px){.border-lg-top{border-top:1px #e1e4e8 solid !important}.border-lg-right{border-right:1px #e1e4e8 solid !important}.border-lg-bottom{border-bottom:1px #e1e4e8 solid !important}.border-lg-left{border-left:1px #e1e4e8 solid !important}.border-lg-top-0{border-top:0 !important}.border-lg-right-0{border-right:0 !important}.border-lg-bottom-0{border-bottom:0 !important}.border-lg-left-0{border-left:0 !important}.rounded-lg-0{border-radius:0 !important}.rounded-lg-1{border-radius:3px !important}.rounded-lg-2{border-radius:6px !important}.rounded-lg-top-0{border-top-left-radius:0 !important;border-top-right-radius:0 !important}.rounded-lg-top-1{border-top-left-radius:3px !important;border-top-right-radius:3px !important}.rounded-lg-top-2{border-top-left-radius:6px !important;border-top-right-radius:6px !important}.rounded-lg-right-0{border-top-right-radius:0 !important;border-bottom-right-radius:0 !important}.rounded-lg-right-1{border-top-right-radius:3px !important;border-bottom-right-radius:3px !important}.rounded-lg-right-2{border-top-right-radius:6px !important;border-bottom-right-radius:6px !important}.rounded-lg-bottom-0{border-bottom-right-radius:0 !important;border-bottom-left-radius:0 !important}.rounded-lg-bottom-1{border-bottom-right-radius:3px !important;border-bottom-left-radius:3px !important}.rounded-lg-bottom-2{border-bottom-right-radius:6px !important;border-bottom-left-radius:6px !important}.rounded-lg-left-0{border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}.rounded-lg-left-1{border-bottom-left-radius:3px !important;border-top-left-radius:3px !important}.rounded-lg-left-2{border-bottom-left-radius:6px !important;border-top-left-radius:6px !important}}@media (min-width: 1280px){.border-xl-top{border-top:1px #e1e4e8 solid !important}.border-xl-right{border-right:1px #e1e4e8 solid !important}.border-xl-bottom{border-bottom:1px #e1e4e8 solid !important}.border-xl-left{border-left:1px #e1e4e8 solid !important}.border-xl-top-0{border-top:0 !important}.border-xl-right-0{border-right:0 !important}.border-xl-bottom-0{border-bottom:0 !important}.border-xl-left-0{border-left:0 !important}.rounded-xl-0{border-radius:0 !important}.rounded-xl-1{border-radius:3px !important}.rounded-xl-2{border-radius:6px !important}.rounded-xl-top-0{border-top-left-radius:0 !important;border-top-right-radius:0 !important}.rounded-xl-top-1{border-top-left-radius:3px !important;border-top-right-radius:3px !important}.rounded-xl-top-2{border-top-left-radius:6px !important;border-top-right-radius:6px !important}.rounded-xl-right-0{border-top-right-radius:0 !important;border-bottom-right-radius:0 !important}.rounded-xl-right-1{border-top-right-radius:3px !important;border-bottom-right-radius:3px !important}.rounded-xl-right-2{border-top-right-radius:6px !important;border-bottom-right-radius:6px !important}.rounded-xl-bottom-0{border-bottom-right-radius:0 !important;border-bottom-left-radius:0 !important}.rounded-xl-bottom-1{border-bottom-right-radius:3px !important;border-bottom-left-radius:3px !important}.rounded-xl-bottom-2{border-bottom-right-radius:6px !important;border-bottom-left-radius:6px !important}.rounded-xl-left-0{border-bottom-left-radius:0 !important;border-top-left-radius:0 !important}.rounded-xl-left-1{border-bottom-left-radius:3px !important;border-top-left-radius:3px !important}.rounded-xl-left-2{border-bottom-left-radius:6px !important;border-top-left-radius:6px !important}}.circle{border-radius:50% !important}.box-shadow{box-shadow:0 1px 1px rgba(27,31,35,0.1) !important}.box-shadow-medium{box-shadow:0 1px 5px rgba(27,31,35,0.15) !important}.box-shadow-large{box-shadow:0 1px 15px rgba(27,31,35,0.15) !important}.box-shadow-extra-large{box-shadow:0 10px 50px rgba(27,31,35,0.07) !important}.box-shadow-none{box-shadow:none !important}.bg-white{background-color:#fff !important}.bg-blue{background-color:#0366d6 !important}.bg-blue-light{background-color:#f1f8ff !important}.bg-gray-dark{background-color:#24292e !important}.bg-gray{background-color:#f6f8fa !important}.bg-gray-light{background-color:#fafbfc !important}.bg-green{background-color:#28a745 !important}.bg-green-light{background-color:#dcffe4 !important}.bg-red{background-color:#d73a49 !important}.bg-red-light{background-color:#ffdce0 !important}.bg-yellow{background-color:#ffd33d !important}.bg-yellow-light{background-color:#fff5b1 !important}.bg-purple{background-color:#6f42c1 !important}.bg-purple-light{background-color:#f5f0ff !important}.bg-shade-gradient{background-image:linear-gradient(180deg, rgba(27,31,35,0.065), rgba(27,31,35,0)) !important;background-repeat:no-repeat !important;background-size:100% 200px !important}.text-blue{color:#0366d6 !important}.text-red{color:#cb2431 !important}.text-gray-light{color:#6a737d !important}.text-gray{color:#586069 !important}.text-gray-dark{color:#24292e !important}.text-green{color:#28a745 !important}.text-orange{color:#a04100 !important}.text-orange-light{color:#e36209 !important}.text-purple{color:#6f42c1 !important}.text-white{color:#fff !important}.text-inherit{color:inherit !important}.text-pending{color:#b08800 !important}.bg-pending{color:#dbab09 !important}.link-gray{color:#586069 !important}.link-gray:hover{color:#0366d6 !important}.link-gray-dark{color:#24292e !important}.link-gray-dark:hover{color:#0366d6 !important}.link-hover-blue:hover{color:#0366d6 !important}.muted-link{color:#586069 !important}.muted-link:hover{color:#0366d6 !important;text-decoration:none}.details-overlay[open]>summary::before{position:fixed;top:0;right:0;bottom:0;left:0;z-index:80;display:block;cursor:default;content:" ";background:transparent}.details-overlay-dark[open]>summary::before{z-index:99;background:rgba(27,31,35,0.5)}.flex-row{flex-direction:row !important}.flex-row-reverse{flex-direction:row-reverse !important}.flex-column{flex-direction:column !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-justify-start{justify-content:flex-start !important}.flex-justify-end{justify-content:flex-end !important}.flex-justify-center{justify-content:center !important}.flex-justify-between{justify-content:space-between !important}.flex-justify-around{justify-content:space-around !important}.flex-items-start{align-items:flex-start !important}.flex-items-end{align-items:flex-end !important}.flex-items-center{align-items:center !important}.flex-items-baseline{align-items:baseline !important}.flex-items-stretch{align-items:stretch !important}.flex-content-start{align-content:flex-start !important}.flex-content-end{align-content:flex-end !important}.flex-content-center{align-content:center !important}.flex-content-between{align-content:space-between !important}.flex-content-around{align-content:space-around !important}.flex-content-stretch{align-content:stretch !important}.flex-auto{flex:1 1 auto !important}.flex-shrink-0{flex-shrink:0 !important}.flex-self-auto{align-self:auto !important}.flex-self-start{align-self:flex-start !important}.flex-self-end{align-self:flex-end !important}.flex-self-center{align-self:center !important}.flex-self-baseline{align-self:baseline !important}.flex-self-stretch{align-self:stretch !important}.flex-item-equal{flex-grow:1;flex-basis:0}@media (min-width: 544px){.flex-sm-row{flex-direction:row !important}.flex-sm-row-reverse{flex-direction:row-reverse !important}.flex-sm-column{flex-direction:column !important}.flex-sm-wrap{flex-wrap:wrap !important}.flex-sm-nowrap{flex-wrap:nowrap !important}.flex-sm-justify-start{justify-content:flex-start !important}.flex-sm-justify-end{justify-content:flex-end !important}.flex-sm-justify-center{justify-content:center !important}.flex-sm-justify-between{justify-content:space-between !important}.flex-sm-justify-around{justify-content:space-around !important}.flex-sm-items-start{align-items:flex-start !important}.flex-sm-items-end{align-items:flex-end !important}.flex-sm-items-center{align-items:center !important}.flex-sm-items-baseline{align-items:baseline !important}.flex-sm-items-stretch{align-items:stretch !important}.flex-sm-content-start{align-content:flex-start !important}.flex-sm-content-end{align-content:flex-end !important}.flex-sm-content-center{align-content:center !important}.flex-sm-content-between{align-content:space-between !important}.flex-sm-content-around{align-content:space-around !important}.flex-sm-content-stretch{align-content:stretch !important}.flex-sm-auto{flex:1 1 auto !important}.flex-sm-shrink-0{flex-shrink:0 !important}.flex-sm-self-auto{align-self:auto !important}.flex-sm-self-start{align-self:flex-start !important}.flex-sm-self-end{align-self:flex-end !important}.flex-sm-self-center{align-self:center !important}.flex-sm-self-baseline{align-self:baseline !important}.flex-sm-self-stretch{align-self:stretch !important}.flex-sm-item-equal{flex-grow:1;flex-basis:0}}@media (min-width: 768px){.flex-md-row{flex-direction:row !important}.flex-md-row-reverse{flex-direction:row-reverse !important}.flex-md-column{flex-direction:column !important}.flex-md-wrap{flex-wrap:wrap !important}.flex-md-nowrap{flex-wrap:nowrap !important}.flex-md-justify-start{justify-content:flex-start !important}.flex-md-justify-end{justify-content:flex-end !important}.flex-md-justify-center{justify-content:center !important}.flex-md-justify-between{justify-content:space-between !important}.flex-md-justify-around{justify-content:space-around !important}.flex-md-items-start{align-items:flex-start !important}.flex-md-items-end{align-items:flex-end !important}.flex-md-items-center{align-items:center !important}.flex-md-items-baseline{align-items:baseline !important}.flex-md-items-stretch{align-items:stretch !important}.flex-md-content-start{align-content:flex-start !important}.flex-md-content-end{align-content:flex-end !important}.flex-md-content-center{align-content:center !important}.flex-md-content-between{align-content:space-between !important}.flex-md-content-around{align-content:space-around !important}.flex-md-content-stretch{align-content:stretch !important}.flex-md-auto{flex:1 1 auto !important}.flex-md-shrink-0{flex-shrink:0 !important}.flex-md-self-auto{align-self:auto !important}.flex-md-self-start{align-self:flex-start !important}.flex-md-self-end{align-self:flex-end !important}.flex-md-self-center{align-self:center !important}.flex-md-self-baseline{align-self:baseline !important}.flex-md-self-stretch{align-self:stretch !important}.flex-md-item-equal{flex-grow:1;flex-basis:0}}@media (min-width: 1012px){.flex-lg-row{flex-direction:row !important}.flex-lg-row-reverse{flex-direction:row-reverse !important}.flex-lg-column{flex-direction:column !important}.flex-lg-wrap{flex-wrap:wrap !important}.flex-lg-nowrap{flex-wrap:nowrap !important}.flex-lg-justify-start{justify-content:flex-start !important}.flex-lg-justify-end{justify-content:flex-end !important}.flex-lg-justify-center{justify-content:center !important}.flex-lg-justify-between{justify-content:space-between !important}.flex-lg-justify-around{justify-content:space-around !important}.flex-lg-items-start{align-items:flex-start !important}.flex-lg-items-end{align-items:flex-end !important}.flex-lg-items-center{align-items:center !important}.flex-lg-items-baseline{align-items:baseline !important}.flex-lg-items-stretch{align-items:stretch !important}.flex-lg-content-start{align-content:flex-start !important}.flex-lg-content-end{align-content:flex-end !important}.flex-lg-content-center{align-content:center !important}.flex-lg-content-between{align-content:space-between !important}.flex-lg-content-around{align-content:space-around !important}.flex-lg-content-stretch{align-content:stretch !important}.flex-lg-auto{flex:1 1 auto !important}.flex-lg-shrink-0{flex-shrink:0 !important}.flex-lg-self-auto{align-self:auto !important}.flex-lg-self-start{align-self:flex-start !important}.flex-lg-self-end{align-self:flex-end !important}.flex-lg-self-center{align-self:center !important}.flex-lg-self-baseline{align-self:baseline !important}.flex-lg-self-stretch{align-self:stretch !important}.flex-lg-item-equal{flex-grow:1;flex-basis:0}}@media (min-width: 1280px){.flex-xl-row{flex-direction:row !important}.flex-xl-row-reverse{flex-direction:row-reverse !important}.flex-xl-column{flex-direction:column !important}.flex-xl-wrap{flex-wrap:wrap !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.flex-xl-justify-start{justify-content:flex-start !important}.flex-xl-justify-end{justify-content:flex-end !important}.flex-xl-justify-center{justify-content:center !important}.flex-xl-justify-between{justify-content:space-between !important}.flex-xl-justify-around{justify-content:space-around !important}.flex-xl-items-start{align-items:flex-start !important}.flex-xl-items-end{align-items:flex-end !important}.flex-xl-items-center{align-items:center !important}.flex-xl-items-baseline{align-items:baseline !important}.flex-xl-items-stretch{align-items:stretch !important}.flex-xl-content-start{align-content:flex-start !important}.flex-xl-content-end{align-content:flex-end !important}.flex-xl-content-center{align-content:center !important}.flex-xl-content-between{align-content:space-between !important}.flex-xl-content-around{align-content:space-around !important}.flex-xl-content-stretch{align-content:stretch !important}.flex-xl-auto{flex:1 1 auto !important}.flex-xl-shrink-0{flex-shrink:0 !important}.flex-xl-self-auto{align-self:auto !important}.flex-xl-self-start{align-self:flex-start !important}.flex-xl-self-end{align-self:flex-end !important}.flex-xl-self-center{align-self:center !important}.flex-xl-self-baseline{align-self:baseline !important}.flex-xl-self-stretch{align-self:stretch !important}.flex-xl-item-equal{flex-grow:1;flex-basis:0}}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.top-0{top:0 !important}.right-0{right:0 !important}.bottom-0{bottom:0 !important}.left-0{left:0 !important}.v-align-middle{vertical-align:middle !important}.v-align-top{vertical-align:top !important}.v-align-bottom{vertical-align:bottom !important}.v-align-text-top{vertical-align:text-top !important}.v-align-text-bottom{vertical-align:text-bottom !important}.v-align-baseline{vertical-align:baseline !important}.overflow-hidden{overflow:hidden !important}.overflow-scroll{overflow:scroll !important}.overflow-auto{overflow:auto !important}.clearfix::before{display:table;content:""}.clearfix::after{display:table;clear:both;content:""}.float-left{float:left !important}.float-right{float:right !important}.float-none{float:none !important}@media (min-width: 544px){.float-sm-left{float:left !important}.float-sm-right{float:right !important}.float-sm-none{float:none !important}}@media (min-width: 768px){.float-md-left{float:left !important}.float-md-right{float:right !important}.float-md-none{float:none !important}}@media (min-width: 1012px){.float-lg-left{float:left !important}.float-lg-right{float:right !important}.float-lg-none{float:none !important}}@media (min-width: 1280px){.float-xl-left{float:left !important}.float-xl-right{float:right !important}.float-xl-none{float:none !important}}.width-fit{max-width:100% !important}.width-full{width:100% !important}.height-fit{max-height:100% !important}.height-full{height:100% !important}.min-width-0{min-width:0 !important}.direction-rtl{direction:rtl !important}.direction-ltr{direction:ltr !important}@media (min-width: 544px){.direction-sm-rtl{direction:rtl !important}.direction-sm-ltr{direction:ltr !important}}@media (min-width: 768px){.direction-md-rtl{direction:rtl !important}.direction-md-ltr{direction:ltr !important}}@media (min-width: 1012px){.direction-lg-rtl{direction:rtl !important}.direction-lg-ltr{direction:ltr !important}}@media (min-width: 1280px){.direction-xl-rtl{direction:rtl !important}.direction-xl-ltr{direction:ltr !important}}.m-0{margin:0 !important}.mt-0{margin-top:0 !important}.mr-0{margin-right:0 !important}.mb-0{margin-bottom:0 !important}.ml-0{margin-left:0 !important}.mx-0{margin-right:0 !important;margin-left:0 !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.m-1{margin:4px !important}.mt-1{margin-top:4px !important}.mr-1{margin-right:4px !important}.mb-1{margin-bottom:4px !important}.ml-1{margin-left:4px !important}.mt-n1{margin-top:-4px !important}.mr-n1{margin-right:-4px !important}.mb-n1{margin-bottom:-4px !important}.ml-n1{margin-left:-4px !important}.mx-1{margin-right:4px !important;margin-left:4px !important}.my-1{margin-top:4px !important;margin-bottom:4px !important}.m-2{margin:8px !important}.mt-2{margin-top:8px !important}.mr-2{margin-right:8px !important}.mb-2{margin-bottom:8px !important}.ml-2{margin-left:8px !important}.mt-n2{margin-top:-8px !important}.mr-n2{margin-right:-8px !important}.mb-n2{margin-bottom:-8px !important}.ml-n2{margin-left:-8px !important}.mx-2{margin-right:8px !important;margin-left:8px !important}.my-2{margin-top:8px !important;margin-bottom:8px !important}.m-3{margin:16px !important}.mt-3{margin-top:16px !important}.mr-3{margin-right:16px !important}.mb-3{margin-bottom:16px !important}.ml-3{margin-left:16px !important}.mt-n3{margin-top:-16px !important}.mr-n3{margin-right:-16px !important}.mb-n3{margin-bottom:-16px !important}.ml-n3{margin-left:-16px !important}.mx-3{margin-right:16px !important;margin-left:16px !important}.my-3{margin-top:16px !important;margin-bottom:16px !important}.m-4{margin:24px !important}.mt-4{margin-top:24px !important}.mr-4{margin-right:24px !important}.mb-4{margin-bottom:24px !important}.ml-4{margin-left:24px !important}.mt-n4{margin-top:-24px !important}.mr-n4{margin-right:-24px !important}.mb-n4{margin-bottom:-24px !important}.ml-n4{margin-left:-24px !important}.mx-4{margin-right:24px !important;margin-left:24px !important}.my-4{margin-top:24px !important;margin-bottom:24px !important}.m-5{margin:32px !important}.mt-5{margin-top:32px !important}.mr-5{margin-right:32px !important}.mb-5{margin-bottom:32px !important}.ml-5{margin-left:32px !important}.mt-n5{margin-top:-32px !important}.mr-n5{margin-right:-32px !important}.mb-n5{margin-bottom:-32px !important}.ml-n5{margin-left:-32px !important}.mx-5{margin-right:32px !important;margin-left:32px !important}.my-5{margin-top:32px !important;margin-bottom:32px !important}.m-6{margin:40px !important}.mt-6{margin-top:40px !important}.mr-6{margin-right:40px !important}.mb-6{margin-bottom:40px !important}.ml-6{margin-left:40px !important}.mt-n6{margin-top:-40px !important}.mr-n6{margin-right:-40px !important}.mb-n6{margin-bottom:-40px !important}.ml-n6{margin-left:-40px !important}.mx-6{margin-right:40px !important;margin-left:40px !important}.my-6{margin-top:40px !important;margin-bottom:40px !important}.mx-auto{margin-right:auto !important;margin-left:auto !important}@media (min-width: 544px){.m-sm-0{margin:0 !important}.mt-sm-0{margin-top:0 !important}.mr-sm-0{margin-right:0 !important}.mb-sm-0{margin-bottom:0 !important}.ml-sm-0{margin-left:0 !important}.mx-sm-0{margin-right:0 !important;margin-left:0 !important}.my-sm-0{margin-top:0 !important;margin-bottom:0 !important}.m-sm-1{margin:4px !important}.mt-sm-1{margin-top:4px !important}.mr-sm-1{margin-right:4px !important}.mb-sm-1{margin-bottom:4px !important}.ml-sm-1{margin-left:4px !important}.mt-sm-n1{margin-top:-4px !important}.mr-sm-n1{margin-right:-4px !important}.mb-sm-n1{margin-bottom:-4px !important}.ml-sm-n1{margin-left:-4px !important}.mx-sm-1{margin-right:4px !important;margin-left:4px !important}.my-sm-1{margin-top:4px !important;margin-bottom:4px !important}.m-sm-2{margin:8px !important}.mt-sm-2{margin-top:8px !important}.mr-sm-2{margin-right:8px !important}.mb-sm-2{margin-bottom:8px !important}.ml-sm-2{margin-left:8px !important}.mt-sm-n2{margin-top:-8px !important}.mr-sm-n2{margin-right:-8px !important}.mb-sm-n2{margin-bottom:-8px !important}.ml-sm-n2{margin-left:-8px !important}.mx-sm-2{margin-right:8px !important;margin-left:8px !important}.my-sm-2{margin-top:8px !important;margin-bottom:8px !important}.m-sm-3{margin:16px !important}.mt-sm-3{margin-top:16px !important}.mr-sm-3{margin-right:16px !important}.mb-sm-3{margin-bottom:16px !important}.ml-sm-3{margin-left:16px !important}.mt-sm-n3{margin-top:-16px !important}.mr-sm-n3{margin-right:-16px !important}.mb-sm-n3{margin-bottom:-16px !important}.ml-sm-n3{margin-left:-16px !important}.mx-sm-3{margin-right:16px !important;margin-left:16px !important}.my-sm-3{margin-top:16px !important;margin-bottom:16px !important}.m-sm-4{margin:24px !important}.mt-sm-4{margin-top:24px !important}.mr-sm-4{margin-right:24px !important}.mb-sm-4{margin-bottom:24px !important}.ml-sm-4{margin-left:24px !important}.mt-sm-n4{margin-top:-24px !important}.mr-sm-n4{margin-right:-24px !important}.mb-sm-n4{margin-bottom:-24px !important}.ml-sm-n4{margin-left:-24px !important}.mx-sm-4{margin-right:24px !important;margin-left:24px !important}.my-sm-4{margin-top:24px !important;margin-bottom:24px !important}.m-sm-5{margin:32px !important}.mt-sm-5{margin-top:32px !important}.mr-sm-5{margin-right:32px !important}.mb-sm-5{margin-bottom:32px !important}.ml-sm-5{margin-left:32px !important}.mt-sm-n5{margin-top:-32px !important}.mr-sm-n5{margin-right:-32px !important}.mb-sm-n5{margin-bottom:-32px !important}.ml-sm-n5{margin-left:-32px !important}.mx-sm-5{margin-right:32px !important;margin-left:32px !important}.my-sm-5{margin-top:32px !important;margin-bottom:32px !important}.m-sm-6{margin:40px !important}.mt-sm-6{margin-top:40px !important}.mr-sm-6{margin-right:40px !important}.mb-sm-6{margin-bottom:40px !important}.ml-sm-6{margin-left:40px !important}.mt-sm-n6{margin-top:-40px !important}.mr-sm-n6{margin-right:-40px !important}.mb-sm-n6{margin-bottom:-40px !important}.ml-sm-n6{margin-left:-40px !important}.mx-sm-6{margin-right:40px !important;margin-left:40px !important}.my-sm-6{margin-top:40px !important;margin-bottom:40px !important}.mx-sm-auto{margin-right:auto !important;margin-left:auto !important}}@media (min-width: 768px){.m-md-0{margin:0 !important}.mt-md-0{margin-top:0 !important}.mr-md-0{margin-right:0 !important}.mb-md-0{margin-bottom:0 !important}.ml-md-0{margin-left:0 !important}.mx-md-0{margin-right:0 !important;margin-left:0 !important}.my-md-0{margin-top:0 !important;margin-bottom:0 !important}.m-md-1{margin:4px !important}.mt-md-1{margin-top:4px !important}.mr-md-1{margin-right:4px !important}.mb-md-1{margin-bottom:4px !important}.ml-md-1{margin-left:4px !important}.mt-md-n1{margin-top:-4px !important}.mr-md-n1{margin-right:-4px !important}.mb-md-n1{margin-bottom:-4px !important}.ml-md-n1{margin-left:-4px !important}.mx-md-1{margin-right:4px !important;margin-left:4px !important}.my-md-1{margin-top:4px !important;margin-bottom:4px !important}.m-md-2{margin:8px !important}.mt-md-2{margin-top:8px !important}.mr-md-2{margin-right:8px !important}.mb-md-2{margin-bottom:8px !important}.ml-md-2{margin-left:8px !important}.mt-md-n2{margin-top:-8px !important}.mr-md-n2{margin-right:-8px !important}.mb-md-n2{margin-bottom:-8px !important}.ml-md-n2{margin-left:-8px !important}.mx-md-2{margin-right:8px !important;margin-left:8px !important}.my-md-2{margin-top:8px !important;margin-bottom:8px !important}.m-md-3{margin:16px !important}.mt-md-3{margin-top:16px !important}.mr-md-3{margin-right:16px !important}.mb-md-3{margin-bottom:16px !important}.ml-md-3{margin-left:16px !important}.mt-md-n3{margin-top:-16px !important}.mr-md-n3{margin-right:-16px !important}.mb-md-n3{margin-bottom:-16px !important}.ml-md-n3{margin-left:-16px !important}.mx-md-3{margin-right:16px !important;margin-left:16px !important}.my-md-3{margin-top:16px !important;margin-bottom:16px !important}.m-md-4{margin:24px !important}.mt-md-4{margin-top:24px !important}.mr-md-4{margin-right:24px !important}.mb-md-4{margin-bottom:24px !important}.ml-md-4{margin-left:24px !important}.mt-md-n4{margin-top:-24px !important}.mr-md-n4{margin-right:-24px !important}.mb-md-n4{margin-bottom:-24px !important}.ml-md-n4{margin-left:-24px !important}.mx-md-4{margin-right:24px !important;margin-left:24px !important}.my-md-4{margin-top:24px !important;margin-bottom:24px !important}.m-md-5{margin:32px !important}.mt-md-5{margin-top:32px !important}.mr-md-5{margin-right:32px !important}.mb-md-5{margin-bottom:32px !important}.ml-md-5{margin-left:32px !important}.mt-md-n5{margin-top:-32px !important}.mr-md-n5{margin-right:-32px !important}.mb-md-n5{margin-bottom:-32px !important}.ml-md-n5{margin-left:-32px !important}.mx-md-5{margin-right:32px !important;margin-left:32px !important}.my-md-5{margin-top:32px !important;margin-bottom:32px !important}.m-md-6{margin:40px !important}.mt-md-6{margin-top:40px !important}.mr-md-6{margin-right:40px !important}.mb-md-6{margin-bottom:40px !important}.ml-md-6{margin-left:40px !important}.mt-md-n6{margin-top:-40px !important}.mr-md-n6{margin-right:-40px !important}.mb-md-n6{margin-bottom:-40px !important}.ml-md-n6{margin-left:-40px !important}.mx-md-6{margin-right:40px !important;margin-left:40px !important}.my-md-6{margin-top:40px !important;margin-bottom:40px !important}.mx-md-auto{margin-right:auto !important;margin-left:auto !important}}@media (min-width: 1012px){.m-lg-0{margin:0 !important}.mt-lg-0{margin-top:0 !important}.mr-lg-0{margin-right:0 !important}.mb-lg-0{margin-bottom:0 !important}.ml-lg-0{margin-left:0 !important}.mx-lg-0{margin-right:0 !important;margin-left:0 !important}.my-lg-0{margin-top:0 !important;margin-bottom:0 !important}.m-lg-1{margin:4px !important}.mt-lg-1{margin-top:4px !important}.mr-lg-1{margin-right:4px !important}.mb-lg-1{margin-bottom:4px !important}.ml-lg-1{margin-left:4px !important}.mt-lg-n1{margin-top:-4px !important}.mr-lg-n1{margin-right:-4px !important}.mb-lg-n1{margin-bottom:-4px !important}.ml-lg-n1{margin-left:-4px !important}.mx-lg-1{margin-right:4px !important;margin-left:4px !important}.my-lg-1{margin-top:4px !important;margin-bottom:4px !important}.m-lg-2{margin:8px !important}.mt-lg-2{margin-top:8px !important}.mr-lg-2{margin-right:8px !important}.mb-lg-2{margin-bottom:8px !important}.ml-lg-2{margin-left:8px !important}.mt-lg-n2{margin-top:-8px !important}.mr-lg-n2{margin-right:-8px !important}.mb-lg-n2{margin-bottom:-8px !important}.ml-lg-n2{margin-left:-8px !important}.mx-lg-2{margin-right:8px !important;margin-left:8px !important}.my-lg-2{margin-top:8px !important;margin-bottom:8px !important}.m-lg-3{margin:16px !important}.mt-lg-3{margin-top:16px !important}.mr-lg-3{margin-right:16px !important}.mb-lg-3{margin-bottom:16px !important}.ml-lg-3{margin-left:16px !important}.mt-lg-n3{margin-top:-16px !important}.mr-lg-n3{margin-right:-16px !important}.mb-lg-n3{margin-bottom:-16px !important}.ml-lg-n3{margin-left:-16px !important}.mx-lg-3{margin-right:16px !important;margin-left:16px !important}.my-lg-3{margin-top:16px !important;margin-bottom:16px !important}.m-lg-4{margin:24px !important}.mt-lg-4{margin-top:24px !important}.mr-lg-4{margin-right:24px !important}.mb-lg-4{margin-bottom:24px !important}.ml-lg-4{margin-left:24px !important}.mt-lg-n4{margin-top:-24px !important}.mr-lg-n4{margin-right:-24px !important}.mb-lg-n4{margin-bottom:-24px !important}.ml-lg-n4{margin-left:-24px !important}.mx-lg-4{margin-right:24px !important;margin-left:24px !important}.my-lg-4{margin-top:24px !important;margin-bottom:24px !important}.m-lg-5{margin:32px !important}.mt-lg-5{margin-top:32px !important}.mr-lg-5{margin-right:32px !important}.mb-lg-5{margin-bottom:32px !important}.ml-lg-5{margin-left:32px !important}.mt-lg-n5{margin-top:-32px !important}.mr-lg-n5{margin-right:-32px !important}.mb-lg-n5{margin-bottom:-32px !important}.ml-lg-n5{margin-left:-32px !important}.mx-lg-5{margin-right:32px !important;margin-left:32px !important}.my-lg-5{margin-top:32px !important;margin-bottom:32px !important}.m-lg-6{margin:40px !important}.mt-lg-6{margin-top:40px !important}.mr-lg-6{margin-right:40px !important}.mb-lg-6{margin-bottom:40px !important}.ml-lg-6{margin-left:40px !important}.mt-lg-n6{margin-top:-40px !important}.mr-lg-n6{margin-right:-40px !important}.mb-lg-n6{margin-bottom:-40px !important}.ml-lg-n6{margin-left:-40px !important}.mx-lg-6{margin-right:40px !important;margin-left:40px !important}.my-lg-6{margin-top:40px !important;margin-bottom:40px !important}.mx-lg-auto{margin-right:auto !important;margin-left:auto !important}}@media (min-width: 1280px){.m-xl-0{margin:0 !important}.mt-xl-0{margin-top:0 !important}.mr-xl-0{margin-right:0 !important}.mb-xl-0{margin-bottom:0 !important}.ml-xl-0{margin-left:0 !important}.mx-xl-0{margin-right:0 !important;margin-left:0 !important}.my-xl-0{margin-top:0 !important;margin-bottom:0 !important}.m-xl-1{margin:4px !important}.mt-xl-1{margin-top:4px !important}.mr-xl-1{margin-right:4px !important}.mb-xl-1{margin-bottom:4px !important}.ml-xl-1{margin-left:4px !important}.mt-xl-n1{margin-top:-4px !important}.mr-xl-n1{margin-right:-4px !important}.mb-xl-n1{margin-bottom:-4px !important}.ml-xl-n1{margin-left:-4px !important}.mx-xl-1{margin-right:4px !important;margin-left:4px !important}.my-xl-1{margin-top:4px !important;margin-bottom:4px !important}.m-xl-2{margin:8px !important}.mt-xl-2{margin-top:8px !important}.mr-xl-2{margin-right:8px !important}.mb-xl-2{margin-bottom:8px !important}.ml-xl-2{margin-left:8px !important}.mt-xl-n2{margin-top:-8px !important}.mr-xl-n2{margin-right:-8px !important}.mb-xl-n2{margin-bottom:-8px !important}.ml-xl-n2{margin-left:-8px !important}.mx-xl-2{margin-right:8px !important;margin-left:8px !important}.my-xl-2{margin-top:8px !important;margin-bottom:8px !important}.m-xl-3{margin:16px !important}.mt-xl-3{margin-top:16px !important}.mr-xl-3{margin-right:16px !important}.mb-xl-3{margin-bottom:16px !important}.ml-xl-3{margin-left:16px !important}.mt-xl-n3{margin-top:-16px !important}.mr-xl-n3{margin-right:-16px !important}.mb-xl-n3{margin-bottom:-16px !important}.ml-xl-n3{margin-left:-16px !important}.mx-xl-3{margin-right:16px !important;margin-left:16px !important}.my-xl-3{margin-top:16px !important;margin-bottom:16px !important}.m-xl-4{margin:24px !important}.mt-xl-4{margin-top:24px !important}.mr-xl-4{margin-right:24px !important}.mb-xl-4{margin-bottom:24px !important}.ml-xl-4{margin-left:24px !important}.mt-xl-n4{margin-top:-24px !important}.mr-xl-n4{margin-right:-24px !important}.mb-xl-n4{margin-bottom:-24px !important}.ml-xl-n4{margin-left:-24px !important}.mx-xl-4{margin-right:24px !important;margin-left:24px !important}.my-xl-4{margin-top:24px !important;margin-bottom:24px !important}.m-xl-5{margin:32px !important}.mt-xl-5{margin-top:32px !important}.mr-xl-5{margin-right:32px !important}.mb-xl-5{margin-bottom:32px !important}.ml-xl-5{margin-left:32px !important}.mt-xl-n5{margin-top:-32px !important}.mr-xl-n5{margin-right:-32px !important}.mb-xl-n5{margin-bottom:-32px !important}.ml-xl-n5{margin-left:-32px !important}.mx-xl-5{margin-right:32px !important;margin-left:32px !important}.my-xl-5{margin-top:32px !important;margin-bottom:32px !important}.m-xl-6{margin:40px !important}.mt-xl-6{margin-top:40px !important}.mr-xl-6{margin-right:40px !important}.mb-xl-6{margin-bottom:40px !important}.ml-xl-6{margin-left:40px !important}.mt-xl-n6{margin-top:-40px !important}.mr-xl-n6{margin-right:-40px !important}.mb-xl-n6{margin-bottom:-40px !important}.ml-xl-n6{margin-left:-40px !important}.mx-xl-6{margin-right:40px !important;margin-left:40px !important}.my-xl-6{margin-top:40px !important;margin-bottom:40px !important}.mx-xl-auto{margin-right:auto !important;margin-left:auto !important}}.p-0{padding:0 !important}.pt-0{padding-top:0 !important}.pr-0{padding-right:0 !important}.pb-0{padding-bottom:0 !important}.pl-0{padding-left:0 !important}.px-0{padding-right:0 !important;padding-left:0 !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.p-1{padding:4px !important}.pt-1{padding-top:4px !important}.pr-1{padding-right:4px !important}.pb-1{padding-bottom:4px !important}.pl-1{padding-left:4px !important}.px-1{padding-right:4px !important;padding-left:4px !important}.py-1{padding-top:4px !important;padding-bottom:4px !important}.p-2{padding:8px !important}.pt-2{padding-top:8px !important}.pr-2{padding-right:8px !important}.pb-2{padding-bottom:8px !important}.pl-2{padding-left:8px !important}.px-2{padding-right:8px !important;padding-left:8px !important}.py-2{padding-top:8px !important;padding-bottom:8px !important}.p-3{padding:16px !important}.pt-3{padding-top:16px !important}.pr-3{padding-right:16px !important}.pb-3{padding-bottom:16px !important}.pl-3{padding-left:16px !important}.px-3{padding-right:16px !important;padding-left:16px !important}.py-3{padding-top:16px !important;padding-bottom:16px !important}.p-4{padding:24px !important}.pt-4{padding-top:24px !important}.pr-4{padding-right:24px !important}.pb-4{padding-bottom:24px !important}.pl-4{padding-left:24px !important}.px-4{padding-right:24px !important;padding-left:24px !important}.py-4{padding-top:24px !important;padding-bottom:24px !important}.p-5{padding:32px !important}.pt-5{padding-top:32px !important}.pr-5{padding-right:32px !important}.pb-5{padding-bottom:32px !important}.pl-5{padding-left:32px !important}.px-5{padding-right:32px !important;padding-left:32px !important}.py-5{padding-top:32px !important;padding-bottom:32px !important}.p-6{padding:40px !important}.pt-6{padding-top:40px !important}.pr-6{padding-right:40px !important}.pb-6{padding-bottom:40px !important}.pl-6{padding-left:40px !important}.px-6{padding-right:40px !important;padding-left:40px !important}.py-6{padding-top:40px !important;padding-bottom:40px !important}@media (min-width: 544px){.p-sm-0{padding:0 !important}.pt-sm-0{padding-top:0 !important}.pr-sm-0{padding-right:0 !important}.pb-sm-0{padding-bottom:0 !important}.pl-sm-0{padding-left:0 !important}.px-sm-0{padding-right:0 !important;padding-left:0 !important}.py-sm-0{padding-top:0 !important;padding-bottom:0 !important}.p-sm-1{padding:4px !important}.pt-sm-1{padding-top:4px !important}.pr-sm-1{padding-right:4px !important}.pb-sm-1{padding-bottom:4px !important}.pl-sm-1{padding-left:4px !important}.px-sm-1{padding-right:4px !important;padding-left:4px !important}.py-sm-1{padding-top:4px !important;padding-bottom:4px !important}.p-sm-2{padding:8px !important}.pt-sm-2{padding-top:8px !important}.pr-sm-2{padding-right:8px !important}.pb-sm-2{padding-bottom:8px !important}.pl-sm-2{padding-left:8px !important}.px-sm-2{padding-right:8px !important;padding-left:8px !important}.py-sm-2{padding-top:8px !important;padding-bottom:8px !important}.p-sm-3{padding:16px !important}.pt-sm-3{padding-top:16px !important}.pr-sm-3{padding-right:16px !important}.pb-sm-3{padding-bottom:16px !important}.pl-sm-3{padding-left:16px !important}.px-sm-3{padding-right:16px !important;padding-left:16px !important}.py-sm-3{padding-top:16px !important;padding-bottom:16px !important}.p-sm-4{padding:24px !important}.pt-sm-4{padding-top:24px !important}.pr-sm-4{padding-right:24px !important}.pb-sm-4{padding-bottom:24px !important}.pl-sm-4{padding-left:24px !important}.px-sm-4{padding-right:24px !important;padding-left:24px !important}.py-sm-4{padding-top:24px !important;padding-bottom:24px !important}.p-sm-5{padding:32px !important}.pt-sm-5{padding-top:32px !important}.pr-sm-5{padding-right:32px !important}.pb-sm-5{padding-bottom:32px !important}.pl-sm-5{padding-left:32px !important}.px-sm-5{padding-right:32px !important;padding-left:32px !important}.py-sm-5{padding-top:32px !important;padding-bottom:32px !important}.p-sm-6{padding:40px !important}.pt-sm-6{padding-top:40px !important}.pr-sm-6{padding-right:40px !important}.pb-sm-6{padding-bottom:40px !important}.pl-sm-6{padding-left:40px !important}.px-sm-6{padding-right:40px !important;padding-left:40px !important}.py-sm-6{padding-top:40px !important;padding-bottom:40px !important}}@media (min-width: 768px){.p-md-0{padding:0 !important}.pt-md-0{padding-top:0 !important}.pr-md-0{padding-right:0 !important}.pb-md-0{padding-bottom:0 !important}.pl-md-0{padding-left:0 !important}.px-md-0{padding-right:0 !important;padding-left:0 !important}.py-md-0{padding-top:0 !important;padding-bottom:0 !important}.p-md-1{padding:4px !important}.pt-md-1{padding-top:4px !important}.pr-md-1{padding-right:4px !important}.pb-md-1{padding-bottom:4px !important}.pl-md-1{padding-left:4px !important}.px-md-1{padding-right:4px !important;padding-left:4px !important}.py-md-1{padding-top:4px !important;padding-bottom:4px !important}.p-md-2{padding:8px !important}.pt-md-2{padding-top:8px !important}.pr-md-2{padding-right:8px !important}.pb-md-2{padding-bottom:8px !important}.pl-md-2{padding-left:8px !important}.px-md-2{padding-right:8px !important;padding-left:8px !important}.py-md-2{padding-top:8px !important;padding-bottom:8px !important}.p-md-3{padding:16px !important}.pt-md-3{padding-top:16px !important}.pr-md-3{padding-right:16px !important}.pb-md-3{padding-bottom:16px !important}.pl-md-3{padding-left:16px !important}.px-md-3{padding-right:16px !important;padding-left:16px !important}.py-md-3{padding-top:16px !important;padding-bottom:16px !important}.p-md-4{padding:24px !important}.pt-md-4{padding-top:24px !important}.pr-md-4{padding-right:24px !important}.pb-md-4{padding-bottom:24px !important}.pl-md-4{padding-left:24px !important}.px-md-4{padding-right:24px !important;padding-left:24px !important}.py-md-4{padding-top:24px !important;padding-bottom:24px !important}.p-md-5{padding:32px !important}.pt-md-5{padding-top:32px !important}.pr-md-5{padding-right:32px !important}.pb-md-5{padding-bottom:32px !important}.pl-md-5{padding-left:32px !important}.px-md-5{padding-right:32px !important;padding-left:32px !important}.py-md-5{padding-top:32px !important;padding-bottom:32px !important}.p-md-6{padding:40px !important}.pt-md-6{padding-top:40px !important}.pr-md-6{padding-right:40px !important}.pb-md-6{padding-bottom:40px !important}.pl-md-6{padding-left:40px !important}.px-md-6{padding-right:40px !important;padding-left:40px !important}.py-md-6{padding-top:40px !important;padding-bottom:40px !important}}@media (min-width: 1012px){.p-lg-0{padding:0 !important}.pt-lg-0{padding-top:0 !important}.pr-lg-0{padding-right:0 !important}.pb-lg-0{padding-bottom:0 !important}.pl-lg-0{padding-left:0 !important}.px-lg-0{padding-right:0 !important;padding-left:0 !important}.py-lg-0{padding-top:0 !important;padding-bottom:0 !important}.p-lg-1{padding:4px !important}.pt-lg-1{padding-top:4px !important}.pr-lg-1{padding-right:4px !important}.pb-lg-1{padding-bottom:4px !important}.pl-lg-1{padding-left:4px !important}.px-lg-1{padding-right:4px !important;padding-left:4px !important}.py-lg-1{padding-top:4px !important;padding-bottom:4px !important}.p-lg-2{padding:8px !important}.pt-lg-2{padding-top:8px !important}.pr-lg-2{padding-right:8px !important}.pb-lg-2{padding-bottom:8px !important}.pl-lg-2{padding-left:8px !important}.px-lg-2{padding-right:8px !important;padding-left:8px !important}.py-lg-2{padding-top:8px !important;padding-bottom:8px !important}.p-lg-3{padding:16px !important}.pt-lg-3{padding-top:16px !important}.pr-lg-3{padding-right:16px !important}.pb-lg-3{padding-bottom:16px !important}.pl-lg-3{padding-left:16px !important}.px-lg-3{padding-right:16px !important;padding-left:16px !important}.py-lg-3{padding-top:16px !important;padding-bottom:16px !important}.p-lg-4{padding:24px !important}.pt-lg-4{padding-top:24px !important}.pr-lg-4{padding-right:24px !important}.pb-lg-4{padding-bottom:24px !important}.pl-lg-4{padding-left:24px !important}.px-lg-4{padding-right:24px !important;padding-left:24px !important}.py-lg-4{padding-top:24px !important;padding-bottom:24px !important}.p-lg-5{padding:32px !important}.pt-lg-5{padding-top:32px !important}.pr-lg-5{padding-right:32px !important}.pb-lg-5{padding-bottom:32px !important}.pl-lg-5{padding-left:32px !important}.px-lg-5{padding-right:32px !important;padding-left:32px !important}.py-lg-5{padding-top:32px !important;padding-bottom:32px !important}.p-lg-6{padding:40px !important}.pt-lg-6{padding-top:40px !important}.pr-lg-6{padding-right:40px !important}.pb-lg-6{padding-bottom:40px !important}.pl-lg-6{padding-left:40px !important}.px-lg-6{padding-right:40px !important;padding-left:40px !important}.py-lg-6{padding-top:40px !important;padding-bottom:40px !important}}@media (min-width: 1280px){.p-xl-0{padding:0 !important}.pt-xl-0{padding-top:0 !important}.pr-xl-0{padding-right:0 !important}.pb-xl-0{padding-bottom:0 !important}.pl-xl-0{padding-left:0 !important}.px-xl-0{padding-right:0 !important;padding-left:0 !important}.py-xl-0{padding-top:0 !important;padding-bottom:0 !important}.p-xl-1{padding:4px !important}.pt-xl-1{padding-top:4px !important}.pr-xl-1{padding-right:4px !important}.pb-xl-1{padding-bottom:4px !important}.pl-xl-1{padding-left:4px !important}.px-xl-1{padding-right:4px !important;padding-left:4px !important}.py-xl-1{padding-top:4px !important;padding-bottom:4px !important}.p-xl-2{padding:8px !important}.pt-xl-2{padding-top:8px !important}.pr-xl-2{padding-right:8px !important}.pb-xl-2{padding-bottom:8px !important}.pl-xl-2{padding-left:8px !important}.px-xl-2{padding-right:8px !important;padding-left:8px !important}.py-xl-2{padding-top:8px !important;padding-bottom:8px !important}.p-xl-3{padding:16px !important}.pt-xl-3{padding-top:16px !important}.pr-xl-3{padding-right:16px !important}.pb-xl-3{padding-bottom:16px !important}.pl-xl-3{padding-left:16px !important}.px-xl-3{padding-right:16px !important;padding-left:16px !important}.py-xl-3{padding-top:16px !important;padding-bottom:16px !important}.p-xl-4{padding:24px !important}.pt-xl-4{padding-top:24px !important}.pr-xl-4{padding-right:24px !important}.pb-xl-4{padding-bottom:24px !important}.pl-xl-4{padding-left:24px !important}.px-xl-4{padding-right:24px !important;padding-left:24px !important}.py-xl-4{padding-top:24px !important;padding-bottom:24px !important}.p-xl-5{padding:32px !important}.pt-xl-5{padding-top:32px !important}.pr-xl-5{padding-right:32px !important}.pb-xl-5{padding-bottom:32px !important}.pl-xl-5{padding-left:32px !important}.px-xl-5{padding-right:32px !important;padding-left:32px !important}.py-xl-5{padding-top:32px !important;padding-bottom:32px !important}.p-xl-6{padding:40px !important}.pt-xl-6{padding-top:40px !important}.pr-xl-6{padding-right:40px !important}.pb-xl-6{padding-bottom:40px !important}.pl-xl-6{padding-left:40px !important}.px-xl-6{padding-right:40px !important;padding-left:40px !important}.py-xl-6{padding-top:40px !important;padding-bottom:40px !important}}.p-responsive{padding-right:16px !important;padding-left:16px !important}@media (min-width: 544px){.p-responsive{padding-right:40px !important;padding-left:40px !important}}@media (min-width: 1012px){.p-responsive{padding-right:16px !important;padding-left:16px !important}}.h1{font-size:26px !important}@media (min-width: 768px){.h1{font-size:32px !important}}.h2{font-size:22px !important}@media (min-width: 768px){.h2{font-size:24px !important}}.h3{font-size:18px !important}@media (min-width: 768px){.h3{font-size:20px !important}}.h4{font-size:16px !important}.h5{font-size:14px !important}.h6{font-size:12px !important}.h1,.h2,.h3,.h4,.h5,.h6{font-weight:600 !important}.f1{font-size:26px !important}@media (min-width: 768px){.f1{font-size:32px !important}}.f2{font-size:22px !important}@media (min-width: 768px){.f2{font-size:24px !important}}.f3{font-size:18px !important}@media (min-width: 768px){.f3{font-size:20px !important}}.f4{font-size:16px !important}@media (min-width: 768px){.f4{font-size:16px !important}}.f5{font-size:14px !important}.f6{font-size:12px !important}.f00-light{font-size:40px !important;font-weight:300 !important}@media (min-width: 768px){.f00-light{font-size:48px !important}}.f0-light{font-size:32px !important;font-weight:300 !important}@media (min-width: 768px){.f0-light{font-size:40px !important}}.f1-light{font-size:26px !important;font-weight:300 !important}@media (min-width: 768px){.f1-light{font-size:32px !important}}.f2-light{font-size:22px !important;font-weight:300 !important}@media (min-width: 768px){.f2-light{font-size:24px !important}}.f3-light{font-size:18px !important;font-weight:300 !important}@media (min-width: 768px){.f3-light{font-size:20px !important}}.text-small{font-size:12px !important}.lead{margin-bottom:30px;font-size:20px;font-weight:300;color:#586069}.lh-condensed-ultra{line-height:1 !important}.lh-condensed{line-height:1.25 !important}.lh-default{line-height:1.5 !important}.lh-0{line-height:0 !important}.text-right{text-align:right !important}.text-left{text-align:left !important}.text-center{text-align:center !important}@media (min-width: 544px){.text-sm-right{text-align:right !important}.text-sm-left{text-align:left !important}.text-sm-center{text-align:center !important}}@media (min-width: 768px){.text-md-right{text-align:right !important}.text-md-left{text-align:left !important}.text-md-center{text-align:center !important}}@media (min-width: 1012px){.text-lg-right{text-align:right !important}.text-lg-left{text-align:left !important}.text-lg-center{text-align:center !important}}@media (min-width: 1280px){.text-xl-right{text-align:right !important}.text-xl-left{text-align:left !important}.text-xl-center{text-align:center !important}}.text-normal{font-weight:400 !important}.text-bold{font-weight:600 !important}.text-italic{font-style:italic !important}.text-uppercase{text-transform:uppercase !important}.text-underline{text-decoration:underline !important}.no-underline{text-decoration:none !important}.no-wrap{white-space:nowrap !important}.ws-normal{white-space:normal !important}.wb-break-all{word-break:break-all !important}.text-emphasized{font-weight:600;color:#24292e}.list-style-none{list-style:none !important}.text-shadow-dark{text-shadow:0 1px 1px rgba(27,31,35,0.25),0 1px 25px rgba(27,31,35,0.75)}.text-shadow-light{text-shadow:0 1px 0 rgba(255,255,255,0.5)}.text-mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace}.user-select-none{user-select:none !important}.d-block{display:block !important}.d-flex{display:flex !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-inline-flex{display:inline-flex !important}.d-none{display:none !important}.d-table{display:table !important}.d-table-cell{display:table-cell !important}@media (min-width: 544px){.d-sm-block{display:block !important}.d-sm-flex{display:flex !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-inline-flex{display:inline-flex !important}.d-sm-none{display:none !important}.d-sm-table{display:table !important}.d-sm-table-cell{display:table-cell !important}}@media (min-width: 768px){.d-md-block{display:block !important}.d-md-flex{display:flex !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-inline-flex{display:inline-flex !important}.d-md-none{display:none !important}.d-md-table{display:table !important}.d-md-table-cell{display:table-cell !important}}@media (min-width: 1012px){.d-lg-block{display:block !important}.d-lg-flex{display:flex !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-inline-flex{display:inline-flex !important}.d-lg-none{display:none !important}.d-lg-table{display:table !important}.d-lg-table-cell{display:table-cell !important}}@media (min-width: 1280px){.d-xl-block{display:block !important}.d-xl-flex{display:flex !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-inline-flex{display:inline-flex !important}.d-xl-none{display:none !important}.d-xl-table{display:table !important}.d-xl-table-cell{display:table-cell !important}}.v-hidden{visibility:hidden !important}.v-visible{visibility:visible !important}@media (max-width: 544px){.hide-sm{display:none !important}}@media (min-width: 544px) and (max-width: 768px){.hide-md{display:none !important}}@media (min-width: 768px) and (max-width: 1012px){.hide-lg{display:none !important}}@media (min-width: 1012px){.hide-xl{display:none !important}}.table-fixed{table-layout:fixed !important}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);word-wrap:normal;border:0}.show-on-focus{position:absolute;width:1px;height:1px;margin:0;overflow:hidden;clip:rect(1px, 1px, 1px, 1px)}.show-on-focus:focus{z-index:20;width:auto;height:auto;clip:auto}.container{width:980px;margin-right:auto;margin-left:auto}.container::before{display:table;content:""}.container::after{display:table;clear:both;content:""}.container-md{max-width:768px;margin-right:auto;margin-left:auto}.container-lg{max-width:1012px;margin-right:auto;margin-left:auto}.container-xl{max-width:1280px;margin-right:auto;margin-left:auto}.columns{margin-right:-10px;margin-left:-10px}.columns::before{display:table;content:""}.columns::after{display:table;clear:both;content:""}.column{float:left;padding-right:10px;padding-left:10px}.one-third{width:33.333333%}.two-thirds{width:66.666667%}.one-fourth{width:25%}.one-half{width:50%}.three-fourths{width:75%}.one-fifth{width:20%}.four-fifths{width:80%}.centered{display:block;float:none;margin-right:auto;margin-left:auto}.col-1{width:8.3333333333%}.col-2{width:16.6666666667%}.col-3{width:25%}.col-4{width:33.3333333333%}.col-5{width:41.6666666667%}.col-6{width:50%}.col-7{width:58.3333333333%}.col-8{width:66.6666666667%}.col-9{width:75%}.col-10{width:83.3333333333%}.col-11{width:91.6666666667%}.col-12{width:100%}@media (min-width: 544px){.col-sm-1{width:8.3333333333%}.col-sm-2{width:16.6666666667%}.col-sm-3{width:25%}.col-sm-4{width:33.3333333333%}.col-sm-5{width:41.6666666667%}.col-sm-6{width:50%}.col-sm-7{width:58.3333333333%}.col-sm-8{width:66.6666666667%}.col-sm-9{width:75%}.col-sm-10{width:83.3333333333%}.col-sm-11{width:91.6666666667%}.col-sm-12{width:100%}}@media (min-width: 768px){.col-md-1{width:8.3333333333%}.col-md-2{width:16.6666666667%}.col-md-3{width:25%}.col-md-4{width:33.3333333333%}.col-md-5{width:41.6666666667%}.col-md-6{width:50%}.col-md-7{width:58.3333333333%}.col-md-8{width:66.6666666667%}.col-md-9{width:75%}.col-md-10{width:83.3333333333%}.col-md-11{width:91.6666666667%}.col-md-12{width:100%}}@media (min-width: 1012px){.col-lg-1{width:8.3333333333%}.col-lg-2{width:16.6666666667%}.col-lg-3{width:25%}.col-lg-4{width:33.3333333333%}.col-lg-5{width:41.6666666667%}.col-lg-6{width:50%}.col-lg-7{width:58.3333333333%}.col-lg-8{width:66.6666666667%}.col-lg-9{width:75%}.col-lg-10{width:83.3333333333%}.col-lg-11{width:91.6666666667%}.col-lg-12{width:100%}}@media (min-width: 1280px){.col-xl-1{width:8.3333333333%}.col-xl-2{width:16.6666666667%}.col-xl-3{width:25%}.col-xl-4{width:33.3333333333%}.col-xl-5{width:41.6666666667%}.col-xl-6{width:50%}.col-xl-7{width:58.3333333333%}.col-xl-8{width:66.6666666667%}.col-xl-9{width:75%}.col-xl-10{width:83.3333333333%}.col-xl-11{width:91.6666666667%}.col-xl-12{width:100%}}.gutter{margin-right:-16px;margin-left:-16px}.gutter>[class*="col-"]{padding-right:16px !important;padding-left:16px !important}.gutter-condensed{margin-right:-8px;margin-left:-8px}.gutter-condensed>[class*="col-"]{padding-right:8px !important;padding-left:8px !important}.gutter-spacious{margin-right:-24px;margin-left:-24px}.gutter-spacious>[class*="col-"]{padding-right:24px !important;padding-left:24px !important}@media (min-width: 544px){.gutter-sm{margin-right:-16px;margin-left:-16px}.gutter-sm>[class*="col-"]{padding-right:16px !important;padding-left:16px !important}.gutter-sm-condensed{margin-right:-8px;margin-left:-8px}.gutter-sm-condensed>[class*="col-"]{padding-right:8px !important;padding-left:8px !important}.gutter-sm-spacious{margin-right:-24px;margin-left:-24px}.gutter-sm-spacious>[class*="col-"]{padding-right:24px !important;padding-left:24px !important}}@media (min-width: 768px){.gutter-md{margin-right:-16px;margin-left:-16px}.gutter-md>[class*="col-"]{padding-right:16px !important;padding-left:16px !important}.gutter-md-condensed{margin-right:-8px;margin-left:-8px}.gutter-md-condensed>[class*="col-"]{padding-right:8px !important;padding-left:8px !important}.gutter-md-spacious{margin-right:-24px;margin-left:-24px}.gutter-md-spacious>[class*="col-"]{padding-right:24px !important;padding-left:24px !important}}@media (min-width: 1012px){.gutter-lg{margin-right:-16px;margin-left:-16px}.gutter-lg>[class*="col-"]{padding-right:16px !important;padding-left:16px !important}.gutter-lg-condensed{margin-right:-8px;margin-left:-8px}.gutter-lg-condensed>[class*="col-"]{padding-right:8px !important;padding-left:8px !important}.gutter-lg-spacious{margin-right:-24px;margin-left:-24px}.gutter-lg-spacious>[class*="col-"]{padding-right:24px !important;padding-left:24px !important}}@media (min-width: 1280px){.gutter-xl{margin-right:-16px;margin-left:-16px}.gutter-xl>[class*="col-"]{padding-right:16px !important;padding-left:16px !important}.gutter-xl-condensed{margin-right:-8px;margin-left:-8px}.gutter-xl-condensed>[class*="col-"]{padding-right:8px !important;padding-left:8px !important}.gutter-xl-spacious{margin-right:-24px;margin-left:-24px}.gutter-xl-spacious>[class*="col-"]{padding-right:24px !important;padding-left:24px !important}}.offset-1{margin-left:8.3333333333% !important}.offset-2{margin-left:16.6666666667% !important}.offset-3{margin-left:25% !important}.offset-4{margin-left:33.3333333333% !important}.offset-5{margin-left:41.6666666667% !important}.offset-6{margin-left:50% !important}.offset-7{margin-left:58.3333333333% !important}.offset-8{margin-left:66.6666666667% !important}.offset-9{margin-left:75% !important}.offset-10{margin-left:83.3333333333% !important}.offset-11{margin-left:91.6666666667% !important}@media (min-width: 544px){.offset-sm-1{margin-left:8.3333333333% !important}.offset-sm-2{margin-left:16.6666666667% !important}.offset-sm-3{margin-left:25% !important}.offset-sm-4{margin-left:33.3333333333% !important}.offset-sm-5{margin-left:41.6666666667% !important}.offset-sm-6{margin-left:50% !important}.offset-sm-7{margin-left:58.3333333333% !important}.offset-sm-8{margin-left:66.6666666667% !important}.offset-sm-9{margin-left:75% !important}.offset-sm-10{margin-left:83.3333333333% !important}.offset-sm-11{margin-left:91.6666666667% !important}}@media (min-width: 768px){.offset-md-1{margin-left:8.3333333333% !important}.offset-md-2{margin-left:16.6666666667% !important}.offset-md-3{margin-left:25% !important}.offset-md-4{margin-left:33.3333333333% !important}.offset-md-5{margin-left:41.6666666667% !important}.offset-md-6{margin-left:50% !important}.offset-md-7{margin-left:58.3333333333% !important}.offset-md-8{margin-left:66.6666666667% !important}.offset-md-9{margin-left:75% !important}.offset-md-10{margin-left:83.3333333333% !important}.offset-md-11{margin-left:91.6666666667% !important}}@media (min-width: 1012px){.offset-lg-1{margin-left:8.3333333333% !important}.offset-lg-2{margin-left:16.6666666667% !important}.offset-lg-3{margin-left:25% !important}.offset-lg-4{margin-left:33.3333333333% !important}.offset-lg-5{margin-left:41.6666666667% !important}.offset-lg-6{margin-left:50% !important}.offset-lg-7{margin-left:58.3333333333% !important}.offset-lg-8{margin-left:66.6666666667% !important}.offset-lg-9{margin-left:75% !important}.offset-lg-10{margin-left:83.3333333333% !important}.offset-lg-11{margin-left:91.6666666667% !important}}@media (min-width: 1280px){.offset-xl-1{margin-left:8.3333333333% !important}.offset-xl-2{margin-left:16.6666666667% !important}.offset-xl-3{margin-left:25% !important}.offset-xl-4{margin-left:33.3333333333% !important}.offset-xl-5{margin-left:41.6666666667% !important}.offset-xl-6{margin-left:50% !important}.offset-xl-7{margin-left:58.3333333333% !important}.offset-xl-8{margin-left:66.6666666667% !important}.offset-xl-9{margin-left:75% !important}.offset-xl-10{margin-left:83.3333333333% !important}.offset-xl-11{margin-left:91.6666666667% !important}}.markdown-body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:16px;line-height:1.5;word-wrap:break-word}.markdown-body::before{display:table;content:""}.markdown-body::after{display:table;clear:both;content:""}.markdown-body>*:first-child{margin-top:0 !important}.markdown-body>*:last-child{margin-bottom:0 !important}.markdown-body a:not([href]){color:inherit;text-decoration:none}.markdown-body .absent{color:#cb2431}.markdown-body .anchor{float:left;padding-right:4px;margin-left:-20px;line-height:1}.markdown-body .anchor:focus{outline:none}.markdown-body p,.markdown-body blockquote,.markdown-body ul,.markdown-body ol,.markdown-body dl,.markdown-body table,.markdown-body pre{margin-top:0;margin-bottom:16px}.markdown-body hr{height:.25em;padding:0;margin:24px 0;background-color:#e1e4e8;border:0}.markdown-body blockquote{padding:0 1em;color:#6a737d;border-left:0.25em solid #dfe2e5}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body kbd{display:inline-block;padding:3px 5px;font-size:11px;line-height:10px;color:#444d56;vertical-align:middle;background-color:#fafbfc;border:solid 1px #c6cbd1;border-bottom-color:#959da5;border-radius:3px;box-shadow:inset 0 -1px 0 #959da5}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#1b1f23;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1 tt,.markdown-body h1 code,.markdown-body h2 tt,.markdown-body h2 code,.markdown-body h3 tt,.markdown-body h3 code,.markdown-body h4 tt,.markdown-body h4 code,.markdown-body h5 tt,.markdown-body h5 code,.markdown-body h6 tt,.markdown-body h6 code{font-size:inherit}.markdown-body h1{padding-bottom:0.3em;font-size:2em;border-bottom:1px solid #eaecef}.markdown-body h2{padding-bottom:0.3em;font-size:1.5em;border-bottom:1px solid #eaecef}.markdown-body h3{font-size:1.25em}.markdown-body h4{font-size:1em}.markdown-body h5{font-size:0.875em}.markdown-body h6{font-size:0.85em;color:#6a737d}.markdown-body ul,.markdown-body ol{padding-left:2em}.markdown-body ul.no-list,.markdown-body ol.no-list{padding:0;list-style-type:none}.markdown-body ul ul,.markdown-body ul ol,.markdown-body ol ol,.markdown-body ol ul{margin-top:0;margin-bottom:0}.markdown-body li{word-wrap:break-all}.markdown-body li>p{margin-top:16px}.markdown-body li+li{margin-top:.25em}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:600}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body table{display:block;width:100%;overflow:auto}.markdown-body table th{font-weight:600}.markdown-body table th,.markdown-body table td{padding:6px 13px;border:1px solid #dfe2e5}.markdown-body table tr{background-color:#fff;border-top:1px solid #c6cbd1}.markdown-body table tr:nth-child(2n){background-color:#f6f8fa}.markdown-body table img{background-color:transparent}.markdown-body img{max-width:100%;box-sizing:content-box;background-color:#fff}.markdown-body img[align=right]{padding-left:20px}.markdown-body img[align=left]{padding-right:20px}.markdown-body .emoji{max-width:none;vertical-align:text-top;background-color:transparent}.markdown-body span.frame{display:block;overflow:hidden}.markdown-body span.frame>span{display:block;float:left;width:auto;padding:7px;margin:13px 0 0;overflow:hidden;border:1px solid #dfe2e5}.markdown-body span.frame span img{display:block;float:left}.markdown-body span.frame span span{display:block;padding:5px 0 0;clear:both;color:#24292e}.markdown-body span.align-center{display:block;overflow:hidden;clear:both}.markdown-body span.align-center>span{display:block;margin:13px auto 0;overflow:hidden;text-align:center}.markdown-body span.align-center span img{margin:0 auto;text-align:center}.markdown-body span.align-right{display:block;overflow:hidden;clear:both}.markdown-body span.align-right>span{display:block;margin:13px 0 0;overflow:hidden;text-align:right}.markdown-body span.align-right span img{margin:0;text-align:right}.markdown-body span.float-left{display:block;float:left;margin-right:13px;overflow:hidden}.markdown-body span.float-left span{margin:13px 0 0}.markdown-body span.float-right{display:block;float:right;margin-left:13px;overflow:hidden}.markdown-body span.float-right>span{display:block;margin:13px auto 0;overflow:hidden;text-align:right}.markdown-body code,.markdown-body tt{padding:0.2em 0.4em;margin:0;font-size:85%;background-color:rgba(27,31,35,0.05);border-radius:3px}.markdown-body code br,.markdown-body tt br{display:none}.markdown-body del code{text-decoration:inherit}.markdown-body pre{word-wrap:normal}.markdown-body pre>code{padding:0;margin:0;font-size:100%;word-break:normal;white-space:pre;background:transparent;border:0}.markdown-body .highlight{margin-bottom:16px}.markdown-body .highlight pre{margin-bottom:0;word-break:normal}.markdown-body .highlight pre,.markdown-body pre{padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f6f8fa;border-radius:3px}.markdown-body pre code,.markdown-body pre tt{display:inline;max-width:auto;padding:0;margin:0;overflow:visible;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-body .csv-data td,.markdown-body .csv-data th{padding:5px;overflow:hidden;font-size:12px;line-height:1;text-align:left;white-space:nowrap}.markdown-body .csv-data .blob-num{padding:10px 8px 9px;text-align:right;background:#fff;border:0}.markdown-body .csv-data tr{border-top:0}.markdown-body .csv-data th{font-weight:600;background:#f6f8fa;border-top:0}.highlight table td{padding:5px}.highlight table pre{margin:0}.highlight .cm{color:#999988;font-style:italic}.highlight .cp{color:#999999;font-weight:bold}.highlight .c1{color:#999988;font-style:italic}.highlight .cs{color:#999999;font-weight:bold;font-style:italic}.highlight .c,.highlight .cd{color:#999988;font-style:italic}.highlight .err{color:#a61717;background-color:#e3d2d2}.highlight .gd{color:#000000;background-color:#ffdddd}.highlight .ge{color:#000000;font-style:italic}.highlight .gr{color:#aa0000}.highlight .gh{color:#999999}.highlight .gi{color:#000000;background-color:#ddffdd}.highlight .go{color:#888888}.highlight .gp{color:#555555}.highlight .gs{font-weight:bold}.highlight .gu{color:#aaaaaa}.highlight .gt{color:#aa0000}.highlight .kc{color:#000000;font-weight:bold}.highlight .kd{color:#000000;font-weight:bold}.highlight .kn{color:#000000;font-weight:bold}.highlight .kp{color:#000000;font-weight:bold}.highlight .kr{color:#000000;font-weight:bold}.highlight .kt{color:#445588;font-weight:bold}.highlight .k,.highlight .kv{color:#000000;font-weight:bold}.highlight .mf{color:#009999}.highlight .mh{color:#009999}.highlight .il{color:#009999}.highlight .mi{color:#009999}.highlight .mo{color:#009999}.highlight .m,.highlight .mb,.highlight .mx{color:#009999}.highlight .sb{color:#d14}.highlight .sc{color:#d14}.highlight .sd{color:#d14}.highlight .s2{color:#d14}.highlight .se{color:#d14}.highlight .sh{color:#d14}.highlight .si{color:#d14}.highlight .sx{color:#d14}.highlight .sr{color:#009926}.highlight .s1{color:#d14}.highlight .ss{color:#990073}.highlight .s{color:#d14}.highlight .na{color:#008080}.highlight .bp{color:#999999}.highlight .nb{color:#0086B3}.highlight .nc{color:#445588;font-weight:bold}.highlight .no{color:#008080}.highlight .nd{color:#3c5d5d;font-weight:bold}.highlight .ni{color:#800080}.highlight .ne{color:#990000;font-weight:bold}.highlight .nf{color:#990000;font-weight:bold}.highlight .nl{color:#990000;font-weight:bold}.highlight .nn{color:#555555}.highlight .nt{color:#000080}.highlight .vc{color:#008080}.highlight .vg{color:#008080}.highlight .vi{color:#008080}.highlight .nv{color:#008080}.highlight .ow{color:#000000;font-weight:bold}.highlight .o{color:#000000;font-weight:bold}.highlight .w{color:#bbbbbb}.highlight{background-color:#f8f8f8} 2 | 3 | body { 4 | display: flex; 5 | min-height: 100vh; 6 | flex-direction: column; 7 | align-items: center; 8 | } 9 | 10 | .markdown-body { 11 | font-family: sans-serif; 12 | flex: 1; 13 | width: 100%; 14 | max-width: 1012px; 15 | } 16 | 17 | .markdown-body tt, .markdown-body code { 18 | font-family: monospace; 19 | font-size: 90% 20 | } 21 | 22 | .hidden { 23 | display: none !important; 24 | } 25 | 26 | .flex { 27 | display: flex !important; 28 | } 29 | 30 | :root { 31 | --color-bg: #ffffff; 32 | --color-bg-secondary: #f4f4f4; 33 | --color-border: #ddd; 34 | --color-shadowed: #f7f7f7; 35 | 36 | --color-green: #2ea44f; 37 | --color-green-active: #258540; 38 | --color-green-disabled: #94d3a2; 39 | --color-red: #cb2431; 40 | --color-red-disabled: #e25c67; 41 | 42 | --color-text-light: #888; 43 | --color-text-secondary: #444; 44 | --color-text-stress: #000; 45 | --color-text: #222; 46 | --color-text-white: white; 47 | } 48 | 49 | #paste-input-panel, #paste-setting-panel, #paste-uploaded-panel { 50 | border: 1px var(--color-border) solid; 51 | margin: 0.5rem 0; 52 | } 53 | 54 | #paste-header { 55 | background-color: var(--color-shadowed); 56 | display: flex; 57 | border-bottom: 1px var(--color-border) 1px; 58 | } 59 | 60 | #paste-header > div:last-child { 61 | border-bottom: 1px var(--color-border) solid; 62 | flex: 1; 63 | } 64 | 65 | .paste-header-tabs { 66 | display: flex; 67 | } 68 | 69 | .paste-tab { 70 | font-size: 13px; 71 | cursor: pointer; 72 | display: inline-block; 73 | padding: 0.6rem 0.5rem 0.7rem; 74 | background-color: inherit; 75 | color: var(--color-text-secondary); 76 | font-weight: 400; 77 | border: 1px transparent solid; 78 | border-bottom-color: var(--color-border); 79 | border-right-color: var(--color-border); 80 | transition: color 0.1s; 81 | } 82 | 83 | .paste-tab:hover { 84 | color: var(--color-text) 85 | } 86 | 87 | .paste-tab.enabled { 88 | border-bottom-color: transparent; 89 | background-color: var(--color-bg); 90 | color: var(--color-text-stress); 91 | font-weight: 600; 92 | } 93 | 94 | .paste-tab:last-child { 95 | border-right-color: transparent; 96 | } 97 | 98 | .paste-tab.enabled:last-child { 99 | border-right-color: var(--color-border); 100 | } 101 | 102 | .paste-tab-page { 103 | display: none; 104 | } 105 | 106 | .paste-tab-page.enabled { 107 | display: inherit; 108 | } 109 | 110 | #paste-textarea { 111 | width: 100%; 112 | font-family: monospace; 113 | font-size: 12px; 114 | border: none; 115 | outline: none; 116 | resize: vertical; 117 | } 118 | 119 | #paste-file-upload { 120 | width: 0; 121 | height: 0; 122 | opacity: 0; 123 | overflow: hidden; 124 | position: absolute; 125 | z-index: -1; 126 | } 127 | 128 | #paste-file-upload + label { 129 | cursor: pointer; 130 | height: 100%; 131 | } 132 | 133 | #paste-uploaded-panel, #paste-setting-panel { 134 | padding: 1rem; 135 | } 136 | 137 | #paste-uploaded-panel > h2, #paste-setting-panel > h2 { 138 | margin: 0; 139 | border: none; 140 | } 141 | 142 | #paste-file-line { 143 | margin: 1rem; 144 | } 145 | 146 | .label-line { 147 | display: block; 148 | margin: 0.3rem 0; 149 | } 150 | 151 | .small-label { 152 | display: block; 153 | color: var(--color-text-light); 154 | font-size: 11px; 155 | } 156 | 157 | .radio-label { 158 | font-size: 14px; 159 | } 160 | 161 | .paste-setting-subitem-panel { 162 | margin: 0.8rem 0; 163 | } 164 | 165 | .paste-setting-subitem-panel > input { 166 | border: 1px var(--color-border) solid; 167 | padding: 0.15rem 0.3rem; 168 | outline: none; 169 | font-size: 15px; 170 | } 171 | 172 | .paste-setting-subitem-panel > input::placeholder { 173 | color: var(--color-text-light) 174 | } 175 | 176 | #paste-url-input-wrapper { 177 | display: inline-block; 178 | border: 1px var(--color-border) solid; 179 | background-color: var(--color-bg); 180 | font-size: 13px; 181 | border-radius: 2px; 182 | min-width: 10em; 183 | } 184 | 185 | #paste-url-input-prefix { 186 | padding-left: 0.5rem; 187 | color: var(--color-text-light); 188 | user-select: none; 189 | } 190 | 191 | #paste-custom-url-input { 192 | border: none; 193 | border-left: 1px var(--color-border) solid; 194 | color: var(--color-text); 195 | background-color: inherit; 196 | padding: 0.1rem; 197 | outline: none; 198 | } 199 | 200 | #paste-admin-url-input { 201 | border: 1px var(--color-border) solid; 202 | background-color: var(--color-bg); 203 | outline: none; 204 | font-size: 13px; 205 | width: 30em; 206 | max-width: 80vw; 207 | border-radius: 2px; 208 | } 209 | 210 | input:not(:checked) ~ #paste-url-input-wrapper, 211 | input:not(:checked) ~ #paste-admin-url-input { 212 | display: none; 213 | } 214 | 215 | #submit-button, #delete-button { 216 | font-size: 12px; 217 | border: none; 218 | border-radius: 5px; 219 | color: var(--color-text-white); 220 | cursor: not-allowed; 221 | } 222 | 223 | @media (max-width: 544px) { 224 | .long-button { 225 | width: 100%; 226 | } 227 | } 228 | 229 | #submit-button { 230 | background-color: var(--color-green-disabled); 231 | transition: background-color 0.15s; 232 | } 233 | 234 | #submit-button.enabled:hover { 235 | background-color: var(--color-green-active); 236 | } 237 | 238 | #submit-button.enabled { 239 | background-color: var(--color-green); 240 | cursor: pointer; 241 | } 242 | 243 | #delete-button { 244 | background-color: var(--color-shadowed); 245 | color: var(--color-red-disabled); 246 | transition: background-color 0.15s; 247 | } 248 | 249 | #delete-button.enabled { 250 | color: var(--color-red); 251 | cursor: pointer; 252 | } 253 | 254 | #delete-button.enabled:hover { 255 | background-color: var(--color-red); 256 | color: var(--color-text-white); 257 | } 258 | 259 | .uploaded-entry { 260 | margin-top: 0.7rem; 261 | display: flex; 262 | align-items: center; 263 | border: 1px var(--color-border) solid; 264 | } 265 | 266 | .uploaded-entry > input { 267 | flex: 1; 268 | width: 100%; 269 | height: 2.0rem; 270 | border: none; 271 | outline: none; 272 | padding-left: 0.3rem; 273 | } 274 | 275 | .copy-button { 276 | padding: 0 0.5rem; 277 | border: none; 278 | background-color: var(--color-green); 279 | color: var(--color-text-white); 280 | font-size: 13px; 281 | height: 2.0rem; 282 | transition: background-color 0.15s; 283 | } 284 | 285 | .copy-button:hover { 286 | background-color: var(--color-green-active); 287 | } 288 | 289 | footer { 290 | color: var(--color-text-light); 291 | font-size: 12px; 292 | width: 100%; 293 | max-width: 1012px; 294 | } 295 | -------------------------------------------------------------------------------- /frontend/tos.md: -------------------------------------------------------------------------------- 1 | # TERMS AND CONDITIONS 2 | 3 | > TL;DR: **No fucking warranty at all, EXCEPT AS ENFORCED BY LAW**. 4 | 5 | Last updated: 2021-08-08 6 | 7 | ## 1. Introduction 8 | 9 | Welcome to the pastebin maintained by **Sharzy** (“Company”, “we”, “our”, “us”)! 10 | 11 | These Terms of Service (“Terms”, “Terms of Service”) govern your use of our website located at **shz.al** (together or individually “Service”) operated by **Sharzy**. 12 | 13 | Our Privacy Policy also governs your use of our Service and explains how we collect, safeguard and disclose information that results from your use of our web pages. 14 | 15 | Your agreement with us includes these Terms and our Privacy Policy (“Agreements”). You acknowledge that you have read and understood Agreements, and agree to be bound of them. 16 | 17 | If you do not agree with (or cannot comply with) Agreements, then you may not use the Service, but please let us know by emailing at **shz.al@sharzy.in** so we can try to find a solution. These Terms apply to all visitors, users and others who wish to access or use Service. 18 | 19 | ## 2. Content 20 | 21 | Our Service allows you to post, link, store, share and otherwise make available certain information, text, graphics, videos, or other material (“Content”). You are responsible for Content that you post on or through Service, including its legality, reliability, and appropriateness. 22 | 23 | By posting Content on or through Service, You represent and warrant that: (i) Content is yours (you own it) and/or you have the right to use it and the right to grant us the rights and license as provided in these Terms, and (ii) that the posting of your Content on or through Service does not violate the privacy rights, publicity rights, copyrights, contract rights or any other rights of any person or entity. We reserve the right to terminate the account of anyone found to be infringing on a copyright. 24 | 25 | You retain any and all of your rights to any Content you submit, post or display on or through Service and you are responsible for protecting those rights. We take no responsibility and assume no liability for Content you or any third party posts on or through Service. However, by posting Content using Service you grant us the right and license to use, modify, publicly perform, publicly display, reproduce, and distribute such Content on and through Service. You agree that this license includes the right for us to make your Content available to other users of Service, who may also use your Content subject to these Terms. 26 | 27 | Sharzy has the right but not the obligation to monitor and edit all Content provided by users. 28 | 29 | In addition, Content found on or through this Service are the property of Sharzy or used with permission. You may not distribute, modify, transmit, reuse, download, repost, copy, or use said Content, whether in whole or in part, for commercial purposes or for personal gain, without express advance written permission from us. 30 | 31 | ## 3. Prohibited Uses 32 | 33 | You may use Service only for lawful purposes and in accordance with Terms. You agree not to use Service: 34 | 35 | 3.1. In any way that violates any applicable national or international law or regulation. 36 | 37 | 3.2. For the purpose of exploiting, harming, or attempting to exploit or harm minors in any way by exposing them to inappropriate content or otherwise. 38 | 39 | 3.3. To transmit, or procure the sending of, any advertising or promotional material, including any “junk mail”, “chain letter,” “spam,” or any other similar solicitation. 40 | 41 | 3.4. To impersonate or attempt to impersonate Company, a Company employee, another user, or any other person or entity. 42 | 43 | 3.5. In any way that infringes upon the rights of others, or in any way is illegal, threatening, fraudulent, or harmful, or in connection with any unlawful, illegal, fraudulent, or harmful purpose or activity. 44 | 45 | 3.6. To engage in any other conduct that restricts or inhibits anyone’s use or enjoyment of Service, or which, as determined by us, may harm or offend Company or users of Service or expose them to liability. 46 | 47 | Additionally, you agree not to: 48 | 49 | 3.1. Use Service in any manner that could disable, overburden, damage, or impair Service or interfere with any other party’s use of Service, including their ability to engage in real time activities through Service. 50 | 51 | 3.2. Use any robot, spider, or other automatic device, process, or means to access Service for any purpose, including monitoring or copying any of the material on Service. 52 | 53 | 3.3. Use any manual process to monitor or copy any of the material on Service or for any other unauthorized purpose without our prior written consent. 54 | 55 | 3.4. Use any device, software, or routine that interferes with the proper working of Service. 56 | 57 | 3.5. Introduce any viruses, trojan horses, worms, logic bombs, or other material which is malicious or technologically harmful. 58 | 59 | 3.6. Attempt to gain unauthorized access to, interfere with, damage, or disrupt any parts of Service, the server on which Service is stored, or any server, computer, or database connected to Service. 60 | 61 | 3.7. Attack Service via a denial-of-service attack or a distributed denial-of-service attack. 62 | 63 | 3.8. Take any action that may damage or falsify Company rating. 64 | 65 | 3.9. Otherwise attempt to interfere with the proper working of Service. 66 | 67 | ## 4. Analytics 68 | 69 | We may use third-party Service Providers to monitor and analyze the use of our Service. 70 | 71 | ## 5. Intellectual Property 72 | 73 | Service and its original content (excluding Content provided by users), features and functionality are and will remain the exclusive property of Sharzy and its licensors. Service is protected by copyright, trademark, and other laws of and foreign countries. Our trademarks may not be used in connection with any product or service without the prior written consent of Sharzy. 74 | 75 | ## 6. Copyright Policy 76 | 77 | We respect the intellectual property rights of others. It is our policy to respond to any claim that Content posted on Service infringes on the copyright or other intellectual property rights (“Infringement”) of any person or entity. 78 | 79 | If you are a copyright owner, or authorized on behalf of one, and you believe that the copyrighted work has been copied in a way that constitutes copyright infringement, please submit your claim via email to shz.al@sharzy.in, with the subject line: “Copyright Infringement” and include in your claim a detailed description of the alleged Infringement as detailed below, under “DMCA Notice and Procedure for Copyright Infringement Claims” 80 | 81 | You may be held accountable for damages (including costs and attorneys’ fees) for misrepresentation or bad-faith claims on the infringement of any Content found on and/or through Service on your copyright. 82 | 83 | ## 7. DMCA Notice and Procedure for Copyright Infringement Claims 84 | 85 | You may submit a notification pursuant to the Digital Millennium Copyright Act (DMCA) by providing our Copyright Agent with the following information in writing (see 17 U.S.C 512(c)(3) for further detail): 86 | 87 | 7.1. an electronic or physical signature of the person authorized to act on behalf of the owner of the copyright’s interest; 88 | 89 | 7.2. a description of the copyrighted work that you claim has been infringed, including the URL (i.e., web page address) of the location where the copyrighted work exists or a copy of the copyrighted work; 90 | 91 | 7.3. identification of the URL or other specific location on Service where the material that you claim is infringing is located; 92 | 93 | 7.4. your address, telephone number, and email address; 94 | 95 | 7.5. a statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agent, or the law; 96 | 97 | 7.6. a statement by you, made under penalty of perjury, that the above information in your notice is accurate and that you are the copyright owner or authorized to act on the copyright owner’s behalf. 98 | 99 | You can contact our Copyright Agent via email at shz.al@sharzy.in. 100 | 101 | ## 8. Error Reporting and Feedback 102 | 103 | You may provide us either directly at shz.al@sharzy.in or via third party sites and tools with information and feedback concerning errors, suggestions for improvements, ideas, problems, complaints, and other matters related to our Service (“Feedback”). You acknowledge and agree that: (i) you shall not retain, acquire or assert any intellectual property right or other right, title or interest in or to the Feedback; (ii) Company may have development ideas similar to the Feedback; (iii) Feedback does not contain confidential information or proprietary information from you or any third party; and (iv) Company is not under any obligation of confidentiality with respect to the Feedback. In the event the transfer of the ownership to the Feedback is not possible due to applicable mandatory laws, you grant Company and its affiliates an exclusive, transferable, irrevocable, free-of-charge, sub-licensable, unlimited and perpetual right to use (including copy, modify, create derivative works, publish, distribute and commercialize) Feedback in any manner and for any purpose. 104 | 105 | ## 9. Links To Other Web Sites 106 | 107 | Our Service may contain links to third party web sites or services that are not owned or controlled by Sharzy. 108 | 109 | Sharzy has no control over, and assumes no responsibility for the content, privacy policies, or practices of any third party web sites or services. We do not warrant the offerings of any of these entities/individuals or their websites. 110 | 111 | YOU ACKNOWLEDGE AND AGREE THAT COMPANY SHALL NOT BE RESPONSIBLE OR LIABLE, DIRECTLY OR INDIRECTLY, FOR ANY DAMAGE OR LOSS CAUSED OR ALLEGED TO BE CAUSED BY OR IN CONNECTION WITH USE OF OR RELIANCE ON ANY SUCH CONTENT, GOODS OR SERVICES AVAILABLE ON OR THROUGH ANY SUCH THIRD PARTY WEB SITES OR SERVICES. 112 | 113 | WE STRONGLY ADVISE YOU TO READ THE TERMS OF SERVICE AND PRIVACY POLICIES OF ANY THIRD PARTY WEB SITES OR SERVICES THAT YOU VISIT. 114 | 115 | ## 10. Disclaimer Of Warranty 116 | 117 | THESE SERVICES ARE PROVIDED BY COMPANY ON AN “AS IS” AND “AS AVAILABLE” BASIS. COMPANY MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, AS TO THE OPERATION OF THEIR SERVICES, OR THE INFORMATION, CONTENT OR MATERIALS INCLUDED THEREIN. YOU EXPRESSLY AGREE THAT YOUR USE OF THESE SERVICES, THEIR CONTENT, AND ANY SERVICES OR ITEMS OBTAINED FROM US IS AT YOUR SOLE RISK. 118 | 119 | NEITHER COMPANY NOR ANY PERSON ASSOCIATED WITH COMPANY MAKES ANY WARRANTY OR REPRESENTATION WITH RESPECT TO THE COMPLETENESS, SECURITY, RELIABILITY, QUALITY, ACCURACY, OR AVAILABILITY OF THE SERVICES. WITHOUT LIMITING THE FOREGOING, NEITHER COMPANY NOR ANYONE ASSOCIATED WITH COMPANY REPRESENTS OR WARRANTS THAT THE SERVICES, THEIR CONTENT, OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE SERVICES WILL BE ACCURATE, RELIABLE, ERROR-FREE, OR UNINTERRUPTED, THAT DEFECTS WILL BE CORRECTED, THAT THE SERVICES OR THE SERVER THAT MAKES IT AVAILABLE ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS OR THAT THE SERVICES OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE SERVICES WILL OTHERWISE MEET YOUR NEEDS OR EXPECTATIONS. 120 | 121 | COMPANY HEREBY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND FITNESS FOR PARTICULAR PURPOSE. 122 | 123 | THE FOREGOING DOES NOT AFFECT ANY WARRANTIES WHICH CANNOT BE EXCLUDED OR LIMITED UNDER APPLICABLE LAW. 124 | 125 | ## 11. Limitation Of Liability 126 | 127 | EXCEPT AS PROHIBITED BY LAW, YOU WILL HOLD US AND OUR OFFICERS, DIRECTORS, EMPLOYEES, AND AGENTS HARMLESS FOR ANY INDIRECT, PUNITIVE, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGE, HOWEVER IT ARISES (INCLUDING ATTORNEYS’ FEES AND ALL RELATED COSTS AND EXPENSES OF LITIGATION AND ARBITRATION, OR AT TRIAL OR ON APPEAL, IF ANY, WHETHER OR NOT LITIGATION OR ARBITRATION IS INSTITUTED), WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE, OR OTHER TORTIOUS ACTION, OR ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT, INCLUDING WITHOUT LIMITATION ANY CLAIM FOR PERSONAL INJURY OR PROPERTY DAMAGE, ARISING FROM THIS AGREEMENT AND ANY VIOLATION BY YOU OF ANY FEDERAL, STATE, OR LOCAL LAWS, STATUTES, RULES, OR REGULATIONS, EVEN IF COMPANY HAS BEEN PREVIOUSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. EXCEPT AS PROHIBITED BY LAW, IF THERE IS LIABILITY FOUND ON THE PART OF COMPANY, IT WILL BE LIMITED TO THE AMOUNT PAID FOR THE PRODUCTS AND/OR SERVICES, AND UNDER NO CIRCUMSTANCES WILL THERE BE CONSEQUENTIAL OR PUNITIVE DAMAGES. SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION OF PUNITIVE, INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THE PRIOR LIMITATION OR EXCLUSION MAY NOT APPLY TO YOU. 128 | 129 | ## 12. Termination 130 | 131 | We may terminate or suspend your account and bar access to Service immediately, without prior notice or liability, under our sole discretion, for any reason whatsoever and without limitation, including but not limited to a breach of Terms. 132 | 133 | If you wish to terminate your account, you may simply discontinue using Service. 134 | 135 | All provisions of Terms which by their nature should survive termination shall survive termination, including, without limitation, ownership provisions, warranty disclaimers, indemnity and limitations of liability. 136 | 137 | ## 13. Governing Law 138 | 139 | These Terms shall be governed and construed in accordance with the laws of China, which governing law applies to agreement without regard to its conflict of law provisions. 140 | 141 | Our failure to enforce any right or provision of these Terms will not be considered a waiver of those rights. If any provision of these Terms is held to be invalid or unenforceable by a court, the remaining provisions of these Terms will remain in effect. These Terms constitute the entire agreement between us regarding our Service and supersede and replace any prior agreements we might have had between us regarding Service. 142 | 143 | ## 14. Changes To Service 144 | 145 | We reserve the right to withdraw or amend our Service, and any service or material we provide via Service, in our sole discretion without notice. We will not be liable if for any reason all or any part of Service is unavailable at any time or for any period. From time to time, we may restrict access to some parts of Service, or the entire Service, to users, including registered users. 146 | 147 | ## 15. Amendments To Terms 148 | 149 | We may amend Terms at any time by posting the amended terms on this site. It is your responsibility to review these Terms periodically. 150 | 151 | Your continued use of the Platform following the posting of revised Terms means that you accept and agree to the changes. You are expected to check this page frequently so you are aware of any changes, as they are binding on you. 152 | 153 | By continuing to access or use our Service after any revisions become effective, you agree to be bound by the revised terms. If you do not agree to the new terms, you are no longer authorized to use Service. 154 | 155 | ## 16. Waiver And Severability 156 | 157 | No waiver by Company of any term or condition set forth in Terms shall be deemed a further or continuing waiver of such term or condition or a waiver of any other term or condition, and any failure of Company to assert a right or provision under Terms shall not constitute a waiver of such right or provision. 158 | 159 | If any provision of Terms is held by a court or other tribunal of competent jurisdiction to be invalid, illegal or unenforceable for any reason, such provision shall be eliminated or limited to the minimum extent such that the remaining provisions of Terms will continue in full force and effect. 160 | 161 | ## 17. Acknowledgement 162 | 163 | BY USING SERVICE OR OTHER SERVICES PROVIDED BY US, YOU ACKNOWLEDGE THAT YOU HAVE READ THESE TERMS OF SERVICE AND AGREE TO BE BOUND BY THEM. 164 | 165 | ## 18. Contact Us 166 | 167 | Please send your feedback, comments, requests for technical support by email: **shz.al@sharzy.in**. 168 | 169 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "pb", 4 | "version": "1.0.0", 5 | "description": "Pastebin based on Cloudflare worker", 6 | "repository": "https://github.com/SharzyL/pastebin-worker", 7 | "main": "dist/worker.js", 8 | "type": "module", 9 | "scripts": { 10 | "test": "webpack --mode=development && mocha", 11 | "build": "webpack --mode=production", 12 | "serve": "miniflare --live-reload" 13 | }, 14 | "author": "SharzyL ", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "argparse": "^2.0.1", 18 | "eslint": "^8.33.0", 19 | "liquidjs": "^10.4.0", 20 | "miniflare": "^2.11.0", 21 | "mocha": "^10.2.0", 22 | "prettier": "^2.8.3", 23 | "remark-cli": "^11.0.0", 24 | "remark-html": "^15.0.1", 25 | "webpack": "^5.75.0", 26 | "webpack-cli": "^5.0.1", 27 | "wrangler": "^2.8.1" 28 | }, 29 | "dependencies": { 30 | "assert": "^2.0.0", 31 | "mdast-util-to-string": "^3.1.1", 32 | "mime": "^3.0.0", 33 | "mime-db": "^1.52.0", 34 | "rehype-stringify": "^9.0.3", 35 | "remark-gfm": "^3.0.1", 36 | "remark-parse": "^10.0.1", 37 | "remark-rehype": "^10.1.0", 38 | "unified": "^10.1.2" 39 | }, 40 | "resolutions": { 41 | "micromark": "^3.1.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Scripts of pastebin-worker 2 | 3 | This directory contains a set of scripts that facilitate the usage and development of pastebin-worker. 4 | 5 | ## `pb`: paste things on command line 6 | 7 | This is a wrapper script to make it easier to use our pastebin. 8 | 9 | **Requirements**: `bash`, `jq`, `getopt`, `curl` 10 | 11 | **Installation**: download `pb` to your `PATH` and give it execution permission. For example: 12 | 13 | ```shell 14 | $ wget https://github.com/SharzyL/pastebin-worker/raw/master/scripts/pb 15 | $ install -Dm755 pb ~/.local/bin 16 | ``` 17 | 18 | **Zsh completion**: download `_pb` in a folder within your zsh `fpath` 19 | 20 | **fish completion**: download `pb.fish` in a folder within your fish `fish_complete_path` 21 | 22 | **Usage**: 23 | 24 | ```text 25 | $ pb -h 26 | Usage: 27 | pb [-h|--help] 28 | print this help message 29 | 30 | pb [p|post] [OPTIONS] [-f] FILE 31 | upload your text to pastebin, if neither 'FILE' and 'CONTENT' are given, 32 | read the paste from stdin. 33 | 34 | pb [u|update] NAME[:PASSWD] 35 | Update your text to pastebin, if neither 'FILE' and 'CONTENT' are given, 36 | read the paste from stdin. If 'PASSWD' is not given, try to read password 37 | from the history file. 38 | 39 | pb [g|get] [OPTIONS] NAME[.EXT] 40 | fetch the paste with name 'NAME' and extension 'EXT' 41 | 42 | pb [d|delete] [OPTIONS] NAME 43 | delete the paste with name 'NAME' 44 | 45 | Options: 46 | post options: 47 | -c, --content CONTENT the content of the paste 48 | -e, --expire SECONDS the expiration time of the paste (in seconds) 49 | -n, --name NAME the name of the paste 50 | -s, --passwd PASSWD the password 51 | -p, --private make the generated paste name longer for better privacy 52 | -x, --clip clip the url to the clipboard 53 | 54 | update options: 55 | -f, --file FILE read the paste from file 56 | -c, --content CONTENT the content of the paste 57 | -e, --expire SECONDS the expiration time of the paste (in seconds) 58 | -s, --passwd PASSWD the password 59 | -x, --clip clip the url to the clipboard 60 | 61 | get options: 62 | -l, --lang LANG highlight the paste with language 'LANG' in a web page 63 | -m, --mime MIME set the 'Content-Type' header according to mime-type MIME 64 | -o, --output FILE output the paste in file 'FILE' 65 | -u, --url make a 302 URL redirection 66 | 67 | delete options: 68 | none 69 | 70 | general options: 71 | -v, --verbose display the 'underlying' curl command 72 | -d, --dry do a dry run, executing no 'curl' command at all 73 | ``` 74 | 75 | ## `deploy-static.sh` 76 | 77 | Deploy html files to Cloudflare KV storage. The map of KV storage key and web page is recorded in `src/staticPages.js`. Usage: 78 | 79 | ```shell 80 | deploy-static.sh [--preview] [...] 81 | ``` 82 | 83 | ## `md2html.sh` 84 | 85 | Convert markdown to HTML, with a GitHub style CSS. Usage: 86 | 87 | ```shell 88 | md2html.sh 89 | ``` 90 | 91 | ## `post-commit` 92 | 93 | A git hook that deploy the code after each commit on `master` branch. 94 | 95 | ## `render.js` 96 | 97 | A wrapper to render [LiquidJS](https://liquidjs.com) template file. 98 | -------------------------------------------------------------------------------- /scripts/_pb: -------------------------------------------------------------------------------- 1 | #compdef pb 2 | 3 | local -a root_args=( 4 | '(-d --dry)'{-d,--dry}'[dry run]' 5 | '(-v --verbose)'{-v,--verbose}'[verbose]' 6 | '(-h --help)'{-h,--help}'[print help]' 7 | '1:command:->commands' 8 | '*::arguments:->arguments' 9 | ) 10 | 11 | _arguments -s $root_args && return 0 12 | 13 | case "$state" in 14 | (commands) 15 | cmdlist=( 16 | {p,post}":Post paste" 17 | {u,update}":Update paste" 18 | {d,delete}":Delete paste" 19 | {g,get}":Get paste" 20 | ) 21 | _describe -t pb-commands 'pb.sh command' cmdlist 22 | ;; 23 | (arguments) 24 | local -a common_args=( 25 | '(-d --dry)'{-d,--dry}'[dry run]' 26 | '(-v --verbose)'{-v,--verbose}'[verbose]' 27 | ) 28 | 29 | case ${line[1]} in 30 | (p|post) 31 | local -a args=( 32 | '(-c --content -)'{-c,--content}'[Content of paste]' 33 | '(-c --content -)-[Read the paste from stdin]' 34 | '(-c --content -)*:paste from file:_files' 35 | '(-e --expire)'{-e,--expire}'[Expiration time]:expiration' 36 | '(-n --name)'{-n,--name}'[Name]:Name of paste' 37 | '(-s --passwd)'{-s,--passwd}'[Password]:Password of paste' 38 | '(-p --private)'{-p,--private}'[Make generated paste name longer for privacy]' 39 | '(-x --clip)'{-x,--clip}'[Clip the url to the clipboard]' 40 | ) 41 | _arguments -s $args $common_args 42 | ;; 43 | 44 | (u|update) 45 | local -a args=( 46 | '(-f -c --file --content -)'{-c,--content}'[Read the paste from file]' 47 | '(-f -c --file --content -)'{-f,--file}'[Content of the paste]:paste from file:_files' 48 | '*:paste name' 49 | 50 | '(-e --expire)'{-e,--expire}'[Expiration time]:expiration' 51 | '(-s --passwd)'{-s,--passwd}'[Password]:Password of paste' 52 | '(-x --clip)'{-x,--clip}'[Clip the url to the clipboard]' 53 | ) 54 | _arguments -s $args $common_args 55 | ;; 56 | 57 | (g|get) 58 | local -a args=( 59 | '*:paste name' 60 | 61 | '(-l --lang)'{-l,--lang}'[Highlight with language in a web page]:language' 62 | '(-m --mine)'{-m,--mine}'[Content of the paste]:mimetype' 63 | '(-o --output)'{-o,--output}'[Output the paste in file]:file:_files' 64 | '(-u --url)'{-u,--url}'[Make a 302 redirection]' 65 | ) 66 | _arguments -s $args $common_args 67 | ;; 68 | 69 | (d|delete) 70 | _arguments -s $common_args 71 | ;; 72 | esac 73 | esac 74 | 75 | -------------------------------------------------------------------------------- /scripts/deploy-static.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # deploy the static pages on Cloudflare workers 3 | # add argument --preview to deploy on preview namespace 4 | 5 | set -e 6 | 7 | declare -a args=('yarn' 'wrangler' 'kv:key' 'put' '--binding=PB') 8 | 9 | if [ "$1" == '--preview' ]; then 10 | args+=('--env' 'preview') 11 | shift 12 | fi 13 | 14 | declare -a files=("$@") 15 | 16 | set -x 17 | for file in "${files[@]}"; do 18 | file_base=$(basename "$file") 19 | "${args[@]}" "${file_base%.html}" --path "$file" 20 | done 21 | -------------------------------------------------------------------------------- /scripts/md2html.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | md_file="$1" 4 | html_file="$2" 5 | title="$3" 6 | 7 | cat > "$html_file" <<-EOF 8 | 9 | 10 | 11 | 12 | $title 13 | 14 | 15 | 16 | 17 | 18 |
19 | EOF 20 | 21 | yarn -s remark "$md_file" --use remark-html >> "$html_file" 22 | 23 | cat >> "$html_file" <<-EOF 24 |
25 | 26 | 27 | EOF 28 | -------------------------------------------------------------------------------- /scripts/pb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | domain="https://shz.al" 4 | hist_file="${XDG_CONFIG_DIR:-$HOME/.config}/pb_hist" 5 | 6 | script_name=${0##*/} 7 | 8 | _print_with_color() { 9 | color=$1 10 | shift 11 | [ -t 2 ] && printf "\x1b[0;%sm" "$color" >&2 12 | echo -n "$*" >&2 13 | [ -t 2 ] && printf "\x1b[0m\n" >&2 14 | } 15 | 16 | _die() { 17 | _print_with_color 31 "$@" 18 | exit 1 19 | } 20 | 21 | [ -d "${hist_file%/*}" ] || mkdir -p "${hist_file%/*}" || _die "cannot create directory for '$hist_file'" 22 | touch "$hist_file" || _die "cannot create history file '$hist_file'" 23 | 24 | _verbose() { 25 | _print_with_color 90 "$script_name: $*" 26 | } 27 | 28 | _clip() { 29 | echo "$@" | xclip -selection clipboard 30 | _verbose "'$*' is copied to your clipboard" 31 | } 32 | 33 | curl_code() { 34 | status_code=$(curl -sS -w '%{response_code}' "$@") 35 | if [ "$status_code" != '200' ] && [ "$status_code" != '302' ]; then 36 | return 1 37 | fi 38 | } 39 | 40 | main() { 41 | action="$1" 42 | shift 43 | case "$action" in 44 | (p|post ) pb_post "$@";; 45 | (u|update) pb_update "$@";; 46 | (d|delete) pb_delete "$@";; 47 | (g|get ) pb_get "$@";; 48 | (-h|--help) usage;; 49 | (*) _die "no action given. try '$script_name' -h for more information" 50 | esac 51 | } 52 | 53 | usage() { 54 | cat <<-EOF 55 | Usage: 56 | ${script_name} [-h|--help] 57 | print this help message 58 | 59 | ${script_name} [p|post] [OPTIONS] [-f] FILE 60 | upload your text to pastebin, if neither 'FILE' and 'CONTENT' are given, 61 | read the paste from stdin. 62 | 63 | ${script_name} [u|update] NAME[:PASSWD] 64 | Update your text to pastebin, if neither 'FILE' and 'CONTENT' are given, 65 | read the paste from stdin. If 'PASSWD' is not given, try to read password 66 | from the history file. 67 | 68 | ${script_name} [g|get] [OPTIONS] NAME[.EXT] 69 | fetch the paste with name 'NAME' and extension 'EXT' 70 | 71 | ${script_name} [d|delete] [OPTIONS] NAME 72 | delete the paste with name 'NAME' 73 | 74 | Options: 75 | post options: 76 | -c, --content CONTENT the content of the paste 77 | -e, --expire SECONDS the expiration time of the paste (in seconds) 78 | -n, --name NAME the name of the paste 79 | -s, --passwd PASSWD the password 80 | -p, --private make the generated paste name longer for better privacy 81 | -x, --clip clip the url to the clipboard 82 | 83 | update options: 84 | -f, --file FILE read the paste from file 85 | -c, --content CONTENT the content of the paste 86 | -e, --expire SECONDS the expiration time of the paste (in seconds) 87 | -s, --passwd PASSWD the password 88 | -x, --clip clip the url to the clipboard 89 | 90 | get options: 91 | -l, --lang LANG highlight the paste with language 'LANG' in a web page 92 | -m, --mime MIME set the 'Content-Type' header according to mime-type MIME 93 | -o, --output FILE output the paste in file 'FILE' 94 | -u, --url make a 302 URL redirection 95 | 96 | delete options: 97 | none 98 | 99 | general options: 100 | -v, --verbose display the 'underlying' curl command 101 | -d, --dry do a dry run, executing no 'curl' command at all 102 | EOF 103 | } 104 | 105 | pb_post() { 106 | # parse args 107 | short_args="c:e:n:s:f:vhpxd" 108 | long_args="content:,expire:,name:,passwd:,file:,verbose,private,clip,dry" 109 | TEMP=$(getopt -o "$short_args" --long "$long_args" -n "$script_name" -- "$@") \ 110 | || return 1 111 | eval set -- "$TEMP"; 112 | 113 | local content expire name passwd private verbose clip dry 114 | while [[ ${1:0:1} == - ]]; do 115 | [[ $1 =~ ^-f|--|--file$ ]] && { 116 | shift 1; 117 | if [ -n "$1" ]; then file="$1"; shift 1; continue; fi 118 | }; 119 | [[ $1 =~ ^-c|--content$ ]] && { content="$2"; shift 2; continue; }; 120 | [[ $1 =~ ^-e|--expire$ ]] && { expire="$2"; shift 2; continue; }; 121 | [[ $1 =~ ^-n|--name$ ]] && { name="$2"; shift 2; continue; }; 122 | [[ $1 =~ ^-s|--passwd$ ]] && { passwd="$2"; shift 2; continue; }; 123 | [[ $1 =~ ^-p|--private$ ]] && { private="true"; shift 1; continue; }; 124 | [[ $1 =~ ^-v|--verbose$ ]] && { verbose="true"; shift 1; continue; }; 125 | [[ $1 =~ ^-x|--clip$ ]] && { clip="true"; shift 1; continue; }; 126 | [[ $1 =~ ^-d|--dry$ ]] && { dry="true"; shift 1; continue; }; 127 | break; 128 | done 129 | [ "$#" -gt 0 ] && _die "redundant arguments '$*'" 130 | 131 | # check arguments 132 | [ -n "$content" ] && [ -n "$file" ] && _die "cannot set both 'content' and 'file'" 133 | [ -n "$file" ] && [ ! -r "$file" ] && _die "cannot read file '${file}'" 134 | [ -n "$private" ] && [ -n "$name" ] && _die "cannot set both 'private' and 'name'" 135 | 136 | # build arguments 137 | declare -a args=("curl_code" "-sS" "$domain") 138 | [ -n "$file" ] && args+=("-Fc=@$file") 139 | [ -n "$content" ] && args+=("-Fc=\"$content\"") 140 | [ -n "$expire" ] && args+=("-Fe=\"$expire\"") 141 | [ -n "$name" ] && args+=("-Fn=\"$name\"") 142 | [ -n "$passwd" ] && args+=("-Fs=\"$passwd\"") 143 | [ -n "$private" ] && args+=("-Fp=true") 144 | [ -z "$content" ] && [ -z "$file" ] && args+=("-Fc=@-") 145 | 146 | # prepare curl 147 | tmp_file=$(mktemp) 148 | trap 'rm -f "$tmp_file"' EXIT 149 | 150 | [ -n "$verbose" ] && _verbose "${args[@]}" 151 | 152 | # curl if not dry-run 153 | if [ -z "$dry" ]; then 154 | "${args[@]}" -o "$tmp_file" || _die "$status_code: $(cat "$tmp_file")" 155 | else 156 | return 0 157 | fi 158 | 159 | # report error if jq parse error 160 | jq . "$tmp_file" 161 | 162 | # record history 163 | admin_url=$(jq -r '.admin' "$tmp_file") 164 | admin_path=${admin_url##*/} 165 | echo "$admin_path" >> "$hist_file" 166 | 167 | # copy URL to clipboard 168 | url=$(jq -r '.url' "$tmp_file") 169 | [ -n "$clip" ] && _clip "$url" 170 | 171 | return 0 172 | } 173 | 174 | pb_update() { 175 | # parse args 176 | short_args="c:e:s:f:vhxd" 177 | long_args="content:,expire:,passwd:,file:,verbose,clip,dry" 178 | TEMP=$(getopt -o "$short_args" --long "$long_args" -n "$script_name" -- "$@") \ 179 | || return 1 180 | eval set -- "$TEMP"; 181 | 182 | local name_passwd file content expire passwd verbose clip dry 183 | while [[ ${1:0:1} == - ]]; do 184 | [[ $1 == -- ]] && { name_passwd="$2"; shift 2; break; }; 185 | [[ $1 =~ ^-f|--file$ ]] && { file="$2"; shift 2; continue; }; 186 | [[ $1 =~ ^-c|--content$ ]] && { content="$2"; shift 2; continue; }; 187 | [[ $1 =~ ^-e|--expire$ ]] && { expire="$2"; shift 2; continue; }; 188 | [[ $1 =~ ^-s|--passwd$ ]] && { passwd="$2"; shift 2; continue; }; 189 | [[ $1 =~ ^-v|--verbose$ ]] && { verbose="true"; shift 1; continue; }; 190 | [[ $1 =~ ^-x|--clip$ ]] && { clip="true"; shift 1; continue; }; 191 | [[ $1 =~ ^-d|--dry$ ]] && { dry="true"; shift 1; continue; }; 192 | break; 193 | done 194 | [ "$#" -gt 0 ] && _die "redundant arguments '$*'" 195 | 196 | # parse name and passwd 197 | [ -z "$name_passwd" ] && _die "no name and passwd given" 198 | name=${name_passwd%:*} 199 | if [[ "$name_passwd" =~ : ]]; then 200 | passwd=${name_passwd#*:} 201 | else 202 | name_passwd=$(grep "^$name:" "$hist_file" | tail -1) && [ -n "$name_passwd" ] && passwd=${name_passwd#*:} 203 | fi 204 | [ -z "$passwd" ] && _die "no passwd given, and cannot find passwd in history file '$hist_file'" 205 | 206 | # check arguments 207 | [ -n "$content" ] && [ -n "$file" ] && _die "cannot set both 'content' and 'file'" 208 | [ -n "$file" ] && [ ! -r "$file" ] && _die "cannot read file '${file}'" 209 | 210 | # build arguments 211 | declare -a args=("curl_code" "-X" "PUT" "$domain/$name:$passwd") 212 | [ -n "$file" ] && args+=("-Fc=@$file") 213 | [ -n "$content" ] && args+=("-Fc=\"$content\"") 214 | [ -n "$expire" ] && args+=("-Fe=\"$expire\"") 215 | [ -n "$passwd" ] && args+=("-Fs=\"$passwd\"") 216 | [ -z "$content" ] && [ -z "$file" ] && args+=("-Fc=@-") 217 | 218 | # prepare curl 219 | tmp_file=$(mktemp) 220 | trap 'rm -f "$tmp_file"' EXIT 221 | 222 | [ -n "$verbose" ] && _verbose "${args[@]}" 223 | 224 | # curl if not dry-run 225 | if [ -z "$dry" ]; then 226 | "${args[@]}" -o "$tmp_file" || _die "$status_code: $(cat "$tmp_file")" 227 | else 228 | return 0 229 | fi 230 | 231 | # report error if jq parse error 232 | jq . "$tmp_file" 2>/dev/null || _die "$(cat "$tmp_file")" 233 | 234 | # record history 235 | admin_url=$(jq -r '.admin' "$tmp_file") 236 | admin_path=${admin_url##*/} 237 | echo "$admin_path" >> "$hist_file" 238 | 239 | # copy URL to clipboard 240 | url=$(jq -r '.url' "$tmp_file") 241 | [ -n "$clip" ] && _clip "$url" 242 | 243 | return 0 244 | } 245 | 246 | pb_get() { 247 | # parse args 248 | short_args="l:m:o::uvd" 249 | long_args="lang:,mime:,output:,url,verbose,dry" 250 | TEMP=$(getopt -o "$short_args" --long "$long_args" -n "$script_name" -- "$@") \ 251 | || return 1 252 | eval set -- "$TEMP"; 253 | 254 | local name lang mime output url verbose dry 255 | while [[ ${1:0:1} == - ]]; do 256 | [[ $1 == -- ]] && { name="$2"; shift 2; break; }; 257 | [[ $1 =~ ^-l|--lang$ ]] && { lang="$2"; shift 2; continue; }; 258 | [[ $1 =~ ^-m|--mime$ ]] && { mime="$2"; shift 2; continue; }; 259 | [[ $1 =~ ^-o|--output$ ]] && { output="$2"; shift 2; continue; }; 260 | [[ $1 =~ ^-u|--url$ ]] && { url="true"; shift 1; continue; }; 261 | [[ $1 =~ ^-v|--verbose$ ]] && { verbose="true"; shift 1; continue; }; 262 | [[ $1 =~ ^-d|--dry$ ]] && { dry="true"; shift 1; continue; }; 263 | break; 264 | done 265 | [ "$#" -gt 1 ] && _die "redundant arguments '$*'" 266 | 267 | # check args 268 | [ -z "$name" ] && _die "no paste name is given" 269 | 270 | # build arguments 271 | tmp_file=$(mktemp) 272 | trap 'rm -f "$tmp_file"' EXIT 273 | declare -a args=("curl_code" "-G") 274 | [ -n "$url" ] && args+=("$domain/u/$name") || args+=("$domain/$name") 275 | [ -n "$lang" ] && args+=("-d" "lang=$lang") 276 | [ -n "$mime" ] && args+=("-d" "mime=$mime") 277 | [ -n "$output" ] && args+=("-o" "$output") || args+=("-o" "$tmp_file") 278 | 279 | # prepare curl 280 | [ -n "$verbose" ] && _verbose "${args[@]}" 281 | 282 | # curl if not dry-run 283 | if [ -z "$dry" ]; then 284 | "${args[@]}" || _die "$status_code: $(cat "$tmp_file")" 285 | [ -z "$output" ] && cat "$tmp_file" 286 | else 287 | return 0 288 | fi 289 | 290 | return 0 291 | } 292 | 293 | pb_delete() { 294 | # parse args 295 | short_args="vd" 296 | long_args="verbose,dry" 297 | TEMP=$(getopt -o "$short_args" --long "$long_args" -n "$script_name" -- "$@") \ 298 | || return 1 299 | eval set -- "$TEMP"; 300 | 301 | local name_passwd verbose dry 302 | while [[ ${1:0:1} == - ]]; do 303 | [[ $1 == -- ]] && { name_passwd="$2"; shift 2; break; }; 304 | [[ $1 =~ ^-v|--verbose$ ]] && { verbose="true"; shift 1; continue; }; 305 | [[ $1 =~ ^-d|--dry$ ]] && { dry="true"; shift 1; continue; }; 306 | break; 307 | done 308 | [ "$#" -gt 0 ] && _die "redundant arguments '$*'" 309 | 310 | # parse name and passwd 311 | [ -z "$name_passwd" ] && _die "no name and passwd given" 312 | name=${name_passwd%:*} 313 | if [[ "$name_passwd" =~ : ]]; then 314 | passwd=${name_passwd#*:} 315 | else 316 | name_passwd=$(grep "^$name:" "$hist_file" | tail -1) && [ -n "$name_passwd" ] && passwd=${name_passwd#*:} 317 | fi 318 | [ -z "$passwd" ] && _die "no passwd given, and cannot find passwd in history file '$hist_file'" 319 | 320 | # build arguments 321 | tmp_file=$(mktemp) 322 | trap 'rm -f "$tmp_file"' EXIT 323 | declare -a args=("curl_code" "-X" "DELETE" "$domain/$name:$passwd") 324 | 325 | [ -n "$verbose" ] && _verbose "${args[@]}" 326 | 327 | # curl if not dry-run 328 | if [ -z "$dry" ]; then 329 | "${args[@]}" -o "$tmp_file" || _die "$status_code: $(cat "$tmp_file")" 330 | else 331 | return 0 332 | fi 333 | 334 | return 0 335 | } 336 | 337 | main "$@" 338 | -------------------------------------------------------------------------------- /scripts/pb.fish: -------------------------------------------------------------------------------- 1 | # fish-shell completions for pb 2 | # See: https://github.com/SharzyL/pastebin-worker/tree/master/scripts 3 | 4 | set -l commands p post u update g get d delete 5 | 6 | complete -c pb -f 7 | 8 | # common_args: 9 | complete -c pb -s d -l dry -d 'dry run' 10 | complete -c pb -s v -l verbose -d 'verbose output' 11 | 12 | # root_args: 13 | complete -c pb -n "not __fish_seen_subcommand_from $commands" -s h -l help -d 'print help' 14 | 15 | # cmdlist: 16 | complete -c pb -n "not __fish_seen_subcommand_from $commands" -a post -d "Post paste" 17 | complete -c pb -n "not __fish_seen_subcommand_from $commands" -a update -d "Update paste" 18 | complete -c pb -n "not __fish_seen_subcommand_from $commands" -a get -d "Get paste" 19 | complete -c pb -n "not __fish_seen_subcommand_from $commands" -a delete -d "Delete paste" 20 | 21 | # case post: 22 | # todo: - Read the paste from stdin 23 | complete -c pb -n "__fish_seen_subcommand_from post p" -F 24 | complete -c pb -n "__fish_seen_subcommand_from post p" -s c -l content -x -d 'Content of paste' 25 | complete -c pb -n "__fish_seen_subcommand_from post p" -s e -l expire -x -d 'Expiration time' 26 | complete -c pb -n "__fish_seen_subcommand_from post p" -s n -l name -x -d Name 27 | complete -c pb -n "__fish_seen_subcommand_from post p" -s s -l password -x -d Password 28 | complete -c pb -n "__fish_seen_subcommand_from post p" -s p -l private -f -d 'Make generated paste name longer for privacy' 29 | complete -c pb -n "__fish_seen_subcommand_from post p" -s x -l clip -f -d 'Clip the url to the clipboard' 30 | 31 | # case update: 32 | complete -c pb -n "__fish_seen_subcommand_from update u" -s c -l content -x -d 'Content of paste' 33 | complete -c pb -n "__fish_seen_subcommand_from update u" -s f -l file -r -d 'Read the paste from file' 34 | complete -c pb -n "__fish_seen_subcommand_from update u" -s e -l expire -x -d 'Expiration time' 35 | complete -c pb -n "__fish_seen_subcommand_from update u" -s s -l password -x -d Password 36 | complete -c pb -n "__fish_seen_subcommand_from update u" -s x -l clip -f -d 'Clip the url to the clipboard' 37 | 38 | # case get: 39 | complete -c pb -n "__fish_seen_subcommand_from get g" -s l -l lang -x -d 'Highlight with language in a web page' 40 | complete -c pb -n "__fish_seen_subcommand_from get g" -s m -l mine -x -d 'Content of the paste' 41 | complete -c pb -n "__fish_seen_subcommand_from get g" -s o -l output -r -d 'Output the paste in file' 42 | complete -c pb -n "__fish_seen_subcommand_from get g" -s u -l url -f -d 'Make a 302 redirection' 43 | -------------------------------------------------------------------------------- /scripts/render.js: -------------------------------------------------------------------------------- 1 | import { Liquid } from 'liquidjs' 2 | import fs from 'fs' 3 | import { spawnSync } from 'child_process' 4 | import { ArgumentParser } from 'argparse' 5 | 6 | function parseArgs() { 7 | const parser = ArgumentParser() 8 | parser.add_argument('-o', '--output', { required: true }) 9 | parser.add_argument('-c', '--config', { required: true }) 10 | parser.add_argument('-r', '--revision') 11 | parser.add_argument('file') 12 | return parser.parse_args() 13 | } 14 | 15 | function getCommitHash() { 16 | const stdout = spawnSync('git', ['rev-parse', '--short=6', 'HEAD']).stdout 17 | return stdout === null ? 'unknown' : stdout.toString().trim() 18 | } 19 | 20 | function main() { 21 | const args = parseArgs() 22 | 23 | const conf = JSON.parse(fs.readFileSync(args.config).toString()) 24 | conf.COMMIT_HASH_6 = getCommitHash() 25 | conf.DEPLOY_DATE = new Date().toString() 26 | 27 | const engine = new Liquid() 28 | const rendered = engine.renderFileSync(args.file, {}, { globals: conf }) 29 | fs.writeFileSync(args.output, rendered) 30 | } 31 | 32 | main() 33 | -------------------------------------------------------------------------------- /src/auth.js: -------------------------------------------------------------------------------- 1 | import { WorkerError } from "./common.js"; 2 | import conf from '../config.json' 3 | 4 | function parseBasicAuth(request) { 5 | const Authorization = request.headers.get('Authorization'); 6 | console.log(Authorization) 7 | 8 | const [scheme, encoded] = Authorization.split(' '); 9 | 10 | // The Authorization header must start with Basic, followed by a space. 11 | if (!encoded || scheme !== 'Basic') { 12 | throw new WorkerError(400, 'malformed authorization header'); 13 | } 14 | 15 | const buffer = Uint8Array.from(atob(encoded), character => character.charCodeAt(0)) 16 | const decoded = new TextDecoder().decode(buffer).normalize(); 17 | 18 | const index = decoded.indexOf(':'); 19 | 20 | if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) { 21 | throw WorkerError(400, 'invalid authorization value'); 22 | } 23 | 24 | return { 25 | user: decoded.substring(0, index), 26 | pass: decoded.substring(index + 1), 27 | }; 28 | } 29 | 30 | export function verifyAuth(request) { 31 | if ('basicAuth' in conf && conf.basicAuth.enabled === true) { 32 | if (request.headers.has('Authorization')) { 33 | const { user, pass } = parseBasicAuth(request) 34 | const passwdMap = conf.basicAuth.passwd 35 | if (passwdMap[user] === undefined) { 36 | throw new WorkerError(401, "user not found for basic auth") 37 | } else if (passwdMap[user] !== pass) { 38 | throw new WorkerError(401, "incorrect passwd for basic auth") 39 | } 40 | } else { 41 | return new Response('HTTP basic auth is required', { 42 | status: 401, 43 | headers: { 44 | // Prompts the user for credentials. 45 | 'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"', 46 | }, 47 | }); 48 | } 49 | } 50 | return null 51 | } 52 | -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- 1 | export const params = { 2 | CHAR_GEN : "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678", 3 | NAME_REGEX : /^[a-zA-Z0-9+_\-\[\]*$:@,;\/]{3,}$/, 4 | RAND_LEN : 4, 5 | PRIVATE_RAND_LEN : 24, 6 | ADMIN_PATH_LEN : 24, 7 | SEP : ":", 8 | MAX_LEN : 25 * 1024 * 1024, 9 | } 10 | 11 | export function decode(arrayBuffer) { 12 | return new TextDecoder().decode(arrayBuffer) 13 | } 14 | 15 | export class WorkerError extends Error { 16 | constructor(statusCode, ...params) { 17 | super(...params) 18 | this.statusCode = statusCode 19 | } 20 | } 21 | 22 | export function genRandStr(len) { 23 | // TODO: switch to Web Crypto random generator 24 | let str = "" 25 | const numOfRand = params.CHAR_GEN.length 26 | for (let i = 0; i < len; i++) { 27 | str += params.CHAR_GEN.charAt(Math.floor(Math.random() * numOfRand)) 28 | } 29 | return str 30 | } 31 | 32 | export function parsePath(pathname) { 33 | // Example of paths (SEP=':'). Note: query string is not processed here 34 | // > example.com/~stocking 35 | // > example.com/~stocking:uLE4Fhb/d3414adlW653Vx0VSVw= 36 | // > example.com/abcd 37 | // > example.com/abcd.jpg 38 | // > example.com/u/abcd 39 | // > example.com/abcd:3ffd2e7ff214989646e006bd9ad36c58d447065e 40 | let role = "", ext = "" 41 | if (pathname[2] === "/") { 42 | role = pathname[1] 43 | pathname = pathname.slice(2) 44 | } 45 | let startOfExt = pathname.indexOf(".") 46 | if (startOfExt >= 0) { 47 | ext = pathname.slice(startOfExt) 48 | pathname = pathname.slice(0, startOfExt) 49 | } 50 | let endOfShort = pathname.indexOf(params.SEP) 51 | if (endOfShort < 0) endOfShort = pathname.length // when there is no SEP, passwd is left empty 52 | const short = pathname.slice(1, endOfShort) 53 | const passwd = pathname.slice(endOfShort + 1) 54 | return { role, short, passwd, ext } 55 | } 56 | 57 | export function parseExpiration(expirationStr) { 58 | let expirationSeconds = parseFloat(expirationStr) 59 | const lastChar = expirationStr[expirationStr.length - 1] 60 | if (lastChar === 'm') expirationSeconds *= 60 61 | else if (lastChar === 'h') expirationSeconds *= 3600 62 | else if (lastChar === 'd') expirationSeconds *= 3600 * 24 63 | else if (lastChar === 'w') expirationSeconds *= 3600 * 24 * 7 64 | else if (lastChar === 'M') expirationSeconds *= 3600 * 24 * 7 * 30 65 | return expirationSeconds 66 | } 67 | 68 | export function escapeHtml(str) { 69 | const tagsToReplace = { 70 | "&": "&", 71 | "<": "<", 72 | ">": ">", 73 | "\"": """, 74 | "'": "'" 75 | } 76 | return str.replace(/[&<>]/g, function (tag) { 77 | return tagsToReplace[tag] || tag 78 | }) 79 | } 80 | 81 | -------------------------------------------------------------------------------- /src/cors.js: -------------------------------------------------------------------------------- 1 | const corsHeaders = { 2 | "Access-Control-Allow-Origin": "*", 3 | "Access-Control-Allow-Methods": "GET,HEAD,PUT,POST,OPTIONS", 4 | "Access-Control-Max-Age": "86400", 5 | } 6 | 7 | export function handleOptions(request) { 8 | let headers = request.headers; 9 | if ( 10 | headers.get("Origin") !== null && 11 | headers.get("Access-Control-Request-Method") !== null 12 | ){ 13 | let respHeaders = { 14 | ...corsHeaders, 15 | "Access-Control-Allow-Headers": request.headers.get("Access-Control-Request-Headers"), 16 | } 17 | 18 | return new Response(null, { 19 | headers: respHeaders, 20 | }) 21 | } 22 | else { 23 | return new Response(null, { 24 | headers: { 25 | Allow: "GET, HEAD, POST, PUT, OPTIONS", 26 | }, 27 | }) 28 | } 29 | } 30 | 31 | export function corsWrapResponse(response) { 32 | if (response.headers !== undefined) 33 | response.headers.set("Access-Control-Allow-Origin", "*") 34 | return response 35 | } 36 | -------------------------------------------------------------------------------- /src/highlight.js: -------------------------------------------------------------------------------- 1 | import { escapeHtml } from "./common.js" 2 | 3 | export function makeHighlight(content, lang) { 4 | return ` 5 | 6 | 7 | Yet another pastebin 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
${escapeHtml(content)}
16 | 17 | 18 | 19 | 20 | 21 | ` 22 | } 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { WorkerError, parsePath, parseExpiration, genRandStr, decode, params } from "./common.js"; 2 | import { handleOptions, corsWrapResponse } from './cors.js' 3 | import { makeHighlight } from "./highlight.js" 4 | import { parseFormdata, getBoundary } from "./parseFormdata.js" 5 | import { staticPageMap } from './staticPages.js' 6 | import { makeMarkdown } from "./markdown.js"; 7 | import conf_production from '../config.json' 8 | import conf_preview from '../config.preview.json' 9 | 10 | const conf = globalThis.ENVIRONMENT === "preview" ? conf_preview : conf_production 11 | 12 | import { getType } from "mime/lite.js" 13 | import {verifyAuth} from "./auth.js"; 14 | 15 | addEventListener("fetch", (event) => { 16 | const { request } = event 17 | return event.respondWith(handleRequest(request)) 18 | }) 19 | 20 | async function handleRequest(request) { 21 | try { 22 | if (request.method === "OPTIONS") { 23 | return handleOptions(request) 24 | } else { 25 | const response = await handleNormalRequest(request) 26 | if (response.status !== 302 && response.headers !== undefined) { // because Cloudflare do not allow modifying redirect headers 27 | response.headers.set("Access-Control-Allow-Origin", "*") 28 | } 29 | return response 30 | } 31 | } catch (e) { 32 | console.log(e.stack) 33 | if (e instanceof WorkerError) { 34 | return corsWrapResponse(new Response(`Error ${e.statusCode}: ${e.message}\n`, { status: e.statusCode })) 35 | } else { 36 | return corsWrapResponse(new Response(`Error 500: ${e.message}\n`, { status: 500 })) 37 | } 38 | } 39 | } 40 | 41 | async function handleNormalRequest(request) { 42 | if (request.method === "POST") { 43 | return await handlePostOrPut(request, false) 44 | } else if (request.method === "GET") { 45 | return await handleGet(request) 46 | } else if (request.method === "DELETE") { 47 | return await handleDelete(request) 48 | } else if (request.method === "PUT") { 49 | return await handlePostOrPut(request, true) 50 | } else { 51 | throw new WorkerError(405, "method not allowed") 52 | } 53 | } 54 | 55 | async function handlePostOrPut(request, isPut) { 56 | const authResponse = verifyAuth(request) 57 | if (authResponse !== null) { 58 | return authResponse 59 | } 60 | 61 | const contentType = request.headers.get("content-type") || "" 62 | const url = new URL(request.url) 63 | 64 | // parse formdata 65 | let form = {} 66 | if (contentType.includes("multipart/form-data")) { 67 | // because cloudflare runtime treat all formdata part as strings thus corrupting binary data, 68 | // we need to manually parse formdata 69 | const uint8Array = new Uint8Array(await request.arrayBuffer()) 70 | try { 71 | form = parseFormdata(uint8Array, getBoundary(contentType)) 72 | } catch (e) { 73 | throw new WorkerError(400, "error occurs when parsing formdata") 74 | } 75 | } else { 76 | throw new WorkerError(400, `bad usage, please use 'multipart/form-data' instead of ${contentType}`) 77 | } 78 | const content = form.get("c") && form.get("c").content 79 | const filename = form.get("c") && form.get("c").fields.filename 80 | const name = form.get("n") && decode(form.get("n").content) 81 | const isPrivate = form.get("p") !== undefined 82 | const passwd = form.get("s") && decode(form.get("s").content) 83 | const expire = 84 | form.has("e") && form.get("e").content.byteLength > 0 85 | ? decode(form.get("e").content) 86 | : undefined 87 | 88 | // check if paste content is legal 89 | if (content === undefined) { 90 | throw new WorkerError(400, "cannot find content in formdata") 91 | } else if (content.length > params.MAX_LEN) { 92 | throw new WorkerError(413, "payload too large") 93 | } 94 | 95 | // check if expiration is legal 96 | let expirationSeconds = undefined 97 | if (expire !== undefined) { 98 | expirationSeconds = parseExpiration(expire) 99 | if (isNaN(expirationSeconds)) { 100 | throw new WorkerError(400, `cannot parse expire ${expirationSeconds} as an number`) 101 | } 102 | if (expirationSeconds < 60) { 103 | throw new WorkerError( 104 | 400, 105 | `due to limitation of Cloudflare, expire should be a integer greater than 60, '${expirationSeconds}' given`, 106 | ) 107 | } 108 | } 109 | 110 | // check if name is legal 111 | if (name !== undefined && !params.NAME_REGEX.test(name)) { 112 | throw new WorkerError( 113 | 400, 114 | `Name ${name} not satisfying regexp ${params.NAME_REGEX}`, 115 | ) 116 | } 117 | 118 | function makeResponse(created) { 119 | return new Response(JSON.stringify(created, null, 2), { 120 | headers: { "content-type": "application/json;charset=UTF-8" }, 121 | }) 122 | } 123 | 124 | if (isPut) { 125 | const { short, passwd } = parsePath(url.pathname) 126 | const item = await PB.getWithMetadata(short) 127 | if (item.value === null) { 128 | throw new WorkerError(404, `paste of name '${short}' is not found`) 129 | } else { 130 | const date = item.metadata.postedAt 131 | if (passwd !== item.metadata.passwd) { 132 | throw new WorkerError(403, `incorrect password for paste '${short}`) 133 | } else { 134 | return makeResponse( 135 | await createPaste(content, isPrivate, expirationSeconds, short, date, passwd, filename), 136 | ) 137 | } 138 | } 139 | } else { 140 | let short = undefined 141 | if (name !== undefined) { 142 | short = "~" + name 143 | if ((await PB.get(short)) !== null) 144 | throw new WorkerError(409, `name '${name}' is already used`) 145 | } 146 | return makeResponse(await createPaste( 147 | content, isPrivate, expirationSeconds, short, undefined, passwd, filename 148 | )) 149 | } 150 | } 151 | 152 | async function handleGet(request) { 153 | const url = new URL(request.url) 154 | const { role, short, ext, passwd } = parsePath(url.pathname) 155 | if (staticPageMap.has(url.pathname)) { 156 | // access to all static pages requires auth 157 | const authResponse = verifyAuth(request) 158 | if (authResponse !== null) { 159 | return authResponse 160 | } 161 | const item = await PB.get(staticPageMap.get(url.pathname)) 162 | return new Response(item, { 163 | headers: { "content-type": "text/html;charset=UTF-8" } 164 | }) 165 | } 166 | 167 | // return the editor for admin URL 168 | if (passwd.length > 0) { 169 | const item = await PB.get('index') 170 | return new Response(item, { 171 | headers: { "content-type": "text/html;charset=UTF-8" } 172 | }) 173 | } 174 | 175 | const mime = url.searchParams.get("mime") || getType(ext) || "text/plain" 176 | 177 | const item = await PB.getWithMetadata(short, { type: "arrayBuffer" }) 178 | 179 | // when paste is not found 180 | if (item.value === null) { 181 | throw new WorkerError(404, `paste of name '${short}' not found`) 182 | } 183 | 184 | // handle URL redirection 185 | if (role === "u") { 186 | return Response.redirect(decode(item.value), 302) 187 | } 188 | if (role === "a") { 189 | const md = await makeMarkdown(decode(item.value)) 190 | return new Response(md, { 191 | headers: { "content-type": `text/html;charset=UTF-8` }, 192 | }) 193 | } 194 | 195 | // handle language highlight 196 | const lang = url.searchParams.get("lang") 197 | if (lang) { 198 | return new Response(makeHighlight(decode(item.value), lang), { 199 | headers: { "content-type": `text/html;charset=UTF-8` }, 200 | }) 201 | } else { 202 | return new Response(item.value, { 203 | headers: { "content-type": `${mime};charset=UTF-8` }, 204 | }) 205 | } 206 | } 207 | 208 | async function handleDelete(request) { 209 | const url = new URL(request.url) 210 | console.log(request.url) 211 | const { short, passwd } = parsePath(url.pathname) 212 | const item = await PB.getWithMetadata(short) 213 | console.log(item, passwd) 214 | if (item.value === null) { 215 | throw new WorkerError(404, `paste of name '${short}' not found`) 216 | } else { 217 | if (passwd !== item.metadata.passwd) { 218 | throw new WorkerError(403, `incorrect password for paste '${short}`) 219 | } else { 220 | await PB.delete(short) 221 | return new Response("the paste will be deleted in seconds") 222 | } 223 | } 224 | } 225 | 226 | async function createPaste(content, isPrivate, expire, short, date, passwd, filename) { 227 | date = date || new Date().toISOString() 228 | passwd = passwd || genRandStr(params.ADMIN_PATH_LEN) 229 | const short_len = isPrivate ? params.PRIVATE_RAND_LEN : params.RAND_LEN 230 | 231 | if (short === undefined) { 232 | while (true) { 233 | short = genRandStr(short_len) 234 | if ((await PB.get(short)) === null) break 235 | } 236 | } 237 | 238 | await PB.put(short, content, { 239 | expirationTtl: expire, 240 | metadata: { 241 | postedAt: date, 242 | passwd: passwd 243 | }, 244 | }) 245 | let accessUrl = conf.BASE_URL + '/' + short 246 | const adminUrl = conf.BASE_URL + '/' + short + params.SEP + passwd 247 | return { 248 | url: accessUrl, 249 | suggestUrl: suggestUrl(content, filename, short), 250 | admin: adminUrl, 251 | isPrivate: isPrivate, 252 | expire: expire, 253 | } 254 | } 255 | 256 | function suggestUrl(content, filename, short) { 257 | function isUrl(text) { 258 | try { 259 | new URL(text) 260 | return true 261 | } catch (e) { 262 | return false 263 | } 264 | } 265 | 266 | if (isUrl(decode(content))) { 267 | return `${conf.BASE_URL}/u/${short}` 268 | } 269 | if (filename) { 270 | const dotIdx = filename.lastIndexOf('.') 271 | if (dotIdx > 0) { 272 | const ext = filename.slice(dotIdx + 1) 273 | return `${conf.BASE_URL}/${short}.${ext}` 274 | } 275 | } 276 | return null 277 | } 278 | -------------------------------------------------------------------------------- /src/markdown.js: -------------------------------------------------------------------------------- 1 | import {unified} from 'unified' 2 | import remarkParse from 'remark-parse' 3 | import remarkGfm from 'remark-gfm' 4 | import remarkRehype from 'remark-rehype' 5 | import rehypeStringify from 'rehype-stringify' 6 | import {toString} from 'mdast-util-to-string' 7 | 8 | import {escapeHtml} from './common.js' 9 | 10 | const descriptionLimit = 200 11 | const defaultTitle = "Untitled" 12 | 13 | function getMetadata(options) { 14 | return (tree) => { 15 | if (tree.children.length == 0) return 16 | 17 | const firstChild = tree.children[0] 18 | // if the document begins with a h1, set its content as the title 19 | if (firstChild.type == 'heading' && firstChild.depth === 1) { 20 | options.result.title = escapeHtml(toString(firstChild)) 21 | 22 | if (tree.children.length > 1) { 23 | // description is set as the content of the second node 24 | const secondChild = tree.children[1] 25 | options.result.description = escapeHtml(toString(secondChild).slice(0, descriptionLimit)) 26 | } 27 | } else { 28 | // no title is set 29 | // description is set as the content of the first node 30 | options.result.description = escapeHtml(toString(firstChild).slice(0, descriptionLimit)) 31 | } 32 | } 33 | } 34 | 35 | export async function makeMarkdown(content) { 36 | const metadata = { title: defaultTitle, description: "" } 37 | const convertedHtml = unified() 38 | .use(remarkParse) 39 | .use(remarkGfm) 40 | .use(getMetadata, { result: metadata }) // result is written to `metadata` variable 41 | .use(remarkRehype) 42 | .use(rehypeStringify) 43 | .processSync(content) 44 | 45 | return ` 46 | 47 | 48 | 49 | 50 | ${metadata.title} 51 | ${metadata.description.length > 0 ? `` : ""} 52 | 53 | 54 | 55 | 56 | 59 | 60 | 61 |
62 | ${convertedHtml} 63 |
64 | 65 | 66 | 67 | 68 | ` 69 | } 70 | -------------------------------------------------------------------------------- /src/parseFormdata.js: -------------------------------------------------------------------------------- 1 | const contentDispositionPrefix = 'Content-Disposition: form-data' 2 | 3 | export function parseFormdata(uint8Array, boundary) { 4 | // return an array of {fields: {...: ...}, content: Int8Array} 5 | 6 | boundary = '--' + boundary 7 | function readLine(idx) { 8 | // return the index before the next '\r\n' occurs after idx 9 | for (let i = idx; i < uint8Array.length - 1; i++) { 10 | if (uint8Array[i] === 0x0D) { 11 | i ++ 12 | if (uint8Array[i] === 0x0A) { 13 | return i - 1 14 | } 15 | } 16 | } 17 | return uint8Array.length 18 | } 19 | 20 | function parseFields(line) { 21 | let fields = {} 22 | for (const match of decoder.decode(line).matchAll(/\b(\w+)="(.+?)"/g)) { 23 | fields[match[1]] = match[2] 24 | } 25 | return fields 26 | } 27 | 28 | function isContentDisposition(line) { 29 | for (let i = 0; i < contentDispositionPrefix.length; i++) { 30 | if (line[i] !== contentDispositionPrefix.charCodeAt(i)) return false 31 | } 32 | return true 33 | } 34 | 35 | function getLineType(line) { 36 | // type: 0 (normal), 1 (boundary), 2 (end) 37 | if (line.length === 0) return 0 38 | if (line.length === boundary.length) { 39 | for (let i = 0; i < boundary.length; i++) { 40 | if (line[i] !== boundary.charCodeAt(i)) return 0 41 | } 42 | return 1 43 | } else if (line.length === boundary.length + 2) { 44 | for (let i = 0; i < boundary.length; i++) { 45 | if (line[i] !== boundary.charCodeAt(i)) return 0 46 | } 47 | if (line[boundary.length] === 0x2D && line[boundary.length + 1] === 0x2D) { 48 | return 2 49 | } 50 | } 51 | return 0 52 | } 53 | 54 | let decoder = new TextDecoder() 55 | 56 | // status: 57 | // 0: expecting a header 58 | // 1: expecting body 59 | let status = 0 60 | let parts = new Map() 61 | let lineStart = readLine(0) + 2 62 | if (isNaN(lineStart)) return parts 63 | let bodyStartIdx = 0 64 | let currentPart = {fields: {}, content: null} 65 | 66 | while (true) { 67 | const lineEnd = readLine(lineStart); 68 | const line = uint8Array.subarray(lineStart, lineEnd) 69 | 70 | // start reading the body 71 | if (status === 0) { 72 | if (line.length === 0) { // encounter end of headers 73 | status = 1 74 | bodyStartIdx = lineEnd + 2 75 | } else if (isContentDisposition(line)) { 76 | currentPart.fields = parseFields(line) 77 | } 78 | } else { 79 | const lineType = getLineType(line) 80 | if (lineType !== 0) { // current line is boundary or EOF 81 | currentPart.content = uint8Array.subarray(bodyStartIdx, lineStart - 2) 82 | parts.set(currentPart.fields.name, currentPart) 83 | currentPart = {fields: {}, content: null} 84 | status = 0 85 | } 86 | if (lineType === 2 || lineEnd === uint8Array.length) break 87 | } 88 | lineStart = lineEnd + 2 89 | } 90 | 91 | return parts 92 | } 93 | 94 | export function getBoundary(contentType) { 95 | return contentType.split('=')[1] 96 | } 97 | -------------------------------------------------------------------------------- /src/staticPages.js: -------------------------------------------------------------------------------- 1 | export const staticPageMap = new Map([ 2 | ['/', 'index'], 3 | ['/index', 'index'], 4 | ['/index.html', 'index'], 5 | ['/tos', 'tos'], 6 | ['/tos.html', 'tos'], 7 | ]) 8 | -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import path from "path" 3 | import assert from "assert" 4 | import { Miniflare } from "miniflare" 5 | import { FormData } from 'miniflare' 6 | 7 | const workerScript = fs.readFileSync(path.resolve("dist/worker.js"), "utf8") 8 | const localAddr = "http://localhost:8787" 9 | 10 | describe("Test simple alert", async () => { 11 | let worker 12 | it("should load the script correctly", () => { 13 | worker = new Miniflare({ 14 | scriptPath: "dist/worker.js", 15 | envPath: "test/test.env", 16 | }) 17 | }) 18 | 19 | it("should return a index page", async () => { 20 | const response = await worker.dispatchFetch(`${localAddr}`) 21 | assert.strictEqual(response.status, 200) 22 | }) 23 | 24 | it("should return 404 for unknown path", async () => { 25 | const response = await worker.dispatchFetch(`${localAddr}/hello`) 26 | assert.strictEqual(response.status, 404) 27 | }) 28 | 29 | // due to bugs in Miniflare Formdata API, developing tests with javascript 30 | // framework is suspended 31 | }) 32 | -------------------------------------------------------------------------------- /test/test.env: -------------------------------------------------------------------------------- 1 | SALT=aaaaaaaaaaaaaaaaaaaaaaaaaa 2 | BASE_URL=http://localhost:8787/ 3 | -------------------------------------------------------------------------------- /test/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | localaddr="http://localhost:8787" 4 | tmp_file=$(mktemp) 5 | trap 'rm "$tmp_file"' EXIT 6 | 7 | die() { 8 | echo "$@" >&2 9 | exit 1 10 | } 11 | 12 | err() { 13 | printf "\x1b[0;37;31m%s\x1b[0m\n" "$@" 14 | } 15 | 16 | it() { 17 | num_tests=$((num_tests+1)) 18 | printf "\x1b[0;37;32m> it %s\x1b[0m: " "$1" 19 | shift 20 | echo "$@" 21 | "$@" 22 | exit_code=$? 23 | if [ "$exit_code" != 0 ]; then 24 | printf "\x1b[0;37;31mBut fails with error code %s\x1b[0m\n" "$exit_code" 25 | else 26 | num_passed=$((num_passed+1)) 27 | fi 28 | } 29 | 30 | curl_code() { # a wrapper of curl that checks status code 31 | expected_status_code="$1" 32 | shift 33 | status_code=$(curl -sS -w '%{response_code}' "$@" -o /dev/null) || return 1 34 | if [ "$status_code" != "$expected_status_code" ]; then 35 | err "status code $status_code, expected $expected_status_code" 36 | return 1 37 | fi 38 | } 39 | 40 | start_test() { 41 | printf "\x1b[0;37;34mStart testing %s\x1b[0m\n" "$*" 42 | } 43 | 44 | conclude() { 45 | echo '------------------------' 46 | echo "$num_passed / $num_tests tests passed" 47 | echo '------------------------' 48 | if [ $num_passed != $num_tests ]; then 49 | exit 1 50 | else 51 | exit 0 52 | fi 53 | } 54 | 55 | test_chapters() { 56 | for i in "$@"; do 57 | echo "---------------------" 58 | printf "\x1b[0;97m%s\x1b[0m\n" "$i" 59 | echo "---------------------" 60 | "_test_${i}" 61 | done 62 | } 63 | 64 | _test_primary() { 65 | test_text="hello world" 66 | wrong_admin_url="this-is-a-wrong-admin-url" 67 | 68 | start_test "uploading paste" 69 | it 'should upload paste' curl_code 200 -o "$tmp_file" -Fc="$test_text" "$localaddr" 70 | 71 | url=$(jq -r '.url' "$tmp_file") 72 | admin_url=$(jq -r '.admin' "$tmp_file") 73 | 74 | start_test "fetching paste" 75 | it 'should fetch paste' curl_code 200 -o "$tmp_file" "$url" 76 | it 'should return the original pas\e' [ "$(cat "$tmp_file")" = "$test_text" ] 77 | 78 | start_test "fetching non-existing paste" 79 | it 'should return 404 for non-existing paste' curl_code 404 -o /dev/null "$localaddr/$wrong_admin_url" 80 | 81 | start_test "updating paste" 82 | new_text="Hello new world" 83 | it 'should return 403 for wrong passwd' curl_code 403 -o /dev/null -X PUT -Fc="$new_text" "$url:$wrong_admin_url" 84 | it 'should return 200 for true passwd' curl_code 200 -o /dev/null -X PUT -Fc="$new_text" "$admin_url" 85 | curl_code 200 -o "$tmp_file" "$url" 86 | it 'should return the updated paste' [ "$(cat "$tmp_file")" = "$new_text" ] 87 | 88 | start_test "deleting paste" 89 | it 'should return 403 for wrong passwd' curl_code 403 -o /dev/null -X DELETE "$url:$wrong_admin_url" 90 | it 'should return 200 for true passwd' curl_code 200 -o /dev/null -X DELETE "$admin_url" 91 | it 'should delete the paste' curl_code 404 -o /dev/null "$url" 92 | } 93 | 94 | _test_long_mode() { 95 | test_text="hello world" 96 | start_test "uploading paste" 97 | it 'should upload paste' curl_code 200 -o "$tmp_file" -Fc="$test_text" -Fp=true "$localaddr" 98 | url=$(jq -r '.url' "$tmp_file") 99 | path=${url##*/} 100 | it 'should return a long path' [ "${#path}" -gt 20 ] 101 | 102 | start_test "fetching paste" 103 | it 'should fetch paste' curl_code 200 -o "$tmp_file" "$url" 104 | it 'should return the original pas\e' [ "$(cat "$tmp_file")" = "$test_text" ] 105 | } 106 | 107 | _test_custom_path() { 108 | test_text="hello world" 109 | wrong_admin_url="this-is-a-wrong-admin-url" 110 | name="$RANDOM"'+_-[]*$=@,;/' 111 | bad_names=("a" "ab" "...") 112 | 113 | start_test "uploading paste of bad name" 114 | for bad_name in "${bad_names[@]}"; do 115 | it 'should upload paste' curl_code 400 -o "$tmp_file" -Fc="$test_text" -Fn="$bad_name" "$localaddr" 116 | done 117 | 118 | start_test "uploading paste" 119 | it 'should upload paste' curl_code 200 -o "$tmp_file" -Fc="$test_text" -Fn="\"$name\"" "$localaddr" 120 | url=$(jq -r '.url' "$tmp_file") 121 | admin_url=$(jq -r '.admin' "$tmp_file") 122 | it 'should give the custom path' [ "$url" == "$localaddr/~$name" ] 123 | 124 | start_test "fetching paste" 125 | it 'should fetch paste' curl_code 200 -o "$tmp_file" "$url" 126 | it 'should return the original paste' [ "$(cat "$tmp_file")" = "$test_text" ] 127 | 128 | start_test "updating paste" 129 | new_text="Hello new world" 130 | it 'should return 403 for wrong passwd' curl_code 403 -o /dev/null -X PUT -Fc="$new_text" "$url:$wrong_admin_url" 131 | it 'should return 200 for true passwd' curl_code 200 -o /dev/null -X PUT -Fc="$new_text" "$admin_url" 132 | curl_code 200 -o "$tmp_file" "$url" 133 | it 'should return the updated paste' [ "$(cat "$tmp_file")" = "$new_text" ] 134 | 135 | start_test "deleting paste" 136 | it 'should return 403 for wrong passwd' curl_code 403 -o /dev/null -X DELETE "$url:$wrong_admin_url" 137 | it 'should return 200 for true passwd' curl_code 200 -o /dev/null -X DELETE "$admin_url" 138 | it 'should delete the paste' curl_code 404 -o /dev/null "$url" 139 | } 140 | 141 | _test_custom_passwd() { 142 | test_text="hello world" 143 | wrong_admin_url="this-is-a-wrong-admin-url" 144 | name="$RANDOM" 145 | passwd="$RANDOM$RANDOM" 146 | 147 | start_test "uploading paste" 148 | it 'should upload paste' curl_code 200 -o "$tmp_file" -Fc="$test_text" -Fn="$name" -Fs="$passwd" "$localaddr" 149 | url=$(jq -r '.url' "$tmp_file") 150 | admin_url=$(jq -r '.admin' "$tmp_file") 151 | path=${url##*/} 152 | admin_path=${admin_url##*/} 153 | it 'should give the custom path' [ "$path" = "~$name" ] 154 | it 'should give the custom passwd' [ "$admin_path" = "~$name:$passwd" ] 155 | 156 | start_test "fetching paste" 157 | it 'should fetch paste' curl_code 200 -o "$tmp_file" "$url" 158 | it 'should return the original paste' [ "$(cat "$tmp_file")" = "$test_text" ] 159 | 160 | start_test "updating paste" 161 | new_text="Hello new world" 162 | it 'should return 403 for wrong passwd' curl_code 403 -o /dev/null -X PUT -Fc="$new_text" "$url:$wrong_admin_url" 163 | it 'should return 200 for true passwd' curl_code 200 -o /dev/null -X PUT -Fc="$new_text" "$admin_url" 164 | curl_code 200 -o "$tmp_file" "$url" 165 | it 'should return the updated paste' [ "$(cat "$tmp_file")" = "$new_text" ] 166 | 167 | start_test "deleting paste" 168 | it 'should return 403 for wrong passwd' curl_code 403 -o /dev/null -X DELETE "$url:$wrong_admin_url" 169 | it 'should return 200 for true passwd' curl_code 200 -o /dev/null -X DELETE "$admin_url" 170 | it 'should delete the paste' curl_code 404 -o /dev/null "$url" 171 | } 172 | 173 | _test_url_redirect() { 174 | test_text="https://sharzy.in/" 175 | 176 | start_test "uploading paste" 177 | it 'should upload paste' curl_code 200 -o "$tmp_file" -Fc="$test_text" "$localaddr" 178 | url=$(jq -r '.url' "$tmp_file") 179 | 180 | start_test "URL redirect" 181 | url_with_u="$localaddr/u/${url##*/}" 182 | redirect_url=$(curl -o /dev/null -sS -w '%{redirect_url}' "$url_with_u") 183 | it 'should make a redirect' [ "$redirect_url" = "$test_text" ] 184 | } 185 | 186 | _test_mime() { 187 | test_text="hello world" 188 | 189 | start_test "uploading paste" 190 | it 'should upload paste' curl_code 200 -o "$tmp_file" -Fc="$test_text" "$localaddr" 191 | url=$(jq -r '.url' "$tmp_file") 192 | 193 | start_test "mimetype" 194 | mimetype=$(curl -o /dev/null -sS -w '%{content_type}' "$url") 195 | it 'should return text/plain for normal fetch' [ "$mimetype" = 'text/plain;charset=UTF-8' ] 196 | mimetype=$(curl -o /dev/null -sS -w '%{content_type}' "$url.jpg") 197 | it 'should return recognize .jpg extension' [ "$mimetype" = 'image/jpeg;charset=UTF-8' ] 198 | mimetype=$(curl -o /dev/null -sS -w '%{content_type}' "$url?mime=random-mime") 199 | it 'should know "mime" query string' [ "$mimetype" = 'random-mime;charset=UTF-8' ] 200 | } 201 | 202 | _test_highlight() { 203 | test_text="hello world" 204 | 205 | start_test "uploading paste" 206 | it 'should upload paste' curl_code 200 -o "$tmp_file" -Fc="$test_text" "$localaddr" 207 | url=$(jq -r '.url' "$tmp_file") 208 | 209 | start_test "language highlight" 210 | curl -o "$tmp_file" -sS "$url?lang=html" 211 | it 'should return a language highlight powered by prismjs' \ 212 | grep -q 'language-html' "$tmp_file" 213 | } 214 | 215 | _test_suggest() { 216 | url_text="http://example.com/a?x=y" 217 | non_url_text="if (x = 1) x++" 218 | it 'should upload url paste' curl_code 200 -o "$tmp_file" -Fc="$url_text" "$localaddr" 219 | it 'should suggest /u/ url' grep -q '"suggestUrl": .*/u/' "$tmp_file" 220 | it 'should upload non-url paste' curl_code 200 -o "$tmp_file" -Fc="$non_url_text" "$localaddr" 221 | it 'should not contain suggestUrl' grep -q '"suggestUrl": null' "$tmp_file" 222 | 223 | tmp_jpg_file="$(mktemp --suffix .jpg)" 224 | echo "guruguruguruguru" >"$tmp_jpg_file" 225 | it 'should upload non-url paste' curl_code 200 -o "$tmp_file" -Fc=@"$tmp_jpg_file" "$localaddr" 226 | it 'should suggest .jpg url' grep -q '"suggestUrl": .*\.jpg' "$tmp_file" 227 | } 228 | 229 | pgrep -f miniflare > /dev/null || die "no miniflare is running, please start one instance first" 230 | 231 | if [ $# -gt 0 ]; then 232 | test_chapters "$@" 233 | else 234 | test_chapters primary long_mode custom_path url_redirect mime highlight custom_passwd suggest 235 | fi 236 | 237 | conclude 238 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | let basicConfig = { 2 | target: "webworker", 3 | entry: { 4 | worker: "./src/index.js", 5 | }, 6 | devtool: "inline-nosources-source-map", 7 | plugins: [] 8 | } 9 | 10 | export default (env, argv) => { 11 | if (argv && argv.mode === "development") { 12 | basicConfig.devtool = "inline-nosources-source-map" 13 | basicConfig.mode = "development" 14 | } else { 15 | basicConfig.devtool = false 16 | basicConfig.mode = "production" 17 | } 18 | 19 | return basicConfig 20 | } 21 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "pb" 2 | compatibility_date = "2023-01-28" 3 | 4 | account_id = "1ddaf86dbca12e8f4fcaa76f32bb707d" 5 | workers_dev = false 6 | main = "src/index.js" 7 | 8 | # config for default production environment 9 | vars = { ENVIRONMENT = "production" } 10 | route = { pattern = "shz.al", custom_domain = true } 11 | kv_namespaces = [ 12 | { binding = "PB", id = "cc398e983a234aa19de5ea6af571a483" }, 13 | ] 14 | 15 | [env.preview] 16 | vars = { ENVIRONMENT = "preview" } 17 | route = { pattern = "pb-preview.shz.al", custom_domain = true } 18 | minify = false 19 | 20 | kv_namespaces = [ 21 | { binding = "PB", id = "f56ae0043abd4bb4ab61c52071bd9c7f" } 22 | ] 23 | 24 | --------------------------------------------------------------------------------