diff --git a/Cargo.lock b/Cargo.lock index 6598cec..2d36d8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,55 +3,45 @@ version = 3 [[package]] -name = "block-buffer" -version = "0.10.4" +name = "ahash" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ - "generic-array", + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" + +[[package]] +name = "cc" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[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 = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "equivalent" version = "1.0.1" @@ -59,13 +49,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] -name = "generic-array" -version = "0.14.7" +name = "getrandom" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ - "typenum", - "version_check", + "cfg-if", + "libc", + "wasi", ] [[package]] @@ -78,41 +69,38 @@ checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" name = "html" version = "0.1.0" dependencies = [ - "html_parser", - "toml", - "walkdir", -] - -[[package]] -name = "html_parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f56db07b6612644f6f7719f8ef944f75fff9d6378fdf3d316fd32194184abd" -dependencies = [ - "doc-comment", - "pest", - "pest_derive", - "serde", - "serde_derive", - "serde_json", - "thiserror", + "inkjet", + "v_htmlescape", ] [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", ] [[package]] -name = "itoa" -version = "1.0.10" +name = "inkjet" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "b44a14d77fc5879e5137be9beee1401ca24038be727ca1b4b68bc81705f98b40" +dependencies = [ + "ahash", + "anyhow", + "cc", + "once_cell", + "serde", + "thiserror", + "toml 0.7.8", + "toml 0.8.12", + "tree-sitter", + "tree-sitter-highlight", + "v_htmlescape", +] [[package]] name = "libc" @@ -122,9 +110,9 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "once_cell" @@ -132,115 +120,73 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "pest" -version = "2.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c0dcc30b6a27553f9cc242972b67f75b60eb0db71f0b5462f38b058c41546" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e1288dbd7786462961e69bfd4df7848c1e37e8b74303dbdab82c3a9cdd2809" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1381c29a877c6d34b8c176e734f35d7f7f5b3adaefe940cb4d1bb7af94678e2e" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0934d6907f148c22a3acbda520c7eed243ad7487a30f51f6ce52b58b7077a8a" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] -name = "ryu" -version = "1.0.17" +name = "regex" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ - "winapi-util", + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] -name = "serde" -version = "1.0.197" +name = "regex-automata" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "serde" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "serde_json" -version = "1.0.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" -dependencies = [ - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_spanned" version = "0.6.5" @@ -250,22 +196,11 @@ dependencies = [ "serde", ] -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "syn" -version = "2.0.51" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -274,18 +209,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", @@ -294,14 +229,26 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.10" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.12", ] [[package]] @@ -315,28 +262,50 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.6" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] -name = "typenum" -version = "1.17.0" +name = "toml_edit" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.6", +] [[package]] -name = "ucd-trie" -version = "0.1.6" +name = "tree-sitter" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d" +dependencies = [ + "cc", + "regex", +] + +[[package]] +name = "tree-sitter-highlight" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "042342584c5a7a0b833d9fc4e2bdab3f9868ddc6c4b339a1e01451c6720868bc" +dependencies = [ + "regex", + "thiserror", + "tree-sitter", +] [[package]] name = "unicode-ident" @@ -344,6 +313,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + [[package]] name = "version_check" version = "0.9.4" @@ -351,51 +326,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] -name = "walkdir" -version = "2.5.0" +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winnow" -version = "0.6.5" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] + +[[package]] +name = "winnow" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 9b06940..5d206b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -html_parser = "0.7" -toml = "0.8" -walkdir = "2.5" +inkjet = "0.10" +v_htmlescape = "0.15" + +[profiles.dev.package.inkjet] +opt-level = 3 diff --git a/blueprint.html b/blueprint.html index dc8a4cf..58d3cf3 100644 --- a/blueprint.html +++ b/blueprint.html @@ -1,7 +1,6 @@ - - - - + + <@children /> + <@children /> diff --git a/default.toml b/default.toml deleted file mode 100644 index cf6430a..0000000 --- a/default.toml +++ /dev/null @@ -1,5 +0,0 @@ -[compile] -output="dist" -templates="tp" -source="src" -minify=false diff --git a/hyper-build/home.html b/hyper-build/home.html new file mode 100644 index 0000000..5c7a9b7 --- /dev/null +++ b/hyper-build/home.html @@ -0,0 +1,35 @@ +

Completed Projects

\ No newline at end of file diff --git a/hyper-build/html-templating.html b/hyper-build/html-templating.html new file mode 100644 index 0000000..09cf5c0 --- /dev/null +++ b/hyper-build/html-templating.html @@ -0,0 +1,88 @@ +

Rust HTML Templating Engine

Front End - Parser Design

Motivation

+ HTML is a powerful tool for creating static web pages, but + is not easily modular or scalable. Front end frameworks like + React provide reusable components. I want to use components + without the overhead of a front-end framework. I want the final + output to be statically generated rather than generated on the fly + by the server or the client, minified and with inlined CSS and + JavaScript. +

Approach

+ After looking at some of the HTML parsing libraries, I was + unsatisfied with the approaches most developers took. The DOM + is represented as a tree structure, where each node keeps a + pointer to its children. +
+

+// From the html_parser crate
+pub enum Node {
+    Text(String),
+    Element(Element),
+    Comment(String),
+}
+
+pub struct Element {
+    pub name: String,
+    pub attributes: HashMap<String, Option<String>>,
+    pub children: Vec<Node>,
+    // other fields omitted
+}
+
+
+ This is bad for cache locality and effectively forces the use + of recursion in order to traverse the DOM. Recursion is undesirable + for performance and robustness reasons. My approach is to create a + flat array of elements. +
+

+// My implementation (fields omitted)
+pub enum HtmlElement {
+  DocType,
+  Comment(/* */),
+  OpenTag { /* */ },
+  CloseTag { /* */ },
+  Text( /* */ ),
+  Script { /* */ },
+  Style { /* */ },
+  Directive { /* */ },
+}
+
+
+ This approach lends itself to linear iterative algorithms. An + element's children can be represented as a slice of the DOM array, + from the opening tag to its close tag. +

Templates

+ I define a "template" as a user defined element which expands into + a larger piece of HTML. Templates can pass on children and attributes + to the final output. Some special templates can perform other functions, + like inlining a CSS file or syntax highlighting a code block. +
+

+<!-- Here is an example of a template definition -->
+<Echo> 
+  <!-- 
+    This means to inherit the 'class' attribute from the invocation
+  -->
+  <h1 class=@class> 
+    <!-- 
+      This means place the children of the invocation here
+    -->
+    <@children/>
+  </h1>
+  <h2> 
+  <!-- This means to inherit the 'href' attribute from the invocation -->
+    <a href="@href"> 
+      <!-- Children can be duplicated any number of times -->
+      <@children/> 
+    </a> 
+  </h2>
+  <!-- 
+    Attributes can also be duplciated, and boolean attributes
+    work exactly as expected 
+  -->
+  <h3 class=@class hidden=@hidden>  
+    <@children/> 
+  </h3>
+</Echo>
+<!-- End of template definition -->
+
+
\ No newline at end of file diff --git a/hyper-build/http-server.html b/hyper-build/http-server.html new file mode 100644 index 0000000..8a2aa1a --- /dev/null +++ b/hyper-build/http-server.html @@ -0,0 +1,56 @@ +

Rust HTTP Server

Keywords: Back End - TCP - SSL

Motivation

+ As a systems programmer, I want to understand the technologies + I use at a very low level. Modern web stacks abstract away the + process of handling TCP connections, serving SSL certificates, + and parsing HTTP requests. The best way to understand a technology + is to implement it from the ground up, I studied the HTTP 1.1 + specification and wrote a server to host my website. +

Architecture

+ At the most basic level, the server will translate the request URL + into a local file path, and respond with the file matching that path + if it exists. Request URLs are sanitized to prevent accessing files + outside of the server's directory. Two tables are used for rewriting + and routing URLs. This makes handling URLs predictable and robust. + Because I route all traffic through Cloudflare, I decided to use its + caching feature rather than implement a cache locally. +
+

+// Snippet from the request handling code
+pub async fn construct_response(&self, request_url: &str) -> Response {
+  let sanitized_url = rewrite_url(request_url);
+  if sanitized_url != request_url {
+    return Self::redirect(&sanitized_url);
+  }
+
+  let routed_url = self.perform_routing(&sanitized_url).await;
+  if let Some(response) = 
+    self.perform_redirecting(&routed_url).await {
+    return response;
+  }
+  match self.get_resource(&routed_url).await {
+    Some(bytes) => Response::from_data(bytes),
+    None => Self::not_found(),
+  }
+}
+
+

Dependencies

