├── .github └── workflows │ └── build-upload.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── Inter.ttc ├── LICENSE ├── README.md ├── assets ├── balls.js ├── bootstrap.min.css ├── keys │ ├── 0.png │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ ├── A.png │ ├── B.png │ ├── C.png │ ├── D.png │ ├── E.png │ ├── F.png │ ├── G.png │ ├── H.png │ ├── I.png │ ├── J.png │ ├── K.png │ ├── L.png │ ├── M.png │ ├── N.png │ ├── O.png │ ├── P.png │ ├── Q.png │ ├── R.png │ ├── S.png │ ├── T.png │ ├── U.png │ ├── V.png │ ├── W.png │ ├── X.png │ ├── Y.png │ └── Z.png ├── matter.min.js ├── preview │ └── homepage.png ├── script.js ├── starry-night.css ├── style.css ├── tiny-utterances.css └── tiny-utterances.js ├── rust-toolchain.toml ├── src ├── bin │ └── preview.rs ├── lib.rs └── main.rs └── templates ├── article.html ├── base.html ├── blog-post-preview.svg ├── index.html └── redirect.html /.github/workflows/build-upload.yml: -------------------------------------------------------------------------------- 1 | name: Build and Upload to gh-pages 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | issues: 9 | # All those event could change the blog rendering, showing/hiding content 10 | types: [opened, closed, reopened, edited, deleted, labeled, unlabeled, transferred] 11 | 12 | jobs: 13 | build-and-upload: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | - uses: dtolnay/rust-toolchain@1.79 20 | 21 | - name: Build the pages using the templates 22 | run: cargo run --release 23 | env: 24 | EMAIL_ADDRESS: ${{ secrets.EMAIL_ADDRESS }} 25 | 26 | - name: Purge the CSS 27 | run: | 28 | npm install -g purgecss 29 | purgecss --content 'output/*.html' --css 'output/assets/bootstrap.min.css' --output 'output/assets' 30 | 31 | - name: Deploy 32 | uses: peaceiris/actions-gh-pages@v3 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | publish_dir: ./output 36 | cname: blog.kerollmops.com 37 | force_orphan: true 38 | user_name: 'github-actions[bot]' 39 | user_email: 'github-actions[bot]@users.noreply.github.com' 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /target 3 | /output 4 | preview.png 5 | preview.webp 6 | preview.svg 7 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | unstable_features = true 2 | 3 | use_small_heuristics = "max" 4 | imports_granularity = "Module" 5 | group_imports = "StdExternalCrate" 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blog" 3 | description = "A small tool that generate the static pages of my blog" 4 | version = "0.1.0" 5 | edition = "2021" 6 | default-run = "blog" 7 | 8 | [dependencies] 9 | anyhow = "1.0.72" 10 | askama = "0.12.0" 11 | big_s = "1.0.2" 12 | http = "0.2.9" 13 | kuchiki = "0.8.1" 14 | octocrab = "0.32.0" 15 | regex = { version = "1.10.2", default-features = false, features = ["unicode-perl"] } 16 | resvg = { version = "0.43.0", default-features = false, features = ["image-webp", "memmap-fonts", "raster-images", "text"] } 17 | rss = { version = "2.0.6", features = ["atom"] } 18 | scraper = { version = "0.17.1", default-features = false } 19 | serde = { version = "1.0.183", features = ["derive"] } 20 | serde_json = "1.0.104" 21 | slice-group-by = "0.3.1" 22 | tiny-skia = { version = "0.11.4", default-features = false, features = ["std"] } 23 | tokio = { version = "1.30.0", features = ["full"] } 24 | unicode-segmentation = "1.11.0" 25 | ureq = "2.10.1" 26 | url = "2.5.0" 27 | usvg = "0.43.0" 28 | 29 | [profile.release] 30 | opt-level = 0 31 | -------------------------------------------------------------------------------- /Inter.ttc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/Inter.ttc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Clément Renault 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blog 2 | My personal blog website 3 | 4 | ## Simple Usage 5 | 6 | You must first specify the GitHub repository identifier by using the `GITHUB_REPOSITORY` var, the `EMAIL_ADDRESS`, and preferably your `GITHUB_TOKEN`. You can find more information on the token on [the action it is used by](https://github.com/peaceiris/actions-gh-pages). 7 | 8 | ```bash 9 | export GITHUB_REPOSITORY=Kerollmops/blog 10 | export EMAIL_ADDRESS=your-wonderful-email-address 11 | export GITHUB_TOKEN=your-github-token 12 | ``` 13 | 14 | Once you are ready you can run the program and you'll notice a new `output/` folder with all the files. 15 | 16 | ```bash 17 | cargo run 18 | ``` 19 | 20 | ## Advanced Tricks 21 | 22 | ### Defining the Post Synopsis 23 | 24 | This blog tool uses HTML comments to let you define the synopsis visible on the homepage of the blog website. 25 | 26 | ```markdown 27 | 28 | 29 | This is the first sentence of my blog post and this will only be visible in the article. 30 | ``` 31 | 32 | ### Using Tiny-Utterances to Display Comments 33 | 34 | I decided to use [tiny-utterances to display the user comments](https://cofx22.github.io/tiny-utterances/) under the blog post. It's a [simplified version of Utterances](https://utteranc.es/) and works great. The only thing is the hardcore GitHub rate-limiting on the API. 35 | -------------------------------------------------------------------------------- /assets/balls.js: -------------------------------------------------------------------------------- 1 | const loadImage = (url, onSuccess, onError) => { 2 | const img = new Image(); 3 | img.onload = () => { 4 | onSuccess(img.src); 5 | }; 6 | img.onerror = onError(); 7 | img.src = url; 8 | }; 9 | 10 | document.addEventListener("DOMContentLoaded", function () { 11 | const startAfter = 1500; 12 | setTimeout(() => { 13 | var Engine = Matter.Engine, 14 | Render = Matter.Render, 15 | Runner = Matter.Runner, 16 | Bodies = Matter.Bodies, 17 | Body = Matter.Body, 18 | Composite = Matter.Composite; 19 | 20 | const canvas = document.getElementById('ballsCanvas'); 21 | let w = canvas.offsetWidth; 22 | let h = canvas.offsetHeight; 23 | 24 | const engine = Engine.create(); 25 | const render = Render.create({ 26 | engine: engine, 27 | canvas: canvas, 28 | options: { 29 | width: w, 30 | height: h, 31 | background: 'transparent', 32 | wireframes: false, 33 | pixelRatio: window.devicePixelRatio, 34 | //showPerformance: true, 35 | } 36 | }); 37 | 38 | let removedStripe = 100; 39 | let ratio = h / w; 40 | 41 | Render.lookAt(render, { 42 | min: { x: 0, y: 0 }, 43 | max: { x: w - removedStripe, y: h - (removedStripe * ratio) } 44 | }); 45 | 46 | engine.world.gravity.x = -0.35; 47 | engine.world.gravity.y = -0.5; 48 | 49 | const boundariesOptions = { 50 | isStatic: true, 51 | render: { visible: false } 52 | }; 53 | 54 | const boundaries = [ 55 | // Top Boundary 56 | Bodies.rectangle(w / 2, -3, w, 10, boundariesOptions), 57 | // Bottom Boundary 58 | Bodies.rectangle(w / 2, h + 3, w, 10, boundariesOptions), 59 | // Left Boundary 60 | Bodies.rectangle(-3, h / 2, 10, h, boundariesOptions), 61 | // Right Boundary 62 | Bodies.rectangle(w + 3, h / 2, 10, h, boundariesOptions), 63 | ]; 64 | 65 | Composite.add(engine.world, boundaries); 66 | 67 | function spawnComposite(image_url) { 68 | const size = 8; 69 | const scale = 0.18; 70 | const spawnX = w - Math.random() * (removedStripe / 2); 71 | const spawnY = h - Math.random() * (h / 2) - removedStripe * ratio; 72 | 73 | const options = { 74 | label: 'key', 75 | restitution: 0.8, 76 | render: { sprite: { texture: image_url, xScale: scale, yScale: scale } } 77 | }; 78 | 79 | const object = Bodies.rectangle(spawnX, spawnY, size * 2, size * 2, options); 80 | Composite.add(engine.world, object); 81 | } 82 | 83 | const count = Math.random() >= 0.5 ? 3 : 4; 84 | const spawnDurationMs = 800; 85 | const keys = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; 86 | 87 | for (let i = 0; i < count; i++) { 88 | const key = keys[Math.floor(Math.random() * keys.length)]; 89 | const url = `/assets/keys/${key}.png` 90 | loadImage(url, (image_url) => { 91 | setTimeout(() => spawnComposite(image_url), i * (spawnDurationMs / count)); 92 | }, (e) => console.log(e)); 93 | } 94 | 95 | // run the renderer 96 | Render.run(render); 97 | 98 | // create runner 99 | var runner = Runner.create(); 100 | 101 | // run the engine 102 | Runner.run(runner, engine); 103 | 104 | const stopAfter = 30000; 105 | setTimeout(() => { 106 | Render.stop(render); 107 | Runner.stop(runner); 108 | }, stopAfter); 109 | }, startAfter); 110 | }); 111 | -------------------------------------------------------------------------------- /assets/keys/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/0.png -------------------------------------------------------------------------------- /assets/keys/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/1.png -------------------------------------------------------------------------------- /assets/keys/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/2.png -------------------------------------------------------------------------------- /assets/keys/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/3.png -------------------------------------------------------------------------------- /assets/keys/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/4.png -------------------------------------------------------------------------------- /assets/keys/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/5.png -------------------------------------------------------------------------------- /assets/keys/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/6.png -------------------------------------------------------------------------------- /assets/keys/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/7.png -------------------------------------------------------------------------------- /assets/keys/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/8.png -------------------------------------------------------------------------------- /assets/keys/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/9.png -------------------------------------------------------------------------------- /assets/keys/A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/A.png -------------------------------------------------------------------------------- /assets/keys/B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/B.png -------------------------------------------------------------------------------- /assets/keys/C.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/C.png -------------------------------------------------------------------------------- /assets/keys/D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/D.png -------------------------------------------------------------------------------- /assets/keys/E.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/E.png -------------------------------------------------------------------------------- /assets/keys/F.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/F.png -------------------------------------------------------------------------------- /assets/keys/G.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/G.png -------------------------------------------------------------------------------- /assets/keys/H.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/H.png -------------------------------------------------------------------------------- /assets/keys/I.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/I.png -------------------------------------------------------------------------------- /assets/keys/J.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/J.png -------------------------------------------------------------------------------- /assets/keys/K.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/K.png -------------------------------------------------------------------------------- /assets/keys/L.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/L.png -------------------------------------------------------------------------------- /assets/keys/M.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/M.png -------------------------------------------------------------------------------- /assets/keys/N.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/N.png -------------------------------------------------------------------------------- /assets/keys/O.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/O.png -------------------------------------------------------------------------------- /assets/keys/P.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/P.png -------------------------------------------------------------------------------- /assets/keys/Q.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/Q.png -------------------------------------------------------------------------------- /assets/keys/R.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/R.png -------------------------------------------------------------------------------- /assets/keys/S.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/S.png -------------------------------------------------------------------------------- /assets/keys/T.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/T.png -------------------------------------------------------------------------------- /assets/keys/U.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/U.png -------------------------------------------------------------------------------- /assets/keys/V.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/V.png -------------------------------------------------------------------------------- /assets/keys/W.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/W.png -------------------------------------------------------------------------------- /assets/keys/X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/X.png -------------------------------------------------------------------------------- /assets/keys/Y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/Y.png -------------------------------------------------------------------------------- /assets/keys/Z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kerollmops/blog/7fab05f2c0e513128bed9561ab933afa9dc30040/assets/keys/Z.png -------------------------------------------------------------------------------- /assets/matter.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * matter-js 0.20.0 by @liabru 3 | * http://brm.io/matter-js/ 4 | * License MIT 5 | */ 6 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("Matter",[],t):"object"==typeof exports?exports.Matter=t():e.Matter=t()}(this,(function(){return function(e){var t={};function n(o){if(t[o])return t[o].exports;var i=t[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(o,i,function(t){return e[t]}.bind(null,i));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=20)}([function(e,t){var n={};e.exports=n,function(){n._baseDelta=1e3/60,n._nextId=0,n._seed=0,n._nowStartTime=+new Date,n._warnedOnce={},n._decomp=null,n.extend=function(e,t){var o,i;"boolean"==typeof t?(o=2,i=t):(o=1,i=!0);for(var r=o;r0;t--){var o=Math.floor(n.random()*(t+1)),i=e[t];e[t]=e[o],e[o]=i}return e},n.choose=function(e){return e[Math.floor(n.random()*e.length)]},n.isElement=function(e){return"undefined"!=typeof HTMLElement?e instanceof HTMLElement:!!(e&&e.nodeType&&e.nodeName)},n.isArray=function(e){return"[object Array]"===Object.prototype.toString.call(e)},n.isFunction=function(e){return"function"==typeof e},n.isPlainObject=function(e){return"object"==typeof e&&e.constructor===Object},n.isString=function(e){return"[object String]"===toString.call(e)},n.clamp=function(e,t,n){return en?n:e},n.sign=function(e){return e<0?-1:1},n.now=function(){if("undefined"!=typeof window&&window.performance){if(window.performance.now)return window.performance.now();if(window.performance.webkitNow)return window.performance.webkitNow()}return Date.now?Date.now():new Date-n._nowStartTime},n.random=function(t,n){return n=void 0!==n?n:1,(t=void 0!==t?t:0)+e()*(n-t)};var e=function(){return n._seed=(9301*n._seed+49297)%233280,n._seed/233280};n.colorToNumber=function(e){return 3==(e=e.replace("#","")).length&&(e=e.charAt(0)+e.charAt(0)+e.charAt(1)+e.charAt(1)+e.charAt(2)+e.charAt(2)),parseInt(e,16)},n.logLevel=1,n.log=function(){console&&n.logLevel>0&&n.logLevel<=3&&console.log.apply(console,["matter-js:"].concat(Array.prototype.slice.call(arguments)))},n.info=function(){console&&n.logLevel>0&&n.logLevel<=2&&console.info.apply(console,["matter-js:"].concat(Array.prototype.slice.call(arguments)))},n.warn=function(){console&&n.logLevel>0&&n.logLevel<=3&&console.warn.apply(console,["matter-js:"].concat(Array.prototype.slice.call(arguments)))},n.warnOnce=function(){var e=Array.prototype.slice.call(arguments).join(" ");n._warnedOnce[e]||(n.warn(e),n._warnedOnce[e]=!0)},n.deprecated=function(e,t,o){e[t]=n.chain((function(){n.warnOnce("🔅 deprecated 🔅",o)}),e[t])},n.nextId=function(){return n._nextId++},n.indexOf=function(e,t){if(e.indexOf)return e.indexOf(t);for(var n=0;ne.max.x&&(e.max.x=i.x),i.xe.max.y&&(e.max.y=i.y),i.y0?e.max.x+=n.x:e.min.x+=n.x,n.y>0?e.max.y+=n.y:e.min.y+=n.y)},n.contains=function(e,t){return t.x>=e.min.x&&t.x<=e.max.x&&t.y>=e.min.y&&t.y<=e.max.y},n.overlaps=function(e,t){return e.min.x<=t.max.x&&e.max.x>=t.min.x&&e.max.y>=t.min.y&&e.min.y<=t.max.y},n.translate=function(e,t){e.min.x+=t.x,e.max.x+=t.x,e.min.y+=t.y,e.max.y+=t.y},n.shift=function(e,t){var n=e.max.x-e.min.x,o=e.max.y-e.min.y;e.min.x=t.x,e.max.x=t.x+n,e.min.y=t.y,e.max.y=t.y+o}},function(e,t){var n={};e.exports=n,n.create=function(e,t){return{x:e||0,y:t||0}},n.clone=function(e){return{x:e.x,y:e.y}},n.magnitude=function(e){return Math.sqrt(e.x*e.x+e.y*e.y)},n.magnitudeSquared=function(e){return e.x*e.x+e.y*e.y},n.rotate=function(e,t,n){var o=Math.cos(t),i=Math.sin(t);n||(n={});var r=e.x*o-e.y*i;return n.y=e.x*i+e.y*o,n.x=r,n},n.rotateAbout=function(e,t,n,o){var i=Math.cos(t),r=Math.sin(t);o||(o={});var a=n.x+((e.x-n.x)*i-(e.y-n.y)*r);return o.y=n.y+((e.x-n.x)*r+(e.y-n.y)*i),o.x=a,o},n.normalise=function(e){var t=n.magnitude(e);return 0===t?{x:0,y:0}:{x:e.x/t,y:e.y/t}},n.dot=function(e,t){return e.x*t.x+e.y*t.y},n.cross=function(e,t){return e.x*t.y-e.y*t.x},n.cross3=function(e,t,n){return(t.x-e.x)*(n.y-e.y)-(t.y-e.y)*(n.x-e.x)},n.add=function(e,t,n){return n||(n={}),n.x=e.x+t.x,n.y=e.y+t.y,n},n.sub=function(e,t,n){return n||(n={}),n.x=e.x-t.x,n.y=e.y-t.y,n},n.mult=function(e,t){return{x:e.x*t,y:e.y*t}},n.div=function(e,t){return{x:e.x/t,y:e.y/t}},n.perp=function(e,t){return{x:(t=!0===t?-1:1)*-e.y,y:t*e.x}},n.neg=function(e){return{x:-e.x,y:-e.y}},n.angle=function(e,t){return Math.atan2(t.y-e.y,t.x-e.x)},n._temp=[n.create(),n.create(),n.create(),n.create(),n.create(),n.create()]},function(e,t,n){var o={};e.exports=o;var i=n(2),r=n(0);o.create=function(e,t){for(var n=[],o=0;o0)return!1;a=n}return!0},o.scale=function(e,t,n,r){if(1===t&&1===n)return e;var a,s;r=r||o.centre(e);for(var l=0;l=0?l-1:e.length-1],u=e[l],d=e[(l+1)%e.length],p=t[l0&&(r|=2),3===r)return!1;return 0!==r||null},o.hull=function(e){var t,n,o=[],r=[];for((e=e.slice(0)).sort((function(e,t){var n=e.x-t.x;return 0!==n?n:e.y-t.y})),n=0;n=2&&i.cross3(r[r.length-2],r[r.length-1],t)<=0;)r.pop();r.push(t)}for(n=e.length-1;n>=0;n-=1){for(t=e[n];o.length>=2&&i.cross3(o[o.length-2],o[o.length-1],t)<=0;)o.pop();o.push(t)}return o.pop(),r.pop(),o.concat(r)}},function(e,t,n){var o={};e.exports=o;var i=n(3),r=n(2),a=n(7),s=n(0),l=n(1),c=n(11);!function(){o._timeCorrection=!0,o._inertiaScale=4,o._nextCollidingGroupId=1,o._nextNonCollidingGroupId=-1,o._nextCategory=1,o._baseDelta=1e3/60,o.create=function(t){var n={id:s.nextId(),type:"body",label:"Body",parts:[],plugin:{},angle:0,vertices:i.fromPath("L 0 0 L 40 0 L 40 40 L 0 40"),position:{x:0,y:0},force:{x:0,y:0},torque:0,positionImpulse:{x:0,y:0},constraintImpulse:{x:0,y:0,angle:0},totalContacts:0,speed:0,angularSpeed:0,velocity:{x:0,y:0},angularVelocity:0,isSensor:!1,isStatic:!1,isSleeping:!1,motion:0,sleepThreshold:60,density:.001,restitution:0,friction:.1,frictionStatic:.5,frictionAir:.01,collisionFilter:{category:1,mask:4294967295,group:0},slop:.05,timeScale:1,render:{visible:!0,opacity:1,strokeStyle:null,fillStyle:null,lineWidth:null,sprite:{xScale:1,yScale:1,xOffset:0,yOffset:0}},events:null,bounds:null,chamfer:null,circleRadius:0,positionPrev:null,anglePrev:0,parent:null,axes:null,area:0,mass:0,inertia:0,deltaTime:1e3/60,_original:null},o=s.extend(n,t);return e(o,t),o},o.nextGroup=function(e){return e?o._nextNonCollidingGroupId--:o._nextCollidingGroupId++},o.nextCategory=function(){return o._nextCategory=o._nextCategory<<1,o._nextCategory};var e=function(e,t){t=t||{},o.set(e,{bounds:e.bounds||l.create(e.vertices),positionPrev:e.positionPrev||r.clone(e.position),anglePrev:e.anglePrev||e.angle,vertices:e.vertices,parts:e.parts||[e],isStatic:e.isStatic,isSleeping:e.isSleeping,parent:e.parent||e}),i.rotate(e.vertices,e.angle,e.position),c.rotate(e.axes,e.angle),l.update(e.bounds,e.vertices,e.velocity),o.set(e,{axes:t.axes||e.axes,area:t.area||e.area,mass:t.mass||e.mass,inertia:t.inertia||e.inertia});var n=e.isStatic?"#14151f":s.choose(["#f19648","#f5d259","#f55a3c","#063e7b","#ececd1"]),a=e.isStatic?"#555":"#ccc",u=e.isStatic&&null===e.render.fillStyle?1:0;e.render.fillStyle=e.render.fillStyle||n,e.render.strokeStyle=e.render.strokeStyle||a,e.render.lineWidth=e.render.lineWidth||u,e.render.sprite.xOffset+=-(e.bounds.min.x-e.position.x)/(e.bounds.max.x-e.bounds.min.x),e.render.sprite.yOffset+=-(e.bounds.min.y-e.position.y)/(e.bounds.max.y-e.bounds.min.y)};o.set=function(e,t,n){var i;for(i in"string"==typeof t&&(i=t,(t={})[i]=n),t)if(Object.prototype.hasOwnProperty.call(t,i))switch(n=t[i],i){case"isStatic":o.setStatic(e,n);break;case"isSleeping":a.set(e,n);break;case"mass":o.setMass(e,n);break;case"density":o.setDensity(e,n);break;case"inertia":o.setInertia(e,n);break;case"vertices":o.setVertices(e,n);break;case"position":o.setPosition(e,n);break;case"angle":o.setAngle(e,n);break;case"velocity":o.setVelocity(e,n);break;case"angularVelocity":o.setAngularVelocity(e,n);break;case"speed":o.setSpeed(e,n);break;case"angularSpeed":o.setAngularSpeed(e,n);break;case"parts":o.setParts(e,n);break;case"centre":o.setCentre(e,n);break;default:e[i]=n}},o.setStatic=function(e,t){for(var n=0;n0&&r.rotateAbout(s.position,o,e.position,s.position)}},o.setVelocity=function(e,t){var n=e.deltaTime/o._baseDelta;e.positionPrev.x=e.position.x-t.x*n,e.positionPrev.y=e.position.y-t.y*n,e.velocity.x=(e.position.x-e.positionPrev.x)/n,e.velocity.y=(e.position.y-e.positionPrev.y)/n,e.speed=r.magnitude(e.velocity)},o.getVelocity=function(e){var t=o._baseDelta/e.deltaTime;return{x:(e.position.x-e.positionPrev.x)*t,y:(e.position.y-e.positionPrev.y)*t}},o.getSpeed=function(e){return r.magnitude(o.getVelocity(e))},o.setSpeed=function(e,t){o.setVelocity(e,r.mult(r.normalise(o.getVelocity(e)),t))},o.setAngularVelocity=function(e,t){var n=e.deltaTime/o._baseDelta;e.anglePrev=e.angle-t*n,e.angularVelocity=(e.angle-e.anglePrev)/n,e.angularSpeed=Math.abs(e.angularVelocity)},o.getAngularVelocity=function(e){return(e.angle-e.anglePrev)*o._baseDelta/e.deltaTime},o.getAngularSpeed=function(e){return Math.abs(o.getAngularVelocity(e))},o.setAngularSpeed=function(e,t){o.setAngularVelocity(e,s.sign(o.getAngularVelocity(e))*t)},o.translate=function(e,t,n){o.setPosition(e,r.add(e.position,t),n)},o.rotate=function(e,t,n,i){if(n){var r=Math.cos(t),a=Math.sin(t),s=e.position.x-n.x,l=e.position.y-n.y;o.setPosition(e,{x:n.x+(s*r-l*a),y:n.y+(s*a+l*r)},i),o.setAngle(e,e.angle+t,i)}else o.setAngle(e,e.angle+t,i)},o.scale=function(e,t,n,r){var a=0,s=0;r=r||e.position;for(var u=0;u0&&(a+=d.area,s+=d.inertia),d.position.x=r.x+(d.position.x-r.x)*t,d.position.y=r.y+(d.position.y-r.y)*n,l.update(d.bounds,d.vertices,e.velocity)}e.parts.length>1&&(e.area=a,e.isStatic||(o.setMass(e,e.density*a),o.setInertia(e,s))),e.circleRadius&&(t===n?e.circleRadius*=t:e.circleRadius=null)},o.update=function(e,t){var n=(t=(void 0!==t?t:1e3/60)*e.timeScale)*t,a=o._timeCorrection?t/(e.deltaTime||t):1,u=1-e.frictionAir*(t/s._baseDelta),d=(e.position.x-e.positionPrev.x)*a,p=(e.position.y-e.positionPrev.y)*a;e.velocity.x=d*u+e.force.x/e.mass*n,e.velocity.y=p*u+e.force.y/e.mass*n,e.positionPrev.x=e.position.x,e.positionPrev.y=e.position.y,e.position.x+=e.velocity.x,e.position.y+=e.velocity.y,e.deltaTime=t,e.angularVelocity=(e.angle-e.anglePrev)*u*a+e.torque/e.inertia*n,e.anglePrev=e.angle,e.angle+=e.angularVelocity;for(var f=0;f0&&(v.position.x+=e.velocity.x,v.position.y+=e.velocity.y),0!==e.angularVelocity&&(i.rotate(v.vertices,e.angularVelocity,e.position),c.rotate(v.axes,e.angularVelocity),f>0&&r.rotateAbout(v.position,e.angularVelocity,e.position,v.position)),l.update(v.bounds,v.vertices,e.velocity)}},o.updateVelocities=function(e){var t=o._baseDelta/e.deltaTime,n=e.velocity;n.x=(e.position.x-e.positionPrev.x)*t,n.y=(e.position.y-e.positionPrev.y)*t,e.speed=Math.sqrt(n.x*n.x+n.y*n.y),e.angularVelocity=(e.angle-e.anglePrev)*t,e.angularSpeed=Math.abs(e.angularVelocity)},o.applyForce=function(e,t,n){var o=t.x-e.position.x,i=t.y-e.position.y;e.force.x+=n.x,e.force.y+=n.y,e.torque+=o*n.y-i*n.x},o._totalProperties=function(e){for(var t={mass:0,area:0,inertia:0,centre:{x:0,y:0}},n=1===e.parts.length?0:1;n0){n||(n={}),o=t.split(" ");for(var c=0;c0&&l.motion=l.sleepThreshold/n&&o.set(l,!0)):l.sleepCounter>0&&(l.sleepCounter-=1)}else o.set(l,!1)}},o.afterCollisions=function(e){for(var t=o._motionSleepThreshold,n=0;nt&&o.set(l,!1)}}}},o.set=function(e,t){var n=e.isSleeping;t?(e.isSleeping=!0,e.sleepCounter=e.sleepThreshold,e.positionImpulse.x=0,e.positionImpulse.y=0,e.positionPrev.x=e.position.x,e.positionPrev.y=e.position.y,e.anglePrev=e.angle,e.speed=0,e.angularSpeed=0,e.motion=0,n||r.trigger(e,"sleepStart")):(e.isSleeping=!1,e.sleepCounter=0,n&&r.trigger(e,"sleepEnd"))}},function(e,t,n){var o={};e.exports=o;var i,r,a,s=n(3),l=n(9);i=[],r={overlap:0,axis:null},a={overlap:0,axis:null},o.create=function(e,t){return{pair:null,collided:!1,bodyA:e,bodyB:t,parentA:e.parent,parentB:t.parent,depth:0,normal:{x:0,y:0},tangent:{x:0,y:0},penetration:{x:0,y:0},supports:[null,null],supportCount:0}},o.collides=function(e,t,n){if(o._overlapAxes(r,e.vertices,t.vertices,e.axes),r.overlap<=0)return null;if(o._overlapAxes(a,t.vertices,e.vertices,t.axes),a.overlap<=0)return null;var i,c,u=n&&n.table[l.id(e,t)];u?i=u.collision:((i=o.create(e,t)).collided=!0,i.bodyA=e.id=0&&(g=-g,x=-x),d.x=g,d.y=x,p.x=-x,p.y=g,f.x=g*m,f.y=x*m,i.depth=m;var h=o._findSupports(e,t,d,1),b=0;if(s.contains(e.vertices,h[0])&&(v[b++]=h[0]),s.contains(e.vertices,h[1])&&(v[b++]=h[1]),b<2){var S=o._findSupports(t,e,d,-1);s.contains(t.vertices,S[0])&&(v[b++]=S[0]),b<2&&s.contains(t.vertices,S[1])&&(v[b++]=S[1])}return 0===b&&(v[b++]=h[0]),i.supportCount=b,i},o._overlapAxes=function(e,t,n,o){var i,r,a,s,l,c,u=t.length,d=n.length,p=t[0].x,f=t[0].y,v=n[0].x,m=n[0].y,y=o.length,g=Number.MAX_VALUE,x=0;for(l=0;lP?P=s:sB?B=s:ss.frictionStatic?a.frictionStatic:s.frictionStatic,e.restitution=a.restitution>s.restitution?a.restitution:s.restitution,e.slop=a.slop>s.slop?a.slop:s.slop,e.contactCount=i,t.pair=e;var l=o[0],c=r[0],u=o[1],d=r[1];d.vertex!==l&&c.vertex!==u||(r[1]=c,r[0]=c=d,d=r[1]),c.vertex=l,d.vertex=u},o.setActive=function(e,t,n){t?(e.isActive=!0,e.timeUpdated=n):(e.isActive=!1,e.contactCount=0)},o.id=function(e,t){return e.id0?1:.7),t.damping=t.damping||0,t.angularStiffness=t.angularStiffness||0,t.angleA=t.bodyA?t.bodyA.angle:t.angleA,t.angleB=t.bodyB?t.bodyB.angle:t.angleB,t.plugin={};var a={visible:!0,lineWidth:2,strokeStyle:"#ffffff",type:"line",anchors:!0};return 0===t.length&&t.stiffness>.1?(a.type="pin",a.anchors=!1):t.stiffness<.9&&(a.type="spring"),t.render=c.extend(a,t.render),t},o.preSolveAll=function(e){for(var t=0;t=1||0===e.length?e.stiffness*t:e.stiffness*t*t,h=e.damping*t,b=r.mult(u,g*x),S=(n?n.inverseMass:0)+(i?i.inverseMass:0),w=S+((n?n.inverseInertia:0)+(i?i.inverseInertia:0));if(h>0){var A=r.create();v=r.div(u,d),y=r.sub(i&&r.sub(i.position,i.positionPrev)||A,n&&r.sub(n.position,n.positionPrev)||A),m=r.dot(v,y)}n&&!n.isStatic&&(f=n.inverseMass/S,n.constraintImpulse.x-=b.x*f,n.constraintImpulse.y-=b.y*f,n.position.x-=b.x*f,n.position.y-=b.y*f,h>0&&(n.positionPrev.x-=h*v.x*m*f,n.positionPrev.y-=h*v.y*m*f),p=r.cross(a,b)/w*o._torqueDampen*n.inverseInertia*(1-e.angularStiffness),n.constraintImpulse.angle-=p,n.angle-=p),i&&!i.isStatic&&(f=i.inverseMass/S,i.constraintImpulse.x+=b.x*f,i.constraintImpulse.y+=b.y*f,i.position.x+=b.x*f,i.position.y+=b.y*f,h>0&&(i.positionPrev.x+=h*v.x*m*f,i.positionPrev.y+=h*v.y*m*f),p=r.cross(s,b)/w*o._torqueDampen*i.inverseInertia*(1-e.angularStiffness),i.constraintImpulse.angle+=p,i.angle+=p)}}},o.postSolveAll=function(e){for(var t=0;t0&&(d.position.x+=c.x,d.position.y+=c.y),0!==c.angle&&(i.rotate(d.vertices,c.angle,n.position),l.rotate(d.axes,c.angle),u>0&&r.rotateAbout(d.position,c.angle,n.position,d.position)),s.update(d.bounds,d.vertices,n.velocity)}c.angle*=o._warming,c.x*=o._warming,c.y*=o._warming}}},o.pointAWorld=function(e){return{x:(e.bodyA?e.bodyA.position.x:0)+(e.pointA?e.pointA.x:0),y:(e.bodyA?e.bodyA.position.y:0)+(e.pointA?e.pointA.y:0)}},o.pointBWorld=function(e){return{x:(e.bodyB?e.bodyB.position.x:0)+(e.pointB?e.pointB.x:0),y:(e.bodyB?e.bodyB.position.y:0)+(e.pointB?e.pointB.y:0)}},o.currentLength=function(e){var t=(e.bodyA?e.bodyA.position.x:0)+(e.pointA?e.pointA.x:0),n=(e.bodyA?e.bodyA.position.y:0)+(e.pointA?e.pointA.y:0),o=t-((e.bodyB?e.bodyB.position.x:0)+(e.pointB?e.pointB.x:0)),i=n-((e.bodyB?e.bodyB.position.y:0)+(e.pointB?e.pointB.y:0));return Math.sqrt(o*o+i*i)}},function(e,t,n){var o={};e.exports=o;var i=n(2),r=n(0);o.fromVertices=function(e){for(var t={},n=0;n=1&&r.warn("Bodies.trapezoid: slope parameter must be < 1.");var c,u=n*(s*=.5),d=u+(1-2*s)*n,p=d+u;c=s<.5?"L 0 0 L "+u+" "+-o+" L "+d+" "+-o+" L "+p+" 0":"L 0 0 L "+d+" "+-o+" L "+p+" 0";var f={label:"Trapezoid Body",position:{x:e,y:t},vertices:i.fromPath(c)};if(l.chamfer){var v=l.chamfer;f.vertices=i.chamfer(f.vertices,v.radius,v.quality,v.qualityMin,v.qualityMax),delete l.chamfer}return a.create(r.extend({},f,l))},o.circle=function(e,t,n,i,a){i=i||{};var s={label:"Circle Body",circleRadius:n};a=a||25;var l=Math.ceil(Math.max(10,Math.min(a,n)));return l%2==1&&(l+=1),o.polygon(e,t,l,n,r.extend({},s,i))},o.polygon=function(e,t,n,s,l){if(l=l||{},n<3)return o.circle(e,t,s,l);for(var c=2*Math.PI/n,u="",d=.5*c,p=0;p0&&i.area(M)1?(v=a.create(r.extend({parts:m.slice(0)},o)),a.setPosition(v,{x:e,y:t}),v):m[0]}},function(e,t,n){var o={};e.exports=o;var i=n(0),r=n(8);o.create=function(e){return i.extend({bodies:[],collisions:[],pairs:null},e)},o.setBodies=function(e,t){e.bodies=t.slice(0)},o.clear=function(e){e.bodies=[],e.collisions=[]},o.collisions=function(e){var t,n,i=e.pairs,a=e.bodies,s=a.length,l=o.canCollide,c=r.collides,u=e.collisions,d=0;for(a.sort(o._compareBoundsX),t=0;tv)break;if(!(mC.max.y)&&(!g||!b.isStatic&&!b.isSleeping)&&l(p.collisionFilter,b.collisionFilter)){var S=b.parts.length;if(h&&1===S)(M=c(p,b,i))&&(u[d++]=M);else for(var w=S>1?1:0,A=x>1?1:0;AC.max.x||f.max.xC.max.y||(M=c(P,_,i))&&(u[d++]=M)}}}}return u.length!==d&&(u.length=d),u},o.canCollide=function(e,t){return e.group===t.group&&0!==e.group?e.group>0:0!=(e.mask&t.category)&&0!=(t.mask&e.category)},o._compareBoundsX=function(e,t){return e.bounds.min.x-t.bounds.min.x}},function(e,t,n){var o={};e.exports=o;var i=n(0);o.create=function(e){var t={};return e||i.log("Mouse.create: element was undefined, defaulting to document.body","warn"),t.element=e||document.body,t.absolute={x:0,y:0},t.position={x:0,y:0},t.mousedownPosition={x:0,y:0},t.mouseupPosition={x:0,y:0},t.offset={x:0,y:0},t.scale={x:1,y:1},t.wheelDelta=0,t.button=-1,t.pixelRatio=parseInt(t.element.getAttribute("data-pixel-ratio"),10)||1,t.sourceEvents={mousemove:null,mousedown:null,mouseup:null,mousewheel:null},t.mousemove=function(e){var n=o._getRelativeMousePosition(e,t.element,t.pixelRatio);e.changedTouches&&(t.button=0,e.preventDefault()),t.absolute.x=n.x,t.absolute.y=n.y,t.position.x=t.absolute.x*t.scale.x+t.offset.x,t.position.y=t.absolute.y*t.scale.y+t.offset.y,t.sourceEvents.mousemove=e},t.mousedown=function(e){var n=o._getRelativeMousePosition(e,t.element,t.pixelRatio);e.changedTouches?(t.button=0,e.preventDefault()):t.button=e.button,t.absolute.x=n.x,t.absolute.y=n.y,t.position.x=t.absolute.x*t.scale.x+t.offset.x,t.position.y=t.absolute.y*t.scale.y+t.offset.y,t.mousedownPosition.x=t.position.x,t.mousedownPosition.y=t.position.y,t.sourceEvents.mousedown=e},t.mouseup=function(e){var n=o._getRelativeMousePosition(e,t.element,t.pixelRatio);e.changedTouches&&e.preventDefault(),t.button=-1,t.absolute.x=n.x,t.absolute.y=n.y,t.position.x=t.absolute.x*t.scale.x+t.offset.x,t.position.y=t.absolute.y*t.scale.y+t.offset.y,t.mouseupPosition.x=t.position.x,t.mouseupPosition.y=t.position.y,t.sourceEvents.mouseup=e},t.mousewheel=function(e){t.wheelDelta=Math.max(-1,Math.min(1,e.wheelDelta||-e.detail)),e.preventDefault(),t.sourceEvents.mousewheel=e},o.setElement(t,t.element),t},o.setElement=function(e,t){e.element=t,t.addEventListener("mousemove",e.mousemove,{passive:!0}),t.addEventListener("mousedown",e.mousedown,{passive:!0}),t.addEventListener("mouseup",e.mouseup,{passive:!0}),t.addEventListener("wheel",e.mousewheel,{passive:!1}),t.addEventListener("touchmove",e.mousemove,{passive:!1}),t.addEventListener("touchstart",e.mousedown,{passive:!1}),t.addEventListener("touchend",e.mouseup,{passive:!1})},o.clearSourceEvents=function(e){e.sourceEvents.mousemove=null,e.sourceEvents.mousedown=null,e.sourceEvents.mouseup=null,e.sourceEvents.mousewheel=null,e.wheelDelta=0},o.setOffset=function(e,t){e.offset.x=t.x,e.offset.y=t.y,e.position.x=e.absolute.x*e.scale.x+e.offset.x,e.position.y=e.absolute.y*e.scale.y+e.offset.y},o.setScale=function(e,t){e.scale.x=t.x,e.scale.y=t.y,e.position.x=e.absolute.x*e.scale.x+e.offset.x,e.position.y=e.absolute.y*e.scale.y+e.offset.y},o._getRelativeMousePosition=function(e,t,n){var o,i,r=t.getBoundingClientRect(),a=document.documentElement||document.body.parentNode||document.body,s=void 0!==window.pageXOffset?window.pageXOffset:a.scrollLeft,l=void 0!==window.pageYOffset?window.pageYOffset:a.scrollTop,c=e.changedTouches;return c?(o=c[0].pageX-r.left-s,i=c[0].pageY-r.top-l):(o=e.pageX-r.left-s,i=e.pageY-r.top-l),{x:o/(t.clientWidth/(t.width||t.clientWidth)*n),y:i/(t.clientHeight/(t.height||t.clientHeight)*n)}}},function(e,t,n){var o={};e.exports=o;var i=n(0);o._registry={},o.register=function(e){if(o.isPlugin(e)||i.warn("Plugin.register:",o.toString(e),"does not implement all required fields."),e.name in o._registry){var t=o._registry[e.name],n=o.versionParse(e.version).number,r=o.versionParse(t.version).number;n>r?(i.warn("Plugin.register:",o.toString(t),"was upgraded to",o.toString(e)),o._registry[e.name]=e):n-1},o.isFor=function(e,t){var n=e.for&&o.dependencyParse(e.for);return!e.for||t.name===n.name&&o.versionSatisfies(t.version,n.range)},o.use=function(e,t){if(e.uses=(e.uses||[]).concat(t||[]),0!==e.uses.length){for(var n=o.dependencies(e),r=i.topologicalSort(n),a=[],s=0;s0&&i.info(a.join(" "))}else i.warn("Plugin.use:",o.toString(e),"does not specify any dependencies to install.")},o.dependencies=function(e,t){var n=o.dependencyParse(e),r=n.name;if(!(r in(t=t||{}))){e=o.resolve(e)||e,t[r]=i.map(e.uses||[],(function(t){o.isPlugin(t)&&o.register(t);var r=o.dependencyParse(t),a=o.resolve(t);return a&&!o.versionSatisfies(a.version,r.range)?(i.warn("Plugin.dependencies:",o.toString(a),"does not satisfy",o.toString(r),"used by",o.toString(n)+"."),a._warned=!0,e._warned=!0):a||(i.warn("Plugin.dependencies:",o.toString(t),"used by",o.toString(n),"could not be resolved."),e._warned=!0),r.name}));for(var a=0;a=|>)?\s*((\d+)\.(\d+)\.(\d+))(-[0-9A-Za-z-+]+)?$/;t.test(e)||i.warn("Plugin.versionParse:",e,"is not a valid version or range.");var n=t.exec(e),o=Number(n[4]),r=Number(n[5]),a=Number(n[6]);return{isRange:Boolean(n[1]||n[2]),version:n[3],range:e,operator:n[1]||n[2]||"",major:o,minor:r,patch:a,parts:[o,r,a],prerelease:n[7],number:1e8*o+1e4*r+a}},o.versionSatisfies=function(e,t){t=t||"*";var n=o.versionParse(t),i=o.versionParse(e);if(n.isRange){if("*"===n.operator||"*"===e)return!0;if(">"===n.operator)return i.number>n.number;if(">="===n.operator)return i.number>=n.number;if("~"===n.operator)return i.major===n.major&&i.minor===n.minor&&i.patch>=n.patch;if("^"===n.operator)return n.major>0?i.major===n.major&&i.number>=n.number:n.minor>0?i.minor===n.minor&&i.patch>=n.patch:i.patch===n.patch}return e===t||"*"===e}},function(e,t){var n={};e.exports=n,n.create=function(e){return{vertex:e,normalImpulse:0,tangentImpulse:0}}},function(e,t,n){var o={};e.exports=o;var i=n(7),r=n(18),a=n(13),s=n(19),l=n(5),c=n(6),u=n(10),d=n(0),p=n(4);o._deltaMax=1e3/60,o.create=function(e){e=e||{};var t=d.extend({positionIterations:6,velocityIterations:4,constraintIterations:2,enableSleeping:!1,events:[],plugin:{},gravity:{x:0,y:1,scale:.001},timing:{timestamp:0,timeScale:1,lastDelta:0,lastElapsed:0,lastUpdatesPerFrame:0}},e);return t.world=e.world||c.create({label:"World"}),t.pairs=e.pairs||s.create(),t.detector=e.detector||a.create(),t.detector.pairs=t.pairs,t.grid={buckets:[]},t.world.gravity=t.gravity,t.broadphase=t.grid,t.metrics={},t},o.update=function(e,t){var n,p=d.now(),f=e.world,v=e.detector,m=e.pairs,y=e.timing,g=y.timestamp;t>o._deltaMax&&d.warnOnce("Matter.Engine.update: delta argument is recommended to be less than or equal to",o._deltaMax.toFixed(3),"ms."),t=void 0!==t?t:d._baseDelta,t*=y.timeScale,y.timestamp+=t,y.lastDelta=t;var x={timestamp:y.timestamp,delta:t};l.trigger(e,"beforeUpdate",x);var h=c.allBodies(f),b=c.allConstraints(f);for(f.isModified&&(a.setBodies(v,h),c.setModified(f,!1,!1,!0)),e.enableSleeping&&i.update(h,t),o._bodiesApplyGravity(h,e.gravity),t>0&&o._bodiesUpdate(h,t),l.trigger(e,"beforeSolve",x),u.preSolveAll(h),n=0;n0&&l.trigger(e,"collisionStart",{pairs:m.collisionStart,timestamp:y.timestamp,delta:t});var w=d.clamp(20/e.positionIterations,0,1);for(r.preSolvePosition(m.list),n=0;n0&&l.trigger(e,"collisionActive",{pairs:m.collisionActive,timestamp:y.timestamp,delta:t}),m.collisionEnd.length>0&&l.trigger(e,"collisionEnd",{pairs:m.collisionEnd,timestamp:y.timestamp,delta:t}),o._bodiesClearForces(h),l.trigger(e,"afterUpdate",x),e.timing.lastElapsed=d.now()-p,e},o.merge=function(e,t){if(d.extend(e,t),t.world){e.world=t.world,o.clear(e);for(var n=c.allBodies(e.world),r=0;rz?(i=W>0?W:-W,(n=m.friction*(W>0?1:-1)*c)<-i?n=-i:n>i&&(n=i)):(n=W,i=f);var X=L*b-F*h,Q=O*b-H*h,Y=_/(A+g.inverseInertia*X*X+x.inverseInertia*Q*Q),Z=(1+m.restitution)*U*Y;if(n*=Y,U0&&(V.normalImpulse=0),Z=V.normalImpulse-$}if(W<-d||W>d)V.tangentImpulse=0;else{var J=V.tangentImpulse;V.tangentImpulse+=n,V.tangentImpulse<-i&&(V.tangentImpulse=-i),V.tangentImpulse>i&&(V.tangentImpulse=i),n=V.tangentImpulse-J}var K=h*Z+S*n,ee=b*Z+w*n;g.isStatic||g.isSleeping||(g.positionPrev.x+=K*g.inverseMass,g.positionPrev.y+=ee*g.inverseMass,g.anglePrev+=(L*ee-F*K)*g.inverseInertia),x.isStatic||x.isSleeping||(x.positionPrev.x-=K*x.inverseMass,x.positionPrev.y-=ee*x.inverseMass,x.anglePrev-=(O*ee-H*K)*x.inverseInertia)}}}}},function(e,t,n){var o={};e.exports=o;var i=n(9),r=n(0);o.create=function(e){return r.extend({table:{},list:[],collisionStart:[],collisionActive:[],collisionEnd:[]},e)},o.update=function(e,t,n){var o,r,a,s=i.update,l=i.create,c=i.setActive,u=e.table,d=e.list,p=d.length,f=p,v=e.collisionStart,m=e.collisionEnd,y=e.collisionActive,g=t.length,x=0,h=0,b=0;for(a=0;a=n?d[f++]=r:(c(r,!1,n),r.collision.bodyA.sleepCounter>0&&r.collision.bodyB.sleepCounter>0?d[f++]=r:(m[h++]=r,delete u[r.id]));d.length!==f&&(d.length=f),v.length!==x&&(v.length=x),m.length!==h&&(m.length=h),y.length!==b&&(y.length=b)},o.clear=function(e){return e.table={},e.list.length=0,e.collisionStart.length=0,e.collisionActive.length=0,e.collisionEnd.length=0,e}},function(e,t,n){var o=e.exports=n(21);o.Axes=n(11),o.Bodies=n(12),o.Body=n(4),o.Bounds=n(1),o.Collision=n(8),o.Common=n(0),o.Composite=n(6),o.Composites=n(22),o.Constraint=n(10),o.Contact=n(16),o.Detector=n(13),o.Engine=n(17),o.Events=n(5),o.Grid=n(23),o.Mouse=n(14),o.MouseConstraint=n(24),o.Pair=n(9),o.Pairs=n(19),o.Plugin=n(15),o.Query=n(25),o.Render=n(26),o.Resolver=n(18),o.Runner=n(27),o.SAT=n(28),o.Sleeping=n(7),o.Svg=n(29),o.Vector=n(2),o.Vertices=n(3),o.World=n(30),o.Engine.run=o.Runner.run,o.Common.deprecated(o.Engine,"run","Engine.run ➤ use Matter.Runner.run(engine) instead")},function(e,t,n){var o={};e.exports=o;var i=n(15),r=n(0);o.name="matter-js",o.version="0.20.0",o.uses=[],o.used=[],o.use=function(){i.use(o,Array.prototype.slice.call(arguments))},o.before=function(e,t){return e=e.replace(/^Matter./,""),r.chainPathBefore(o,e,t)},o.after=function(e,t){return e=e.replace(/^Matter./,""),r.chainPathAfter(o,e,t)}},function(e,t,n){var o={};e.exports=o;var i=n(6),r=n(10),a=n(0),s=n(4),l=n(12),c=a.deprecated;o.stack=function(e,t,n,o,r,a,l){for(var c,u=i.create({label:"Stack"}),d=e,p=t,f=0,v=0;vm&&(m=x),s.translate(g,{x:.5*h,y:.5*x}),d=g.bounds.max.x+r,i.addBody(u,g),c=g,f+=1}else d+=r}p+=m+a,d=e}return u},o.chain=function(e,t,n,o,s,l){for(var c=e.bodies,u=1;u0)for(c=0;c0&&(p=f[c-1+(l-1)*t],i.addConstraint(e,r.create(a.extend({bodyA:p,bodyB:d},s)))),o&&cp||a<(c=p-c)||a>n-1-c))return 1===d&&s.translate(u,{x:(a+(n%2==1?1:-1))*f,y:0}),l(e+(u?a*f:0)+a*r,o,a,c,u,d)}))},o.newtonsCradle=function(e,t,n,o,a){for(var s=i.create({label:"Newtons Cradle"}),c=0;cu.bounds.max.x||f.bounds.max.yu.bounds.max.y))){var v=o._getRegion(e,f);if(!f.region||v.id!==f.region.id||i){f.region&&!i||(f.region=v);var m=o._regionUnion(v,f.region);for(a=m.startCol;a<=m.endCol;a++)for(s=m.startRow;s<=m.endRow;s++){l=d[c=o._getBucketId(a,s)];var y=a>=v.startCol&&a<=v.endCol&&s>=v.startRow&&s<=v.endRow,g=a>=f.region.startCol&&a<=f.region.endCol&&s>=f.region.startRow&&s<=f.region.endRow;!y&&g&&g&&l&&o._bucketRemoveBody(e,l,f),(f.region===v||y&&!g||i)&&(l||(l=o._createBucket(d,c)),o._bucketAddBody(e,l,f))}f.region=v,p=!0}}}p&&(e.pairsList=o._createActivePairsList(e))},a(o,"update","Grid.update ➤ replaced by Matter.Detector"),o.clear=function(e){e.buckets={},e.pairs={},e.pairsList=[]},a(o,"clear","Grid.clear ➤ replaced by Matter.Detector"),o._regionUnion=function(e,t){var n=Math.min(e.startCol,t.startCol),i=Math.max(e.endCol,t.endCol),r=Math.min(e.startRow,t.startRow),a=Math.max(e.endRow,t.endRow);return o._createRegion(n,i,r,a)},o._getRegion=function(e,t){var n=t.bounds,i=Math.floor(n.min.x/e.bucketWidth),r=Math.floor(n.max.x/e.bucketWidth),a=Math.floor(n.min.y/e.bucketHeight),s=Math.floor(n.max.y/e.bucketHeight);return o._createRegion(i,r,a,s)},o._createRegion=function(e,t,n,o){return{id:e+","+t+","+n+","+o,startCol:e,endCol:t,startRow:n,endRow:o}},o._getBucketId=function(e,t){return"C"+e+"R"+t},o._createBucket=function(e,t){return e[t]=[]},o._bucketAddBody=function(e,t,n){var o,r=e.pairs,a=i.id,s=t.length;for(o=0;o0?s.push(t):delete o[i[n]];return s}},function(e,t,n){var o={};e.exports=o;var i=n(3),r=n(7),a=n(14),s=n(5),l=n(13),c=n(10),u=n(6),d=n(0),p=n(1);o.create=function(e,t){var n=(e?e.mouse:null)||(t?t.mouse:null);n||(e&&e.render&&e.render.canvas?n=a.create(e.render.canvas):t&&t.element?n=a.create(t.element):(n=a.create(),d.warn("MouseConstraint.create: options.mouse was undefined, options.element was undefined, may not function as expected")));var i={type:"mouseConstraint",mouse:n,element:null,body:null,constraint:c.create({label:"Mouse Constraint",pointA:n.position,pointB:{x:0,y:0},length:.01,stiffness:.1,angularStiffness:1,render:{strokeStyle:"#90EE90",lineWidth:3}}),collisionFilter:{category:1,mask:4294967295,group:0}},r=d.extend(i,t);return s.on(e,"beforeUpdate",(function(){var t=u.allBodies(e.world);o.update(r,t),o._triggerEvents(r)})),r},o.update=function(e,t){var n=e.mouse,o=e.constraint,a=e.body;if(0===n.button){if(o.bodyB)r.set(o.bodyB,!1),o.pointA=n.position;else for(var c=0;c1?1:0;ui.max.x&&(i.max.x=c.x),l.yi.max.y&&(i.max.y=c.y))}var d=i.max.x-i.min.x+2*n.x,p=i.max.y-i.min.y+2*n.y,f=e.canvas.height,v=e.canvas.width/f,m=d/p,y=1,g=1;m>v?g=m/v:y=v/m,e.options.hasBounds=!0,e.bounds.min.x=i.min.x,e.bounds.max.x=i.min.x+d*y,e.bounds.min.y=i.min.y,e.bounds.max.y=i.min.y+p*g,o&&(e.bounds.min.x+=.5*d-d*y*.5,e.bounds.max.x+=.5*d-d*y*.5,e.bounds.min.y+=.5*p-p*g*.5,e.bounds.max.y+=.5*p-p*g*.5),e.bounds.min.x-=n.x,e.bounds.max.x-=n.x,e.bounds.min.y-=n.y,e.bounds.max.y-=n.y,e.mouse&&(u.setScale(e.mouse,{x:(e.bounds.max.x-e.bounds.min.x)/e.canvas.width,y:(e.bounds.max.y-e.bounds.min.y)/e.canvas.height}),u.setOffset(e.mouse,e.bounds.min))},o.startViewTransform=function(e){var t=e.bounds.max.x-e.bounds.min.x,n=e.bounds.max.y-e.bounds.min.y,o=t/e.options.width,i=n/e.options.height;e.context.setTransform(e.options.pixelRatio/o,0,0,e.options.pixelRatio/i,0,0),e.context.translate(-e.bounds.min.x,-e.bounds.min.y)},o.endViewTransform=function(e){e.context.setTransform(e.options.pixelRatio,0,0,e.options.pixelRatio,0,0)},o.world=function(e,t){var n,i=r.now(),d=e.engine,p=d.world,f=e.canvas,v=e.context,y=e.options,g=e.timing,x=a.allBodies(p),h=a.allConstraints(p),b=y.wireframes?y.wireframeBackground:y.background,S=[],w=[],A={timestamp:d.timing.timestamp};if(l.trigger(e,"beforeRender",A),e.currentBackground!==b&&m(e,b),v.globalCompositeOperation="source-in",v.fillStyle="transparent",v.fillRect(0,0,f.width,f.height),v.globalCompositeOperation="source-over",y.hasBounds){for(n=0;n1?1:0;a1?1:0;s1?1:0;r1?1:0;s1?1:0;r1?1:0;r1?1:0;i0)){var u=o.contacts[0].vertex.x,d=o.contacts[0].vertex.y;2===o.contactCount&&(u=(o.contacts[0].vertex.x+o.contacts[1].vertex.x)/2,d=(o.contacts[0].vertex.y+o.contacts[1].vertex.y)/2),i.bodyB===i.supports[0].body||!0===i.bodyA.isStatic?s.moveTo(u-8*i.normal.x,d-8*i.normal.y):s.moveTo(u+8*i.normal.x,d+8*i.normal.y),s.lineTo(u,d)}l.wireframes?s.strokeStyle="rgba(255,165,0,0.7)":s.strokeStyle="orange",s.lineWidth=1,s.stroke()},o.separations=function(e,t,n){var o,i,r,a,s,l=n,c=e.options;for(l.beginPath(),s=0;sMath.max(o._maxFrameDelta,t.maxFrameTime))&&(d=t.frameDelta||o._frameDeltaFallback),t.frameDeltaSmoothing){t.frameDeltaHistory.push(d),t.frameDeltaHistory=t.frameDeltaHistory.slice(-t.frameDeltaHistorySize);var p=t.frameDeltaHistory.slice(0).sort(),f=t.frameDeltaHistory.slice(p.length*o._smoothingLowerBound,p.length*o._smoothingUpperBound);d=e(f)||d}t.frameDeltaSnapping&&(d=1e3/Math.round(1e3/d)),t.frameDelta=d,t.timeLastTick=s,t.timeBuffer+=t.frameDelta,t.timeBuffer=a.clamp(t.timeBuffer,0,t.frameDelta+c*o._timeBufferMargin),t.lastUpdatesDeferred=0;var v=t.maxUpdates||Math.ceil(t.maxFrameTime/c),m={timestamp:n.timing.timestamp};i.trigger(t,"beforeTick",m),i.trigger(t,"tick",m);for(var y=a.now();c>0&&t.timeBuffer>=c*o._timeBufferMargin;){i.trigger(t,"beforeUpdate",m),r.update(n,c),i.trigger(t,"afterUpdate",m),t.timeBuffer-=c,u+=1;var g=a.now()-l,x=a.now()-y,h=g+o._elapsedNextEstimate*x/u;if(u>=v||h>t.maxFrameTime){t.lastUpdatesDeferred=Math.round(Math.max(0,t.timeBuffer/c-o._timeBufferMargin));break}}n.timing.lastUpdatesPerFrame=u,i.trigger(t,"afterTick",m),t.frameDeltaHistory.length>=100&&(t.lastUpdatesDeferred&&Math.round(t.frameDelta/c)>v?a.warnOnce("Matter.Runner: runner reached runner.maxUpdates, see docs."):t.lastUpdatesDeferred&&a.warnOnce("Matter.Runner: runner reached runner.maxFrameTime, see docs."),void 0!==t.isFixed&&a.warnOnce("Matter.Runner: runner.isFixed is now redundant, see docs."),(t.deltaMin||t.deltaMax)&&a.warnOnce("Matter.Runner: runner.deltaMin and runner.deltaMax were removed, see docs."),0!==t.fps&&a.warnOnce("Matter.Runner: runner.fps was replaced by runner.delta, see docs."))},o.stop=function(e){o._cancelNextFrame(e)},o._onNextFrame=function(e,t){if("undefined"==typeof window||!window.requestAnimationFrame)throw new Error("Matter.Runner: missing required global window.requestAnimationFrame.");return e.frameRequestId=window.requestAnimationFrame(t),e.frameRequestId},o._cancelNextFrame=function(e){if("undefined"==typeof window||!window.cancelAnimationFrame)throw new Error("Matter.Runner: missing required global window.cancelAnimationFrame.");window.cancelAnimationFrame(e.frameRequestId)};var e=function(e){for(var t=0,n=e.length,o=0;o1;if(!p||e!=p.x||t!=p.y){p&&o?(f=p.x,v=p.y):(f=0,v=0);var i={x:f+e,y:v+t};!o&&p||(p=i),m.push(i),g=f+e,x=v+t}},b=function(e){var t=e.pathSegTypeAsLetter.toUpperCase();if("Z"!==t){switch(t){case"M":case"L":case"T":case"C":case"S":case"Q":g=e.x,x=e.y;break;case"H":g=e.x;break;case"V":x=e.y}h(g,x,e.pathSegType)}};for(o._svgPathToAbsolute(e),a=e.getTotalLength(),c=[],n=0;n for more info. */ 4 | :root { 5 | --color-prettylights-syntax-comment: #6e7781; 6 | --color-prettylights-syntax-constant: #0550ae; 7 | --color-prettylights-syntax-entity: #8250df; 8 | --color-prettylights-syntax-storage-modifier-import: #24292f; 9 | --color-prettylights-syntax-entity-tag: #116329; 10 | --color-prettylights-syntax-keyword: #cf222e; 11 | --color-prettylights-syntax-string: #0a3069; 12 | --color-prettylights-syntax-variable: #953800; 13 | --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; 14 | --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; 15 | --color-prettylights-syntax-invalid-illegal-bg: #82071e; 16 | --color-prettylights-syntax-carriage-return-text: #f6f8fa; 17 | --color-prettylights-syntax-carriage-return-bg: #cf222e; 18 | --color-prettylights-syntax-string-regexp: #116329; 19 | --color-prettylights-syntax-markup-list: #3b2300; 20 | --color-prettylights-syntax-markup-heading: #0550ae; 21 | --color-prettylights-syntax-markup-italic: #24292f; 22 | --color-prettylights-syntax-markup-bold: #24292f; 23 | --color-prettylights-syntax-markup-deleted-text: #82071e; 24 | --color-prettylights-syntax-markup-deleted-bg: #ffebe9; 25 | --color-prettylights-syntax-markup-inserted-text: #116329; 26 | --color-prettylights-syntax-markup-inserted-bg: #dafbe1; 27 | --color-prettylights-syntax-markup-changed-text: #953800; 28 | --color-prettylights-syntax-markup-changed-bg: #ffd8b5; 29 | --color-prettylights-syntax-markup-ignored-text: #eaeef2; 30 | --color-prettylights-syntax-markup-ignored-bg: #0550ae; 31 | --color-prettylights-syntax-meta-diff-range: #8250df; 32 | --color-prettylights-syntax-brackethighlighter-angle: #57606a; 33 | --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; 34 | --color-prettylights-syntax-constant-other-reference-link: #0a3069; 35 | } 36 | 37 | @media (prefers-color-scheme: dark) { 38 | :root { 39 | --color-prettylights-syntax-comment: #8b949e; 40 | --color-prettylights-syntax-constant: #79c0ff; 41 | --color-prettylights-syntax-entity: #d2a8ff; 42 | --color-prettylights-syntax-storage-modifier-import: #c9d1d9; 43 | --color-prettylights-syntax-entity-tag: #7ee787; 44 | --color-prettylights-syntax-keyword: #ff7b72; 45 | --color-prettylights-syntax-string: #a5d6ff; 46 | --color-prettylights-syntax-variable: #ffa657; 47 | --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; 48 | --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; 49 | --color-prettylights-syntax-invalid-illegal-bg: #8e1519; 50 | --color-prettylights-syntax-carriage-return-text: #f0f6fc; 51 | --color-prettylights-syntax-carriage-return-bg: #b62324; 52 | --color-prettylights-syntax-string-regexp: #7ee787; 53 | --color-prettylights-syntax-markup-list: #f2cc60; 54 | --color-prettylights-syntax-markup-heading: #1f6feb; 55 | --color-prettylights-syntax-markup-italic: #c9d1d9; 56 | --color-prettylights-syntax-markup-bold: #c9d1d9; 57 | --color-prettylights-syntax-markup-deleted-text: #ffdcd7; 58 | --color-prettylights-syntax-markup-deleted-bg: #67060c; 59 | --color-prettylights-syntax-markup-inserted-text: #aff5b4; 60 | --color-prettylights-syntax-markup-inserted-bg: #033a16; 61 | --color-prettylights-syntax-markup-changed-text: #ffdfb6; 62 | --color-prettylights-syntax-markup-changed-bg: #5a1e02; 63 | --color-prettylights-syntax-markup-ignored-text: #c9d1d9; 64 | --color-prettylights-syntax-markup-ignored-bg: #1158c7; 65 | --color-prettylights-syntax-meta-diff-range: #d2a8ff; 66 | --color-prettylights-syntax-brackethighlighter-angle: #8b949e; 67 | --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; 68 | --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; 69 | } 70 | } 71 | 72 | .pl-c { 73 | color: var(--color-prettylights-syntax-comment); 74 | } 75 | 76 | .pl-c1, 77 | .pl-s .pl-v { 78 | color: var(--color-prettylights-syntax-constant); 79 | } 80 | 81 | .pl-e, 82 | .pl-en { 83 | color: var(--color-prettylights-syntax-entity); 84 | } 85 | 86 | .pl-smi, 87 | .pl-s .pl-s1 { 88 | color: var(--color-prettylights-syntax-storage-modifier-import); 89 | } 90 | 91 | .pl-ent { 92 | color: var(--color-prettylights-syntax-entity-tag); 93 | } 94 | 95 | .pl-k { 96 | color: var(--color-prettylights-syntax-keyword); 97 | } 98 | 99 | .pl-s, 100 | .pl-pds, 101 | .pl-s .pl-pse .pl-s1, 102 | .pl-sr, 103 | .pl-sr .pl-cce, 104 | .pl-sr .pl-sre, 105 | .pl-sr .pl-sra { 106 | color: var(--color-prettylights-syntax-string); 107 | } 108 | 109 | .pl-v, 110 | .pl-smw { 111 | color: var(--color-prettylights-syntax-variable); 112 | } 113 | 114 | .pl-bu { 115 | color: var(--color-prettylights-syntax-brackethighlighter-unmatched); 116 | } 117 | 118 | .pl-ii { 119 | color: var(--color-prettylights-syntax-invalid-illegal-text); 120 | background-color: var(--color-prettylights-syntax-invalid-illegal-bg); 121 | } 122 | 123 | .pl-c2 { 124 | color: var(--color-prettylights-syntax-carriage-return-text); 125 | background-color: var(--color-prettylights-syntax-carriage-return-bg); 126 | } 127 | 128 | .pl-sr .pl-cce { 129 | font-weight: bold; 130 | color: var(--color-prettylights-syntax-string-regexp); 131 | } 132 | 133 | .pl-ml { 134 | color: var(--color-prettylights-syntax-markup-list); 135 | } 136 | 137 | .pl-mh, 138 | .pl-mh .pl-en, 139 | .pl-ms { 140 | font-weight: bold; 141 | color: var(--color-prettylights-syntax-markup-heading); 142 | } 143 | 144 | .pl-mi { 145 | font-style: italic; 146 | color: var(--color-prettylights-syntax-markup-italic); 147 | } 148 | 149 | .pl-mb { 150 | font-weight: bold; 151 | color: var(--color-prettylights-syntax-markup-bold); 152 | } 153 | 154 | .pl-md { 155 | color: var(--color-prettylights-syntax-markup-deleted-text); 156 | background-color: var(--color-prettylights-syntax-markup-deleted-bg); 157 | } 158 | 159 | .pl-mi1 { 160 | color: var(--color-prettylights-syntax-markup-inserted-text); 161 | background-color: var(--color-prettylights-syntax-markup-inserted-bg); 162 | } 163 | 164 | .pl-mc { 165 | color: var(--color-prettylights-syntax-markup-changed-text); 166 | background-color: var(--color-prettylights-syntax-markup-changed-bg); 167 | } 168 | 169 | .pl-mi2 { 170 | color: var(--color-prettylights-syntax-markup-ignored-text); 171 | background-color: var(--color-prettylights-syntax-markup-ignored-bg); 172 | } 173 | 174 | .pl-mdr { 175 | font-weight: bold; 176 | color: var(--color-prettylights-syntax-meta-diff-range); 177 | } 178 | 179 | .pl-ba { 180 | color: var(--color-prettylights-syntax-brackethighlighter-angle); 181 | } 182 | 183 | .pl-sg { 184 | color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); 185 | } 186 | 187 | .pl-corl { 188 | text-decoration: underline; 189 | color: var(--color-prettylights-syntax-constant-other-reference-link); 190 | } 191 | -------------------------------------------------------------------------------- /assets/style.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 720px; 3 | } 4 | 5 | h1 { 6 | font-weight: 700; 7 | } 8 | 9 | .h1, 10 | .h2, 11 | .h3, 12 | .h4, 13 | .h5, 14 | .h6, 15 | h1, 16 | h2, 17 | h3, 18 | h4, 19 | h5, 20 | h6 { 21 | margin-top: 2.5rem; 22 | margin-bottom: 1rem; 23 | } 24 | 25 | article img { 26 | margin-bottom: 1rem; 27 | } 28 | 29 | .markdown-alert.markdown-alert-note { 30 | border-left: var(--bs-link-color) 0.5rem solid; 31 | padding: 0.5rem 1rem; 32 | margin-bottom: 2rem; 33 | } 34 | 35 | .markdown-alert.markdown-alert-warning { 36 | border-left: #b35b00 0.5rem solid; 37 | padding: 0.5rem 1rem; 38 | margin-bottom: 2rem; 39 | } 40 | 41 | .markdown-alert-title > .octicon { 42 | margin-right: 5px; 43 | margin-top: -3px; 44 | fill: var(--bs-body-color); 45 | } 46 | 47 | .highlight { 48 | margin-left: -12px; 49 | margin-right: -12px; 50 | padding-left: 12px; 51 | padding-right: 12px; 52 | background-color: var(--bs-light-bg-subtle); 53 | padding-top: 1rem; 54 | margin-bottom: 1rem; 55 | border-top: 0.03rem solid var(--bs-dark-border-subtle); 56 | border-bottom: 0.03rem solid var(--bs-dark-border-subtle); 57 | } 58 | 59 | header.profil { 60 | margin-top: 3rem; 61 | margin-bottom: 2.5rem; 62 | } 63 | 64 | header.profil a { 65 | text-decoration: none; 66 | } 67 | 68 | .profil .long-text { 69 | color: var(--bs-secondary-color); 70 | font-weight: 300; 71 | letter-spacing: 0.1rem; 72 | font-size: 1rem; 73 | margin-top: 0.3rem; 74 | } 75 | 76 | .profil-picture { 77 | width: 3rem; 78 | height: 3rem; 79 | border-radius: 10rem; 80 | } 81 | 82 | article { 83 | margin-top: 4rem; 84 | margin-bottom: 2rem; 85 | } 86 | 87 | h2:hover > a:after, 88 | h3:hover > a:after, 89 | h4:hover > a:after { 90 | margin-left: 0.2em; 91 | content: "#"; 92 | } 93 | 94 | h2 > a, 95 | h3 > a, 96 | h4 > a { 97 | color: var(--bs-heading-color); 98 | text-decoration: none; 99 | } 100 | 101 | .vote-emojis { 102 | margin-bottom: 1rem; 103 | } 104 | 105 | .vote-emojis > .btn.btn-outline-secondary.vote-emoji { 106 | border-radius: 20px; 107 | font-size: 0.8rem; 108 | margin-right: 0.2rem; 109 | padding: 0.2rem 0.5rem; 110 | letter-spacing: 0.1rem; 111 | } 112 | 113 | .vote-emojis > .btn.btn-outline-secondary.vote-emoji:hover { 114 | color: var(--bs-btn-color); 115 | background-color: var(--bs-primary-bg-subtle); 116 | } 117 | 118 | a.user-mention { 119 | text-decoration: none; 120 | font-weight: 600; 121 | color: rgba(var(--bs-secondary-text-emphasis), var(--bs-link-opacity, 1)); 122 | } 123 | 124 | a.user-mention:hover { 125 | text-decoration: underline; 126 | } 127 | 128 | footer { 129 | margin-bottom: 4rem; 130 | } 131 | 132 | footer p, 133 | .profil p { 134 | color: var(--bs-gray); 135 | font-weight: 250; 136 | } 137 | 138 | a.article-link { 139 | text-decoration: none; 140 | } 141 | 142 | a.article-link:hover .card-title { 143 | text-decoration: underline; 144 | } 145 | 146 | ol.custom { 147 | list-style-type: none; 148 | padding-left: 0; 149 | } 150 | 151 | ol.custom > li { 152 | color: var(--bs-gray-500); 153 | margin-bottom: 4rem; 154 | display: inline-block; 155 | transition: 0.3s; 156 | padding: 5px; 157 | border-radius: 3px; 158 | } 159 | 160 | p.card-text { 161 | text-align: left; 162 | color: var(--bs-gray-600); 163 | } 164 | 165 | .card-title { 166 | color: var(--bs-body-color); 167 | margin-top: 0.5rem; 168 | margin-bottom: 0.5rem; 169 | } 170 | 171 | hr { 172 | width: 20%; 173 | margin: 0 auto; 174 | } 175 | 176 | .table { 177 | display: block; 178 | overflow: auto; 179 | border-collapse: collapse; 180 | } 181 | 182 | #ballsCanvas { 183 | position: absolute; 184 | top: 0; 185 | left: 0; 186 | width: 100vw; 187 | height: 100vh; 188 | pointer-events: none; 189 | } 190 | 191 | :root { 192 | --bs-font-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 193 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", 194 | "Segoe UI Symbol"; 195 | --bs-font-monospace: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, 196 | Courier, monospace; 197 | } 198 | -------------------------------------------------------------------------------- /assets/tiny-utterances.css: -------------------------------------------------------------------------------- 1 | .tiny-utterances { 2 | display: flex; 3 | flex-direction: column; 4 | font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; 5 | font-size: 14px; 6 | gap: 1rem; 7 | margin-bottom: 4rem; 8 | } 9 | 10 | .tu-comment { 11 | border-radius: 0.25rem; 12 | border: 1px solid var(--bs-border-color); 13 | padding: 12px; 14 | word-wrap: break-word; 15 | } 16 | 17 | .tu-comment > *:last-child { 18 | margin-bottom: 0; 19 | } 20 | 21 | .tu-comment .tu-created-at { 22 | color: var(--bs-secondary); 23 | } 24 | 25 | .tu-comment .tu-created-at a { 26 | color: var(--bs-secondary); 27 | } 28 | 29 | .tu-comment .tu-created-at a:hover { 30 | color: var(--bs-secondary); 31 | } 32 | 33 | .tu-comment blockquote { 34 | border-left: 0.25rem solid var(--bs-secondary); 35 | margin: 0 0 1rem; 36 | padding: 0 1rem; 37 | color: var(--bs-secondary); 38 | } 39 | 40 | .tu-comment blockquote p { 41 | margin: 0; 42 | padding: 0; 43 | } 44 | 45 | .tu-header { 46 | align-items: center; 47 | display: flex; 48 | flex-direction: row; 49 | gap: 0.3rem; 50 | margin-bottom: 1rem; 51 | } 52 | 53 | .tu-header a { 54 | text-decoration: none; 55 | } 56 | 57 | .tu-header a:hover { 58 | text-decoration: underline; 59 | } 60 | 61 | .tu-avatar { 62 | border: 1px solid gray; 63 | border-radius: 50%; 64 | width: 2rem; 65 | } 66 | 67 | a.tu-button { 68 | background-color: #2da44e; 69 | border-radius: 0.25rem; 70 | color: white; 71 | display: inline-block; 72 | font-weight: 600; 73 | padding: 0.5rem; 74 | text-decoration: none; 75 | transition: 80ms; 76 | transition-property: background-color; 77 | width: fit-content; 78 | } 79 | 80 | a.tu-button:hover { 81 | background-color: #2c974b; 82 | } 83 | -------------------------------------------------------------------------------- /assets/tiny-utterances.js: -------------------------------------------------------------------------------- 1 | const fetchComments = async ( 2 | repositoryOwner, repositoryName, issueNumber, maxNumberOfComments 3 | ) => { 4 | const response = await fetch( 5 | `https://api.github.com/repos/${repositoryOwner}/${repositoryName}/issues/${issueNumber}/comments` + 6 | `?per_page=${maxNumberOfComments}`, 7 | { 8 | method: "GET", 9 | headers: { 10 | "Accept": "application/vnd.github.html+json" 11 | } 12 | } 13 | ); 14 | 15 | if (response.status == 200) { 16 | return response.json(); 17 | } 18 | 19 | throw new Error("Unexpected status: " + response.status); 20 | } 21 | 22 | const renderComment = comment => { 23 | const createdAt = new Date(comment.created_at).toLocaleString('en-US', { dateStyle: "medium" }); 24 | 25 | return `
26 |
27 | 28 | 29 | commented on ${createdAt} 30 |
31 | ${comment.body_html} 32 |
`; 33 | } 34 | 35 | const renderButton = (noComments, repoName, repoOwner, issueNumber) => { 36 | const text = noComments ? "Be the first to comment on GitHub" : "Join the discussion on GitHub"; 37 | const url = `https://github.com/${repoOwner}/${repoName}/issues/${issueNumber}#issuecomment-new`; 38 | 39 | return `${text}`; 40 | } 41 | 42 | var elements = document.querySelectorAll(".tiny-utterances"); 43 | elements.forEach(element => { 44 | const dataset = element.dataset; 45 | const repoOwner = dataset.repoOwner; 46 | const repoName = dataset.repoName; 47 | const issueNumber = Number(dataset.issueNumber); 48 | fetchComments(repoOwner, repoName, issueNumber, Number(dataset.maxComments)).then(comments => { 49 | const renderedComments = comments.map(renderComment).join(""); 50 | const renderedJoinButton = renderButton(comments.length == 0, repoName, repoOwner, issueNumber); 51 | element.innerHTML = renderedComments + renderedJoinButton; 52 | }).catch(console.error); 53 | }); 54 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.79.0" 3 | -------------------------------------------------------------------------------- /src/bin/preview.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use blog::Preview; 4 | 5 | /// Generates an image preview with the publkish date, title and comment count. 6 | /// Stores it in the preview.png image. 7 | fn main() -> anyhow::Result<()> { 8 | let args: Vec = std::env::args().collect(); 9 | let username = args.get(1).expect("missing `username` (first) argument").clone(); 10 | let publish_date = args.get(2).expect("missing `publish_date` (second) argument").clone(); 11 | let title = args.get(3).expect("missing `title` (third) argument").clone(); 12 | let comments_count = args.get(4).expect("missing `comments_count` (fourth) argument"); 13 | 14 | let comment_count: u32 = comments_count.parse()?; 15 | let preview = Preview { username, publish_date, title, comment_count }; 16 | fs::write("preview.png", preview.generate_png()?)?; 17 | 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::Context; 4 | use askama::Template; 5 | use resvg::render; 6 | use tiny_skia::{Pixmap, Transform}; 7 | use unicode_segmentation::UnicodeSegmentation; 8 | use usvg::{ImageHrefResolver, ImageKind, Options, Tree}; 9 | 10 | use crate::Spans::*; 11 | 12 | pub const WIDTH: u32 = 1200; 13 | pub const HEIGHT: u32 = 630; 14 | 15 | #[derive(Template)] 16 | #[template(path = "blog-post-preview.svg", escape = "none")] 17 | struct PreviewTemplate { 18 | username: String, 19 | publish_date: String, 20 | title_spans: Spans, 21 | comments_text: String, 22 | } 23 | 24 | enum Spans { 25 | One(String), 26 | Two(String, String), 27 | Three(String, String, String), 28 | } 29 | 30 | pub struct Preview { 31 | pub username: String, 32 | pub publish_date: String, 33 | pub title: String, 34 | pub comment_count: u32, 35 | } 36 | 37 | impl Preview { 38 | pub fn generate_png(self) -> anyhow::Result> { 39 | let Preview { username, publish_date, title, comment_count } = self; 40 | 41 | let comments_text = if comment_count == 1 { 42 | format!("{comment_count} comment") 43 | } else { 44 | format!("{comment_count} comments") 45 | }; 46 | 47 | let title_spans = cut_title(&title); 48 | let template = PreviewTemplate { username, publish_date, title_spans, comments_text }; 49 | let svg = template.to_string(); 50 | 51 | // Create a new pixmap buffer to render to 52 | let mut pixmap = Pixmap::new(WIDTH, HEIGHT).context("Pixmap allocation error")?; 53 | 54 | // Use default settings 55 | let mut options = Options { 56 | dpi: 192.0, 57 | text_rendering: usvg::TextRendering::GeometricPrecision, 58 | shape_rendering: usvg::ShapeRendering::CrispEdges, 59 | image_href_resolver: ImageHrefResolver { 60 | resolve_string: Box::new(move |path: &str, _| { 61 | let response = ureq::get(path).call().ok()?; 62 | let content_type = response.header("content-type")?; 63 | match content_type { 64 | "image/png" => { 65 | let mut image_buffer = Vec::new(); 66 | response.into_reader().read_to_end(&mut image_buffer).ok()?; 67 | Some(ImageKind::PNG(Arc::new(image_buffer))) 68 | } 69 | // ... excluding other content types 70 | _ => None, 71 | } 72 | }), 73 | ..Default::default() 74 | }, 75 | ..Default::default() 76 | }; 77 | 78 | options.fontdb_mut().load_font_data(include_bytes!("../Inter.ttc").to_vec()); 79 | 80 | let tree = Tree::from_str(&svg, &options)?; 81 | render(&tree, Transform::default(), &mut pixmap.as_mut()); 82 | pixmap.encode_png().map_err(Into::into) 83 | } 84 | } 85 | 86 | fn cut_title(title: &str) -> Spans { 87 | const MAX_LINE_CHARS: usize = 26; 88 | 89 | let mut acc = 0; 90 | let mut previous_stop = 0; 91 | let mut parts = Vec::new(); 92 | 93 | for (indice, word) in title.split_word_bound_indices() { 94 | if acc + word.len() > MAX_LINE_CHARS { 95 | parts.push(&title[previous_stop..indice]); 96 | previous_stop = indice; 97 | acc = 0; 98 | } else { 99 | acc += word.len(); 100 | } 101 | } 102 | 103 | let remaining = &title[previous_stop..]; 104 | if !remaining.is_empty() { 105 | parts.push(remaining); 106 | } 107 | 108 | match parts.len() { 109 | 1 => Spans::One(parts[0].to_string()), 110 | 2 => Spans::Two(parts[0].to_string(), parts[1].to_string()), 111 | _ => { 112 | let ellipsis = if parts.len() > 3 { "..." } else { "" }; 113 | let part = format!("{}{ellipsis}", parts[2]); 114 | Spans::Three(parts[0].to_string(), parts[1].to_string(), part) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::env; 3 | use std::hash::{BuildHasher, BuildHasherDefault, DefaultHasher}; 4 | use std::path::{Path, PathBuf}; 5 | 6 | use anyhow::Context; 7 | use askama::Template; 8 | use big_s::S; 9 | use http::header::{ACCEPT, AUTHORIZATION}; 10 | use octocrab::issues::IssueHandler; 11 | use octocrab::models::reactions::ReactionContent; 12 | use octocrab::models::timelines::Rename; 13 | use octocrab::params::State; 14 | use octocrab::{format_media_type, OctocrabBuilder}; 15 | use regex::Captures; 16 | use rss::extension::atom::{AtomExtension, Link}; 17 | use rss::{Channel, Guid, Item}; 18 | use scraper::Html; 19 | use serde::Deserialize; 20 | use tokio::fs::{self, File}; 21 | use tokio::io::{self, ErrorKind}; 22 | use url::Url; 23 | 24 | const GITHUB_BASE_URL: &str = "https://github.com/"; 25 | 26 | #[tokio::main] 27 | async fn main() -> anyhow::Result<()> { 28 | let owner_repo = std::env::var("GITHUB_REPOSITORY").expect("please define `GITHUB_REPOSITORY`"); 29 | let email_address = std::env::var("EMAIL_ADDRESS").expect("please define `EMAIL_ADDRESS`"); 30 | let (owner, repo) = owner_repo.split_once('/').unwrap(); 31 | 32 | fs::remove_dir_all("output").await.or_else(ignore_not_found)?; 33 | fs::create_dir("output").await?; 34 | fs::create_dir("output/assets").await?; 35 | fs::create_dir("output/preview").await?; 36 | fs::create_dir("output/assets/keys").await?; 37 | 38 | // Copy the JS assets 39 | fs::copy("assets/preview/homepage.png", "output/preview/homepage.png").await?; 40 | fs::copy("assets/script.js", "output/assets/script.js").await?; 41 | fs::copy("assets/script.js", "output/assets/script.js").await?; 42 | fs::copy("assets/balls.js", "output/assets/balls.js").await?; 43 | fs::copy("assets/matter.min.js", "output/assets/matter.min.js").await?; 44 | fs::copy("assets/tiny-utterances.js", "output/assets/tiny-utterances.js").await?; 45 | fs::copy("assets/style.css", "output/assets/style.css").await?; 46 | fs::copy("assets/tiny-utterances.css", "output/assets/tiny-utterances.css").await?; 47 | fs::copy("assets/bootstrap.min.css", "output/assets/bootstrap.min.css").await?; 48 | fs::copy("assets/starry-night.css", "output/assets/starry-night.css").await?; 49 | 50 | // Copy the keys assets 51 | for key in ('A'..='Z').chain('0'..='9') { 52 | let src = format!("assets/keys/{key}.png"); 53 | let dst = format!("output/assets/keys/{key}.png"); 54 | fs::copy(src, dst).await?; 55 | } 56 | 57 | // force GitHub to return HTML content 58 | let octocrab = if let Ok(token) = env::var("GITHUB_TOKEN") { 59 | eprintln!("I am authenticated!"); 60 | OctocrabBuilder::default() 61 | .add_header(ACCEPT, format_media_type("full")) 62 | .add_header(AUTHORIZATION, format!("Bearer {}", token)) 63 | .build()? 64 | } else { 65 | eprintln!("I am not authenticated!"); 66 | OctocrabBuilder::default().add_header(ACCEPT, format_media_type("full")).build()? 67 | }; 68 | 69 | let user: User = octocrab::instance().get(format!("/users/{}", owner), None::<&()>).await?; 70 | let html_bio_owner = linkify_at_references(user.bio); 71 | 72 | let repository = octocrab::instance().repos(owner, repo).get().await?; 73 | let homepage = repository 74 | .homepage 75 | .context("You must set the homepage URL of your blog on the repository")?; 76 | let homepage_url = Url::parse(&homepage)?; 77 | 78 | let page = octocrab 79 | .issues(owner, repo) 80 | .list() 81 | .state(State::Open) 82 | .labels(&[S("article")]) 83 | .per_page(50) 84 | .send() 85 | .await?; 86 | 87 | let mut items = Vec::new(); 88 | let mut articles = Vec::new(); 89 | for mut issue in page { 90 | let falback_date = issue.created_at; 91 | let body = issue.body.as_ref().unwrap(); 92 | let issue_handler = octocrab.issues(owner, repo); 93 | let url = correct_dash_case(&issue.title); 94 | let synopsis = synopsis(body); 95 | 96 | if let Some(html) = issue.body_html { 97 | let (urls_to_path, html) = replace_img_srcs_with_hashes(html); 98 | issue.body_html = Some(html); 99 | 100 | let mut body_bytes = Vec::new(); 101 | std::fs::create_dir_all("output/assets/images")?; 102 | for (url, path) in urls_to_path { 103 | body_bytes.clear(); 104 | let resp = ureq::get(&url).call()?; 105 | resp.into_reader().read_to_end(&mut body_bytes)?; 106 | std::fs::write(Path::new("output").join(path), &body_bytes)?; 107 | } 108 | } 109 | 110 | // But we must also create the redirection HTML pages to redirect 111 | // from the previous names of the article. 112 | let events = issue_handler.list_timeline_events(issue.number).per_page(100).send().await?; 113 | 114 | let mut publish_date = None; 115 | for event in events { 116 | if let Some(from_title) = event.rename.and_then(extract_from_field_from_rename) { 117 | create_and_write_template_into( 118 | format!("output/{}.html", correct_dash_case(from_title)), 119 | RedirectTemplate { redirect_url: correct_dash_case(&issue.title) }, 120 | ) 121 | .await?; 122 | } 123 | if event.label.map_or(false, |e| e.name == "article") { 124 | publish_date = event.created_at; 125 | } 126 | } 127 | 128 | articles.push(ArticleInList { 129 | title: issue.title.clone(), 130 | synopsis: synopsis.clone(), 131 | url: url.clone(), 132 | publish_date: publish_date.unwrap_or(falback_date).format("%B %d, %Y").to_string(), 133 | comments_count: issue.comments, 134 | guest_user: Some(issue.user.login.clone()).filter(|u| !u.eq_ignore_ascii_case(owner)), 135 | }); 136 | 137 | // Everytime we fetch an article we also fetch the author real name 138 | let author: User = 139 | octocrab::instance().get(format!("/users/{}", issue.user.login), None::<&()>).await?; 140 | let html_bio = linkify_at_references(author.bio); 141 | 142 | let mut profil_picture_url = author.avatar_url; 143 | profil_picture_url.set_query(Some("v=4&s=100")); 144 | let reaction_counts = collect_reactions(&issue_handler, issue.number).await?; 145 | 146 | items.push(Item { 147 | guid: Some(Guid { value: homepage_url.join(&url)?.to_string(), permalink: true }), 148 | title: Some(issue.title.clone()), 149 | link: Some(homepage_url.join(&url)?.to_string()), 150 | description: Some(synopsis.clone()), 151 | author: Some(format!("{email_address} ({})", author.name)), 152 | atom_ext: Some(AtomExtension { 153 | links: vec![Link { 154 | rel: "related".into(), 155 | href: homepage_url.join(&url)?.to_string(), 156 | title: Some(issue.title.clone()), 157 | ..Default::default() 158 | }], 159 | }), 160 | pub_date: Some(publish_date.as_ref().unwrap_or(&falback_date).to_rfc2822()), 161 | ..Default::default() 162 | }); 163 | 164 | // We create the article HTML pages. We must do that after the redirection 165 | // pages to be sure to replace the final HTML page by the article. 166 | let post_dash_case = correct_dash_case(&issue.title); 167 | create_and_write_template_into( 168 | format!("output/{post_dash_case}.html"), 169 | ArticleTemplate { 170 | profil_picture_url, 171 | username: author.name.clone(), 172 | html_bio: html_bio.clone(), 173 | url: format!("{homepage}{post_dash_case}"), 174 | publish_date: publish_date.unwrap_or(falback_date).format("%B %d, %Y").to_string(), 175 | title: issue.title.clone(), 176 | description: synopsis, 177 | html_content: insert_table_class_to_table(insert_anchor_to_headers( 178 | issue.body_html.unwrap(), 179 | )), 180 | article_comments_url: issue.html_url, 181 | comments_count: issue.comments, 182 | reaction_counts, 183 | owner: owner.to_string(), 184 | repository: repo.to_string(), 185 | issue_number: issue.number, 186 | preview_url: format!("{homepage}preview/{post_dash_case}.png"), 187 | }, 188 | ) 189 | .await?; 190 | 191 | // Generate the preview 192 | let preview_png = tokio::task::block_in_place(|| { 193 | let preview = blog::Preview { 194 | username: issue.user.login, 195 | publish_date: publish_date.unwrap_or(falback_date).format("%B %d, %Y").to_string(), 196 | title: issue.title.clone(), 197 | comment_count: issue.comments, 198 | }; 199 | preview.generate_png().unwrap() 200 | }); 201 | 202 | // And write it to disk 203 | tokio::fs::write(format!("output/preview/{post_dash_case}.png"), preview_png).await?; 204 | } 205 | 206 | let mut profil_picture_url = user.avatar_url; 207 | profil_picture_url.set_query(Some("v=4&s=100")); 208 | 209 | create_and_write_template_into( 210 | "output/index.html", 211 | IndexTemplate { 212 | profil_picture_url, 213 | username: user.name.clone(), 214 | description: "A chill and fun blog about Rust stuff and the journey of building my company: Meilisearch".to_string(), 215 | html_bio: html_bio_owner, 216 | url: homepage_url.clone(), 217 | preview_url: format!("{homepage}preview/homepage.png"), 218 | articles, 219 | }, 220 | ) 221 | .await?; 222 | 223 | let channel = Channel { 224 | title: format!("{}'s blog", user.name), 225 | items, 226 | link: homepage_url.to_string(), 227 | ..Default::default() 228 | }; 229 | fs::write("output/atom.xml", channel.to_string()) 230 | .await 231 | .context("writing into `output/feed.atom`")?; 232 | 233 | Ok(()) 234 | } 235 | 236 | #[derive(Deserialize)] 237 | struct User { 238 | avatar_url: Url, 239 | name: String, 240 | bio: String, 241 | } 242 | 243 | #[derive(Template)] 244 | #[template(path = "index.html", escape = "none")] 245 | struct IndexTemplate { 246 | profil_picture_url: Url, 247 | username: String, 248 | description: String, 249 | url: Url, 250 | preview_url: String, 251 | html_bio: String, 252 | articles: Vec, 253 | } 254 | 255 | struct ArticleInList { 256 | title: String, 257 | synopsis: String, 258 | url: String, 259 | publish_date: String, 260 | guest_user: Option, 261 | comments_count: u32, 262 | } 263 | 264 | #[derive(Template)] 265 | #[template(path = "article.html", escape = "none")] 266 | struct ArticleTemplate { 267 | profil_picture_url: Url, 268 | username: String, 269 | owner: String, 270 | repository: String, 271 | issue_number: u64, 272 | html_bio: String, 273 | url: String, 274 | publish_date: String, 275 | title: String, 276 | description: String, 277 | html_content: String, 278 | article_comments_url: Url, 279 | preview_url: String, 280 | comments_count: u32, 281 | reaction_counts: ReactionCounts, 282 | } 283 | 284 | #[derive(Template)] 285 | #[template(path = "redirect.html", escape = "none")] 286 | struct RedirectTemplate { 287 | redirect_url: String, 288 | } 289 | 290 | fn linkify_at_references(bio: impl AsRef) -> String { 291 | regex::Regex::new(r"(@(\w+))") 292 | .unwrap() 293 | .replace_all(bio.as_ref(), format!("$1")) 294 | .into_owned() 295 | } 296 | 297 | fn insert_table_class_to_table(html: impl AsRef) -> String { 298 | regex::Regex::new(r#"()"#) 299 | .unwrap() 300 | .replace_all(html.as_ref(), r#"$1 class="table table-striped" $2"#) 301 | .into_owned() 302 | } 303 | 304 | fn insert_anchor_to_headers(html: impl AsRef) -> String { 305 | regex::Regex::new(r#"<(h[234]) (.*)>(.*)"#) 306 | .unwrap() 307 | .replace_all(html.as_ref(), |captures: &Captures| { 308 | assert_eq!(&captures[1], &captures[4]); 309 | let header = &captures[1]; 310 | let header_attrs = &captures[2]; 311 | let text = &captures[3]; 312 | let dash_case = correct_dash_case(&captures[3]); 313 | format!(r##"<{header} id="{dash_case}" {header_attrs}>{text}"##) 314 | }) 315 | .into_owned() 316 | } 317 | 318 | fn replace_img_srcs_with_hashes(html: impl AsRef) -> (HashMap, String) { 319 | use kuchiki::parse_html; 320 | use kuchiki::traits::*; 321 | 322 | let mut urls_to_local_path = HashMap::new(); 323 | let document = parse_html().one(html.as_ref()); 324 | 325 | for a_element in document.select("a > img").unwrap() { 326 | let a_node = a_element.as_node().parent().unwrap(); 327 | let a_element_ref = a_node.as_element().unwrap(); 328 | let img_element_ref = a_element.as_node().as_element().unwrap(); 329 | let img_src = 330 | img_element_ref.attributes.borrow().get("src").map(|s| s.to_string()).unwrap(); 331 | 332 | let local_path = hash_path_from_url(&img_src); 333 | urls_to_local_path.insert(img_src.to_string(), local_path.clone()); 334 | a_element_ref.attributes.borrow_mut().insert("href", local_path.display().to_string()); 335 | img_element_ref.attributes.borrow_mut().insert("src", local_path.display().to_string()); 336 | } 337 | 338 | (urls_to_local_path, document.to_string()) 339 | } 340 | 341 | fn hash_path_from_url(url: impl AsRef) -> PathBuf { 342 | let hasher = BuildHasherDefault::::default(); 343 | let hash = hasher.hash_one(url.as_ref()); 344 | let url = Url::parse(url.as_ref()).unwrap(); 345 | let url_path = url.path(); 346 | let path = PathBuf::new().join("assets").join("images").join(format!("{hash:x}")); 347 | match Path::new(url_path).extension() { 348 | Some(extension) => path.with_extension(extension.to_str().unwrap()), 349 | None => path.with_extension("png"), 350 | } 351 | } 352 | 353 | fn synopsis(s: impl AsRef) -> String { 354 | let html = scraper::Html::parse_fragment(s.as_ref()); 355 | fn get_first_html_comment(document: &Html) -> Option<&str> { 356 | for node in document.tree.nodes() { 357 | if let Some(comment) = node.value().as_comment() { 358 | return Some(comment); 359 | } 360 | } 361 | None 362 | } 363 | 364 | get_first_html_comment(&html).map_or_else(String::new, ToOwned::to_owned) 365 | } 366 | 367 | fn correct_dash_case(s: impl AsRef) -> String { 368 | use slice_group_by::StrGroupBy; 369 | 370 | let mut output = String::new(); 371 | for group in s.as_ref().linear_group_by_key(|x| x.is_ascii_alphanumeric()) { 372 | if let Some(x) = group.chars().next() { 373 | if x.is_alphanumeric() { 374 | output.extend(group.chars().map(|x| x.to_ascii_lowercase())); 375 | } else { 376 | output.push('-'); 377 | } 378 | } 379 | } 380 | 381 | if output.ends_with('-') { 382 | output.pop(); 383 | } 384 | 385 | output 386 | } 387 | 388 | #[derive(Debug, Default)] 389 | struct ReactionCounts { 390 | heart: usize, 391 | plus_one: usize, 392 | laugh: usize, 393 | confused: usize, 394 | hooray: usize, 395 | minus_one: usize, 396 | rocket: usize, 397 | eyes: usize, 398 | } 399 | 400 | async fn collect_reactions( 401 | handler: &IssueHandler<'_>, 402 | issue_id: u64, 403 | ) -> anyhow::Result { 404 | let mut output = ReactionCounts::default(); 405 | 406 | for reaction in handler.list_reactions(issue_id).per_page(100).send().await? { 407 | match reaction.content { 408 | ReactionContent::Heart => output.heart += 1, 409 | ReactionContent::PlusOne => output.plus_one += 1, 410 | ReactionContent::Laugh => output.laugh += 1, 411 | ReactionContent::Confused => output.confused += 1, 412 | ReactionContent::Hooray => output.hooray += 1, 413 | ReactionContent::MinusOne => output.minus_one += 1, 414 | ReactionContent::Rocket => output.rocket += 1, 415 | ReactionContent::Eyes => output.eyes += 1, 416 | } 417 | } 418 | 419 | Ok(output) 420 | } 421 | 422 | async fn create_and_write_template_into( 423 | path: impl AsRef, 424 | template: impl Template, 425 | ) -> anyhow::Result<()> { 426 | let path = path.as_ref(); 427 | let mut article_file = File::create(path) 428 | .await 429 | .with_context(|| format!("When opening {:?}", path.display()))? 430 | .into_std() 431 | .await; 432 | template.write_into(&mut article_file)?; 433 | Ok(()) 434 | } 435 | 436 | fn ignore_not_found(e: io::Error) -> io::Result<()> { 437 | if e.kind() == ErrorKind::NotFound { 438 | Ok(()) 439 | } else { 440 | Err(e) 441 | } 442 | } 443 | 444 | /// Because the Rename struct only has private field we are 445 | /// forced to serialize/deserialize-trick to extract the from field, for now. 446 | fn extract_from_field_from_rename(rename: Rename) -> Option { 447 | match serde_json::to_value(rename).unwrap()["from"] { 448 | serde_json::Value::String(ref s) => Some(s.clone()), 449 | _ => None, 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /templates/article.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ title }}{% endblock %} 4 | {% block description %}{{ description }}{% endblock %} 5 | {% block url %}{{ url }}{% endblock %} 6 | {% block preview_url %}{{ preview_url }}{% endblock %} 7 | 8 | {% block head %} 9 | 10 | 11 | 12 | {% endblock %} 13 | 14 | {% block header %} 15 |
16 | 17 |
18 | Profil picture of {{ username }} 19 |

