` 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 @@
+
+
+
+
+
+
+
+
+ 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 @@
+
+
+
+
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
+}
+@code>
+ 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 { /* */ },
+}
+@code>
+ 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/>
+
+
+
+@code>
+
+
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 @@
+
+
+
+
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(),
+ }
+}
+@code>
+ 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"]}
+@code>
+ 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!(
+ "",
+ 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("") {
+ if let Some((HtmlElement::CloseTag { name }, _, _)) = parse_close_tag(i)
+ {
+ if name == open_name {
+ break;
+ }
+ }
+ }
+ if text.is_empty() {
+ i = &i[1..];
+ o2 += 1;
+ contents.push('<');
+ }
+ }
+ let (close, i, o3) = parse_close_tag(i)?;
+ let close_name = match close {
+ HtmlElement::CloseTag { name } => 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, "")?;
+ let (name, i, o2) = parse_until(i, NAME_REGEX);
+ if o2 == 0 {
+ return None;
+ }
+ let (_, i, o3) = parse_until(i, WS_REGEX);
+ let (_, i, o4) = 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" />
+
+
+
+
+
+
+
+
+
+
+
+
+ Return Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <@children />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test.html b/test.html
deleted file mode 100644
index 5348d7d..0000000
--- a/test.html
+++ /dev/null
@@ -1,90 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- HEX
- 00
-
-
- DEC
- 0
-
-
- OCT
- 000
-
-
- BIN
- 0000 0000
-
-
-
-
- BYTE
- WORD
- DWORD
- QWORD
-
-
-
-
-
-
- 0
- 0
- 0
- 0
- 0
- 0
- 0
- 0
-
-
-
-
-
- Remember Clear
- history
-
-
-
-
-
-
-
-