+ My goal with this project was to provide a functional server using as few + libraries as possible. The final project directly depends on four crates: +
+

+// Snippet from Cargo.toml
+[dependencies]
+log = "0.4"
+log4rs = "1.3"
+tiny_http = "0.12"
+tokio = {version = "1", features = ["full"]}
+
+
+ The first two crates, log and log4rs, provide logging functions which aren't critical to the + server's functionality. The tiny_http crate provides a simple wrapper for the standard + library TCP stream, and performs SSL encryption. It is used as the backbone + for many Rust web frameworks. While I could implement these features myself, I + decided against it for security and browser compatibility reasons. Finally there is + tokio, an async runtime. This is necessary because Rust does not provide a runtime + by default, and building one is out of scope. +
\ No newline at end of file diff --git a/hyper-build/resources/CascadiaMonoPL.woff2 b/hyper-build/resources/CascadiaMonoPL.woff2 new file mode 100644 index 0000000..fc6ddd3 Binary files /dev/null and b/hyper-build/resources/CascadiaMonoPL.woff2 differ diff --git a/hyper-build/resources/award.svg b/hyper-build/resources/award.svg new file mode 100644 index 0000000..67c760b --- /dev/null +++ b/hyper-build/resources/award.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/hyper-build/resources/cloudflare-icon.svg b/hyper-build/resources/cloudflare-icon.svg new file mode 100644 index 0000000..d014bdc --- /dev/null +++ b/hyper-build/resources/cloudflare-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-build/resources/favicon.svg b/hyper-build/resources/favicon.svg new file mode 100644 index 0000000..739a2de --- /dev/null +++ b/hyper-build/resources/favicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/hyper-build/resources/github-icon.svg b/hyper-build/resources/github-icon.svg new file mode 100644 index 0000000..4eca062 --- /dev/null +++ b/hyper-build/resources/github-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-build/resources/godot-icon.svg b/hyper-build/resources/godot-icon.svg new file mode 100644 index 0000000..9eea112 --- /dev/null +++ b/hyper-build/resources/godot-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-build/resources/html5-icon.svg b/hyper-build/resources/html5-icon.svg new file mode 100644 index 0000000..c2dda3a --- /dev/null +++ b/hyper-build/resources/html5-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-build/resources/java-icon.svg b/hyper-build/resources/java-icon.svg new file mode 100644 index 0000000..d4bbd1c --- /dev/null +++ b/hyper-build/resources/java-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-build/resources/mona lisa.gif b/hyper-build/resources/mona lisa.gif new file mode 100644 index 0000000..f64ad45 Binary files /dev/null and b/hyper-build/resources/mona lisa.gif differ diff --git a/hyper-build/resources/python-icon.svg b/hyper-build/resources/python-icon.svg new file mode 100644 index 0000000..bde3773 --- /dev/null +++ b/hyper-build/resources/python-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-build/resources/rust-icon.svg b/hyper-build/resources/rust-icon.svg new file mode 100644 index 0000000..a1fe0d8 --- /dev/null +++ b/hyper-build/resources/rust-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-build/resources/starry night.gif b/hyper-build/resources/starry night.gif new file mode 100644 index 0000000..f452497 Binary files /dev/null and b/hyper-build/resources/starry night.gif differ diff --git a/hyper-build/resources/wanderer.gif b/hyper-build/resources/wanderer.gif new file mode 100644 index 0000000..cf3d824 Binary files /dev/null and b/hyper-build/resources/wanderer.gif differ diff --git a/hyper-build/resources/web-assembly-icon.svg b/hyper-build/resources/web-assembly-icon.svg new file mode 100644 index 0000000..64775a9 --- /dev/null +++ b/hyper-build/resources/web-assembly-icon.svg @@ -0,0 +1,5 @@ +web-assembly-icon \ No newline at end of file diff --git a/hyper-build/styles/extend.css b/hyper-build/styles/extend.css new file mode 100644 index 0000000..eedb55b --- /dev/null +++ b/hyper-build/styles/extend.css @@ -0,0 +1,231 @@ +@font-face { + font-family: Cascadia; + src: url("resources/CascadiaMonoPL.woff2"); +} + +body { + color: #444; + font-size: 1.2em; +} +.pure-img-responsive { max-width: 100%; height: auto; +} + +.cascadia { + font-family: Cascadia; +} + +.home-menu { + padding: 0.5em; + text-align: center; + box-shadow: 0 1px 1px rgba(0,0,0, 0.10); +} +.home-menu { + background: #333; +} + +.home-menu a { + color: #ccc; +} + +.home-menu li a:hover, +.home-menu li a:focus { + animation: menu-hover-anim 0.25s ease-out; + animation-fill-mode: forwards; + background: none; + border: none; +} + + +.link { + color: #00f; + text-decoration: none; +} + + +.video { + text-align: center; +} + +.video iframe { + width: 32em; + height: 18em; + frameborder: "0"; +} + +/* +Add transition to containers so they can push in and out. +*/ +#layout, +#menu, +.menu-link { + -webkit-transition: all 0.2s ease-out; + -moz-transition: all 0.2s ease-out; + -ms-transition: all 0.2s ease-out; + -o-transition: all 0.2s ease-out; + transition: all 0.2s ease-out; +} + +/* +This is the parent `
` that contains the menu and the content area. +*/ +#layout { + position: relative; + left: 0; + padding-left: 0; +} + #layout.active #menu { + left: 150px; + width: 150px; + } + + #layout.active .menu-link { + left: 150px; + } +/* +The content `
` is where all your content goes. +*/ +.content { + margin: 0 auto; + padding: 0 2em; + max-width: 800px; + margin-bottom: 50px; + line-height: 1.6em; +} + +.header { + margin: 0; + color: #444; + text-align: center; + padding: 2.5em 4em 0; + border-bottom: 1px solid #eee; + } + .header h1 { + margin: 0.2em 0; + font-size: 2.5em; + font-weight: bold; + } + .header h2 { + font-weight: 300; + color: #888; + padding: 0; + margin-top: 0; + } + +.distinct { + background-color: #444; + color: #fff; + padding: 0.5em 0.5em 0.5em 0.5em; +} + +@keyframes card-hover-anim { + to {background-color: #ccf;} +} +@keyframes card-text-hover-anim { + to {color: #222;} +} + +.card { + background-color: #eee; + padding: 0.5em 0.5em 0.5em 0.5em; + margin: 1em 0em 1em 0em; + text-decoration: none; +} + + .card h1 { + color: #444; + } + .card h2 { + color: #777; + } + .card h3 { + color: #999; + } + .card a { + text-decoration: none; + display: block; + } + + .card:hover { + animation: card-hover-anim 0.25s ease-out; + animation-fill-mode: forwards; + } + + + /* .card:hover h3 { */ + /* animation: card-text-hover-anim 0.25s ease-out; */ + /* animation-fill-mode: forwards; */ + /* } */ + + .card img { + vertical-align: bottom; + float: right; + padding: 0em 0.1em 0em 0.1em; + width: 60px; + } + .card svg { + vertical-align: bottom; + float: right; + padding: 0em 0.1em 0em 0.1em; + width: 60px; + } + + +.content-subhead { + margin: 50px 0 20px 0; + font-weight: 300; + color: #444; +} + + + +.code-block { + background-color: #fdf6e3; + color: #002b36; + font-family: Cascadia; + padding: 0em 0.5em 0em 0.5em; + margin: 1em 0em 1em 0em; + page-break-inside: avoid; + display: block; + overflow: auto; + word-wrap: break-word; + max-width: 100%; +} +.code-block pre { + display: block; + margin: 0 0 0 0; + padding: 0 0 0 0; +} + +.code-block pre code { + font-family: Cascadia; + display: block; + white-space: pre-wrap; +} +.code-block span { + background-color: #fdf6e3; +} + .keyword { + color: #6c71c4; + } + .constant { + color: #cb4b16; + } + .function { + color: #268bd2; + } + .operator { + color: #6c71c4; + } + .punctuation { + color: #586e75; + } + .string { + color: #859900; + } + .type { + color: #2aa198; + } + .property { + color: #586e75; + } + diff --git a/hyper-build/styles/main.css b/hyper-build/styles/main.css new file mode 100644 index 0000000..acdc431 --- /dev/null +++ b/hyper-build/styles/main.css @@ -0,0 +1,11 @@ +/*! +Pure v3.0.0 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/pure-css/pure/blob/master/LICENSE +*/ +/*! +normalize.css v | MIT License | https://necolas.github.io/normalize.css/ +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{display:flex;flex-flow:row wrap;align-content:flex-start}.pure-u{display:inline-block;vertical-align:top}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;user-select:none;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-0.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent;cursor:default}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} \ No newline at end of file diff --git a/hyper-src/home.html b/hyper-src/home.html new file mode 100644 index 0000000..47e61dc --- /dev/null +++ b/hyper-src/home.html @@ -0,0 +1,101 @@ + +
+

+

Completed Projects

+
+ + +
+ + + +

HTTP Server

+

Back End - TCP - SSL

+

+ Currently serving you this website +

+
+ + + +

HTML Templating Engine

+

Front End - Parser Design

+

+ Used to create this website +

+
+ + + +

Forte Assembly Language

+

Programming Language - Hackathon

+

+ Radically different machine code. A creative-coding endeavor +

+
+ + + +

Fishbowl

+

Image Encoding - Hardware Rendering

+

Kinematic image processing with GPU acceleration

+
+ + + +

Math Interpreter

+

Parser Design

+

+ Interpret and evaluate plain-text math expressions +

+
+ + +

nd-range

+

Vector Math - Standard Library

+

+ An extension of Rust's 'Range' type + using the Cartesian Product Algorithm +

+
+ + +

Fractal Explorer

+

Parallel Algorithms - Optimization - Hackathon

+

+ A Mandelbrot Fractal viewer using CPU parallelism + and the derivative bail algorithm +

+
+ + +

Pokédex

+

TKinter - Web APIs - Native UI

+

+ A TKinter app for viewing the original Pokédex, with + stats scraped from online sources +

+
+ + +

Stock Trading A.I.

+

Command Line App - Web APIs

+

+ A simple heuristic trading algorithm +

+
+
+
diff --git a/hyper-src/html-templating.html b/hyper-src/html-templating.html new file mode 100644 index 0000000..1396234 --- /dev/null +++ b/hyper-src/html-templating.html @@ -0,0 +1,90 @@ + +
+

Rust HTML Templating Engine

+

Front End - Parser Design

+
+
+

Motivation

+ HTML is a powerful tool for creating static web pages, but + is not easily modular or scalable. Front end frameworks like + React provide reusable components. I want to use components + without the overhead of a front-end framework. I want the final + output to be statically generated rather than generated on the fly + by the server or the client, minified and with inlined CSS and + JavaScript. +

Approach

+ After looking at some of the HTML parsing libraries, I was + unsatisfied with the approaches most developers took. The DOM + is represented as a tree structure, where each node keeps a + pointer to its children. +<@code lang="rust"> +// From the html_parser crate +pub enum Node { + Text(String), + Element(Element), + Comment(String), +} + +pub struct Element { + pub name: String, + pub attributes: HashMap>, + pub children: Vec, + // other fields omitted +} + + This is bad for cache locality and effectively forces the use + of recursion in order to traverse the DOM. Recursion is undesirable + for performance and robustness reasons. My approach is to create a + flat array of elements. +<@code lang="rust"> +// My implementation (fields omitted) +pub enum HtmlElement { + DocType, + Comment(/* */), + OpenTag { /* */ }, + CloseTag { /* */ }, + Text( /* */ ), + Script { /* */ }, + Style { /* */ }, + Directive { /* */ }, +} + + This approach lends itself to linear iterative algorithms. An + element's children can be represented as a slice of the DOM array, + from the opening tag to its close tag. +

Templates

+ I define a "template" as a user defined element which expands into + a larger piece of HTML. Templates can pass on children and attributes + to the final output. Some special templates can perform other functions, + like inlining a CSS file or syntax highlighting a code block. +<@code lang="html"> + + + +

+ + <@children/> +

+

+ + + + <@children/> + +

+ + +
+ + +
+
diff --git a/hyper-src/http-server.html b/hyper-src/http-server.html new file mode 100644 index 0000000..5120d21 --- /dev/null +++ b/hyper-src/http-server.html @@ -0,0 +1,60 @@ + +
+

Rust HTTP Server

+

Keywords: Back End - TCP - SSL

+
+
+

Motivation

+ As a systems programmer, I want to understand the technologies + I use at a very low level. Modern web stacks abstract away the + process of handling TCP connections, serving SSL certificates, + and parsing HTTP requests. The best way to understand a technology + is to implement it from the ground up, I studied the HTTP 1.1 + specification and wrote a server to host my website. +

Architecture

+ At the most basic level, the server will translate the request URL + into a local file path, and respond with the file matching that path + if it exists. Request URLs are sanitized to prevent accessing files + outside of the server's directory. Two tables are used for rewriting + and routing URLs. This makes handling URLs predictable and robust. + Because I route all traffic through Cloudflare, I decided to use its + caching feature rather than implement a cache locally. +<@code lang="rust"> +// Snippet from the request handling code +pub async fn construct_response(&self, request_url: &str) -> Response { + let sanitized_url = rewrite_url(request_url); + if sanitized_url != request_url { + return Self::redirect(&sanitized_url); + } + + let routed_url = self.perform_routing(&sanitized_url).await; + if let Some(response) = + self.perform_redirecting(&routed_url).await { + return response; + } + match self.get_resource(&routed_url).await { + Some(bytes) => Response::from_data(bytes), + None => Self::not_found(), + } +} + +

Dependencies

+ My goal with this project was to provide a functional server using as few + libraries as possible. The final project directly depends on four crates: +<@code lang=toml> +// Snippet from Cargo.toml +[dependencies] +log = "0.4" +log4rs = "1.3" +tiny_http = "0.12" +tokio = {version = "1", features = ["full"]} + + The first two crates, log and log4rs, provide logging functions which aren't critical to the + server's functionality. The tiny_http crate provides a simple wrapper for the standard + library TCP stream, and performs SSL encryption. It is used as the backbone + for many Rust web frameworks. While I could implement these features myself, I + decided against it for security and browser compatibility reasons. Finally there is + tokio, an async runtime. This is necessary because Rust does not provide a runtime + by default, and building one is out of scope. +
+
diff --git a/hyper-src/resources/CascadiaMonoPL.woff2 b/hyper-src/resources/CascadiaMonoPL.woff2 new file mode 100644 index 0000000..fc6ddd3 Binary files /dev/null and b/hyper-src/resources/CascadiaMonoPL.woff2 differ diff --git a/hyper-src/resources/award.svg b/hyper-src/resources/award.svg new file mode 100644 index 0000000..67c760b --- /dev/null +++ b/hyper-src/resources/award.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/hyper-src/resources/cloudflare-icon.svg b/hyper-src/resources/cloudflare-icon.svg new file mode 100644 index 0000000..d014bdc --- /dev/null +++ b/hyper-src/resources/cloudflare-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-src/resources/favicon.svg b/hyper-src/resources/favicon.svg new file mode 100644 index 0000000..739a2de --- /dev/null +++ b/hyper-src/resources/favicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/hyper-src/resources/github-icon.svg b/hyper-src/resources/github-icon.svg new file mode 100644 index 0000000..4eca062 --- /dev/null +++ b/hyper-src/resources/github-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-src/resources/godot-icon.svg b/hyper-src/resources/godot-icon.svg new file mode 100644 index 0000000..9eea112 --- /dev/null +++ b/hyper-src/resources/godot-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-src/resources/html5-icon.svg b/hyper-src/resources/html5-icon.svg new file mode 100644 index 0000000..c2dda3a --- /dev/null +++ b/hyper-src/resources/html5-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-src/resources/java-icon.svg b/hyper-src/resources/java-icon.svg new file mode 100644 index 0000000..d4bbd1c --- /dev/null +++ b/hyper-src/resources/java-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-src/resources/mona lisa.gif b/hyper-src/resources/mona lisa.gif new file mode 100644 index 0000000..f64ad45 Binary files /dev/null and b/hyper-src/resources/mona lisa.gif differ diff --git a/hyper-src/resources/python-icon.svg b/hyper-src/resources/python-icon.svg new file mode 100644 index 0000000..bde3773 --- /dev/null +++ b/hyper-src/resources/python-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-src/resources/rust-icon.svg b/hyper-src/resources/rust-icon.svg new file mode 100644 index 0000000..a1fe0d8 --- /dev/null +++ b/hyper-src/resources/rust-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hyper-src/resources/starry night.gif b/hyper-src/resources/starry night.gif new file mode 100644 index 0000000..f452497 Binary files /dev/null and b/hyper-src/resources/starry night.gif differ diff --git a/hyper-src/resources/wanderer.gif b/hyper-src/resources/wanderer.gif new file mode 100644 index 0000000..cf3d824 Binary files /dev/null and b/hyper-src/resources/wanderer.gif differ diff --git a/hyper-src/resources/web-assembly-icon.svg b/hyper-src/resources/web-assembly-icon.svg new file mode 100644 index 0000000..64775a9 --- /dev/null +++ b/hyper-src/resources/web-assembly-icon.svg @@ -0,0 +1,5 @@ +web-assembly-icon \ No newline at end of file diff --git a/hyper-src/styles/extend.css b/hyper-src/styles/extend.css new file mode 100644 index 0000000..eedb55b --- /dev/null +++ b/hyper-src/styles/extend.css @@ -0,0 +1,231 @@ +@font-face { + font-family: Cascadia; + src: url("resources/CascadiaMonoPL.woff2"); +} + +body { + color: #444; + font-size: 1.2em; +} +.pure-img-responsive { max-width: 100%; height: auto; +} + +.cascadia { + font-family: Cascadia; +} + +.home-menu { + padding: 0.5em; + text-align: center; + box-shadow: 0 1px 1px rgba(0,0,0, 0.10); +} +.home-menu { + background: #333; +} + +.home-menu a { + color: #ccc; +} + +.home-menu li a:hover, +.home-menu li a:focus { + animation: menu-hover-anim 0.25s ease-out; + animation-fill-mode: forwards; + background: none; + border: none; +} + + +.link { + color: #00f; + text-decoration: none; +} + + +.video { + text-align: center; +} + +.video iframe { + width: 32em; + height: 18em; + frameborder: "0"; +} + +/* +Add transition to containers so they can push in and out. +*/ +#layout, +#menu, +.menu-link { + -webkit-transition: all 0.2s ease-out; + -moz-transition: all 0.2s ease-out; + -ms-transition: all 0.2s ease-out; + -o-transition: all 0.2s ease-out; + transition: all 0.2s ease-out; +} + +/* +This is the parent `
` that contains the menu and the content area. +*/ +#layout { + position: relative; + left: 0; + padding-left: 0; +} + #layout.active #menu { + left: 150px; + width: 150px; + } + + #layout.active .menu-link { + left: 150px; + } +/* +The content `
` is where all your content goes. +*/ +.content { + margin: 0 auto; + padding: 0 2em; + max-width: 800px; + margin-bottom: 50px; + line-height: 1.6em; +} + +.header { + margin: 0; + color: #444; + text-align: center; + padding: 2.5em 4em 0; + border-bottom: 1px solid #eee; + } + .header h1 { + margin: 0.2em 0; + font-size: 2.5em; + font-weight: bold; + } + .header h2 { + font-weight: 300; + color: #888; + padding: 0; + margin-top: 0; + } + +.distinct { + background-color: #444; + color: #fff; + padding: 0.5em 0.5em 0.5em 0.5em; +} + +@keyframes card-hover-anim { + to {background-color: #ccf;} +} +@keyframes card-text-hover-anim { + to {color: #222;} +} + +.card { + background-color: #eee; + padding: 0.5em 0.5em 0.5em 0.5em; + margin: 1em 0em 1em 0em; + text-decoration: none; +} + + .card h1 { + color: #444; + } + .card h2 { + color: #777; + } + .card h3 { + color: #999; + } + .card a { + text-decoration: none; + display: block; + } + + .card:hover { + animation: card-hover-anim 0.25s ease-out; + animation-fill-mode: forwards; + } + + + /* .card:hover h3 { */ + /* animation: card-text-hover-anim 0.25s ease-out; */ + /* animation-fill-mode: forwards; */ + /* } */ + + .card img { + vertical-align: bottom; + float: right; + padding: 0em 0.1em 0em 0.1em; + width: 60px; + } + .card svg { + vertical-align: bottom; + float: right; + padding: 0em 0.1em 0em 0.1em; + width: 60px; + } + + +.content-subhead { + margin: 50px 0 20px 0; + font-weight: 300; + color: #444; +} + + + +.code-block { + background-color: #fdf6e3; + color: #002b36; + font-family: Cascadia; + padding: 0em 0.5em 0em 0.5em; + margin: 1em 0em 1em 0em; + page-break-inside: avoid; + display: block; + overflow: auto; + word-wrap: break-word; + max-width: 100%; +} +.code-block pre { + display: block; + margin: 0 0 0 0; + padding: 0 0 0 0; +} + +.code-block pre code { + font-family: Cascadia; + display: block; + white-space: pre-wrap; +} +.code-block span { + background-color: #fdf6e3; +} + .keyword { + color: #6c71c4; + } + .constant { + color: #cb4b16; + } + .function { + color: #268bd2; + } + .operator { + color: #6c71c4; + } + .punctuation { + color: #586e75; + } + .string { + color: #859900; + } + .type { + color: #2aa198; + } + .property { + color: #586e75; + } + diff --git a/hyper-src/styles/main.css b/hyper-src/styles/main.css new file mode 100644 index 0000000..acdc431 --- /dev/null +++ b/hyper-src/styles/main.css @@ -0,0 +1,11 @@ +/*! +Pure v3.0.0 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/pure-css/pure/blob/master/LICENSE +*/ +/*! +normalize.css v | MIT License | https://necolas.github.io/normalize.css/ +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{display:flex;flex-flow:row wrap;align-content:flex-start}.pure-u{display:inline-block;vertical-align:top}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;user-select:none;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-0.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent;cursor:default}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index e69de29..0000000 diff --git a/lg.toml b/lg.toml deleted file mode 100644 index cf6430a..0000000 --- a/lg.toml +++ /dev/null @@ -1,5 +0,0 @@ -[compile] -output="dist" -templates="tp" -source="src" -minify=false diff --git a/simple.html b/simple.html deleted file mode 100644 index 19ddcad..0000000 --- a/simple.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - asdf - - - diff --git a/src/compile.rs b/src/compile.rs new file mode 100644 index 0000000..1b0cdef --- /dev/null +++ b/src/compile.rs @@ -0,0 +1,354 @@ +use crate::trace::*; +use std::{collections::HashMap, io::Write, path::Path, rc::Rc}; + +use crate::parse::*; + +type Lexeme = HtmlElement; + +pub const RECURSION_LIMIT: usize = 256; + +#[derive(Debug, Clone)] +pub struct Element { + attributes: Attributes, + child_span: Vec, +} +pub type Templates = HashMap; + +fn parse_element<'a>(tail: &'a [Lexeme]) -> Option<(Element, &'a [Lexeme])> { + // Parse first element + let (_, attributes) = match tail.first() { + Some(lm) => match lm { + Lexeme::OpenTag { + attributes, + is_empty: true, + .. + } => { + return Some(( + Element { + attributes: attributes.clone(), + child_span: vec![], + }, + &tail[1..], + )) + }, + Lexeme::OpenTag { + name, + attributes, + is_empty: false, + } => (name, attributes), + _ => return None, + }, + None => return None, + }; + + let mut depth = 0; + let mut end = 1; + + for lm in tail { + match lm { + Lexeme::OpenTag { is_empty, .. } => { + if !is_empty { + depth += 1; + } + }, + Lexeme::CloseTag { .. } => { + depth -= 1; + if depth == 0 { + break; + } + }, + _ => { + if depth == 0 { + return None; + } + }, + } + end += 1; + } + Some(( + Element { + attributes: attributes.clone(), + child_span: tail[1..end - 1].to_vec(), + }, + &tail[end..], + )) +} + +pub fn parse_templates(source: impl AsRef) -> Result { + let mut tail: &[Lexeme] = &parse_html(source.as_ref())?; + let mut new_templates: Templates = Default::default(); + while let Some(lm) = tail.first() { + match lm { + Lexeme::OpenTag { + name, + is_empty: false, + .. + } => { + let (head, new_tail) = parse_element(tail) + .ctx(format!("at template definition {}", name))?; + + new_templates.insert(name.clone(), head); + tail = new_tail; + }, + // Ignore comments + Lexeme::Comment(_) => { + tail = &tail[1..]; + }, + _ => { + eprintln!("[WARNING] Unexpected root node: {}", lm.serialize()); + tail = &tail[1..]; + }, + } + } + Ok(new_templates) +} + +pub fn parse_templates_file(path: impl AsRef) -> Result { + let file = read_file(path.as_ref()).ctx(format!("opening templates file"))?; + parse_templates(file).ctx(format!("in file {}", path.as_ref().display())) +} + +fn expand_template( + base: &Element, + template: &Element, + output: &mut Vec, +) { + for element in &template.child_span { + match element { + Lexeme::OpenTag { + name, + is_empty, + attributes, + } => { + let mut new_attributes = HashMap::new(); + + for (key, value) in attributes { + if let Some(at_key) = value.strip_prefix('@') { + if let Some(at_value) = base.attributes.get(at_key) { + new_attributes.insert(at_key.to_string(), at_value.clone()); + } + } else { + new_attributes.insert(key.into(), value.into()); + } + } + + // for (key, value) in attributes { + // // If value is non-null + // if let Some(id) = value { + // // If value is '@' prefixed + // if let Some(variable_name) = id.strip_prefix('@') { + // // If key exists in base + // if let Some(new_value) = + // base.attributes.get(variable_name) { + // new_attributes.insert(key.clone(), + // new_value.clone()); } + // // Otherwise discard variable + // } + // // Insert non-null non-variable value + // else { + // new_attributes.insert(key.clone(), value.clone()); + // } + // } else { + // // Insert null non-variable value + // new_attributes.insert(key.clone(), value.clone()); + // } + // } + + output.push(Lexeme::OpenTag { + name: name.clone(), + attributes: new_attributes, + is_empty: *is_empty, + }); + }, + Lexeme::Directive { name, .. } => { + if name == "children" { + output.append(&mut base.child_span.to_vec()); + } else { + output.push(element.clone()); + } + }, + _ => output.push(element.clone()), + } + } +} + +fn compilation_pass( + mut source: &[Lexeme], + templates: &Templates, +) -> Result<(Vec, usize)> { + let mut output: Vec = vec![]; + let mut num_expanded = 0; + while let Some(lm) = source.first() { + match lm { + Lexeme::OpenTag { name, .. } => { + if let Some(tmp) = templates.get(name) { + num_expanded += 1; + let (base, new_tail) = + parse_element(source).ctx(format!("at template usage {}", name))?; + expand_template(&base, tmp, &mut output); + source = new_tail; + continue; + } else { + output.push(lm.clone()); + } + }, + _ => output.push(lm.clone()), + } + source = &source[1..]; + } + Ok((output, num_expanded)) +} + +pub fn compile_source( + source: impl AsRef, + templates: &Templates, +) -> Result> { + let mut source: Vec = parse_html(source.as_ref())?; + for i in 1..=RECURSION_LIMIT { + let (new_source, num_expanded) = compilation_pass(&source, templates)?; + source = new_source; + if num_expanded == 0 { + break; + } + if source.len() >= LEXEME_MEMORY_LIMIT { + return Err(compile_error("reached memory limit expanding templates")); + } + if i == RECURSION_LIMIT { + return Err(compile_error("reached recursion limit expanding templates")); + } + } + Ok(source.to_vec()) +} + +pub fn compile_source_file( + path: impl AsRef, + templates: &Templates, +) -> Result> { + let file = read_file(&path)?; + compile_source(file, templates) + .ctx(format!("while compiling file {}", path.as_ref().display())) +} + +pub fn serialize(output: &Vec) -> String { + output + .iter() + .map(|lm| lm.serialize()) + .collect::>() + .join("") +} + +pub fn serialize_mini(output: &Vec) -> String { + output + .iter() + .map(|lm| lm.serialize()) + .collect::>() + .join("") +} + +pub struct Compiler { + templates: HashMap, +} + +impl Compiler { + pub fn new() -> Self { + Self { + templates: Default::default(), + } + } + + pub fn with_template_file( + &mut self, + path: impl AsRef, + ) -> Result<&mut Self> { + let templates = parse_templates_file(path)?; + self.templates.extend(templates); + Ok(self) + } + + pub fn with_src( + &mut self, + from: impl AsRef, + to: impl AsRef, + ) -> Result<&mut Self> { + let source = compile_source_file(from, &self.templates)?; + let serial = serialize(&source); + let mut new_file = std::fs::File::create(to)?; + new_file.write_all(serial.as_bytes())?; + Ok(self) + } + + pub fn with_template_folder( + &mut self, + from: impl AsRef, + ) -> Result<&mut Self> { + for file in std::fs::read_dir(&from)? { + let file = + file.ctx(format!("reading directory {}", from.as_ref().display()))?; + let path = file.path(); + let last = path.components().last().ctx("empty path encountered")?; + let ft = match file.file_type() { + Ok(ft) => ft, + Err(_) => continue, + }; + if ft.is_dir() { + self.with_template_folder(&from.as_ref().join(&last))?; + continue; + } + let ext = match path.extension() { + Some(ext) => ext, + None => continue, + }; + + if ext != "html" { + continue; + } + self.with_template_file(&path)?; + } + Ok(self) + } + + pub fn with_src_folder( + &mut self, + from: impl AsRef, + to: impl AsRef, + ) -> Result<&mut Self> { + let _ = std::fs::remove_dir_all(&to); + let _ = std::fs::create_dir_all(&to); + for file in std::fs::read_dir(&from)? { + let file = + file.ctx(format!("reading directory {}", from.as_ref().display()))?; + let path = file.path(); + let last = path.components().last().ctx("empty path encountered")?; + let destination = to.as_ref().join(&last); + let ft = match file.file_type() { + Ok(ft) => ft, + Err(_) => continue, + }; + if ft.is_dir() { + self.with_src_folder( + &from.as_ref().join(&last), + &to.as_ref().join(&last), + )?; + continue; + } + let ext = match path.extension() { + Some(ext) => ext, + None => continue, + }; + + // Copy file but do not compile + if ext != "html" { + let file = std::fs::read(&path) + .ctx(format!("opening file to read: {}", path.display()))?; + let mut new_file = std::fs::File::create(&destination) + .ctx(format!("opening file to write: {}", destination.display()))?; + new_file + .write_all(&file) + .ctx(format!("writing to file: {}", destination.display()))?; + continue; + } + self.with_src(&path, destination)?; + } + Ok(self) + } +} diff --git a/src/compiler.rs b/src/compiler.rs deleted file mode 100644 index 89adb6e..0000000 --- a/src/compiler.rs +++ /dev/null @@ -1,278 +0,0 @@ -use html_parser::{Dom, DomVariant, Element, ElementVariant, Node}; -use std::{collections::HashMap, path::Path}; - -use crate::{ - macros::*, - trace::{WithContext, *}, -}; - -/// Replaces @variables in template with appropriate -/// values, or removes if not provided -fn init_vars( - node: Node, - attributes: &HashMap>, - children: &Vec, -) -> Node { - let mut element = match node { - Node::Text(_) | Node::Comment(_) => return node, - Node::Element(e) => e, - }; - - let mut new_classes = Vec::with_capacity(element.classes.len()); - - for cls in &mut element.classes { - if !cls.starts_with('@') { - new_classes.push(cls.clone()); - continue; - } - let name = &cls[1..]; - // Attributes in class must not be null - if let Some(Some(k)) = attributes.get(name) { - new_classes.push(k.clone()); - } - // Otherwise discard variable from classes - } - element.classes = new_classes; - - // Attributes in ID must not be null - if let Some(id) = &element.id { - if let Some(name) = id.strip_prefix('@') { - element.id = attributes.get(name).unwrap_or(&None).clone(); - } else { - element.id = None; - } - } - - let mut new_attributes = HashMap::new(); - - for (key, value) in &element.attributes { - if let Some(id) = value { - if let Some(variable_name) = id.strip_prefix('@') { - if let Some(value) = attributes.get(variable_name) { - // Insert null and non-null variables if key exists - new_attributes.insert(key.clone(), value.clone()); - } - // Otherwise discard variable - } - // Insert non-null non-variable value - else { - new_attributes.insert(key.clone(), value.clone()); - } - } else { - // Insert null non-variable value - new_attributes.insert(key.clone(), value.clone()); - } - } - - element.attributes = new_attributes; - let mut new_children = Vec::with_capacity(element.children.len()); - for child in &element.children { - let name = child.element().map(|c| c.name.as_str()).unwrap_or(""); - if "tp:children" == name { - for child in children { - new_children.push(child.clone()); - } - } else { - let child = init_vars(child.clone(), attributes, children); - new_children.push(child); - } - } - - element.children = new_children; - - Node::Element(element) -} - -/// Swaps out a template invocation for its definition -fn expand_templates( - invocation: Element, - templates: &Element, -) -> Result> { - for child in &invocation.children { - if child - .element() - .map(|c| c.name.as_str().strip_prefix("tp:").is_some()) - .unwrap_or(false) - { - return Err(Error::new( - ErrorKind::Compilation, - "Illegal use of tp namespace", - )); - } - for subchild in child { - if subchild - .element() - .map(|c| c.name.as_str().strip_prefix("tp:").is_some()) - .unwrap_or(false) - { - return Err(Error::new( - ErrorKind::Compilation, - "Illegal use of tp namespace", - )); - } - } - } - // Collect params - let mut attributes = invocation.attributes; - let classes = invocation.classes.join(" "); - if classes.len() != 0 { - attributes.insert("class".into(), Some(classes)); - } - if let Some(id) = invocation.id { - attributes.insert("id".into(), Some(id)); - } - // Swap params - let expanded = init_vars( - Node::Element(templates.clone()), - &attributes, - &invocation.children, - ); - Ok(expanded.element().unwrap().children.clone()) -} - -/// Serializes HTML node -/// * `node` - Node to serialize -/// * `templates` - Map of templates by their names -fn node_to_string( - node: &Node, - templates: &HashMap, - macros: &HashMap, -) -> Result { - const OPEN: bool = false; - const CLOSE: bool = true; - let mut stack: Vec<(Node, bool)> = vec![(node.clone(), OPEN)]; - let mut buf = String::new(); - while let Some((current, closing)) = stack.pop() { - if closing == OPEN { - stack.push((current.clone(), CLOSE)); - match current { - Node::Text(t) => { - buf.push_str(&t); - }, - Node::Element(e) => { - // Expand if macro - if let Some(mc) = macros.get(&e.name) { - stack.pop().unwrap(); - let expanded = - mc(&e.attributes).ctx(format!("Expanding macro: {}", e.name))?; - buf.push_str(&expanded); - continue; - } - // Expand if template - if let Some(tp) = templates.get(&e.name) { - let _ = stack.pop(); - let elements = expand_templates(e.clone(), tp) - .ctx(format!("Expanding template: {}", e.name))?; - for element in elements.into_iter().rev() { - stack.push((element, OPEN)); - } - continue; - } - // < - buf.push('<'); - // Tag name - buf.push_str(&e.name); - // Classes - if !e.classes.is_empty() { - buf.push_str(&format!(" class='{}'", e.classes.join(" "))); - } - // ID - if let Some(id) = &e.id { - buf.push_str(&format!(" id='{}'", id)); - } - // Attributes - for (k, v) in &e.attributes { - match v { - Some(v) => buf.push_str(&format!(" {}='{}'", k, v)), - None => buf.push_str(&format!(" {}", k)), - } - } - match e.variant { - ElementVariant::Normal => { - buf.push_str(">\n"); - for child in e.children.iter().rev() { - stack.push((child.clone(), OPEN)); - } - }, - ElementVariant::Void => { - buf.push_str("/>\n"); - let _ = stack.pop(); - }, - } - }, - Node::Comment(_) => {}, - } - } else { - match current { - Node::Text(_) => buf.push('\n'), - Node::Comment(_) => {}, - Node::Element(e) => { - buf.push_str(&format!("\n", e.name)); - }, - } - } - } - Ok(buf) -} - -pub struct Compiler { - templates: HashMap, - macros: HashMap, -} - -impl Compiler { - pub fn new() -> Self { - Self { - templates: HashMap::new(), - macros: default_macros(), - } - } - - pub fn parse_templates_file(&mut self, path: impl AsRef) -> Result<()> { - let file = read_file(&path)?; - self.parse_templates(&file).ctx(format!( - "Parsing templates file: {}", - path.as_ref().to_string_lossy() - ))?; - Ok(()) - } - - pub fn parse_templates(&mut self, html: &str) -> Result<()> { - let tps: HashMap = Dom::parse(html)? - .children - .into_iter() - .map(|i| i.element().cloned()) - .flatten() - .map(|i| (i.name.clone(), i)) - .collect(); - if tps.is_empty() { - return Err(Error::new(ErrorKind::Parsing, "No blueprints found")); - } - self.templates.extend(tps); - Ok(()) - } - - pub fn compile_source_file(&self, path: impl AsRef) -> Result { - let file = read_file(&path)?; - self.compile_source(&file).ctx(format!( - "Compiling source file: {}", - path.as_ref().to_string_lossy() - )) - } - - pub fn compile_source(&self, html: &str) -> Result { - let dom = Dom::parse(html)?; - if !dom.errors.is_empty() {} - if dom.tree_type == DomVariant::Empty { - return Err(Error::new(ErrorKind::Parsing, "Empty DOM")); - } - if dom.tree_type != DomVariant::Document { - return Err(Error::new( - ErrorKind::Parsing, - "DOM must exactly have 1 root node", - )); - } - let tree = &dom.children[0]; - node_to_string(&tree, &self.templates, &self.macros) - } -} diff --git a/src/directives.rs b/src/directives.rs new file mode 100644 index 0000000..fef427e --- /dev/null +++ b/src/directives.rs @@ -0,0 +1,106 @@ +use crate::parse::Attributes; + +pub fn expand_directive( + name: &str, + attributes: &Attributes, + contents: &str, +) -> String { + match name { + "style" => style_dir(attributes).unwrap_or("".to_string()), + "script" => script_dir(attributes).unwrap_or("".to_string()), + "code" => code(attributes, contents).unwrap_or("".to_string()), + _ => "".to_string(), + } +} + +fn style_dir(attributes: &Attributes) -> Option { + let path = attributes.get("href")?; + let file = std::fs::read_to_string(path).ok()?; + Some(format!("", file.trim())) +} + +fn script_dir(attributes: &Attributes) -> Option { + let path = attributes.get("href")?; + let file = std::fs::read_to_string(path).ok()?; + Some(format!("", file.trim())) +} + +fn code(attributes: &Attributes, contents: &str) -> Option { + use inkjet::*; + let minimum_indent = contents + .trim() + .lines() + .map(|line| line.len() - line.trim_start().len()) + .min() + .unwrap_or(0); + + let normalized_string: String = contents + .lines() + .map(|line| line.replacen(" ", "", minimum_indent)) + .map(|line| line + "\n") + .collect(); + + let language = parse_language(attributes.get("lang")?)?; + + let mut hl = Highlighter::new(); + let buffer = hl + .highlight_to_string(language, &MyFormatter(), normalized_string) + .ok()?; + + let buffer = format!( + "
\n
{}
\n
", + buffer + ); + + Some(buffer) +} + +struct MyFormatter(); +use inkjet::{ + constants::HIGHLIGHT_CLASS_NAMES, formatter::*, + tree_sitter_highlight::HighlightEvent, +}; + +impl Formatter for MyFormatter { + fn write( + &self, + source: &str, + writer: &mut W, + event: inkjet::tree_sitter_highlight::HighlightEvent, + ) -> inkjet::Result<()> + where + W: std::fmt::Write, + { + match event { + HighlightEvent::Source { start, end } => { + let span = source + .get(start..end) + .expect("Source bounds should be in bounds!"); + let span = v_htmlescape::escape(span).to_string(); + writer.write_str(&span)?; + }, + HighlightEvent::HighlightStart(idx) => { + let name = HIGHLIGHT_CLASS_NAMES[idx.0]; + write!(writer, "", name)?; + }, + HighlightEvent::HighlightEnd => { + writer.write_str("")?; + }, + } + + Ok(()) + } +} + +fn parse_language(lang: &str) -> Option { + use inkjet::*; + Some(match lang { + "rust" => Language::Rust, + "python" => Language::Python, + "javascript" => Language::Javascript, + "html" => Language::Html, + "css" => Language::Css, + "toml" => Language::Toml, + _ => return None, + }) +} diff --git a/src/macros.rs b/src/macros.rs deleted file mode 100644 index 83b9447..0000000 --- a/src/macros.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::trace::*; -use std::collections::HashMap; - -pub fn default_macros() -> HashMap { - let mut hm: HashMap = HashMap::new(); - hm.insert("lg:include".into(), INCLUDE_MACRO); - hm -} - -pub type Macro = fn(&HashMap>) -> Result; - -const INCLUDE_MACRO: Macro = |input| { - let href = input - .get("href") - .ok_or(compile_error("'href' attribute missing"))? - .as_ref() - .ok_or(compile_error("'href' attribute is empty"))?; - - let rel = input - .get("rel") - .ok_or(compile_error("'rel' attribute missing"))? - .as_ref() - .ok_or(compile_error("'rel' attribute is empty"))?; - - let r = match rel.as_str() { - "css" => format!("\n", read_file(href)?), - "js" => format!("\n", read_file(href)?), - "inline" => read_file(href)?, - _ => return Err(compile_error(format!("Invalid 'rel' value: {}", rel))), - }; - Ok(r) -}; diff --git a/src/main.rs b/src/main.rs index 4c1a85f..d030a6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,81 +1,11 @@ -mod compiler; -mod macros; -mod parser; +mod compile; +mod directives; +mod parse; mod trace; -use compiler::*; -use trace::*; -fn run_compiler() -> Result<()> { - // Read config file - let default_config = include_str!("../default.toml") - .parse::() - .unwrap(); - let user_config = std::fs::read_to_string("lg.toml") - .ctx("Reading config file")? - .parse::() - .ctx("Parsing config file")?; - let mut config = default_config.clone(); - config.extend(user_config); - - let mut c = Compiler::new(); - let compile_table = config["compile"] - .as_table() - .ctx("Reading compile options")?; - - // Parse all templates - let templates_path = compile_table["templates"] - .as_str() - .ctx("Reading templates path")?; - let dirs = walkdir::WalkDir::new(templates_path) - .into_iter() - .flatten() - .filter_map(|v| { - if v.file_name().to_str().unwrap_or("").ends_with(".html") - && v.file_type().is_file() - { - Some(v) - } else { - None - } - }); - for file in dirs { - c.parse_templates_file(file.path())?; - } - // Compile all source files - let source_path = compile_table["source"] - .as_str() - .ctx("Reading templates path")?; - let dirs = walkdir::WalkDir::new(source_path) - .into_iter() - .flatten() - .filter_map(|v| { - if v.file_name().to_str().unwrap_or("").ends_with(".html") - && v.file_type().is_file() - { - Some(v) - } else { - None - } - }); - - for file in dirs {} - - // println!("{}", s); +fn main() -> Result<(), trace::Error> { + let mut c = compile::Compiler::new(); + c.with_template_folder("templates/")? + .with_src_folder("hyper-src/", "hyper-build/")?; Ok(()) } - -fn main() { - let test = include_str!("../simple.html").to_string(); - // let test = " ".to_string(); - let r = parser::parse_html(&test).unwrap(); - for l in r { - println!("{l:?}"); - } - // match run_compiler() { - // Ok(_) => {}, - // Err(e) => { - // eprintln!("{}", e); - // std::process::exit(1); - // }, - // } -} diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..dfc7f75 --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,497 @@ +use std::collections::HashMap; + +use crate::directives::expand_directive; + +pub type Attributes = HashMap; +pub type Offset = usize; +pub type Parse<'a, T> = (T, &'a str, Offset); +pub type MaybeParse<'a, T> = Option>; + +#[inline] +fn parse_until(i: &str, condition: impl Fn(char) -> bool) -> Parse<&str> { + match i.chars().position(condition) { + Some(pos) => (&i[..pos], &i[pos..], pos), + None => (&i, "", i.len()), + } +} + +fn parse_until_str<'a>( + tail: &'a str, + to_match: &'static str, +) -> MaybeParse<'a, &'a str> { + for i in 0..tail.len() { + let substr = &tail[0..i]; + if substr.ends_with(to_match) { + let end = i - to_match.len(); + return Some((&tail[0..end], &tail[end..], end)); + } + } + None +} + +#[inline] +fn parse_str<'a>(i: &'a str, matches: &str) -> MaybeParse<'a, &'a str> { + let length = matches.len(); + match i.get(0..matches.len()) { + Some(s) => { + if s.eq_ignore_ascii_case(matches) { + Some((s, &i[length..], length)) + } else { + None + } + }, + None => None, + } +} + +fn parse_char(i: &str, matches: char) -> MaybeParse { + if let Some(c) = i.chars().next() { + if c == matches { + return Some((c, &i[1..], 1)); + } + } + None +} + +fn parse_delimited(i: &str, delim: char) -> MaybeParse<&str> { + let (_start, i, o1) = parse_char(i, delim)?; + let (contents, i, o2) = parse_until(i, |c| c == delim); + let (_end, i, o3) = parse_char(i, delim)?; + Some((contents, i, o1 + o2 + o3)) +} + +fn index_to_rc(input: &str, index: usize) -> (usize, usize) { + let (mut row, mut col) = (1, 1); + for c in input[0..index].chars() { + if c == '\n' { + row += 1; + col = 1; + } else { + col += 1; + } + } + (row, col) +} + +pub const LEXEME_MEMORY_LIMIT: usize = 65535; + +// Tags that are implicitly self closing, ending in /> is +// optional +const VOID_ELEMENTS: [&str; 16] = [ + "area", "base", "br", "col", "command", "embed", "hr", "img", "input", + "keygen", "link", "meta", "param", "source", "track", "wbr", +]; + +#[derive(Clone, Debug)] +pub enum ErrorKind { + /// Encountered illegal sequence + Illegal, + /// Closing tag does not match previous opening tag + UnbalancedTags, + /// Tried to parse `N > 65535` elements + MemoryLimit, +} + +impl From for crate::trace::Error { + fn from(value: Error) -> Self { + Self { + kind: crate::trace::ErrorKind::Parsing, + reason: match value.kind { + ErrorKind::Illegal => "Illegal character encountered", + ErrorKind::UnbalancedTags => "Unbalanced open and close tags", + ErrorKind::MemoryLimit => "Ran out of memory", + } + .to_string(), + backtrace: vec![format!("at line {} column {}", value.row, value.column)], + } + } +} + +#[derive(Clone, Debug)] +pub struct Error { + pub kind: ErrorKind, + pub char_index: usize, + pub row: usize, + pub column: usize, +} + +#[derive(Clone, Debug)] +pub enum HtmlElement { + /// The required `` preamble + DocType, + /// Text inside of a `` + Comment(String), + /// Any opening tag, including tags + OpenTag { + /// The name of the tag + name: String, + /// Attribute names and their values if present + attributes: Attributes, + /// Whether the tag closes itself. Tags that are + /// implicitly empty are: + /// `area, base, br, col, command, embed, hr, img, input + /// keygen, link, meta, param, source, track, wbr` + is_empty: bool, + }, + /// Any closing tag that is not empty. Closing implicitly + /// empty tags is an error + CloseTag { name: String }, + /// Any inner text that is not entirely whitespace + Text(String), + /// A `", + serialize_attributes(attributes), + contents + ) + }, + Self::Text(t) => t.clone(), + Self::Directive { + name, + attributes, + contents, + } => expand_directive(&name, &attributes, &contents), + } + } +} + +/// Used as the `condition` argument for `parse_until` to +/// parse names of things +const NAME_REGEX: fn(char) -> bool = + |c| !(c.is_ascii_alphanumeric() || [':', '_', '-', '@'].contains(&c)); + +/// Used as the `condition` argument for `parse_until` to +/// arbitrary whitespace +const WS_REGEX: fn(char) -> bool = |c| !c.is_whitespace(); + +fn parse_doctype(i: &str) -> MaybeParse { + let (_, i, o1) = parse_str(i, "")?; + Some((HtmlElement::DocType, i, o1 + o2 + o3 + o4 + o5)) +} + +fn parse_comment<'a>(tail: &'a str) -> MaybeParse { + let (_, tail, o1) = parse_str(tail, "")?; + let (_, tail, o3) = parse_str(tail, "-->")?; + Some((HtmlElement::Comment(comment.into()), tail, o1 + o2 + o3)) +} + +fn parse_raw_text(i: &str) -> MaybeParse<(String, Attributes, String)> { + let (open, mut i, o1) = parse_open_tag(i)?; + let (open_name, attributes, is_empty) = match open { + HtmlElement::OpenTag { + name, + attributes, + is_empty, + } => (name, attributes, is_empty), + _ => unreachable!(), + }; + + let (close_name, contents, i, o2) = if is_empty { + (open_name.clone(), "".to_string(), i, 0) + } else { + let mut contents = String::new(); + let mut o2 = 0; + while !i.is_empty() { + let (text, new_i, new_off) = parse_text(i); + contents.push_str(&text); + o2 += new_off; + i = new_i; + if i.starts_with(" name, + _ => unreachable!(), + }; + (close_name, contents, i, o2 + o3) + }; + if open_name != close_name { + return None; + } + + Some(((open_name, attributes, contents), i, o1 + o2)) +} + +fn parse_style(i: &str) -> MaybeParse { + let ((name, attributes, contents), i, o) = parse_raw_text(i)?; + if name != "style" { + None + } else { + Some(( + HtmlElement::Style { + attributes, + contents, + }, + i, + o, + )) + } +} + +fn parse_script(i: &str) -> MaybeParse { + let ((name, attributes, contents), i, o) = parse_raw_text(i)?; + if name != "script" { + None + } else { + Some(( + HtmlElement::Script { + attributes, + contents, + }, + i, + o, + )) + } +} + +fn parse_directive(i: &str) -> MaybeParse { + let ((name, attributes, contents), i, o) = parse_raw_text(i)?; + if let Some(name) = name.strip_prefix('@') { + Some(( + HtmlElement::Directive { + name: name.to_string(), + attributes, + contents, + }, + i, + o, + )) + } else { + None + } +} + +fn parse_open_tag(i: &str) -> MaybeParse { + let (_, i, o1) = parse_str(i, "<")?; + let (name, i, o2) = parse_until(i, NAME_REGEX); + let mut attributes = HashMap::new(); + let mut i = i; + let mut o3 = 0; + while let Some(((key, value), new_i, new_o)) = parse_attribute(i) { + attributes.insert(key, value); + i = new_i; + o3 += new_o; + } + let (_, i, o4) = parse_until(i, WS_REGEX); + // Find all attributes + let (is_empty, i, o5) = match parse_str(i, "/") { + Some((_, i, o5)) => (true, i, o5), + None => (false, i, 0), + }; + + let is_empty = is_empty || VOID_ELEMENTS.contains(&name); + let (_, i, o6) = parse_char(i, '>')?; + Some(( + HtmlElement::OpenTag { + name: name.to_string(), + attributes, + is_empty, + }, + i, + o1 + o2 + o3 + o4 + o5 + o6, + )) +} + +fn parse_attribute(i: &str) -> MaybeParse<(String, String)> { + let (_, i, o1) = parse_until(i, WS_REGEX); + if o1 == 0 { + return None; + } + let (key, i, o2) = parse_until(i, NAME_REGEX); + if o2 == 0 { + return None; + } + let get_value = || -> Option<(&str, &str, Offset)> { + let (_, i, o1) = parse_until(i, WS_REGEX); + let (_, i, o2) = parse_str(i, "=")?; + let (_, i, o3) = parse_until(i, WS_REGEX); + let (value, i, o4) = parse_delimited(i, '\"') + .or_else(|| parse_delimited(i, '\'')) + .unwrap_or_else(|| parse_until(i, NAME_REGEX)); + Some((value, i, o1 + o2 + o3 + o4)) + }; + let (value, i, o3) = get_value().unwrap_or(("", i, 0)); + Some(((key.to_string(), value.to_string()), i, o1 + o2 + o3)) +} + +fn parse_close_tag(i: &str) -> MaybeParse { + let (_, i, o1) = parse_str(i, "")?; + Some(( + HtmlElement::CloseTag { + name: name.to_string(), + }, + i, + o1 + o2 + o3 + o4, + )) +} + +fn parse_text(i: &str) -> Parse { + let (text, i, o1) = parse_until(i, |c| c == '<'); + (text.to_string(), i, o1) +} + +pub fn parse_html( + input: &str, +) -> Result, crate::trace::Error> { + let mut output = vec![]; + let mut validation_stack = vec![]; + let mut i = input; + let mut offset = 0; + let throw_err = |kind, offset| { + let (row, column) = index_to_rc(input, offset); + Error { + kind, + char_index: offset, + row, + column, + } + .into() + }; + while i.len() > 0 { + let (lm, new_i, new_off) = if i.starts_with("", &mut new_index).ok_or(ParseError::Unknown)?; - *index += new_index; - Ok((Lexeme::Comment, tail)) -} - -fn parse_text<'a>( - tail: &'a str, - index: &mut usize, -) -> Result<(Lexeme<'a>, &'a str), ParseError> { - let mut new_index = 0; - let (txt, tail) = parse_while(tail, |c| c != '<', &mut new_index); - if txt.is_empty() { - Err(ParseError::Unknown) - } else { - *index += new_index; - Ok((Lexeme::Text(txt), tail)) - } -} - -fn or_keep_error<'a>( - r: Result<(Lexeme<'a>, &'a str), ParseError>, - op: impl FnOnce() -> Result<(Lexeme<'a>, &'a str), ParseError>, -) -> Result<(Lexeme<'a>, &'a str), ParseError> { - match r { - Ok(val) => Ok(val), - Err(e) => op().map_err(|new_e| match e { - ParseError::Unknown => new_e, - _ => e, - }), - } -} - -fn index_to_rc(input: &str, index: usize) -> (usize, usize) { - let (mut row, mut col) = (1, 1); - for c in input[0..index].chars() { - if c == '\n' { - row += 1; - col = 1; - } else { - col += 1; - } - } - (row, col) -} - -pub fn parse_html(input: &str) -> trace::Result> { - let mut tail = input; - let mut lexeme_stack = vec![]; - let mut validation_stack = vec![]; - let mut index = 0; - - let err = |error: ParseError, index: usize| -> trace::Result> { - let (row, col) = index_to_rc(input, index); - let e: trace::Error = error.into(); - Err(e).ctx(format!("Starting at line {} character {}", row, col)) - }; - - while !tail.is_empty() { - let (_, new_tail) = parse_whitespace(tail, &mut index); - if new_tail.is_empty() { - break; - } - let result = or_keep_error(parse_open_tag(new_tail, &mut index), || { - parse_close_tag(new_tail, &mut index) - }); - let result = or_keep_error(result, || parse_text(new_tail, &mut index)); - let result = or_keep_error(result, || parse_comment(new_tail, &mut index)); - let (lm, new_tail) = - match or_keep_error(result, || parse_doctype(new_tail, &mut index)) { - Ok(v) => v, - Err(e) => { - return err(e, index); - }, - }; - // Validate that open and close tags match - match lm { - Lexeme::OpenTag { name, is_void, .. } => { - if !is_void { - validation_stack.push(name); - } - }, - Lexeme::CloseTag { name } => { - if VOID_ELEMENTS.contains(&name) { - return err(ParseError::VoidClosingTag(name.into()).into(), index); - } - if let Some(top) = validation_stack.pop() { - if name != top { - return err( - ParseError::MismatchedClosing { - expected: top.into(), - found: name.into(), - }, - index, - ); - } - } else { - return err(ParseError::UnmatchedClose(name.into()), index); - } - }, - Lexeme::Comment => { - tail = new_tail; - continue; - }, - _ => {}, - }; - - lexeme_stack.push(lm); - - tail = new_tail; - } - if let Some(top) = validation_stack.pop() { - let e: trace::Error = ParseError::UnmatchedOpen(top.into()).into(); - return Err(e).ctx("At end of file"); - } - Ok(lexeme_stack) -} diff --git a/src/trace.rs b/src/trace.rs index f12566f..7eaad72 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -41,6 +41,7 @@ pub enum ErrorKind { IO, Parsing, Compilation, + Memory, Unknown, } @@ -54,6 +55,7 @@ impl Display for ErrorKind { ErrorKind::Parsing => "PARSING", ErrorKind::Compilation => "COMPILATION", ErrorKind::Unknown => "UNKNOWN", + ErrorKind::Memory => "MEMORY", } ) } @@ -85,39 +87,6 @@ impl From for Error { } } -impl From for Error { - fn from(value: html_parser::Error) -> Self { - match value { - html_parser::Error::Parsing(e) => Self { - kind: ErrorKind::Parsing, - reason: e, - backtrace: vec![], - }, - html_parser::Error::Cli(e) => Self { - kind: ErrorKind::Unknown, - reason: e, - backtrace: vec![], - }, - html_parser::Error::IO(e) => Self { - kind: ErrorKind::IO, - reason: format!("{}", e), - backtrace: vec![], - }, - html_parser::Error::Serde(e) => Self { - kind: ErrorKind::Unknown, - reason: format!("{}", e), - backtrace: vec![], - }, - } - } -} - -impl From for Error { - fn from(value: toml::de::Error) -> Self { - Error::new(ErrorKind::Parsing, value.message()) - } -} - pub trait WithContext> { fn ctx(self, s: S) -> Result; } diff --git a/style.css b/style.css deleted file mode 100644 index fdd4e5b..0000000 --- a/style.css +++ /dev/null @@ -1,8 +0,0 @@ -body { - background-color: linen; -} - -h1 { - color: maroon; - margin-left: 40px; -} diff --git a/templates/blueprints.html b/templates/blueprints.html new file mode 100644 index 0000000..a7e4965 --- /dev/null +++ b/templates/blueprints.html @@ -0,0 +1,183 @@ + + + + + + + + + <@style rel="stylesheet" href="input/styles/main.css" /> + <@style rel="stylesheet" href="input/styles/extend.css" /> + + + +
+ +
+ <@children /> +
+
+ + + +
+ + + + + + + + +
+

Page does not exist

+

Error 404

+ +
+
+ + +
+ +
+
+ + + + <@children /> + + + + + + + + + @alt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test.html b/test.html deleted file mode 100644 index 5348d7d..0000000 --- a/test.html +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - - -
-
-
- -
-
-
-
- - - - - - - - - - - - - - - - - -
HEX00
DEC0
OCT000
BIN0000 0000
- - - - - - - -
BYTEWORDDWORDQWORD
-
-
- - - - - - - - - - - -
00000000
-
-
-
-
-
-
- -
-
- - -