Initial commit of rewritten version
This commit is contained in:
commit
8907487e32
97 changed files with 7378 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
*.beam
|
||||||
|
*.ez
|
||||||
|
/build
|
||||||
|
erl_crash.dump
|
||||||
|
/output
|
||||||
|
.DS_Store
|
||||||
|
/data
|
||||||
|
node_modules
|
2
.tool-versions
Normal file
2
.tool-versions
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
gleam 1.0.0
|
||||||
|
nodejs 20.10.0
|
25
README.md
Normal file
25
README.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# gloss2
|
||||||
|
|
||||||
|
[![Package Version](https://img.shields.io/hexpm/v/gloss2)](https://hex.pm/packages/gloss2)
|
||||||
|
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gloss2/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gleam add gloss2
|
||||||
|
```
|
||||||
|
```gleam
|
||||||
|
import gloss2
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
// TODO: An example of the project in use
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Further documentation can be found at <https://hexdocs.pm/gloss2>.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gleam run # Run the project
|
||||||
|
gleam test # Run the tests
|
||||||
|
gleam shell # Run an Erlang shell
|
||||||
|
```
|
78
assets/css/custom.css
Normal file
78
assets/css/custom.css
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
@charset "UTF-8";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #fbfaf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
grid-template: "title main" min-content "sidebar main" 1fr "sidebar footer" auto / 350px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
|
grid-area: title;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title h1 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 3rem 1rem 1rem 1rem;
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > main {
|
||||||
|
grid-area: main;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
grid-area: sidebar;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > footer {
|
||||||
|
grid-area: footer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tags {
|
||||||
|
list-style-type: none;
|
||||||
|
font-size: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tags li {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tags a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tags a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#archives li {
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post h2 {
|
||||||
|
font-size: 4rem;
|
||||||
|
line-height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post header {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
476
assets/css/fonts.css
Normal file
476
assets/css/fonts.css
Normal file
|
@ -0,0 +1,476 @@
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Averia Libre";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Averia_Libre/AveriaLibre-Italic.ttf") format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Averia Libre";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Averia_Libre/AveriaLibre-BoldItalic.ttf")
|
||||||
|
format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Averia Libre";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Averia_Libre/AveriaLibre-Regular.ttf") format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Averia Libre";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Averia_Libre/AveriaLibre-Bold.ttf") format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Averia Serif Libre";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Averia_Serif_Libre/AveriaSerifLibre-LightItalic.ttf")
|
||||||
|
format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Averia Serif Libre";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Averia_Serif_Libre/AveriaSerifLibre-BoldItalic.ttf")
|
||||||
|
format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Averia Serif Libre";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Averia_Serif_Libre/AveriaSerifLibre-Light.ttf")
|
||||||
|
format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Averia Serif Libre";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Averia_Serif_Libre/AveriaSerifLibre-Bold.ttf")
|
||||||
|
format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Caveat";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Caveat/Caveat-VariableFont_wght.ttf") format("truetype");
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
|
||||||
|
U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Caveat";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Caveat/Caveat-VariableFont_wght.ttf") format("truetype");
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Caveat";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Caveat/Caveat-VariableFont_wght.ttf") format("truetype");
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||||
|
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Caveat";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Caveat/Caveat-VariableFont_wght.ttf") format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Courier Prime";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Courier_Prime/CourierPrime-Italic.ttf") format("truetype");
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||||
|
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Courier Prime";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Courier_Prime/CourierPrime-Italic.ttf") format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Courier Prime";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Courier_Prime/CourierPrime-BoldItalic.ttf")
|
||||||
|
format("truetype");
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||||
|
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Courier Prime";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Courier_Prime/CourierPrime-BoldItalic.ttf")
|
||||||
|
format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Courier Prime";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Courier_Prime/CourierPrime-Regular.ttf") format("truetype");
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||||
|
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Courier Prime";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Courier_Prime/CourierPrime-Regular.ttf") format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Courier Prime";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Courier_Prime/CourierPrime-Bold.ttf") format("truetype");
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||||
|
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Courier Prime";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Courier_Prime/CourierPrime-Bold.ttf") format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* braille */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Noto Sans Symbols 2";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Noto_Sans_Symbols_2/NotoSansSymbols2-Regular.ttf")
|
||||||
|
format("truetype");
|
||||||
|
unicode-range: U+2800-28FF;
|
||||||
|
}
|
||||||
|
/* math */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Noto Sans Symbols 2";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Noto_Sans_Symbols_2/NotoSansSymbols2-Regular.ttf")
|
||||||
|
format("truetype");
|
||||||
|
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0330, U+0391-03A1,
|
||||||
|
U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5,
|
||||||
|
U+2034-2037, U+2057, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2102, U+210A-210E,
|
||||||
|
U+2110-2112, U+2115, U+2119-211D, U+2124, U+2128, U+212C-212D, U+212F-2131,
|
||||||
|
U+2133-2138, U+213C-2140, U+2145-2149, U+2190, U+2192, U+2194-21AE,
|
||||||
|
U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B,
|
||||||
|
U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B6,
|
||||||
|
U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1,
|
||||||
|
U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11,
|
||||||
|
U+2B30-2B4C, U+2BFE, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
|
||||||
|
}
|
||||||
|
/* mayan-numerals */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Noto Sans Symbols 2";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Noto_Sans_Symbols_2/NotoSansSymbols2-Regular.ttf")
|
||||||
|
format("truetype");
|
||||||
|
unicode-range: U+1D2E0-1D2F3;
|
||||||
|
}
|
||||||
|
/* symbols */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Noto Sans Symbols 2";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Noto_Sans_Symbols_2/NotoSansSymbols2-Regular.ttf")
|
||||||
|
format("truetype");
|
||||||
|
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4,
|
||||||
|
U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3,
|
||||||
|
U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF,
|
||||||
|
U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF,
|
||||||
|
U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0,
|
||||||
|
U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F,
|
||||||
|
U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C,
|
||||||
|
U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F,
|
||||||
|
U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE,
|
||||||
|
U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415,
|
||||||
|
U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449,
|
||||||
|
U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9,
|
||||||
|
U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6,
|
||||||
|
U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B,
|
||||||
|
U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F,
|
||||||
|
U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA,
|
||||||
|
U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3,
|
||||||
|
U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859,
|
||||||
|
U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8B1, U+1F900-1F90B, U+1F93B, U+1F946,
|
||||||
|
U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA88,
|
||||||
|
U+1FA90-1FABD, U+1FABF-1FAC5, U+1FACE-1FADB, U+1FAE0-1FAE8, U+1FAF0-1FAF8,
|
||||||
|
U+1FB00-1FBFF;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Noto Sans Symbols 2";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Noto_Sans_Symbols_2/NotoSansSymbols2-Regular.ttf")
|
||||||
|
format("truetype");
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||||
|
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Noto Sans Symbols 2";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Noto_Sans_Symbols_2/NotoSansSymbols2-Regular.ttf")
|
||||||
|
format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-LightItalic.ttf") format("truetype");
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-LightItalic.ttf") format("truetype");
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
|
||||||
|
U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
|
||||||
|
U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-LightItalic.ttf") format("truetype");
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||||
|
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-LightItalic.ttf") format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-SemiBoldItalic.ttf") format("truetype");
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-SemiBoldItalic.ttf") format("truetype");
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
|
||||||
|
U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
|
||||||
|
U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-SemiBoldItalic.ttf") format("truetype");
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||||
|
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-SemiBoldItalic.ttf") format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-Light.ttf") format("truetype");
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-Light.ttf") format("truetype");
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
|
||||||
|
U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
|
||||||
|
U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-Light.ttf") format("truetype");
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||||
|
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-Light.ttf") format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-SemiBold.ttf") format("truetype");
|
||||||
|
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-SemiBold.ttf") format("truetype");
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
|
||||||
|
U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
|
||||||
|
U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-SemiBold.ttf") format("truetype");
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||||
|
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Spectral";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/Spectral/Spectral-SemiBold.ttf") format("truetype");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||||
|
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
||||||
|
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
751
assets/css/magick.css
Normal file
751
assets/css/magick.css
Normal file
|
@ -0,0 +1,751 @@
|
||||||
|
/*
|
||||||
|
* Magick CSS
|
||||||
|
* by: winterveil (https://github.com/wintermute-cell/)
|
||||||
|
* license: MIT
|
||||||
|
* version: 1.0.5
|
||||||
|
*/
|
||||||
|
|
||||||
|
@charset "UTF-8";
|
||||||
|
|
||||||
|
/* Importing the fonts. */
|
||||||
|
@import url("./fonts.css");
|
||||||
|
|
||||||
|
/* Simple CSS Reset */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Theme colors */
|
||||||
|
--fg: #0e0e0e;
|
||||||
|
--bg: #fefefe;
|
||||||
|
--form-bg: #fbfbfb;
|
||||||
|
--form-fg: #00004d;
|
||||||
|
--form-fg-placeholder: #00004d9a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--fg: #fefefe;
|
||||||
|
--bg: #0e0e0e;
|
||||||
|
--form-bg: #1a1a1a;
|
||||||
|
--form-fg: #fefefe;
|
||||||
|
--form-fg-placeholder: #fefefe9a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Modify the base font-size to 62.5% so that 1.6rem = 16px.
|
||||||
|
2) Set box-sizing globally to handle padding and border widths.
|
||||||
|
*/
|
||||||
|
html {
|
||||||
|
font-size: 62.5%; /* 1 */
|
||||||
|
box-sizing: border-box; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Use smaller sizes on mobile devices. */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
html {
|
||||||
|
font-size: 56%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Set the base font-size to 18px with a normal weight.
|
||||||
|
2) Set the text and background colors to match the theme.
|
||||||
|
3) Use the 'Averia Serif Libre' font for the body text.
|
||||||
|
4) Reset the counter for sidenotes.
|
||||||
|
*/
|
||||||
|
body {
|
||||||
|
font-size: 1.8rem; /* 1 */
|
||||||
|
background-color: var(--bg); /* 2 */
|
||||||
|
color: var(--fg); /* 2 */
|
||||||
|
font-family: "Averia Serif Libre", serif; /* 3 */
|
||||||
|
font-style: normal; /* 3 */
|
||||||
|
line-height: 2.2rem; /* 3 */
|
||||||
|
font-weight: 300; /* 3 */
|
||||||
|
|
||||||
|
counter-reset: sidenote-counter; /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Center the main content.
|
||||||
|
2) Set the width of the element to 760px, with lower padding on mobile devices.
|
||||||
|
3) Relative position as the default allows for absolute positioning of child elements.
|
||||||
|
*/
|
||||||
|
article,
|
||||||
|
main {
|
||||||
|
margin: auto; /* 1 */
|
||||||
|
max-width: 76rem; /* 2 */
|
||||||
|
padding: 0 1rem; /* 2 */
|
||||||
|
width: 100%; /* 2 */
|
||||||
|
position: relative; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
article,
|
||||||
|
main {
|
||||||
|
padding: 0 0.2rem; /* 2 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================================================================================================= */
|
||||||
|
/* Structure & Layout ===================================================================================================== */
|
||||||
|
/* ========================================================================================================================= */
|
||||||
|
|
||||||
|
/* Display the header, main, and footer sections as distinctly separate blocks. */
|
||||||
|
header,
|
||||||
|
section,
|
||||||
|
footer {
|
||||||
|
margin: 0.7rem;
|
||||||
|
padding: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On mobile devices, a smaller margin looks more fitting due to the smaller view. */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
header,
|
||||||
|
section,
|
||||||
|
footer {
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Avoid double margin on the last child of each section */
|
||||||
|
header > *:last-child,
|
||||||
|
section > *:last-child,
|
||||||
|
footer > *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add large margins to the header to visually separate it from the main content. */
|
||||||
|
header {
|
||||||
|
margin-top: 12rem;
|
||||||
|
margin-bottom: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On mobile devices, reduce the margin around the header to save space. */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
header {
|
||||||
|
margin: 0;
|
||||||
|
padding: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Center align the text in the footer.
|
||||||
|
2) Add a margin to the top of the footer to visually separate it from the main content.
|
||||||
|
3) Add a margin to the bottom of the footer to not have it stuck to the bottom of the page.
|
||||||
|
*/
|
||||||
|
footer {
|
||||||
|
text-align: center; /* 1 */
|
||||||
|
margin-top: 5rem; /* 2 */
|
||||||
|
margin-bottom: 2rem; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================================================================================================= */
|
||||||
|
/* Typography & Links ===================================================================================================== */
|
||||||
|
/* ========================================================================================================================= */
|
||||||
|
|
||||||
|
/* Add some space between paragraphs. */
|
||||||
|
p {
|
||||||
|
margin: 1.6rem 0; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove link color. */
|
||||||
|
a {
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make string a little more bold, to adjust for the font. */
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make emphasized text a little larger, to adjust for the font. */
|
||||||
|
i,
|
||||||
|
em {
|
||||||
|
font-size: calc(1em + 0.1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Set the font-family, color, and font-style for the headings.
|
||||||
|
2) Add a margin to the top and bottom of the headings.
|
||||||
|
*/
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4 {
|
||||||
|
font-family: "Averia Libre", cursive; /* 1 */
|
||||||
|
color: var(--fg); /* 1 */
|
||||||
|
font-style: normal; /* 1 */
|
||||||
|
font-weight: 600; /* 1 */
|
||||||
|
margin: 1.6rem 0 1.6rem 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* h1 headings are uppercase and 2x the size of the base font. */
|
||||||
|
h1 {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 3.6rem;
|
||||||
|
line-height: 3.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* h2 headings are uppercase and 1.250x (major third) the size of the base font. */
|
||||||
|
h2 {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 1.2rem 0 1.2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* h3 headings are 1.125x (major second) the size of the base font. */
|
||||||
|
h3 {
|
||||||
|
font-size: 2.025rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add a decorative element before h3 headings. */
|
||||||
|
h3:before {
|
||||||
|
font-family: "Noto Sans Symbols 2", sans-serif;
|
||||||
|
content: "🙛 ";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* h4 headings are the same size as h3 headings, but italic and without the decorative element. */
|
||||||
|
h4 {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 2.025rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A uniquely styled h1 for the header */
|
||||||
|
header h1 {
|
||||||
|
font-size: 4rem;
|
||||||
|
color: var(--fg);
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 0 1.2rem 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional decorations for the header h1 */
|
||||||
|
header h1:before,
|
||||||
|
header h1:after {
|
||||||
|
content: "✦";
|
||||||
|
color: var(--fg);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Remove any list symbols.
|
||||||
|
2) Center align the nav links.
|
||||||
|
3) Add a margin to the top of the nav links.
|
||||||
|
4) Remove the default padding from the list.
|
||||||
|
*/
|
||||||
|
header nav ul {
|
||||||
|
list-style-type: none; /* 1 */
|
||||||
|
text-align: center; /* 2 */
|
||||||
|
margin-top: 1rem; /* 3 */
|
||||||
|
padding-inline-start: 0; /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display the navigation links as a centered, horizontal list. */
|
||||||
|
header nav ul li {
|
||||||
|
display: inline;
|
||||||
|
margin: 0 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove default link styles. */
|
||||||
|
header nav ul li a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add a hover effect to the navigation links. */
|
||||||
|
header nav ul li a::before {
|
||||||
|
content: "❯ "; /* 1 */
|
||||||
|
opacity: 0; /* 1 */
|
||||||
|
}
|
||||||
|
header nav ul li a:hover::before {
|
||||||
|
opacity: 1; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================================================================================================== */
|
||||||
|
/* Lists ===================================================================================================== */
|
||||||
|
/* ===================================================================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Add some indentation to the list items.
|
||||||
|
2) Add a margin to the top and bottom of the list.
|
||||||
|
*/
|
||||||
|
:where(main ol, main ul) {
|
||||||
|
margin-inline-start: 0; /* 1 */
|
||||||
|
padding-inline-start: 3rem; /* 1 */
|
||||||
|
margin: 0.8rem 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add some vertical space around a definition list. */
|
||||||
|
dl {
|
||||||
|
margin: 0.8rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add an indent to the definition term. */
|
||||||
|
dd {
|
||||||
|
margin: 0 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================================================================================================== */
|
||||||
|
/* Media ===================================================================================================== */
|
||||||
|
/* ===================================================================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Set the maximum width of the image to 100% so they don't overflow the main column.
|
||||||
|
2) Set the height in respect to the width to prevent the image from stretching.
|
||||||
|
3) Add some margin to standalone images.
|
||||||
|
*/
|
||||||
|
img {
|
||||||
|
max-width: 100%; /* 1 */
|
||||||
|
height: auto; /* 2 */
|
||||||
|
margin: 0.8rem 0; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Images in figures should not have their own margins */
|
||||||
|
figure img {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Set margins and padding for figures.
|
||||||
|
2) Center align any text inside figures.
|
||||||
|
*/
|
||||||
|
figure {
|
||||||
|
margin: 2rem 0; /* 1 */
|
||||||
|
padding: 0; /* 1 */
|
||||||
|
text-align: center; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Center align any element that is part of a figure */
|
||||||
|
figure * {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We don't want to center prealigned text or code in figures */
|
||||||
|
figure code,
|
||||||
|
figure pre {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set the typography for the figure captions */
|
||||||
|
figcaption {
|
||||||
|
margin: 0.8rem 0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================================================================================================== */
|
||||||
|
/* Forms & Inputs ===================================================================================================== */
|
||||||
|
/* ===================================================================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Display the form elements in a grid layout, two columns wide.
|
||||||
|
2) Add some space between the form elements.
|
||||||
|
3) Set padding and margin for the form.
|
||||||
|
4) Give the form a pop out paper note look.
|
||||||
|
*/
|
||||||
|
form {
|
||||||
|
display: grid; /* 1 */
|
||||||
|
grid-template-columns: 1fr 1fr; /* 1 */
|
||||||
|
gap: 10px; /* 2 */
|
||||||
|
padding: 1rem; /* 3 */
|
||||||
|
margin: 0.8rem 0; /* 3 */
|
||||||
|
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); /* 4 */
|
||||||
|
background-color: var(--form-bg); /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make form inputs and labels span two columns. (They get their own line) */
|
||||||
|
form input[type="email"],
|
||||||
|
form input[type="number"],
|
||||||
|
form input[type="password"],
|
||||||
|
form input[type="search"],
|
||||||
|
form input[type="tel"],
|
||||||
|
form input[type="text"],
|
||||||
|
form input[type="url"],
|
||||||
|
form label,
|
||||||
|
form fieldset,
|
||||||
|
form textarea {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Adjust the elements to take up full width of their grid cell.
|
||||||
|
2) Prevent textarea from being resized horizontally and overflowing the main column.
|
||||||
|
*/
|
||||||
|
form input,
|
||||||
|
form button,
|
||||||
|
form textarea {
|
||||||
|
width: 100%; /* 1 */
|
||||||
|
resize: vertical; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Prevent radio and checkbox from taking up full width, so they can be put next to each other.
|
||||||
|
2) Add space between radio and checkbox options.
|
||||||
|
*/
|
||||||
|
input[type="radio"],
|
||||||
|
input[type="checkbox"] {
|
||||||
|
width: auto; /* 1 */
|
||||||
|
margin-right: 0.5rem; /* 2 */
|
||||||
|
}
|
||||||
|
input[type="radio"] + label,
|
||||||
|
input[type="checkbox"] + label {
|
||||||
|
margin-right: 2rem; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add a disabled variant for radio and checkbox inputs */
|
||||||
|
input[type="radio"]:disabled + label,
|
||||||
|
input[type="checkbox"]:disabled + label {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Remove the default appearance of the input.
|
||||||
|
2) Set padding and margin for the input.
|
||||||
|
3) Apply some custom styles in place of the default ones.
|
||||||
|
4) Give the input a handwritten look.
|
||||||
|
*/
|
||||||
|
input[type="email"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="password"],
|
||||||
|
input[type="search"],
|
||||||
|
input[type="tel"],
|
||||||
|
input[type="text"],
|
||||||
|
input[type="url"],
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
-webkit-appearance: none; /* 1 */
|
||||||
|
-moz-appearance: none; /* 1 */
|
||||||
|
appearance: none; /* 1 */
|
||||||
|
box-shadow: none; /* 1 */
|
||||||
|
box-sizing: inherit; /* 1 */
|
||||||
|
border: none; /* 1 */
|
||||||
|
|
||||||
|
padding: 0.4rem 1rem; /* 2 */
|
||||||
|
margin-bottom: 1.6rem; /* 2 */
|
||||||
|
|
||||||
|
font-size: 2rem; /* 3 */
|
||||||
|
color: var(--fg); /* 3 */
|
||||||
|
background-color: transparent; /* 3 */
|
||||||
|
border-bottom: 1px solid var(--fg); /* 3 */
|
||||||
|
border-radius: 0; /* 3 */
|
||||||
|
font-size: 1.8rem; /* 3 */
|
||||||
|
|
||||||
|
font-family: "Caveat", cursive; /* 4 */
|
||||||
|
font-size: 2.6rem; /* 4 */
|
||||||
|
color: var(--form-fg); /* 4 */
|
||||||
|
caret-color: var(--form-fg); /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add disabled variant for input fields */
|
||||||
|
input[type="email"]:disabled,
|
||||||
|
input[type="number"]:disabled,
|
||||||
|
input[type="password"]:disabled,
|
||||||
|
input[type="search"]:disabled,
|
||||||
|
input[type="tel"]:disabled,
|
||||||
|
input[type="text"]:disabled,
|
||||||
|
input[type="url"]:disabled,
|
||||||
|
textarea:disabled,
|
||||||
|
select:disabled {
|
||||||
|
border-bottom: 1px dashed var(--fg); /* 3 */
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A slightly more transparent color for the placeholder text. */
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
color: var(--form-fg-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Remove the default focus outline.
|
||||||
|
2) Add a thicker bottom border to the input when focused, reducing margin to prevent layout shifting.
|
||||||
|
*/
|
||||||
|
input[type="email"]:focus,
|
||||||
|
input[type="number"]:focus,
|
||||||
|
input[type="password"]:focus,
|
||||||
|
input[type="search"]:focus,
|
||||||
|
input[type="tel"]:focus,
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="url"]:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
outline: none; /* 1 */
|
||||||
|
border-bottom: 2px solid var(--fg); /* 2 */
|
||||||
|
margin-bottom: calc(1.6rem - 1px); /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Match the theme colors.
|
||||||
|
2) Add padding.
|
||||||
|
3) Add a top margin to visually separate the buttons for the rest of the form.
|
||||||
|
4) Add a thin border.
|
||||||
|
5) Add a pointer cursor on hover.
|
||||||
|
*/
|
||||||
|
button,
|
||||||
|
input[type="button"],
|
||||||
|
input[type="reset"],
|
||||||
|
input[type="submit"] {
|
||||||
|
background-color: transparent; /* 1 */
|
||||||
|
color: var(--fg); /* 1 */
|
||||||
|
padding: 10px; /* 2 */
|
||||||
|
margin-top: 1.6rem; /* 3 */
|
||||||
|
border: 1px solid var(--fg); /* 4 */
|
||||||
|
cursor: pointer; /* 5 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) A thin border around the fieldset.
|
||||||
|
2) Set the width of the fieldset to fit around the content.
|
||||||
|
*/
|
||||||
|
fieldset {
|
||||||
|
border: 1px solid var(--fg); /* 1 */
|
||||||
|
width: fit-content; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* In a form, the fieldset takes up 100% of the width. */
|
||||||
|
form fieldset {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Set the border of a disabled button to be dashed.
|
||||||
|
2) Add the not-allowed cursor when hovering a disabled button.
|
||||||
|
*/
|
||||||
|
button:disabled,
|
||||||
|
input[type="button"]:disabled,
|
||||||
|
input[type="reset"]:disabled,
|
||||||
|
input[type="submit"]:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
border-style: dashed; /* 1 */
|
||||||
|
cursor: not-allowed; /* 2 */
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================================================================================================== */
|
||||||
|
/* Tables ========================================================================================================== */
|
||||||
|
/* ===================================================================================================================== */
|
||||||
|
|
||||||
|
/* Remove the distance between adjacent cells, since we don't have vertical border lines. */
|
||||||
|
table {
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add some padding around table cells. */
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
padding: 0.4rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove left padding for first cell in a row. */
|
||||||
|
td:first-child,
|
||||||
|
th:first-child {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove right padding for last cell in a row. */
|
||||||
|
td:last-child,
|
||||||
|
th:last-child {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Add a border under the table header.
|
||||||
|
2) Align the text to the left in the table header.
|
||||||
|
*/
|
||||||
|
th {
|
||||||
|
border-bottom: 2px solid var(--fg); /* 1 */
|
||||||
|
text-align: left; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================================================================================ */
|
||||||
|
/* Preformatting, Quotes & Code ============================================================================================ */
|
||||||
|
/* ============================================================================================================================ */
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Set custom padding and margins.
|
||||||
|
2) Hide the vertical scroll bar.
|
||||||
|
3) Set the width to fit just around the content, but limit it to 80% of the main column.
|
||||||
|
4) Center the blockquote horizontally and add some vertical margins.
|
||||||
|
*/
|
||||||
|
blockquote {
|
||||||
|
padding: 1rem 1.6rem; /* 1 */
|
||||||
|
overflow-y: hidden; /* 2 */
|
||||||
|
width: fit-content; /* 3 */
|
||||||
|
max-width: 80%; /* 3 */
|
||||||
|
margin: 2rem auto; /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Set a custom font for blockquote text paragraphs.
|
||||||
|
2) Add some space between the paragraphs.
|
||||||
|
*/
|
||||||
|
blockquote p {
|
||||||
|
font-family: "Spectral", serif; /* 1 */
|
||||||
|
font-style: italic; /* 1 */
|
||||||
|
font-size: 2.1rem; /* 1 */
|
||||||
|
font-weight: 300; /* 1 */
|
||||||
|
line-height: 2.4rem; /* 1 */
|
||||||
|
margin: 2.1rem 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add a footer to the blockquote for citations. */
|
||||||
|
/*
|
||||||
|
1) Reset any margins and padding from the main footer.
|
||||||
|
2) Set the footer to float and align to the right.
|
||||||
|
3) Limit the width of the footer to 55% of the main column.
|
||||||
|
4) Set a custom font for the footer.
|
||||||
|
*/
|
||||||
|
blockquote footer {
|
||||||
|
margin: 0; /* 1 */
|
||||||
|
padding: 0; /* 1 */
|
||||||
|
float: right; /* 2 */
|
||||||
|
text-align: right; /* 2 */
|
||||||
|
width: 55%; /* 3 */
|
||||||
|
font-family: "Spectral", serif; /* 4 */
|
||||||
|
font-style: normal; /* 4 */
|
||||||
|
font-size: 1.4rem; /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the actual citation italic */
|
||||||
|
blockquote footer cite {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Set custom padding.
|
||||||
|
2) Hide the vertical scroll bar.
|
||||||
|
3) Set a custom monospace font.
|
||||||
|
4) Add a top and bottom border line.
|
||||||
|
*/
|
||||||
|
pre:has(code) {
|
||||||
|
padding: 1rem 1.6rem; /* 1 */
|
||||||
|
margin: 1.6rem 0; /* 1 */
|
||||||
|
overflow-y: hidden; /* 2 */
|
||||||
|
font-family: "Courier Prime", monospace; /* 3 */
|
||||||
|
font-size: 1.6rem; /* 3 */
|
||||||
|
border-top: 2px solid var(--fg); /* 4 */
|
||||||
|
border-bottom: 2px solid var(--fg); /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set a custom monospace font */
|
||||||
|
code {
|
||||||
|
font-family: "Courier Prime", monospace;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Float the line numbers to the left, next to the code.
|
||||||
|
2) Make some distance between the line numbers and the code, and pull it all to the left with a negative margin.
|
||||||
|
3) Add a vertical line to separate the line numbers from the code.
|
||||||
|
4) Align the line numbers against the separator.
|
||||||
|
*/
|
||||||
|
pre .line-number {
|
||||||
|
float: left; /* 1 */
|
||||||
|
margin: 0 1.5rem 0 -1.5rem; /* 2 */
|
||||||
|
border-right: 1px solid; /* 3 */
|
||||||
|
text-align: right; /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Display as block, so we get a new line for each line number.
|
||||||
|
2) Add some padding to the line numbers.
|
||||||
|
*/
|
||||||
|
pre .line-number span {
|
||||||
|
display: block; /* 1 */
|
||||||
|
padding: 0 0.8rem 0 1.6rem; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================================================================================ */
|
||||||
|
/* Sidenotes & Asides ====================================================================================================== */
|
||||||
|
/* ============================================================================================================================ */
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) On mobile devices, sidenotes behave the same as asides.
|
||||||
|
2) Float the sidenotes to the right.
|
||||||
|
3) Make sure the sidenotes don't clash.
|
||||||
|
4) Set the width of the sidenotes to 40% of the main column.
|
||||||
|
5) Add padding, margins and a border for better visual separation.
|
||||||
|
6) Adjust typography to be more compact.
|
||||||
|
*/
|
||||||
|
.sidenote, /* 1 */
|
||||||
|
aside {
|
||||||
|
float: right; /* 2 */
|
||||||
|
clear: right; /* 3 */
|
||||||
|
width: 40%; /* 4 */
|
||||||
|
margin: 1rem 1rem 1rem 3rem; /* 5 */
|
||||||
|
padding: 0.5rem 0.5rem 0.5rem 2rem; /* 5 */
|
||||||
|
border-left: 3px solid var(--fg); /* 5 */
|
||||||
|
font-size: 1.4rem; /* 6 */
|
||||||
|
line-height: 1.3; /* 6 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent double top margins */
|
||||||
|
aside h1,
|
||||||
|
aside h2,
|
||||||
|
aside h3,
|
||||||
|
aside h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1) Set the distance from the main column.
|
||||||
|
2) Set the width of the element to a little less than the remaining space on one side, limited to 40% of the main column.
|
||||||
|
3) Remove any values set for the mobile version of the sidenotes.
|
||||||
|
4) Set the width to the variable defined above.
|
||||||
|
5) Set a negative right margin to (self-width + distance-from-main) to pull the sidenote to the right.
|
||||||
|
*/
|
||||||
|
/* Sadly, CSS does not support var() and rem in media queries, so we have to hardcode pixels. */
|
||||||
|
/* This will break if the main column width is changed without adjusting this media query. */
|
||||||
|
@media (min-width: calc(760px + 400px)) {
|
||||||
|
.sidenote {
|
||||||
|
--distance-from-main: 3rem; /* 1 */
|
||||||
|
--self-width: min(
|
||||||
|
calc((100vw - 760px) / 2 - (var(--distance-from-main))),
|
||||||
|
40%
|
||||||
|
); /* 2 */
|
||||||
|
margin: 0; /* 3 */
|
||||||
|
padding: 0; /* 3 */
|
||||||
|
border: none; /* 3 */
|
||||||
|
width: var(--self-width);
|
||||||
|
margin-right: calc(
|
||||||
|
calc(var(--self-width) + var(--distance-from-main)) * -1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Each time a sidenote anchor is encountered, increment the counter */
|
||||||
|
.sidenote-anchor {
|
||||||
|
counter-increment: sidenote-counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Use a custom font for the sidenote numbers */
|
||||||
|
.sidenote-anchor:after,
|
||||||
|
.sidenote:before {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
position: relative;
|
||||||
|
font-family: "Spectral", serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fine-adjust the number position for the sidenote anchor */
|
||||||
|
.sidenote-anchor:after {
|
||||||
|
content: counter(sidenote-counter);
|
||||||
|
top: -0.5rem;
|
||||||
|
left: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fine-adjust the number position for the sidenote */
|
||||||
|
.sidenote:before {
|
||||||
|
content: counter(sidenote-counter) " ";
|
||||||
|
top: -0.5rem;
|
||||||
|
}
|
351
assets/css/normalize.css
vendored
Normal file
351
assets/css/normalize.css
vendored
Normal file
|
@ -0,0 +1,351 @@
|
||||||
|
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
|
||||||
|
/* Document
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the line height in all browsers.
|
||||||
|
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the margin in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the `main` element consistently in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the font size and margin on `h1` elements within `section` and
|
||||||
|
* `article` contexts in Chrome, Firefox, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in Firefox.
|
||||||
|
* 2. Show the overflow in Edge and IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box; /* 1 */
|
||||||
|
height: 0; /* 1 */
|
||||||
|
overflow: visible; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the gray background on active links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove the bottom border in Chrome 57-
|
||||||
|
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: none; /* 1 */
|
||||||
|
text-decoration: underline; /* 2 */
|
||||||
|
text-decoration: underline dotted; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||||
|
* all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the border on images inside links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Change the font styles in all browsers.
|
||||||
|
* 2. Remove the margin in Firefox and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit; /* 1 */
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the overflow in IE.
|
||||||
|
* 1. Show the overflow in Edge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
/* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||||
|
* 1. Remove the inheritance of text transform in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
/* 1 */
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner border and padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type="button"]::-moz-focus-inner,
|
||||||
|
[type="reset"]::-moz-focus-inner,
|
||||||
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
border-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the focus styles unset by the previous rule.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button:-moz-focusring,
|
||||||
|
[type="button"]:-moz-focusring,
|
||||||
|
[type="reset"]:-moz-focusring,
|
||||||
|
[type="submit"]:-moz-focusring {
|
||||||
|
outline: 1px dotted ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: 0.35em 0.75em 0.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the text wrapping in Edge and IE.
|
||||||
|
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||||
|
* 3. Remove the padding so developers are not caught out when they zero out
|
||||||
|
* `fieldset` elements in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
color: inherit; /* 2 */
|
||||||
|
display: table; /* 1 */
|
||||||
|
max-width: 100%; /* 1 */
|
||||||
|
padding: 0; /* 3 */
|
||||||
|
white-space: normal; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the default vertical scrollbar in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in IE 10.
|
||||||
|
* 2. Remove the padding in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="checkbox"],
|
||||||
|
[type="radio"] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
* 2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
outline-offset: -2px; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
* 2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
details {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Misc
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
BIN
assets/fonts/Averia_Libre/AveriaLibre-Bold.ttf
Normal file
BIN
assets/fonts/Averia_Libre/AveriaLibre-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Averia_Libre/AveriaLibre-BoldItalic.ttf
Normal file
BIN
assets/fonts/Averia_Libre/AveriaLibre-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Averia_Libre/AveriaLibre-Italic.ttf
Normal file
BIN
assets/fonts/Averia_Libre/AveriaLibre-Italic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Averia_Libre/AveriaLibre-Light.ttf
Normal file
BIN
assets/fonts/Averia_Libre/AveriaLibre-Light.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Averia_Libre/AveriaLibre-LightItalic.ttf
Normal file
BIN
assets/fonts/Averia_Libre/AveriaLibre-LightItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Averia_Libre/AveriaLibre-Regular.ttf
Normal file
BIN
assets/fonts/Averia_Libre/AveriaLibre-Regular.ttf
Normal file
Binary file not shown.
94
assets/fonts/Averia_Libre/OFL.txt
Normal file
94
assets/fonts/Averia_Libre/OFL.txt
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
Copyright (c) 2011, Dan Sayers (i@iotic.com),
|
||||||
|
with Reserved Font Name 'Averia' and 'Averia Libre'.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Bold.ttf
Normal file
BIN
assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-BoldItalic.ttf
Normal file
BIN
assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Italic.ttf
Normal file
BIN
assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Italic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Light.ttf
Normal file
BIN
assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Light.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-LightItalic.ttf
Normal file
BIN
assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-LightItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Regular.ttf
Normal file
BIN
assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Regular.ttf
Normal file
Binary file not shown.
94
assets/fonts/Averia_Serif_Libre/OFL.txt
Normal file
94
assets/fonts/Averia_Serif_Libre/OFL.txt
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
Copyright (c) 2011, Dan Sayers (i@iotic.com),
|
||||||
|
with Reserved Font Name 'Averia' and 'Averia Libre'.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
assets/fonts/Caveat/Caveat-VariableFont_wght.ttf
Normal file
BIN
assets/fonts/Caveat/Caveat-VariableFont_wght.ttf
Normal file
Binary file not shown.
93
assets/fonts/Caveat/OFL.txt
Normal file
93
assets/fonts/Caveat/OFL.txt
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2014 The Caveat Project Authors (https://github.com/googlefonts/caveat)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
66
assets/fonts/Caveat/README.txt
Normal file
66
assets/fonts/Caveat/README.txt
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
Caveat Variable Font
|
||||||
|
====================
|
||||||
|
|
||||||
|
This download contains Caveat as both a variable font and static fonts.
|
||||||
|
|
||||||
|
Caveat is a variable font with this axis:
|
||||||
|
wght
|
||||||
|
|
||||||
|
This means all the styles are contained in a single file:
|
||||||
|
Caveat-VariableFont_wght.ttf
|
||||||
|
|
||||||
|
If your app fully supports variable fonts, you can now pick intermediate styles
|
||||||
|
that aren’t available as static fonts. Not all apps support variable fonts, and
|
||||||
|
in those cases you can use the static font files for Caveat:
|
||||||
|
static/Caveat-Regular.ttf
|
||||||
|
static/Caveat-Medium.ttf
|
||||||
|
static/Caveat-SemiBold.ttf
|
||||||
|
static/Caveat-Bold.ttf
|
||||||
|
|
||||||
|
Get started
|
||||||
|
-----------
|
||||||
|
|
||||||
|
1. Install the font files you want to use
|
||||||
|
|
||||||
|
2. Use your app's font picker to view the font family and all the
|
||||||
|
available styles
|
||||||
|
|
||||||
|
Learn more about variable fonts
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
|
||||||
|
https://variablefonts.typenetwork.com
|
||||||
|
https://medium.com/variable-fonts
|
||||||
|
|
||||||
|
In desktop apps
|
||||||
|
|
||||||
|
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
|
||||||
|
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
|
||||||
|
|
||||||
|
Online
|
||||||
|
|
||||||
|
https://developers.google.com/fonts/docs/getting_started
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
|
||||||
|
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
|
||||||
|
|
||||||
|
Installing fonts
|
||||||
|
|
||||||
|
MacOS: https://support.apple.com/en-us/HT201749
|
||||||
|
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
|
||||||
|
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
|
||||||
|
|
||||||
|
Android Apps
|
||||||
|
|
||||||
|
https://developers.google.com/fonts/docs/android
|
||||||
|
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
Please read the full license text (OFL.txt) to understand the permissions,
|
||||||
|
restrictions and requirements for usage, redistribution, and modification.
|
||||||
|
|
||||||
|
You can use them in your products & projects – print or digital,
|
||||||
|
commercial or otherwise.
|
||||||
|
|
||||||
|
This isn't legal advice, please consider consulting a lawyer and see the full
|
||||||
|
license for all details.
|
BIN
assets/fonts/Caveat/static/Caveat-Bold.ttf
Normal file
BIN
assets/fonts/Caveat/static/Caveat-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Caveat/static/Caveat-Medium.ttf
Normal file
BIN
assets/fonts/Caveat/static/Caveat-Medium.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Caveat/static/Caveat-Regular.ttf
Normal file
BIN
assets/fonts/Caveat/static/Caveat-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Caveat/static/Caveat-SemiBold.ttf
Normal file
BIN
assets/fonts/Caveat/static/Caveat-SemiBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Courier_Prime/CourierPrime-Bold.ttf
Normal file
BIN
assets/fonts/Courier_Prime/CourierPrime-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Courier_Prime/CourierPrime-BoldItalic.ttf
Normal file
BIN
assets/fonts/Courier_Prime/CourierPrime-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Courier_Prime/CourierPrime-Italic.ttf
Normal file
BIN
assets/fonts/Courier_Prime/CourierPrime-Italic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Courier_Prime/CourierPrime-Regular.ttf
Normal file
BIN
assets/fonts/Courier_Prime/CourierPrime-Regular.ttf
Normal file
Binary file not shown.
93
assets/fonts/Courier_Prime/OFL.txt
Normal file
93
assets/fonts/Courier_Prime/OFL.txt
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2015 The Courier Prime Project Authors (https://github.com/quoteunquoteapps/CourierPrime).
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
assets/fonts/Noto_Sans_Symbols_2/NotoSansSymbols2-Regular.ttf
Normal file
BIN
assets/fonts/Noto_Sans_Symbols_2/NotoSansSymbols2-Regular.ttf
Normal file
Binary file not shown.
93
assets/fonts/Noto_Sans_Symbols_2/OFL.txt
Normal file
93
assets/fonts/Noto_Sans_Symbols_2/OFL.txt
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/symbols)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
93
assets/fonts/Spectral/OFL.txt
Normal file
93
assets/fonts/Spectral/OFL.txt
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2017 The Spectral Project Authors (http://github.com/productiontype/spectral)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
BIN
assets/fonts/Spectral/Spectral-Bold.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Spectral/Spectral-BoldItalic.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Spectral/Spectral-ExtraBold.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Spectral/Spectral-ExtraBoldItalic.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Spectral/Spectral-ExtraLight.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Spectral/Spectral-ExtraLightItalic.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Spectral/Spectral-Italic.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-Italic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Spectral/Spectral-Light.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-Light.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Spectral/Spectral-LightItalic.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-LightItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Spectral/Spectral-Medium.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-Medium.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Spectral/Spectral-MediumItalic.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Spectral/Spectral-Regular.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Spectral/Spectral-SemiBold.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-SemiBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Spectral/Spectral-SemiBoldItalic.ttf
Normal file
BIN
assets/fonts/Spectral/Spectral-SemiBoldItalic.ttf
Normal file
Binary file not shown.
25
gleam.toml
Normal file
25
gleam.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
name = "gloss2"
|
||||||
|
version = "1.0.0"
|
||||||
|
target = "javascript"
|
||||||
|
|
||||||
|
# Fill out these fields if you intend to generate HTML documentation or publish
|
||||||
|
# your project to the Hex package manager.
|
||||||
|
#
|
||||||
|
# description = ""
|
||||||
|
# licences = ["Apache-2.0"]
|
||||||
|
# repository = { type = "github", user = "username", repo = "project" }
|
||||||
|
# links = [{ title = "Website", href = "https://gleam.run" }]
|
||||||
|
#
|
||||||
|
# For a full reference of all the available options, you can have a look at
|
||||||
|
# https://gleam.run/writing-gleam/gleam-toml/.
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gleam_stdlib = "~> 0.36 or ~> 1.0"
|
||||||
|
lustre = "~> 4.0"
|
||||||
|
lustre_ssg = { path = "../ssg" }
|
||||||
|
gleam_javascript = "~> 0.8"
|
||||||
|
ranged_int = "~> 1.0"
|
||||||
|
bigi = "~> 2.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gleeunit = "~> 1.0"
|
28
manifest.toml
Normal file
28
manifest.toml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# This file was generated by Gleam
|
||||||
|
# You typically do not need to edit this file
|
||||||
|
|
||||||
|
packages = [
|
||||||
|
{ name = "bigi", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "bigi", source = "hex", outer_checksum = "B6F7CAF319F13F32DB4331A750534912A9AEE1C195DD8E5DA83A42A4AD390274" },
|
||||||
|
{ name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
|
||||||
|
{ name = "gleam_javascript", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "14D5B7E1A70681E0776BF0A0357F575B822167960C844D3D3FA114D3A75F05A8" },
|
||||||
|
{ name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" },
|
||||||
|
{ name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
|
||||||
|
{ name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
|
||||||
|
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
|
||||||
|
{ name = "jot", version = "0.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "jot", source = "hex", outer_checksum = "574A2DACA106E9B4826C9F3F2D3911844C7826D554C08E404696CC16F85E0392" },
|
||||||
|
{ name = "lustre", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "1D40C1378279F7015687F8C9DB739D6880BB0B843F4428B85C61EDDA8BF21FC6" },
|
||||||
|
{ name = "lustre_ssg", version = "0.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "jot", "lustre", "simplifile", "tom"], source = "local", path = "../ssg" },
|
||||||
|
{ name = "ranged_int", version = "1.0.0", build_tools = ["gleam"], requirements = ["bigi", "gleam_stdlib"], otp_app = "ranged_int", source = "hex", outer_checksum = "8AACD49213E87BC6E7CE5F80038C1989966CF8187382760B6168E5EA9F364B09" },
|
||||||
|
{ name = "simplifile", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C44DB387524F90DC42142699C78C850003289D32C7C99C7D32873792A299CDF7" },
|
||||||
|
{ name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" },
|
||||||
|
{ name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[requirements]
|
||||||
|
bigi = { version = "~> 2.1"}
|
||||||
|
gleam_javascript = { version = "~> 0.8" }
|
||||||
|
gleam_stdlib = { version = "~> 0.36 or ~> 1.0" }
|
||||||
|
gleeunit = { version = "~> 1.0" }
|
||||||
|
lustre = { version = "~> 4.0" }
|
||||||
|
lustre_ssg = { path = "../ssg" }
|
||||||
|
ranged_int = { version = "~> 1.0" }
|
24
package-lock.json
generated
Normal file
24
package-lock.json
generated
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "gloss",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "gloss",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"dependencies": {
|
||||||
|
"luxon": "^3.4.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/luxon": {
|
||||||
|
"version": "3.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
||||||
|
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
package.json
Normal file
28
package.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "gloss",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Glossy blog generator",
|
||||||
|
"main": "\"\"",
|
||||||
|
"directories": {
|
||||||
|
"test": "test"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://gitlab.com/Nicd/gloss.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"blog"
|
||||||
|
],
|
||||||
|
"author": "Mikko Ahlroth <mikko@ahlroth.fi>",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://gitlab.com/Nicd/gloss/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://gitlab.com/Nicd/gloss#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"luxon": "^3.4.4"
|
||||||
|
}
|
||||||
|
}
|
2950
priv/vendor/marked.esm.mjs
vendored
Normal file
2950
priv/vendor/marked.esm.mjs
vendored
Normal file
File diff suppressed because it is too large
Load diff
3
src/ffi_buffer.mjs
Normal file
3
src/ffi_buffer.mjs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function to_string(buffer) {
|
||||||
|
return buffer.toString("utf8");
|
||||||
|
}
|
9
src/ffi_exceptions.mjs
Normal file
9
src/ffi_exceptions.mjs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Ok, Error } from "./gleam.mjs";
|
||||||
|
|
||||||
|
export function resultify(callback) {
|
||||||
|
try {
|
||||||
|
return new Ok(callback());
|
||||||
|
} catch (err) {
|
||||||
|
return new Error(err);
|
||||||
|
}
|
||||||
|
}
|
5
src/ffi_fs.mjs
Normal file
5
src/ffi_fs.mjs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { mkdirSync } from "node:fs";
|
||||||
|
|
||||||
|
export function mkdirP(path) {
|
||||||
|
return mkdirSync(path, { recursive: true });
|
||||||
|
}
|
34
src/ffi_luxon.mjs
Normal file
34
src/ffi_luxon.mjs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
import { Ok, Error } from "./gleam.mjs";
|
||||||
|
import { Date, parse_month } from "./gloss/utils/date.mjs";
|
||||||
|
import { Time } from "./gloss/utils/time.mjs";
|
||||||
|
|
||||||
|
export function dateTimeInZone(dtStr, tz) {
|
||||||
|
const dt = DateTime.fromISO(dtStr, { zone: tz });
|
||||||
|
if (!dt.isValid) {
|
||||||
|
return new Error(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Ok(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toDate(dt) {
|
||||||
|
return new Date(
|
||||||
|
dt.year,
|
||||||
|
// assert Ok
|
||||||
|
parse_month(dt.month)[0],
|
||||||
|
dt.day
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toTime(dt) {
|
||||||
|
return new Time(dt.hour, dt.minute);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRFC2822(dt) {
|
||||||
|
return dt.toRFC2822();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toISO(dt) {
|
||||||
|
return dt.toISO();
|
||||||
|
}
|
3
src/ffi_meta_url.mjs
Normal file
3
src/ffi_meta_url.mjs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function metaURL() {
|
||||||
|
return import.meta.url;
|
||||||
|
}
|
11
src/ffi_object.mjs
Normal file
11
src/ffi_object.mjs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export function create() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set(obj, prop, val) {
|
||||||
|
return { ...obj, [prop]: val };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get(obj, prop) {
|
||||||
|
return obj[prop];
|
||||||
|
}
|
3
src/ffi_url.mjs
Normal file
3
src/ffi_url.mjs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function fromString(str) {
|
||||||
|
return new URL(str);
|
||||||
|
}
|
26
src/gloss/builder.gleam
Normal file
26
src/gloss/builder.gleam
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import gleam/result
|
||||||
|
import gloss/parser
|
||||||
|
import gloss/rendering/database as render_database
|
||||||
|
import gloss/renderer
|
||||||
|
import gloss/writer
|
||||||
|
import gloss/config.{type Configuration}
|
||||||
|
import gloss/models/database.{type Database}
|
||||||
|
|
||||||
|
pub type BuildError {
|
||||||
|
ParseError(err: parser.ParseError)
|
||||||
|
WriteError(err: writer.WriteError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(config: Configuration) {
|
||||||
|
config.parser()
|
||||||
|
|> result.map_error(ParseError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(db: Database, config: Configuration) {
|
||||||
|
renderer.render(db, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(posts: render_database.Database, config: Configuration) {
|
||||||
|
config.writer(posts, config.paths)
|
||||||
|
|> result.map_error(WriteError)
|
||||||
|
}
|
36
src/gloss/config.gleam
Normal file
36
src/gloss/config.gleam
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import gloss/paths.{type PathConfiguration}
|
||||||
|
import gloss/rendering/templates.{
|
||||||
|
type BaseRenderer, type ListPageRenderer, type PostContentRenderer,
|
||||||
|
type SinglePostRenderer,
|
||||||
|
}
|
||||||
|
import gloss/parser.{type Parser}
|
||||||
|
import gloss/writer.{type Writer}
|
||||||
|
import gloss/models/database.{type Database}
|
||||||
|
|
||||||
|
pub type Templates {
|
||||||
|
Templates(
|
||||||
|
base: fn(Database, Configuration) -> BaseRenderer,
|
||||||
|
single_post_full: fn(Database, Configuration) -> SinglePostRenderer,
|
||||||
|
single_post_list: fn(Database, Configuration) -> SinglePostRenderer,
|
||||||
|
list_page: fn(Database, Configuration) -> ListPageRenderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Rendering {
|
||||||
|
Rendering(
|
||||||
|
templates: Templates,
|
||||||
|
copyright: String,
|
||||||
|
content_renderer: PostContentRenderer,
|
||||||
|
posts_per_page: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Configuration {
|
||||||
|
Configuration(
|
||||||
|
blog_name: String,
|
||||||
|
rendering: Rendering,
|
||||||
|
paths: PathConfiguration,
|
||||||
|
parser: Parser,
|
||||||
|
writer: Writer,
|
||||||
|
)
|
||||||
|
}
|
33
src/gloss/defaults.gleam
Normal file
33
src/gloss/defaults.gleam
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import gloss/config.{type Configuration, Configuration, Rendering}
|
||||||
|
import gloss/rendering/templates/single_post
|
||||||
|
import gloss/rendering/templates/base
|
||||||
|
import gloss/rendering/templates/list_page
|
||||||
|
import gloss/paths
|
||||||
|
import gloss/parser
|
||||||
|
import gloss/writer
|
||||||
|
|
||||||
|
const default_templates = config.Templates(
|
||||||
|
base: base.generate,
|
||||||
|
single_post_full: single_post.full_view,
|
||||||
|
single_post_list: single_post.list_view,
|
||||||
|
list_page: list_page.generate,
|
||||||
|
)
|
||||||
|
|
||||||
|
pub fn default_config() -> Configuration {
|
||||||
|
Configuration(
|
||||||
|
blog_name: "",
|
||||||
|
rendering: Rendering(
|
||||||
|
templates: default_templates,
|
||||||
|
copyright: "",
|
||||||
|
content_renderer: single_post.content_renderer,
|
||||||
|
posts_per_page: 10,
|
||||||
|
),
|
||||||
|
paths: paths.conf(
|
||||||
|
paths.default_index,
|
||||||
|
paths.default_single_post,
|
||||||
|
paths.default_tag,
|
||||||
|
),
|
||||||
|
parser: parser.default_parse,
|
||||||
|
writer: writer.write,
|
||||||
|
)
|
||||||
|
}
|
117
src/gloss/models/database.gleam
Normal file
117
src/gloss/models/database.gleam
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import gleam/dict.{type Dict}
|
||||||
|
import gleam/list
|
||||||
|
import gleam/option.{None, Some}
|
||||||
|
import gleam/order.{type Order}
|
||||||
|
import gloss/models/post.{type Post, type Tag}
|
||||||
|
import gloss/utils/date.{type Month}
|
||||||
|
import gloss/utils/ordered_tree.{type OrderedTree}
|
||||||
|
import gloss/utils/uniqid.{type Generator, type UniqID}
|
||||||
|
|
||||||
|
pub type PostID =
|
||||||
|
UniqID
|
||||||
|
|
||||||
|
pub type PostWithID {
|
||||||
|
PostWithID(id: PostID, post: Post)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type TagPosts =
|
||||||
|
Dict(Tag, OrderedTree(PostWithID))
|
||||||
|
|
||||||
|
pub type MonthPosts =
|
||||||
|
Dict(Month, OrderedTree(PostWithID))
|
||||||
|
|
||||||
|
pub type YearPosts =
|
||||||
|
Dict(Int, MonthPosts)
|
||||||
|
|
||||||
|
pub opaque type Database {
|
||||||
|
Database(
|
||||||
|
posts: OrderedTree(PostWithID),
|
||||||
|
tags: TagPosts,
|
||||||
|
years: YearPosts,
|
||||||
|
posts_by_id: Dict(PostID, Post),
|
||||||
|
id_generator: Generator,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() -> Database {
|
||||||
|
Database(
|
||||||
|
posts: new_tree(),
|
||||||
|
tags: dict.new(),
|
||||||
|
years: dict.new(),
|
||||||
|
posts_by_id: dict.new(),
|
||||||
|
id_generator: uniqid.new(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_post(db: Database, post: Post) -> Database {
|
||||||
|
let post_date = post.get_date(post)
|
||||||
|
let #(id, id_generator) = uniqid.get(db.id_generator)
|
||||||
|
let post_with_id = PostWithID(id, post)
|
||||||
|
|
||||||
|
let posts_by_id = dict.insert(db.posts_by_id, id, post)
|
||||||
|
let posts = ordered_tree.insert(db.posts, post_with_id)
|
||||||
|
let tags =
|
||||||
|
list.fold(post.tags, db.tags, fn(acc, tag) {
|
||||||
|
dict.update(acc, tag, fn(existing) {
|
||||||
|
case existing {
|
||||||
|
None ->
|
||||||
|
new_tree()
|
||||||
|
|> ordered_tree.insert(post_with_id)
|
||||||
|
Some(posts) -> ordered_tree.insert(posts, post_with_id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
let years =
|
||||||
|
dict.update(db.years, post_date.year, fn(years) {
|
||||||
|
case years {
|
||||||
|
None ->
|
||||||
|
dict.from_list([
|
||||||
|
#(
|
||||||
|
post_date.month,
|
||||||
|
new_tree()
|
||||||
|
|> ordered_tree.insert(post_with_id),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
Some(months) ->
|
||||||
|
dict.update(months, post_date.month, fn(posts) {
|
||||||
|
case posts {
|
||||||
|
None ->
|
||||||
|
new_tree()
|
||||||
|
|> ordered_tree.insert(post_with_id)
|
||||||
|
Some(posts) -> ordered_tree.insert(posts, post_with_id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Database(
|
||||||
|
id_generator: id_generator,
|
||||||
|
posts: posts,
|
||||||
|
tags: tags,
|
||||||
|
years: years,
|
||||||
|
posts_by_id: posts_by_id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tags(db: Database) {
|
||||||
|
db.tags
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn years(db: Database) {
|
||||||
|
db.years
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_posts_with_ids(
|
||||||
|
db: Database,
|
||||||
|
order: ordered_tree.WalkOrder,
|
||||||
|
) -> List(PostWithID) {
|
||||||
|
ordered_tree.to_list(db.posts, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_tree() -> OrderedTree(PostWithID) {
|
||||||
|
ordered_tree.new(comparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn comparator(a: PostWithID, b: PostWithID) -> Order {
|
||||||
|
post.comparator(a.post, b.post)
|
||||||
|
}
|
65
src/gloss/models/post.gleam
Normal file
65
src/gloss/models/post.gleam
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import gleam/option.{type Option}
|
||||||
|
import gleam/order.{type Order, Eq, Gt, Lt}
|
||||||
|
import gloss/utils/date.{type Date}
|
||||||
|
import gloss/utils/time.{type Time, Time}
|
||||||
|
import gloss/utils/luxon.{type DateTime}
|
||||||
|
|
||||||
|
pub type PostedAt {
|
||||||
|
JustDate(Date)
|
||||||
|
DateTime(date: Date, time: Time, tz: String, luxon: DateTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Header =
|
||||||
|
#(String, String)
|
||||||
|
|
||||||
|
pub type Tag =
|
||||||
|
String
|
||||||
|
|
||||||
|
pub type Post {
|
||||||
|
Post(
|
||||||
|
title: String,
|
||||||
|
slug: String,
|
||||||
|
tags: List(Tag),
|
||||||
|
headers: List(Header),
|
||||||
|
content: String,
|
||||||
|
short_content: Option(String),
|
||||||
|
date: PostedAt,
|
||||||
|
order: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_date(post: Post) -> Date {
|
||||||
|
case post.date {
|
||||||
|
JustDate(date) -> date
|
||||||
|
DateTime(date, ..) -> date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_time(post: Post) -> Option(Time) {
|
||||||
|
case post.date {
|
||||||
|
JustDate(..) -> option.None
|
||||||
|
DateTime(time: time, ..) -> option.Some(time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_luxon(post: Post) -> Option(luxon.DateTime) {
|
||||||
|
case post.date {
|
||||||
|
JustDate(..) -> option.None
|
||||||
|
DateTime(luxon: luxon, ..) -> option.Some(luxon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn comparator(a: Post, b: Post) -> Order {
|
||||||
|
let a_date = get_date(a)
|
||||||
|
let b_date = get_date(b)
|
||||||
|
|
||||||
|
case date.compare(a_date, b_date) {
|
||||||
|
Lt -> Lt
|
||||||
|
Gt -> Gt
|
||||||
|
Eq -> {
|
||||||
|
let a_time = option.lazy_unwrap(get_time(a), time.nil_time)
|
||||||
|
let b_time = option.lazy_unwrap(get_time(b), time.nil_time)
|
||||||
|
time.compare(a_time, b_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
src/gloss/parser.gleam
Normal file
56
src/gloss/parser.gleam
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import gleam/result
|
||||||
|
import gleam/list
|
||||||
|
import gleam/regex
|
||||||
|
import gloss/utils/fs
|
||||||
|
import gloss/models/database.{type Database}
|
||||||
|
import gloss/parser/post
|
||||||
|
|
||||||
|
const default_data_path = "./data"
|
||||||
|
|
||||||
|
pub type Parser =
|
||||||
|
fn() -> Result(Database, ParseError)
|
||||||
|
|
||||||
|
pub type ParseError {
|
||||||
|
FileError(path: String, err: fs.FSError)
|
||||||
|
PostParseError(filename: String, err: post.ParseError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_parse() -> Result(Database, ParseError) {
|
||||||
|
let db = database.new()
|
||||||
|
parse_posts(post_path(), db)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_posts(path: String, db: Database) -> Result(Database, ParseError) {
|
||||||
|
use filenames <- result.try(
|
||||||
|
fs.readdir(path)
|
||||||
|
|> result.map_error(fn(err) { FileError(path, err) }),
|
||||||
|
)
|
||||||
|
|
||||||
|
let assert Ok(filename_regex) =
|
||||||
|
regex.compile(
|
||||||
|
post.filename_regex,
|
||||||
|
regex.Options(case_insensitive: False, multi_line: False),
|
||||||
|
)
|
||||||
|
|
||||||
|
let filenames =
|
||||||
|
list.filter(filenames, fn(file) { regex.check(filename_regex, file) })
|
||||||
|
|
||||||
|
use posts <- result.try(result.all(list.map(
|
||||||
|
filenames,
|
||||||
|
fn(file) {
|
||||||
|
use contents <- result.try(
|
||||||
|
fs.read_file(path <> "/" <> file)
|
||||||
|
|> result.map_error(fn(err) { FileError(file, err) }),
|
||||||
|
)
|
||||||
|
|
||||||
|
post.parse(file, contents)
|
||||||
|
|> result.map_error(fn(err) { PostParseError(file, err) })
|
||||||
|
},
|
||||||
|
)))
|
||||||
|
|
||||||
|
Ok(list.fold(posts, db, database.add_post))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_path() {
|
||||||
|
default_data_path <> "/posts"
|
||||||
|
}
|
205
src/gloss/parser/post.gleam
Normal file
205
src/gloss/parser/post.gleam
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
import gleam/string
|
||||||
|
import gleam/result
|
||||||
|
import gleam/list
|
||||||
|
import gleam/option
|
||||||
|
import gleam/int
|
||||||
|
import gleam/bool
|
||||||
|
import gleam/regex
|
||||||
|
import gloss/models/post.{type Header, type Post, type PostedAt, type Tag, Post}
|
||||||
|
import gloss/utils/date.{Date}
|
||||||
|
import gloss/utils/time.{type Time, Time}
|
||||||
|
import gloss/utils/ints/day
|
||||||
|
import gloss/utils/luxon
|
||||||
|
|
||||||
|
pub const filename_regex = "^\\d{4}-\\d\\d-\\d\\d-.*\\.md$"
|
||||||
|
|
||||||
|
const filename_separator = "-"
|
||||||
|
|
||||||
|
const filename_postfix = ".md"
|
||||||
|
|
||||||
|
const tag_separator = ","
|
||||||
|
|
||||||
|
const header_separator = ":"
|
||||||
|
|
||||||
|
const split_re = "<!--\\s*SPLIT\\s*-->"
|
||||||
|
|
||||||
|
type FilenameMeta {
|
||||||
|
FilenameMeta(date: PostedAt, order: Int, slug: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ParseError {
|
||||||
|
EmptyFile
|
||||||
|
HeaderMissing
|
||||||
|
MalformedFilename
|
||||||
|
YearNotInt
|
||||||
|
MonthNotInt
|
||||||
|
DayNotInt
|
||||||
|
InvalidDate
|
||||||
|
MalformedHeader(header: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(filename: String, contents: String) -> Result(Post, ParseError) {
|
||||||
|
let lines = string.split(contents, "\n")
|
||||||
|
|
||||||
|
use title <- try(list.first(lines), EmptyFile)
|
||||||
|
use rest <- try(list.rest(lines), HeaderMissing)
|
||||||
|
|
||||||
|
use tags <- try(list.first(rest), HeaderMissing)
|
||||||
|
use rest <- try(list.rest(rest), HeaderMissing)
|
||||||
|
|
||||||
|
let filename = case string.ends_with(filename, filename_postfix) {
|
||||||
|
True ->
|
||||||
|
string.slice(
|
||||||
|
filename,
|
||||||
|
0,
|
||||||
|
string.length(filename) - string.length(filename_postfix),
|
||||||
|
)
|
||||||
|
False -> filename
|
||||||
|
}
|
||||||
|
use meta <- result.try(parse_filename_meta(filename))
|
||||||
|
|
||||||
|
let #(headers, body) =
|
||||||
|
list.split_while(rest, fn(line) { !string.is_empty(line) })
|
||||||
|
|
||||||
|
let tags = parse_tags(tags)
|
||||||
|
use headers <- result.try(parse_headers(headers))
|
||||||
|
let body = string.join(body, "\n")
|
||||||
|
let short_content = parse_short_content(body)
|
||||||
|
use time <- result.try(parse_time(headers))
|
||||||
|
|
||||||
|
let assert post.JustDate(just_date) = meta.date
|
||||||
|
use date <- result.try(case time {
|
||||||
|
option.Some(#(time, tz)) -> {
|
||||||
|
use dt <- result.try(
|
||||||
|
luxon.date_time_in_zone(just_date, time, tz)
|
||||||
|
|> result.replace_error(InvalidDate),
|
||||||
|
)
|
||||||
|
Ok(post.DateTime(just_date, time, tz, dt))
|
||||||
|
}
|
||||||
|
option.None -> Ok(meta.date)
|
||||||
|
})
|
||||||
|
|
||||||
|
Ok(Post(
|
||||||
|
title: title,
|
||||||
|
slug: meta.slug,
|
||||||
|
date: date,
|
||||||
|
content: body,
|
||||||
|
headers: headers,
|
||||||
|
order: meta.order,
|
||||||
|
short_content: short_content,
|
||||||
|
tags: tags,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_filename_meta(filename: String) -> Result(FilenameMeta, ParseError) {
|
||||||
|
let filename_parts = string.split(filename, filename_separator)
|
||||||
|
let #(meta_parts, rest_parts) = list.split(filename_parts, 4)
|
||||||
|
|
||||||
|
use #(year_str, month_str, day_str, maybe_order) <- result.try(case
|
||||||
|
meta_parts
|
||||||
|
{
|
||||||
|
[y, m, d, o] -> Ok(#(y, m, d, o))
|
||||||
|
_ -> Error(MalformedFilename)
|
||||||
|
})
|
||||||
|
|
||||||
|
use year <- try(int.parse(year_str), YearNotInt)
|
||||||
|
use month_int <- try(int.parse(month_str), MonthNotInt)
|
||||||
|
use month <- try(date.parse_month(month_int), InvalidDate)
|
||||||
|
use day <- try(int.parse(day_str), DayNotInt)
|
||||||
|
use day <- try(day.from_int(day), InvalidDate)
|
||||||
|
let #(order, slug) = parse_order_slug(maybe_order, rest_parts)
|
||||||
|
|
||||||
|
let date = Date(year: year, month: month, day: day)
|
||||||
|
use <- bool.guard(date.is_valid_date(date), Error(InvalidDate))
|
||||||
|
|
||||||
|
Ok(FilenameMeta(
|
||||||
|
date: post.JustDate(date),
|
||||||
|
order: option.unwrap(order, 0),
|
||||||
|
slug: slug,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_order_slug(
|
||||||
|
maybe_order: String,
|
||||||
|
rest_parts: List(String),
|
||||||
|
) -> #(option.Option(Int), String) {
|
||||||
|
let fail_case = fn() {
|
||||||
|
#(option.None, string.join([maybe_order, ..rest_parts], filename_separator))
|
||||||
|
}
|
||||||
|
|
||||||
|
case string.length(maybe_order) {
|
||||||
|
o if o >= 1 && o <= 2 -> {
|
||||||
|
case int.parse(maybe_order) {
|
||||||
|
Ok(order) -> #(
|
||||||
|
option.Some(order),
|
||||||
|
string.join(rest_parts, filename_separator),
|
||||||
|
)
|
||||||
|
_ -> fail_case()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ -> fail_case()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_tags(tags: String) -> List(Tag) {
|
||||||
|
tags
|
||||||
|
|> string.split(tag_separator)
|
||||||
|
|> list.map(string.trim)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_headers(headers: List(String)) -> Result(List(Header), ParseError) {
|
||||||
|
headers
|
||||||
|
|> list.map(parse_header)
|
||||||
|
|> result.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_header(header: String) -> Result(Header, ParseError) {
|
||||||
|
let header_parts = string.split(header, header_separator)
|
||||||
|
let parts_amount = list.length(header_parts)
|
||||||
|
|
||||||
|
use <- bool.guard(parts_amount < 2, Error(MalformedHeader(header)))
|
||||||
|
|
||||||
|
let assert Ok(name) = list.first(header_parts)
|
||||||
|
let assert Ok(rest) = list.rest(header_parts)
|
||||||
|
let value = string.join(rest, header_separator)
|
||||||
|
|
||||||
|
let name = string.trim(name)
|
||||||
|
let value = string.trim(value)
|
||||||
|
|
||||||
|
Ok(#(name, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_short_content(body: String) -> option.Option(String) {
|
||||||
|
let assert Ok(re) =
|
||||||
|
regex.compile(
|
||||||
|
split_re,
|
||||||
|
regex.Options(case_insensitive: False, multi_line: False),
|
||||||
|
)
|
||||||
|
let body_parts = regex.split(re, body)
|
||||||
|
|
||||||
|
case body_parts {
|
||||||
|
[first, ..] -> option.Some(first)
|
||||||
|
_ -> option.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_time(
|
||||||
|
headers: List(Header),
|
||||||
|
) -> Result(option.Option(#(Time, String)), ParseError) {
|
||||||
|
let time_hdr = list.find(headers, fn(hdr) { hdr.0 == "time" })
|
||||||
|
case time_hdr {
|
||||||
|
Error(Nil) -> Ok(option.None)
|
||||||
|
Ok(hdr) ->
|
||||||
|
case list.split(string.split(hdr.1, " "), 1) {
|
||||||
|
#([ts], rest) ->
|
||||||
|
time.parse(ts)
|
||||||
|
|> result.replace_error(InvalidDate)
|
||||||
|
|> result.map(fn(ts) { option.Some(#(ts, string.join(rest, " "))) })
|
||||||
|
_ -> Error(MalformedHeader(hdr.0 <> ": " <> hdr.1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try(value: Result(a, b), error: c, if_ok: fn(a) -> Result(d, c)) {
|
||||||
|
result.try(result.replace_error(value, error), if_ok)
|
||||||
|
}
|
68
src/gloss/paths.gleam
Normal file
68
src/gloss/paths.gleam
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import gleam/int
|
||||||
|
import gleam/string
|
||||||
|
import gloss/models/post.{type Post}
|
||||||
|
import gloss/paths/post as post_paths
|
||||||
|
import gloss/utils/date.{type Month}
|
||||||
|
|
||||||
|
pub const default_index = "/index"
|
||||||
|
|
||||||
|
const archive_prefix = "/archive"
|
||||||
|
|
||||||
|
pub type PathConfiguration {
|
||||||
|
PathConfiguration(
|
||||||
|
index: String,
|
||||||
|
single_post: fn(Post) -> String,
|
||||||
|
tag: fn(String) -> String,
|
||||||
|
year: fn(Int) -> String,
|
||||||
|
month: fn(Int, Month) -> String,
|
||||||
|
list_page: fn(String, Int) -> String,
|
||||||
|
html: fn(String) -> String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn conf(index, single_post, tag) -> PathConfiguration {
|
||||||
|
PathConfiguration(
|
||||||
|
index,
|
||||||
|
single_post,
|
||||||
|
tag,
|
||||||
|
year_archive,
|
||||||
|
month_archive,
|
||||||
|
list_page,
|
||||||
|
html,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_single_post(post: Post) {
|
||||||
|
let post_path = post_paths.post_to_path(post)
|
||||||
|
|
||||||
|
"/" <> post_path.date_path <> "--" <> post_path.slug
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_tag(tag: String) {
|
||||||
|
"/tag--" <> tag
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn year_archive(year: Int) {
|
||||||
|
archive_prefix <> "--" <> int.to_string(year)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn month_archive(year: Int, month: Month) {
|
||||||
|
year_archive(year)
|
||||||
|
<> "-"
|
||||||
|
<> string.pad_left(int.to_string(date.month_to_int(month)), 2, "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the given list path with a page number.
|
||||||
|
///
|
||||||
|
/// The first page does not get any appended page number.
|
||||||
|
pub fn list_page(path: String, page: Int) {
|
||||||
|
case page {
|
||||||
|
1 -> path
|
||||||
|
other -> path <> "--" <> int.to_string(other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get path with the .html extension
|
||||||
|
pub fn html(path: String) {
|
||||||
|
path <> ".html"
|
||||||
|
}
|
31
src/gloss/paths/post.gleam
Normal file
31
src/gloss/paths/post.gleam
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import gleam/string
|
||||||
|
import gleam/int
|
||||||
|
import gleam/list
|
||||||
|
import gloss/models/post.{type Post}
|
||||||
|
import gloss/utils/date
|
||||||
|
import gloss/utils/ints/day
|
||||||
|
|
||||||
|
pub type PostPath {
|
||||||
|
PostPath(date_path: String, slug: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn post_to_path(post: Post) -> PostPath {
|
||||||
|
let post_date = post.get_date(post)
|
||||||
|
let date_parts =
|
||||||
|
list.map(
|
||||||
|
[
|
||||||
|
post_date.year,
|
||||||
|
date.month_to_int(post_date.month),
|
||||||
|
day.to_int(post_date.day),
|
||||||
|
],
|
||||||
|
pad_int,
|
||||||
|
)
|
||||||
|
|
||||||
|
PostPath(date_path: string.join(date_parts, "-"), slug: post.slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad_int(number: Int) -> String {
|
||||||
|
number
|
||||||
|
|> int.to_string()
|
||||||
|
|> string.pad_left(to: 2, with: "0")
|
||||||
|
}
|
215
src/gloss/renderer.gleam
Normal file
215
src/gloss/renderer.gleam
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
import gleam/list
|
||||||
|
import gleam/dict.{type Dict}
|
||||||
|
import gleam/result
|
||||||
|
import gleam/int
|
||||||
|
import lustre/element.{type Element}
|
||||||
|
import gloss/rendering/templates.{
|
||||||
|
type BaseRenderer, type ListPageRenderer, type PostContentRenderer,
|
||||||
|
type SinglePostRenderer, ListInfo,
|
||||||
|
}
|
||||||
|
import gloss/rendering/database.{
|
||||||
|
type Database as RenderDatabase, type RenderedPost, type RenderedSinglePost,
|
||||||
|
ListPage, RenderedPost, RenderedSinglePost,
|
||||||
|
} as render_database
|
||||||
|
import gloss/models/database.{type Database, type PostID, type PostWithID}
|
||||||
|
import gloss/utils/ordered_tree
|
||||||
|
import gloss/config.{type Configuration}
|
||||||
|
import gloss/utils/date
|
||||||
|
|
||||||
|
pub type Renderers {
|
||||||
|
Renderers(
|
||||||
|
base: BaseRenderer,
|
||||||
|
single_post_full: SinglePostRenderer,
|
||||||
|
list_page: ListPageRenderer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(db: Database, config: Configuration) -> RenderDatabase {
|
||||||
|
let renderers =
|
||||||
|
Renderers(
|
||||||
|
base: config.rendering.templates.base(db, config),
|
||||||
|
single_post_full: config.rendering.templates.single_post_full(db, config),
|
||||||
|
list_page: config.rendering.templates.list_page(db, config),
|
||||||
|
)
|
||||||
|
|
||||||
|
let all_posts = database.get_posts_with_ids(db, ordered_tree.Desc)
|
||||||
|
let post_contents =
|
||||||
|
render_post_contents(all_posts, config.rendering.content_renderer)
|
||||||
|
let posts = render_posts(db, post_contents, renderers)
|
||||||
|
let index_pages =
|
||||||
|
render_index_pages(config, all_posts, post_contents, renderers)
|
||||||
|
let tag_pages = render_tag_pages(config, db, post_contents, renderers)
|
||||||
|
let year_pages = render_year_pages(config, db, post_contents, renderers)
|
||||||
|
let month_pages = render_month_pages(config, db, post_contents, renderers)
|
||||||
|
|
||||||
|
render_database.Database(
|
||||||
|
orig: db,
|
||||||
|
posts: post_contents,
|
||||||
|
single_posts: posts,
|
||||||
|
index: [],
|
||||||
|
index_pages: index_pages,
|
||||||
|
tag_pages: tag_pages,
|
||||||
|
year_pages: year_pages,
|
||||||
|
month_pages: month_pages,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_post_contents(
|
||||||
|
all_posts: List(database.PostWithID),
|
||||||
|
renderer: PostContentRenderer,
|
||||||
|
) -> Dict(PostID, RenderedPost) {
|
||||||
|
let posts =
|
||||||
|
all_posts
|
||||||
|
|> list.map(fn(post_with_id) {
|
||||||
|
let content = renderer(post_with_id.post)
|
||||||
|
#(post_with_id.id, RenderedPost(post_with_id.post, content: content))
|
||||||
|
})
|
||||||
|
|
||||||
|
dict.from_list(posts)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_posts(
|
||||||
|
db: Database,
|
||||||
|
post_contents: Dict(PostID, RenderedPost),
|
||||||
|
renderers: Renderers,
|
||||||
|
) -> List(RenderedSinglePost) {
|
||||||
|
let all_posts = database.get_posts_with_ids(db, ordered_tree.Desc)
|
||||||
|
|
||||||
|
all_posts
|
||||||
|
|> list.map(fn(post_with_id) {
|
||||||
|
let assert Ok(content) = dict.get(post_contents, post_with_id.id)
|
||||||
|
let rendered =
|
||||||
|
renderers.base(
|
||||||
|
renderers.single_post_full(content),
|
||||||
|
post_with_id.post.title,
|
||||||
|
)
|
||||||
|
RenderedSinglePost(post_with_id.post, rendered)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_index_pages(
|
||||||
|
config: Configuration,
|
||||||
|
posts: List(database.PostWithID),
|
||||||
|
posts_with_contents: Dict(PostID, RenderedPost),
|
||||||
|
renderers: Renderers,
|
||||||
|
) {
|
||||||
|
pageify_posts(
|
||||||
|
posts,
|
||||||
|
config,
|
||||||
|
posts_with_contents,
|
||||||
|
renderers,
|
||||||
|
"",
|
||||||
|
element.none(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_tag_pages(
|
||||||
|
config: Configuration,
|
||||||
|
db: Database,
|
||||||
|
posts_with_contents: Dict(PostID, RenderedPost),
|
||||||
|
renderers: Renderers,
|
||||||
|
) {
|
||||||
|
let tags = database.tags(db)
|
||||||
|
|
||||||
|
dict.map_values(tags, fn(tag, posts) {
|
||||||
|
let posts = ordered_tree.to_list(posts, ordered_tree.Desc)
|
||||||
|
pageify_posts(
|
||||||
|
posts,
|
||||||
|
config,
|
||||||
|
posts_with_contents,
|
||||||
|
renderers,
|
||||||
|
tag,
|
||||||
|
element.none(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_year_pages(
|
||||||
|
config: Configuration,
|
||||||
|
db: Database,
|
||||||
|
posts_with_contents: Dict(PostID, RenderedPost),
|
||||||
|
renderers: Renderers,
|
||||||
|
) {
|
||||||
|
let years = database.years(db)
|
||||||
|
|
||||||
|
dict.map_values(years, fn(year, posts) {
|
||||||
|
let posts =
|
||||||
|
date.months
|
||||||
|
|> list.reverse()
|
||||||
|
|> list.map(fn(month) {
|
||||||
|
posts
|
||||||
|
|> dict.get(month)
|
||||||
|
|> result.map(ordered_tree.to_list(_, ordered_tree.Desc))
|
||||||
|
|> result.unwrap([])
|
||||||
|
})
|
||||||
|
|> list.flatten()
|
||||||
|
|
||||||
|
pageify_posts(
|
||||||
|
posts,
|
||||||
|
config,
|
||||||
|
posts_with_contents,
|
||||||
|
renderers,
|
||||||
|
"Archives for " <> int.to_string(year),
|
||||||
|
element.none(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_month_pages(
|
||||||
|
config: Configuration,
|
||||||
|
db: Database,
|
||||||
|
posts_with_contents: Dict(PostID, RenderedPost),
|
||||||
|
renderers: Renderers,
|
||||||
|
) {
|
||||||
|
let years = database.years(db)
|
||||||
|
|
||||||
|
dict.fold(years, dict.new(), fn(acc, year, year_posts) {
|
||||||
|
dict.fold(year_posts, acc, fn(acc2, month, month_posts) {
|
||||||
|
let posts = ordered_tree.to_list(month_posts, ordered_tree.Desc)
|
||||||
|
dict.insert(
|
||||||
|
acc2,
|
||||||
|
#(year, month),
|
||||||
|
pageify_posts(
|
||||||
|
posts,
|
||||||
|
config,
|
||||||
|
posts_with_contents,
|
||||||
|
renderers,
|
||||||
|
"Archives for "
|
||||||
|
<> date.month_to_string(month)
|
||||||
|
<> " "
|
||||||
|
<> int.to_string(year),
|
||||||
|
element.none(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pageify_posts(
|
||||||
|
posts: List(PostWithID),
|
||||||
|
config: Configuration,
|
||||||
|
posts_with_contents: Dict(PostID, RenderedPost),
|
||||||
|
renderers: Renderers,
|
||||||
|
title_prefix: String,
|
||||||
|
extra_header: Element(Nil),
|
||||||
|
) {
|
||||||
|
let posts = list.sized_chunk(posts, config.rendering.posts_per_page)
|
||||||
|
let total_pages = list.length(posts)
|
||||||
|
list.index_map(posts, fn(page_posts, index) {
|
||||||
|
let page = index + 1
|
||||||
|
|
||||||
|
let info =
|
||||||
|
ListInfo(
|
||||||
|
current_page: page,
|
||||||
|
total_pages: total_pages,
|
||||||
|
posts: list.map(page_posts, fn(post_with_id) {
|
||||||
|
let assert Ok(post) = dict.get(posts_with_contents, post_with_id.id)
|
||||||
|
post
|
||||||
|
}),
|
||||||
|
extra_header: extra_header,
|
||||||
|
)
|
||||||
|
|
||||||
|
let page_content = renderers.base(renderers.list_page(info), title_prefix)
|
||||||
|
ListPage(page: page, content: page_content)
|
||||||
|
})
|
||||||
|
}
|
38
src/gloss/rendering/database.gleam
Normal file
38
src/gloss/rendering/database.gleam
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import gleam/option.{type Option}
|
||||||
|
import gleam/dict.{type Dict}
|
||||||
|
import lustre/element.{type Element}
|
||||||
|
import gloss/models/database.{type Database as OrigDatabase, type PostID} as _
|
||||||
|
import gloss/models/post.{type Post}
|
||||||
|
import gloss/utils/date.{type Month}
|
||||||
|
|
||||||
|
pub type PostList =
|
||||||
|
List(PostID)
|
||||||
|
|
||||||
|
pub type RenderedContent {
|
||||||
|
RenderedContent(full: String, short: Option(String))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RenderedPost {
|
||||||
|
RenderedPost(orig: Post, content: RenderedContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RenderedSinglePost {
|
||||||
|
RenderedSinglePost(orig: Post, content: Element(Nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RenderedPage {
|
||||||
|
ListPage(page: Int, content: Element(Nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Database {
|
||||||
|
Database(
|
||||||
|
orig: OrigDatabase,
|
||||||
|
posts: Dict(PostID, RenderedPost),
|
||||||
|
single_posts: List(RenderedSinglePost),
|
||||||
|
index: PostList,
|
||||||
|
index_pages: List(RenderedPage),
|
||||||
|
tag_pages: Dict(String, List(RenderedPage)),
|
||||||
|
year_pages: Dict(Int, List(RenderedPage)),
|
||||||
|
month_pages: Dict(#(Int, Month), List(RenderedPage)),
|
||||||
|
)
|
||||||
|
}
|
24
src/gloss/rendering/templates.gleam
Normal file
24
src/gloss/rendering/templates.gleam
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import lustre/element.{type Element}
|
||||||
|
import gloss/models/post.{type Post}
|
||||||
|
import gloss/rendering/database.{type RenderedContent, type RenderedPost} as _
|
||||||
|
|
||||||
|
pub type PostContentRenderer =
|
||||||
|
fn(Post) -> RenderedContent
|
||||||
|
|
||||||
|
pub type BaseRenderer =
|
||||||
|
fn(Element(Nil), String) -> Element(Nil)
|
||||||
|
|
||||||
|
pub type SinglePostRenderer =
|
||||||
|
fn(RenderedPost) -> Element(Nil)
|
||||||
|
|
||||||
|
pub type ListInfo {
|
||||||
|
ListInfo(
|
||||||
|
current_page: Int,
|
||||||
|
total_pages: Int,
|
||||||
|
posts: List(RenderedPost),
|
||||||
|
extra_header: Element(Nil),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ListPageRenderer =
|
||||||
|
fn(ListInfo) -> Element(Nil)
|
184
src/gloss/rendering/templates/base.gleam
Normal file
184
src/gloss/rendering/templates/base.gleam
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import gleam/dict
|
||||||
|
import gleam/list
|
||||||
|
import gleam/int
|
||||||
|
import gleam/float
|
||||||
|
import gleam/string
|
||||||
|
import lustre/element.{type Element, text}
|
||||||
|
import lustre/element/html.{
|
||||||
|
a, body, footer, h1, head, header, html, li, link, main, meta, nav, p, section,
|
||||||
|
title, ul,
|
||||||
|
}
|
||||||
|
import lustre/attribute.{attribute, href, id, rel, role, style}
|
||||||
|
import gloss/models/database.{type Database}
|
||||||
|
import gloss/utils/ordered_tree
|
||||||
|
import gloss/utils/date
|
||||||
|
import gloss/config.{type Configuration}
|
||||||
|
|
||||||
|
const tag_min_size = 0.5
|
||||||
|
|
||||||
|
pub type PreRendered {
|
||||||
|
PreRendered(pages: Element(Nil), tags: Element(Nil), archives: Element(Nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate(db: Database, config: Configuration) {
|
||||||
|
let pre_rendered = pre_render(db, config)
|
||||||
|
fn(inner: Element(Nil), title_prefix: String) {
|
||||||
|
view(db, config, pre_rendered, inner, title_prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pre_render(db, config) -> PreRendered {
|
||||||
|
PreRendered(
|
||||||
|
pages: element.none(),
|
||||||
|
tags: tags(db, config),
|
||||||
|
archives: archives(db, config),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(
|
||||||
|
_db: Database,
|
||||||
|
config: Configuration,
|
||||||
|
pre_rendered: PreRendered,
|
||||||
|
inner: Element(Nil),
|
||||||
|
title_prefix: String,
|
||||||
|
) {
|
||||||
|
let title_text = case title_prefix {
|
||||||
|
"" -> config.blog_name
|
||||||
|
prefix -> prefix <> " · " <> config.blog_name
|
||||||
|
}
|
||||||
|
|
||||||
|
html([], [
|
||||||
|
head([], [
|
||||||
|
meta([attribute("charset", "utf-8")]),
|
||||||
|
title([], title_text),
|
||||||
|
link([href("./css/normalize.css"), rel("stylesheet")]),
|
||||||
|
link([href("./css/magick.css"), rel("stylesheet")]),
|
||||||
|
link([href("./css/custom.css"), rel("stylesheet")]),
|
||||||
|
]),
|
||||||
|
body([], [
|
||||||
|
header([id("title"), role("banner")], [
|
||||||
|
h1([], [
|
||||||
|
a([href(config.paths.html(config.paths.index))], [
|
||||||
|
text(config.blog_name),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
section([id("sidebar")], [
|
||||||
|
nav([id("tags")], [pre_rendered.tags]),
|
||||||
|
nav([id("archives")], [pre_rendered.archives]),
|
||||||
|
]),
|
||||||
|
main([], [inner]),
|
||||||
|
footer([], [
|
||||||
|
p([], [text(config.rendering.copyright)]),
|
||||||
|
p([], [
|
||||||
|
text("Powered by: "),
|
||||||
|
a([href("https://gleam.run/")], [text("Gleam")]),
|
||||||
|
text(" · "),
|
||||||
|
a([href("https://hexdocs.pm/lustre")], [text("Lustre")]),
|
||||||
|
text(" · "),
|
||||||
|
a([href("https://gitlab.com/Nicd/gloss")], [text("Gloss")]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tags(db: Database, config: Configuration) {
|
||||||
|
let tags =
|
||||||
|
db
|
||||||
|
|> database.tags()
|
||||||
|
|> dict.map_values(fn(_key, posts) {
|
||||||
|
int.to_float(ordered_tree.length(posts))
|
||||||
|
})
|
||||||
|
|
||||||
|
let most_posts =
|
||||||
|
tags
|
||||||
|
|> dict.values()
|
||||||
|
|> list.fold(0.0, float.max)
|
||||||
|
|
||||||
|
tags
|
||||||
|
|> dict.to_list()
|
||||||
|
|> list.sort(fn(a, b) {
|
||||||
|
string.compare(string.lowercase(a.0), string.lowercase(b.0))
|
||||||
|
})
|
||||||
|
|> list.map(fn(item) {
|
||||||
|
let #(tag, post_count) = item
|
||||||
|
let percentage =
|
||||||
|
float.round(
|
||||||
|
{
|
||||||
|
{ { post_count /. most_posts } *. { 1.0 -. tag_min_size } }
|
||||||
|
+. tag_min_size
|
||||||
|
}
|
||||||
|
*. 100.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
li([], [
|
||||||
|
a(
|
||||||
|
[
|
||||||
|
href(config.paths.html(config.paths.tag(tag))),
|
||||||
|
style([#("font-size", int.to_string(percentage) <> "%")]),
|
||||||
|
],
|
||||||
|
[text(tag)],
|
||||||
|
),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|> ul([], _)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn archives(db: Database, config: Configuration) {
|
||||||
|
db
|
||||||
|
|> database.years()
|
||||||
|
|> dict.to_list()
|
||||||
|
|> list.sort(fn(a, b) { int.compare(a.0, b.0) })
|
||||||
|
|> list.fold([], fn(year_archive, year) {
|
||||||
|
let #(year, months) = year
|
||||||
|
[
|
||||||
|
li([], [
|
||||||
|
a(
|
||||||
|
[
|
||||||
|
href(
|
||||||
|
config.paths.html(config.paths.list_page(
|
||||||
|
config.paths.year(year),
|
||||||
|
1,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[text(int.to_string(year))],
|
||||||
|
),
|
||||||
|
ul(
|
||||||
|
[],
|
||||||
|
list.fold(date.months, [], fn(month_archive, month) {
|
||||||
|
case dict.get(months, month) {
|
||||||
|
Ok(posts) -> [
|
||||||
|
li([], [
|
||||||
|
a(
|
||||||
|
[
|
||||||
|
href(
|
||||||
|
config.paths.html(config.paths.list_page(
|
||||||
|
config.paths.month(year, month),
|
||||||
|
1,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
text(
|
||||||
|
date.month_to_string(month)
|
||||||
|
<> " ("
|
||||||
|
<> int.to_string(ordered_tree.length(posts))
|
||||||
|
<> ")",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
..month_archive
|
||||||
|
]
|
||||||
|
Error(_) -> month_archive
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
..year_archive
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|> ul([], _)
|
||||||
|
}
|
28
src/gloss/rendering/templates/list_page.gleam
Normal file
28
src/gloss/rendering/templates/list_page.gleam
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import gleam/list
|
||||||
|
import lustre/element
|
||||||
|
import lustre/element/html
|
||||||
|
import lustre/attribute.{class}
|
||||||
|
import gloss/models/database.{type Database}
|
||||||
|
import gloss/rendering/templates.{type ListInfo}
|
||||||
|
import gloss/config.{type Configuration}
|
||||||
|
|
||||||
|
pub fn generate(db: Database, config: Configuration) {
|
||||||
|
let single_post_renderer =
|
||||||
|
config.rendering.templates.single_post_list(db, config)
|
||||||
|
|
||||||
|
fn(info: ListInfo) {
|
||||||
|
let none = element.none()
|
||||||
|
|
||||||
|
html.section(
|
||||||
|
[class("post-list")],
|
||||||
|
list.flatten([
|
||||||
|
case info.extra_header {
|
||||||
|
el if el == none -> []
|
||||||
|
el -> [el]
|
||||||
|
},
|
||||||
|
list.map(info.posts, single_post_renderer),
|
||||||
|
[html.footer([], [])],
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
87
src/gloss/rendering/templates/single_post.gleam
Normal file
87
src/gloss/rendering/templates/single_post.gleam
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import gleam/option
|
||||||
|
import gleam/list
|
||||||
|
import lustre/element/html.{
|
||||||
|
a, article, div, footer, h2, header, li, nav, p, time, ul,
|
||||||
|
}
|
||||||
|
import lustre/element.{type Element, text}
|
||||||
|
import lustre/attribute.{attribute, class, href}
|
||||||
|
import gloss/models/database.{type Database}
|
||||||
|
import gloss/models/post.{type Post}
|
||||||
|
import gloss/rendering/database.{type RenderedPost, RenderedContent} as _
|
||||||
|
import gloss/config.{type Configuration}
|
||||||
|
import gloss/utils/marked
|
||||||
|
import gloss/utils/date
|
||||||
|
import gloss/utils/time
|
||||||
|
import gloss/utils/luxon
|
||||||
|
|
||||||
|
pub fn full_view(db: Database, config: Configuration) {
|
||||||
|
view(_, True, db, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_view(db: Database, config: Configuration) {
|
||||||
|
view(_, False, db, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content_renderer(post: Post) {
|
||||||
|
let full = marked.default_parse(post.content)
|
||||||
|
let short = option.map(post.short_content, marked.default_parse)
|
||||||
|
RenderedContent(full: full, short: short)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(post: RenderedPost, is_full: Bool, _db: Database, config: Configuration) {
|
||||||
|
let post_url = config.paths.html(config.paths.single_post(post.orig))
|
||||||
|
|
||||||
|
let content = case post.content.short, is_full {
|
||||||
|
option.Some(content), False -> content
|
||||||
|
_, _ -> post.content.full
|
||||||
|
}
|
||||||
|
|
||||||
|
article([class("post")], [
|
||||||
|
header([], [
|
||||||
|
wrap_heading(h2([], [text(post.orig.title)]), post_url, is_full),
|
||||||
|
p([class("post__time")], [
|
||||||
|
text("Posted on "),
|
||||||
|
post_time(post.orig),
|
||||||
|
text("."),
|
||||||
|
]),
|
||||||
|
nav([attribute("aria-label", "Tags")], [
|
||||||
|
ul(
|
||||||
|
[],
|
||||||
|
list.map(post.orig.tags, fn(tag) { li([], [a([], [text(tag)])]) }),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
div([attribute("dangerous-unescaped-html", content)], []),
|
||||||
|
case is_full {
|
||||||
|
True -> element.none()
|
||||||
|
False -> footer([], [a([href(post_url)], [text("Read more…")])])
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrap_heading(heading: Element(Nil), post_url: String, is_full: Bool) {
|
||||||
|
case is_full {
|
||||||
|
True -> heading
|
||||||
|
False -> a([href(post_url)], [heading])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_time(post: Post) {
|
||||||
|
let post_date = post.get_date(post)
|
||||||
|
|
||||||
|
let date_str = date.format(post_date)
|
||||||
|
let time_str =
|
||||||
|
option.unwrap(
|
||||||
|
option.map(post.get_time(post), fn(t) { ", " <> time.format(t) }),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
let human_str = date_str <> time_str
|
||||||
|
|
||||||
|
let datetime_str = case post.get_luxon(post) {
|
||||||
|
option.Some(lx) -> luxon.to_iso(lx)
|
||||||
|
option.None -> date.format_iso(post_date)
|
||||||
|
}
|
||||||
|
|
||||||
|
time([attribute("datetime", datetime_str)], [text(human_str)])
|
||||||
|
}
|
4
src/gloss/utils/buffer.gleam
Normal file
4
src/gloss/utils/buffer.gleam
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub type Buffer
|
||||||
|
|
||||||
|
@external(javascript, "../../ffi_buffer.mjs", "to_string")
|
||||||
|
pub fn to_string(buf buf: Buffer) -> String
|
150
src/gloss/utils/date.gleam
Normal file
150
src/gloss/utils/date.gleam
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import gleam/bool
|
||||||
|
import gleam/order.{type Order, Gt, Lt}
|
||||||
|
import gleam/int
|
||||||
|
import gleam/string
|
||||||
|
import gloss/utils/ints/day.{type Day}
|
||||||
|
|
||||||
|
pub type Month {
|
||||||
|
Jan
|
||||||
|
Feb
|
||||||
|
Mar
|
||||||
|
Apr
|
||||||
|
May
|
||||||
|
Jun
|
||||||
|
Jul
|
||||||
|
Aug
|
||||||
|
Sep
|
||||||
|
Oct
|
||||||
|
Nov
|
||||||
|
Dec
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All months in order
|
||||||
|
pub const months = [Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
|
||||||
|
|
||||||
|
/// A date with 1-indexed years and days
|
||||||
|
pub type Date {
|
||||||
|
Date(year: Int, month: Month, day: Day)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_month(month_int: Int) -> Result(Month, Nil) {
|
||||||
|
case month_int {
|
||||||
|
1 -> Ok(Jan)
|
||||||
|
2 -> Ok(Feb)
|
||||||
|
3 -> Ok(Mar)
|
||||||
|
4 -> Ok(Apr)
|
||||||
|
5 -> Ok(May)
|
||||||
|
6 -> Ok(Jun)
|
||||||
|
7 -> Ok(Jul)
|
||||||
|
8 -> Ok(Aug)
|
||||||
|
9 -> Ok(Sep)
|
||||||
|
10 -> Ok(Oct)
|
||||||
|
11 -> Ok(Nov)
|
||||||
|
12 -> Ok(Dec)
|
||||||
|
_other -> Error(Nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn days_in_month(month: Month, year: Int) {
|
||||||
|
case month {
|
||||||
|
Jan -> 31
|
||||||
|
Feb -> {
|
||||||
|
case year % 4 {
|
||||||
|
0 -> {
|
||||||
|
case year % 100 {
|
||||||
|
0 -> {
|
||||||
|
case year % 400 {
|
||||||
|
0 -> 29
|
||||||
|
_ -> 28
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ -> 29
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ -> 28
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Mar -> 31
|
||||||
|
Apr -> 30
|
||||||
|
May -> 31
|
||||||
|
Jun -> 30
|
||||||
|
Jul -> 31
|
||||||
|
Aug -> 31
|
||||||
|
Sep -> 30
|
||||||
|
Oct -> 31
|
||||||
|
Nov -> 30
|
||||||
|
Dec -> 31
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_valid_date(date: Date) -> Bool {
|
||||||
|
let day = day.to_int(date.day)
|
||||||
|
use <- bool.guard(day < 1, False)
|
||||||
|
use <- bool.guard(day <= days_in_month(date.month, date.year), False)
|
||||||
|
True
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare if `a` is before (lower than) `b`.
|
||||||
|
pub fn compare(a: Date, b: Date) -> Order {
|
||||||
|
case a.year, b.year {
|
||||||
|
a_year, b_year if a_year < b_year -> Lt
|
||||||
|
a_year, b_year if a_year > b_year -> Gt
|
||||||
|
_, _ -> {
|
||||||
|
case month_to_int(a.month), month_to_int(b.month) {
|
||||||
|
a_int, b_int if a_int < b_int -> Lt
|
||||||
|
a_int, b_int if a_int > b_int -> Gt
|
||||||
|
_, _ -> day.compare(a.day, b.day)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn month_to_int(month: Month) -> Int {
|
||||||
|
case month {
|
||||||
|
Jan -> 1
|
||||||
|
Feb -> 2
|
||||||
|
Mar -> 3
|
||||||
|
Apr -> 4
|
||||||
|
May -> 5
|
||||||
|
Jun -> 6
|
||||||
|
Jul -> 7
|
||||||
|
Aug -> 8
|
||||||
|
Sep -> 9
|
||||||
|
Oct -> 10
|
||||||
|
Nov -> 11
|
||||||
|
Dec -> 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn month_to_string(month: Month) -> String {
|
||||||
|
case month {
|
||||||
|
Jan -> "January"
|
||||||
|
Feb -> "February"
|
||||||
|
Mar -> "March"
|
||||||
|
Apr -> "April"
|
||||||
|
May -> "May"
|
||||||
|
Jun -> "June"
|
||||||
|
Jul -> "July"
|
||||||
|
Aug -> "August"
|
||||||
|
Sep -> "September"
|
||||||
|
Oct -> "October"
|
||||||
|
Nov -> "November"
|
||||||
|
Dec -> "December"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(date: Date) -> String {
|
||||||
|
int.to_string(day.to_int(date.day))
|
||||||
|
<> " "
|
||||||
|
<> string.slice(month_to_string(date.month), 0, 3)
|
||||||
|
<> " "
|
||||||
|
<> int.to_string(date.year)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_iso(date: Date) -> String {
|
||||||
|
int.to_string(date.year)
|
||||||
|
<> "-"
|
||||||
|
<> string.pad_left(int.to_string(month_to_int(date.month)), 2, "0")
|
||||||
|
<> "-"
|
||||||
|
<> string.pad_left(int.to_string(day.to_int(date.day)), 2, "0")
|
||||||
|
}
|
10
src/gloss/utils/exceptions.gleam
Normal file
10
src/gloss/utils/exceptions.gleam
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
/// A result (of type a) from an external function that can also raise an error
|
||||||
|
/// (of type b).
|
||||||
|
pub type CanRaise(a, b) {
|
||||||
|
CanRaise(value: a, error: b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a callback function (that should call an external function) from one
|
||||||
|
/// that can raise to one that will return a Result.
|
||||||
|
@external(javascript, "../../ffi_exceptions.mjs", "resultify")
|
||||||
|
pub fn resultify(callback callback: fn() -> CanRaise(a, b)) -> Result(a, b)
|
63
src/gloss/utils/fs.gleam
Normal file
63
src/gloss/utils/fs.gleam
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import gleam/result
|
||||||
|
import gleam/list
|
||||||
|
import gleam/javascript/array.{type Array}
|
||||||
|
import gloss/utils/buffer.{type Buffer}
|
||||||
|
import gloss/utils/exceptions.{type CanRaise}
|
||||||
|
|
||||||
|
pub type FSError
|
||||||
|
|
||||||
|
pub fn read_file(path: String) -> Result(String, FSError) {
|
||||||
|
use contents <- result.try(exceptions.resultify(fn() { do_read_file(path) }))
|
||||||
|
|
||||||
|
Ok(buffer.to_string(contents))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readdir(path: String) -> Result(List(String), FSError) {
|
||||||
|
use files <- result.try(exceptions.resultify(fn() { do_readdir(path) }))
|
||||||
|
|
||||||
|
Ok(list.map(array.to_list(files), buffer.to_string))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mkdir(path: String) -> Result(Nil, FSError) {
|
||||||
|
use _undefined <- result.try(exceptions.resultify(fn() { do_mkdir(path) }))
|
||||||
|
|
||||||
|
Ok(Nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mkdir_p(path: String) -> Result(String, FSError) {
|
||||||
|
use created <- result.try(exceptions.resultify(fn() { do_mkdir_p(path) }))
|
||||||
|
|
||||||
|
Ok(created)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exists(path: String) -> Result(Bool, FSError) {
|
||||||
|
use created <- result.try(exceptions.resultify(fn() { do_exists(path) }))
|
||||||
|
|
||||||
|
Ok(created)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_file(path: String, data: String) -> Result(Nil, FSError) {
|
||||||
|
use _undefined <- result.try(
|
||||||
|
exceptions.resultify(fn() { do_write_file(path, data) }),
|
||||||
|
)
|
||||||
|
|
||||||
|
Ok(Nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@external(javascript, "fs", "readFileSync")
|
||||||
|
fn do_read_file(path: String) -> CanRaise(Buffer, FSError)
|
||||||
|
|
||||||
|
@external(javascript, "fs", "readdirSync")
|
||||||
|
fn do_readdir(path: String) -> CanRaise(Array(Buffer), FSError)
|
||||||
|
|
||||||
|
@external(javascript, "fs", "mkdirSync")
|
||||||
|
fn do_mkdir(path: String) -> CanRaise(Nil, FSError)
|
||||||
|
|
||||||
|
@external(javascript, "../../ffi_fs.mjs", "mkdirP")
|
||||||
|
fn do_mkdir_p(path: String) -> CanRaise(String, FSError)
|
||||||
|
|
||||||
|
@external(javascript, "fs", "existsSync")
|
||||||
|
fn do_exists(path: String) -> CanRaise(Bool, FSError)
|
||||||
|
|
||||||
|
@external(javascript, "fs", "writeFileSync")
|
||||||
|
fn do_write_file(path: String, data: String) -> CanRaise(Nil, FSError)
|
44
src/gloss/utils/ints/day.gleam
Normal file
44
src/gloss/utils/ints/day.gleam
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import bigi.{type BigInt}
|
||||||
|
import ranged_int/interface.{type Interface, Interface}
|
||||||
|
|
||||||
|
const max_limit = 31
|
||||||
|
|
||||||
|
const min_limit = 1
|
||||||
|
|
||||||
|
const iface: Interface(Day, interface.Overflowable) = Interface(
|
||||||
|
from_bigint_unsafe: from_bigint_unsafe,
|
||||||
|
to_bigint: to_bigint,
|
||||||
|
limits: limits,
|
||||||
|
)
|
||||||
|
|
||||||
|
pub opaque type Day {
|
||||||
|
Day(data: BigInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_bigint(value: Day) {
|
||||||
|
value.data
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_int(value: Day) {
|
||||||
|
let assert Ok(int) = bigi.to_int(to_bigint(value))
|
||||||
|
int
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_int(value: Int) {
|
||||||
|
interface.from_bigint(bigi.from_int(value), iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compare(a: Day, b: Day) {
|
||||||
|
interface.compare(a, b, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn limits() {
|
||||||
|
interface.overflowable_limits(
|
||||||
|
bigi.from_int(min_limit),
|
||||||
|
bigi.from_int(max_limit),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_bigint_unsafe(value: BigInt) {
|
||||||
|
Day(data: value)
|
||||||
|
}
|
44
src/gloss/utils/ints/hour.gleam
Normal file
44
src/gloss/utils/ints/hour.gleam
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import bigi.{type BigInt}
|
||||||
|
import ranged_int/interface.{type Interface, Interface}
|
||||||
|
|
||||||
|
const max_limit = 23
|
||||||
|
|
||||||
|
const min_limit = 0
|
||||||
|
|
||||||
|
const iface: Interface(Hour, interface.Overflowable) = Interface(
|
||||||
|
from_bigint_unsafe: from_bigint_unsafe,
|
||||||
|
to_bigint: to_bigint,
|
||||||
|
limits: limits,
|
||||||
|
)
|
||||||
|
|
||||||
|
pub opaque type Hour {
|
||||||
|
Hour(data: BigInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_bigint(value: Hour) {
|
||||||
|
value.data
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_int(value: Hour) {
|
||||||
|
let assert Ok(int) = bigi.to_int(to_bigint(value))
|
||||||
|
int
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_int(value: Int) {
|
||||||
|
interface.from_bigint(bigi.from_int(value), iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compare(a: Hour, b: Hour) {
|
||||||
|
interface.compare(a, b, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn limits() {
|
||||||
|
interface.overflowable_limits(
|
||||||
|
bigi.from_int(min_limit),
|
||||||
|
bigi.from_int(max_limit),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_bigint_unsafe(value: BigInt) {
|
||||||
|
Hour(data: value)
|
||||||
|
}
|
44
src/gloss/utils/ints/minute.gleam
Normal file
44
src/gloss/utils/ints/minute.gleam
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import bigi.{type BigInt}
|
||||||
|
import ranged_int/interface.{type Interface, Interface}
|
||||||
|
|
||||||
|
const max_limit = 59
|
||||||
|
|
||||||
|
const min_limit = 0
|
||||||
|
|
||||||
|
const iface: Interface(Minute, interface.Overflowable) = Interface(
|
||||||
|
from_bigint_unsafe: from_bigint_unsafe,
|
||||||
|
to_bigint: to_bigint,
|
||||||
|
limits: limits,
|
||||||
|
)
|
||||||
|
|
||||||
|
pub opaque type Minute {
|
||||||
|
Minute(data: BigInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_bigint(value: Minute) {
|
||||||
|
value.data
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_int(value: Minute) {
|
||||||
|
let assert Ok(int) = bigi.to_int(to_bigint(value))
|
||||||
|
int
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_int(value: Int) {
|
||||||
|
interface.from_bigint(bigi.from_int(value), iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compare(a: Minute, b: Minute) {
|
||||||
|
interface.compare(a, b, iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn limits() {
|
||||||
|
interface.overflowable_limits(
|
||||||
|
bigi.from_int(min_limit),
|
||||||
|
bigi.from_int(max_limit),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_bigint_unsafe(value: BigInt) {
|
||||||
|
Minute(data: value)
|
||||||
|
}
|
21
src/gloss/utils/luxon.gleam
Normal file
21
src/gloss/utils/luxon.gleam
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import gloss/utils/date.{type Date}
|
||||||
|
import gloss/utils/time.{type Time}
|
||||||
|
|
||||||
|
pub type DateTime
|
||||||
|
|
||||||
|
pub fn date_time_in_zone(date: Date, time: Time, tz: String) {
|
||||||
|
let datetime_str = date.format_iso(date) <> "T" <> time.format(time)
|
||||||
|
do_date_time_in_zone(datetime_str, tz)
|
||||||
|
}
|
||||||
|
|
||||||
|
@external(javascript, "../../ffi_luxon.mjs", "dateTimeInZone")
|
||||||
|
fn do_date_time_in_zone(
|
||||||
|
datetime_str: String,
|
||||||
|
tz: String,
|
||||||
|
) -> Result(DateTime, Nil)
|
||||||
|
|
||||||
|
@external(javascript, "../../ffi_luxon.mjs", "toRFC2822")
|
||||||
|
pub fn to_rfc_2822(dt: DateTime) -> String
|
||||||
|
|
||||||
|
@external(javascript, "../../ffi_luxon.mjs", "toISO")
|
||||||
|
pub fn to_iso(dt: DateTime) -> String
|
33
src/gloss/utils/marked.gleam
Normal file
33
src/gloss/utils/marked.gleam
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import gloss/utils/object.{type Object}
|
||||||
|
|
||||||
|
pub type Options =
|
||||||
|
Object
|
||||||
|
|
||||||
|
pub fn new_options() -> Options {
|
||||||
|
object.new()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mangle(options: Options, do_mangle: Bool) -> Options {
|
||||||
|
object.set(options, "mangle", do_mangle)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_header_ids(options: Options, ids: Bool) -> Options {
|
||||||
|
object.set(options, "headerIds", ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_header_prefix(options: Options, prefix: String) -> Options {
|
||||||
|
object.set(options, "headerPrefix", prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_parse(content: String) -> String {
|
||||||
|
let options =
|
||||||
|
new_options()
|
||||||
|
|> set_mangle(False)
|
||||||
|
|> set_header_ids(False)
|
||||||
|
|> set_header_prefix("")
|
||||||
|
|
||||||
|
parse(content, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
@external(javascript, "../../priv/vendor/marked.esm.mjs", "parse")
|
||||||
|
pub fn parse(content content: String, options options: Options) -> String
|
2
src/gloss/utils/meta_url.gleam
Normal file
2
src/gloss/utils/meta_url.gleam
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
@external(javascript, "../../ffi_meta_url.mjs", "metaURL")
|
||||||
|
pub fn get() -> String
|
10
src/gloss/utils/object.gleam
Normal file
10
src/gloss/utils/object.gleam
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
pub type Object
|
||||||
|
|
||||||
|
@external(javascript, "../../ffi_object.mjs", "create")
|
||||||
|
pub fn new() -> Object
|
||||||
|
|
||||||
|
@external(javascript, "../../ffi_object.mjs", "set")
|
||||||
|
pub fn set(object object: Object, prop prop: String, value value: a) -> Object
|
||||||
|
|
||||||
|
@external(javascript, "../../ffi_object.mjs", "get")
|
||||||
|
pub fn get(object object: Object, prop prop: String) -> b
|
112
src/gloss/utils/ordered_tree.gleam
Normal file
112
src/gloss/utils/ordered_tree.gleam
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
//// An ordered unbalanced tree.
|
||||||
|
////
|
||||||
|
//// Ordering of items is maintained as new items are inserted into the tree.
|
||||||
|
//// Sort of a replacement for an ordered list. Worst case performance for
|
||||||
|
//// insertion is O(n).
|
||||||
|
|
||||||
|
import gleam/order.{type Order, Eq, Gt, Lt}
|
||||||
|
|
||||||
|
/// Item ordering for when walking through a tree.
|
||||||
|
pub type WalkOrder {
|
||||||
|
Asc
|
||||||
|
Desc
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Comparator function to resolve the order of items. Must return `Lt` if the
|
||||||
|
/// first argument is before the second, `Gt` if the opposite, or `Eq` if they
|
||||||
|
/// are equal.
|
||||||
|
pub type Comparator(a) =
|
||||||
|
fn(a, a) -> Order
|
||||||
|
|
||||||
|
pub opaque type OrderedTree(a) {
|
||||||
|
OrderedTree(root: Node(a), comparator: Comparator(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node(a) {
|
||||||
|
Empty
|
||||||
|
Node(before: Node(a), after: Node(a), value: a)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new, empty tree, with the given comparator.
|
||||||
|
pub fn new(comparator: Comparator(a)) -> OrderedTree(a) {
|
||||||
|
OrderedTree(root: Empty, comparator: comparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a new item into the tree.
|
||||||
|
pub fn insert(tree: OrderedTree(a), item: a) -> OrderedTree(a) {
|
||||||
|
OrderedTree(..tree, root: do_insert(tree.root, item, tree.comparator))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fold over the elements in the tree in the given order.
|
||||||
|
pub fn fold(
|
||||||
|
over tree: OrderedTree(a),
|
||||||
|
from initial: b,
|
||||||
|
order order: WalkOrder,
|
||||||
|
with fun: fn(b, a) -> b,
|
||||||
|
) -> b {
|
||||||
|
do_fold(tree.root, initial, fun, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_fold(node: Node(a), acc: b, fun: fn(b, a) -> b, order: WalkOrder) -> b {
|
||||||
|
case node {
|
||||||
|
Empty -> acc
|
||||||
|
Node(before: before, after: after, value: value) -> {
|
||||||
|
case order {
|
||||||
|
Desc -> {
|
||||||
|
let afters = do_fold(after, acc, fun, order)
|
||||||
|
do_fold(before, fun(afters, value), fun, order)
|
||||||
|
}
|
||||||
|
Asc -> {
|
||||||
|
let befores = do_fold(before, acc, fun, order)
|
||||||
|
do_fold(after, fun(befores, value), fun, order)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the amount of items in the tree.
|
||||||
|
///
|
||||||
|
/// This operation runs in O(n) time.
|
||||||
|
pub fn length(tree: OrderedTree(a)) -> Int {
|
||||||
|
fold(tree, 0, Asc, fn(acc, _item) { acc + 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_insert(node: Node(a), item: a, comparator: Comparator(a)) -> Node(a) {
|
||||||
|
case node {
|
||||||
|
Empty -> new_node(item)
|
||||||
|
Node(before: before, after: after, value: value) -> {
|
||||||
|
case comparator(value, item) {
|
||||||
|
Lt | Eq ->
|
||||||
|
Node(
|
||||||
|
before: before,
|
||||||
|
after: do_insert(after, item, comparator),
|
||||||
|
value: value,
|
||||||
|
)
|
||||||
|
Gt ->
|
||||||
|
Node(
|
||||||
|
before: do_insert(before, item, comparator),
|
||||||
|
after: after,
|
||||||
|
value: value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_node(item: a) -> Node(a) {
|
||||||
|
Node(before: Empty, after: Empty, value: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the tree as a list in the given list order.
|
||||||
|
pub fn to_list(tree: OrderedTree(a), order: WalkOrder) -> List(a) {
|
||||||
|
fold(
|
||||||
|
tree,
|
||||||
|
[],
|
||||||
|
case order {
|
||||||
|
Asc -> Desc
|
||||||
|
Desc -> Asc
|
||||||
|
},
|
||||||
|
fn(acc, item) { [item, ..acc] },
|
||||||
|
)
|
||||||
|
}
|
2
src/gloss/utils/path.gleam
Normal file
2
src/gloss/utils/path.gleam
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
@external(javascript, "path", "dirname")
|
||||||
|
pub fn dirname(filename: String) -> String
|
9
src/gloss/utils/priv.gleam
Normal file
9
src/gloss/utils/priv.gleam
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import gleam/uri
|
||||||
|
import gloss/utils/meta_url
|
||||||
|
import gloss/utils/path
|
||||||
|
|
||||||
|
pub fn path() -> String {
|
||||||
|
let assert Ok(meta_url) = uri.parse(meta_url.get())
|
||||||
|
|
||||||
|
path.dirname(meta_url.path) <> "/priv"
|
||||||
|
}
|
9
src/gloss/utils/string.gleam
Normal file
9
src/gloss/utils/string.gleam
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import gleam/string
|
||||||
|
|
||||||
|
/// Split the given string at the given index
|
||||||
|
pub fn split_at(str: String, index: Int) -> #(String, String) {
|
||||||
|
let len = string.length(str)
|
||||||
|
let first = string.slice(str, 0, index)
|
||||||
|
let rest = string.slice(str, index, len - index)
|
||||||
|
#(first, rest)
|
||||||
|
}
|
48
src/gloss/utils/time.gleam
Normal file
48
src/gloss/utils/time.gleam
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import gleam/order.{type Order, Eq}
|
||||||
|
import gleam/int
|
||||||
|
import gleam/string
|
||||||
|
import gloss/utils/ints/hour.{type Hour}
|
||||||
|
import gloss/utils/ints/minute.{type Minute}
|
||||||
|
|
||||||
|
pub type Time {
|
||||||
|
Time(hours: Hour, minutes: Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare if `a` is before (lower than) than `b`.
|
||||||
|
pub fn compare(a: Time, b: Time) -> Order {
|
||||||
|
case hour.compare(a.hours, b.hours) {
|
||||||
|
Eq -> minute.compare(a.minutes, b.minutes)
|
||||||
|
other -> other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(str: String) {
|
||||||
|
case string.split(str, ":") {
|
||||||
|
[hours, minutes] ->
|
||||||
|
case int.parse(hours), int.parse(minutes) {
|
||||||
|
Ok(h), Ok(m) ->
|
||||||
|
case hour.from_int(h), minute.from_int(m) {
|
||||||
|
Ok(h), Ok(m) -> Ok(Time(h, m))
|
||||||
|
_, _ -> Error(Nil)
|
||||||
|
}
|
||||||
|
_, _ -> Error(Nil)
|
||||||
|
}
|
||||||
|
_ -> Error(Nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(time: Time) -> String {
|
||||||
|
pad(int.to_string(hour.to_int(time.hours)))
|
||||||
|
<> ":"
|
||||||
|
<> pad(int.to_string(minute.to_int(time.minutes)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nil_time() {
|
||||||
|
let assert Ok(h) = hour.from_int(0)
|
||||||
|
let assert Ok(m) = minute.from_int(0)
|
||||||
|
Time(h, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad(part: String) {
|
||||||
|
string.pad_left(part, 2, "0")
|
||||||
|
}
|
17
src/gloss/utils/uniqid.gleam
Normal file
17
src/gloss/utils/uniqid.gleam
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import bigi.{type BigInt}
|
||||||
|
|
||||||
|
pub type UniqID =
|
||||||
|
BigInt
|
||||||
|
|
||||||
|
pub opaque type Generator {
|
||||||
|
Generator(id: BigInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() -> Generator {
|
||||||
|
Generator(bigi.zero())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(gen: Generator) -> #(UniqID, Generator) {
|
||||||
|
let new = bigi.add(gen.id, bigi.from_int(1))
|
||||||
|
#(new, Generator(new))
|
||||||
|
}
|
74
src/gloss/writer.gleam
Normal file
74
src/gloss/writer.gleam
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import gleam/list
|
||||||
|
import gleam/dict
|
||||||
|
import gleam/result
|
||||||
|
import lustre/ssg
|
||||||
|
import gloss/rendering/database.{type Database} as _
|
||||||
|
import gloss/models/post.{type Post}
|
||||||
|
import gloss/paths/post.{type PostPath} as _
|
||||||
|
import gloss/paths.{type PathConfiguration}
|
||||||
|
|
||||||
|
const default_output = "./output"
|
||||||
|
|
||||||
|
pub type PostPathGenerator =
|
||||||
|
fn(Post) -> PostPath
|
||||||
|
|
||||||
|
pub type Writer =
|
||||||
|
fn(Database, PathConfiguration) -> Result(Nil, WriteError)
|
||||||
|
|
||||||
|
pub type WriteError {
|
||||||
|
WriteError(err: ssg.BuildError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(db: Database, path_conf: PathConfiguration) {
|
||||||
|
let site =
|
||||||
|
ssg.new(default_output)
|
||||||
|
|> ssg.add_static_dir("./assets")
|
||||||
|
|
||||||
|
let single_posts =
|
||||||
|
db.single_posts
|
||||||
|
|> list.map(fn(post) {
|
||||||
|
let path = path_conf.single_post(post.orig)
|
||||||
|
#(path, post.content)
|
||||||
|
})
|
||||||
|
|> dict.from_list()
|
||||||
|
|
||||||
|
let assert [index, ..rest] = db.index_pages
|
||||||
|
|
||||||
|
let site = ssg.add_static_route(site, "/", index.content)
|
||||||
|
|
||||||
|
let site =
|
||||||
|
list.fold(rest, site, fn(acc, page) {
|
||||||
|
let path = path_conf.list_page(path_conf.index, page.page)
|
||||||
|
ssg.add_static_route(acc, path, page.content)
|
||||||
|
})
|
||||||
|
|
||||||
|
let site =
|
||||||
|
dict.fold(db.tag_pages, site, fn(acc, tag, posts) {
|
||||||
|
list.fold(posts, acc, fn(acc2, page) {
|
||||||
|
let path = path_conf.list_page(path_conf.tag(tag), page.page)
|
||||||
|
ssg.add_static_route(acc2, path, page.content)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let site =
|
||||||
|
dict.fold(db.year_pages, site, fn(acc, year, posts) {
|
||||||
|
list.fold(posts, acc, fn(acc2, page) {
|
||||||
|
let path = path_conf.list_page(path_conf.year(year), page.page)
|
||||||
|
ssg.add_static_route(acc2, path, page.content)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let site =
|
||||||
|
dict.fold(db.month_pages, site, fn(acc, year_month, posts) {
|
||||||
|
let #(year, month) = year_month
|
||||||
|
list.fold(posts, acc, fn(acc2, page) {
|
||||||
|
let path = path_conf.list_page(path_conf.month(year, month), page.page)
|
||||||
|
ssg.add_static_route(acc2, path, page.content)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
site
|
||||||
|
|> ssg.add_dynamic_route("/", single_posts, fn(c) { c })
|
||||||
|
|> ssg.build()
|
||||||
|
|> result.map_error(WriteError)
|
||||||
|
}
|
17
src/gloss2.gleam
Normal file
17
src/gloss2.gleam
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import gleam/result
|
||||||
|
import gleam/io
|
||||||
|
import gloss/builder
|
||||||
|
import gloss/config.{type Configuration, Configuration}
|
||||||
|
import gloss/defaults
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
let config = defaults.default_config()
|
||||||
|
let config = Configuration(..config, blog_name: "Random Notes")
|
||||||
|
io.debug(build(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(config: Configuration) {
|
||||||
|
use db <- result.try(builder.parse(config))
|
||||||
|
let posts = builder.render(db, config)
|
||||||
|
builder.write(posts, config)
|
||||||
|
}
|
12
test/gloss2_test.gleam
Normal file
12
test/gloss2_test.gleam
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import gleeunit
|
||||||
|
import gleeunit/should
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
gleeunit.main()
|
||||||
|
}
|
||||||
|
|
||||||
|
// gleeunit test functions end in `_test`
|
||||||
|
pub fn hello_world_test() {
|
||||||
|
1
|
||||||
|
|> should.equal(1)
|
||||||
|
}
|
Loading…
Reference in a new issue