Reorganize
This commit is contained in:
20
crates/service/service-webpage/Cargo.toml
Normal file
20
crates/service/service-webpage/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "service-webpage"
|
||||
version = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
libservice = { workspace = true }
|
||||
macro-assets = { workspace = true }
|
||||
macro-sass = { workspace = true }
|
||||
assetserver = { workspace = true }
|
||||
|
||||
axum = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
utoipa = { workspace = true }
|
||||
maud = { workspace = true }
|
||||
markdown = { workspace = true }
|
||||
BIN
crates/service/service-webpage/assets/fonts/fa/fa-brands-400.ttf
Normal file
BIN
crates/service/service-webpage/assets/fonts/fa/fa-brands-400.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
crates/service/service-webpage/assets/fonts/fa/fa-solid-900.ttf
Normal file
BIN
crates/service/service-webpage/assets/fonts/fa/fa-solid-900.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
crates/service/service-webpage/assets/images/betalupi-map.png
Normal file
BIN
crates/service/service-webpage/assets/images/betalupi-map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 284 KiB |
BIN
crates/service/service-webpage/assets/images/cover-small.jpg
Normal file
BIN
crates/service/service-webpage/assets/images/cover-small.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
BIN
crates/service/service-webpage/assets/images/icon.png
Normal file
BIN
crates/service/service-webpage/assets/images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
144
crates/service/service-webpage/css/blocks.scss
Normal file
144
crates/service/service-webpage/css/blocks.scss
Normal file
@@ -0,0 +1,144 @@
|
||||
ul {
|
||||
list-style: none;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
li p {
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
/* Bonus space after last li */
|
||||
li:last-child {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
ul li::marker {
|
||||
content: "> ";
|
||||
color: var(--metaColor);
|
||||
}
|
||||
|
||||
ul li:hover::marker {
|
||||
content: ">> ";
|
||||
font-weight: 1000;
|
||||
color: var(--linkColor);
|
||||
transition: 100ms;
|
||||
}
|
||||
|
||||
|
||||
.titleList li {
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
|
||||
blockquote {
|
||||
border-left: .5rem solid var(--metaColor);
|
||||
margin: 1rem;
|
||||
padding: 0 0 0 1rem
|
||||
}
|
||||
|
||||
|
||||
textarea {
|
||||
border: 2px dotted;
|
||||
outline: 0;
|
||||
resize: none;
|
||||
overflow: auto;
|
||||
background-color: var(--bgColor)
|
||||
}
|
||||
|
||||
|
||||
|
||||
pre .wrap {
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
pre :not(.wrap) {
|
||||
padding: 1rem;
|
||||
font-style: monospace;
|
||||
white-space: pre;
|
||||
overflow: scroll;
|
||||
display: block;
|
||||
border: solid .2rem transparent;
|
||||
transition: 150ms;
|
||||
counter-reset: line;
|
||||
}
|
||||
|
||||
pre:hover {
|
||||
border: solid .2rem var(--lightBgColor);
|
||||
}
|
||||
|
||||
pre span:first-of-type:before {
|
||||
border-top: 1px dashed var(--lightBgColor);
|
||||
}
|
||||
|
||||
pre span:before {
|
||||
counter-increment: line;
|
||||
content: counter(line, decimal-leading-zero);
|
||||
display: inline-block;
|
||||
border-right: 1px solid var(--lightBgColor);
|
||||
border-bottom: 1px dashed var(--lightBgColor);
|
||||
padding: 0 .5em;
|
||||
margin-right: .5em;
|
||||
color: #888
|
||||
}
|
||||
|
||||
p code,
|
||||
li code,
|
||||
div code {
|
||||
padding: 0 .2rem 0 .2rem;
|
||||
border-radius: .3rem;
|
||||
color: var(--codeFgColor);
|
||||
background-color: var(--codeBgColor);
|
||||
}
|
||||
|
||||
pre code {
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
color: inherit;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: none;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.1
|
||||
}
|
||||
|
||||
thead th:first-child {
|
||||
width: 20%
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
text-align: left
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 400
|
||||
}
|
||||
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: .5rem;
|
||||
border: dashed .1rem var(--metaColor)
|
||||
}
|
||||
|
||||
.metaData,
|
||||
hr,
|
||||
textarea {
|
||||
color: var(--metaColor)
|
||||
}
|
||||
152
crates/service/service-webpage/css/fontawesome/_animated.scss
vendored
Normal file
152
crates/service/service-webpage/css/fontawesome/_animated.scss
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
// animating icons
|
||||
// --------------------------
|
||||
|
||||
.#{$fa-css-prefix}-beat {
|
||||
animation-name: #{$fa-css-prefix}-beat;
|
||||
animation-delay: var(--#{$fa-css-prefix}-animation-delay, 0s);
|
||||
animation-direction: var(--#{$fa-css-prefix}-animation-direction, normal);
|
||||
animation-duration: var(--#{$fa-css-prefix}-animation-duration, 1s);
|
||||
animation-iteration-count: var(--#{$fa-css-prefix}-animation-iteration-count, infinite);
|
||||
animation-timing-function: var(--#{$fa-css-prefix}-animation-timing, ease-in-out);
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-bounce {
|
||||
animation-name: #{$fa-css-prefix}-bounce;
|
||||
animation-delay: var(--#{$fa-css-prefix}-animation-delay, 0s);
|
||||
animation-direction: var(--#{$fa-css-prefix}-animation-direction, normal);
|
||||
animation-duration: var(--#{$fa-css-prefix}-animation-duration, 1s);
|
||||
animation-iteration-count: var(--#{$fa-css-prefix}-animation-iteration-count, infinite);
|
||||
animation-timing-function: var(--#{$fa-css-prefix}-animation-timing, cubic-bezier(0.280, 0.840, 0.420, 1));
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-fade {
|
||||
animation-name: #{$fa-css-prefix}-fade;
|
||||
animation-delay: var(--#{$fa-css-prefix}-animation-delay, 0s);
|
||||
animation-direction: var(--#{$fa-css-prefix}-animation-direction, normal);
|
||||
animation-duration: var(--#{$fa-css-prefix}-animation-duration, 1s);
|
||||
animation-iteration-count: var(--#{$fa-css-prefix}-animation-iteration-count, infinite);
|
||||
animation-timing-function: var(--#{$fa-css-prefix}-animation-timing, cubic-bezier(.4,0,.6,1));
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-beat-fade {
|
||||
animation-name: #{$fa-css-prefix}-beat-fade;
|
||||
animation-delay: var(--#{$fa-css-prefix}-animation-delay, 0s);
|
||||
animation-direction: var(--#{$fa-css-prefix}-animation-direction, normal);
|
||||
animation-duration: var(--#{$fa-css-prefix}-animation-duration, 1s);
|
||||
animation-iteration-count: var(--#{$fa-css-prefix}-animation-iteration-count, infinite);
|
||||
animation-timing-function: var(--#{$fa-css-prefix}-animation-timing, cubic-bezier(.4,0,.6,1));
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-flip {
|
||||
animation-name: #{$fa-css-prefix}-flip;
|
||||
animation-delay: var(--#{$fa-css-prefix}-animation-delay, 0s);
|
||||
animation-direction: var(--#{$fa-css-prefix}-animation-direction, normal);
|
||||
animation-duration: var(--#{$fa-css-prefix}-animation-duration, 1s);
|
||||
animation-iteration-count: var(--#{$fa-css-prefix}-animation-iteration-count, infinite);
|
||||
animation-timing-function: var(--#{$fa-css-prefix}-animation-timing, ease-in-out);
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-shake {
|
||||
animation-name: #{$fa-css-prefix}-shake;
|
||||
animation-delay: var(--#{$fa-css-prefix}-animation-delay, 0s);
|
||||
animation-direction: var(--#{$fa-css-prefix}-animation-direction, normal);
|
||||
animation-duration: var(--#{$fa-css-prefix}-animation-duration, 1s);
|
||||
animation-iteration-count: var(--#{$fa-css-prefix}-animation-iteration-count, infinite);
|
||||
animation-timing-function: var(--#{$fa-css-prefix}-animation-timing, linear);
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-spin {
|
||||
animation-name: #{$fa-css-prefix}-spin;
|
||||
animation-delay: var(--#{$fa-css-prefix}-animation-delay, 0s);
|
||||
animation-direction: var(--#{$fa-css-prefix}-animation-direction, normal);
|
||||
animation-duration: var(--#{$fa-css-prefix}-animation-duration, 2s);
|
||||
animation-iteration-count: var(--#{$fa-css-prefix}-animation-iteration-count, infinite);
|
||||
animation-timing-function: var(--#{$fa-css-prefix}-animation-timing, linear);
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-spin-reverse {
|
||||
--#{$fa-css-prefix}-animation-direction: reverse;
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-pulse,
|
||||
.#{$fa-css-prefix}-spin-pulse {
|
||||
animation-name: #{$fa-css-prefix}-spin;
|
||||
animation-direction: var(--#{$fa-css-prefix}-animation-direction, normal);
|
||||
animation-duration: var(--#{$fa-css-prefix}-animation-duration, 1s);
|
||||
animation-iteration-count: var(--#{$fa-css-prefix}-animation-iteration-count, infinite);
|
||||
animation-timing-function: var(--#{$fa-css-prefix}-animation-timing, steps(8));
|
||||
}
|
||||
|
||||
// if agent or operating system prefers reduced motion, disable animations
|
||||
// see: https://www.smashingmagazine.com/2020/09/design-reduced-motion-sensitivities/
|
||||
// see: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.#{$fa-css-prefix}-beat,
|
||||
.#{$fa-css-prefix}-bounce,
|
||||
.#{$fa-css-prefix}-fade,
|
||||
.#{$fa-css-prefix}-beat-fade,
|
||||
.#{$fa-css-prefix}-flip,
|
||||
.#{$fa-css-prefix}-pulse,
|
||||
.#{$fa-css-prefix}-shake,
|
||||
.#{$fa-css-prefix}-spin,
|
||||
.#{$fa-css-prefix}-spin-pulse {
|
||||
animation-delay: -1ms;
|
||||
animation-duration: 1ms;
|
||||
animation-iteration-count: 1;
|
||||
transition-delay: 0s;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes #{$fa-css-prefix}-beat {
|
||||
0%, 90% { transform: scale(1); }
|
||||
45% { transform: scale(var(--#{$fa-css-prefix}-beat-scale, 1.25)); }
|
||||
}
|
||||
|
||||
@keyframes #{$fa-css-prefix}-bounce {
|
||||
0% { transform: scale(1,1) translateY(0); }
|
||||
10% { transform: scale(var(--#{$fa-css-prefix}-bounce-start-scale-x, 1.1),var(--#{$fa-css-prefix}-bounce-start-scale-y, 0.9)) translateY(0); }
|
||||
30% { transform: scale(var(--#{$fa-css-prefix}-bounce-jump-scale-x, 0.9),var(--#{$fa-css-prefix}-bounce-jump-scale-y, 1.1)) translateY(var(--#{$fa-css-prefix}-bounce-height, -0.5em)); }
|
||||
50% { transform: scale(var(--#{$fa-css-prefix}-bounce-land-scale-x, 1.05),var(--#{$fa-css-prefix}-bounce-land-scale-y, 0.95)) translateY(0); }
|
||||
57% { transform: scale(1,1) translateY(var(--#{$fa-css-prefix}-bounce-rebound, -0.125em)); }
|
||||
64% { transform: scale(1,1) translateY(0); }
|
||||
100% { transform: scale(1,1) translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes #{$fa-css-prefix}-fade {
|
||||
50% { opacity: var(--#{$fa-css-prefix}-fade-opacity, 0.4); }
|
||||
}
|
||||
|
||||
@keyframes #{$fa-css-prefix}-beat-fade {
|
||||
0%, 100% {
|
||||
opacity: var(--#{$fa-css-prefix}-beat-fade-opacity, 0.4);
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(var(--#{$fa-css-prefix}-beat-fade-scale, 1.125));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes #{$fa-css-prefix}-flip {
|
||||
50% {
|
||||
transform: rotate3d(var(--#{$fa-css-prefix}-flip-x, 0), var(--#{$fa-css-prefix}-flip-y, 1), var(--#{$fa-css-prefix}-flip-z, 0), var(--#{$fa-css-prefix}-flip-angle, -180deg));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes #{$fa-css-prefix}-shake {
|
||||
0% { transform: rotate(-15deg); }
|
||||
4% { transform: rotate(15deg); }
|
||||
8%, 24% { transform: rotate(-18deg); }
|
||||
12%, 28% { transform: rotate(18deg); }
|
||||
16% { transform: rotate(-22deg); }
|
||||
20% { transform: rotate(22deg); }
|
||||
32% { transform: rotate(-12deg); }
|
||||
36% { transform: rotate(12deg); }
|
||||
40%, 100% { transform: rotate(0deg); }
|
||||
}
|
||||
|
||||
@keyframes #{$fa-css-prefix}-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
20
crates/service/service-webpage/css/fontawesome/_bordered-pulled.scss
vendored
Normal file
20
crates/service/service-webpage/css/fontawesome/_bordered-pulled.scss
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
// bordered + pulled icons
|
||||
// -------------------------
|
||||
|
||||
.#{$fa-css-prefix}-border {
|
||||
border-color: var(--#{$fa-css-prefix}-border-color, #{$fa-border-color});
|
||||
border-radius: var(--#{$fa-css-prefix}-border-radius, #{$fa-border-radius});
|
||||
border-style: var(--#{$fa-css-prefix}-border-style, #{$fa-border-style});
|
||||
border-width: var(--#{$fa-css-prefix}-border-width, #{$fa-border-width});
|
||||
padding: var(--#{$fa-css-prefix}-border-padding, #{$fa-border-padding});
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-pull-left {
|
||||
float: left;
|
||||
margin-right: var(--#{$fa-css-prefix}-pull-margin, #{$fa-pull-margin});
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-pull-right {
|
||||
float: right;
|
||||
margin-left: var(--#{$fa-css-prefix}-pull-margin, #{$fa-pull-margin});
|
||||
}
|
||||
43
crates/service/service-webpage/css/fontawesome/_core.scss
vendored
Normal file
43
crates/service/service-webpage/css/fontawesome/_core.scss
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
// base icon class definition
|
||||
// -------------------------
|
||||
|
||||
.#{$fa-css-prefix} {
|
||||
font-family: var(--#{$fa-css-prefix}-style-family, '#{$fa-style-family}');
|
||||
font-weight: var(--#{$fa-css-prefix}-style, #{$fa-style});
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix},
|
||||
.#{$fa-css-prefix}-classic,
|
||||
.#{$fa-css-prefix}-sharp,
|
||||
.fas,
|
||||
.#{$fa-css-prefix}-solid,
|
||||
.far,
|
||||
.#{$fa-css-prefix}-regular,
|
||||
.fab,
|
||||
.#{$fa-css-prefix}-brands {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: var(--#{$fa-css-prefix}-display, #{$fa-display});
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
line-height: 1;
|
||||
text-rendering: auto;
|
||||
}
|
||||
|
||||
.fas,
|
||||
.#{$fa-css-prefix}-classic,
|
||||
.#{$fa-css-prefix}-solid,
|
||||
.far,
|
||||
.#{$fa-css-prefix}-regular {
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
}
|
||||
|
||||
.fab,
|
||||
.#{$fa-css-prefix}-brands {
|
||||
font-family: 'Font Awesome 6 Brands';
|
||||
}
|
||||
|
||||
|
||||
%fa-icon {
|
||||
@include fa-icon;
|
||||
}
|
||||
7
crates/service/service-webpage/css/fontawesome/_fixed-width.scss
vendored
Normal file
7
crates/service/service-webpage/css/fontawesome/_fixed-width.scss
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// fixed-width icons
|
||||
// -------------------------
|
||||
|
||||
.#{$fa-css-prefix}-fw {
|
||||
text-align: center;
|
||||
width: $fa-fw-width;
|
||||
}
|
||||
57
crates/service/service-webpage/css/fontawesome/_functions.scss
vendored
Normal file
57
crates/service/service-webpage/css/fontawesome/_functions.scss
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
// functions
|
||||
// --------------------------
|
||||
|
||||
// fa-content: convenience function used to set content property
|
||||
@function fa-content($fa-var) {
|
||||
@return unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
|
||||
// fa-divide: Originally obtained from the Bootstrap https://github.com/twbs/bootstrap
|
||||
//
|
||||
// Licensed under: The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2011-2021 Twitter, Inc.
|
||||
// Copyright (c) 2011-2021 The Bootstrap Authors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
@function fa-divide($dividend, $divisor, $precision: 10) {
|
||||
$sign: if($dividend > 0 and $divisor > 0, 1, -1);
|
||||
$dividend: abs($dividend);
|
||||
$divisor: abs($divisor);
|
||||
$quotient: 0;
|
||||
$remainder: $dividend;
|
||||
@if $dividend == 0 {
|
||||
@return 0;
|
||||
}
|
||||
@if $divisor == 0 {
|
||||
@error "Cannot divide by 0";
|
||||
}
|
||||
@if $divisor == 1 {
|
||||
@return $dividend;
|
||||
}
|
||||
@while $remainder >= $divisor {
|
||||
$quotient: $quotient + 1;
|
||||
$remainder: $remainder - $divisor;
|
||||
}
|
||||
@if $remainder > 0 and $precision > 0 {
|
||||
$remainder: fa-divide($remainder * 10, $divisor, $precision - 1) * .1;
|
||||
}
|
||||
@return ($quotient + $remainder) * $sign;
|
||||
}
|
||||
10
crates/service/service-webpage/css/fontawesome/_icons.scss
vendored
Normal file
10
crates/service/service-webpage/css/fontawesome/_icons.scss
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
// specific icon class definition
|
||||
// -------------------------
|
||||
|
||||
/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
|
||||
readers do not read off random characters that represent icons */
|
||||
|
||||
@each $name, $icon in $fa-icons {
|
||||
.#{$fa-css-prefix}-#{$name}::before { content: unquote("\"#{ $icon }\""); }
|
||||
}
|
||||
|
||||
18
crates/service/service-webpage/css/fontawesome/_list.scss
vendored
Normal file
18
crates/service/service-webpage/css/fontawesome/_list.scss
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
// icons in a list
|
||||
// -------------------------
|
||||
|
||||
.#{$fa-css-prefix}-ul {
|
||||
list-style-type: none;
|
||||
margin-left: var(--#{$fa-css-prefix}-li-margin, #{$fa-li-margin});
|
||||
padding-left: 0;
|
||||
|
||||
> li { position: relative; }
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-li {
|
||||
left: calc(var(--#{$fa-css-prefix}-li-width, #{$fa-li-width}) * -1);
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: var(--#{$fa-css-prefix}-li-width, #{$fa-li-width});
|
||||
line-height: inherit;
|
||||
}
|
||||
72
crates/service/service-webpage/css/fontawesome/_mixins.scss
vendored
Normal file
72
crates/service/service-webpage/css/fontawesome/_mixins.scss
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
// mixins
|
||||
// --------------------------
|
||||
|
||||
// base rendering for an icon
|
||||
@mixin fa-icon {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
font-weight: normal;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
// sets relative font-sizing and alignment (in _sizing)
|
||||
@mixin fa-size ($font-size) {
|
||||
font-size: fa-divide($font-size, $fa-size-scale-base) * 1em; // converts step in sizing scale into an em-based value that's relative to the scale's base
|
||||
line-height: fa-divide(1, $font-size) * 1em; // sets the line-height of the icon back to that of it's parent
|
||||
vertical-align: (fa-divide(6, $font-size) - fa-divide(3, 8)) * 1em; // vertically centers the icon taking into account the surrounding text's descender
|
||||
}
|
||||
|
||||
// only display content to screen readers
|
||||
// see: https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/
|
||||
// see: https://hugogiraudel.com/2016/10/13/css-hide-and-seek/
|
||||
@mixin fa-sr-only() {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
// use in conjunction with .sr-only to only display content when it's focused
|
||||
@mixin fa-sr-only-focusable() {
|
||||
&:not(:focus) {
|
||||
@include fa-sr-only();
|
||||
}
|
||||
}
|
||||
|
||||
// sets a specific icon family to use alongside style + icon mixins
|
||||
|
||||
// convenience mixins for declaring pseudo-elements by CSS variable,
|
||||
// including all style-specific font properties, and both the ::before
|
||||
// and ::after elements in the duotone case.
|
||||
@mixin fa-icon-solid($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-solid;
|
||||
|
||||
&::before {
|
||||
content: unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
}
|
||||
@mixin fa-icon-regular($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-regular;
|
||||
|
||||
&::before {
|
||||
content: unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
}
|
||||
@mixin fa-icon-brands($fa-var) {
|
||||
@extend %fa-icon;
|
||||
@extend .fa-brands;
|
||||
|
||||
&::before {
|
||||
content: unquote("\"#{ $fa-var }\"");
|
||||
}
|
||||
}
|
||||
31
crates/service/service-webpage/css/fontawesome/_rotated-flipped.scss
vendored
Normal file
31
crates/service/service-webpage/css/fontawesome/_rotated-flipped.scss
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
// rotating + flipping icons
|
||||
// -------------------------
|
||||
|
||||
.#{$fa-css-prefix}-rotate-90 {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-rotate-180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-rotate-270 {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-flip-horizontal {
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-flip-vertical {
|
||||
transform: scale(1, -1);
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-flip-both,
|
||||
.#{$fa-css-prefix}-flip-horizontal.#{$fa-css-prefix}-flip-vertical {
|
||||
transform: scale(-1, -1);
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-rotate-by {
|
||||
transform: rotate(var(--#{$fa-css-prefix}-rotate-angle, 0));
|
||||
}
|
||||
14
crates/service/service-webpage/css/fontawesome/_screen-reader.scss
vendored
Normal file
14
crates/service/service-webpage/css/fontawesome/_screen-reader.scss
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
// screen-reader utilities
|
||||
// -------------------------
|
||||
|
||||
// only display content to screen readers
|
||||
.sr-only,
|
||||
.#{$fa-css-prefix}-sr-only {
|
||||
@include fa-sr-only;
|
||||
}
|
||||
|
||||
// use in conjunction with .sr-only to only display content when it's focused
|
||||
.sr-only-focusable,
|
||||
.#{$fa-css-prefix}-sr-only-focusable {
|
||||
@include fa-sr-only-focusable;
|
||||
}
|
||||
1578
crates/service/service-webpage/css/fontawesome/_shims.scss
vendored
Normal file
1578
crates/service/service-webpage/css/fontawesome/_shims.scss
vendored
Normal file
File diff suppressed because it is too large
Load Diff
16
crates/service/service-webpage/css/fontawesome/_sizing.scss
vendored
Normal file
16
crates/service/service-webpage/css/fontawesome/_sizing.scss
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
// sizing icons
|
||||
// -------------------------
|
||||
|
||||
// literal magnification scale
|
||||
@for $i from 1 through 10 {
|
||||
.#{$fa-css-prefix}-#{$i}x {
|
||||
font-size: $i * 1em;
|
||||
}
|
||||
}
|
||||
|
||||
// step-based scale (with alignment)
|
||||
@each $size, $value in $fa-sizes {
|
||||
.#{$fa-css-prefix}-#{$size} {
|
||||
@include fa-size($value);
|
||||
}
|
||||
}
|
||||
32
crates/service/service-webpage/css/fontawesome/_stacked.scss
vendored
Normal file
32
crates/service/service-webpage/css/fontawesome/_stacked.scss
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
// stacking icons
|
||||
// -------------------------
|
||||
|
||||
.#{$fa-css-prefix}-stack {
|
||||
display: inline-block;
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
position: relative;
|
||||
vertical-align: $fa-stack-vertical-align;
|
||||
width: $fa-stack-width;
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-stack-1x,
|
||||
.#{$fa-css-prefix}-stack-2x {
|
||||
left: 0;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
z-index: var(--#{$fa-css-prefix}-stack-z-index, #{$fa-stack-z-index});
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-stack-1x {
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-stack-2x {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.#{$fa-css-prefix}-inverse {
|
||||
color: var(--#{$fa-css-prefix}-inverse, #{$fa-inverse});
|
||||
}
|
||||
5015
crates/service/service-webpage/css/fontawesome/_variables.scss
vendored
Normal file
5015
crates/service/service-webpage/css/fontawesome/_variables.scss
vendored
Normal file
File diff suppressed because it is too large
Load Diff
30
crates/service/service-webpage/css/fontawesome/brands.scss
vendored
Normal file
30
crates/service/service-webpage/css/fontawesome/brands.scss
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2024 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
:root, :host {
|
||||
--#{$fa-css-prefix}-style-family-brands: 'Font Awesome 6 Brands';
|
||||
--#{$fa-css-prefix}-font-brands: normal 400 1em/1 'Font Awesome 6 Brands';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Brands';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: $fa-font-display;
|
||||
src: url('#{$fa-font-path}/fa-brands-400.woff2') format('woff2'),
|
||||
url('#{$fa-font-path}/fa-brands-400.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.fab,
|
||||
.#{$fa-css-prefix}-brands {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@each $name, $icon in $fa-brand-icons {
|
||||
.#{$fa-css-prefix}-#{$name}:before { content: unquote("\"#{ $icon }\""); }
|
||||
}
|
||||
21
crates/service/service-webpage/css/fontawesome/fontawesome.scss
vendored
Normal file
21
crates/service/service-webpage/css/fontawesome/fontawesome.scss
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2024 Fonticons, Inc.
|
||||
*/
|
||||
// Font Awesome core compile (Web Fonts-based)
|
||||
// -------------------------
|
||||
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
@import 'mixins';
|
||||
@import 'core';
|
||||
@import 'sizing';
|
||||
@import 'fixed-width';
|
||||
@import 'list';
|
||||
@import 'bordered-pulled';
|
||||
@import 'animated';
|
||||
@import 'rotated-flipped';
|
||||
@import 'stacked';
|
||||
@import 'icons';
|
||||
@import 'screen-reader';
|
||||
26
crates/service/service-webpage/css/fontawesome/regular.scss
vendored
Normal file
26
crates/service/service-webpage/css/fontawesome/regular.scss
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2024 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
:root, :host {
|
||||
--#{$fa-css-prefix}-style-family-classic: '#{ $fa-style-family }';
|
||||
--#{$fa-css-prefix}-font-regular: normal 400 1em/1 '#{ $fa-style-family }';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: $fa-font-display;
|
||||
src: url('#{$fa-font-path}/fa-regular-400.woff2') format('woff2'),
|
||||
url('#{$fa-font-path}/fa-regular-400.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.far,
|
||||
.#{$fa-css-prefix}-regular {
|
||||
font-weight: 400;
|
||||
}
|
||||
26
crates/service/service-webpage/css/fontawesome/solid.scss
vendored
Normal file
26
crates/service/service-webpage/css/fontawesome/solid.scss
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2024 Fonticons, Inc.
|
||||
*/
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
|
||||
:root, :host {
|
||||
--#{$fa-css-prefix}-style-family-classic: '#{ $fa-style-family }';
|
||||
--#{$fa-css-prefix}-font-solid: normal 900 1em/1 '#{ $fa-style-family }';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: $fa-font-display;
|
||||
src: url('#{$fa-font-path}/fa-solid-900.woff2') format('woff2'),
|
||||
url('#{$fa-font-path}/fa-solid-900.ttf') format('truetype');
|
||||
}
|
||||
|
||||
.fas,
|
||||
.#{$fa-css-prefix}-solid {
|
||||
font-weight: 900;
|
||||
}
|
||||
11
crates/service/service-webpage/css/fontawesome/v4-shims.scss
vendored
Normal file
11
crates/service/service-webpage/css/fontawesome/v4-shims.scss
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2024 Fonticons, Inc.
|
||||
*/
|
||||
// V4 shims compile (Web Fonts-based)
|
||||
// -------------------------
|
||||
|
||||
@import 'functions';
|
||||
@import 'variables';
|
||||
@import 'shims';
|
||||
31
crates/service/service-webpage/css/images.scss
Normal file
31
crates/service/service-webpage/css/images.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
img {
|
||||
max-width: 90%;
|
||||
height: auto;
|
||||
margin: .2rem;
|
||||
padding: .2rem;
|
||||
border-radius: 15px;
|
||||
border: solid .2rem transparent;
|
||||
transition: 150ms;
|
||||
}
|
||||
|
||||
img:hover {
|
||||
border: solid .2rem var(--metaColor);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
|
||||
.icons {
|
||||
width: 2.0rem;
|
||||
height: 2.0rem;
|
||||
aspect-ratio: 1/1;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
color: var(--fgColor);
|
||||
fill: var(--fgColor);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.icons__background:hover {
|
||||
background-color: transparent;
|
||||
color: var(--metaColor);
|
||||
}
|
||||
119
crates/service/service-webpage/css/main.scss
Normal file
119
crates/service/service-webpage/css/main.scss
Normal file
@@ -0,0 +1,119 @@
|
||||
@import "text";
|
||||
@import "blocks";
|
||||
@import "images";
|
||||
@import "special";
|
||||
|
||||
@import "fontawesome/fontawesome";
|
||||
@import "fontawesome/brands";
|
||||
@import "fontawesome/regular";
|
||||
@import "fontawesome/solid";
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Light.woff2") format("woff2");
|
||||
font-weight: light;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Medium.woff2") format("woff2");
|
||||
font-weight: medium;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira";
|
||||
src: url("/assets/fonts/FiraCode-Regular.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
:root {
|
||||
// Misc colors
|
||||
--bgColor: #121212;
|
||||
--lightBgColor: #3a3f46;
|
||||
--fgColor: #ebebeb;
|
||||
--metaColor: #6199bb;
|
||||
--lightMetaColor: #638c86;
|
||||
--linkColor: #e4dab3;
|
||||
--codeBgColor: var(--lightBgColor);
|
||||
--codeFgColor: var(--fgColor);
|
||||
|
||||
// Main colors
|
||||
--grey: #696969;
|
||||
|
||||
// Accent colors, used only manally
|
||||
--green: #a2c579;
|
||||
--magenta: #ad79c5;
|
||||
--orange: #e86a33;
|
||||
--yellow: #e8bc00;
|
||||
--pink: #fa9f83;
|
||||
}
|
||||
|
||||
::selection,
|
||||
::-moz-selection {
|
||||
color: var(--bgColor);
|
||||
background: var(--metaColor);
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
font-size: 62.5%;
|
||||
scrollbar-color: var(--metaColor) var(--bgColor);
|
||||
scrollbar-width: auto;
|
||||
background: var(--bgColor);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Fira";
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.35;
|
||||
max-width: 64rem;
|
||||
margin: auto;
|
||||
overflow-wrap: break-word;
|
||||
background: var(--bgColor);
|
||||
color: var(--fgColor);
|
||||
}
|
||||
|
||||
main {
|
||||
margin-top: 2ex;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
hr.footline {
|
||||
border: 1pt solid;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1pt dashed;
|
||||
}
|
||||
|
||||
iframe {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.footContainer {
|
||||
padding-top: 0;
|
||||
padding-bottom: 1em;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
.wrapper {
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
93
crates/service/service-webpage/css/special.scss
Normal file
93
crates/service/service-webpage/css/special.scss
Normal file
@@ -0,0 +1,93 @@
|
||||
// Handout list pages
|
||||
// works with "{{ handout() }}" shortcode.
|
||||
|
||||
.handout-li-links {
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.handout-li-links a {
|
||||
@extend a;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 1.5pt;
|
||||
padding-left: 1ex;
|
||||
padding-right: 1ex;
|
||||
}
|
||||
|
||||
.handout-ul li:hover {
|
||||
margin-left: 1ex;
|
||||
transition: 50ms;
|
||||
}
|
||||
|
||||
.handout-ul li {
|
||||
transition: 50ms;
|
||||
transition-delay: 50ms;
|
||||
}
|
||||
|
||||
|
||||
.handout-ul li:hover .handout-li-links,
|
||||
.handout-ul .handout-li-desc {
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
transition: 100ms;
|
||||
}
|
||||
|
||||
.handout-ul li:hover .handout-li-desc,
|
||||
.handout-ul li .handout-li-links {
|
||||
transition-delay: 50ms;
|
||||
transition: 100ms;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.handout-star {
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
// Email obfuscation
|
||||
// Works with "{{ email_*() }}" shortcodes.
|
||||
.eobf {
|
||||
@extend a;
|
||||
}
|
||||
|
||||
.eobf:hover {
|
||||
@extend a, :hover;
|
||||
}
|
||||
|
||||
// Change icon on hover
|
||||
.eobf #eobf-kb,
|
||||
.eobf:hover #eobf-en {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.eobf #eobf-en,
|
||||
.eobf:hover #eobf-kb {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
// Hover text
|
||||
.eobf:hover span:before {
|
||||
unicode-bidi: bidi-override;
|
||||
direction: ltr;
|
||||
content: attr(data-h);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Text for email_beta
|
||||
.eobf-beta span:before {
|
||||
content: "mo" attr(data-a) "teb" "\0040" attr(data-b) "m";
|
||||
unicode-bidi: bidi-override;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
// Text for email_goog
|
||||
.eobf-goog span:before {
|
||||
content: "mo" attr(data-a) "mg" "\0040" attr(data-b) "p." attr(data-c) "m";
|
||||
unicode-bidi: bidi-override;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
// Text for email_this
|
||||
.eobf-this span:before {
|
||||
content: attr(data-a);
|
||||
}
|
||||
78
crates/service/service-webpage/css/text.scss
Normal file
78
crates/service/service-webpage/css/text.scss
Normal file
@@ -0,0 +1,78 @@
|
||||
h1 {
|
||||
font-size: 3.5rem;
|
||||
margin-top: 1ex;
|
||||
margin-bottom: 1ex;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.5rem;
|
||||
margin-top: 1ex;
|
||||
margin-bottom: 0.5ex;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h3::before {
|
||||
color: var(--lightMetaColor);
|
||||
content: '## '
|
||||
}
|
||||
|
||||
h1::before,
|
||||
h2::before,
|
||||
h4::before,
|
||||
h5::before,
|
||||
h6::before {
|
||||
color: var(--metaColor);
|
||||
content: '# '
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
border-radius: .3rem;
|
||||
padding: 0 .2ex 0 .2ex;
|
||||
color: var(--linkColor);
|
||||
transition: 150ms;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: var(--linkColor);
|
||||
color: var(--bgColor);
|
||||
transition: 150ms;
|
||||
}
|
||||
|
||||
|
||||
footer {
|
||||
font-size: 1.4rem;
|
||||
clear: both;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
footer {
|
||||
text-align: left
|
||||
}
|
||||
|
||||
|
||||
|
||||
.footnote-definition {
|
||||
margin: 0 0 0 2rem;
|
||||
}
|
||||
|
||||
.footnote-definition-label {
|
||||
color: var(--metaColor);
|
||||
}
|
||||
|
||||
.footnote-definition p {
|
||||
display: inline;
|
||||
padding: 0 0 0 1rem;
|
||||
}
|
||||
|
||||
.footContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
5
crates/service/service-webpage/src/ast/mod.rs
Normal file
5
crates/service/service-webpage/src/ast/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod walk;
|
||||
pub use walk::*;
|
||||
|
||||
mod walk_mut;
|
||||
pub use walk_mut::*;
|
||||
384
crates/service/service-webpage/src/ast/walk.rs
Normal file
384
crates/service/service-webpage/src/ast/walk.rs
Normal file
@@ -0,0 +1,384 @@
|
||||
use markdown::mdast::Node;
|
||||
use std::fmt::Debug;
|
||||
use std::marker::PhantomPinned;
|
||||
|
||||
pub enum AstWalkStep<'a, T> {
|
||||
Enter(&'a T),
|
||||
Exit(&'a T),
|
||||
}
|
||||
|
||||
impl<T: Debug> Debug for AstWalkStep<'_, T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Enter(x) => f.debug_tuple("AstWalkStep::Enter").field(x).finish(),
|
||||
Self::Exit(x) => f.debug_tuple("AstWalkStep::Exit").field(x).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AstWalk<'a> {
|
||||
_pin: PhantomPinned,
|
||||
child_stack: Vec<usize>,
|
||||
node_stack: Vec<&'a Node>,
|
||||
}
|
||||
|
||||
impl<'a> AstWalk<'a> {
|
||||
pub fn new(root: &'a Node) -> Self {
|
||||
let mut res = Self {
|
||||
_pin: PhantomPinned {},
|
||||
node_stack: Vec::with_capacity(32),
|
||||
child_stack: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
res.node_stack.push(root);
|
||||
return res;
|
||||
}
|
||||
|
||||
fn _next_inner(&mut self) -> Option<AstWalkStep<'a, Node>> {
|
||||
if self.node_stack.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let current_node = *self.node_stack.last().unwrap();
|
||||
|
||||
// The index of the next child we should look at.
|
||||
// If `None`, we look at the parent.
|
||||
let current_child = {
|
||||
let n_nodes = self.node_stack.len();
|
||||
let n_childs = self.child_stack.len();
|
||||
match n_nodes - n_childs {
|
||||
2.. => unreachable!(),
|
||||
1 => None,
|
||||
0 => Some(self.child_stack.pop().unwrap()),
|
||||
}
|
||||
};
|
||||
|
||||
match current_child {
|
||||
None => {
|
||||
self.child_stack.push(0);
|
||||
return Some(AstWalkStep::Enter(current_node));
|
||||
}
|
||||
|
||||
Some(current_child) => {
|
||||
let child = current_node
|
||||
.children()
|
||||
.map(|x| x.get(current_child))
|
||||
.flatten();
|
||||
|
||||
match child {
|
||||
None => {
|
||||
self.node_stack.pop();
|
||||
return Some(AstWalkStep::Exit(current_node));
|
||||
}
|
||||
|
||||
Some(x) => {
|
||||
self.child_stack.push(current_child + 1);
|
||||
self.node_stack.push(&x);
|
||||
self.child_stack.push(0);
|
||||
return Some(AstWalkStep::Enter(x));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AstWalk<'a> {
|
||||
type Item = AstWalkStep<'a, Node>;
|
||||
|
||||
fn next(self: &mut Self) -> Option<Self::Item> {
|
||||
return self._next_inner();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// MARK: tests
|
||||
//
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use markdown::mdast::{Emphasis, Paragraph, Root, Strong, Text};
|
||||
|
||||
#[test]
|
||||
fn single_leaf() {
|
||||
let node = Node::Text(Text {
|
||||
value: "Hello".to_string(),
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalk::new(&node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
assert_eq!(steps.len(), 2);
|
||||
assert!(matches!(steps[0], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_child() {
|
||||
let node = Node::Paragraph(Paragraph {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "Hello".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalk::new(&node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Should be: Enter(Paragraph), Leaf(Text), Exit(Paragraph)
|
||||
assert_eq!(steps.len(), 4);
|
||||
assert!(matches!(steps[0], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[2], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[3], AstWalkStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_children() {
|
||||
let node = Node::Paragraph(Paragraph {
|
||||
children: vec![
|
||||
Node::Text(Text {
|
||||
value: "Hello".to_string(),
|
||||
position: None,
|
||||
}),
|
||||
Node::Text(Text {
|
||||
value: " ".to_string(),
|
||||
position: None,
|
||||
}),
|
||||
Node::Text(Text {
|
||||
value: "World".to_string(),
|
||||
position: None,
|
||||
}),
|
||||
],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalk::new(&node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Should be: Enter(Paragraph), Leaf(Text1), Leaf(Text2), Leaf(Text3), Exit(Paragraph)
|
||||
assert_eq!(steps.len(), 8);
|
||||
assert!(matches!(steps[0], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[2], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[3], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[4], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[5], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[6], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[7], AstWalkStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_1() {
|
||||
let node = Node::Paragraph(Paragraph {
|
||||
children: vec![Node::Emphasis(Emphasis {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "emphasized".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalk::new(&node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Should be: Enter(Paragraph), Enter(Emphasis), Leaf(Text), Exit(Emphasis), Exit(Paragraph)
|
||||
assert_eq!(steps.len(), 6);
|
||||
assert!(matches!(steps[0], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[2], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[3], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[4], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[5], AstWalkStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_2() {
|
||||
// Create: Paragraph -> [Text, Strong -> Text, Text]
|
||||
let node = Node::Paragraph(Paragraph {
|
||||
children: vec![
|
||||
Node::Text(Text {
|
||||
value: "Before ".to_string(),
|
||||
position: None,
|
||||
}),
|
||||
Node::Strong(Strong {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "bold".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
}),
|
||||
Node::Text(Text {
|
||||
value: " after".to_string(),
|
||||
position: None,
|
||||
}),
|
||||
],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalk::new(&node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Expected order:
|
||||
// 0: Enter(Paragraph)
|
||||
// 1: Leaf(Text "Before ")
|
||||
// 2: Enter(Strong)
|
||||
// 3: Leaf(Text "bold")
|
||||
// 4: Exit(Strong)
|
||||
// 5: Leaf(Text " after")
|
||||
// 6: Exit(Paragraph)
|
||||
assert_eq!(steps.len(), 10);
|
||||
assert!(matches!(steps[0], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[2], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[3], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[4], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[5], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[6], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[7], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[8], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[9], AstWalkStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_3() {
|
||||
let node = Node::Paragraph(Paragraph {
|
||||
children: vec![Node::Emphasis(Emphasis {
|
||||
children: vec![Node::Strong(Strong {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "deeply nested".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalk::new(&node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Should be: Enter(Paragraph), Enter(Emphasis), Enter(Strong), Leaf(Text), Exit(Strong), Exit(Emphasis), Exit(Paragraph)
|
||||
assert_eq!(steps.len(), 8);
|
||||
assert!(matches!(steps[0], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[2], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[3], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[4], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[5], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[6], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[7], AstWalkStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_parent() {
|
||||
let node = Node::Paragraph(Paragraph {
|
||||
children: vec![],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalk::new(&node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Should be: Enter(Paragraph), Exit(Paragraph)
|
||||
assert_eq!(steps.len(), 2);
|
||||
assert!(matches!(steps[0], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_paragraphs() {
|
||||
let node = Node::Root(Root {
|
||||
children: vec![
|
||||
Node::Paragraph(Paragraph {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "First".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
}),
|
||||
Node::Paragraph(Paragraph {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "Second".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
}),
|
||||
],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalk::new(&node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Expected order:
|
||||
// 0: Enter(Root)
|
||||
// 1: Enter(Paragraph)
|
||||
// 2: Enter(Text "First")
|
||||
// 2: Exit(Text "First")
|
||||
// 3: Exit(Paragraph)
|
||||
// 4: Enter(Paragraph)
|
||||
// 5: Enter(Text "Second")
|
||||
// 5: Exit(Text "Second")
|
||||
// 6: Exit(Paragraph)
|
||||
// 7: Exit(Root)
|
||||
assert_eq!(steps.len(), 10);
|
||||
assert!(matches!(steps[0], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[2], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[3], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[4], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[5], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[6], AstWalkStep::Enter(_)));
|
||||
assert!(matches!(steps[7], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[8], AstWalkStep::Exit(_)));
|
||||
assert!(matches!(steps[9], AstWalkStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_exit() {
|
||||
let node = Node::Root(Root {
|
||||
children: vec![Node::Paragraph(Paragraph {
|
||||
children: vec![
|
||||
Node::Emphasis(Emphasis {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "a".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
}),
|
||||
Node::Strong(Strong {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "b".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
}),
|
||||
],
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalk::new(&node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
let enter_count = steps
|
||||
.iter()
|
||||
.filter(|s| matches!(s, AstWalkStep::Enter(_)))
|
||||
.count();
|
||||
let exit_count = steps
|
||||
.iter()
|
||||
.filter(|s| matches!(s, AstWalkStep::Exit(_)))
|
||||
.count();
|
||||
|
||||
assert_eq!(enter_count, exit_count);
|
||||
assert_eq!(enter_count, 6);
|
||||
}
|
||||
}
|
||||
378
crates/service/service-webpage/src/ast/walk_mut.rs
Normal file
378
crates/service/service-webpage/src/ast/walk_mut.rs
Normal file
@@ -0,0 +1,378 @@
|
||||
use markdown::mdast::Node;
|
||||
use std::{fmt::Debug, marker::PhantomData};
|
||||
|
||||
pub enum AstWalkMutStep<'a, T> {
|
||||
Enter(&'a T),
|
||||
Exit(&'a mut T),
|
||||
}
|
||||
|
||||
impl<T: Debug> Debug for AstWalkMutStep<'_, T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Enter(x) => f.debug_tuple("AstWalkMutStep::Enter").field(x).finish(),
|
||||
Self::Exit(x) => f.debug_tuple("AstWalkMutStep::Exit").field(x).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AstWalkMut<'a> {
|
||||
_life: PhantomData<&'a mut Node>,
|
||||
child_stack: Vec<usize>,
|
||||
node_stack: Vec<*mut Node>,
|
||||
}
|
||||
|
||||
impl<'a> AstWalkMut<'a> {
|
||||
pub fn new(root: &'a mut Node) -> Self {
|
||||
let mut res = Self {
|
||||
_life: PhantomData,
|
||||
node_stack: Vec::with_capacity(32),
|
||||
child_stack: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
res.node_stack.push(root);
|
||||
return res;
|
||||
}
|
||||
|
||||
fn _next_inner(&mut self) -> Option<AstWalkMutStep<'a, Node>> {
|
||||
if self.node_stack.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let current_node = unsafe { &mut **self.node_stack.last().unwrap_unchecked() };
|
||||
|
||||
// The index of the next child we should look at.
|
||||
// If `None`, we look at the parent.
|
||||
let current_child = {
|
||||
let n_nodes = self.node_stack.len();
|
||||
let n_childs = self.child_stack.len();
|
||||
match n_nodes - n_childs {
|
||||
2.. => unreachable!(),
|
||||
1 => None,
|
||||
0 => Some(self.child_stack.pop().unwrap()),
|
||||
}
|
||||
};
|
||||
|
||||
match current_child {
|
||||
None => {
|
||||
self.child_stack.push(0);
|
||||
return Some(AstWalkMutStep::Enter(current_node));
|
||||
}
|
||||
|
||||
Some(current_child) => {
|
||||
if current_node.children().is_none()
|
||||
|| current_child >= current_node.children().unwrap().len()
|
||||
{
|
||||
self.node_stack.pop();
|
||||
return Some(AstWalkMutStep::Exit(current_node));
|
||||
}
|
||||
|
||||
let child = &mut current_node.children_mut().unwrap()[current_child];
|
||||
|
||||
self.child_stack.push(current_child + 1);
|
||||
self.node_stack.push(child);
|
||||
self.child_stack.push(0);
|
||||
return Some(AstWalkMutStep::Enter(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AstWalkMut<'a> {
|
||||
type Item = AstWalkMutStep<'a, Node>;
|
||||
|
||||
fn next(self: &mut Self) -> Option<Self::Item> {
|
||||
return self._next_inner();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// MARK: tests
|
||||
//
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use markdown::mdast::{Emphasis, Paragraph, Root, Strong, Text};
|
||||
|
||||
#[test]
|
||||
fn single_leaf() {
|
||||
let mut node = Node::Text(Text {
|
||||
value: "Hello".to_string(),
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalkMut::new(&mut node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
assert_eq!(steps.len(), 2);
|
||||
assert!(matches!(steps[0], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkMutStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_child() {
|
||||
let mut node = Node::Paragraph(Paragraph {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "Hello".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalkMut::new(&mut node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Should be: Enter(Paragraph), Leaf(Text), Exit(Paragraph)
|
||||
assert_eq!(steps.len(), 4);
|
||||
assert!(matches!(steps[0], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[2], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[3], AstWalkMutStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_children() {
|
||||
let mut node = Node::Paragraph(Paragraph {
|
||||
children: vec![
|
||||
Node::Text(Text {
|
||||
value: "Hello".to_string(),
|
||||
position: None,
|
||||
}),
|
||||
Node::Text(Text {
|
||||
value: " ".to_string(),
|
||||
position: None,
|
||||
}),
|
||||
Node::Text(Text {
|
||||
value: "World".to_string(),
|
||||
position: None,
|
||||
}),
|
||||
],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalkMut::new(&mut node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Should be: Enter(Paragraph), Leaf(Text1), Leaf(Text2), Leaf(Text3), Exit(Paragraph)
|
||||
assert_eq!(steps.len(), 8);
|
||||
assert!(matches!(steps[0], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[2], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[3], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[4], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[5], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[6], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[7], AstWalkMutStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_1() {
|
||||
let mut node = Node::Paragraph(Paragraph {
|
||||
children: vec![Node::Emphasis(Emphasis {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "emphasized".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalkMut::new(&mut node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Should be: Enter(Paragraph), Enter(Emphasis), Leaf(Text), Exit(Emphasis), Exit(Paragraph)
|
||||
assert_eq!(steps.len(), 6);
|
||||
assert!(matches!(steps[0], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[2], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[3], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[4], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[5], AstWalkMutStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_2() {
|
||||
// Create: Paragraph -> [Text, Strong -> Text, Text]
|
||||
let mut node = Node::Paragraph(Paragraph {
|
||||
children: vec![
|
||||
Node::Text(Text {
|
||||
value: "Before ".to_string(),
|
||||
position: None,
|
||||
}),
|
||||
Node::Strong(Strong {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "bold".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
}),
|
||||
Node::Text(Text {
|
||||
value: " after".to_string(),
|
||||
position: None,
|
||||
}),
|
||||
],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalkMut::new(&mut node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Expected order:
|
||||
// 0: Enter(Paragraph)
|
||||
// 1: Leaf(Text "Before ")
|
||||
// 2: Enter(Strong)
|
||||
// 3: Leaf(Text "bold")
|
||||
// 4: Exit(Strong)
|
||||
// 5: Leaf(Text " after")
|
||||
// 6: Exit(Paragraph)
|
||||
assert_eq!(steps.len(), 10);
|
||||
assert!(matches!(steps[0], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[2], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[3], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[4], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[5], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[6], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[7], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[8], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[9], AstWalkMutStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_3() {
|
||||
let mut node = Node::Paragraph(Paragraph {
|
||||
children: vec![Node::Emphasis(Emphasis {
|
||||
children: vec![Node::Strong(Strong {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "deeply nested".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalkMut::new(&mut node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Should be: Enter(Paragraph), Enter(Emphasis), Enter(Strong), Leaf(Text), Exit(Strong), Exit(Emphasis), Exit(Paragraph)
|
||||
assert_eq!(steps.len(), 8);
|
||||
assert!(matches!(steps[0], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[2], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[3], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[4], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[5], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[6], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[7], AstWalkMutStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_parent() {
|
||||
let mut node = Node::Paragraph(Paragraph {
|
||||
children: vec![],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalkMut::new(&mut node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Should be: Enter(Paragraph), Exit(Paragraph)
|
||||
assert_eq!(steps.len(), 2);
|
||||
assert!(matches!(steps[0], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkMutStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_paragraphs() {
|
||||
let mut node = Node::Root(Root {
|
||||
children: vec![
|
||||
Node::Paragraph(Paragraph {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "First".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
}),
|
||||
Node::Paragraph(Paragraph {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "Second".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
}),
|
||||
],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalkMut::new(&mut node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
// Expected order:
|
||||
// 0: Enter(Root)
|
||||
// 1: Enter(Paragraph)
|
||||
// 2: Enter(Text "First")
|
||||
// 2: Exit(Text "First")
|
||||
// 3: Exit(Paragraph)
|
||||
// 4: Enter(Paragraph)
|
||||
// 5: Enter(Text "Second")
|
||||
// 5: Exit(Text "Second")
|
||||
// 6: Exit(Paragraph)
|
||||
// 7: Exit(Root)
|
||||
assert_eq!(steps.len(), 10);
|
||||
assert!(matches!(steps[0], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[1], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[2], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[3], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[4], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[5], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[6], AstWalkMutStep::Enter(_)));
|
||||
assert!(matches!(steps[7], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[8], AstWalkMutStep::Exit(_)));
|
||||
assert!(matches!(steps[9], AstWalkMutStep::Exit(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_exit() {
|
||||
let mut node = Node::Root(Root {
|
||||
children: vec![Node::Paragraph(Paragraph {
|
||||
children: vec![
|
||||
Node::Emphasis(Emphasis {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "a".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
}),
|
||||
Node::Strong(Strong {
|
||||
children: vec![Node::Text(Text {
|
||||
value: "b".to_string(),
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
}),
|
||||
],
|
||||
position: None,
|
||||
})],
|
||||
position: None,
|
||||
});
|
||||
|
||||
let walker = AstWalkMut::new(&mut node);
|
||||
let steps: Vec<_> = walker.collect();
|
||||
|
||||
let enter_count = steps
|
||||
.iter()
|
||||
.filter(|s| matches!(s, AstWalkMutStep::Enter(_)))
|
||||
.count();
|
||||
let exit_count = steps
|
||||
.iter()
|
||||
.filter(|s| matches!(s, AstWalkMutStep::Exit(_)))
|
||||
.count();
|
||||
|
||||
assert_eq!(enter_count, exit_count);
|
||||
assert_eq!(enter_count, 6);
|
||||
}
|
||||
}
|
||||
87
crates/service/service-webpage/src/components/base.rs
Normal file
87
crates/service/service-webpage/src/components/base.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use macro_sass::sass;
|
||||
use maud::{DOCTYPE, Markup, PreEscaped, Render, html};
|
||||
|
||||
use crate::components::misc::FarLink;
|
||||
|
||||
pub struct PageMetadata {
|
||||
pub title: String,
|
||||
pub author: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub image: Option<String>,
|
||||
}
|
||||
|
||||
impl Render for PageMetadata {
|
||||
fn render(&self) -> Markup {
|
||||
let empty = String::new();
|
||||
let title = &self.title;
|
||||
let author = &self.author.as_ref().unwrap_or(&empty);
|
||||
let description = &self.description.as_ref().unwrap_or(&empty);
|
||||
let image = &self.image.as_ref().unwrap_or(&empty);
|
||||
|
||||
html!(
|
||||
meta property="og:site_name" content=(title) {}
|
||||
meta name="title" content=(title) {}
|
||||
meta property="og:title" content=(title) {}
|
||||
meta property="twitter:title" content=(title) {}
|
||||
|
||||
meta name="author" content=(author) {}
|
||||
|
||||
meta name="description" content=(description) {}
|
||||
meta property="og:description" content=(description) {}
|
||||
meta property="twitter:description" content=(description) {}
|
||||
|
||||
|
||||
meta content=(image) property="og:image" {}
|
||||
link rel="shortcut icon" href=(image) type="image/x-icon" {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const CSS: &str = sass!("css/main.scss");
|
||||
|
||||
pub struct BasePage<T: Render>(pub PageMetadata, pub T);
|
||||
|
||||
impl<T: Render> Render for BasePage<T> {
|
||||
fn render(&self) -> Markup {
|
||||
let meta = &self.0;
|
||||
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
html {
|
||||
head {
|
||||
meta charset="UTF" {}
|
||||
meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" {}
|
||||
meta content="text/html; charset=UTF-8" http-equiv="content-type" {}
|
||||
meta property="og:type" content="website" {}
|
||||
|
||||
(meta)
|
||||
title { (PreEscaped(meta.title.clone())) }
|
||||
style { (PreEscaped(CSS)) }
|
||||
}
|
||||
|
||||
body {
|
||||
div class="wrapper" {
|
||||
main { (self.1) }
|
||||
|
||||
footer {
|
||||
hr class = "footline" {}
|
||||
div class = "footContainer" {
|
||||
p {
|
||||
"This site was built by hand using "
|
||||
(FarLink("https://rust-lang.org", "Rust"))
|
||||
", "
|
||||
(FarLink("https://maud.lambda.xyz", "Maud"))
|
||||
", "
|
||||
(FarLink("https://github.com/connorskees/grass", "Grass"))
|
||||
", and "
|
||||
(FarLink("https://docs.rs/axum/latest/axum", "Axum"))
|
||||
"."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
crates/service/service-webpage/src/components/fa.rs
Normal file
56
crates/service/service-webpage/src/components/fa.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use maud::{Markup, Render, html};
|
||||
|
||||
#[expect(clippy::allow_attributes)]
|
||||
#[allow(dead_code)]
|
||||
pub enum FAIcon {
|
||||
Github,
|
||||
Git,
|
||||
Python,
|
||||
Rust,
|
||||
Discord,
|
||||
Instagram,
|
||||
|
||||
Link,
|
||||
Envelope,
|
||||
At,
|
||||
Key,
|
||||
SStar,
|
||||
RStar,
|
||||
Leaf,
|
||||
|
||||
Lock,
|
||||
Fire,
|
||||
Pen,
|
||||
Pencil,
|
||||
}
|
||||
|
||||
impl Render for FAIcon {
|
||||
fn render(&self) -> Markup {
|
||||
let classes = match self {
|
||||
Self::Github => "fa-brands fa-github",
|
||||
Self::Git => "fa-brands fa-git-alt",
|
||||
Self::Python => "fa-brands fa-python",
|
||||
Self::Rust => "fa-brands fa-rust",
|
||||
Self::Discord => "fa-brands fa-discord",
|
||||
Self::Instagram => "fa-brands fa-instagram",
|
||||
Self::Link => "fa-solid fa-link",
|
||||
Self::Envelope => "fa-solid fa-envelope",
|
||||
Self::At => "fa-solid fa-at",
|
||||
Self::Key => "fa-solid fa-key",
|
||||
Self::SStar => "fa-solid fa-star",
|
||||
Self::RStar => "fa-regular fa-star",
|
||||
Self::Leaf => "fa-regular fa-leaf",
|
||||
Self::Lock => "fa-solid fa-lock",
|
||||
Self::Fire => "fa-solid fa-fire",
|
||||
Self::Pen => "fa-solid fa-pen-nib",
|
||||
Self::Pencil => "fa-solid fa-pencil",
|
||||
};
|
||||
|
||||
html!(
|
||||
i
|
||||
class=(classes)
|
||||
style="margin-right:5pt" // TODO: configure, color
|
||||
{}
|
||||
)
|
||||
}
|
||||
}
|
||||
39
crates/service/service-webpage/src/components/mangle.rs
Normal file
39
crates/service/service-webpage/src/components/mangle.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use maud::{Markup, Render, html};
|
||||
|
||||
pub struct MangledBetaEmail {}
|
||||
|
||||
impl Render for MangledBetaEmail {
|
||||
fn render(&self) -> Markup {
|
||||
html!(
|
||||
span class="eobf eobf-beta" {
|
||||
i id="eobf-en" class="fas fa-envelope" style="margin-right: 5pt" {}
|
||||
i id="eobf-kb" class="fas fa-keyboard" style="margin-right: 5pt" {}
|
||||
span
|
||||
data-b="kra"
|
||||
data-a="c.ipula"
|
||||
data-h="Type this manually"
|
||||
style="unicode-bidi: bidi-override; direction: rtl;"
|
||||
{}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MangledGoogleEmail {}
|
||||
|
||||
impl Render for MangledGoogleEmail {
|
||||
fn render(&self) -> Markup {
|
||||
html!(
|
||||
span class="eobf eobf-goog" {
|
||||
i id="eobf-en" class="fas fa-envelope" style="margin-right: 5pt" {}
|
||||
i id="eobf-kb" class="fas fa-keyboard" style="margin-right: 5pt" {}
|
||||
span
|
||||
data-c="kra"
|
||||
data-b="rmn"
|
||||
data-a="c.lia"
|
||||
data-h="Type this manually"
|
||||
{}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
106
crates/service/service-webpage/src/components/md.rs
Normal file
106
crates/service/service-webpage/src/components/md.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use markdown::{CompileOptions, Constructs, LineEnding, Options, ParseOptions};
|
||||
use maud::{Markup, PreEscaped, Render};
|
||||
|
||||
pub struct Markdown<'a>(pub &'a str);
|
||||
|
||||
const OPTS: Options = Options {
|
||||
parse: ParseOptions {
|
||||
constructs: Constructs {
|
||||
attention: true,
|
||||
autolink: false,
|
||||
block_quote: true,
|
||||
character_escape: true,
|
||||
character_reference: true,
|
||||
code_indented: false,
|
||||
code_fenced: true,
|
||||
code_text: true,
|
||||
definition: true,
|
||||
frontmatter: false,
|
||||
gfm_autolink_literal: true,
|
||||
gfm_footnote_definition: false,
|
||||
gfm_label_start_footnote: false,
|
||||
gfm_strikethrough: false,
|
||||
gfm_table: false,
|
||||
gfm_task_list_item: false,
|
||||
hard_break_escape: false,
|
||||
hard_break_trailing: false,
|
||||
heading_atx: true,
|
||||
heading_setext: false,
|
||||
label_start_image: false,
|
||||
label_start_link: true,
|
||||
label_end: true,
|
||||
list_item: true,
|
||||
math_flow: false,
|
||||
math_text: false,
|
||||
mdx_esm: false,
|
||||
thematic_break: false,
|
||||
|
||||
// INLINE HTML
|
||||
html_flow: false,
|
||||
html_text: false,
|
||||
|
||||
// INLINE {}
|
||||
mdx_expression_flow: true,
|
||||
mdx_expression_text: true,
|
||||
|
||||
// INLINE HTML (alternative)
|
||||
mdx_jsx_flow: false,
|
||||
mdx_jsx_text: false,
|
||||
},
|
||||
|
||||
gfm_strikethrough_single_tilde: false,
|
||||
math_text_single_dollar: false,
|
||||
mdx_expression_parse: None,
|
||||
mdx_esm_parse: None,
|
||||
},
|
||||
|
||||
compile: CompileOptions {
|
||||
allow_any_img_src: false,
|
||||
allow_dangerous_html: false,
|
||||
allow_dangerous_protocol: false,
|
||||
default_line_ending: LineEnding::LineFeed,
|
||||
gfm_footnote_back_label: None,
|
||||
gfm_footnote_clobber_prefix: None,
|
||||
gfm_footnote_label_attributes: None,
|
||||
gfm_footnote_label_tag_name: None,
|
||||
gfm_footnote_label: None,
|
||||
gfm_task_list_item_checkable: false,
|
||||
gfm_tagfilter: false,
|
||||
},
|
||||
};
|
||||
|
||||
impl Render for Markdown<'_> {
|
||||
fn render(&self) -> Markup {
|
||||
/*
|
||||
let mut ast = markdown::to_mdast(MD_A, &opts.parse).unwrap();
|
||||
let walk = AstWalkMut::new(&mut ast);
|
||||
|
||||
for i in walk {
|
||||
match i {
|
||||
AstWalkMutStep::Exit(node) => {
|
||||
match node {
|
||||
Node::MdxTextExpression(x) => {
|
||||
println!("{x:?}");
|
||||
}
|
||||
Node::MdxFlowExpression(x) => {
|
||||
println!("{x:?}");
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
|
||||
*node = Node::Text(Text {
|
||||
value: "LOL".to_owned(),
|
||||
position: node.position().cloned(),
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
println!("{ast:?}");
|
||||
*/
|
||||
|
||||
let md = markdown::to_html_with_options(self.0, &OPTS).unwrap();
|
||||
return PreEscaped(md);
|
||||
}
|
||||
}
|
||||
17
crates/service/service-webpage/src/components/misc.rs
Normal file
17
crates/service/service-webpage/src/components/misc.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use maud::{Markup, Render, html};
|
||||
|
||||
/// Shorthand for an `<a>` link that opens a new tab
|
||||
/// Values are (url, text)
|
||||
pub struct FarLink<'a, T: Render>(pub &'a str, pub T);
|
||||
|
||||
impl<T: Render> Render for FarLink<'_, T> {
|
||||
fn render(&self) -> Markup {
|
||||
html!(
|
||||
a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=(self.0)
|
||||
{ (self.1) }
|
||||
)
|
||||
}
|
||||
}
|
||||
5
crates/service/service-webpage/src/components/mod.rs
Normal file
5
crates/service/service-webpage/src/components/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod base;
|
||||
pub mod fa;
|
||||
pub mod mangle;
|
||||
pub mod md;
|
||||
pub mod misc;
|
||||
33
crates/service/service-webpage/src/lib.rs
Normal file
33
crates/service/service-webpage/src/lib.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use axum::Router;
|
||||
use libservice::ToService;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
mod ast;
|
||||
mod components;
|
||||
mod routes;
|
||||
|
||||
pub struct WebpageService {}
|
||||
|
||||
impl WebpageService {
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToService for WebpageService {
|
||||
#[inline]
|
||||
fn make_router(&self) -> Option<Router<()>> {
|
||||
Some(routes::router())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn make_openapi(&self) -> utoipa::openapi::OpenApi {
|
||||
routes::Api::openapi()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn service_name(&self) -> Option<String> {
|
||||
Some("webpage".to_owned())
|
||||
}
|
||||
}
|
||||
94
crates/service/service-webpage/src/routes/assets.rs
Normal file
94
crates/service/service-webpage/src/routes/assets.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use assetserver::Asset;
|
||||
use macro_assets::assets;
|
||||
|
||||
assets! {
|
||||
prefix: "/assets"
|
||||
router: asset_router()
|
||||
|
||||
//
|
||||
// MARK: images
|
||||
//
|
||||
|
||||
Image_Cover {
|
||||
source: "../../assets/images/cover-small.jpg",
|
||||
target: "/img/face.jpg"
|
||||
}
|
||||
|
||||
Image_Betalupi {
|
||||
source: "../../assets/images/betalupi-map.png",
|
||||
target: "/img/betalupi.png"
|
||||
}
|
||||
|
||||
Image_Icon {
|
||||
source: "../../assets/images/icon.png",
|
||||
target: "/img/icon.png"
|
||||
}
|
||||
|
||||
//
|
||||
// MARK:fonts
|
||||
//
|
||||
|
||||
FiraCode_Bold_woff2 {
|
||||
source: "../../assets/fonts/fira/FiraCode-Bold.woff2",
|
||||
target: "/fonts/FiraCode-Bold.woff2"
|
||||
}
|
||||
|
||||
FiraCode_Light_woff2 {
|
||||
source: "../../assets/fonts/fira/FiraCode-Light.woff2",
|
||||
target: "/fonts/FiraCode-Light.woff2"
|
||||
}
|
||||
|
||||
FiraCode_Medium_woff2 {
|
||||
source: "../../assets/fonts/fira/FiraCode-Medium.woff2",
|
||||
target: "/fonts/FiraCode-Medium.woff2"
|
||||
}
|
||||
|
||||
FiraCode_Regular_woff2 {
|
||||
source: "../../assets/fonts/fira/FiraCode-Regular.woff2",
|
||||
target: "/fonts/FiraCode-Regular.woff2"
|
||||
}
|
||||
|
||||
FiraCode_SemiBold_woff2 {
|
||||
source: "../../assets/fonts/fira/FiraCode-SemiBold.woff2",
|
||||
target: "/fonts/FiraCode-SemiBold.woff2"
|
||||
}
|
||||
|
||||
FiraCode_VF_woff2 {
|
||||
source: "../../assets/fonts/fira/FiraCode-VF.woff2",
|
||||
target: "/fonts/FiraCode-VF.woff2"
|
||||
}
|
||||
|
||||
//
|
||||
// MARK: icons
|
||||
//
|
||||
|
||||
Fa_Brands_woff2 {
|
||||
source: "../../assets/fonts/fa/fa-brands-400.woff2",
|
||||
target: "/fonts/fa/fa-brands-400.woff2"
|
||||
}
|
||||
|
||||
Fa_Regular_woff2 {
|
||||
source: "../../assets/fonts/fa/fa-regular-400.woff2",
|
||||
target: "/fonts/fa/fa-regular-400.woff2"
|
||||
}
|
||||
|
||||
Fa_Solid_woff2 {
|
||||
source: "../../assets/fonts/fa/fa-solid-900.woff2",
|
||||
target: "/fonts/fa/fa-solid-900.woff2"
|
||||
}
|
||||
|
||||
Fa_Brands_ttf {
|
||||
source: "../../assets/fonts/fa/fa-brands-400.ttf",
|
||||
target: "/fonts/fa/fa-brands-400.ttf"
|
||||
}
|
||||
|
||||
Fa_Regular_ttf {
|
||||
source: "../../assets/fonts/fa/fa-regular-400.ttf",
|
||||
target: "/fonts/fa/fa-regular-400.ttf"
|
||||
}
|
||||
|
||||
Fa_Solid_ttf {
|
||||
source: "../../assets/fonts/fa/fa-solid-900.ttf",
|
||||
target: "/fonts/fa/fa-solid-900.ttf"
|
||||
}
|
||||
}
|
||||
65
crates/service/service-webpage/src/routes/betalupi.rs
Normal file
65
crates/service/service-webpage/src/routes/betalupi.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use assetserver::Asset;
|
||||
use maud::{Markup, html};
|
||||
|
||||
use crate::{
|
||||
components::{
|
||||
base::{BasePage, PageMetadata},
|
||||
md::Markdown,
|
||||
},
|
||||
routes::assets::{Image_Betalupi, Image_Icon},
|
||||
};
|
||||
|
||||
pub async fn betalupi() -> Markup {
|
||||
let meta = PageMetadata {
|
||||
title: "What's a \"betalupi?\"".into(),
|
||||
author: Some("Mark".into()),
|
||||
description: None,
|
||||
image: Some(Image_Icon::URL.into()),
|
||||
};
|
||||
|
||||
html! {
|
||||
(BasePage(
|
||||
meta,
|
||||
html!(
|
||||
// TODO: no metadata class, generate backlink array
|
||||
div {
|
||||
a href="/" style="padding-left:4pt;padding-right:4pt;" {"home"}
|
||||
"/"
|
||||
span class="metaData" style="padding-left:4pt;padding-right:4pt;" { "whats-a-betalupi" }
|
||||
}
|
||||
|
||||
(Markdown(MD_A))
|
||||
|
||||
br {}
|
||||
|
||||
(Markdown(MD_B))
|
||||
|
||||
br {}
|
||||
|
||||
img alt="betalupi map" class="image" src=(Image_Betalupi::URL) {}
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
const MD_A: &str = r#"[es]: https://github.com/endless-sky/endless-sky
|
||||
[*Stellaris*]: https://www.paradoxinteractive.com/games/stellaris/about
|
||||
[Arabic]: https://en.wikipedia.org/wiki/List_of_Arabic_star_names
|
||||
[wiki-betalupi]: https://en.wikipedia.org/wiki/Beta_Lupi
|
||||
|
||||
# What's a "betalupi?"
|
||||
|
||||
Beta Lupi is a solar system on the [_Endless Sky_][es] galaxy map,
|
||||
which is the first place I look whenever I need to name a server.
|
||||
|
||||
Stellar names (especially those of [Arabic] origin) make pretty good hostnames: they're meaningless (in English), they sound interesting, and the "hyperlanes" that connect them in titles like [_Endless Sky_][es] and [*Stellaris*] look a lot like a network topology.
|
||||
|
||||
Beta Lupi also happens to be a real star in the southern constellation of Lupus ([wiki][wiki-betalupi]), but that's not particularly important.
|
||||
|
||||
A snippet of the [_Endless Sky_][es] map is below."#;
|
||||
|
||||
const MD_B: &str = r#"**In other words:** Try finding a `.com` domain that...
|
||||
|
||||
- Isn't already taken
|
||||
- Doesn't sound awful
|
||||
- Isn't owned by a scalper that's selling it for $300"#;
|
||||
248
crates/service/service-webpage/src/routes/index.rs
Normal file
248
crates/service/service-webpage/src/routes/index.rs
Normal file
@@ -0,0 +1,248 @@
|
||||
use assetserver::Asset;
|
||||
use maud::{Markup, html};
|
||||
|
||||
use crate::{
|
||||
components::{
|
||||
base::{BasePage, PageMetadata},
|
||||
fa::FAIcon,
|
||||
mangle::{MangledBetaEmail, MangledGoogleEmail},
|
||||
md::Markdown,
|
||||
misc::FarLink,
|
||||
},
|
||||
routes::assets::{Image_Cover, Image_Icon},
|
||||
};
|
||||
|
||||
pub async fn index() -> Markup {
|
||||
let meta = PageMetadata {
|
||||
title: "Betalupi: About".into(),
|
||||
author: Some("Mark".into()),
|
||||
description: Some("Description".into()),
|
||||
image: Some(Image_Icon::URL.into()),
|
||||
};
|
||||
|
||||
html! {
|
||||
(BasePage(
|
||||
meta,
|
||||
html!(
|
||||
h2 id="about" { "About" }
|
||||
|
||||
div {
|
||||
img
|
||||
src=(Image_Cover::URL)
|
||||
style="float:left;margin:10px 10px 10px 10px;display:block;width:25%;"
|
||||
{}
|
||||
|
||||
div style="margin:2ex 1ex 2ex 1ex;display:inline-block;overflow:hidden;width:60%;" {
|
||||
"Welcome, you've reached Mark's main page. Here you'll find"
|
||||
" links to various projects I've worked on."
|
||||
|
||||
ul {
|
||||
li { (MangledBetaEmail {}) }
|
||||
li { (MangledGoogleEmail {}) }
|
||||
|
||||
li {
|
||||
(
|
||||
FarLink(
|
||||
"https://github.com/rm-dr",
|
||||
html!(
|
||||
(FAIcon::Github)
|
||||
"rm-dr"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
li {
|
||||
(
|
||||
FarLink(
|
||||
"https://git.betalupi.com",
|
||||
html!(
|
||||
(FAIcon::Git)
|
||||
"git.betalupi.com"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
br style="clear:both;" {}
|
||||
}
|
||||
|
||||
"Also see "
|
||||
a href="/whats-a-betalupi" { "what's a \"betalupi?\"" }
|
||||
|
||||
|
||||
(Markdown(concat!(
|
||||
"## Pages\n",
|
||||
" - [Handouts](/handouts): Math circle lessons I've written\n",
|
||||
" - [Links](/links): Interesting parts of the internet"
|
||||
)))
|
||||
|
||||
hr style="margin-top: 6rem; margin-bottom: 6rem" {}
|
||||
|
||||
h2 { "Projects" }
|
||||
ul {
|
||||
li {
|
||||
p {
|
||||
b { "RedoxOS" }
|
||||
", a general-purpose, microkernel-based operating system written in Rust. "
|
||||
|
||||
em { span style="color: var(--grey);" {"[enthusiast]"} }
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
span style="color: var(--grey);" {"Status: "}
|
||||
span style="color: var(--yellow);" {"Passive"}
|
||||
}
|
||||
|
||||
|
||||
li {
|
||||
span style="color: var(--grey);" {"Website: "}
|
||||
(
|
||||
FarLink(
|
||||
"https://www.redox-os.org",
|
||||
html!(
|
||||
(FAIcon::Link)
|
||||
"redox-os.org"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
p {
|
||||
b { "Tectonic" }
|
||||
", the LaTeX engine that is pleasant to use. Experimental, but fully functional. "
|
||||
|
||||
em { span style="color: var(--grey);" {"[co-maintainer]"} }
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
span style="color: var(--grey);" {"Status: "}
|
||||
span style="color: var(--yellow);" {"Passive. "}
|
||||
(FarLink("https://github.com/typst/typst", "Typst"))
|
||||
" is better."
|
||||
}
|
||||
|
||||
|
||||
li {
|
||||
span style="color: var(--grey);" {"Main Repo: "}
|
||||
(
|
||||
FarLink(
|
||||
"https://github.com/tectonic-typesetting/tectonic",
|
||||
html!( (FAIcon::Github) "Tectonic" )
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
li {
|
||||
span style="color: var(--grey);" {"Bundle Tools: "}
|
||||
(
|
||||
FarLink(
|
||||
"https://github.com/tectonic-typesetting/tectonic-texlive-bundles",
|
||||
html!( (FAIcon::Github) "tectonic-texlive-bundles" )
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
li {
|
||||
p {
|
||||
b { "Daisy" }
|
||||
", a pretty TUI scientific calculator. "
|
||||
|
||||
em { span style="color: var(--grey);" {"[author]"} }
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
span style="color: var(--grey);" {"Status: "}
|
||||
span style="color: var(--orange);" {"Done. "}
|
||||
"Used this to learn Rust. "
|
||||
(FarLink("https://numbat.dev", "Numbat"))
|
||||
" is better."
|
||||
}
|
||||
|
||||
li {
|
||||
span style="color: var(--grey);" {"Repository: "}
|
||||
(
|
||||
FarLink(
|
||||
"https://github.com/rm-dr/daisy",
|
||||
html!( (FAIcon::Github) "rm-dr/daisy" )
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
li {
|
||||
span style="color: var(--grey);" {"Website: "}
|
||||
(
|
||||
FarLink(
|
||||
"https://daisy.betalupi.com",
|
||||
html!(
|
||||
(FAIcon::Link)
|
||||
"daisy.betalupi.com"
|
||||
)
|
||||
)
|
||||
)
|
||||
" (WASM demo)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
p {
|
||||
b { "Lamb" }
|
||||
", a lambda calculus engine. "
|
||||
|
||||
em { span style="color: var(--grey);" {"[author]"} }
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
span style="color: var(--grey);" {"Status: "}
|
||||
span style="color: var(--orange);" {"Done. "}
|
||||
"Fun little project."
|
||||
}
|
||||
|
||||
li {
|
||||
span style="color: var(--grey);" {"Repository: "}
|
||||
(
|
||||
FarLink(
|
||||
"https://github.com/rm-dr/lamb",
|
||||
html!( (FAIcon::Github) "rm-dr/lamb" )
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
li {
|
||||
span style="color: var(--grey);" {"PyPi: "}
|
||||
(
|
||||
FarLink(
|
||||
"https://pypi.org/project/lamb-engine",
|
||||
html!(
|
||||
(FAIcon::Python)
|
||||
"lamb-engine"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
174
crates/service/service-webpage/src/routes/links.rs
Normal file
174
crates/service/service-webpage/src/routes/links.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
use assetserver::Asset;
|
||||
use maud::{Markup, html};
|
||||
|
||||
use crate::{
|
||||
components::{
|
||||
base::{BasePage, PageMetadata},
|
||||
md::Markdown,
|
||||
},
|
||||
routes::assets::Image_Icon,
|
||||
};
|
||||
|
||||
pub async fn links() -> Markup {
|
||||
let meta = PageMetadata {
|
||||
title: "Links".into(),
|
||||
author: Some("Mark".into()),
|
||||
description: None,
|
||||
image: Some(Image_Icon::URL.into()),
|
||||
};
|
||||
|
||||
html! {
|
||||
(BasePage(
|
||||
meta,
|
||||
html!(
|
||||
// TODO: no metadata class, generate backlink array
|
||||
div {
|
||||
a href="/" style="padding-left:4pt;padding-right:4pt;" {"home"}
|
||||
"/"
|
||||
span class="metaData" style="padding-left:4pt;padding-right:4pt;" { "links" }
|
||||
}
|
||||
|
||||
(Markdown(MD_A))
|
||||
|
||||
hr style="margin-top: 8rem; margin-bottom: 8rem" {}
|
||||
|
||||
(Markdown(MD_B))
|
||||
|
||||
hr style="margin-top: 8rem; margin-bottom: 8rem" {}
|
||||
|
||||
(Markdown(MD_C))
|
||||
|
||||
hr style="margin-top: 8rem; margin-bottom: 8rem" {}
|
||||
|
||||
(Markdown(MD_D))
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Dead links:
|
||||
|
||||
https://www.commitstrip.com/en/
|
||||
http://www.3dprintmath.com/
|
||||
*/
|
||||
|
||||
const MD_A: &str = r#"# Bookmarks
|
||||
|
||||
This is a heavily opinionated bookmarks toolbar."#;
|
||||
|
||||
const MD_B: &str = r#"## Podcasts
|
||||
|
||||
- :star: [Darknet Diaries](https://darknetdiaries.com/): A perennial classic.
|
||||
- [Art of Manliness](https://www.artofmanliness.com/podcast/): Philosophy, literaure, psychology.
|
||||
- [The Overanalyzers](https://www.theoveranalyzers.com/): Amateur podcasters talk amateur psychology. This is a complement---they're _very_ good!
|
||||
- [Rust in Production](https://corrode.dev/podcast/): Operations, Rust, and modern tech.
|
||||
- :star: [On the Metal](https://onthemetal.transistor.fm/): Quality stories from quality engineers.
|
||||
- [Security Cryptography Whatever](https://securitycryptographywhatever.com/): Modern cryptography, for those who understand the underlying theory.
|
||||
|
||||
## Essays
|
||||
|
||||
- [Real Programmers don't use Pascal](https://www.ee.torontomu.ca/~elf/hack/realmen.html)
|
||||
- [A Mathematician's Lament](/files/lockhart.pdf)
|
||||
- :star: [The Jargon File](http://www.catb.org/jargon/)
|
||||
|
||||
## Textbooks
|
||||
|
||||
- :star: [OpenLogic](https://openlogicproject.org/): The gold standard
|
||||
- [Operating Systems: from 0 to 1](https://github.com/tuhdo/os01)
|
||||
- [Operating Systems: Three Easy Pieces](https://pages.cs.wisc.edu/~remzi/OSTEP/)
|
||||
- [Crafting Interpreters](https://craftinginterpreters.com/contents.html)
|
||||
- [An Introduction to Mathematical Cryptography](https://link.springer.com/book/10.1007/978-0-387-77993-5)
|
||||
- [Stories about Maxima and Minima](https://archive.org/details/storiesaboutmaxi0000tikh)
|
||||
|
||||
## Miscellanea
|
||||
|
||||
- [Hackaday](https://hackaday.com/)
|
||||
- [Grumpy Website](https://grumpy.website/): Daily design crimes
|
||||
- :star: [ZSA Voyager](https://www.zsa.io/voyager): World's best keyboard
|
||||
- [Nikita Prokopov](https://tonsky.me/)
|
||||
- [Faster Than Lime](https://fasterthanli.me/)
|
||||
- [Pepper & Carrot](https://www.peppercarrot.com/): The open-source webcomic
|
||||
- [User Friendly](https://en.wikipedia.org/wiki/User_Friendly): Old-school. Offline and archived.
|
||||
- [PhD Comics](https://phdcomics.com/): Quality academic humor. Discontinued.
|
||||
- :star: [Spintronics](https://store.upperstory.com/collections/spintronics/products/spintronics-act-one): Mechanical circuits. Very clever toy.
|
||||
- :star: [Turing Tumble](https://store.upperstory.com/collections/turing-tumble-game/products/turing-tumble): Modern Dr. Nim"#;
|
||||
|
||||
const MD_C: &str = r#"## Tools
|
||||
|
||||
- :star: [Numbat](https://numbat.dev/)
|
||||
- [MxToolbox](https://mxtoolbox.com/)
|
||||
- [Brainfuck Interpreter](https://copy.sh/brainfuck/)
|
||||
- [Compiler Explorer](https://godbolt.org/)
|
||||
- [Monkeytype](https://monkeytype.com/)
|
||||
- :star: [TLDR](https://tldr.sh/)
|
||||
- [TOS;DR](https://tosdr.org/)
|
||||
- [Regexr](https://regexr.com/)
|
||||
- [Choose a License](https://choosealicense.com/)
|
||||
- [DeepL](https://www.deepl.com/translator)
|
||||
- [ShuffleCake](https://shufflecake.net/)
|
||||
- [Zola](https://www.getzola.org/): Static site generator
|
||||
- [Presenterm](https://github.com/mfontanini/presenterm)
|
||||
- [mprocs](https://github.com/pvolok/mprocs): Simpler tmux
|
||||
- [mask](https://github.com/jacobdeichert/mask)
|
||||
- [gitui](https://github.com/gitui-org/gitui): git tui
|
||||
- [tokei](https://github.com/XAMPPRocky/tokei): count lines of code
|
||||
- [delta](https://github.com/dandavison/delta): pretty pager for diffs
|
||||
- [dust](https://github.com/dandavison/delta): `du`, but better
|
||||
|
||||
## Math Resources
|
||||
|
||||
- [Quantum Quest](https://www.quantum-quest.org/)
|
||||
- [The Natural Number Game](https://www.ma.imperial.ac.uk/~buzzard/xena/natural_number_game/)
|
||||
- [Intro to Lambda Calculus](https://www.driverlesscrocodile.com/technology/lambda-calculus-for-people-a-step-behind-me-1/)
|
||||
- [FSM Simulator](https://ivanzuzak.info/noam/webapps/fsm_simulator/)
|
||||
- [Euclidea](https://www.euclidea.xyz/)
|
||||
- [Problems.ru](https://problems.ru/)
|
||||
|
||||
## OS Dev Resources
|
||||
|
||||
- [OS Dev Wiki](https://wiki.osdev.org/Expanded_Main_Page)
|
||||
- [Nand2Tetris](https://www.nand2tetris.org/course)
|
||||
- :star: [Writing an OS in Rust](https://os.phil-opp.com/)
|
||||
- [x86 Assembly Guide](https://www.cs.virginia.edu/~evans/cs216/guides/x86.html)
|
||||
- [Writing a simple x86 Bootloader](https://www.alanfoster.me/posts/writing-a-bootloader/)
|
||||
- [FDC Programming](http://www.brokenthorn.com/Resources/OSDev20.html)
|
||||
- [CS77 at Bristol College](http://www.c-jump.com/CIS77/CIS77syllabus.htm)
|
||||
|
||||
## Misc Resources
|
||||
|
||||
- [Learn OpenGL](https://learnopengl.com/)
|
||||
- [Learn WGPU](https://sotrh.github.io/learn-wgpu)
|
||||
- [WGPU Fundamentals](https://webgpufundamentals.org/)
|
||||
- [Acko](https://acko.net/)
|
||||
- [Bezier Info](https://pomax.github.io/bezierinfo/)
|
||||
- [SHA256 Algorithm](https://sha256algorithm.com/)
|
||||
- [ML Playground](https://ml-playground.com/)
|
||||
- [Learn you some Erlang](https://learnyousomeerlang.com/)
|
||||
- [Learn you a Haskell](https://learnyouahaskell.github.io/)
|
||||
- [Teach yourself CS](https://teachyourselfcs.com/)
|
||||
- [The Architecture of Open Source Applications](http://aosabook.org/en/index.html)
|
||||
- [wtfjs](https://github.com/denysdovhan/wtfjs): js [wat](https://www.destroyallsoftware.com/talks/wat)s
|
||||
|
||||
## Reference
|
||||
|
||||
- [DevHints](https://devhints.io/)
|
||||
- [OverAPI](https://overapi.com/)
|
||||
- [TSConfig Cheat Sheet](https://www.totaltypescript.com/tsconfig-cheat-sheet)
|
||||
- [Makefile Tutorial](https://makefiletutorial.com/)
|
||||
- [The Pinouts Book](https://pinouts.org/)
|
||||
- [Laws of UX](https://lawsofux.com/)
|
||||
|
||||
## Rust
|
||||
- [Understanding Memory Ordering in Rust](https://emschwartz.me/understanding-memory-ordering-in-rust/)
|
||||
- [Unfair Rust Quiz](https://this.quiz.is.fckn.gay/): wtfjs, but in Rust."#;
|
||||
|
||||
const MD_D: &str = r#"## Misc
|
||||
|
||||
- [Slide Rule Collection](https://www.followingtherules.info/)
|
||||
- [MK-61 Command Reference](http://www.thimet.de/CalcCollection/Calculators/Elektronika-MK-61/CmdRef.html)
|
||||
- [Why Privacy Matters](https://www.ted.com/talks/glenn_greenwald_why_privacy_matters)
|
||||
- [Papers we Love](https://paperswelove.org/)
|
||||
- [Grug Brain Dev](https://grugbrain.dev/)
|
||||
- [Zen of Python](https://peps.python.org/pep-0020/)
|
||||
- [The XY Problem](https://xyproblem.info/)"#;
|
||||
24
crates/service/service-webpage/src/routes/mod.rs
Normal file
24
crates/service/service-webpage/src/routes/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use axum::Router;
|
||||
use axum::routing::get;
|
||||
use tracing::info;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
mod assets;
|
||||
mod betalupi;
|
||||
mod index;
|
||||
mod links;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(tags(), paths(), components(schemas()))]
|
||||
pub(super) struct Api;
|
||||
|
||||
pub(super) fn router() -> Router<()> {
|
||||
let (asset_prefix, asset_router) = assets::asset_router();
|
||||
info!("Serving assets at {asset_prefix}");
|
||||
|
||||
Router::new()
|
||||
.route("/", get(index::index))
|
||||
.route("/whats-a-betalupi", get(betalupi::betalupi))
|
||||
.route("/links", get(links::links))
|
||||
.nest(asset_prefix, asset_router)
|
||||
}
|
||||
Reference in New Issue
Block a user