{{ username }}

20 |
21 |
22 |
23 | {% endblock %} 24 | 25 | {% block content %} 26 |

27 | {{ publish_date }}{{ comments_count }} comments 28 |

29 |
30 |

{{ title }}

31 | 32 | {{ html_content }} 33 |
34 | 35 |
36 | 🙂 ✚ 37 | 38 | {% if reaction_counts.plus_one != 0 %} 39 | 👍 {{ reaction_counts.plus_one }} 40 | {% endif %} 41 | 42 | {% if reaction_counts.minus_one != 0 %} 43 | 👎 {{ reaction_counts.minus_one }} 44 | {% endif %} 45 | 46 | {% if reaction_counts.laugh != 0 %} 47 | 😄 {{ reaction_counts.laugh }} 48 | {% endif %} 49 | 50 | {% if reaction_counts.heart != 0 %} 51 | ❤️ {{ reaction_counts.heart }} 52 | {% endif %} 53 | 54 | {% if reaction_counts.hooray != 0 %} 55 | 🎉 {{ reaction_counts.hooray }} 56 | {% endif %} 57 | 58 | {% if reaction_counts.confused != 0 %} 59 | 😕 {{ reaction_counts.confused }} 60 | {% endif %} 61 | 62 | {% if reaction_counts.rocket != 0 %} 63 | 🚀 {{ reaction_counts.rocket }} 64 | {% endif %} 65 | 66 | {% if reaction_counts.eyes != 0 %} 67 | 👀 {{ reaction_counts.eyes }} 68 | {% endif %} 69 |
70 | 71 | 81 | {% endblock %} 82 | 83 | {% block footer %} 84 |
85 |

