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