diff --git a/.gitignore b/.gitignore index ea8c4bf..8d1d5b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +/build +/node_modules +/content/css/syntax.css diff --git a/Cargo.lock b/Cargo.lock index 3dd57be..bdce8fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,30 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -58,24 +82,195 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "clap" version = "4.5.59" @@ -122,6 +317,105 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -132,24 +426,467 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "geo-types" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f8647af4005fa11da47cd56252c6ef030be8fa97bdbf355e7dfb6348f0a82c" +dependencies = [ + "approx", + "num-traits", + "serde", +] + +[[package]] +name = "geoutils" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36d244a08113319b5ebcabad2b8b7925732d15eec46d7e7ac3c11734f3b7a6ad" + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.11.0", + "ignore", + "walkdir", +] + +[[package]] +name = "gpx" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfabaf0e8a17a6fb7977fac3bd5846488462edb9f8b246605835483a5501e698" +dependencies = [ + "geo-types", + "thiserror 1.0.69", + "time", + "xml-rs", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.11.0", + "libc", + "redox_syscall 0.7.1", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "lock_api" version = "0.4.14" @@ -159,6 +896,50 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.1" @@ -166,25 +947,111 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] +[[package]] +name = "notify" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +dependencies = [ + "bitflags 2.11.0", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +dependencies = [ + "instant", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "olla" version = "0.1.0" dependencies = [ "anyhow", + "axum", + "chrono", "clap", + "fs_extra", + "geoutils", + "gpx", + "notify", + "pulldown-cmark", + "regex", + "serde", + "syntect", + "tera", "tokio", + "tower-http", + "yaml-rust2", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.11.0", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -203,17 +1070,153 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -223,6 +1226,34 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.11.0", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.44" @@ -232,13 +1263,102 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", ] [[package]] @@ -247,6 +1367,89 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -257,6 +1460,34 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -290,6 +1521,126 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 2.0.18", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "tera" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unicode-segmentation", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tokio" version = "1.49.0" @@ -318,37 +1669,286 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -360,6 +1960,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -367,60 +1983,160 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yaml-rust2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631a50d867fafb7093e709d75aaee9e0e0d5deb934021fcea25ac2fe09edc51e" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 4898e2b..90291ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,18 @@ edition = "2024" clap = { version = "4", features = ["derive"] } tokio = { version = "1", features = ["full"] } anyhow = "1" + +tera = "1" +pulldown-cmark = "0.13" +serde = { version = "1", features = ["derive"] } +yaml-rust2 = "0.11" +syntect = "5" +regex = "1" +chrono = { version = "0.4", features = ["serde"] } +gpx = "0.10" +geoutils = "0.5" +fs_extra = "1" + +notify = "7" +axum = "0.8" +tower-http = { version = "0.6", features = ["fs"] } diff --git a/content/css/Ascetic White.tmTheme b/content/css/Ascetic White.tmTheme new file mode 100644 index 0000000..116ffa1 --- /dev/null +++ b/content/css/Ascetic White.tmTheme @@ -0,0 +1,157 @@ + + + + + name + Ascetic White + settings + + + settings + + background + #FFFFFF + caret + #202020 + foreground + #202020 + invisibles + #D0D0D0 + lineHighlight + #D0D0D0 + selection + #C0C0C0 + findHighlight + #FFE792 + findHighlightForeground + #000000 + selectionBorder + #CCCCCC + activeGuide + #9D550FB0 + gutterForeground + #303030 + + bracketsForeground + #F8F8F2A5 + bracketsOptions + underline + + bracketContentsForeground + #F8F8F2A5 + bracketContentsOptions + underline + + tagsOptions + stippled_underline + + + + name + Comment + scope + comment + settings + + fontStyle + + foreground + #BBBBBB + + + + name + String + scope + string + settings + + foreground + #707070 + + + + name + Number + scope + constant.numeric + settings + + foreground + #707070 + + + + + name + diff.header + scope + meta.diff, meta.diff.header + settings + + foreground + #707070 + + + + name + diff.deleted + scope + markup.deleted + settings + + background + #FF8888 + foreground + + + + + name + diff.inserted + scope + markup.inserted + settings + + background + #99CC99 + foreground + + + + + name + diff.changed + scope + markup.changed + settings + + foreground + #E6DB74 + + + + + scope + constant.numeric.line-number.find-in-files - match + settings + + foreground + #202020 + + + + scope + entity.name.filename.find-in-files + settings + + foreground + #707070 + + + + + uuid + F01C6DC0-0977-11E3-8FFD-0800200C9A66 + + diff --git a/content/css/app.css b/content/css/app.css new file mode 100644 index 0000000..4fc98f7 --- /dev/null +++ b/content/css/app.css @@ -0,0 +1,40 @@ +@import "tailwindcss"; +@import "./syntax.css"; +@import "typeface-merriweather"; + +@plugin "@tailwindcss/typography"; + +@theme { + --font-serif: "Merriweather", ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; + --color-solarized: #fdf6e3; +} + +.prose { + --tw-prose-links: var(--color-indigo-600); + --tw-prose-pre-bg: var(--color-solarized); + --tw-prose-pre-code: inherit; +} + +.prose a { + font-style: italic; +} + +.prose pre code::after { + content: none !important; +} + +.prose pre { + width: 100vw; + margin-left: calc(-1 * max(50vw - 450px + 1rem, 1rem)); + padding-left: max(50vw - 450px + 1rem, 1rem); + padding-right: 1rem; + border-radius: 0; + overflow-x: visible; +} + +@utility container { + max-width: 900px; + width: 100%; + margin-inline: auto; + padding-inline: 1rem; +} diff --git a/content/css/verdandi.tmTheme b/content/css/verdandi.tmTheme new file mode 100644 index 0000000..b28f36e --- /dev/null +++ b/content/css/verdandi.tmTheme @@ -0,0 +1,386 @@ + + + + + name + Verdandi + settings + + + name + Variable and parameter name + scope + support.variable,meta.definition.variable.name,variable + settings + + fontStyle + + foreground + #51575e + + + + name + Object keys, TS grammar specific + scope + meta.object-literal.key entity.name.function,meta.object-literal.key + settings + + fontStyle + + foreground + #51575e + + + + name + Comment + scope + punctuation.definition.comment,punctuation.comment,comment + settings + + fontStyle + + foreground + #939ba4 + + + + name + Operator + scope + keyword.operator + settings + + fontStyle + bold + foreground + #212529 + + + + name + Punctuation + scope + meta.brace,string.link.md,punctuation.definition.metadata.md,meta.property-value punctuation.separator.key-value,tag.xml,tag.html,punctuation.tag,delimiter.tag,paren,brace,bracket,delimiter,punctuation + settings + + fontStyle + + foreground + #727981 + + + + name + JavaScript string interpolation ${} + scope + punctuation.section.embedded.end.metatag.php,punctuation.section.embedded.begin.metatag.php,punctuation.definition.template-expression.end.js,punctuation.definition.template-expression.end.ts,punctuation.definition.template-expression.begin.ts,punctuation.definition.template-expression.begin.js + settings + + fontStyle + + foreground + #212529 + + + + name + String + scope + meta.preprocessor string,meta.structure.dictionary.json string.quoted.double.json,meta.structure.dictionary.value.json string.quoted.double.json,support.constant.property-value.string,meta.property-value.string,string + settings + + fontStyle + italic + foreground + #727981 + + + + name + Primitive Literals + scope + + constant.numeric + meta.property-value.numeric + support.constant.property-value.numeric + meta.property-value.color + support.constant.property-value.color + constant.language + + settings + + fontStyle + + foreground + #212529 + + + + name + User names + scope + + constant.character + constant.other + entity.name.function + entity.name.class + entity.other.inherited-class + entity.other.attribute-name + entity.name + entity.other.attribute-name + entity.other.attribute-name.html + support.type.property-name + entity.name.tag.table + meta.structure.dictionary.json string.quoted.double.json + + settings + + fontStyle + + foreground + #212529 + + + + name + Keyword + scope + + keyword + meta.property-value.keyword + support.constant.property-value.keyword + meta.preprocessor.keyword + keyword.other.use + keyword.other.function.use + keyword.other.namespace + keyword.other.new + keyword.other.special-method + keyword.other.unit + keyword.other.use-as + + settings + + fontStyle + bold + foreground + #212529 + + + + name + Storage + scope + + storage + storage.type + storage.type.ts + storage.type.var.ts + storage.type.js + storage.type.var.js + storage.type.const.ts + storage.type.let.ts + storage.type.let.js + storage.type.const.js + entity.name.tag + + settings + + fontStyle + bold + foreground + #212529 + + + + name + Pointer, access, etc + scope + + meta.ptr + meta.pointer + meta.address + meta.array.cxx + + settings + + + + name + Preprocessor + scope + meta.preprocessor + settings + + fontStyle + + foreground + #212529 + + + + name + Library + scope + + support.type + support.class + support.function + support.constant + + settings + + fontStyle + + foreground + #212529 + + + + name + Invalid + scope + invalid + settings + + foreground + #e24747 + + + + name + Invalid deprecated + scope + invalid.deprecated + settings + + foreground + #e24747 + + + + name + Markdown Title Hash + scope + beginning.punctuation,entity.name.type.md,punctuation.definition.heading.md + settings + + fontStyle + + foreground + #212529 + + + + name + Markdown titles + scope + entity.name.section,markup.heading + settings + + fontStyle + bold + foreground + #212529 + + + + name + Markdown Raw + scope + markup.fenced_code,markup.fenced,markup.inline.raw,markup.raw + settings + + fontStyle + italic + foreground + #727981 + + + + name + Markdown link + scope + meta.image.inline,meta.link.inline,markup.link,string.other.link.title,string.other.link.description + settings + + fontStyle + + foreground + #212529 + + + + name + Makefile Variables + scope + variable.language.makefile,variable.other.makefile + settings + + fontStyle + + foreground + #212529 + + + + scope + markup.italic + settings + + fontStyle + italic + + + + scope + markup.bold + settings + + fontStyle + bold + + + + name + CSS Class + scope + entity.other.attribute-name.class.css + settings + + fontStyle + + foreground + #212529 + + + + name + CSS Tag name + scope + entity.name.tag.css + settings + + fontStyle + bold + foreground + #212529 + + + + name + CSS Property + scope + meta.property-name.css + settings + + fontStyle + + foreground + #212529 + + + + + diff --git a/content/images/adludio.png b/content/images/adludio.png new file mode 100644 index 0000000..4e205f8 Binary files /dev/null and b/content/images/adludio.png differ diff --git a/content/images/ansible-changed.jpg b/content/images/ansible-changed.jpg new file mode 100644 index 0000000..b5dfbf6 Binary files /dev/null and b/content/images/ansible-changed.jpg differ diff --git a/content/images/ansible-ok.jpg b/content/images/ansible-ok.jpg new file mode 100644 index 0000000..e8aa64f Binary files /dev/null and b/content/images/ansible-ok.jpg differ diff --git a/content/images/ansible.png b/content/images/ansible.png new file mode 100644 index 0000000..41082de Binary files /dev/null and b/content/images/ansible.png differ diff --git a/content/images/artifacts_browser.png b/content/images/artifacts_browser.png new file mode 100644 index 0000000..73ed4ee Binary files /dev/null and b/content/images/artifacts_browser.png differ diff --git a/content/images/artifacts_button.png b/content/images/artifacts_button.png new file mode 100644 index 0000000..f5d15bc Binary files /dev/null and b/content/images/artifacts_button.png differ diff --git a/content/images/background-laravel.jpg b/content/images/background-laravel.jpg new file mode 100644 index 0000000..72e5e6f Binary files /dev/null and b/content/images/background-laravel.jpg differ diff --git a/content/images/bepo.png b/content/images/bepo.png new file mode 100644 index 0000000..f3328c0 Binary files /dev/null and b/content/images/bepo.png differ diff --git a/content/images/codeurs-en-seine/injection-dependance.png b/content/images/codeurs-en-seine/injection-dependance.png new file mode 100644 index 0000000..5d0be85 Binary files /dev/null and b/content/images/codeurs-en-seine/injection-dependance.png differ diff --git a/content/images/cryptcheck.png b/content/images/cryptcheck.png new file mode 100644 index 0000000..fe05b48 Binary files /dev/null and b/content/images/cryptcheck.png differ diff --git a/content/images/favicon.png b/content/images/favicon.png new file mode 100644 index 0000000..0635aa7 Binary files /dev/null and b/content/images/favicon.png differ diff --git a/content/images/freshrss.jpg b/content/images/freshrss.jpg new file mode 100644 index 0000000..3d54084 Binary files /dev/null and b/content/images/freshrss.jpg differ diff --git a/content/images/freshrss.png b/content/images/freshrss.png new file mode 100644 index 0000000..2bcd6e6 Binary files /dev/null and b/content/images/freshrss.png differ diff --git a/content/images/guard.jpg b/content/images/guard.jpg new file mode 100644 index 0000000..18ea923 Binary files /dev/null and b/content/images/guard.jpg differ diff --git a/content/images/hacker.jpg b/content/images/hacker.jpg new file mode 100644 index 0000000..83851b8 Binary files /dev/null and b/content/images/hacker.jpg differ diff --git a/content/images/hexagon.svg b/content/images/hexagon.svg new file mode 100644 index 0000000..b880f50 --- /dev/null +++ b/content/images/hexagon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/content/images/html5.png b/content/images/html5.png new file mode 100644 index 0000000..5d290d9 Binary files /dev/null and b/content/images/html5.png differ diff --git a/content/images/https.png b/content/images/https.png new file mode 100644 index 0000000..f09eeac Binary files /dev/null and b/content/images/https.png differ diff --git a/content/images/insa/nosql.png b/content/images/insa/nosql.png new file mode 100644 index 0000000..0250329 Binary files /dev/null and b/content/images/insa/nosql.png differ diff --git a/content/images/ipv6.png b/content/images/ipv6.png new file mode 100644 index 0000000..2057925 Binary files /dev/null and b/content/images/ipv6.png differ diff --git a/content/images/lambda.png b/content/images/lambda.png new file mode 100644 index 0000000..ba03141 Binary files /dev/null and b/content/images/lambda.png differ diff --git a/content/images/latex-blank.jpg b/content/images/latex-blank.jpg new file mode 100644 index 0000000..cf5048d Binary files /dev/null and b/content/images/latex-blank.jpg differ diff --git a/content/images/latex-compact.jpg b/content/images/latex-compact.jpg new file mode 100644 index 0000000..b4357f7 Binary files /dev/null and b/content/images/latex-compact.jpg differ diff --git a/content/images/latex.png b/content/images/latex.png new file mode 100644 index 0000000..a07157a Binary files /dev/null and b/content/images/latex.png differ diff --git a/content/images/libon.png b/content/images/libon.png new file mode 100644 index 0000000..2230db8 Binary files /dev/null and b/content/images/libon.png differ diff --git a/content/images/migrations-subdirectories.png b/content/images/migrations-subdirectories.png new file mode 100644 index 0000000..d1e3d4d Binary files /dev/null and b/content/images/migrations-subdirectories.png differ diff --git a/content/images/nginx-apache.jpg b/content/images/nginx-apache.jpg new file mode 100644 index 0000000..dae6946 Binary files /dev/null and b/content/images/nginx-apache.jpg differ diff --git a/content/images/nginx.svg b/content/images/nginx.svg new file mode 100644 index 0000000..c308eb2 --- /dev/null +++ b/content/images/nginx.svg @@ -0,0 +1,33 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/content/images/no-image-available.png b/content/images/no-image-available.png new file mode 100644 index 0000000..fcf2a44 Binary files /dev/null and b/content/images/no-image-available.png differ diff --git a/content/images/pattern-matching.png b/content/images/pattern-matching.png new file mode 100644 index 0000000..cfac5cf Binary files /dev/null and b/content/images/pattern-matching.png differ diff --git a/content/images/pin-icon-end.png b/content/images/pin-icon-end.png new file mode 100644 index 0000000..190442f Binary files /dev/null and b/content/images/pin-icon-end.png differ diff --git a/content/images/pin-icon-start.png b/content/images/pin-icon-start.png new file mode 100644 index 0000000..b1b9c7e Binary files /dev/null and b/content/images/pin-icon-start.png differ diff --git a/content/images/pin-shadow.png b/content/images/pin-shadow.png new file mode 100644 index 0000000..948646e Binary files /dev/null and b/content/images/pin-shadow.png differ diff --git a/content/images/quantic-telecom-logo.png b/content/images/quantic-telecom-logo.png new file mode 100644 index 0000000..c9449f8 Binary files /dev/null and b/content/images/quantic-telecom-logo.png differ diff --git a/content/images/responsable.png b/content/images/responsable.png new file mode 100644 index 0000000..562b61b Binary files /dev/null and b/content/images/responsable.png differ diff --git a/content/images/rss.png b/content/images/rss.png new file mode 100644 index 0000000..26f6545 Binary files /dev/null and b/content/images/rss.png differ diff --git a/content/images/schroedinger.jpg b/content/images/schroedinger.jpg new file mode 100644 index 0000000..04b754b Binary files /dev/null and b/content/images/schroedinger.jpg differ diff --git a/content/images/stripe.png b/content/images/stripe.png new file mode 100644 index 0000000..b5a586b Binary files /dev/null and b/content/images/stripe.png differ diff --git a/content/images/ticket-beast-promoters.png b/content/images/ticket-beast-promoters.png new file mode 100644 index 0000000..3ad8f59 Binary files /dev/null and b/content/images/ticket-beast-promoters.png differ diff --git a/content/images/tp-live-laravel/prise-en-main.jpg b/content/images/tp-live-laravel/prise-en-main.jpg new file mode 100644 index 0000000..2fb4b20 Binary files /dev/null and b/content/images/tp-live-laravel/prise-en-main.jpg differ diff --git a/content/images/tp-live-laravel/testing.jpg b/content/images/tp-live-laravel/testing.jpg new file mode 100644 index 0000000..702d2ad Binary files /dev/null and b/content/images/tp-live-laravel/testing.jpg differ diff --git a/content/images/tp-live-laravel/todoapp.jpg b/content/images/tp-live-laravel/todoapp.jpg new file mode 100644 index 0000000..50d2ddb Binary files /dev/null and b/content/images/tp-live-laravel/todoapp.jpg differ diff --git a/content/images/typematrix.png b/content/images/typematrix.png new file mode 100644 index 0000000..4b59ba8 Binary files /dev/null and b/content/images/typematrix.png differ diff --git a/content/images/www.svg b/content/images/www.svg new file mode 100644 index 0000000..3162978 --- /dev/null +++ b/content/images/www.svg @@ -0,0 +1,74 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/content/images/your-app-doesnt-have-users.png b/content/images/your-app-doesnt-have-users.png new file mode 100644 index 0000000..0349ef2 Binary files /dev/null and b/content/images/your-app-doesnt-have-users.png differ diff --git a/content/posts/2015-10-18-new-static-website.md b/content/posts/2015-10-18-new-static-website.md new file mode 100644 index 0000000..a0ecf62 --- /dev/null +++ b/content/posts/2015-10-18-new-static-website.md @@ -0,0 +1,52 @@ +--- +title: Welcome to my new static website! +image: /images/guard.jpg +thumbnail: /images/thumbnail-guard.jpg +lang: en +website: dev +--- + +I just rebuilt my website with Hakyll, a library for generating static sites. No major visual changes: the design remains almost the same except I'm now using the newest [KNACSS](http://knacss.com/) version and I dropped [Geomicons](http://geomicons.com/) for [Font awesome](http://fontawesome.io/). I also took advantage of this change to add a new blog to my website, I'll try to keep it updated weekly (even if we all know it's impossible ^^). + +Why switch from PHP and [Laravel](https://laravel.com) to just HTML files? Why a static website generator? + + + +### Why a static website generator? + +Because, to quote [the Hakyll website](http://jaspervdj.be/hakyll/): + +> Static sites are fast, secure, easy to deploy, and manageable using version control. + +![A static, fast, secure and easy to deploy Buckingham's guard](/images/guard.jpg) + +I like the idea of using only an Apache web server (~~yeah, no Nginx for me, I'm old school~~ not anymore, only fools never change their minds ) and deploy my application easily with ~~`scp`~~ `rsync`. And, of course, no security breach, it's not a Wordpress! + +But, if HTML files are so cool, why don't just use HTML files? Generators brings a lot of possibilities for a blog. + +- Use of layouts or templates for your pages. Don't repeat yourself: don't copy / paste the `doctype` and the `header` for each post. +- Create a dynamic index for all your posts (with a summary), an archive page and a RSS / Atom feed. +- Write in [Markdown](https://daringfireball.net/projects/markdown/) instead of HTML, it's a bit easier and it's always possible to add HTML tags in your posts for specific information. + +### How is it working? + +As I said, I'm using [Hakyll](http://jaspervdj.be/hakyll/). It's develop with [Haskell](https://www.haskell.org/) but every language has a static site generator available: [Pelican](https://github.com/getpelican/pelican/) in Python, [Jekyll](http://jekyllrb.com/) in Ruby, [Sculpin](https://sculpin.io/) in PHP… + +I don't think Hakyll is the best one, but I'm currently in functional programming and I really wanted to try to do some Haskell. Hopefully, there is not a lot of Haskell to write and thanks to [Yann Esposito's article](http://yannesposito.com/Scratch/en/blog/Hakyll-setup/), the Haskell part was really easy to do. + +After developing the main Haskell file, I just run `stack build` once to compile my program. Then, to write this blog post, I run `stack exec thibaud watch`, open `http://localhost:8000` and start writing my post with [my editor](https://atom.io). Each time I save the file, a rebuild is triggered, and I can see the changes directly in my browser. Really efficient workflow! + +I write Markdown compiled in HTML thanks to the great [Pandoc](http://pandoc.org/) library. I use an HTML comment `` to create a « teaser » field. And I also can use HTML to add font awesome character for example like ``. All of the code is on [Github](https://github.com/ThibaudDauce/thibaud-dauce), and I'm open to contributions . + +### What's remaining to do? + +This is a first quick version of my Hakyll website, a lot is remaining: + +- Remove the `.html` extensions in the URL; +- Add a RSS feed; +- Clean some SASS files; +- Remove external calls to Google fonts and Font Awesome CDN; +- Concatenate CSS files; +- Add texts about my current work in Spark / Scala and my summer internship with NodeJS and Amazon Kinesis. + +Thanks for reading me! My English is far from perfect, so I'm open to all remarks, comments and improvements either with Github pull requests or on [Twitter](https://twitter.com/ThibaudDauce). diff --git a/content/posts/2015-11-07-live-without-flash.md b/content/posts/2015-11-07-live-without-flash.md new file mode 100644 index 0000000..576e4ce --- /dev/null +++ b/content/posts/2015-11-07-live-without-flash.md @@ -0,0 +1,61 @@ +--- +title: Live without Flash +image: /images/html5.png +thumbnail: /images/thumbnail-html5.png +lang: en +website: dev +--- + +Adobe Flash is awful. There are a lot of bugs and security problems with this software. Today, HTML5 is a good alternative but it requires a bit of setup. Some websites still have video players in Flash and it's impossible to watch without. + +What can we do? + + + +![The future is HTML5](/images/html5.png) + +### First, Firefox configuration + +Firefox is the best browser, not because its performances are better, but because Gecko is an alternative to Webkit (the layout engine software component for rendering web pages in Chrome, Chromium, Safari and Opera). If we don't want Google to be the new Internet Explorer, we need to fight for diversity in rendering engines. + +How run HTML5 videos in Firefox? First, we need to check some configuration. YouTube has a perfect web page for this task: [Youtube HTML5](https://www.youtube.com/html5). + +To enable all the possibilities of HTML5, you need to use the default HTML5 player. Then to be able to watch 480p and 1080p videos, there are a few steps. + +* [Media Source Extensions.](http://www.ghacks.net/2014/05/10/enable-media-source-extensions-firefox/) tldr; go to `about:config` and switch to true `media.mediasource.enabled`. +* [MSE & H.264.](http://www.ghacks.net/2014/07/25/enable-mse-h2-64-support-youtube-firefox-right-now/) tldr; go to `about:config` and switch to true `media.fragmented-mp4.*` except `media.fragmented-mp4.use-blank-decoder`. +* [MSE & WebM VP9](https://www.youtube.com/watch?v=R4No4kv3TA8) tldr; go to `about:config` and switch to true `media.mediasource.webm.enabled`. + +With Arch Linux, I think you should install `gst-plugins-good` and `gst-libav` too. + +Now you can watch HTML5 videos with Firefox in every website. + +### Then some trick for old Flash websites + +I use two CLI tools to watch Flash videos: `youtube-dl` and `livestreamer`. + +#### youtube-dl + +To install `youtube-dl`, visit [their website](https://rg3.github.io/youtube-dl/). Or if you're using Arch Linux, just run `yaourt -S youtube-dl`. This little piece of software allows you to download YouTube videos but not only. You can list all available websites with `youtube-dl --list-extractors`. To use it: +```bash +youtube-dl link-to-your-video +``` + +You can also download YouTube play lists, pages, etc. There are a lot of options like `--write-all-thumbnails` or `--write-subs`. Just check the man page, everything is explained. + +#### Livestreamer + +Same as `youtube-dl`, for the installation, just go to [their website](https://github.com/chrippa/livestreamer). With Arch Linux, run `yaourt -S livestreamer`. The problem with `youtube-dl` is you can't watch the video being downloaded because it downloads the video and the audio separately. With `livestreamer` you can stream a video from a lot of websites (`livestreamer --plugins`). To use it: +```bash +livestreamer -p vlc link-to-your-video best +``` + +I used a lot `livestreamer` with my old computer because VLC is way better than HTML5 to decode 1080p videos. I also use it with Twitch streams. + +#### Last trick + +I often watch AlloCiné, a french website which provides information on French cinema. They have a Flash video player and neither `youtube-dl` nor `livestreamer` support it. With the plugin Greasemonkey on Firefox, I use the script [AlloCine_Zap](http://userscripts-mirror.org/scripts/show/59373) to watch videos as HTML5 (and remove adds ^^). + + +---- +**Edit: thanks to Christophe Cluizel (Awesome guy! Go follow him [\@CCluizel](https://twitter.com/CCluizel) for some Scala, Spark and Machine Learning stuff), there's now a solution for "MSE & WebM VP9". It's not working on my computer but it may work for you. Thanks for his contribution.** diff --git a/content/posts/2015-11-15-rss-feed-with-hakyll.md b/content/posts/2015-11-15-rss-feed-with-hakyll.md new file mode 100644 index 0000000..466616d --- /dev/null +++ b/content/posts/2015-11-15-rss-feed-with-hakyll.md @@ -0,0 +1,65 @@ +--- +title: An RSS Feed with Hakyll +image: /images/rss.png +thumbnail: /images/thumbnail-rss.png +lang: en +website: dev +--- + +Twitter is a really cool social network. I daily use it and I discover a lot of new stuff with it. Unfortunately, it's a company, and they need to earn some money. More and more often, Twitter shows me unwanted content like *tweets you could like*, *accounts to follow* or *what happens when you're away*. + +I don't want all these features, I just want the tweets of people I follow in chronological order. What I want is an RSS feed. + + + +![RSS, the perfect alternative to Twitter](/images/rss.png) + +### Add an RSS feed in Hakyll + +It's a really simple operation, everything is explained [here](http://jaspervdj.be/hakyll/tutorials/05-snapshots-feeds.html). In the tutorial, they explain how to render either an Atom or an RSS feed. I decided to provide both. To do that, I've created a small function named `createFeed`. + +```hs +createFeed :: Identifier -> RenderingFunction -> Rules () +``` + +Where `RenderingFunction` is the signature provided by Hakyll for `renderAtom` and `renderRss`. + +```hs +type RenderingFunction = FeedConfiguration + -> Context String + -> [Item String] + -> Compiler (Item String) +``` + +Then, my function is just a copy paste from the function in the tutorial. + +```hs +createFeed name renderingFunction = create [name] $ do + route idRoute + compile $ do + posts <- fmap (take 10) . recentFirst =<< + loadAllSnapshots "posts/*" "content" + renderingFunction myFeedConfiguration feedCtx posts +``` + +Eventually, I just need to call my new function for `renderAtom` and `renderRss`. + +```hs +createFeed "feed.xml" renderRss +createFeed "atom.xml" renderAtom +``` + +### The HTML part + +To make my feeds work with most of the aggregators, I need to add two `links` to my default template. + +```html + + +``` + +### Which reader + +I personally use [FreshRSS](http://freshrss.org/). It's a simple, self-hostable aggregator under the AGPL license. The installation is really simple and there is a lot of useful features (like keyboard shortcuts and integration with [Wallabag](https://www.wallabag.org/)) + +![The clear interface of FreshRSS](/images/freshrss.jpg) diff --git a/content/posts/2015-11-22-4-lines-to-break-a-password-in-haskell.md b/content/posts/2015-11-22-4-lines-to-break-a-password-in-haskell.md new file mode 100644 index 0000000..988ec80 --- /dev/null +++ b/content/posts/2015-11-22-4-lines-to-break-a-password-in-haskell.md @@ -0,0 +1,67 @@ +--- +title: 4 lines to break a password in Haskell +image: /images/hacker.jpg +thumbnail: /images/thumbnail-hacker.jpg +lang: en +website: dev +--- + +I'm relatively new in functional programming but I really enjoy trying to code some piece of software in Haskell. It's a pure, strongly type and efficient language. I tried to develop [a little program](https://github.com/ThibaudDauce/habreaker) to break SHA1 hashes and the code in only 4 lines long. + + + +![A random hacker with the crucial black mask and really useful ski gloves to type on a keyboard :-)](/images/hacker.jpg) + +### Generate the list of all the possible strings + +I found [a Stack Overflow answer](http://stackoverflow.com/questions/9542313/how-to-generate-a-list-of-all-possible-strings-from-shortest-to-longest) where someone explains how to use the laziness of Haskell to build an infinite list of all the possible strings. The one line function is : +```hs +allStrings :: [String] +allStrings = [ c : s | s <- "" : allStrings, c <- ['a'..'z'] ++ ['0'..'9'] ] +``` + +`allStrings` is a list of strings defined as the concatenation of one character `c` in the list `['a'..'z'] ++ ['0'..'9']` and one string of `allStrings`. The definition is here recursive and we get an infinite list. + +### Compute the SHA1 hash + +First, I decided to use the `sha1` library but it wasn't really efficient. I decided to switch to the `cryptohash` library which is really faster. + +| | `sha1` | `cryptohash` | +|-----------------------|--------|--------------| +| 4 characters password | 4,74s | 0,56s | + +We need to transform the `String` in `ByteString` before computing the hash so I needed one line to code my SHA1 function: +```hs +sha1 :: String -> Digest SHA1 +sha1 = hash . B.pack +``` + +### Check the password hash + +Now we need to compute the hash to check if it matches the hash provided by the user. Here again it's a really simple operation in Haskell: +```hs +checkPassword :: Digest SHA1 -> String -> Bool +checkPassword hash string = (sha1 string) == hash +``` + +The function takes the hash we're looking for and a string, compute the SHA1 of the string and compare the two results. + +### Find the password + +Eventually, we need to find the correct password in the list of all possible strings. +```hs +findPassword :: Digest SHA1 -> String +findPassword passwordHash = (head . filter (checkPassword passwordHash)) allStrings +``` + +First we build the filtering function `checkPassword passwordHash`, then we build a function that filter a list and take the head `head . filter (checkPassword passwordHash)` and finally, we call this function on our infinite list of strings. + +Here we take advantage of the lazy evaluation of Haskell. The compiler is smart enough to stop the computation of the infinite list as soon as one element match the filter function because we're only looking for the head of the computed list. + +### Haskell in parallel + +I tried to parallelize this algorithm but I didn't find a way to do it with my infinite list. I need to dig further in the parallel strategies in Haskell. Maybe for a future blog post. I leave here my first attempt where the program never stop because Haskell can't figure out that we only need the head of the list and I ask for the evaluation of all the elements of the infinite list. +```hs +parFilter :: (S.NFData a) => (a -> Bool) -> [a] -> [a] +parFilter p = S.withStrategy (S.evalBuffer 1000 S.rseq) . filter p +``` diff --git a/content/posts/2015-12-06-write-well-formed-documents-with-latex.md b/content/posts/2015-12-06-write-well-formed-documents-with-latex.md new file mode 100644 index 0000000..9b47f2e --- /dev/null +++ b/content/posts/2015-12-06-write-well-formed-documents-with-latex.md @@ -0,0 +1,92 @@ +--- +title: Write well-formed documents with LaTeX +image: /images/latex.png +thumbnail: /images/thumbnail-latex.png +lang: en +website: dev +--- + +[LaTeX](http://www.latex-project.org/) is an awesome tool. It allows writing well-formed documents with no efforts. You can manage your references, your bibliography, your acronyms and a lot more. + + + +![It's really hard to illustrate LaTeX, try to find out why by searching "latex" in Google image :-)](/images/no-image-available.png) + +### LaTeX installation + +There are two different ways to install LaTeX. First, the basic installation with nothing more than the few main packages. LaTeX is a modular system, each functionality is in a package and you can install them separately. But it's a lot of pain to fix compilation issues when some package is missing and it's really time-consuming. On the other hand you can install the full LaTeX pack. It's the LaTeX compiler with every well-known packages so it's really big (a few Go). The choice of one method over the other is just about the size of your disk. + +### Functionalities + +#### Language specific typography + +LaTeX will take care of adding typographical spaces where needed. It's really useful, especially in language like French where you need to add thin non-breaking spaces before a lot of punctuation signs. + +#### Images + +LaTeX can be really useful with images, except the verbosity of the code. +```tex +\begin{figure} + \centering + \includegraphics[width=0.5\textwidth]{image.png} + \caption{Title of the image.} + \label{label-of-the-image} +\end{figure} +``` + +To include an image, you need to use the `\includegraphics` command with some options and the name of your file. To provide a title and a label (useful for references), you need to wrap the image in the `figure` environment. Finally, you need to specify the `\centering` command because it's not the default in LaTeX. + +One thing to know with LaTeX is you don't have a lot of control about where your image will be rendered: it could be just after the text, just before or two-page away. It's an advantage because you don't have to bother about the placement of your pictures and it forces you to use the good practice of referencing all your pictures in your text (use "figure 2.1" instead of "below" or "right after"). The main problem here is when you have highly visual documents with figures not only here to help the comprehension of your text but also with a real meaning. It can be annoying to switch between pages to read the document. In these cases, LaTeX may not be the most suitable tool to write your report. + +#### References + +One of the most useful feature of LaTeX is the references. It allows you to add labels in your text and use them to link to some parts of your document. LaTeX will replace the command `\ref{a label}` with the according number of the chapter, section or figure. If a label is missing, you can easily find it out looking for `??` in your PDF (or in the ugly LaTeX logs ^^). Of course, every reference is a real link in the PDF to jump to the referring section. If you add a chapter, every reference stay up-to-date without any modification. + +#### Bibliography and glossary + +Who never run into this problem of unused abbreviation or bibliography? LaTeX can help you in these cases. For the bibliography, you just need to specify all your sources and then use them in the text. [BibTeX](https://en.wikibooks.org/wiki/LaTeX/Bibliography_Management) will take care of everything else. +```tex +@Misc{documents-with-latex, + author = {Thibaud Dauce}, + title = {Write well-formed documents with LaTeX}, + howpublished = {\url{https://thibaud.dauce.fr/posts/2015-12-06-write-well-formed-documents-with-latex.html}}, + month = {December}, + year = {2015}, +} +``` + +This is an example of entry in a BibTex file. You can use this entry in your document with the `\cite` command, for example: +```tex +Thibaud thinks \LaTeX is awesome \cite{documents-with-latex}. +``` + +If one of the entry of your bibliography is not used anymore in your document, it will not be printed at the end. The same concept works with [the glossary](https://en.wikibooks.org/wiki/LaTeX/Glossary). You just need to define all your acronyms and definitions: +```tex +\newacronym[longplural={Frames per Second}]{fpsLabel}{FPS}{Frame per Second} +``` + +and use it: +```tex +My camera is recording at 30 \glspl{fpsLabel}. +``` + +If it's the first use of the acronym, LaTeX will show the full name and after that he'll use the acronym only in the document. A lot of options and configuration are available in the [documentation](https://en.wikibooks.org/wiki/LaTeX/Glossary). + +#### Syntax highlighting + +You can use the Python package `pygments` and the LaTeX package `minted` to add syntax highlighting in LaTeX. To use it, you need to install `pygments`: +```bash +yaourt -S python-pygments +``` + +Then you can compile your document with the `-shell-escape` flag to allow the LaTeX compiler to run system commands and that's it! + +### LaTeX generator + +I recently discover there was a [Yeoman](http://yeoman.io/) generator for LaTeX named `generator-latex` [available on Github](https://github.com/LeoColomb/generator-latex). The source code is a bit old and I didn't use it yet on a real project but it seems promising. The creation of the LaTeX boilerplate can be really long and I often copy paste old projects. This generator could be really awesome. It includes `grunt`, a NodeJS build tool to compile the project each time there is a modification on files and a live-reload of the PDF in the browser. + +### Conclusion + +It was a really just an introduction to all the features available with LaTeX. Write documents with LaTeX is harder than in Markdown or in LibreOffice but it brings a lot of very useful features. + +The compilation logs of LaTex are awful but the documentation is really great with a lot of examples and explanations. diff --git a/content/posts/2015-12-12-ux-fails-moving-elements.md b/content/posts/2015-12-12-ux-fails-moving-elements.md new file mode 100644 index 0000000..a50adf3 --- /dev/null +++ b/content/posts/2015-12-12-ux-fails-moving-elements.md @@ -0,0 +1,46 @@ +--- +title: "UX fails: moving elements" +video: /videos/ux-fails-twitter.ogv +lang: en +website: dev +--- + +What's UX? UX stands for User Experience. It refers to "a person's emotions and attitudes about using a particular product, system or service" according to [Wikipedia](https://en.wikipedia.org/wiki/User_experience). I'm starting today a series of blog posts about UX and some fails I ran into on software I use every day. + + + +### Twitter + +I'll start with [Twitter](https://twitter.com/ThibaudDauce). First of all, I'm a big fan of the service and I use it a lot. Especially for *favorites* (now *likes*). I follow a lot of people and I favorite a lot of articles as a read-it-later. I don't tweet much, and most of the time, I just scroll my timeline and favorite everything seems interesting. + +What happens when I scroll my timeline and click on some tweets. More information are shown under the text. Then, I continue scrolling and sometimes, I just click on the background. Twitter folds all previous opened tweets and I lose my position in my timeline. Just check the video below, I need to scroll up to find where I was. + + + +### DuckDuckGo + + +I'm using [DuckDuckGo](https://duckduckgo.com/) every +day and I almost never fallback to Google for my research online. When I try to use Google, it's just to discover that the result are not better than DuckDuckGo and I will not find my answer on the Internet. What's my favorite feature of DuckDuckGo? Bangs. I can type `!w Neil Patrick Harris` to go straight on [a Wikipedia page](https://fr.wikipedia.org/wiki/Neil_Patrick_Harris) or `!php date` to find the parameters of [a PHP function](http://php.net/manual/en/function.date.php). + +Do you know *Instant answers* from Google? DuckDuckGo has that too. But instead of showing a card in the right of your screen, DuckDuckGo put it before the first result. When I'm really fast and I want to click on the first result, DuckDuckGo often replaces the first official website by the Wikipedia page and I'm redirected even if I didn't want that in the first place. Check the video below. + + + +### Online.net + +[Online.net](https://www.online.net/en) is a famous hosting company in France. They tried to improve their website with a modern look. But they just created an awful user experience. + +The *Starting at €5.99 / month* text looks really like a button. But when I want to click on it, it just disappears, replaced quickly by a clickable table of options. Check the video below. + + + +### Conclusion + +If you don't want to confuse your users, please don't move elements of your page. In case of an asynchronous request, save the space right at the beginning. And if you really have to move an element, make the animation not too fast for the user to understand what's happening. diff --git a/content/posts/2015-12-19-maybe-its-now-an-option-to-avoid-null-pointer-exception.md b/content/posts/2015-12-19-maybe-its-now-an-option-to-avoid-null-pointer-exception.md new file mode 100644 index 0000000..70aa5a3 --- /dev/null +++ b/content/posts/2015-12-19-maybe-its-now-an-option-to-avoid-null-pointer-exception.md @@ -0,0 +1,60 @@ +--- +title: Maybe it's now an option to avoid NullPointerException? +image: /images/schroedinger.jpg +thumbnail: /images/thumbnail-schroedinger.jpg +lang: en +website: dev +--- + +`NullPointerException` is an obvious runtime problem in most Java applications. But it's not limited to Java, in PHP for example we could get a "Trying to get property of non-object". But today, there's a compilation alternative to this runtime problem. + + + +![Maybe Cat | Option[Cat] | Optional\](/images/schroedinger.jpg) + +Scala is a modern functional (and Object Oriented) language based on the JVM. I did a lot of Scala during the past year, mostly with the Spark framework for distributed computing. In Java, if you try to access a element in a `Map` for example, [the signature](http://docs.oracle.com/javase/7/docs/api/java/util/Map.html#get%28java.lang.Object%29) will look like this: +```java +/** + * Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key. + * + * @return Cat or null +*/ +public Cat get(String key); +``` + +And it's correct (for the compiler) to write: +```java +this.get("Schroedinger").miaou(); +``` + +If the `Cat` is here, it's fine. If he's not, the program crash with a `NullPointerException`. The compiler will not help you to catch this kind of bugs. + +In Scala [the signature](http://www.scala-lang.org/api/current/index.html#scala.collection.Map) for a type `Map[String, Cat]` is: +```scala +def get(key: String): Option[Cat] +``` + +So you cannot write something like before. The compiler will not allow it because the object `Option[Cat]` doesn't have a `miaou` method. So, how did we manage this? We use pattern matching to check the `None` option. Indeed, the `Option[Map]` type is divided into two types: `Some[Cat]` and `None`. +```scala +this.get("Schroedinger") match { + Some(cat) => cat.miaou() + None => // do something +} +``` + +Scala is not a strict language so it's possible to get the content of an option or a `null` with the `get` method on `Option[Cat]`: +```scala +// throw a NullPointerException +this.get("Schroedinger").get.miaou() +``` + +This concept of optional type come from functional programming. The last version of Java introduces a lot of functional programming stuff, it's now possible to use [the new type](http://docs.oracle.com/javase/8/docs/api/java/util/Optional.html): `Optional`. + +And, last but not least, it's not a new hype feature from 2015, some great (and old) languages like Haskell has [the `Maybe` type](http://haddock.stackage.org/lts-3.18/base-4.8.1.0/Prelude.html#t:Maybe) for a long time. `Maybe Cat` could be `Just Cat` or `Nothing`. + +---- +In conclusion, if you want to know more about this amazing functionality: + +* Scala explanations of `Option` type: [http://www.scala-lang.org/api/2.8.1/scala/Option.html](http://www.scala-lang.org/api/2.8.1/scala/Option.html) +* Haskell explanations of `Maybe` type: [http://learnyouahaskell.com/a-fistful-of-monads#getting-our-feet-wet-with-maybe](http://learnyouahaskell.com/a-fistful-of-monads#getting-our-feet-wet-with-maybe) +* Rust explanations of `Option` type: [https://doc.rust-lang.org/std/option/index.html](https://doc.rust-lang.org/std/option/index.html) diff --git a/content/posts/2016-01-24-automate-deployment-with-ansible.md b/content/posts/2016-01-24-automate-deployment-with-ansible.md new file mode 100644 index 0000000..a49be2a --- /dev/null +++ b/content/posts/2016-01-24-automate-deployment-with-ansible.md @@ -0,0 +1,58 @@ +--- +title: Automate deployment with Ansible +image: /images/ansible.png +thumbnail: /images/thumbnail-ansible.png +lang: en +website: dev +--- + +It's been a while since I wrote my last blog post. Today, I want to share with you my small experience with [Ansible](http://www.ansible.com/). Ansible is an open source project for application deployment. I heard of it last year but never took the time to dig in. Last week, I gave it a new try for [Quantic Telecom](https://www.quantic-telecom.net) to deploy new containers easily and I really enjoy it! + + + +### Why using Ansible? + +![Ansible is simple IT automation](/images/ansible.png) + +There is a lot of reasons to use Ansible for your deployments or any other tool like [Puppet](https://puppetlabs.com/) or [Chef](https://www.chef.io/chef/). In my specific use case, I needed to install a lot of new services, each one on a private container: [Piwik Analytics](https://piwik.org/), [OwnCloud](https://owncloud.org/), [Cachet HQ](https://cachethq.io/), [Gitlab](https://about.gitlab.com/), etc (yes, I'm a big fan of open-source software!). At first sight, nothing in common with all these services, there are Ruby and PHP, Nginx and Apache, Debian packages and manual installs. But in fact, for all these containers, I needed to configure the timezone, secure SSH, add some users and private keys, configure ZSH… + +In fact, as soon as you start having a documentation explaining how to create and configure new machines with some code to copy paste, I think you need to look into some deployment tools like Ansible. + +### Why I like Ansible + +Ansible is state-driven, it means that everything in your scripts will be link to state (if you do your job correctly). For example, instead of saying: "I want to install texlive-full", you will say: "I want the package texlive-full to be installed". It changes everything, this feature allows you to run your scripts multiple times without to worry about bad changes. The first time Ansible will check if texlive-full is installed and will install it if it's not. Then, the next times, it will show you that everything is already good to go. Moreover, it's really fast to run. + +![latex-full is already installed, two "ok"s, no "changed"](/images/ansible-ok.jpg) + +The second thing I like about Ansible is the big number of [Core Modules](http://docs.ansible.com/ansible/modules_core.html). Core Modules are really easy to use and can do almost every simple system administration task with a cool syntax, sometimes they're even easier to remember than the Linux commands. +```yaml +--- + +- name: be sure user thibaud exists + user: name=thibaud shell=/bin/zsh state=present + +- name: be sure thibaud's public key is in authorized_keys + authorized_key: + user: "thibaud" + key: "{{ lookup('file', 'thibaud.pub') }}" + state: present +``` + +In this example, I use the "user" Core Module and the "authorized_key" Core Module. The module documentation is really clear and you have access to every parameters and a lot of examples to understand how to use it. If I change one single thing manually on a server, Ansible will correct the problem. It's important to name things, and with this capture, I realize that my naming for the first task is not really accurate and should be: "be sure user thibaud exists and use ZSH". + +![thibaud is using bash for the staging server, one "ok", one "changed"](/images/ansible-changed.jpg) + +Last thing important with Ansible, it's only text so you can easily use Git to keep version control. We use a lot Proxmox and templates for containers and it's hard to keep track of the changes for several hundreds MBs images. With Ansible, everything can be saved within a few kBs. + +### What I dislike with Ansible + +The first time I open the documentation, I had trouble finding a good way to start. By default, Ansible read a weird `/etc/ansible` for the roles. I don't know who is using this and why but, for me, it shouldn't be the default. Everyone should have an ansible directory with their roles and their hosts and Ansible should throw an error if it can't detect any of this. + +I also try to use roles build by the community, and I found two kinds of roles : the easy ones and the over engineered ones. Due to the lack of package manager, I preferred to code the easy ones from scratch, because it's easier to manage than to have to add in the documentation to download these roles. And for the complex ones, I had troubles make them work with my system so I just develop my owns. + +### Conclusion + +Go get [Ansible](http://www.ansible.com/)! :-) +```bash +pacman -S ansible +``` diff --git a/content/posts/2016-02-12-tips-and-tricks-latex.md b/content/posts/2016-02-12-tips-and-tricks-latex.md new file mode 100644 index 0000000..ef6a7ad --- /dev/null +++ b/content/posts/2016-02-12-tips-and-tricks-latex.md @@ -0,0 +1,100 @@ +--- +title: Tips & Tricks with LaTeX +image: /images/latex.png +thumbnail: /images/thumbnail-latex.png +lang: en +website: dev +--- + +In [my previous blog post](/posts/2015-12-06-write-well-formed-documents-with-latex.html), I talked about how LaTeX is a fantastic tool and I concluded with some generators that could be interesting. I had to write some documents with LaTeX and I wanted to improve my work flow. I will share with you some tips & tricks I discovered. + + + +### Title page + +Create a title page with LaTeX could scare you but I realized that with the awesome LaTeX documentation, it was really easy to customize the title page. Just check [this wiki page](https://en.wikibooks.org/wiki/LaTeX/Title_Creation) and it should explain everything to you. + +### White spaces! + +![An ugly and compact document with a lot of text](/images/latex-compact.jpg) + +I don't like reading documents with a lot of text and no spaces. By default, paragraphs in LaTeX don't have margins, so I've got into the habit of adding `\\` after every paragraph to add a new line. I didn't know but this practice was the reason of all my LaTeX warning, complaining about *Underfull \\hbox (badness 10000) in paragraph at lines 11--51*. I knew it wasn't the best solution but I didn't take the time to look deeper. + +After just a few searches, I found the solution. And it's really straight forward, just add in your main file: +```tex +\setlength{\parskip}{\baselineskip} +``` + +The main drawback of this solution is that the table of content is using paragraph to print the titles. I ended up with big skip after every line in my table of content. For my use case it was really cool and the result is in fact nicer (because I don't have a lot of chapters and I think it's clearer) but for the majority, it's gonna be awful. I'm sorry I don't have a solution right now but as soon as I need to write another document, I will check a solution for this problem. + +![A document with a lot of text but more white spaces. Better!](/images/latex-blank.jpg) + +### Automatic compilation work flow + +* Save file +* Switch desktop to Okular +* Alt-Tab to switch to Konsole +* Up arrow to get the `make` +* Enter to execute the command +* Alt-Tab to go back to Okular +* Look the result + +This was my previous work flow. Not optimized at all, so I looked for some file watcher tool and I found the most basic one: *inotify-tools*. With Arch Linux it's just: +```bash +pacman -S inotify-tools +``` + +And then I wrote a simple bash script I named `watch.sh` watching recursively for changes in my current folder and running the `make` command each time a file was saved. +```bash +while true; do + change=$(inotifywait -re close_write,moved_to,create .) + make && make clean +done +``` + +And here is my new work flow: + +* Save file +* Switch desktop to Okular +* Look the result + +Much better! + +### GitLab CI + +I'm a big fan of GitLab, did you know that you can have as many private repositories as you want in [GitLab.com](https://gitlab.com/users/sign_in)? + +GitLab is an open-source alternative to Github, the interface and the features are awesome! GitLab comes with a Continuous Integration tool called GitLab CI. As with Travis, you can write a `.gitlab-ci.yml` and run the tests of your project after each `git push`. What's the point for LaTeX documents? I don't have tests. But I have an artifact. An artifact is a compiled file you want to access after your build (a binary to run your software, a documentation to deploy or a PDF document for example). Here is my `.gitlab-ci.yml` file: +```yml +--- + +pdflatex: + script: make + artifacts: +``` + +After your first build, you should see a new button in the GitLab UI. + +![Access your artifacts right from GitLab](/images/artifacts_button.png) + +And browse all the files. + +![Your PDF file should be listed here](/images/artifacts_browser.png) + +Of course, to use GitLab CI you need to deploy a GitLab CI runner with LaTeX installed. But it's really [with Ansible](/posts/2016-01-24-automate-deployment-with-ansible.html) :-) +```yml +--- + +- name: be sure Tex Live is present + apt: name=texlive-full state=latest +``` + +### A few more tips + +Use `\graphicspath{{images/}}` to set the root folder of all your graphics. + +If you use a french keyboard like me and manually write all you non-breakable spaces, insert this `\DeclareUnicodeCharacter{00A0}{~}` + +If you want a font size bigger than 12pt, `\usepackage{extsizes}` and define your document as a `\documentclass[14pt,a4paper]{extreport}`. + +And if you want two words to always be one (no hyphenation breaks), define `\hyphenation{Quantic Telecom}`. diff --git a/content/posts/2016-03-05-welcome-2016-ipv6-https-http2.md b/content/posts/2016-03-05-welcome-2016-ipv6-https-http2.md new file mode 100644 index 0000000..46df6e3 --- /dev/null +++ b/content/posts/2016-03-05-welcome-2016-ipv6-https-http2.md @@ -0,0 +1,133 @@ +--- +title: "Welcome 2016: IPv6, HTTPS & HTTP/2.0" +image: /images/https.png +thumbnail: /images/thumbnail-https.png +lang: en +website: dev +--- + +It's been a few weeks since a set a AAAA record for my domain name and allow IPv6 to visit my website. Same with SSL and Let's Encrypt. Today I switched from Apache 2 to Nginx and enabled HTTP/2.0 on my server. + + + +### IPv6 + +I'm the co-founder of Quantic Telecom, an operator and ISP for student in Rouen, France and I can tell you, IPv4 is dead! We really need IPv6, and not 3 years from now, today! So, change your VPS provider if you don't have an IPv6, set your AAAA DNS records and listen it: +``` +listen [::]:443; +``` + +![IPv6 is the most recent version of the Internet Protocol](/images/ipv6.png) + +### HTTPS everywhere + +#### Generate certificates + +With [Let's Encrypt](https://letsencrypt.org/), certificates are now free for everyone. So no excuse, just set up HTTPS (and HTTPS only). I first use the Let's Encrypt Python script but today I switch to an unofficial bash implementation of the free (as in free speech) Let's Encrypt protocol: [Neilpang/le](https://github.com/Neilpang/le). If you want to set up HTTPS on your server, just type: +```bash +git clone https://github.com/Neilpang/le.git +cd le +./le.sh install +# reset terminal +le issue /var/www/html/ thibaud.dauce.fr thibaud-dauce.fr,www.thibaud-dauce.fr ec-384 +le installcert thibaud.dauce.fr /etc/nginx/cert.pem /etc/nginx/cert.key /etc/nginx/ca.crt "cp /etc/nginx/cert.pem /etc/nginx/fullchain.pem && cat /etc/nginx/ca.crt >> /etc/nginx/fullchain.pem && service nginx reload" +``` + +#### Use them with Nginx + +![Nging webserver](/images/nginx.svg) + +```bash +ssl_certificate /etc/nginx/cert.pem; +ssl_certificate_key /etc/nginx/cert.key; +ssl_client_certificate /etc/nginx/ca.crt; +``` + +#### Secure SSL + +```bash +ssl_protocols TLSv1 TLSv1.1 TLSv1.2; +ssl_prefer_server_ciphers on; +ssl_ciphers "EECDH+AES:+AES128:+AES256:+SHA"; + +# HSTS +add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; +``` + +#### Why did I switch from Apache? + +![Apache is losing the web to Nginx ([source](http://www.nextplatform.com/2016/02/24/how-apache-is-losing-the-web-to-nginx/))](/images/nginx-apache.jpg) + +The length of the configuration files are similar but I prefer the Nginx JSON-like syntax over Apache XML. I'm sure there is a lot of errors in my configuration files (I'm new to Nginx) so, if you find something to change, please ping me on [Twitter](https://twitter.com/ThibaudDauce). + +```bash +# /etc/nginx/conf.d/default.conf +server { + listen 80; + listen [::]:80; + server_name thibaud.dauce.fr; + return 301 https://$server_name$request_uri; +} +``` + +```bash +# /etc/nginx/conf.d/default-ssl.conf +server { + ssl on; + ssl_certificate /etc/nginx/fullchain.pem; + ssl_certificate_key /etc/nginx/cert.key; + + # SSL security + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers "EECDH+AES:+AES128:+AES256:+SHA"; + + ssl_dhparam /etc/ssl/certs/dhparam.pem; + ssl_ecdh_curve secp384r1; + + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + + # HSTS + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; + + listen 443 ssl http2 default_server; + listen [::]:443 ssl http2 default_server; + server_name thibaud.dauce.fr; + + location / { + root /var/www/html; + } +} +``` + +To use this config file, you need to generate a stronger DHE parameter (it's gonna take a while ^^): +```bash +cd /etc/ssl/certs +openssl dhparam -out dhparam.pem 4096 +``` + +But then you should get an A+ [on every existing SSL test](https://tls.imirhil.fr/https/thibaud.dauce.fr)! + +![CryptCheck](/images/cryptcheck.png) + +### HTTP/2.0 + +As you can see in the previous config files, I simply add `http2` at the end of my `listen` line. It's really just that with Nginx 1.9. If you run a Debian 8 as I do, add these deb repositories to get the last version: +```bash +# /etc/apt/sources.list.d/nginx.list +deb http://nginx.org/packages/mainline/debian/ jessie nginx +deb-src http://nginx.org/packages/mainline/debian/ jessie nginx +``` + +And then: +```bash +wget http://nginx.org/keys/nginx_signing.key +apt-key add nginx_signing.key +apt-get update +apt-get upgrade +``` + +

IPv6, HTTPS & HTTP/2.0

+

Welcome 2016!

+ +Thanks [Aeris](https://twitter.com/aeris22) for your help, check [his french blog post](https://blog.imirhil.fr/2015/09/02/cryptcheck-verifiez-implementations-tls.html) about SSL security. diff --git a/content/posts/2016-12-07-laravel-foreign-relations.md b/content/posts/2016-12-07-laravel-foreign-relations.md new file mode 100644 index 0000000..08b0478 --- /dev/null +++ b/content/posts/2016-12-07-laravel-foreign-relations.md @@ -0,0 +1,167 @@ +--- +title: "Laravel Foreign Relations" +image: /images/stripe.png +thumbnail: /images/thumbnail-stripe.png +code: https://framagit.org/ThibaudDauce/laravel-foreign-relations +description: In this article, I'll create a proof of concept using Laravel's Eloquent relations to fetch a foreign data source as Stripe. +lang: en +website: dev +--- + +The Laravel community is a huge fan of [Stripe](https://stripe.com). As Laravel, Stripe provides a great API to interact with the product. In this article, I'll create a proof of concept using Laravel's Eloquent relations to fetch a foreign data source as Stripe. The same idea could be applied to other foreign data providers. + + + +### Laravel's relation mecanism + +As you may know, you can [define relations](https://laravel.com/docs/5.3/eloquent-relationships) with Laravel. In your model, `hasMany`, `belongsTo` or `belongsToMany` are already-provided helpers to create links between your models. + +```php +hasMany(Comment::class); + } +} +``` + +But behind the scene, these methods returns children of the `Relation` object. These children could be `HasMany`, `BelongsTo`, `BelongsToMany`, etc. The `Relation` object is an abstract class you need to extend to build a new type of relations. This class is tightly tied with the Eloquent Builder class, which is a big problem. Laravel assumes Eloquent models in both sides of our relation. But in our case, it's one Eloquent model and one `Stripe\Customer` object. + +### Building the `StripeRelation` + +`StripeRelation` is my child for the `Relation` object. It will fetch and hydrate my models with the customers from Stripe. + +#### Constructor + +In order to build a `Relation` object, Laravel needs a builder from the foreign class and the instance of the main model. In our example, the foreign class is `Stripe\Customer` and the main model is `App\User`. + +First problem: I don't have an Eloquent builder for Stripe customers. I'll need [to create my own](#building-the-stripequerybuilder). + +```php +public function __construct(Model $model) +{ + parent::__construct(app(StripeQueryBuilder::class), $model); +} +``` + +#### `addConstraints` and `addEagerConstraints` + +The main goal of these methods is to set a `where` clause on the query builder with "id = stripe_id" or "id in [stripe_id_1, stripe_id_2, stripe_id_3]". Since my query builder is a special one, and I know it will be a custom `StripeQueryBuilder`, I can set two public properties `id` for when I need to fetch one Stripe customer, `ids` when I need to fetch all Stripe customers for a collection of users. We will see in the next part how we use these information to fetch the customers from Stripe. + +```php +public function addConstraints() +{ + $this->query->id = $this->parent->{$this->localKey}; +} + +public function addEagerConstraints(array $models) +{ + $this->query->ids = $this->getKeys($models, $this->localKey); +} +``` + +In these methods `$this->localKey` is the name of the attribute of the user model which contains the Stripe ID (default to `stripe_id`). + +#### `initRelation` + +This method sets the default foreign model for all the users. I don't have a default Stripe model so I just return the unchanged array of models. + +#### `getResults` + +The name of this method is misleading, its goal is just to fetch one foreign model (one Stripe customer) for one parent model (one user) when you simply type `$user->stripe`. I can delegate the API call to my `StripeQueryBuilder` class. The `stripe_id` of the user has already been set in the `StripeQueryBuilder` by the parent `Relation` class with `addConstraints`. + +```php +public function getResults() +{ + return $this->query->first(); +} +``` + +#### `match` + +This method is called when you try to load the relation on a collection to avoid the [N + 1 problem](https://laravel.com/docs/5.3/eloquent-relationships#eager-loading). It takes three arguments: + +- the models — an array of users which needs to be populated with the customers from Stripe +- the results — an Eloquent Collection from the query builder (basicaly a call to `$query->addEagerConstraints()->get()`) which contains for each Stripe ID the Stripe customer associated +- the relation — the name of the current relation being processed + +The logic is simple, loop over the models and set the relation to whatever is in the `$results` collection: + +```php +public function match(array $models, Collection $results, $relation) +{ + foreach ($models as $model) { + $model->setRelation($relation, $results[$model->{$this->localKey}]); + } + + return $models; +} +``` + +### Building the `StripeQueryBuilder` + +Now we need to build our `StripeQueryBuilder` which will call the Stripe API to fetch the requested customers. My class needs to extend the Eloquent Builder and override some methods. Most of the methods will not be changed so it will be impossible to call more *intelligent* methods like `$user->stripe()->update(['delinquent' => true])`. + +#### Constructor + +The parent model needs a Query. I will simply mock it. + +```php +public function __construct() +{ + parent::__construct(Mockery::mock(QueryBuilder::class)); +} +``` + +#### `first` + +The `first` method fetch one customer based on the ID previously set in the `$id` attribute of the Query Builder. + +```php +public function first($columns = ['*']) +{ + if ($this->id) { + return Customer::retrieve($this->id); + } + + throw new \Exception("'first' method called with no ID provided."); +} +``` + +#### `get` + +It's impossible to ask Stripe for all customers with IDs in an array (a simple `WHERE IN` with SQL). I need to fetch all customers and then filter them with PHP. The maximum number of customers for each request is 100. So I need to ask for 100 customers until Stripe tells me that there is no more or all my IDs have been found. + +```php +public function get($columns = ['*']) +{ + $models = new Collection; + + if ($this->ids) { + $totalCount = count($this->ids); + + foreach ($this->fetchAllStripeCustomers() as $customer) { + if (in_array($customer->id, $this->ids)) { + $models[$customer->id] = $customer; + } + + if ($totalCount === $models->count()) { + // All models were found, no need to continue to fetch the customers. + break; + } + } + } + + return $models; +} +``` + +### Conclusion + +This project is a very basic implementation. It's probably possible to do more. But it shows how you can keep a simple API for your models even with foreign relations. The working final project is available on [https://framagit.org/ThibaudDauce/laravel-foreign-relations](https://framagit.org/ThibaudDauce/laravel-foreign-relations). + +I may put this work in a package if some people are interested. + +Eventually, for french artisan, I offer Laravel trainings, just visit [https://www.formations-laravel.fr](https://www.formations-laravel.fr) and send me a message! diff --git a/content/posts/2016-12-15-laravel-recursive-migrations.md b/content/posts/2016-12-15-laravel-recursive-migrations.md new file mode 100644 index 0000000..712a6a5 --- /dev/null +++ b/content/posts/2016-12-15-laravel-recursive-migrations.md @@ -0,0 +1,189 @@ +--- +title: "Laravel Recursive Migrations" +image: /images/migrations-subdirectories.png +thumbnail: /images/thumbnail-migrations-subdirectories.png +code: https://framagit.org/ThibaudDauce/laravel-recursive-migrations +description: This package allows to put Laravel migrations into sub-directories. Let's build it together! +lang: en +website: dev +--- + +I'm currently developing the [Quantic Telecom](https://www.quantic-telecom.net) website with Laravel. My `app` folder is well-organized as my `views` and my `resources` but not my migrations because the Laravel `php artisan migrate` command doesn't run nested migrations. My package [`thibaud-dauce/laravel-recursive-migrations`](https://framagit.org/ThibaudDauce/laravel-recursive-migrations) will allow us to put migrations into sub-directories. + +In this blog post, I will go through the development of this package. For information, I found two links speaking about this particular problem: on StackOverflow ["Laravel running migrations on “app/database/migrations” folder recursively"](http://stackoverflow.com/questions/21641606/laravel-running-migrations-on-app-database-migrations-folder-recursively) and this issue on Github ["[Proposal/Enhancement] Artisan CLI Migrate command does not go through all sub folders of the migrations folder."](https://github.com/laravel/framework/issues/2561) + +The goal of my package is not to create new commands like `submigrate`, `submigrate:rollback`… I want to keep the original names `migrate`, `migrate:rollback` and add an option `--recursive` which will look into all sub-directories. + + + +When thinking about this problem, several solutions came into my mind: + +- Extending [`Illuminate\Database\Migrations\Migrator`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Migrations/Migrator.php) +- Extending all commands (migrate, refresh, reset…) + +### Extending [`Illuminate\Database\Migrations\Migrator`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Migrations/Migrator.php) + +The easiest approach is to extend the [`Illuminate\Database\Migrations\Migrator`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Migrations/Migrator.php) and override the [`getMigrationFiles()`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Migrations/Migrator.php#L298-L307) method. This method currently use the [`glob()`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Filesystem/Filesystem.php#L364-L367) method of the filesystem to get all files in the migration directory following the "*_*.php" pattern. Using the [`allFiles()`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Filesystem/Filesystem.php#L398-L401) recursive method instead should do the trick. + +The main benefit of this approach is to act on all commands (migrate, rollback, reset…) with only one change because the [`Migrator`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Migrations/Migrator.php) is used by all of them. But the drawback is that the [`Migrator`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Migrations/Migrator.php) is a low level class, so it can't access the command's flags. So I will be forced to apply migrations recursively all the time. + +### Extending all commands + +Extending the commands' classes will allow me to override some of their methods and modify their behavior. It seems to be a good option for me. The only disadvantage is the fact that I need to create a new class for each of the migrations' commands: [`MigrateCommand`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/MigrateCommand.php), [`RefreshCommand`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/RefreshCommand.php), [`ResetCommand`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/ResetCommand.php), [`RollbackCommand`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/RollbackCommand.php) and [`StatusCommand`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/StatusCommand.php). But I can manage the duplicate code with some traits. + +So, I will split the work into two traits. The first one will be in charged of fetching subdirectories and will be general (pure and not linked to Laravel or the migration system). The second one, the ugly one, will need to be add to a child of the [`Illuminate\Database\Console\Migrations\BaseCommand`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/BaseCommand.php) and will override the [`getMigrationPaths()`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/BaseCommand.php#L24-L36), [`getOptions()`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/MigrateCommand.php#L104-L119) and [`call()`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Console/Command.php#L179-L186) methods. I think it's often a bad choice to force a trait into an inheritance tree, but I couldn't find another solution… + +#### My first trait: [`Subdirectories`](https://framagit.org/ThibaudDauce/laravel-recursive-migrations/blob/master/src/Subdirectories.php) + +This trait will be pure. It will fetch the sub-directories for a path. I could have done a service class injected by the container but I thought it will be over complicated for some business logic that will never change (sub-directories will always be sub-directories). A service class would have given more flexibility to the end user by allowing him to change or extend my implementation. + +The first method of this trait will fetch all sub-directories in a path with the help of the [`Finder`](https://github.com/symfony/symfony/blob/3.2/src/Symfony/Component/Finder/Finder.php) component: + +```php +/** + * Fetch all subdirectories from a path. + * The original path is included in the array. + * + * @param string $path + * @return string[] + */ +protected function subdirectories($path) +{ + return Collection::make(Finder::create()->in($path)->directories()) + ->map(function (SplFileInfo $directory) { + return $directory->getPathname(); + }) + ->prepend($path) + ->values() + ->toArray(); +} +``` + +The second method will flatMap the result of the first method for multiple base paths. flatMap is a function which map all element in an array to an array (you get an array of array) and then merge all arrays into one. + +```php +/** + * Fetch all subdirectories from an array of base paths. + * The original paths are included in the array. + * + * @param string[] $paths + * @return string[] + */ +protected function allSubdirectories($paths) +{ + return Collection::make($paths) + ->flatMap(function ($path) { + return $this->subdirectories($path); + }) + ->toArray(); +} +``` + +#### My second trait: [`RecursiveMigrationCommand`](https://framagit.org/ThibaudDauce/laravel-recursive-migrations/blob/master/src/RecursiveMigrationCommand.php) + +The goal of this trait is to provide new methods for the children of [`Illuminate\Database\Console\Migrations\BaseCommand`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/BaseCommand.php). It will use the previously seen [`Subdirectories`](https://framagit.org/ThibaudDauce/laravel-recursive-migrations/blob/master/src/Subdirectories.php) trait. + +First, it will add the `--recursive` option. Nothing special here to the [`getOptions()`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/MigrateCommand.php#L104-L119) method. + +```php +/** + * Get the console command options. + * + * @return array + */ +protected function getOptions() +{ + return array_merge(parent::getOptions(), [ + ['recursive', 'r', InputOption::VALUE_NONE, 'Indicates if the migrations should be run recursively (nested directories).'] + ]); +} +``` + +Then, it will add the sub-directories to the migration paths if the flag is set. The [`allSubdirectories()`](https://framagit.org/ThibaudDauce/laravel-recursive-migrations/blob/master/src/Subdirectories.php#L36-43) method comes from the [`Subdirectories`](https://framagit.org/ThibaudDauce/laravel-recursive-migrations/blob/master/src/Subdirectories.php) trait. + +```php +/** + * Get all of the migration paths. + * + * @return array + */ +protected function getMigrationPaths() +{ + $paths = parent::getMigrationPaths(); + return $this->option('recursive') ? $this->allSubdirectories($paths) : $paths; +} +``` + +And finally, the last one, `call()`, is only required for the [`RefreshCommand`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/RefreshCommand.php). [`RefreshCommand`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/RefreshCommand.php) does not contain any logic. It's only a wrapper around the [`ResetCommand`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/ResetCommand.php) and the [`MigrateCommand`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/MigrateCommand.php). Internally, it calls the others commands with the [`call()`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/Console/Migrations/RefreshCommand.php#L50-L66) method, passing its arguments like "step", "database" or "path"… But in this implementation, it will not pass the "recursive" flag to the other commands. I need to set it if the command called is a *migrate* command: + +```php +/** + * Call another console command. + * + * @param string $command + * @param array $arguments + * @return int + */ +public function call($command, array $arguments = []) +{ + if (starts_with($command, 'migrate')) { + $arguments['--recursive'] = $this->option('recursive'); + } + + parent::call($command, $arguments); +} +``` + +#### New Commands + +I also need to create my own commands for the [`MigrateCommand`](https://framagit.org/ThibaudDauce/laravel-recursive-migrations/blob/master/src/Commands/MigrateCommand.php), [`RefreshCommand`](https://framagit.org/ThibaudDauce/laravel-recursive-migrations/blob/master/src/Commands/RefreshCommand.php), [`ResetCommand`](https://framagit.org/ThibaudDauce/laravel-recursive-migrations/blob/master/src/Commands/ResetCommand.php), [`RollbackCommand`](https://framagit.org/ThibaudDauce/laravel-recursive-migrations/blob/master/src/Commands/RollbackCommand.php) and [`StatusCommand`](https://framagit.org/ThibaudDauce/laravel-recursive-migrations/blob/master/src/Commands/StatusCommand.php). All the implementations are the same thanks to the [`RecursiveMigrationCommand`](https://framagit.org/ThibaudDauce/laravel-recursive-migrations/blob/master/src/RecursiveMigrationCommand.php) trait. For example, my [`MigrateCommand`](https://framagit.org/ThibaudDauce/laravel-recursive-migrations/blob/master/src/Commands/MigrateCommand.php): + +```php +app->extend('command.migrate', function () { + return new MigrateCommand($this->app['migrator']); +}); + +$this->app->extend('command.migrate.rollback', function () { + return new RollbackCommand($this->app['migrator']); +}); + +$this->app->extend('command.migrate.refresh', function () { + return new RefreshCommand($this->app['migrator']); +}); + +$this->app->extend('command.migrate.reset', function () { + return new ResetCommand($this->app['migrator']); +}); + +$this->app->extend('command.migrate.status', function () { + return new StatusCommand($this->app['migrator']); +}); +``` + +I didn't found a way to override the first binding with `bind()` because the [`MigrationServiceProvider`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Database/MigrationServiceProvider.php) is deferred by the [`ConsoleSupportServiceProvider`](https://github.com/laravel/framework/blob/5.3/src/Illuminate/Foundation/Providers/ConsoleSupportServiceProvider.php) so it will register his binding after mines. If I also defer my service provider, I get a "Provider already exists!" exception. + +So, I need to use the `extend()` method which is not really the correct one I think. The extend method will first resolve the previous binding and give it to my closure. In my service provider, I don't care of the previous instance of the command since I create a new one (a new child) from scratch. + +### Conclusion + +The project is missing PHPUnit tests even if I already try the commands by hand in a fresh Laravel project. But it's available now on [Packagist](https://packagist.org/packages/thibaud-dauce/laravel-recursive-migrations) and on [Git](https://framagit.org/ThibaudDauce/laravel-recursive-migrations). Please open an issue if you find anything wrong in it. + +I discover a lot about the migration system in Laravel during this little project, and I encourage you to dig in the source code of the framework: you'll learn a lot! diff --git a/content/posts/2017-02-13-pattern-matching-in-php.md b/content/posts/2017-02-13-pattern-matching-in-php.md new file mode 100644 index 0000000..97ce465 --- /dev/null +++ b/content/posts/2017-02-13-pattern-matching-in-php.md @@ -0,0 +1,161 @@ +--- +title: "Pattern Matching in PHP" +image: /images/pattern-matching.png +thumbnail: /images/thumbnail-pattern-matching.png +code: https://gitlab.com/thibauddauce/pattern-matching +description: How to build a pattern matching library in PHP? +lang: en +website: dev +--- + +I'm a big fan of Haskell and one of my favorite feature of this awesome language is pattern matching. If you don't know what's pattern matching, it's this: + +```hs +data Customer = Student | Individual + +getPrice :: Customer -> Int +getPrice Student = 10 +getPrice Individual = 30 +``` + +The main advantage is, if I add a possibility in my type, for example `data Customer = Student | Individual | Company`, the Haskell compiler will tell me that my function is missing a pattern. I will need to provide a `getPrice Company = ?`. + +Of course, in PHP we don't have a compiler but we can still have some checks… + + + +### The problem + +Imagine I have a class for my customer: + +```php +type === self::STUDENT) { + return 10; + } elseif ($this->type === self::INDIVIDUAL) { + return 30; + } else { + throw new InvalidArgumentException('Neither student nor individual.'); + } + } +} +``` + +By the way, pay attention of how 4 lines of code became 18 lines. But, of course, Haskell is a incomprehensible language ;-) + +If I have my unit tests like these: + +```php +$customer = new Customer; // in real life, I would use a name constructor… +$customer->type = Customer::STUDENT; + +$this->assertEquals(10, $customer->getPrice()); +``` + +```php +$customer = new Customer; +$customer->type = Customer::INDIVIDUAL; + +$this->assertEquals(30, $customer->getPrice()); +``` + +If latter, I add a new type `const COMPANY = 'company'` in my class, my tests will remain green. If I use this `if() {} elseif () {} else {}` in a lot of places, it will be difficult to catch all occurrences. + +### The solution + +Use [my small pattern matching library](https://gitlab.com/thibauddauce/pattern-matching) (or build your own, it's only a mater of hours…): + +```php +match($this->type, $actions); + } + + public function getPrice() + { + $this->patternMatchOnType([ + self::STUDENT => 10, + self::INDIVIDUAL => 30, + ]); + } +} +``` + +If I add a new type and I change my enumerate definition in one place `new Pattern([self::STUDENT, self::INDIVIDUAL, self::COMPANY])`, my previous tests are now failing with a new exception: + +```html +MissingPatternsDuringMatch: 'company' was missing during the match. +Expected patterns were 'student', 'individual', 'company' and received patterns were 'student', 'individual'. +``` + +Even if, I never wrote a test to check if the price for a company is correct. + +### Extra features of my library + +You can check [the test file](https://gitlab.com/thibauddauce/pattern-matching/blob/master/tests/PatternTest.php) for all features. + +#### Checks + +My `match()` function is three simple checks happening every time. It means, it raises an exception even if the pattern could be resolved. + +- there is no missing pattern in the array (all cases must have a response) +- there is no extra pattern in the array (if a type is removed, I want to remove all the occurrences of the dead code) +- the value provided is in the pattern list + +#### Callbacks + +If your application is doing expensive work for each pattern (more expensive than returning "10" or "30"), you can wrap the result in a callback: + +```php +$this->patternMatchOnType([ + self::STUDENT => function() { + return StudentPrice::fetchFromFile(); + }, + self::INDIVIDUAL => function() { + return Price::where('type', self::INDIVIDUAL)->first()->value; + }, +]); +``` + +#### Callbacks arguments + +And you can also give arguments to your callbacks with `with`: + +```php +public function patternMatchOnType(array $actions) +{ + return (new Pattern([self::STUDENT, self::INDIVIDUAL])) + ->with($this) // The callbacks will receive the instance of the customer + ->match($this->type, $actions); +} + +public function getPrice() +{ + $this->patternMatchOnType([ + self::STUDENT => 10, + self::INDIVIDUAL => function(Customer $customer) { + return IndividualPrice::where('age', '>', $customer->age)->first()->value; + }, + ]); +} +``` + +Even if, in this case, you could simply use `$this->age` inside the callback. diff --git a/content/posts/2017-07-18-your-app-doesnt-have-users.md b/content/posts/2017-07-18-your-app-doesnt-have-users.md new file mode 100644 index 0000000..f71d738 --- /dev/null +++ b/content/posts/2017-07-18-your-app-doesnt-have-users.md @@ -0,0 +1,56 @@ +--- +title: "Your app doesn't have \"users\"" +image: /images/your-app-doesnt-have-users.png +thumbnail: /images/thumbnail-your-app-doesnt-have-users.png +description: The most important part of your code is how you're naming things. Don't forget your first class! +lang: en +website: dev +--- + +I'm a big fan of Laravel. The thing I like the most about the community is the focus on understandable code. Not patterns, not architectures, but how to choose the best API for a task so the result is readable as plain English. + +But today, I want to talk about one name that Laravel's developers often get wrong. + + + +--- + +The default installation of Laravel comes with a `User` class and a migration to create a `users` table. Why? Because, obviously, every application has users. + +*That's not true.* + +Adam Wathan is a Laravel developer who provides an awesome course to learn how to build great applications: ["Test Driven Laravel"](https://adamwathan.me/test-driven-laravel/). + +![Test Driven Laravel course](/images/ticket-beast-promoters.png) + +TicketBeast, the Test Driven Laravel sample application, is about promoters and concerts, but in the application there is a `User` class and a `Concert` class but no `Promoter` class. + +He is also producing free lives, twice a week, on his [YouTube channel](https://www.youtube.com/channel/UCy1H38XrN7hi7wHSClfXPqQ/videos) where he builds [KiteTail](https://kitetail.co/). Adam put a lot of thought into the name of his users and eventually chose **"makers"** after a few videos. But once again, there is no `Maker` class in KiteTail. + +If you watch (and you should) the new series at Laracasts ["Let's Build A Forum with Laravel and TDD"](https://laracasts.com/series/lets-build-a-forum-with-laravel/), you will notice that Jeffrey also uses the `User` class. But this time, he really uses the term "user" when speaking and writing descriptions for his tests. + +On the other hand, in the second video, he used the term **"creator"** for "a thread has a creator" and he renamed his relation from `user()` to `creator()`. That's much more readable than `user()`, because what is the user of a thread anyway? If the default doesn't seem right for you, don't use it! + +When you don't use the default, Laravel will look for a "creator_id" column in the table instead of a "user_id" one. Jeffrey chose to override the default with `return $this->belongsTo(User::class, 'user_id');`, but I think it could have been better to embrace the new term and rename the table's column from "user_id" to "creator_id". If you need to speak about this table, you could then say "I store the ID of the creator with the body of the thread" instead of "I store the ID of the user with the body of the thread". The first one seems much more natural to me. + +--- + +Thread **creator**, concert **promoter**, product **maker**, it's only three examples among many. + +The most important thing to keep in mind is that you should name your classes as it feels natural to **you**. For example, I would use **"members"** to designate the users of a forum but Jeffrey seems to use **"users"** naturally. There is no right or wrong here (as often in development), you only need to choose the correct one so you can speak about your code without translating anything. + +So next time you run `laravel new my-project`, don't be lazy, take 5 minutes to think about your product and if you need to rename your `User` class into something else. It's always easier when it's done on a fresh project :-) + +I choose to speak about `User` because I think it's often forgotten but it's also true in most applications for "status", "data", "items", "result", "payload"… But don't forget that in a `Collection` class, "items" is perfectly natural, and in the Guzzle HTTP library, "payload" is fine too. + +If you want to learn more about it: + +- [https://murze.be/2017/06/the-status-antipattern/](https://murze.be/2017/06/the-status-antipattern/) +- [http://twentypercent.fm/naming-things](http://twentypercent.fm/naming-things) + +And if you want to learn how to improve your code: + +- [https://adamwathan.me/test-driven-laravel/](https://adamwathan.me/test-driven-laravel/) +- [Adam's YouTube channel](https://www.youtube.com/channel/UCy1H38XrN7hi7wHSClfXPqQ/videos) +- [Laracasts](https://laracasts.com/) +- For french speakers: [Formations Laravel](https://www.formations-laravel.fr/) diff --git a/content/posts/2017-07-26-improvements-of-the-new-Responsable-interface-in-Laravel.md b/content/posts/2017-07-26-improvements-of-the-new-Responsable-interface-in-Laravel.md new file mode 100644 index 0000000..0ee9267 --- /dev/null +++ b/content/posts/2017-07-26-improvements-of-the-new-Responsable-interface-in-Laravel.md @@ -0,0 +1,198 @@ +--- +title: "Improvements of the new Responsable interface in Laravel" +image: /images/responsable.png +thumbnail: /images/thumbnail-responsable.png +description: "Laravel 5.5 will ship with a new feature: the Responsable interface. Why not add some sugar to it?" +lang: en +website: dev +--- + +The new `Responsable` interface in Laravel is really awesome, it allows us to simplify our controllers with custom responses objects. + +But one of the complexity with responses is the content negotiation and [the new feature](https://github.com/laravel/framework/commit/c0c89fd73cebf9ed56e6c5e69ad35106df03d9db) doesn't help us on this. How could we solve this problem? + + + +--- + +### The new feature + +First, let's look at how Laravel uses the `Responsable` interface in the Router. + +```php +if ($response instanceof Responsable) { + $response = $response->toResponse(); +} + +if ($response instanceof PsrResponseInterface) { + $response = (new HttpFoundationFactory)->createResponse($response); +} elseif (! $response instanceof SymfonyResponse && + ($response instanceof Arrayable || + $response instanceof Jsonable || + $response instanceof ArrayObject || + $response instanceof JsonSerializable || + is_array($response))) { + $response = new JsonResponse($response); +} elseif (! $response instanceof SymfonyResponse) { + $response = new Response($response); +} +``` + +The only method on the `Respondable` interface is the `toResponse` method. Then, if the response is `Jsonable` or something similar, Laravel will use the `JsonResponse` which `json_encode` all the data. + +### Adding Content Negotiation + +There is no sign of Content-Negotiation in this method. So if I need to build a response in JSON or in HTML depending of the type the request, I will have some conditions in my controller: + +```php +public function index() +{ + $users = User::all(); + + if (request()->wantsJson()) { + return new UsersJsonResponse($users); + } + + return new UsersHtmlResponse($users); +} +``` + +Not too bad, thanks to the awesome `wantsJson()` method, but it could be better (especially if my controller is not a simple database query): + +```php +public function index() +{ + $users = User::all(); + + return UsersResponse($users); +} +``` + +Better! Now let's dive in the `UsersResponse`: + +```php +class UsersResponse implements Responsable +{ + public $users; + + public function __construct($users) + { + $this->users = $users; + } + + public function toResponse() + { + if (request()->wantsJson()) { + return $this->users; + } + + return view('users.index', ['users' => $this->users]); + } +} +``` + +### Avoiding conditionals with polymorphism + +It's often a good thing to replace conditionals by polymorphism. I would rather prefer this response with a new `NegociateContent` trait: + +```php +class UsersResponse implements Responsable +{ + use NegociateContent; + + public $users; + + public function __construct($users) + { + $this->users = $users; + } + + public function toJsonResponse() + { + return $this->users; + } + + public function toHtmlResponse() + { + return view('users.index', ['users' => $this->users]); + } +} +``` + +Or this one which extends a new `BaseResponse`: + +```php +class UsersResponse extends BaseResponse +{ + public $users; + + public function __construct($users) + { + $this->users = $users; + } + + public function toJsonResponse() + { + return $this->users; + } + + public function toHtmlResponse() + { + return view('users.index', ['users' => $this->users]); + } +} +``` + +The `NegociateContent` or the `BaseResponse` implementations are really straightforward. We can add more checks and content type if needed of course. + +```php +class BaseResponse implements Respondable +{ + public function toResponse() + { + if (request()->wantsJson()) { + return $this->toJsonResponse(); + } + + return $this->toHtmlResponse(); + } +} +``` + +### Let's try to build the same `view` helper as in mailables + +We can even simplify the `toHtmlResponse` method with the same concept as in `Mailable`. As you may know, in mailables, all public properties are directly accessible in the mail view. + +```php +public $users; + +public function toHtmlResponse() +{ + return $this->view('users.index'); +} +``` + +I stole this code from the [`Illuminate\Mail\Mailable`](https://github.com/laravel/framework/blob/master/src/Illuminate/Mail/Mailable.php#L223) class in the framework (I just refactored it to collections). + +```php +public function view($view, $overrides = []) +{ + return collect((new ReflectionClass($this))->getProperties(ReflectionProperty::IS_PUBLIC)) + ->reject(function($property) { + return $property->getDeclaringClass()->getName() == self::class; + }) + ->mapWithKeys(function($property) { + return [$property->getName(), $property->getValue($this)]; + }) + ->merge($overrides) + ->pipe(function($data) use($view) { + return view($view, $data); + }); +} +``` + +### Conclusion + +I think these few lines of code really improve the `Responsable` interface. I really like the `Mailable` reflection as it feels really natural to me. + +I may try to pull request the framework to add these features if people like them so don't hesitate to send me a message [on Twitter \@ThibaudDauce](https://twitter.com/ThibaudDauce) or [by mail (thibaud@dauce.fr)](mailto:thibaud@dauce.fr) to tell me what you think! diff --git a/content/posts/2019-01-24-what-to-learn-in-2019.md b/content/posts/2019-01-24-what-to-learn-in-2019.md new file mode 100644 index 0000000..9a50df0 --- /dev/null +++ b/content/posts/2019-01-24-what-to-learn-in-2019.md @@ -0,0 +1,56 @@ +--- +title: "What to learn in 2019?" +description: "In 2019, I want to challenge the way I build applications by exploring other interesting technologies." +lang: en +website: dev +--- + +Computer science is evolving at a rapid pace. There are new developers bringing new ideas. And there are many ancient discoveries forgotten or underused today. The fact is: there is a lot to learn in computer science in 2019. + +This year, I want to go out of my comfort zone and try new stuff. Don't get me wrong, I still love the PHP ecosystem, and Laravel is still an amazing framework to work with (I even [teach Laravel](https://www.formation-laravel.fr/) if you're interested). + +So, why switch? To begin with, I don't want to switch but only becoming productive in other areas of computed sciences. Secondly, PHP, as a language, is not that great and the recent features added are not really interesting. I have the feeling PHP is trying to copy Java or other mainstream languages without the correctness of the compilation. I don't consider my program crashing at runtime because of a type annotation is a real improvement over my program crashing at runtime because the method is called on the wrong object. + +### What are the goals? Productivity, correctness and performances. + +**Productivity** is the most meaningful one. I'm paid to deliver value to a customer and not to do research around programming languages. But productivity is also the easiest metric to improve, I'm sure you can be productive in any language if you practice enough. + +**Correctness** is about writing a program and have some guaranties that the result will work as expected. PHP is especially bad at this, but other languages can have different levels of verification with more or less strict type systems. + +And for **performances**, even though PHP 7 brought a significant speed boost, the language is still slow. The web is also a really slow platform, and I would like experimenting unfamiliar things like high performance CLI apps and native GUIs. + +### Who are the challengers? + +[Haskell](https://haskell-lang.org/) is a strictly typed, purely functional, "if it compiles, it works" kind of language. I'm afraid that the accent on absolute correctness and the bad documentation will slow me down on some manageable problems. +
+★★☆☆ Productivity | +★★★★ Correctness | +★★☆☆ Performances + +[Elixir](https://elixir-lang.org/) is a functional language based on the Erlang VM with the Ruby syntax. I think I can become highly productive with this kind of language. The correctness is a little bit behind because the language is dynamically typed but the compilation can still identify some issues before going into production. And last but not least, the performance are amazing: less than a millisecond to render a simple HTML webpage with [Phoenix](https://phoenixframework.org/). +
+★★★ Productivity | +★★☆☆ Correctness | +★★★ Performances + +[Rust](https://www.rust-lang.org/) is a low-level programming language with C-like performances. Memory management lower the productivity, but the documentation is really good. And the type system can detect the majority of silly errors at compile-time. +
+☆☆☆ Productivity | +★★★ Correctness | +★★★★ Performances + +[PHP](https://secure.php.net/), because this is the tool I provide the most value with. The ecosystem is amazing, but I will not discover anything new soon. And I don't like that. +
+★★★★ Productivity | +☆☆☆ Correctness | +☆☆☆ Performances + +### So? What will I learn in 2019? + +My first choice is Rust. It's the language where I have the most to learn. + +For a few weeks I tried building a text editor with Rust, and it was very hard. How to render a character on the screen? How to create a blinking cursor? How to add words in the middle of a large string? I think this project is overly complex for me now but it was a good first try. + +I also built a small utility (a few lines of code) to print statistics about my websites. I don't use standard analytics, so I just grabbed the last few days of Nginx's logs and printed the visits for each day and each URI. + +Thanks for reading, it's been a while (more than one year) between my previous post and this one, and my written english is a little bit Rusty, please forgive me :-) You can subscribe to my RSS feed to receive my future posts if you want. \ No newline at end of file diff --git a/content/posts/2019-07-01-I-dont-like-camel-case.md b/content/posts/2019-07-01-I-dont-like-camel-case.md new file mode 100644 index 0000000..c049ba4 --- /dev/null +++ b/content/posts/2019-07-01-I-dont-like-camel-case.md @@ -0,0 +1,22 @@ +--- +title: "I don't like camelCase" +description: "Snake case, camel case… Why different languages made different choices?" +lang: en +website: dev +--- + +Which one is the most readable? + +```php +function getFirstUserFrom(SiretNumber $siretNumber): User {} +``` + +```php +function get_first_user_from(Siret_Number $siret_number): User {} +``` + +I think the snake case version is much more readable than the camel case. + +How we name our functions is not a technical choice, it's a convention. Nobody can say that the snake case version is worse than the camel case one. The people from the PSR committee decided [a long time ago](https://www.php-fig.org/psr/psr-1/) that, in PHP, methods will be named in camel case and classes will be named in studly case. But in Rust, or in Python, a different choice was made: it's snake case for functions and methods. + +Don't let other people decide what works best for you. Recently, I started using snake case for all my functions, methods and variables. And I think I'll switch to the `Siret_Number` approach for all my classes. \ No newline at end of file diff --git a/content/posts/2020-10-07-taylor-otwell-a-propos-de-jetstream-et-ma-chaine-youtube.md b/content/posts/2020-10-07-taylor-otwell-a-propos-de-jetstream-et-ma-chaine-youtube.md new file mode 100644 index 0000000..f702bc5 --- /dev/null +++ b/content/posts/2020-10-07-taylor-otwell-a-propos-de-jetstream-et-ma-chaine-youtube.md @@ -0,0 +1,25 @@ +--- +title: "Taylor Otwell à propos de Jetstream. Le futur de ma chaîne YouTube." +description: "Taylor explique dans une récente vidéo la différence entre open-source et produits commerciaux ce qui me fait penser à mes prochains projets sur YouTube" +website: dev +--- + +Dans [une récente vidéo](https://www.youtube.com/watch?v=krn39HjxPTs), Taylor Otwell revient sur les critiques apportées à [Laravel Jetstream](https://jetstream.laravel.com/1.x/introduction.html). Il y explique que lorsqu'il développe des applications open-source, il le fait pour lui. C'est pour cela que Jetsream ne propose « que » deux options : Livewire ou Inertia. Taylor n'utilisant ni Bootstrap, ni Vue Router, ni Blade seul… c'est normal pour lui ne pas développer ces options. Mais il laisse libre la communauté de créer ses propres bibliothèques open-source si des personnes sont intéressées (d'après ce que j'en ai compris, [Laravel Fortify](https://github.com/laravel/fortify) est l'interface sans aucun visuel permettant de créer ses propres options). + +Il explique également que, contrairement à ses travaux open-source où il fait ce qu'il veut (et il a bien raison), lorsqu'il développe des produits commerciaux il se plie aux demandes de ses clients. Je comprends parfaitement cette réflexion pour en avoir un aperçu avec [la chaîne YouTube](https://www.youtube.com/user/tdauce/) lorsque des personnes me demandent pourquoi je ne fais pas une nouvelle série sur Laravel, pourquoi je n'ai pas terminé la série sur Nova, pourquoi je ne fais pas plus de VueJS, etc. Lorsque je fais des vidéos gratuitement sur Internet, je me permets de ne parler que des sujets qui m'intéressent, sans aucune autre obligation. Cela ne m'empêche pas de continuer à faire des formations et de l'accompagnement sur Laravel [pour mes clients](https://www.formation-laravel.fr/). + +Pour ceux qui l'avaient remarqué, ce blog était en anglais mais j'ai récemment décidé de reprendre l'écriture car ma situation actuelle m'empêche de faire des vidéos (le format écrit est plus simple à produire). Or, écrire en anglais me demande plus d'efforts, et ma communauté étant principale francophone, j'ai décidé de le reprendre en français. + +Pour ceux intéressés par la suite, voici les sujets dont j'aimerais parler dans le futur sur ce blog, mais aussi en vidéo quand ma situation sera plus calme : +- Des astuces que j'utilise sur mes applications Laravel +- La transformation de [ce site statique](https://github.com/ThibaudDauce/thibaud.dauce.fr) d'Hakyll vers Rust +- Un retour d'expérience sur le développement d'une SPA avec [Elm](https://elm-lang.org/) +- Un retour d'expérience sur le développement d'une application Android avec [Flutter](https://flutter.dev/) +- Intégration d'[AlpineJS](https://github.com/alpinejs/alpine/) dans mon projet [Têtes de Coton](https://www.youtube.com/playlist?list=PLMWEEzYqZ0em0jJa4LmQPjDqJNaApqKan) +- Développement d'un blog avec [Rust](https://www.rust-lang.org/) et [Rocket](https://rocket.rs/) (avec plus de choses que [ma précédente expérimentation](https://www.youtube.com/playlist?list=PLMWEEzYqZ0ekOG6_G4q_GXPpVHWrIH--x) : connexion admin, WYSIWYG, envoi de fichiers…) +- Développement d'un composant [DataTables](https://datatables.net/) en VueJS v3 +- Expérimentation d'une application native multiplateformec en C++ avec [WxWidgets](https://www.wxwidgets.org/) + +Et sans doute d'autres choses autour de la programmation mais pas que ! Si vous voulez commenter, discuter, n'hésitez pas à rejoindre [mon Discord](https://discordapp.com/invite/tPtVM9V). Si vous souhaitez être notifié des prochains articles de ce blog, le plus sûr est de s'abonner à [mon flux RSS](https://thibaud.dauce.fr/feed.xml), vous pouvez aussi me suivre [sur Twitter](https://twitter.com/ThibaudDauce/) même si j'y suis très peu actif ces derniers temps. + +PS : je reviendrai dans un futur article sur mon avis à propos Jetsream et autres… \ No newline at end of file diff --git a/content/posts/2020-10-13-projet-windows.md b/content/posts/2020-10-13-projet-windows.md new file mode 100644 index 0000000..5c9346f --- /dev/null +++ b/content/posts/2020-10-13-projet-windows.md @@ -0,0 +1,48 @@ +--- +title: "Projet Windows" +description: "Un développeur Linux depuis plus de 10 ans peut-il passer à Windows ?" +website: dev +--- + +J'ai reçu hier un Dell XPS 17 2020. Après avoir consulté quelques forums, je me suis rendu compte que [les pilotes Linux ne semblent pas tout à fait à jour](https://wiki.archlinux.org/index.php/Dell_XPS_17) pour cette machine et je risque fort d'avoir, au minimum, des problèmes de son. C'est l'occasion rêvée de faire une petite expérimentation : que donne Windows aujourd'hui pour développer ? + +Je n'ai personnellement jamais utilisé Windows pour développer, j'ai commencé avec un CD d'Ubuntu (envoyé gratuitement par la poste à l'époque !) il y une dizaine d'années, puis, il y a maintenant plus de 6 ans, je suis passé à ArchLinux pour bénéficier des dernières versions logicielles. + +## Objectifs lors de la réception + +Mon objectif est donc de tester Windows pendant quelques semaines/mois le temps que le noyau Linux se mette à jour et ensuite d'évaluer l'expérience. Je compte y aller à fond : Windows, mais aussi les applications Mail et Calendar intégrées, Edge en tant que navigateur principal, le nouveau Windows Terminal… + +Pour référence, voici les principaux points qui peuvent potentiellement m'embêter par ordre de priorité : + +1. Je ne sais pas du tout comment fonctionne les outils de développement du nouveau Edge (est-ce qu'il est compatible avec les extensions Chrome ? Est-ce que toutes les infos sont présentes dans l'explorateur ?) +1. Les bureaux multiples semblent simplistes sous Windows : j'utilise actuellement 4 bureaux virtuels en carré avec des raccourcis pour se déplacer dans l'espace (vers le haut, gauche, droite, bas…) et des catégories de programmes automatiquement ouvertes sur chaque bureau. +1. Des problèmes insolvables : sous Linux j'ai toujours la possibilité d'aller assez loin dans le fonctionnement du système en ligne de commande pour trouver une solution. +1. Un environnement de bureau plus lourd : difficile de faire moins lourd que mon [bspwm](https://wiki.archlinux.org/index.php/Bspwm) actuel. + +Mais je pense aussi que Windows va m'apporter quelques avantages : + +1. Une meilleure autonomie : en particulier via une meilleure gestion de la puce GPU intégrée d'Intel et de la carte graphique Nvidia dédiée. +1. Une meilleure gestion des périphériques USB (télépone Android, liseuse, imprimante…). +1. Moins de temps passé à configurer des choses, réparer des problèmes, installer des mises à jour et plus de temps à développer. +1. La possibilité d'utiliser Windows Hello et le capteur d'empreintes pour déverrouiller l'ordinateur. +1. La possibilité d'installer des jeux vidéos +1. Des meilleurs débuggeurs si je me lance vraiment dans le développement Rust/C++ + +## Première mise à jour suite à une journée de développement + +Après avoir installé mon ordinateur et développé une journée avec, voici mes premiers retours. + +Au niveau des problèmes rencontrés : +- Certains texte de Windows lors des redémarrages pour les mises à jour sont en anglais, incompréhensible… La date et l'heure étaient également en format anglais alors que ma configuration était en français. +- La configuration de mon clavier Bépo n'était pas la même que sous Linux, mais [j'ai pu corriger ce problème via quelques recherches](/posts/2020-10-14-bépo-sur-windows.html) +- Redis n'existe pas en version Windows (du moins pas depuis la version 3) +- Je n'ai pas trouvé d'alternative à `rsync` pour le moment +- L'application Mail n'arrive pas à se connecter à ProtonMail Bridge et ne propose visiblement pas d'identités multiples pour pouvoir envoyer des mails avec différentes adresses via le même compte SMTP. J'ai installé Thunderbird. +- Je n'ai pas encore trouvé d'alternative à `inotifywait` pour compiler mon code dès qu'il y a une modification (je peux utiliser `cargo watch` dans l'écosystème Rust, cela doit exister dans l'écosystème d'Elm mais je n'ai pas encore cherché). +- Beaucoup de logiciels inutiles pré-installés, dont certains néfastes. J'ai passé un certain temps à rechercher pourquoi mon micro enregistrait avec un écho avant de réaliser que le logiciel Maxx Audio Pro essayait de modifier le son en sortie de mon casque pour lui donner un effet 3D (personne n'a jamais dû tester ce logiciel avant de l'installer par défaut sur tous les ordinateurs Dell vu que ça ne fonctionne pas du tout…). + +Mais aussi des bonnes surprises : +- J'adore Windows Hello, c'est très agréable à utiliser et très efficace pour déverrouiller son ordinateur. +- Toutes mes applications à l'exception de Redis/`rsync` tournent sans problème sur Windows. Pour référence j'ai installé : PHP, Composer, Node/NPM, MariaDB, PostgreSQL, FFMPEG, youtube-dl, Git, Github Desktop, VS Code, OpenVPN, Flutter/Dart, Rust/Rustup/Cargo et Elm. Redis fonctionne sans problème via WSL. +- Les extensions PHP sont très faciles à installer, pas besoin de compiler l'extension SSH2 par exemple, il suffit de télécharger le DLL depuis [PECL](https://windows.php.net/downloads/pecl/releases/) et de le déplacer dans mon dossier PHP. +- Les extensions Google Chrome sont compatibles avec Microsoft Edge donc aucun problème à ce niveau là. Je n'ai pas encore eu à faire de l'inspecteur d'élément pour le moment. \ No newline at end of file diff --git a/content/posts/2020-10-14-bépo-sur-windows.md b/content/posts/2020-10-14-bépo-sur-windows.md new file mode 100644 index 0000000..7eafb86 --- /dev/null +++ b/content/posts/2020-10-14-bépo-sur-windows.md @@ -0,0 +1,36 @@ +--- +title: "Bépo sur Windows" +description: "Comment installer la disposition clavier spéciale français sur Windows ?" +website: dev +--- + +J'utilise depuis plusieurs années [Bépo](https://bepo.fr/wiki/Accueil) sur mes ordinateurs à la place du traditionnel Azerty. Bépo est une disposition spécialement conçue pour le français. Son principal avantage est que les touches sont placées de manière à faciliter l'écriture du français (lettres les plus communes sur la ligne de repos, alternance entre les lettres main gauche/main droite, lettres rares sur les doigts les plus faibles…). Les positions des touches sur un clavier Azerty ont été choisies afin d'éviter les contraintes mécaniques des machines à écrire, et ne sont donc pas du tout adaptées à l'écriture sur ordinateur. Bépo permet également d'utiliser facilement des caractères typographiques comme les espaces insécables fines, les guillemets français, les majuscules accentuées, etc. + +Je n'ai jamais eu aucun souci avec ma disposition sur Linux, mais après l'installation sur Windows je me suis rendu compte de deux problèmes : +- Sur Linux je tapais deux fois sur la touche accent grave afin d'écrire un « \` » (très utile pour le Markdown). Sur Windows cette technique affiche un caractère étrange « ̏` ». La seule technique que j'ai trouvée était de taper un accent grave puis une espace : beaucoup moins pratique en plus d'être une habitude difficile à changer. +- Deuxième problème, [le pilote Bépo Windows officiel](https://bepo.fr/wiki/Windows) remplace la touche « apostrophe » par des apostrophes typographiques « ’ ». C'est typographiquement le bon caractère pour de la rédaction en français mais inutilisable pour un développeur qui a besoin de l'apostrophe classique dans son code. + +J'ai donc décidé de modifier la disposition clavier classique via le logiciel [Microsoft Keyboard Layout Creator](https://www.microsoft.com/en-us/download/details.aspx?id=102134). Ce logiciel fonctionne très bien : j'ai pu importer la disposition officielle et résoudre mes deux problèmes. +- Retrait de l'option « Touche morte » sur la touche de l'accent grave. L'option « Touche morte », d'après ce que j'en ai compris, permet, en tapant une fois sur l'accent grave puis sur une lettre classique, de mettre cet accent sur la lettre. Je n'utilise jamais cette fonctionnalité car la disposition Bépo fournit déjà des touches pour toutes les lettres avec des accents graves. En retirant cette option, je n'ai maintenant qu'à appuyer une fois sur la touche pour voir s'afficher un accent grave seul. Ce changement va me demander un petit temps d'adaptation mais il sera à terme bien plus pratique que sur Linux. +- Remplacer l'apostrophe typographique par une apostrophe normale. + +Mais lors de l'export, le logiciel refuse de créer la disposition clavier. +``` +ERROR: 'VK_SPACE' in Shift State 'Ctl+Alt' must be made up of white space character(s), but is defined as '_' (U+005f) instead. +``` + +En Bépo, le « _ » est lié à la touche `Shift+Espace` et le logiciel est configuré pour lancer une erreur si un caractère autre qu'une espace est défini sur la touche espace. + +Je me suis donc demandé comment la version officielle de la disposition avait pu être compilée avec cette « erreur » et je suis tombé sur ce forum : « [Utiliser Microsoft Keyboard Layout Creator avec le fichier bepo.klc](https://forum.bepo.fr/viewtopic.php?id=1306) » expliquant comment modifier l'exécutable de Microsoft Keyboard Layout Creator pour supprimer l'erreur. + +Je recopie ici les explications au cas où le sujet du forum disparaîtrait : +- Faire une copie de l'exécutable par sécurité +- Ouvrir l'exécutable avec un éditeur hexadécimal (j'ai téléchargé [HxD](https://mh-nexus.de/en/hxd/)) +- Rechercher `2D 05 04 2D 02 17 2A 17 0A` (en faisant bien attention d'être en mode recherche hexadécimale) +- Remplacer `0A` par `2A` + +Je trouve ça incroyable qu'une modification aussi simple dans l'exécutable permette de corriger ce problème sans poser de plantage autre part. Je remercie les personnes qui ont décompilé le programme et trouvé cette solution à mon problème ! + +Il me reste encore un problème avec la touche accent circonflexe « ^ » qui, comme pour l'accent grave, m'affiche « ôô » lors d'une répétition, alors que sous Linux cela me permettait de créer le smiley « ^^ ». Mais vu que Windows possède un clavier de smiley accessible via `Windows+.` peut-être que je n'aurais pas trop à utiliser cette touche 😊. Je ne peux pas faire la même modification que pour l'accent circonflexe (retirer l'option « Touche morte ») car je l'utilise pour écrire « ô », « î » ou « û » (Bépo n'a pas de touche dédiée à ces lettres). + +Si cela vous intéresse, j'écrirai prochainement un article sur mon changement d'Azerty à Bépo, les techniques que j'ai utilisées, le temps que cela m'a pris mais ce que je peux déjà vous dire c'est qu'en 6 ans je n'ai jamais regretté le changement ! \ No newline at end of file diff --git a/content/posts/2020-10-27-j-ai-change-d-avis-sur-Firefox.md b/content/posts/2020-10-27-j-ai-change-d-avis-sur-Firefox.md new file mode 100644 index 0000000..c4c3fe2 --- /dev/null +++ b/content/posts/2020-10-27-j-ai-change-d-avis-sur-Firefox.md @@ -0,0 +1,15 @@ +--- +title: "J'ai changé d'avis sur Firefox" +description: "Pendant longtemps j'ai promu Firefox et débattu pour mettre fin au monopole de Chrome. J'avais tort." +website: dev +--- + +Pendant longtemps j'ai débattu pour mettre fin au monopole de Chrome et encourager les personnes autour de moi à utiliser un navigateur autre tel que Firefox : « Le web doit rester ouvert, si tout le monde utilise Chrome, les développeurs de Google auront la possibilité de contrôler les spécifications ». Cette affirmation reste correcte aujourd'hui mais je ne pense pas que ça soit une mauvaise chose. + +Beaucoup de personnes pensent que le web est le seul écosystème à permettre un développement multi-plateforme ce qui est complètement faux. Le contre-exemple le plus connu est Java, mais tous les langages basés sur une machine virtuelle qui elle-même est développée sur plusieurs plateformes le permette. Lorsque j'ai commencé à considérer le web comme une machine virtuelle, mon avis a changé. + +L'avantage d'une machine virtuelle est d'avoir des spécifications précises pour permettre justement un développement unique, aujourd'hui le web est sous-optimal car les spécifications sont mal respectées par les implémentations et ces dernières doivent être mises à jour individuellement. C'est comme si les développeurs Java devaient attendre que 4 JVM différentes implémentent la nouvelle spécification pour l'utiliser, ou encore qu'une méthode d'affichage GUI donne des résultats un peu différents en fonction de la JVM du client (oui je pense à toi le CSS…). + +En observant le web sous cet angle, il serait largement préférable d'avoir un seul moteur de rendu HTML/CSS et un seul interpréteur JavaScript utilisé par tous. Il serait préférable que ce moteur soit un moteur totalement open-source contrôlé par une fondation plutôt que par une entreprise mais ce n'est pas le cas aujourd'hui. Je pense que le web a déjà assez de problèmes (je pourrais en parler plus tard sur le blog…) pour rajouter en plus de ça une contrainte inutile supplémentaire. + +Cette réflexion est toute récente, j'ai peut-être manqué certains avantages de cette approche, n'hésitez pas à passer sur [mon Discord](https://discordapp.com/invite/tPtVM9V) si vous voulez discuter. Aussi, je ne connais pas d'autre machine virtuelle basée sur le même modèle que le web (avec plusieurs implémentations principales), si vous en connaissez, ça m'intéresse ! diff --git a/content/posts/3000-01-01-je-suis-accro.md b/content/posts/3000-01-01-je-suis-accro.md new file mode 100644 index 0000000..8255247 --- /dev/null +++ b/content/posts/3000-01-01-je-suis-accro.md @@ -0,0 +1,5 @@ +--- +title: "Je suis accro" +description: "La plupart des services gratuits que nous utilisons au quotidien sont développés pour nous rendre addicts, comment y rémédier ?" +website: dev +--- diff --git a/content/posts/3000-01-01-laravel-authentification.md b/content/posts/3000-01-01-laravel-authentification.md new file mode 100644 index 0000000..f2f961e --- /dev/null +++ b/content/posts/3000-01-01-laravel-authentification.md @@ -0,0 +1,15 @@ +--- +title: "Laravel autentification : Laravel UI, Jetsream, Fortify, Sanctum, Passport…" +description: "Pourquoi je n'utilise pas le système ou les systèmes d'authentification proposés par Laravel ?" +website: dev +--- + +Ces dernières années, les différents systèmes d'autentification ont émergé dans l'écosystème Laravel : que des bibliothèques officiellement supportées par le Framework ! + +Tout a commencé avec Laravel UI qui permettait via `php artisan make:auth` de créer les vues et les contrôleurs pour l'inscription, la connexion, la réinitialisation du mot de passe, etc. Je n'ai jamais utilisé ce système car il était conçu pour Bootstrap, lié à une classe `User` (et je vous rappelle que [votre application n'a pas d'utilisateurs](/posts/2017-07-18-your-app-doesnt-have-users.html)), etc. Vu que le code généré ne contenait que quelques classes et quelques bouts de HTML, il était aussi plus rapide pour moi de réécrire cette partie là afin de la maîtriser de A à Z. Je travaille rarement sur des projets courts, mon principal projet Laravel dure depuis des années et les quelques heures de gagnées en utilisant `php artisan make:auth` auraient été perdues 10 fois durant ces années de maintenance à rechercher comment personnaliser tel ou tel élément. + +Ensuite, nous avons eu Passport. Passport implémente le protocole OAuth 2 qui est un protocole extrêmement complexe, inutile dans la plupart des cas. Le protocol OAuth 2 est utile dans un ménage à trois : un visiteur qui veut s'authentifier (moi par exemple), un site qui ne veut pas gérer l'authentification (votre application Laravel) et un site connu où le visiteur a déjà un compte (Facebook). Attention, contrairement à ce que l'on pourrait croire, Passport ne permet pas de gérer la partie connexion avec Facebook (c'est le rôle d'une autre bibliothèque officielle : Socialite), Passport permet de créer le système d'authentification de Facebook. Ce qui, vous y conviendrez, n'est pas très utile. L'OAuth 2 fonctionne bien lorsque vous êtes un site connu mais pour des petites applications, il n'y a pas beaucoup d'intérêt. Je pense que la plupart des développeurs qui utilisent Passport l'utilise de manière détournée en étant le client et le serveur, dans ce cas là l'OAuth 2 ajoute une complexité énorme pour un gain nul. + +Avec Laravel 7, nous avons eu Sanctum qui est une version bien plus intéressante que Passport pour gérer ses API (application mobile, SPA…) mais encore trop complexe. Sanctum fonctionne avec des clés API à générer et à stocker en base de données avant de pouvoir les utiliser. Le principal problème c'est que ce système oblige l'utilisateur a générer ses clés sur son compte afin de les utiliser, quelque chose qui n'est pas adapté au commun des mortels. Gitub possède ce système mais c'est un site à destination des développeurs qui connaissent l'utilité d'une clé API et qui peuvent sans problème en créer une pour la renseigner à Composer par exemple. + +Avec Laravel 8 nous avons deux nouvelle bibliothèque : Fortify et Jetstream. \ No newline at end of file diff --git a/content/posts/3000-01-01-plusieurs-bureaux-virtuels-pour-travailler.md b/content/posts/3000-01-01-plusieurs-bureaux-virtuels-pour-travailler.md new file mode 100644 index 0000000..7272ecf --- /dev/null +++ b/content/posts/3000-01-01-plusieurs-bureaux-virtuels-pour-travailler.md @@ -0,0 +1,26 @@ +--- +title: "Plusieurs bureaux virtuels pour travailler ?" +description: "J'ai longtemps travaillé avec plusieurs bureaux virtuels, comment travailler avec un seul espace de travail ?" +website: dev +--- + +Je ne travaille jamais avec plusieurs écrans, j'utilisais donc la fonctionnalité des bureaux multiples. J'en utilisais 4, disposés en carré : + +``` +Firefox Thunderbird +Terminal Éditeur +``` + +L'avantage de ces 4 espaces de travail, c'est que je pouvais me rendre en deux touches de clavier, de façon déterministe vers n'importe quelle application. Mes raccourcis étaient configurés pour émuler le déplacement dans Vim, donc réduire les mouvements des mains sur le clavier : « Alt + h » pour aller à gauche, « Alt + j » pour aller en bas, « Alt + k » pour aller en haut et « Alt + l » pour aller à droite. Où que je sois, je savais que le raccourcis « Alt + h, Alt + k » m'emmenaient à Firefox, « Alt + h, Alt + j » au terminal, etc. + +Je pouvais également avoir une deuxième application sur chaque bureau en fonction des besoins (régulièrement Discord avec Firefox, et Github Desktop avec mon éditeur). Dans ce cas, j'utilisais « Alt + Tab » sur le bureau en question pour alterner les deux fenêtres. + +J'ai trouvé cette organisation très efficace pendant plusieurs années (avec [KDE](https://kde.org/), puis [bspwm](https://github.com/baskerville/bspwm)) jusqu'à récemment où j'ai dû passer à Windows à la place de mon ArchLinux. C'était d'ailleurs dans [mes problèmes possibles de mon premier article sur Windows](/posts/2020-10-13-projet-windows.html). + +Effectivement, comme je le pensais, Windows possède des bureaux virtuels limités en ligne horizontale, et même si j'aurais pu trouver une application tierce ou un menu dans les paramètres pour configurer tout ça, j'ai préféré expérimenter avec un seul bureau. + +Avec un seul bureau, j'utilise principalement « Alt + Tab » pour changer d'application. Le problème de cette méthode c'est qu'elle n'est pas déterministe. Lorsque vous avez deux applications ouvertes, vous pouvez être sûr de la prochaine application après un « Alt + Tab ». Avec 3 fenêtres, il faut retenir l'ordre dans lequel vous avez visité les applications la dernière fois pour savoir où un (ou deux) « Alt + Tab » vont vous mener. À partir de 4 applications ça me semble presque impossible de retenir où un, deux et trois « Alt + Tab » vont rediriger. + +Au quotidien, je trouve cette organisation peu efficace car elle me limite à 3 applications ouvertes mais c'est également un point très positif, si je dois choisir trois applications ça sera : Microsoft Edge, Windows Terminal et Visual Studio Code. Sous Windows, je n'ai donc pas la place d'ouvrir Discord ou Thunderbird et je me rends compte à la fin de la journée que je regarde beaucoup moins mes mails ou mes notifications en travaillant. + +Pour conclure, je suis assez satisfait de cette organisation avec 3 applications ouvertes, sous réserve qu'après plusieurs semaines de travail je devienne un peu plus conscient de mes déplacements via « Alt + Tab ». Aujourd'hui encore, il m'arrive de perdre un peu de temps lorsque je souhaite me rendre sur une application spécifique et que je ne sais plus trop où je me trouve de mon historique d'« Alt + Tab ». \ No newline at end of file diff --git a/content/talks/2015-02-20-bases-de-donnees-aujourdhui-et-bases-de-donnees-no-sql.md b/content/talks/2015-02-20-bases-de-donnees-aujourdhui-et-bases-de-donnees-no-sql.md new file mode 100644 index 0000000..10fd820 --- /dev/null +++ b/content/talks/2015-02-20-bases-de-donnees-aujourdhui-et-bases-de-donnees-no-sql.md @@ -0,0 +1,10 @@ +--- +title: Bases de données aujourd'hui et bases de données NoSQL +thumbnail: /images/insa/thumbnail-nosql.png +slides: /talks/insa-nosql/slides.pdf +video: https://www.youtube.com/watch?v=oIpjcqHyx2M +--- + +Cours d'1h30 devant les étudiants [ASI](http://asi.insa-rouen.fr/) de 3e année à l'[INSA de Rouen](http://www.insa-rouen.fr/) le 20 février 2015. Nous avons redonné la même présentation mise à jour le 4 mars 2016. + +La formation en bases de données de l'INSA est orientée bases de données relationnelles. Avec [Antoine Augusti](https://www.antoine-augusti.fr/), nous avons choisi un Projet Approfondissement et d'Ouverture (option encadrée par un professeur à l'INSA) sur le NoSQL. Nous avons travaillé un an sur un état de l'art des bases de données NoSQL et un prototype d'application. Pour clôturer notre projet, nous avons donné une introduction au NoSQL aux étudiants de 3e année lors de leur dernier cours de bases de données. L'intégralité de notre projet est disponible [sur Github](https://github.com/AntoineAugusti/NoSQL-etatart). diff --git a/content/talks/2015-11-26-injection-de-dependance-ou-comment-decoupler-ses-objets.md b/content/talks/2015-11-26-injection-de-dependance-ou-comment-decoupler-ses-objets.md new file mode 100644 index 0000000..0fa1eeb --- /dev/null +++ b/content/talks/2015-11-26-injection-de-dependance-ou-comment-decoupler-ses-objets.md @@ -0,0 +1,9 @@ +--- +title: L'injection de dépendance ou comment découpler ses objets +thumbnail: /images/codeurs-en-seine/thumbnail-injection-dependance.png +slides: /talks/codeurs-en-seine-injection-dependance/slides.pdf +--- + +Quickie de 15 minutes le 26 novembre 2015 à la fac de Rouen lors de [Codeurs en Seine](http://www.codeursenseine.com/). + +Ce petit quickie parlera de l'injection de dépendance : un mécanisme permettant de construire des objets totalement découplés les uns des autres mais facilement utilisable entre eux afin de créer des applications plus pérennes. Vos objets seront plus simples, les tester deviendra un vrai plaisir et la maintenance sera beaucoup plus facile. diff --git a/content/talks/2016-02-23-event-store.md b/content/talks/2016-02-23-event-store.md new file mode 100644 index 0000000..e474094 --- /dev/null +++ b/content/talks/2016-02-23-event-store.md @@ -0,0 +1,11 @@ +--- +title: Event Store +iframe: /talks/jug-event-store/index.html +slides: /talks/jug-event-store/index.html +--- + +Quickie de 15 minutes le 23 février 2016 à Seine Innopolis lors du [Normandy Java User Group](http://www.normandyjug.org/). + +Le CQRS tout le monde en parle, mais comment le mettre en place. L’event sourcing est une des solutions, et Event Store est une des bases de données NoSQL la plus intéressante dans ce domaine, voyons ce que l’on peut faire avec. + +Les slides sont également disponibles [sur Github](https://github.com/ThibaudDauce/jug-event-store). diff --git a/content/talks/2016-02-23-qu-est-ce-qu-internet.md b/content/talks/2016-02-23-qu-est-ce-qu-internet.md new file mode 100644 index 0000000..0aad44a --- /dev/null +++ b/content/talks/2016-02-23-qu-est-ce-qu-internet.md @@ -0,0 +1,11 @@ +--- +title: Qu'est-ce qu'Internet ? +iframe: /talks/jug-internet/index.html +slides: /talks/jug-internet/index.html +--- + +Quickie de 15 minutes le 23 février 2016 à Seine Innopolis lors du [Normandy Java User Group](http://www.normandyjug.org/). + +Vous l’utilisez tous les jours, toutes les heures, voir même chaque minute. Et pourtant, savez-vous vraiment ce qu’est Internet ? Comment fonctionne Internet ? + +Les slides sont également disponibles [sur Github](https://github.com/ThibaudDauce/jug-internet). diff --git a/content/talks/2017-01-31-l-event-sourcing-en-pratique-ca-donne-quoi.md b/content/talks/2017-01-31-l-event-sourcing-en-pratique-ca-donne-quoi.md new file mode 100644 index 0000000..09c8c2b --- /dev/null +++ b/content/talks/2017-01-31-l-event-sourcing-en-pratique-ca-donne-quoi.md @@ -0,0 +1,10 @@ +--- +title: L'Event Sourcing en pratique, ça donne quoi ? +iframe: /talks/orleans-tech-event-sourcing/index.html +slides: /talks/orleans-tech-event-sourcing/index.html +tweet: https://twitter.com/orleans_tech/status/826505597159890945 +--- + +Conférence d'une trentaine de minutes lors du meetup [Orléans Tech](https://orleans-tech.com/) le 31 janvier 2017. + +L'Event Sourcing en théorie c'est génial, on en parle partout, mais en pratique, l'important c'est de savoir quand l'utiliser et comment le mettre en place correctement. diff --git a/content/talks/2017-06-08-decouverte-de-laravel.md b/content/talks/2017-06-08-decouverte-de-laravel.md new file mode 100644 index 0000000..6c4b298 --- /dev/null +++ b/content/talks/2017-06-08-decouverte-de-laravel.md @@ -0,0 +1,10 @@ +--- +title: Découverte de Laravel +iframe: /talks/wild-code-school-decouverte-laravel/index.html +slides: /talks/wild-code-school-decouverte-laravel/index.html +tweet: https://twitter.com/WildSchool_Orls/status/872867406087106560 +--- + +Découverte de Laravel pendant une après-midi avec les élèves de la [Wild Code School](https://wildcodeschool.fr/) d'Orléans le 8 juin 2017. + +Présentation pendant une heure de quelques différences de développement et d'architecture entre Symfony (que les élèves étudient à temps plein) et Laravel. Puis accompagnement des élèves sur un exercice pratique de découverte de Laravel le reste de l'après midi. diff --git a/content/talks/2017-08-29-orleans-tech-programmation-fonctionnelle.md b/content/talks/2017-08-29-orleans-tech-programmation-fonctionnelle.md new file mode 100644 index 0000000..0727842 --- /dev/null +++ b/content/talks/2017-08-29-orleans-tech-programmation-fonctionnelle.md @@ -0,0 +1,10 @@ +--- +title: La programmation fonctionnelle pour les développeurs web +iframe: /talks/orleans-tech-programmation-fonctionnelle/index.html +slides: /talks/orleans-tech-programmation-fonctionnelle/index.html +tweet: https://twitter.com/orleans_tech/status/897084855112126465 +--- + +Conférence d'une trentaine de minutes lors du meetup [Orléans Tech](https://orleans-tech.com/) le 29 août 2017. + +Nous pensons tous faire de la programmation fonctionnelle lorsque nous écrivons nos fonctions PHP mais la programmation fonctionnelle c'est bien plus que ça ! Dans ce talk je montre comment en mettre en place de manière très simple et naturelle dans notre code de tous les jours. diff --git a/content/talks/2019-04-18-afup-tours-programmation-fonctionnelle.md b/content/talks/2019-04-18-afup-tours-programmation-fonctionnelle.md new file mode 100644 index 0000000..c9f4c6f --- /dev/null +++ b/content/talks/2019-04-18-afup-tours-programmation-fonctionnelle.md @@ -0,0 +1,10 @@ +--- +title: La programmation fonctionnelle pour les développeurs web v2 +iframe: /talks/afup-tours-programmation-fonctionnelle/index.html +slides: /talks/afup-tours-programmation-fonctionnelle/index.html +tweet: https://twitter.com/AFUP_Tours/status/1118932291458412546 +--- + +Conférence de 45 minutes lors du meetup de l'[AFUP Tours](https://tours.afup.org) le 18 avril 2019. + +Tous les développeurs savent ce qu'est une fonction, mais elles se font souvent voler la vedette par les objets en programmation moderne. Dans ce talk, je présente de nouveaux concepts faciles autour des fonctions permettant de simplifier votre code dans vos projets du quotidien. diff --git a/content/talks/2019-09-17-orleans-tech-conf-informatique-complexite-ecologie.md b/content/talks/2019-09-17-orleans-tech-conf-informatique-complexite-ecologie.md new file mode 100644 index 0000000..a5c1f90 --- /dev/null +++ b/content/talks/2019-09-17-orleans-tech-conf-informatique-complexite-ecologie.md @@ -0,0 +1,10 @@ +--- +title: Informatique, complexité et écologie +iframe: /talks/orleans-tech-conf-informatique-complexite-ecologie/index.html +slides: /talks/orleans-tech-conf-informatique-complexite-ecologie/index.html +tweet: https://twitter.com/orleans_tech/status/1173935000812736513 +--- + +Conférence de 30 minutes lors de la première conférence [Orléans Tech Conf](https://orleans-tech-conf.com/) le 17 septembre 2019. + +L’objectif de ce talk est de faire réfléchir les participants à l’augmentation de la complexité dans nos applications. Aujourd’hui, la moindre application web demande l’exécution de millions de lignes de code invisibles provenant de divers projets open-source pour fonctionner. Cette complexité fait que les développeurs ne sont plus aussi efficaces qu’ils le pourraient, elle entraîne également l’augmentation des bugs dans les logiciels et la diminution des performances avec un impact écologique certain. \ No newline at end of file diff --git a/content/talks/afup-tours-programmation-fonctionnelle/.gitignore b/content/talks/afup-tours-programmation-fonctionnelle/.gitignore new file mode 100644 index 0000000..6b7f95d --- /dev/null +++ b/content/talks/afup-tours-programmation-fonctionnelle/.gitignore @@ -0,0 +1,14 @@ +.idea/ +*.iml +*.iws +*.eml +out/ +.DS_Store +.svn +log/*.log +tmp/** +node_modules/ +.sass-cache +css/reveal.min.css +js/reveal.min.js +elm-stuff/ \ No newline at end of file diff --git a/content/talks/afup-tours-programmation-fonctionnelle/.travis.yml b/content/talks/afup-tours-programmation-fonctionnelle/.travis.yml new file mode 100644 index 0000000..e65e0df --- /dev/null +++ b/content/talks/afup-tours-programmation-fonctionnelle/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - 11 +after_script: + - npm run build -- retire diff --git a/content/talks/afup-tours-programmation-fonctionnelle/CONTRIBUTING.md b/content/talks/afup-tours-programmation-fonctionnelle/CONTRIBUTING.md new file mode 100644 index 0000000..c2091e8 --- /dev/null +++ b/content/talks/afup-tours-programmation-fonctionnelle/CONTRIBUTING.md @@ -0,0 +1,23 @@ +## Contributing + +Please keep the [issue tracker](http://github.com/hakimel/reveal.js/issues) limited to **bug reports**, **feature requests** and **pull requests**. + + +### Personal Support +If you have personal support or setup questions the best place to ask those are [StackOverflow](http://stackoverflow.com/questions/tagged/reveal.js). + + +### Bug Reports +When reporting a bug make sure to include information about which browser and operating system you are on as well as the necessary steps to reproduce the issue. If possible please include a link to a sample presentation where the bug can be tested. + + +### Pull Requests +- Should follow the coding style of the file you work in, most importantly: + - Tabs to indent + - Single-quoted strings +- Should be made towards the **dev branch** +- Should be submitted from a feature/topic branch (not your master) + + +### Plugins +Please do not submit plugins as pull requests. They should be maintained in their own separate repository. More information here: https://github.com/hakimel/reveal.js/wiki/Plugin-Guidelines diff --git a/content/talks/afup-tours-programmation-fonctionnelle/LICENSE b/content/talks/afup-tours-programmation-fonctionnelle/LICENSE new file mode 100644 index 0000000..697d156 --- /dev/null +++ b/content/talks/afup-tours-programmation-fonctionnelle/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2019 Hakim El Hattab, http://hakim.se, and reveal.js contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/content/talks/afup-tours-programmation-fonctionnelle/Main.elm b/content/talks/afup-tours-programmation-fonctionnelle/Main.elm new file mode 100644 index 0000000..9f09dd7 --- /dev/null +++ b/content/talks/afup-tours-programmation-fonctionnelle/Main.elm @@ -0,0 +1,26 @@ +import Browser +import Html exposing (Html, Attribute, button, div, text, input) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick, onInput) + +main = + Browser.sandbox { init = init, update = update, view = view } + +type alias Model = String +type Event = NewName String-- | Reverse + +init : Model +init = "Thibaud" + +update : Event -> Model -> Model +update event oldName = case event of + NewName newName -> newName + -- Reverse -> String.reverse oldName + +view : Model -> Html Event +view name = + div [] + [ input [ placeholder "New Name", onInput NewName, value name ] [] + , button [ onClick Reverse ] [ text "Reverse Name" ] + , div [] [ text name ] + ] \ No newline at end of file diff --git a/content/talks/afup-tours-programmation-fonctionnelle/README.md b/content/talks/afup-tours-programmation-fonctionnelle/README.md new file mode 100644 index 0000000..e5a1911 --- /dev/null +++ b/content/talks/afup-tours-programmation-fonctionnelle/README.md @@ -0,0 +1,1388 @@ +# reveal.js [![Build Status](https://travis-ci.org/hakimel/reveal.js.svg?branch=master)](https://travis-ci.org/hakimel/reveal.js) Slides + +A framework for easily creating beautiful presentations using HTML. [Check out the live demo](http://revealjs.com/). + +reveal.js comes with a broad range of features including [nested slides](https://github.com/hakimel/reveal.js#markup), [Markdown contents](https://github.com/hakimel/reveal.js#markdown), [PDF export](https://github.com/hakimel/reveal.js#pdf-export), [speaker notes](https://github.com/hakimel/reveal.js#speaker-notes) and a [JavaScript API](https://github.com/hakimel/reveal.js#api). There's also a fully featured visual editor and platform for sharing reveal.js presentations at [slides.com](https://slides.com?ref=github). + + +## Table of contents + +- [Online Editor](#online-editor) +- [Installation](#installation) + - [Basic setup](#basic-setup) + - [Full setup](#full-setup) + - [Folder Structure](#folder-structure) +- [Instructions](#instructions) + - [Markup](#markup) + - [Markdown](#markdown) + - [Element Attributes](#element-attributes) + - [Slide Attributes](#slide-attributes) +- [Configuration](#configuration) +- [Presentation Size](#presentation-size) +- [Dependencies](#dependencies) +- [Ready Event](#ready-event) +- [Auto-sliding](#auto-sliding) +- [Keyboard Bindings](#keyboard-bindings) +- [Vertical Slide Navigation](#vertical-slide-navigation) +- [Touch Navigation](#touch-navigation) +- [Lazy Loading](#lazy-loading) +- [API](#api) + - [Slide Changed Event](#slide-changed-event) + - [Presentation State](#presentation-state) + - [Slide States](#slide-states) + - [Slide Backgrounds](#slide-backgrounds) + - [Parallax Background](#parallax-background) + - [Slide Transitions](#slide-transitions) + - [Internal links](#internal-links) + - [Fragments](#fragments) + - [Fragment events](#fragment-events) + - [Code syntax highlighting](#code-syntax-highlighting) + - [Slide number](#slide-number) + - [Overview mode](#overview-mode) + - [Fullscreen mode](#fullscreen-mode) + - [Embedded media](#embedded-media) + - [Stretching elements](#stretching-elements) + - [Resize Event](#resize-event) + - [postMessage API](#postmessage-api) +- [PDF Export](#pdf-export) +- [Theming](#theming) +- [Speaker Notes](#speaker-notes) + - [Share and Print Speaker Notes](#share-and-print-speaker-notes) + - [Server Side Speaker Notes](#server-side-speaker-notes) +- [Multiplexing](#multiplexing) + - [Master presentation](#master-presentation) + - [Client presentation](#client-presentation) + - [Socket.io server](#socketio-server) +- [MathJax](#mathjax) +- [License](#license) + +#### More reading + +- [Changelog](https://github.com/hakimel/reveal.js/releases): Up-to-date version history. +- [Examples](https://github.com/hakimel/reveal.js/wiki/Example-Presentations): Presentations created with reveal.js, add your own! +- [Browser Support](https://github.com/hakimel/reveal.js/wiki/Browser-Support): Explanation of browser support and fallbacks. +- [Plugins](https://github.com/hakimel/reveal.js/wiki/Plugins,-Tools-and-Hardware): A list of plugins that can be used to extend reveal.js. + + +## Online Editor + +Presentations are written using HTML or Markdown but there's also an online editor for those of you who prefer a graphical interface. Give it a try at [https://slides.com](https://slides.com?ref=github). + + +## Installation + +The **basic setup** is for authoring presentations only. The **full setup** gives you access to all reveal.js features and plugins such as speaker notes as well as the development tasks needed to make changes to the source. + +### Basic setup + +The core of reveal.js is very easy to install. You'll simply need to download a copy of this repository and open the index.html file directly in your browser. + +1. Download the latest version of reveal.js from +2. Unzip and replace the example contents in index.html with your own +3. Open index.html in a browser to view it + +### Full setup + +Some reveal.js features, like external Markdown and speaker notes, require that presentations run from a local web server. The following instructions will set up such a server as well as all of the development tasks needed to make edits to the reveal.js source code. + +1. Install [Node.js](http://nodejs.org/) (4.0.0 or later) + +1. Clone the reveal.js repository + ```sh + $ git clone https://github.com/hakimel/reveal.js.git + ``` + +1. Navigate to the reveal.js folder + ```sh + $ cd reveal.js + ``` + +1. Install dependencies + ```sh + $ npm install + ``` + +1. Serve the presentation and monitor source files for changes + ```sh + $ npm start + ``` + +1. Open to view your presentation + + You can change the port by using `npm start -- --port=8001`. + +### Folder Structure + +- **css/** Core styles without which the project does not function +- **js/** Like above but for JavaScript +- **plugin/** Components that have been developed as extensions to reveal.js +- **lib/** All other third party assets (JavaScript, CSS, fonts) + + +## Instructions + +### Markup + +Here's a barebones example of a fully working reveal.js presentation: +```html + + + + + + +
+
+
Slide 1
+
Slide 2
+
+
+ + + + +``` + +The presentation markup hierarchy needs to be `.reveal > .slides > section` where the `section` represents one slide and can be repeated indefinitely. If you place multiple `section` elements inside of another `section` they will be shown as vertical slides. The first of the vertical slides is the "root" of the others (at the top), and will be included in the horizontal sequence. For example: + +```html +
+
+
Single Horizontal Slide
+
+
Vertical Slide 1
+
Vertical Slide 2
+
+
+
+``` + +### Markdown + +It's possible to write your slides using Markdown. To enable Markdown, add the `data-markdown` attribute to your `
` elements and wrap the contents in a ` +
+``` + +#### External Markdown + +You can write your content as a separate file and have reveal.js load it at runtime. Note the separator arguments which determine how slides are delimited in the external file: the `data-separator` attribute defines a regular expression for horizontal slides (defaults to `^\r?\n---\r?\n$`, a newline-bounded horizontal rule) and `data-separator-vertical` defines vertical slides (disabled by default). The `data-separator-notes` attribute is a regular expression for specifying the beginning of the current slide's speaker notes (defaults to `notes?:`, so it will match both "note:" and "notes:"). The `data-charset` attribute is optional and specifies which charset to use when loading the external file. + +When used locally, this feature requires that reveal.js [runs from a local web server](#full-setup). The following example customises all available options: + +```html +
+ +
+``` + +#### Element Attributes + +Special syntax (through HTML comments) is available for adding attributes to Markdown elements. This is useful for fragments, amongst other things. + +```html +
+ +
+``` + +#### Slide Attributes + +Special syntax (through HTML comments) is available for adding attributes to the slide `
` elements generated by your Markdown. + +```html +
+ +
+``` + +#### Configuring *marked* + +We use [marked](https://github.com/chjj/marked) to parse Markdown. To customise marked's rendering, you can pass in options when [configuring Reveal](#configuration): + +```javascript +Reveal.initialize({ + // Options which are passed into marked + // See https://marked.js.org/#/USING_ADVANCED.md#options + markdown: { + smartypants: true + } +}); +``` + +### Configuration + +At the end of your page you need to initialize reveal by running the following code. Note that all configuration values are optional and will default to the values specified below. + +```javascript +Reveal.initialize({ + + // Display presentation control arrows + controls: true, + + // Help the user learn the controls by providing hints, for example by + // bouncing the down arrow when they first encounter a vertical slide + controlsTutorial: true, + + // Determines where controls appear, "edges" or "bottom-right" + controlsLayout: 'bottom-right', + + // Visibility rule for backwards navigation arrows; "faded", "hidden" + // or "visible" + controlsBackArrows: 'faded', + + // Display a presentation progress bar + progress: true, + + // Display the page number of the current slide + slideNumber: false, + + // Add the current slide number to the URL hash so that reloading the + // page/copying the URL will return you to the same slide + hash: false, + + // Push each slide change to the browser history. Implies `hash: true` + history: false, + + // Enable keyboard shortcuts for navigation + keyboard: true, + + // Enable the slide overview mode + overview: true, + + // Vertical centering of slides + center: true, + + // Enables touch navigation on devices with touch input + touch: true, + + // Loop the presentation + loop: false, + + // Change the presentation direction to be RTL + rtl: false, + + // See https://github.com/hakimel/reveal.js/#navigation-mode + navigationMode: 'default', + + // Randomizes the order of slides each time the presentation loads + shuffle: false, + + // Turns fragments on and off globally + fragments: true, + + // Flags whether to include the current fragment in the URL, + // so that reloading brings you to the same fragment position + fragmentInURL: false, + + // Flags if the presentation is running in an embedded mode, + // i.e. contained within a limited portion of the screen + embedded: false, + + // Flags if we should show a help overlay when the questionmark + // key is pressed + help: true, + + // Flags if speaker notes should be visible to all viewers + showNotes: false, + + // Global override for autoplaying embedded media (video/audio/iframe) + // - null: Media will only autoplay if data-autoplay is present + // - true: All media will autoplay, regardless of individual setting + // - false: No media will autoplay, regardless of individual setting + autoPlayMedia: null, + + // Global override for preloading lazy-loaded iframes + // - null: Iframes with data-src AND data-preload will be loaded when within + // the viewDistance, iframes with only data-src will be loaded when visible + // - true: All iframes with data-src will be loaded when within the viewDistance + // - false: All iframes with data-src will be loaded only when visible + preloadIframes: null, + + // Number of milliseconds between automatically proceeding to the + // next slide, disabled when set to 0, this value can be overwritten + // by using a data-autoslide attribute on your slides + autoSlide: 0, + + // Stop auto-sliding after user input + autoSlideStoppable: true, + + // Use this method for navigation when auto-sliding + autoSlideMethod: Reveal.navigateNext, + + // Specify the average time in seconds that you think you will spend + // presenting each slide. This is used to show a pacing timer in the + // speaker view + defaultTiming: 120, + + // Enable slide navigation via mouse wheel + mouseWheel: false, + + // Hide cursor if inactive + hideInactiveCursor: true, + + // Time before the cursor is hidden (in ms) + hideCursorTime: 5000, + + // Hides the address bar on mobile devices + hideAddressBar: true, + + // Opens links in an iframe preview overlay + // Add `data-preview-link` and `data-preview-link="false"` to customise each link + // individually + previewLinks: false, + + // Transition style + transition: 'slide', // none/fade/slide/convex/concave/zoom + + // Transition speed + transitionSpeed: 'default', // default/fast/slow + + // Transition style for full page slide backgrounds + backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom + + // Number of slides away from the current that are visible + viewDistance: 3, + + // Parallax background image + parallaxBackgroundImage: '', // e.g. "'https://s3.amazonaws.com/hakim-static/reveal-js/reveal-parallax-1.jpg'" + + // Parallax background size + parallaxBackgroundSize: '', // CSS syntax, e.g. "2100px 900px" + + // Number of pixels to move the parallax background per slide + // - Calculated automatically unless specified + // - Set to 0 to disable movement along an axis + parallaxBackgroundHorizontal: null, + parallaxBackgroundVertical: null, + + // The display mode that will be used to show slides + display: 'block' + +}); +``` + +The configuration can be updated after initialization using the `configure` method: + +```javascript +// Turn autoSlide off +Reveal.configure({ autoSlide: 0 }); + +// Start auto-sliding every 5s +Reveal.configure({ autoSlide: 5000 }); +``` + +### Presentation Size + +All presentations have a normal size, that is, the resolution at which they are authored. The framework will automatically scale presentations uniformly based on this size to ensure that everything fits on any given display or viewport. + +See below for a list of configuration options related to sizing, including default values: + +```javascript +Reveal.initialize({ + + // ... + + // The "normal" size of the presentation, aspect ratio will be preserved + // when the presentation is scaled to fit different resolutions. Can be + // specified using percentage units. + width: 960, + height: 700, + + // Factor of the display size that should remain empty around the content + margin: 0.1, + + // Bounds for smallest/largest possible scale to apply to content + minScale: 0.2, + maxScale: 1.5 + +}); +``` + +If you wish to disable this behavior and do your own scaling (e.g. using media queries), try these settings: + +```javascript +Reveal.initialize({ + + // ... + + width: "100%", + height: "100%", + margin: 0, + minScale: 1, + maxScale: 1 +}); +``` + +### Dependencies + +Reveal.js doesn't _rely_ on any third party scripts to work but a few optional libraries are included by default. These libraries are loaded as dependencies in the order they appear, for example: + +```javascript +Reveal.initialize({ + dependencies: [ + // Interpret Markdown in
elements + { src: 'plugin/markdown/marked.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } }, + { src: 'plugin/markdown/markdown.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } }, + + // Syntax highlight for elements + { src: 'plugin/highlight/highlight.js', async: true, callback: function() { hljs.initHighlightingOnLoad(); } }, + + // Zoom in and out with Alt+click + { src: 'plugin/zoom-js/zoom.js', async: true }, + + // Speaker notes + { src: 'plugin/notes/notes.js', async: true }, + + // MathJax + { src: 'plugin/math/math.js', async: true } + ] +}); +``` + +You can add your own extensions using the same syntax. The following properties are available for each dependency object: +- **src**: Path to the script to load +- **async**: [optional] Flags if the script should load after reveal.js has started, defaults to false +- **callback**: [optional] Function to execute when the script has loaded +- **condition**: [optional] Function which must return true for the script to be loaded + +### Ready Event + +A `ready` event is fired when reveal.js has loaded all non-async dependencies and is ready to start navigating. To check if reveal.js is already 'ready' you can call `Reveal.isReady()`. + +```javascript +Reveal.addEventListener( 'ready', function( event ) { + // event.currentSlide, event.indexh, event.indexv +} ); +``` + +Note that we also add a `.ready` class to the `.reveal` element so that you can hook into this with CSS. + +### Auto-sliding + +Presentations can be configured to progress through slides automatically, without any user input. To enable this you will need to tell the framework how many milliseconds it should wait between slides: + +```javascript +// Slide every five seconds +Reveal.configure({ + autoSlide: 5000 +}); +``` + +When this is turned on a control element will appear that enables users to pause and resume auto-sliding. Alternatively, sliding can be paused or resumed by pressing »A« on the keyboard. Sliding is paused automatically as soon as the user starts navigating. You can disable these controls by specifying `autoSlideStoppable: false` in your reveal.js config. + +You can also override the slide duration for individual slides and fragments by using the `data-autoslide` attribute: + +```html +
+

After 2 seconds the first fragment will be shown.

+

After 10 seconds the next fragment will be shown.

+

Now, the fragment is displayed for 2 seconds before the next slide is shown.

+
+``` + +To override the method used for navigation when auto-sliding, you can specify the `autoSlideMethod` setting. To only navigate along the top layer and ignore vertical slides, set this to `Reveal.navigateRight`. + +Whenever the auto-slide mode is resumed or paused the `autoslideresumed` and `autoslidepaused` events are fired. + +### Keyboard Bindings + +If you're unhappy with any of the default keyboard bindings you can override them using the `keyboard` config option: + +```javascript +Reveal.configure({ + keyboard: { + 13: 'next', // go to the next slide when the ENTER key is pressed + 27: function() {}, // do something custom when ESC is pressed + 32: null // don't do anything when SPACE is pressed (i.e. disable a reveal.js default binding) + } +}); +``` + +### Vertical Slide Navigation + +Slides can be nested within other slides to create vertical stacks (see [Markup](#markup)). When presenting, you use the left/right arrows to step through the main (horizontal) slides. When you arrive at a vertical stack you can optionally press the up/down arrows to view the vertical slides or skip past them by pressing the right arrow. Here's an example showing a bird's-eye view of what this looks like in action: + + + +#### Navigation Mode +You can finetune the reveal.js navigation behavior by using the `navigationMode` config option. Note that these options are only useful for presnetations that use a mix of horizontal and vertical slides. The following navigation modes are available: + +| Value | Description | +| :--------------------------- | :---------- | +| default | Left/right arrow keys step between horizontal slides. Up/down arrow keys step between vertical slides. Space key steps through all slides (both horizontal and vertical). | +| linear | Removes the up/down arrows. Left/right arrows step through all slides (both horizontal and vertical). | +| grid | When this is enabled, stepping left/right from a vertical stack to an adjacent vertical stack will land you at the same vertical index.

Consider a deck with six slides ordered in two vertical stacks:
`1.1`    `2.1`
`1.2`    `2.2`
`1.3`    `2.3`

If you're on slide 1.3 and navigate right, you will normally move from 1.3 -> 2.1. With navigationMode set to "grid" the same navigation takes you from 1.3 -> 2.3. | + +### Touch Navigation + +You can swipe to navigate through a presentation on any touch-enabled device. Horizontal swipes change between horizontal slides, vertical swipes change between vertical slides. If you wish to disable this you can set the `touch` config option to false when initializing reveal.js. + +If there's some part of your content that needs to remain accessible to touch events you'll need to highlight this by adding a `data-prevent-swipe` attribute to the element. One common example where this is useful is elements that need to be scrolled. + +### Lazy Loading + +When working on presentation with a lot of media or iframe content it's important to load lazily. Lazy loading means that reveal.js will only load content for the few slides nearest to the current slide. The number of slides that are preloaded is determined by the `viewDistance` configuration option. + +To enable lazy loading all you need to do is change your `src` attributes to `data-src` as shown below. This is supported for image, video, audio and iframe elements. + +```html +
+ + + +
+``` + +#### Lazy Loading Iframes + +Note that lazy loaded iframes ignore the `viewDistance` configuration and will only load when their containing slide becomes visible. Iframes are also unloaded as soon as the slide is hidden. + +When we lazy load a video or audio element, reveal.js won't start playing that content until the slide becomes visible. However there is no way to control this for an iframe since that could contain any kind of content. That means if we loaded an iframe before the slide is visible on screen it could begin playing media and sound in the background. + +You can override this behavior with the `data-preload` attribute. The iframe below will be loaded +according to the `viewDistance`. + +```html +
+ +
+``` + +You can also change the default globally with the `preloadIframes` configuration option. If set to +`true` ALL iframes with a `data-src` attribute will be preloaded when within the `viewDistance` +regardless of individual `data-preload` attributes. If set to `false`, all iframes will only be +loaded when they become visible. + +### API + +The `Reveal` object exposes a JavaScript API for controlling navigation and reading state: + +```javascript +// Navigation +Reveal.slide( indexh, indexv, indexf ); +Reveal.left(); +Reveal.right(); +Reveal.up(); +Reveal.down(); +Reveal.prev(); +Reveal.next(); +Reveal.prevFragment(); +Reveal.nextFragment(); + +// Randomize the order of slides +Reveal.shuffle(); + +// Toggle presentation states, optionally pass true/false to force on/off +Reveal.toggleOverview(); +Reveal.togglePause(); +Reveal.toggleAutoSlide(); + +// Shows a help overlay with keyboard shortcuts, optionally pass true/false +// to force on/off +Reveal.toggleHelp(); + +// Change a config value at runtime +Reveal.configure({ controls: true }); + +// Returns the present configuration options +Reveal.getConfig(); + +// Fetch the current scale of the presentation +Reveal.getScale(); + +// Retrieves the previous and current slide elements +Reveal.getPreviousSlide(); +Reveal.getCurrentSlide(); + +Reveal.getIndices(); // { h: 0, v: 0, f: 0 } +Reveal.getSlidePastCount(); +Reveal.getProgress(); // (0 == first slide, 1 == last slide) +Reveal.getSlides(); // Array of all slides +Reveal.getTotalSlides(); // Total number of slides + +// Returns the speaker notes for the current slide +Reveal.getSlideNotes(); + +// State checks +Reveal.isFirstSlide(); +Reveal.isLastSlide(); +Reveal.isOverview(); +Reveal.isPaused(); +Reveal.isAutoSliding(); + +// Returns the top-level DOM element +getRevealElement(); //
...
+``` + +### Custom Key Bindings + +Custom key bindings can be added and removed using the following Javascript API. Custom key bindings will override the default keyboard bindings, but will in turn be overridden by the user defined bindings in the ``keyboard`` config option. + +```javascript +Reveal.addKeyBinding( binding, callback ); +Reveal.removeKeyBinding( keyCode ); +``` + +For example + +```javascript +// The binding parameter provides the following properties +// keyCode: the keycode for binding to the callback +// key: the key label to show in the help overlay +// description: the description of the action to show in the help overlay +Reveal.addKeyBinding( { keyCode: 84, key: 'T', description: 'Start timer' }, function() { + // start timer +} ) + +// The binding parameter can also be a direct keycode without providing the help description +Reveal.addKeyBinding( 82, function() { + // reset timer +} ) +``` + +This allows plugins to add key bindings directly to Reveal so they can + +* make use of Reveal's pre-processing logic for key handling (for example, ignoring key presses when paused); and +* be included in the help overlay (optional) + +### Slide Changed Event + +A `slidechanged` event is fired each time the slide is changed (regardless of state). The event object holds the index values of the current slide as well as a reference to the previous and current slide HTML nodes. + +Some libraries, like MathJax (see [#226](https://github.com/hakimel/reveal.js/issues/226#issuecomment-10261609)), get confused by the transforms and display states of slides. Often times, this can be fixed by calling their update or render function from this callback. + +```javascript +Reveal.addEventListener( 'slidechanged', function( event ) { + // event.previousSlide, event.currentSlide, event.indexh, event.indexv +} ); +``` + +### Presentation State + +The presentation's current state can be fetched by using the `getState` method. A state object contains all of the information required to put the presentation back as it was when `getState` was first called. Sort of like a snapshot. It's a simple object that can easily be stringified and persisted or sent over the wire. + +```javascript +Reveal.slide( 1 ); +// we're on slide 1 + +var state = Reveal.getState(); + +Reveal.slide( 3 ); +// we're on slide 3 + +Reveal.setState( state ); +// we're back on slide 1 +``` + +### Slide States + +If you set `data-state="somestate"` on a slide `
`, "somestate" will be applied as a class on the document element when that slide is opened. This allows you to apply broad style changes to the page based on the active slide. + +Furthermore you can also listen to these changes in state via JavaScript: + +```javascript +Reveal.addEventListener( 'somestate', function() { + // TODO: Sprinkle magic +}, false ); +``` + +### Slide Backgrounds + +Slides are contained within a limited portion of the screen by default to allow them to fit any display and scale uniformly. You can apply full page backgrounds outside of the slide area by adding a `data-background` attribute to your `
` elements. Four different types of backgrounds are supported: color, image, video and iframe. + +#### Color Backgrounds + +All CSS color formats are supported, including hex values, keywords, `rgba()` or `hsl()`. + +```html +
+

Color

+
+``` + +#### Image Backgrounds + +By default, background images are resized to cover the full page. Available options: + +| Attribute | Default | Description | +| :------------------------------- | :--------- | :---------- | +| data-background-image | | URL of the image to show. GIFs restart when the slide opens. | +| data-background-size | cover | See [background-size](https://developer.mozilla.org/docs/Web/CSS/background-size) on MDN. | +| data-background-position | center | See [background-position](https://developer.mozilla.org/docs/Web/CSS/background-position) on MDN. | +| data-background-repeat | no-repeat | See [background-repeat](https://developer.mozilla.org/docs/Web/CSS/background-repeat) on MDN. | +| data-background-opacity | 1 | Opacity of the background image on a 0-1 scale. 0 is transparent and 1 is fully opaque. | + +```html +
+

Image

+
+
+

This background image will be sized to 100px and repeated

+
+``` + +#### Video Backgrounds + +Automatically plays a full size video behind the slide. + +| Attribute | Default | Description | +| :--------------------------- | :------ | :---------- | +| data-background-video | | A single video source, or a comma separated list of video sources. | +| data-background-video-loop | false | Flags if the video should play repeatedly. | +| data-background-video-muted | false | Flags if the audio should be muted. | +| data-background-size | cover | Use `cover` for full screen and some cropping or `contain` for letterboxing. | +| data-background-opacity | 1 | Opacity of the background video on a 0-1 scale. 0 is transparent and 1 is fully opaque. | + +```html +
+

Video

+
+``` + +#### Iframe Backgrounds + +Embeds a web page as a slide background that covers 100% of the reveal.js width and height. The iframe is in the background layer, behind your slides, and as such it's not possible to interact with it by default. To make your background interactive, you can add the `data-background-interactive` attribute. + +```html +
+

Iframe

+
+``` + +#### Background Transitions + +Backgrounds transition using a fade animation by default. This can be changed to a linear sliding transition by passing `backgroundTransition: 'slide'` to the `Reveal.initialize()` call. Alternatively you can set `data-background-transition` on any section with a background to override that specific transition. + + +### Parallax Background + +If you want to use a parallax scrolling background, set the first two properties below when initializing reveal.js (the other two are optional). + +```javascript +Reveal.initialize({ + + // Parallax background image + parallaxBackgroundImage: '', // e.g. "https://s3.amazonaws.com/hakim-static/reveal-js/reveal-parallax-1.jpg" + + // Parallax background size + parallaxBackgroundSize: '', // CSS syntax, e.g. "2100px 900px" - currently only pixels are supported (don't use % or auto) + + // Number of pixels to move the parallax background per slide + // - Calculated automatically unless specified + // - Set to 0 to disable movement along an axis + parallaxBackgroundHorizontal: 200, + parallaxBackgroundVertical: 50 + +}); +``` + +Make sure that the background size is much bigger than screen size to allow for some scrolling. [View example](http://revealjs.com/?parallaxBackgroundImage=https%3A%2F%2Fs3.amazonaws.com%2Fhakim-static%2Freveal-js%2Freveal-parallax-1.jpg¶llaxBackgroundSize=2100px%20900px). + +### Slide Transitions + +The global presentation transition is set using the `transition` config value. You can override the global transition for a specific slide by using the `data-transition` attribute: + +```html +
+

This slide will override the presentation transition and zoom!

+
+ +
+

Choose from three transition speeds: default, fast or slow!

+
+``` + +You can also use different in and out transitions for the same slide: + +```html +
+ The train goes on … +
+
+ and on … +
+
+ and stops. +
+
+ (Passengers entering and leaving) +
+
+ And it starts again. +
+``` +You can choose from `none`, `fade`, `slide`, `convex`, `concave` and `zoom`. +### Internal links + +It's easy to link between slides. The first example below targets the index of another slide whereas the second targets a slide with an ID attribute (`
`): + +```html +Link +Link +``` + +You can also add relative navigation links, similar to the built in reveal.js controls, by appending one of the following classes on any element. Note that each element is automatically given an `enabled` class when it's a valid navigation route based on the current slide. + +```html + + + + + + +``` + +### Fragments + +Fragments are used to highlight individual elements on a slide. Every element with the class `fragment` will be stepped through before moving on to the next slide. Here's an example: http://revealjs.com/#/fragments + +The default fragment style is to start out invisible and fade in. This style can be changed by appending a different class to the fragment: + +```html +
+

grow

+

shrink

+

fade-out

+

fade-up (also down, left and right!)

+

fades in, then out when we move to the next step

+

fades in, then obfuscate when we move to the next step

+

blue only once

+

highlight-red

+

highlight-green

+

highlight-blue

+
+``` + +Multiple fragments can be applied to the same element sequentially by wrapping it, this will fade in the text on the first step and fade it back out on the second. + +```html +
+ + I'll fade in, then out + +
+``` + +The display order of fragments can be controlled using the `data-fragment-index` attribute. + +```html +
+

Appears last

+

Appears first

+

Appears second

+
+``` + +### Fragment events + +When a slide fragment is either shown or hidden reveal.js will dispatch an event. + +Some libraries, like MathJax (see #505), get confused by the initially hidden fragment elements. Often times this can be fixed by calling their update or render function from this callback. + +```javascript +Reveal.addEventListener( 'fragmentshown', function( event ) { + // event.fragment = the fragment DOM element +} ); +Reveal.addEventListener( 'fragmenthidden', function( event ) { + // event.fragment = the fragment DOM element +} ); +``` + +### Code syntax highlighting + +By default, Reveal is configured with [highlight.js](https://highlightjs.org/) for code syntax highlighting. To enable syntax highlighting, you'll have to load the highlight plugin ([plugin/highlight/highlight.js](plugin/highlight/highlight.js)) and a highlight.js CSS theme (Reveal comes packaged with the Monokai themes: [lib/css/monokai.css](lib/css/monokai.css)). + +```javascript +Reveal.initialize({ + // More info https://github.com/hakimel/reveal.js#dependencies + dependencies: [ + { src: 'plugin/highlight/highlight.js', async: true, callback: function() { hljs.initHighlightingOnLoad(); } }, + ] +}); +``` + +Below is an example with clojure code that will be syntax highlighted. When the `data-trim` attribute is present, surrounding whitespace is automatically removed. HTML will be escaped by default. To avoid this, for example if you are using `` to call out a line of code, add the `data-noescape` attribute to the `` element. + +```html +
+

+(def lazy-fib
+  (concat
+   [0 1]
+   ((fn rfib [a b]
+        (lazy-cons (+ a b) (rfib b (+ a b)))) 0 1)))
+	
+
+``` + +### Slide number + +If you would like to display the page number of the current slide you can do so using the `slideNumber` and `showSlideNumber` configuration values. + +```javascript +// Shows the slide number using default formatting +Reveal.configure({ slideNumber: true }); + +// Slide number formatting can be configured using these variables: +// "h.v": horizontal . vertical slide number (default) +// "h/v": horizontal / vertical slide number +// "c": flattened slide number +// "c/t": flattened slide number / total slides +Reveal.configure({ slideNumber: 'c/t' }); + +// You can provide a function to fully customize the number: +Reveal.configure({ slideNumber: function() { + // Ignore numbering of vertical slides + return [ Reveal.getIndices().h ]; +}}); + +// Control which views the slide number displays on using the "showSlideNumber" value: +// "all": show on all views (default) +// "speaker": only show slide numbers on speaker notes view +// "print": only show slide numbers when printing to PDF +Reveal.configure({ showSlideNumber: 'speaker' }); +``` + +### Overview mode + +Press »ESC« or »O« keys to toggle the overview mode on and off. While you're in this mode, you can still navigate between slides, +as if you were at 1,000 feet above your presentation. The overview mode comes with a few API hooks: + +```javascript +Reveal.addEventListener( 'overviewshown', function( event ) { /* ... */ } ); +Reveal.addEventListener( 'overviewhidden', function( event ) { /* ... */ } ); + +// Toggle the overview mode programmatically +Reveal.toggleOverview(); +``` + +### Fullscreen mode + +Just press »F« on your keyboard to show your presentation in fullscreen mode. Press the »ESC« key to exit fullscreen mode. + +### Embedded media + +Add `data-autoplay` to your media element if you want it to automatically start playing when the slide is shown: + +```html + +``` + +If you want to enable or disable autoplay globally, for all embedded media, you can use the `autoPlayMedia` configuration option. If you set this to `true` ALL media will autoplay regardless of individual `data-autoplay` attributes. If you initialize with `autoPlayMedia: false` NO media will autoplay. + +Note that embedded HTML5 `