About {{ username }}

86 |

{{ html_bio }}

87 |
88 |

Subscribe to my RSS/Atom feed for the latest updates and articles.

89 |
90 | {% endblock %} 91 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% block title %}{% endblock %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% block head %}{% endblock %} 34 | 35 | 36 | 37 |
38 | {% block header %}{% endblock %} 39 | 40 | {% block content %}{% endblock %} 41 | 42 | {% block footer %}{% endblock %} 43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /templates/blog-post-preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Published by 22 | {{ username }} 23 | on 24 | {{ publish_date }} 25 | 26 | 27 | 28 | 29 | {% match title_spans %} 30 | {% when One with (first) %} 31 | 32 | {{ first }} 33 | {% when Two with (first, second) %} 34 | 35 | {{ first }} 36 | {{ second }} 37 | {% when Three with (first, second, third) %} 38 | 39 | {{ first }} 40 | {{ second }} 41 | {{ third }} 42 | {% endmatch %} 43 | 44 | 45 | 46 | 47 | blog.kerollmops.com 48 | 49 | 50 | 51 | 52 | {{ comments_text }} 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ username }}{% endblock %} 4 | {% block description %}{{ description }}{% endblock %} 5 | {% block url %}{{ url }}{% endblock %} 6 | {% block preview_url %}{{ preview_url }}{% endblock %} 7 | 8 | {% block head %} 9 | 10 | 12 | {% endblock %} 13 | 14 | {% block header %} 15 |
16 | 17 |
18 | Profil picture of {{ username }} 19 |

{{ username }}

20 |
21 |
22 |

{{ html_bio }}

23 |
24 |
25 | {% endblock %} 26 | 27 | {% block content %} 28 |
    29 | {% for article in articles %} 30 |
  1. 31 | 32 | {{ article.publish_date }} — {{ article.comments_count }} comments 33 | {% if let Some(username) = article.guest_user %} 34 | — written by {{ username }} 35 | {% endif %} 36 | 37 | 38 |

    {{ article.title }}

    39 |

    {{ article.synopsis }}

    40 |
    41 |
  2. 42 | {% endfor %} 43 |
44 | {% endblock %} 45 | 46 | {% block footer %} 47 |
48 |
49 |

Subscribe to my RSS/Atom feed for the latest updates and articles.

50 |
51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /templates/redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Redirecting to {{ redirect_url }} 4 | 5 | 6 | --------------------------------------------------------------------------------