commit 8907487e327be6ef7d59f7e0662deea889a9bfe1 Author: Mikko Ahlroth Date: Thu Apr 4 14:42:55 2024 +0300 Initial commit of rewritten version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaacfef --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.beam +*.ez +/build +erl_crash.dump +/output +.DS_Store +/data +node_modules diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..aa5a120 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +gleam 1.0.0 +nodejs 20.10.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..03c021d --- /dev/null +++ b/README.md @@ -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 . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +gleam shell # Run an Erlang shell +``` diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..bae0638 --- /dev/null +++ b/assets/css/custom.css @@ -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; +} diff --git a/assets/css/fonts.css b/assets/css/fonts.css new file mode 100644 index 0000000..0bac21b --- /dev/null +++ b/assets/css/fonts.css @@ -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; +} diff --git a/assets/css/magick.css b/assets/css/magick.css new file mode 100644 index 0000000..891141a --- /dev/null +++ b/assets/css/magick.css @@ -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; +} diff --git a/assets/css/normalize.css b/assets/css/normalize.css new file mode 100644 index 0000000..bb6e2a7 --- /dev/null +++ b/assets/css/normalize.css @@ -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; +} diff --git a/assets/fonts/Averia_Libre/AveriaLibre-Bold.ttf b/assets/fonts/Averia_Libre/AveriaLibre-Bold.ttf new file mode 100644 index 0000000..1e14965 Binary files /dev/null and b/assets/fonts/Averia_Libre/AveriaLibre-Bold.ttf differ diff --git a/assets/fonts/Averia_Libre/AveriaLibre-BoldItalic.ttf b/assets/fonts/Averia_Libre/AveriaLibre-BoldItalic.ttf new file mode 100644 index 0000000..2eb8a22 Binary files /dev/null and b/assets/fonts/Averia_Libre/AveriaLibre-BoldItalic.ttf differ diff --git a/assets/fonts/Averia_Libre/AveriaLibre-Italic.ttf b/assets/fonts/Averia_Libre/AveriaLibre-Italic.ttf new file mode 100644 index 0000000..2f5f4ed Binary files /dev/null and b/assets/fonts/Averia_Libre/AveriaLibre-Italic.ttf differ diff --git a/assets/fonts/Averia_Libre/AveriaLibre-Light.ttf b/assets/fonts/Averia_Libre/AveriaLibre-Light.ttf new file mode 100644 index 0000000..e0d8b6b Binary files /dev/null and b/assets/fonts/Averia_Libre/AveriaLibre-Light.ttf differ diff --git a/assets/fonts/Averia_Libre/AveriaLibre-LightItalic.ttf b/assets/fonts/Averia_Libre/AveriaLibre-LightItalic.ttf new file mode 100644 index 0000000..054e153 Binary files /dev/null and b/assets/fonts/Averia_Libre/AveriaLibre-LightItalic.ttf differ diff --git a/assets/fonts/Averia_Libre/AveriaLibre-Regular.ttf b/assets/fonts/Averia_Libre/AveriaLibre-Regular.ttf new file mode 100644 index 0000000..00675a7 Binary files /dev/null and b/assets/fonts/Averia_Libre/AveriaLibre-Regular.ttf differ diff --git a/assets/fonts/Averia_Libre/OFL.txt b/assets/fonts/Averia_Libre/OFL.txt new file mode 100644 index 0000000..2b79368 --- /dev/null +++ b/assets/fonts/Averia_Libre/OFL.txt @@ -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. diff --git a/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Bold.ttf b/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Bold.ttf new file mode 100644 index 0000000..81e81a0 Binary files /dev/null and b/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Bold.ttf differ diff --git a/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-BoldItalic.ttf b/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-BoldItalic.ttf new file mode 100644 index 0000000..8935c9b Binary files /dev/null and b/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-BoldItalic.ttf differ diff --git a/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Italic.ttf b/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Italic.ttf new file mode 100644 index 0000000..82eb4f1 Binary files /dev/null and b/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Italic.ttf differ diff --git a/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Light.ttf b/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Light.ttf new file mode 100644 index 0000000..6e2e46c Binary files /dev/null and b/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Light.ttf differ diff --git a/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-LightItalic.ttf b/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-LightItalic.ttf new file mode 100644 index 0000000..e845662 Binary files /dev/null and b/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-LightItalic.ttf differ diff --git a/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Regular.ttf b/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Regular.ttf new file mode 100644 index 0000000..63073f3 Binary files /dev/null and b/assets/fonts/Averia_Serif_Libre/AveriaSerifLibre-Regular.ttf differ diff --git a/assets/fonts/Averia_Serif_Libre/OFL.txt b/assets/fonts/Averia_Serif_Libre/OFL.txt new file mode 100644 index 0000000..2b79368 --- /dev/null +++ b/assets/fonts/Averia_Serif_Libre/OFL.txt @@ -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. diff --git a/assets/fonts/Caveat/Caveat-VariableFont_wght.ttf b/assets/fonts/Caveat/Caveat-VariableFont_wght.ttf new file mode 100644 index 0000000..0ae4d99 Binary files /dev/null and b/assets/fonts/Caveat/Caveat-VariableFont_wght.ttf differ diff --git a/assets/fonts/Caveat/OFL.txt b/assets/fonts/Caveat/OFL.txt new file mode 100644 index 0000000..a1bd351 --- /dev/null +++ b/assets/fonts/Caveat/OFL.txt @@ -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. diff --git a/assets/fonts/Caveat/README.txt b/assets/fonts/Caveat/README.txt new file mode 100644 index 0000000..34124c0 --- /dev/null +++ b/assets/fonts/Caveat/README.txt @@ -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. diff --git a/assets/fonts/Caveat/static/Caveat-Bold.ttf b/assets/fonts/Caveat/static/Caveat-Bold.ttf new file mode 100644 index 0000000..64c12c7 Binary files /dev/null and b/assets/fonts/Caveat/static/Caveat-Bold.ttf differ diff --git a/assets/fonts/Caveat/static/Caveat-Medium.ttf b/assets/fonts/Caveat/static/Caveat-Medium.ttf new file mode 100644 index 0000000..513854f Binary files /dev/null and b/assets/fonts/Caveat/static/Caveat-Medium.ttf differ diff --git a/assets/fonts/Caveat/static/Caveat-Regular.ttf b/assets/fonts/Caveat/static/Caveat-Regular.ttf new file mode 100644 index 0000000..1febd9f Binary files /dev/null and b/assets/fonts/Caveat/static/Caveat-Regular.ttf differ diff --git a/assets/fonts/Caveat/static/Caveat-SemiBold.ttf b/assets/fonts/Caveat/static/Caveat-SemiBold.ttf new file mode 100644 index 0000000..e5df6cf Binary files /dev/null and b/assets/fonts/Caveat/static/Caveat-SemiBold.ttf differ diff --git a/assets/fonts/Courier_Prime/CourierPrime-Bold.ttf b/assets/fonts/Courier_Prime/CourierPrime-Bold.ttf new file mode 100644 index 0000000..7e6b222 Binary files /dev/null and b/assets/fonts/Courier_Prime/CourierPrime-Bold.ttf differ diff --git a/assets/fonts/Courier_Prime/CourierPrime-BoldItalic.ttf b/assets/fonts/Courier_Prime/CourierPrime-BoldItalic.ttf new file mode 100644 index 0000000..2e70ab7 Binary files /dev/null and b/assets/fonts/Courier_Prime/CourierPrime-BoldItalic.ttf differ diff --git a/assets/fonts/Courier_Prime/CourierPrime-Italic.ttf b/assets/fonts/Courier_Prime/CourierPrime-Italic.ttf new file mode 100644 index 0000000..15d9463 Binary files /dev/null and b/assets/fonts/Courier_Prime/CourierPrime-Italic.ttf differ diff --git a/assets/fonts/Courier_Prime/CourierPrime-Regular.ttf b/assets/fonts/Courier_Prime/CourierPrime-Regular.ttf new file mode 100644 index 0000000..4af1ff5 Binary files /dev/null and b/assets/fonts/Courier_Prime/CourierPrime-Regular.ttf differ diff --git a/assets/fonts/Courier_Prime/OFL.txt b/assets/fonts/Courier_Prime/OFL.txt new file mode 100644 index 0000000..7072510 --- /dev/null +++ b/assets/fonts/Courier_Prime/OFL.txt @@ -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. diff --git a/assets/fonts/Noto_Sans_Symbols_2/NotoSansSymbols2-Regular.ttf b/assets/fonts/Noto_Sans_Symbols_2/NotoSansSymbols2-Regular.ttf new file mode 100644 index 0000000..90581c1 Binary files /dev/null and b/assets/fonts/Noto_Sans_Symbols_2/NotoSansSymbols2-Regular.ttf differ diff --git a/assets/fonts/Noto_Sans_Symbols_2/OFL.txt b/assets/fonts/Noto_Sans_Symbols_2/OFL.txt new file mode 100644 index 0000000..106e5d8 --- /dev/null +++ b/assets/fonts/Noto_Sans_Symbols_2/OFL.txt @@ -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. diff --git a/assets/fonts/Spectral/OFL.txt b/assets/fonts/Spectral/OFL.txt new file mode 100644 index 0000000..98df38e --- /dev/null +++ b/assets/fonts/Spectral/OFL.txt @@ -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. diff --git a/assets/fonts/Spectral/Spectral-Bold.ttf b/assets/fonts/Spectral/Spectral-Bold.ttf new file mode 100644 index 0000000..1a2852c Binary files /dev/null and b/assets/fonts/Spectral/Spectral-Bold.ttf differ diff --git a/assets/fonts/Spectral/Spectral-BoldItalic.ttf b/assets/fonts/Spectral/Spectral-BoldItalic.ttf new file mode 100644 index 0000000..48d3f0d Binary files /dev/null and b/assets/fonts/Spectral/Spectral-BoldItalic.ttf differ diff --git a/assets/fonts/Spectral/Spectral-ExtraBold.ttf b/assets/fonts/Spectral/Spectral-ExtraBold.ttf new file mode 100644 index 0000000..5fa1618 Binary files /dev/null and b/assets/fonts/Spectral/Spectral-ExtraBold.ttf differ diff --git a/assets/fonts/Spectral/Spectral-ExtraBoldItalic.ttf b/assets/fonts/Spectral/Spectral-ExtraBoldItalic.ttf new file mode 100644 index 0000000..c79829c Binary files /dev/null and b/assets/fonts/Spectral/Spectral-ExtraBoldItalic.ttf differ diff --git a/assets/fonts/Spectral/Spectral-ExtraLight.ttf b/assets/fonts/Spectral/Spectral-ExtraLight.ttf new file mode 100644 index 0000000..a4427ca Binary files /dev/null and b/assets/fonts/Spectral/Spectral-ExtraLight.ttf differ diff --git a/assets/fonts/Spectral/Spectral-ExtraLightItalic.ttf b/assets/fonts/Spectral/Spectral-ExtraLightItalic.ttf new file mode 100644 index 0000000..d15160c Binary files /dev/null and b/assets/fonts/Spectral/Spectral-ExtraLightItalic.ttf differ diff --git a/assets/fonts/Spectral/Spectral-Italic.ttf b/assets/fonts/Spectral/Spectral-Italic.ttf new file mode 100644 index 0000000..f706e13 Binary files /dev/null and b/assets/fonts/Spectral/Spectral-Italic.ttf differ diff --git a/assets/fonts/Spectral/Spectral-Light.ttf b/assets/fonts/Spectral/Spectral-Light.ttf new file mode 100644 index 0000000..70dbaa2 Binary files /dev/null and b/assets/fonts/Spectral/Spectral-Light.ttf differ diff --git a/assets/fonts/Spectral/Spectral-LightItalic.ttf b/assets/fonts/Spectral/Spectral-LightItalic.ttf new file mode 100644 index 0000000..cd04bc8 Binary files /dev/null and b/assets/fonts/Spectral/Spectral-LightItalic.ttf differ diff --git a/assets/fonts/Spectral/Spectral-Medium.ttf b/assets/fonts/Spectral/Spectral-Medium.ttf new file mode 100644 index 0000000..6180c94 Binary files /dev/null and b/assets/fonts/Spectral/Spectral-Medium.ttf differ diff --git a/assets/fonts/Spectral/Spectral-MediumItalic.ttf b/assets/fonts/Spectral/Spectral-MediumItalic.ttf new file mode 100644 index 0000000..a4bc4df Binary files /dev/null and b/assets/fonts/Spectral/Spectral-MediumItalic.ttf differ diff --git a/assets/fonts/Spectral/Spectral-Regular.ttf b/assets/fonts/Spectral/Spectral-Regular.ttf new file mode 100644 index 0000000..b67dca1 Binary files /dev/null and b/assets/fonts/Spectral/Spectral-Regular.ttf differ diff --git a/assets/fonts/Spectral/Spectral-SemiBold.ttf b/assets/fonts/Spectral/Spectral-SemiBold.ttf new file mode 100644 index 0000000..3414cab Binary files /dev/null and b/assets/fonts/Spectral/Spectral-SemiBold.ttf differ diff --git a/assets/fonts/Spectral/Spectral-SemiBoldItalic.ttf b/assets/fonts/Spectral/Spectral-SemiBoldItalic.ttf new file mode 100644 index 0000000..c84a43d Binary files /dev/null and b/assets/fonts/Spectral/Spectral-SemiBoldItalic.ttf differ diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..f4e1211 --- /dev/null +++ b/gleam.toml @@ -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" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..26104d4 --- /dev/null +++ b/manifest.toml @@ -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" } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a24f2a6 --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..aab9fcf --- /dev/null +++ b/package.json @@ -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 ", + "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" + } +} diff --git a/priv/vendor/marked.esm.mjs b/priv/vendor/marked.esm.mjs new file mode 100644 index 0000000..d13a381 --- /dev/null +++ b/priv/vendor/marked.esm.mjs @@ -0,0 +1,2950 @@ +/** + * marked v5.1.0 - a markdown parser + * Copyright (c) 2011-2023, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + +/** + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ + */ + +function getDefaults() { + return { + async: false, + baseUrl: null, + breaks: false, + extensions: null, + gfm: true, + headerIds: true, + headerPrefix: '', + highlight: null, + hooks: null, + langPrefix: 'language-', + mangle: true, + pedantic: false, + renderer: null, + sanitize: false, + sanitizer: null, + silent: false, + smartypants: false, + tokenizer: null, + walkTokens: null, + xhtml: false + }; +} + +let defaults = getDefaults(); + +function changeDefaults(newDefaults) { + defaults = newDefaults; +} + +/** + * Helpers + */ +const escapeTest = /[&<>"']/; +const escapeReplace = new RegExp(escapeTest.source, 'g'); +const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/; +const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g'); +const escapeReplacements = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' +}; +const getEscapeReplacement = (ch) => escapeReplacements[ch]; +function escape(html, encode) { + if (encode) { + if (escapeTest.test(html)) { + return html.replace(escapeReplace, getEscapeReplacement); + } + } else { + if (escapeTestNoEncode.test(html)) { + return html.replace(escapeReplaceNoEncode, getEscapeReplacement); + } + } + + return html; +} + +const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig; + +/** + * @param {string} html + */ +function unescape(html) { + // explicitly match decimal, hex, and named HTML entities + return html.replace(unescapeTest, (_, n) => { + n = n.toLowerCase(); + if (n === 'colon') return ':'; + if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' + ? String.fromCharCode(parseInt(n.substring(2), 16)) + : String.fromCharCode(+n.substring(1)); + } + return ''; + }); +} + +const caret = /(^|[^\[])\^/g; + +/** + * @param {string | RegExp} regex + * @param {string} opt + */ +function edit(regex, opt) { + regex = typeof regex === 'string' ? regex : regex.source; + opt = opt || ''; + const obj = { + replace: (name, val) => { + val = val.source || val; + val = val.replace(caret, '$1'); + regex = regex.replace(name, val); + return obj; + }, + getRegex: () => { + return new RegExp(regex, opt); + } + }; + return obj; +} + +const nonWordAndColonTest = /[^\w:]/g; +const originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i; + +/** + * @param {boolean} sanitize + * @param {string} base + * @param {string} href + */ +function cleanUrl(sanitize, base, href) { + if (sanitize) { + let prot; + try { + prot = decodeURIComponent(unescape(href)) + .replace(nonWordAndColonTest, '') + .toLowerCase(); + } catch (e) { + return null; + } + if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { + return null; + } + } + if (base && !originIndependentUrl.test(href)) { + href = resolveUrl(base, href); + } + try { + href = encodeURI(href).replace(/%25/g, '%'); + } catch (e) { + return null; + } + return href; +} + +const baseUrls = {}; +const justDomain = /^[^:]+:\/*[^/]*$/; +const protocol = /^([^:]+:)[\s\S]*$/; +const domain = /^([^:]+:\/*[^/]*)[\s\S]*$/; + +/** + * @param {string} base + * @param {string} href + */ +function resolveUrl(base, href) { + if (!baseUrls[' ' + base]) { + // we can ignore everything in base after the last slash of its path component, + // but we might need to add _that_ + // https://tools.ietf.org/html/rfc3986#section-3 + if (justDomain.test(base)) { + baseUrls[' ' + base] = base + '/'; + } else { + baseUrls[' ' + base] = rtrim(base, '/', true); + } + } + base = baseUrls[' ' + base]; + const relativeBase = base.indexOf(':') === -1; + + if (href.substring(0, 2) === '//') { + if (relativeBase) { + return href; + } + return base.replace(protocol, '$1') + href; + } else if (href.charAt(0) === '/') { + if (relativeBase) { + return href; + } + return base.replace(domain, '$1') + href; + } else { + return base + href; + } +} + +const noopTest = { exec: function noopTest() {} }; + +function splitCells(tableRow, count) { + // ensure that every cell-delimiting pipe has a space + // before it to distinguish it from an escaped pipe + const row = tableRow.replace(/\|/g, (match, offset, str) => { + let escaped = false, + curr = offset; + while (--curr >= 0 && str[curr] === '\\') escaped = !escaped; + if (escaped) { + // odd number of slashes means | is escaped + // so we leave it alone + return '|'; + } else { + // add space before unescaped | + return ' |'; + } + }), + cells = row.split(/ \|/); + let i = 0; + + // First/last cell in a row cannot be empty if it has no leading/trailing pipe + if (!cells[0].trim()) { cells.shift(); } + if (cells.length > 0 && !cells[cells.length - 1].trim()) { cells.pop(); } + + if (cells.length > count) { + cells.splice(count); + } else { + while (cells.length < count) cells.push(''); + } + + for (; i < cells.length; i++) { + // leading or trailing whitespace is ignored per the gfm spec + cells[i] = cells[i].trim().replace(/\\\|/g, '|'); + } + return cells; +} + +/** + * Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). + * /c*$/ is vulnerable to REDOS. + * + * @param {string} str + * @param {string} c + * @param {boolean} invert Remove suffix of non-c chars instead. Default falsey. + */ +function rtrim(str, c, invert) { + const l = str.length; + if (l === 0) { + return ''; + } + + // Length of suffix matching the invert condition. + let suffLen = 0; + + // Step left until we fail to match the invert condition. + while (suffLen < l) { + const currChar = str.charAt(l - suffLen - 1); + if (currChar === c && !invert) { + suffLen++; + } else if (currChar !== c && invert) { + suffLen++; + } else { + break; + } + } + + return str.slice(0, l - suffLen); +} + +function findClosingBracket(str, b) { + if (str.indexOf(b[1]) === -1) { + return -1; + } + const l = str.length; + let level = 0, + i = 0; + for (; i < l; i++) { + if (str[i] === '\\') { + i++; + } else if (str[i] === b[0]) { + level++; + } else if (str[i] === b[1]) { + level--; + if (level < 0) { + return i; + } + } + } + return -1; +} + +function checkDeprecations(opt, callback) { + if (!opt || opt.silent) { + return; + } + + if (callback) { + console.warn('marked(): callback is deprecated since version 5.0.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/using_pro#async'); + } + + if (opt.sanitize || opt.sanitizer) { + console.warn('marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options'); + } + + if (opt.highlight || opt.langPrefix !== 'language-') { + console.warn('marked(): highlight and langPrefix parameters are deprecated since version 5.0.0, should not be used and will be removed in the future. Instead use https://www.npmjs.com/package/marked-highlight.'); + } + + if (opt.mangle) { + console.warn('marked(): mangle parameter is enabled by default, but is deprecated since version 5.0.0, and will be removed in the future. To clear this warning, install https://www.npmjs.com/package/marked-mangle, or disable by setting `{mangle: false}`.'); + } + + if (opt.baseUrl) { + console.warn('marked(): baseUrl parameter is deprecated since version 5.0.0, should not be used and will be removed in the future. Instead use https://www.npmjs.com/package/marked-base-url.'); + } + + if (opt.smartypants) { + console.warn('marked(): smartypants parameter is deprecated since version 5.0.0, should not be used and will be removed in the future. Instead use https://www.npmjs.com/package/marked-smartypants.'); + } + + if (opt.xhtml) { + console.warn('marked(): xhtml parameter is deprecated since version 5.0.0, should not be used and will be removed in the future. Instead use https://www.npmjs.com/package/marked-xhtml.'); + } + + if (opt.headerIds || opt.headerPrefix) { + console.warn('marked(): headerIds and headerPrefix parameters enabled by default, but are deprecated since version 5.0.0, and will be removed in the future. To clear this warning, install https://www.npmjs.com/package/marked-gfm-heading-id, or disable by setting `{headerIds: false}`.'); + } +} + +function outputLink(cap, link, raw, lexer) { + const href = link.href; + const title = link.title ? escape(link.title) : null; + const text = cap[1].replace(/\\([\[\]])/g, '$1'); + + if (cap[0].charAt(0) !== '!') { + lexer.state.inLink = true; + const token = { + type: 'link', + raw, + href, + title, + text, + tokens: lexer.inlineTokens(text) + }; + lexer.state.inLink = false; + return token; + } + return { + type: 'image', + raw, + href, + title, + text: escape(text) + }; +} + +function indentCodeCompensation(raw, text) { + const matchIndentToCode = raw.match(/^(\s+)(?:```)/); + + if (matchIndentToCode === null) { + return text; + } + + const indentToCode = matchIndentToCode[1]; + + return text + .split('\n') + .map(node => { + const matchIndentInNode = node.match(/^\s+/); + if (matchIndentInNode === null) { + return node; + } + + const [indentInNode] = matchIndentInNode; + + if (indentInNode.length >= indentToCode.length) { + return node.slice(indentToCode.length); + } + + return node; + }) + .join('\n'); +} + +/** + * Tokenizer + */ +class Tokenizer { + constructor(options) { + this.options = options || defaults; + } + + space(src) { + const cap = this.rules.block.newline.exec(src); + if (cap && cap[0].length > 0) { + return { + type: 'space', + raw: cap[0] + }; + } + } + + code(src) { + const cap = this.rules.block.code.exec(src); + if (cap) { + const text = cap[0].replace(/^ {1,4}/gm, ''); + return { + type: 'code', + raw: cap[0], + codeBlockStyle: 'indented', + text: !this.options.pedantic + ? rtrim(text, '\n') + : text + }; + } + } + + fences(src) { + const cap = this.rules.block.fences.exec(src); + if (cap) { + const raw = cap[0]; + const text = indentCodeCompensation(raw, cap[3] || ''); + + return { + type: 'code', + raw, + lang: cap[2] ? cap[2].trim().replace(this.rules.inline._escapes, '$1') : cap[2], + text + }; + } + } + + heading(src) { + const cap = this.rules.block.heading.exec(src); + if (cap) { + let text = cap[2].trim(); + + // remove trailing #s + if (/#$/.test(text)) { + const trimmed = rtrim(text, '#'); + if (this.options.pedantic) { + text = trimmed.trim(); + } else if (!trimmed || / $/.test(trimmed)) { + // CommonMark requires space before trailing #s + text = trimmed.trim(); + } + } + + return { + type: 'heading', + raw: cap[0], + depth: cap[1].length, + text, + tokens: this.lexer.inline(text) + }; + } + } + + hr(src) { + const cap = this.rules.block.hr.exec(src); + if (cap) { + return { + type: 'hr', + raw: cap[0] + }; + } + } + + blockquote(src) { + const cap = this.rules.block.blockquote.exec(src); + if (cap) { + const text = cap[0].replace(/^ *>[ \t]?/gm, ''); + const top = this.lexer.state.top; + this.lexer.state.top = true; + const tokens = this.lexer.blockTokens(text); + this.lexer.state.top = top; + return { + type: 'blockquote', + raw: cap[0], + tokens, + text + }; + } + } + + list(src) { + let cap = this.rules.block.list.exec(src); + if (cap) { + let raw, istask, ischecked, indent, i, blankLine, endsWithBlankLine, + line, nextLine, rawLine, itemContents, endEarly; + + let bull = cap[1].trim(); + const isordered = bull.length > 1; + + const list = { + type: 'list', + raw: '', + ordered: isordered, + start: isordered ? +bull.slice(0, -1) : '', + loose: false, + items: [] + }; + + bull = isordered ? `\\d{1,9}\\${bull.slice(-1)}` : `\\${bull}`; + + if (this.options.pedantic) { + bull = isordered ? bull : '[*+-]'; + } + + // Get next list item + const itemRegex = new RegExp(`^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))`); + + // Check if current bullet point can start a new List Item + while (src) { + endEarly = false; + if (!(cap = itemRegex.exec(src))) { + break; + } + + if (this.rules.block.hr.test(src)) { // End list if bullet was actually HR (possibly move into itemRegex?) + break; + } + + raw = cap[0]; + src = src.substring(raw.length); + + line = cap[2].split('\n', 1)[0].replace(/^\t+/, (t) => ' '.repeat(3 * t.length)); + nextLine = src.split('\n', 1)[0]; + + if (this.options.pedantic) { + indent = 2; + itemContents = line.trimLeft(); + } else { + indent = cap[2].search(/[^ ]/); // Find first non-space char + indent = indent > 4 ? 1 : indent; // Treat indented code blocks (> 4 spaces) as having only 1 indent + itemContents = line.slice(indent); + indent += cap[1].length; + } + + blankLine = false; + + if (!line && /^ *$/.test(nextLine)) { // Items begin with at most one blank line + raw += nextLine + '\n'; + src = src.substring(nextLine.length + 1); + endEarly = true; + } + + if (!endEarly) { + const nextBulletRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`); + const hrRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`); + const fencesBeginRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\`\`\`|~~~)`); + const headingBeginRegex = new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`); + + // Check if following lines should be included in List Item + while (src) { + rawLine = src.split('\n', 1)[0]; + nextLine = rawLine; + + // Re-align to follow commonmark nesting rules + if (this.options.pedantic) { + nextLine = nextLine.replace(/^ {1,4}(?=( {4})*[^ ])/g, ' '); + } + + // End list item if found code fences + if (fencesBeginRegex.test(nextLine)) { + break; + } + + // End list item if found start of new heading + if (headingBeginRegex.test(nextLine)) { + break; + } + + // End list item if found start of new bullet + if (nextBulletRegex.test(nextLine)) { + break; + } + + // Horizontal rule found + if (hrRegex.test(src)) { + break; + } + + if (nextLine.search(/[^ ]/) >= indent || !nextLine.trim()) { // Dedent if possible + itemContents += '\n' + nextLine.slice(indent); + } else { + // not enough indentation + if (blankLine) { + break; + } + + // paragraph continuation unless last line was a different block level element + if (line.search(/[^ ]/) >= 4) { // indented code block + break; + } + if (fencesBeginRegex.test(line)) { + break; + } + if (headingBeginRegex.test(line)) { + break; + } + if (hrRegex.test(line)) { + break; + } + + itemContents += '\n' + nextLine; + } + + if (!blankLine && !nextLine.trim()) { // Check if current line is blank + blankLine = true; + } + + raw += rawLine + '\n'; + src = src.substring(rawLine.length + 1); + line = nextLine.slice(indent); + } + } + + if (!list.loose) { + // If the previous item ended with a blank line, the list is loose + if (endsWithBlankLine) { + list.loose = true; + } else if (/\n *\n *$/.test(raw)) { + endsWithBlankLine = true; + } + } + + // Check for task list items + if (this.options.gfm) { + istask = /^\[[ xX]\] /.exec(itemContents); + if (istask) { + ischecked = istask[0] !== '[ ] '; + itemContents = itemContents.replace(/^\[[ xX]\] +/, ''); + } + } + + list.items.push({ + type: 'list_item', + raw, + task: !!istask, + checked: ischecked, + loose: false, + text: itemContents + }); + + list.raw += raw; + } + + // Do not consume newlines at end of final item. Alternatively, make itemRegex *start* with any newlines to simplify/speed up endsWithBlankLine logic + list.items[list.items.length - 1].raw = raw.trimRight(); + list.items[list.items.length - 1].text = itemContents.trimRight(); + list.raw = list.raw.trimRight(); + + const l = list.items.length; + + // Item child tokens handled here at end because we needed to have the final item to trim it first + for (i = 0; i < l; i++) { + this.lexer.state.top = false; + list.items[i].tokens = this.lexer.blockTokens(list.items[i].text, []); + + if (!list.loose) { + // Check if list should be loose + const spacers = list.items[i].tokens.filter(t => t.type === 'space'); + const hasMultipleLineBreaks = spacers.length > 0 && spacers.some(t => /\n.*\n/.test(t.raw)); + + list.loose = hasMultipleLineBreaks; + } + } + + // Set all items to loose if list is loose + if (list.loose) { + for (i = 0; i < l; i++) { + list.items[i].loose = true; + } + } + + return list; + } + } + + html(src) { + const cap = this.rules.block.html.exec(src); + if (cap) { + const token = { + type: 'html', + block: true, + raw: cap[0], + pre: !this.options.sanitizer + && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'), + text: cap[0] + }; + if (this.options.sanitize) { + const text = this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0]); + token.type = 'paragraph'; + token.text = text; + token.tokens = this.lexer.inline(text); + } + return token; + } + } + + def(src) { + const cap = this.rules.block.def.exec(src); + if (cap) { + const tag = cap[1].toLowerCase().replace(/\s+/g, ' '); + const href = cap[2] ? cap[2].replace(/^<(.*)>$/, '$1').replace(this.rules.inline._escapes, '$1') : ''; + const title = cap[3] ? cap[3].substring(1, cap[3].length - 1).replace(this.rules.inline._escapes, '$1') : cap[3]; + return { + type: 'def', + tag, + raw: cap[0], + href, + title + }; + } + } + + table(src) { + const cap = this.rules.block.table.exec(src); + if (cap) { + const item = { + type: 'table', + header: splitCells(cap[1]).map(c => { return { text: c }; }), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + rows: cap[3] && cap[3].trim() ? cap[3].replace(/\n[ \t]*$/, '').split('\n') : [] + }; + + if (item.header.length === item.align.length) { + item.raw = cap[0]; + + let l = item.align.length; + let i, j, k, row; + for (i = 0; i < l; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } + + l = item.rows.length; + for (i = 0; i < l; i++) { + item.rows[i] = splitCells(item.rows[i], item.header.length).map(c => { return { text: c }; }); + } + + // parse child tokens inside headers and cells + + // header child tokens + l = item.header.length; + for (j = 0; j < l; j++) { + item.header[j].tokens = this.lexer.inline(item.header[j].text); + } + + // cell child tokens + l = item.rows.length; + for (j = 0; j < l; j++) { + row = item.rows[j]; + for (k = 0; k < row.length; k++) { + row[k].tokens = this.lexer.inline(row[k].text); + } + } + + return item; + } + } + } + + lheading(src) { + const cap = this.rules.block.lheading.exec(src); + if (cap) { + return { + type: 'heading', + raw: cap[0], + depth: cap[2].charAt(0) === '=' ? 1 : 2, + text: cap[1], + tokens: this.lexer.inline(cap[1]) + }; + } + } + + paragraph(src) { + const cap = this.rules.block.paragraph.exec(src); + if (cap) { + const text = cap[1].charAt(cap[1].length - 1) === '\n' + ? cap[1].slice(0, -1) + : cap[1]; + return { + type: 'paragraph', + raw: cap[0], + text, + tokens: this.lexer.inline(text) + }; + } + } + + text(src) { + const cap = this.rules.block.text.exec(src); + if (cap) { + return { + type: 'text', + raw: cap[0], + text: cap[0], + tokens: this.lexer.inline(cap[0]) + }; + } + } + + escape(src) { + const cap = this.rules.inline.escape.exec(src); + if (cap) { + return { + type: 'escape', + raw: cap[0], + text: escape(cap[1]) + }; + } + } + + tag(src) { + const cap = this.rules.inline.tag.exec(src); + if (cap) { + if (!this.lexer.state.inLink && /^/i.test(cap[0])) { + this.lexer.state.inLink = false; + } + if (!this.lexer.state.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.lexer.state.inRawBlock = true; + } else if (this.lexer.state.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) { + this.lexer.state.inRawBlock = false; + } + + return { + type: this.options.sanitize + ? 'text' + : 'html', + raw: cap[0], + inLink: this.lexer.state.inLink, + inRawBlock: this.lexer.state.inRawBlock, + block: false, + text: this.options.sanitize + ? (this.options.sanitizer + ? this.options.sanitizer(cap[0]) + : escape(cap[0])) + : cap[0] + }; + } + } + + link(src) { + const cap = this.rules.inline.link.exec(src); + if (cap) { + const trimmedUrl = cap[2].trim(); + if (!this.options.pedantic && /^$/.test(trimmedUrl))) { + return; + } + + // ending angle bracket cannot be escaped + const rtrimSlash = rtrim(trimmedUrl.slice(0, -1), '\\'); + if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) { + return; + } + } else { + // find closing parenthesis + const lastParenIndex = findClosingBracket(cap[2], '()'); + if (lastParenIndex > -1) { + const start = cap[0].indexOf('!') === 0 ? 5 : 4; + const linkLen = start + cap[1].length + lastParenIndex; + cap[2] = cap[2].substring(0, lastParenIndex); + cap[0] = cap[0].substring(0, linkLen).trim(); + cap[3] = ''; + } + } + let href = cap[2]; + let title = ''; + if (this.options.pedantic) { + // split pedantic href and title + const link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href); + + if (link) { + href = link[1]; + title = link[3]; + } + } else { + title = cap[3] ? cap[3].slice(1, -1) : ''; + } + + href = href.trim(); + if (/^$/.test(trimmedUrl))) { + // pedantic allows starting angle bracket without ending angle bracket + href = href.slice(1); + } else { + href = href.slice(1, -1); + } + } + return outputLink(cap, { + href: href ? href.replace(this.rules.inline._escapes, '$1') : href, + title: title ? title.replace(this.rules.inline._escapes, '$1') : title + }, cap[0], this.lexer); + } + } + + reflink(src, links) { + let cap; + if ((cap = this.rules.inline.reflink.exec(src)) + || (cap = this.rules.inline.nolink.exec(src))) { + let link = (cap[2] || cap[1]).replace(/\s+/g, ' '); + link = links[link.toLowerCase()]; + if (!link) { + const text = cap[0].charAt(0); + return { + type: 'text', + raw: text, + text + }; + } + return outputLink(cap, link, cap[0], this.lexer); + } + } + + emStrong(src, maskedSrc, prevChar = '') { + let match = this.rules.inline.emStrong.lDelim.exec(src); + if (!match) return; + + // _ can't be between two alphanumerics. \p{L}\p{N} includes non-english alphabet/numbers as well + if (match[3] && prevChar.match(/[\p{L}\p{N}]/u)) return; + + const nextChar = match[1] || match[2] || ''; + + if (!nextChar || !prevChar || this.rules.inline.punctuation.exec(prevChar)) { + const lLength = match[0].length - 1; + let rDelim, rLength, delimTotal = lLength, midDelimTotal = 0; + + const endReg = match[0][0] === '*' ? this.rules.inline.emStrong.rDelimAst : this.rules.inline.emStrong.rDelimUnd; + endReg.lastIndex = 0; + + // Clip maskedSrc to same section of string as src (move to lexer?) + maskedSrc = maskedSrc.slice(-1 * src.length + lLength); + + while ((match = endReg.exec(maskedSrc)) != null) { + rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6]; + + if (!rDelim) continue; // skip single * in __abc*abc__ + + rLength = rDelim.length; + + if (match[3] || match[4]) { // found another Left Delim + delimTotal += rLength; + continue; + } else if (match[5] || match[6]) { // either Left or Right Delim + if (lLength % 3 && !((lLength + rLength) % 3)) { + midDelimTotal += rLength; + continue; // CommonMark Emphasis Rules 9-10 + } + } + + delimTotal -= rLength; + + if (delimTotal > 0) continue; // Haven't found enough closing delimiters + + // Remove extra characters. *a*** -> *a* + rLength = Math.min(rLength, rLength + delimTotal + midDelimTotal); + + const raw = src.slice(0, lLength + match.index + rLength + 1); + + // Create `em` if smallest delimiter has odd char count. *a*** + if (Math.min(lLength, rLength) % 2) { + const text = raw.slice(1, -1); + return { + type: 'em', + raw, + text, + tokens: this.lexer.inlineTokens(text) + }; + } + + // Create 'strong' if smallest delimiter has even char count. **a*** + const text = raw.slice(2, -2); + return { + type: 'strong', + raw, + text, + tokens: this.lexer.inlineTokens(text) + }; + } + } + } + + codespan(src) { + const cap = this.rules.inline.code.exec(src); + if (cap) { + let text = cap[2].replace(/\n/g, ' '); + const hasNonSpaceChars = /[^ ]/.test(text); + const hasSpaceCharsOnBothEnds = /^ /.test(text) && / $/.test(text); + if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) { + text = text.substring(1, text.length - 1); + } + text = escape(text, true); + return { + type: 'codespan', + raw: cap[0], + text + }; + } + } + + br(src) { + const cap = this.rules.inline.br.exec(src); + if (cap) { + return { + type: 'br', + raw: cap[0] + }; + } + } + + del(src) { + const cap = this.rules.inline.del.exec(src); + if (cap) { + return { + type: 'del', + raw: cap[0], + text: cap[2], + tokens: this.lexer.inlineTokens(cap[2]) + }; + } + } + + autolink(src, mangle) { + const cap = this.rules.inline.autolink.exec(src); + if (cap) { + let text, href; + if (cap[2] === '@') { + text = escape(this.options.mangle ? mangle(cap[1]) : cap[1]); + href = 'mailto:' + text; + } else { + text = escape(cap[1]); + href = text; + } + + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text + } + ] + }; + } + } + + url(src, mangle) { + let cap; + if (cap = this.rules.inline.url.exec(src)) { + let text, href; + if (cap[2] === '@') { + text = escape(this.options.mangle ? mangle(cap[0]) : cap[0]); + href = 'mailto:' + text; + } else { + // do extended autolink path validation + let prevCapZero; + do { + prevCapZero = cap[0]; + cap[0] = this.rules.inline._backpedal.exec(cap[0])[0]; + } while (prevCapZero !== cap[0]); + text = escape(cap[0]); + if (cap[1] === 'www.') { + href = 'http://' + cap[0]; + } else { + href = cap[0]; + } + } + return { + type: 'link', + raw: cap[0], + text, + href, + tokens: [ + { + type: 'text', + raw: text, + text + } + ] + }; + } + } + + inlineText(src, smartypants) { + const cap = this.rules.inline.text.exec(src); + if (cap) { + let text; + if (this.lexer.state.inRawBlock) { + text = this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0]; + } else { + text = escape(this.options.smartypants ? smartypants(cap[0]) : cap[0]); + } + return { + type: 'text', + raw: cap[0], + text + }; + } + } +} + +/** + * Block-Level Grammar + */ +const block = { + newline: /^(?: *(?:\n|$))+/, + code: /^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/, + fences: /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/, + hr: /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/, + heading: /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/, + blockquote: /^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/, + list: /^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/, + html: '^ {0,3}(?:' // optional indentation + + '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) + + '|comment[^\\n]*(\\n+|$)' // (2) + + '|<\\?[\\s\\S]*?(?:\\?>\\n*|$)' // (3) + + '|\\n*|$)' // (4) + + '|\\n*|$)' // (5) + + '|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (6) + + '|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) open tag + + '|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)' // (7) closing tag + + ')', + def: /^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/, + table: noopTest, + lheading: /^((?:(?!^bull ).|\n(?!\n|bull ))+?)\n {0,3}(=+|-+) *(?:\n+|$)/, + // regex template, placeholders will be replaced according to different paragraph + // interruption rules of commonmark and the original markdown spec: + _paragraph: /^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/, + text: /^[^\n]+/ +}; + +block._label = /(?!\s*\])(?:\\.|[^\[\]\\])+/; +block._title = /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/; +block.def = edit(block.def) + .replace('label', block._label) + .replace('title', block._title) + .getRegex(); + +block.bullet = /(?:[*+-]|\d{1,9}[.)])/; +block.listItemStart = edit(/^( *)(bull) */) + .replace('bull', block.bullet) + .getRegex(); + +block.list = edit(block.list) + .replace(/bull/g, block.bullet) + .replace('hr', '\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))') + .replace('def', '\\n+(?=' + block.def.source + ')') + .getRegex(); + +block._tag = 'address|article|aside|base|basefont|blockquote|body|caption' + + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption' + + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe' + + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option' + + '|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr' + + '|track|ul'; +block._comment = /|$)/; +block.html = edit(block.html, 'i') + .replace('comment', block._comment) + .replace('tag', block._tag) + .replace('attribute', / +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/) + .getRegex(); + +block.lheading = edit(block.lheading) + .replace(/bull/g, block.bullet) // lists can interrupt + .getRegex(); + +block.paragraph = edit(block._paragraph) + .replace('hr', block.hr) + .replace('heading', ' {0,3}#{1,6} ') + .replace('|lheading', '') // setex headings don't interrupt commonmark paragraphs + .replace('|table', '') + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', block._tag) // pars can be interrupted by type (6) html blocks + .getRegex(); + +block.blockquote = edit(block.blockquote) + .replace('paragraph', block.paragraph) + .getRegex(); + +/** + * Normal Block Grammar + */ + +block.normal = { ...block }; + +/** + * GFM Block Grammar + */ + +block.gfm = { + ...block.normal, + table: '^ *([^\\n ].*\\|.*)\\n' // Header + + ' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?' // Align + + '(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)' // Cells +}; + +block.gfm.table = edit(block.gfm.table) + .replace('hr', block.hr) + .replace('heading', ' {0,3}#{1,6} ') + .replace('blockquote', ' {0,3}>') + .replace('code', ' {4}[^\\n]') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', block._tag) // tables can be interrupted by type (6) html blocks + .getRegex(); + +block.gfm.paragraph = edit(block._paragraph) + .replace('hr', block.hr) + .replace('heading', ' {0,3}#{1,6} ') + .replace('|lheading', '') // setex headings don't interrupt commonmark paragraphs + .replace('table', block.gfm.table) // interrupt paragraphs with table + .replace('blockquote', ' {0,3}>') + .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n') + .replace('list', ' {0,3}(?:[*+-]|1[.)]) ') // only lists starting from 1 can interrupt + .replace('html', ')|<(?:script|pre|style|textarea|!--)') + .replace('tag', block._tag) // pars can be interrupted by type (6) html blocks + .getRegex(); +/** + * Pedantic grammar (original John Gruber's loose markdown specification) + */ + +block.pedantic = { + ...block.normal, + html: edit( + '^ *(?:comment *(?:\\n|\\s*$)' + + '|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)' // closed tag + + '|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))') + .replace('comment', block._comment) + .replace(/tag/g, '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub' + + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)' + + '\\b)\\w+(?!:|[^\\w\\s@]*@)\\b') + .getRegex(), + def: /^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/, + heading: /^(#{1,6})(.*)(?:\n+|$)/, + fences: noopTest, // fences not supported + lheading: /^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/, + paragraph: edit(block.normal._paragraph) + .replace('hr', block.hr) + .replace('heading', ' *#{1,6} *[^\n]') + .replace('lheading', block.lheading) + .replace('blockquote', ' {0,3}>') + .replace('|fences', '') + .replace('|list', '') + .replace('|html', '') + .getRegex() +}; + +/** + * Inline-Level Grammar + */ +const inline = { + escape: /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/, + autolink: /^<(scheme:[^\s\x00-\x1f<>]*|email)>/, + url: noopTest, + tag: '^comment' + + '|^' // self-closing tag + + '|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>' // open tag + + '|^<\\?[\\s\\S]*?\\?>' // processing instruction, e.g. + + '|^' // declaration, e.g. + + '|^', // CDATA section + link: /^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/, + reflink: /^!?\[(label)\]\[(ref)\]/, + nolink: /^!?\[(ref)\](?:\[\])?/, + reflinkSearch: 'reflink|nolink(?!\\()', + emStrong: { + lDelim: /^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/, + // (1) and (2) can only be a Right Delimiter. (3) and (4) can only be Left. (5) and (6) can be either Left or Right. + // | Skip orphan inside strong | Consume to delim | (1) #*** | (2) a***#, a*** | (3) #***a, ***a | (4) ***# | (5) #***# | (6) a***a + rDelimAst: /^[^_*]*?__[^_*]*?\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\*)[punct](\*+)(?=[\s]|$)|[^punct\s](\*+)(?!\*)(?=[punct\s]|$)|(?!\*)[punct\s](\*+)(?=[^punct\s])|[\s](\*+)(?!\*)(?=[punct])|(?!\*)[punct](\*+)(?!\*)(?=[punct])|[^punct\s](\*+)(?=[^punct\s])/, + rDelimUnd: /^[^_*]*?\*\*[^_*]*?_[^_*]*?(?=\*\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\s]|$)|[^punct\s](_+)(?!_)(?=[punct\s]|$)|(?!_)[punct\s](_+)(?=[^punct\s])|[\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])/ // ^- Not allowed for _ + }, + code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/, + br: /^( {2,}|\\)\n(?!\s*$)/, + del: noopTest, + text: /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\`^|~'; +inline.punctuation = edit(inline.punctuation, 'u').replace(/punctuation/g, inline._punctuation).getRegex(); + +// sequences em should skip over [title](link), `code`, +inline.blockSkip = /\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g; +inline.anyPunctuation = /\\[punct]/g; +inline._escapes = /\\([punct])/g; + +inline._comment = edit(block._comment).replace('(?:-->|$)', '-->').getRegex(); + +inline.emStrong.lDelim = edit(inline.emStrong.lDelim, 'u') + .replace(/punct/g, inline._punctuation) + .getRegex(); + +inline.emStrong.rDelimAst = edit(inline.emStrong.rDelimAst, 'gu') + .replace(/punct/g, inline._punctuation) + .getRegex(); + +inline.emStrong.rDelimUnd = edit(inline.emStrong.rDelimUnd, 'gu') + .replace(/punct/g, inline._punctuation) + .getRegex(); + +inline.anyPunctuation = edit(inline.anyPunctuation, 'gu') + .replace(/punct/g, inline._punctuation) + .getRegex(); + +inline._escapes = edit(inline._escapes, 'gu') + .replace(/punct/g, inline._punctuation) + .getRegex(); + +inline._scheme = /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/; +inline._email = /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/; +inline.autolink = edit(inline.autolink) + .replace('scheme', inline._scheme) + .replace('email', inline._email) + .getRegex(); + +inline._attribute = /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/; + +inline.tag = edit(inline.tag) + .replace('comment', inline._comment) + .replace('attribute', inline._attribute) + .getRegex(); + +inline._label = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/; +inline._href = /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/; +inline._title = /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/; + +inline.link = edit(inline.link) + .replace('label', inline._label) + .replace('href', inline._href) + .replace('title', inline._title) + .getRegex(); + +inline.reflink = edit(inline.reflink) + .replace('label', inline._label) + .replace('ref', block._label) + .getRegex(); + +inline.nolink = edit(inline.nolink) + .replace('ref', block._label) + .getRegex(); + +inline.reflinkSearch = edit(inline.reflinkSearch, 'g') + .replace('reflink', inline.reflink) + .replace('nolink', inline.nolink) + .getRegex(); + +/** + * Normal Inline Grammar + */ + +inline.normal = { ...inline }; + +/** + * Pedantic Inline Grammar + */ + +inline.pedantic = { + ...inline.normal, + strong: { + start: /^__|\*\*/, + middle: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, + endAst: /\*\*(?!\*)/g, + endUnd: /__(?!_)/g + }, + em: { + start: /^_|\*/, + middle: /^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/, + endAst: /\*(?!\*)/g, + endUnd: /_(?!_)/g + }, + link: edit(/^!?\[(label)\]\((.*?)\)/) + .replace('label', inline._label) + .getRegex(), + reflink: edit(/^!?\[(label)\]\s*\[([^\]]*)\]/) + .replace('label', inline._label) + .getRegex() +}; + +/** + * GFM Inline Grammar + */ + +inline.gfm = { + ...inline.normal, + escape: edit(inline.escape).replace('])', '~|])').getRegex(), + _extended_email: /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/, + url: /^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, + _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, + del: /^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/, + text: /^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\ 0.5) { + ch = 'x' + ch.toString(16); + } + out += '&#' + ch + ';'; + } + + return out; +} + +/** + * Block Lexer + */ +class Lexer { + constructor(options) { + this.tokens = []; + this.tokens.links = Object.create(null); + this.options = options || defaults; + this.options.tokenizer = this.options.tokenizer || new Tokenizer(); + this.tokenizer = this.options.tokenizer; + this.tokenizer.options = this.options; + this.tokenizer.lexer = this; + this.inlineQueue = []; + this.state = { + inLink: false, + inRawBlock: false, + top: true + }; + + const rules = { + block: block.normal, + inline: inline.normal + }; + + if (this.options.pedantic) { + rules.block = block.pedantic; + rules.inline = inline.pedantic; + } else if (this.options.gfm) { + rules.block = block.gfm; + if (this.options.breaks) { + rules.inline = inline.breaks; + } else { + rules.inline = inline.gfm; + } + } + this.tokenizer.rules = rules; + } + + /** + * Expose Rules + */ + static get rules() { + return { + block, + inline + }; + } + + /** + * Static Lex Method + */ + static lex(src, options) { + const lexer = new Lexer(options); + return lexer.lex(src); + } + + /** + * Static Lex Inline Method + */ + static lexInline(src, options) { + const lexer = new Lexer(options); + return lexer.inlineTokens(src); + } + + /** + * Preprocessing + */ + lex(src) { + src = src + .replace(/\r\n|\r/g, '\n'); + + this.blockTokens(src, this.tokens); + + let next; + while (next = this.inlineQueue.shift()) { + this.inlineTokens(next.src, next.tokens); + } + + return this.tokens; + } + + /** + * Lexing + */ + blockTokens(src, tokens = []) { + if (this.options.pedantic) { + src = src.replace(/\t/g, ' ').replace(/^ +$/gm, ''); + } else { + src = src.replace(/^( *)(\t+)/gm, (_, leading, tabs) => { + return leading + ' '.repeat(tabs.length); + }); + } + + let token, lastToken, cutSrc, lastParagraphClipped; + + while (src) { + if (this.options.extensions + && this.options.extensions.block + && this.options.extensions.block.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + + // newline + if (token = this.tokenizer.space(src)) { + src = src.substring(token.raw.length); + if (token.raw.length === 1 && tokens.length > 0) { + // if there's a single \n as a spacer, it's terminating the last line, + // so move it there so that we don't get unecessary paragraph tags + tokens[tokens.length - 1].raw += '\n'; + } else { + tokens.push(token); + } + continue; + } + + // code + if (token = this.tokenizer.code(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + // An indented code block cannot interrupt a paragraph. + if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } else { + tokens.push(token); + } + continue; + } + + // fences + if (token = this.tokenizer.fences(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // heading + if (token = this.tokenizer.heading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // hr + if (token = this.tokenizer.hr(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // blockquote + if (token = this.tokenizer.blockquote(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // list + if (token = this.tokenizer.list(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // html + if (token = this.tokenizer.html(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // def + if (token = this.tokenizer.def(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && (lastToken.type === 'paragraph' || lastToken.type === 'text')) { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.raw; + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } else if (!this.tokens.links[token.tag]) { + this.tokens.links[token.tag] = { + href: token.href, + title: token.title + }; + } + continue; + } + + // table (gfm) + if (token = this.tokenizer.table(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // lheading + if (token = this.tokenizer.lheading(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // top-level paragraph + // prevent paragraph consuming extensions by clipping 'src' to extension start + cutSrc = src; + if (this.options.extensions && this.options.extensions.startBlock) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startBlock.forEach(function(getStartIndex) { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { startIndex = Math.min(startIndex, tempStart); } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) { + lastToken = tokens[tokens.length - 1]; + if (lastParagraphClipped && lastToken.type === 'paragraph') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } else { + tokens.push(token); + } + lastParagraphClipped = (cutSrc.length !== src.length); + src = src.substring(token.raw.length); + continue; + } + + // text + if (token = this.tokenizer.text(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && lastToken.type === 'text') { + lastToken.raw += '\n' + token.raw; + lastToken.text += '\n' + token.text; + this.inlineQueue.pop(); + this.inlineQueue[this.inlineQueue.length - 1].src = lastToken.text; + } else { + tokens.push(token); + } + continue; + } + + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } else { + throw new Error(errMsg); + } + } + } + + this.state.top = true; + return tokens; + } + + inline(src, tokens = []) { + this.inlineQueue.push({ src, tokens }); + return tokens; + } + + /** + * Lexing/Compiling + */ + inlineTokens(src, tokens = []) { + let token, lastToken, cutSrc; + + // String with links masked to avoid interference with em and strong + let maskedSrc = src; + let match; + let keepPrevChar, prevChar; + + // Mask out reflinks + if (this.tokens.links) { + const links = Object.keys(this.tokens.links); + if (links.length > 0) { + while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) { + if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex); + } + } + } + } + // Mask out other blocks + while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '[' + 'a'.repeat(match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex); + } + + // Mask out escaped characters + while ((match = this.tokenizer.rules.inline.anyPunctuation.exec(maskedSrc)) != null) { + maskedSrc = maskedSrc.slice(0, match.index) + '++' + maskedSrc.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex); + } + + while (src) { + if (!keepPrevChar) { + prevChar = ''; + } + keepPrevChar = false; + + // extensions + if (this.options.extensions + && this.options.extensions.inline + && this.options.extensions.inline.some((extTokenizer) => { + if (token = extTokenizer.call({ lexer: this }, src, tokens)) { + src = src.substring(token.raw.length); + tokens.push(token); + return true; + } + return false; + })) { + continue; + } + + // escape + if (token = this.tokenizer.escape(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // tag + if (token = this.tokenizer.tag(src)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && token.type === 'text' && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } else { + tokens.push(token); + } + continue; + } + + // link + if (token = this.tokenizer.link(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // reflink, nolink + if (token = this.tokenizer.reflink(src, this.tokens.links)) { + src = src.substring(token.raw.length); + lastToken = tokens[tokens.length - 1]; + if (lastToken && token.type === 'text' && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } else { + tokens.push(token); + } + continue; + } + + // em & strong + if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // code + if (token = this.tokenizer.codespan(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // br + if (token = this.tokenizer.br(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // del (gfm) + if (token = this.tokenizer.del(src)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // autolink + if (token = this.tokenizer.autolink(src, mangle)) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // url (gfm) + if (!this.state.inLink && (token = this.tokenizer.url(src, mangle))) { + src = src.substring(token.raw.length); + tokens.push(token); + continue; + } + + // text + // prevent inlineText consuming extensions by clipping 'src' to extension start + cutSrc = src; + if (this.options.extensions && this.options.extensions.startInline) { + let startIndex = Infinity; + const tempSrc = src.slice(1); + let tempStart; + this.options.extensions.startInline.forEach(function(getStartIndex) { + tempStart = getStartIndex.call({ lexer: this }, tempSrc); + if (typeof tempStart === 'number' && tempStart >= 0) { startIndex = Math.min(startIndex, tempStart); } + }); + if (startIndex < Infinity && startIndex >= 0) { + cutSrc = src.substring(0, startIndex + 1); + } + } + if (token = this.tokenizer.inlineText(cutSrc, smartypants)) { + src = src.substring(token.raw.length); + if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started + prevChar = token.raw.slice(-1); + } + keepPrevChar = true; + lastToken = tokens[tokens.length - 1]; + if (lastToken && lastToken.type === 'text') { + lastToken.raw += token.raw; + lastToken.text += token.text; + } else { + tokens.push(token); + } + continue; + } + + if (src) { + const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0); + if (this.options.silent) { + console.error(errMsg); + break; + } else { + throw new Error(errMsg); + } + } + } + + return tokens; + } +} + +/** + * Renderer + */ +class Renderer { + constructor(options) { + this.options = options || defaults; + } + + code(code, infostring, escaped) { + const lang = (infostring || '').match(/\S*/)[0]; + if (this.options.highlight) { + const out = this.options.highlight(code, lang); + if (out != null && out !== code) { + escaped = true; + code = out; + } + } + + code = code.replace(/\n$/, '') + '\n'; + + if (!lang) { + return '
'
+        + (escaped ? code : escape(code, true))
+        + '
\n'; + } + + return '
'
+      + (escaped ? code : escape(code, true))
+      + '
\n'; + } + + /** + * @param {string} quote + */ + blockquote(quote) { + return `
\n${quote}
\n`; + } + + html(html, block) { + return html; + } + + /** + * @param {string} text + * @param {string} level + * @param {string} raw + * @param {any} slugger + */ + heading(text, level, raw, slugger) { + if (this.options.headerIds) { + const id = this.options.headerPrefix + slugger.slug(raw); + return `${text}\n`; + } + + // ignore IDs + return `${text}\n`; + } + + hr() { + return this.options.xhtml ? '
\n' : '
\n'; + } + + list(body, ordered, start) { + const type = ordered ? 'ol' : 'ul', + startatt = (ordered && start !== 1) ? (' start="' + start + '"') : ''; + return '<' + type + startatt + '>\n' + body + '\n'; + } + + /** + * @param {string} text + */ + listitem(text) { + return `
  • ${text}
  • \n`; + } + + checkbox(checked) { + return ' '; + } + + /** + * @param {string} text + */ + paragraph(text) { + return `

    ${text}

    \n`; + } + + /** + * @param {string} header + * @param {string} body + */ + table(header, body) { + if (body) body = `${body}`; + + return '\n' + + '\n' + + header + + '\n' + + body + + '
    \n'; + } + + /** + * @param {string} content + */ + tablerow(content) { + return `\n${content}\n`; + } + + tablecell(content, flags) { + const type = flags.header ? 'th' : 'td'; + const tag = flags.align + ? `<${type} align="${flags.align}">` + : `<${type}>`; + return tag + content + `\n`; + } + + /** + * span level renderer + * @param {string} text + */ + strong(text) { + return `${text}`; + } + + /** + * @param {string} text + */ + em(text) { + return `${text}`; + } + + /** + * @param {string} text + */ + codespan(text) { + return `${text}`; + } + + br() { + return this.options.xhtml ? '
    ' : '
    '; + } + + /** + * @param {string} text + */ + del(text) { + return `${text}`; + } + + /** + * @param {string} href + * @param {string} title + * @param {string} text + */ + link(href, title, text) { + href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); + if (href === null) { + return text; + } + let out = '
    '; + return out; + } + + /** + * @param {string} href + * @param {string} title + * @param {string} text + */ + image(href, title, text) { + href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); + if (href === null) { + return text; + } + + let out = `${text}' : '>'; + return out; + } + + text(text) { + return text; + } +} + +/** + * TextRenderer + * returns only the textual part of the token + */ +class TextRenderer { + // no need for block level renderers + strong(text) { + return text; + } + + em(text) { + return text; + } + + codespan(text) { + return text; + } + + del(text) { + return text; + } + + html(text) { + return text; + } + + text(text) { + return text; + } + + link(href, title, text) { + return '' + text; + } + + image(href, title, text) { + return '' + text; + } + + br() { + return ''; + } +} + +/** + * Slugger generates header id + */ +class Slugger { + constructor() { + this.seen = {}; + } + + /** + * @param {string} value + */ + serialize(value) { + return value + .toLowerCase() + .trim() + // remove html tags + .replace(/<[!\/a-z].*?>/ig, '') + // remove unwanted chars + .replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, '') + .replace(/\s/g, '-'); + } + + /** + * Finds the next safe (unique) slug to use + * @param {string} originalSlug + * @param {boolean} isDryRun + */ + getNextSafeSlug(originalSlug, isDryRun) { + let slug = originalSlug; + let occurenceAccumulator = 0; + if (this.seen.hasOwnProperty(slug)) { + occurenceAccumulator = this.seen[originalSlug]; + do { + occurenceAccumulator++; + slug = originalSlug + '-' + occurenceAccumulator; + } while (this.seen.hasOwnProperty(slug)); + } + if (!isDryRun) { + this.seen[originalSlug] = occurenceAccumulator; + this.seen[slug] = 0; + } + return slug; + } + + /** + * Convert string to unique id + * @param {object} [options] + * @param {boolean} [options.dryrun] Generates the next unique slug without + * updating the internal accumulator. + */ + slug(value, options = {}) { + const slug = this.serialize(value); + return this.getNextSafeSlug(slug, options.dryrun); + } +} + +/** + * Parsing & Compiling + */ +class Parser { + constructor(options) { + this.options = options || defaults; + this.options.renderer = this.options.renderer || new Renderer(); + this.renderer = this.options.renderer; + this.renderer.options = this.options; + this.textRenderer = new TextRenderer(); + this.slugger = new Slugger(); + } + + /** + * Static Parse Method + */ + static parse(tokens, options) { + const parser = new Parser(options); + return parser.parse(tokens); + } + + /** + * Static Parse Inline Method + */ + static parseInline(tokens, options) { + const parser = new Parser(options); + return parser.parseInline(tokens); + } + + /** + * Parse Loop + */ + parse(tokens, top = true) { + let out = '', + i, + j, + k, + l2, + l3, + row, + cell, + header, + body, + token, + ordered, + start, + loose, + itemBody, + item, + checked, + task, + checkbox, + ret; + + const l = tokens.length; + for (i = 0; i < l; i++) { + token = tokens[i]; + + // Run any renderer extensions + if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[token.type]) { + ret = this.options.extensions.renderers[token.type].call({ parser: this }, token); + if (ret !== false || !['space', 'hr', 'heading', 'code', 'table', 'blockquote', 'list', 'html', 'paragraph', 'text'].includes(token.type)) { + out += ret || ''; + continue; + } + } + + switch (token.type) { + case 'space': { + continue; + } + case 'hr': { + out += this.renderer.hr(); + continue; + } + case 'heading': { + out += this.renderer.heading( + this.parseInline(token.tokens), + token.depth, + unescape(this.parseInline(token.tokens, this.textRenderer)), + this.slugger); + continue; + } + case 'code': { + out += this.renderer.code(token.text, + token.lang, + token.escaped); + continue; + } + case 'table': { + header = ''; + + // header + cell = ''; + l2 = token.header.length; + for (j = 0; j < l2; j++) { + cell += this.renderer.tablecell( + this.parseInline(token.header[j].tokens), + { header: true, align: token.align[j] } + ); + } + header += this.renderer.tablerow(cell); + + body = ''; + l2 = token.rows.length; + for (j = 0; j < l2; j++) { + row = token.rows[j]; + + cell = ''; + l3 = row.length; + for (k = 0; k < l3; k++) { + cell += this.renderer.tablecell( + this.parseInline(row[k].tokens), + { header: false, align: token.align[k] } + ); + } + + body += this.renderer.tablerow(cell); + } + out += this.renderer.table(header, body); + continue; + } + case 'blockquote': { + body = this.parse(token.tokens); + out += this.renderer.blockquote(body); + continue; + } + case 'list': { + ordered = token.ordered; + start = token.start; + loose = token.loose; + l2 = token.items.length; + + body = ''; + for (j = 0; j < l2; j++) { + item = token.items[j]; + checked = item.checked; + task = item.task; + + itemBody = ''; + if (item.task) { + checkbox = this.renderer.checkbox(checked); + if (loose) { + if (item.tokens.length > 0 && item.tokens[0].type === 'paragraph') { + item.tokens[0].text = checkbox + ' ' + item.tokens[0].text; + if (item.tokens[0].tokens && item.tokens[0].tokens.length > 0 && item.tokens[0].tokens[0].type === 'text') { + item.tokens[0].tokens[0].text = checkbox + ' ' + item.tokens[0].tokens[0].text; + } + } else { + item.tokens.unshift({ + type: 'text', + text: checkbox + }); + } + } else { + itemBody += checkbox; + } + } + + itemBody += this.parse(item.tokens, loose); + body += this.renderer.listitem(itemBody, task, checked); + } + + out += this.renderer.list(body, ordered, start); + continue; + } + case 'html': { + out += this.renderer.html(token.text, token.block); + continue; + } + case 'paragraph': { + out += this.renderer.paragraph(this.parseInline(token.tokens)); + continue; + } + case 'text': { + body = token.tokens ? this.parseInline(token.tokens) : token.text; + while (i + 1 < l && tokens[i + 1].type === 'text') { + token = tokens[++i]; + body += '\n' + (token.tokens ? this.parseInline(token.tokens) : token.text); + } + out += top ? this.renderer.paragraph(body) : body; + continue; + } + + default: { + const errMsg = 'Token with "' + token.type + '" type was not found.'; + if (this.options.silent) { + console.error(errMsg); + return; + } else { + throw new Error(errMsg); + } + } + } + } + + return out; + } + + /** + * Parse Inline Tokens + */ + parseInline(tokens, renderer) { + renderer = renderer || this.renderer; + let out = '', + i, + token, + ret; + + const l = tokens.length; + for (i = 0; i < l; i++) { + token = tokens[i]; + + // Run any renderer extensions + if (this.options.extensions && this.options.extensions.renderers && this.options.extensions.renderers[token.type]) { + ret = this.options.extensions.renderers[token.type].call({ parser: this }, token); + if (ret !== false || !['escape', 'html', 'link', 'image', 'strong', 'em', 'codespan', 'br', 'del', 'text'].includes(token.type)) { + out += ret || ''; + continue; + } + } + + switch (token.type) { + case 'escape': { + out += renderer.text(token.text); + break; + } + case 'html': { + out += renderer.html(token.text); + break; + } + case 'link': { + out += renderer.link(token.href, token.title, this.parseInline(token.tokens, renderer)); + break; + } + case 'image': { + out += renderer.image(token.href, token.title, token.text); + break; + } + case 'strong': { + out += renderer.strong(this.parseInline(token.tokens, renderer)); + break; + } + case 'em': { + out += renderer.em(this.parseInline(token.tokens, renderer)); + break; + } + case 'codespan': { + out += renderer.codespan(token.text); + break; + } + case 'br': { + out += renderer.br(); + break; + } + case 'del': { + out += renderer.del(this.parseInline(token.tokens, renderer)); + break; + } + case 'text': { + out += renderer.text(token.text); + break; + } + default: { + const errMsg = 'Token with "' + token.type + '" type was not found.'; + if (this.options.silent) { + console.error(errMsg); + return; + } else { + throw new Error(errMsg); + } + } + } + } + return out; + } +} + +class Hooks { + constructor(options) { + this.options = options || defaults; + } + + static passThroughHooks = new Set([ + 'preprocess', + 'postprocess' + ]); + + /** + * Process markdown before marked + */ + preprocess(markdown) { + return markdown; + } + + /** + * Process HTML after marked is finished + */ + postprocess(html) { + return html; + } +} + +class Marked { + defaults = getDefaults(); + options = this.setOptions; + + parse = this.#parseMarkdown(Lexer.lex, Parser.parse); + parseInline = this.#parseMarkdown(Lexer.lexInline, Parser.parseInline); + + Parser = Parser; + parser = Parser.parse; + Renderer = Renderer; + TextRenderer = TextRenderer; + Lexer = Lexer; + lexer = Lexer.lex; + Tokenizer = Tokenizer; + Slugger = Slugger; + Hooks = Hooks; + + constructor(...args) { + this.use(...args); + } + + walkTokens(tokens, callback) { + let values = []; + for (const token of tokens) { + values = values.concat(callback.call(this, token)); + switch (token.type) { + case 'table': { + for (const cell of token.header) { + values = values.concat(this.walkTokens(cell.tokens, callback)); + } + for (const row of token.rows) { + for (const cell of row) { + values = values.concat(this.walkTokens(cell.tokens, callback)); + } + } + break; + } + case 'list': { + values = values.concat(this.walkTokens(token.items, callback)); + break; + } + default: { + if (this.defaults.extensions && this.defaults.extensions.childTokens && this.defaults.extensions.childTokens[token.type]) { // Walk any extensions + this.defaults.extensions.childTokens[token.type].forEach((childTokens) => { + values = values.concat(this.walkTokens(token[childTokens], callback)); + }); + } else if (token.tokens) { + values = values.concat(this.walkTokens(token.tokens, callback)); + } + } + } + } + return values; + } + + use(...args) { + const extensions = this.defaults.extensions || { renderers: {}, childTokens: {} }; + + args.forEach((pack) => { + // copy options to new object + const opts = { ...pack }; + + // set async to true if it was set to true before + opts.async = this.defaults.async || opts.async || false; + + // ==-- Parse "addon" extensions --== // + if (pack.extensions) { + pack.extensions.forEach((ext) => { + if (!ext.name) { + throw new Error('extension name required'); + } + if (ext.renderer) { // Renderer extensions + const prevRenderer = extensions.renderers[ext.name]; + if (prevRenderer) { + // Replace extension with func to run new extension but fall back if false + extensions.renderers[ext.name] = function(...args) { + let ret = ext.renderer.apply(this, args); + if (ret === false) { + ret = prevRenderer.apply(this, args); + } + return ret; + }; + } else { + extensions.renderers[ext.name] = ext.renderer; + } + } + if (ext.tokenizer) { // Tokenizer Extensions + if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) { + throw new Error("extension level must be 'block' or 'inline'"); + } + if (extensions[ext.level]) { + extensions[ext.level].unshift(ext.tokenizer); + } else { + extensions[ext.level] = [ext.tokenizer]; + } + if (ext.start) { // Function to check for start of token + if (ext.level === 'block') { + if (extensions.startBlock) { + extensions.startBlock.push(ext.start); + } else { + extensions.startBlock = [ext.start]; + } + } else if (ext.level === 'inline') { + if (extensions.startInline) { + extensions.startInline.push(ext.start); + } else { + extensions.startInline = [ext.start]; + } + } + } + } + if (ext.childTokens) { // Child tokens to be visited by walkTokens + extensions.childTokens[ext.name] = ext.childTokens; + } + }); + opts.extensions = extensions; + } + + // ==-- Parse "overwrite" extensions --== // + if (pack.renderer) { + const renderer = this.defaults.renderer || new Renderer(this.defaults); + for (const prop in pack.renderer) { + const prevRenderer = renderer[prop]; + // Replace renderer with func to run extension, but fall back if false + renderer[prop] = (...args) => { + let ret = pack.renderer[prop].apply(renderer, args); + if (ret === false) { + ret = prevRenderer.apply(renderer, args); + } + return ret; + }; + } + opts.renderer = renderer; + } + if (pack.tokenizer) { + const tokenizer = this.defaults.tokenizer || new Tokenizer(this.defaults); + for (const prop in pack.tokenizer) { + const prevTokenizer = tokenizer[prop]; + // Replace tokenizer with func to run extension, but fall back if false + tokenizer[prop] = (...args) => { + let ret = pack.tokenizer[prop].apply(tokenizer, args); + if (ret === false) { + ret = prevTokenizer.apply(tokenizer, args); + } + return ret; + }; + } + opts.tokenizer = tokenizer; + } + + // ==-- Parse Hooks extensions --== // + if (pack.hooks) { + const hooks = this.defaults.hooks || new Hooks(); + for (const prop in pack.hooks) { + const prevHook = hooks[prop]; + if (Hooks.passThroughHooks.has(prop)) { + hooks[prop] = (arg) => { + if (this.defaults.async) { + return Promise.resolve(pack.hooks[prop].call(hooks, arg)).then(ret => { + return prevHook.call(hooks, ret); + }); + } + + const ret = pack.hooks[prop].call(hooks, arg); + return prevHook.call(hooks, ret); + }; + } else { + hooks[prop] = (...args) => { + let ret = pack.hooks[prop].apply(hooks, args); + if (ret === false) { + ret = prevHook.apply(hooks, args); + } + return ret; + }; + } + } + opts.hooks = hooks; + } + + // ==-- Parse WalkTokens extensions --== // + if (pack.walkTokens) { + const walkTokens = this.defaults.walkTokens; + opts.walkTokens = function(token) { + let values = []; + values.push(pack.walkTokens.call(this, token)); + if (walkTokens) { + values = values.concat(walkTokens.call(this, token)); + } + return values; + }; + } + + this.defaults = { ...this.defaults, ...opts }; + }); + + return this; + } + + setOptions(opt) { + this.defaults = { ...this.defaults, ...opt }; + return this; + } + + #parseMarkdown(lexer, parser) { + return (src, opt, callback) => { + if (typeof opt === 'function') { + callback = opt; + opt = null; + } + + const origOpt = { ...opt }; + opt = { ...this.defaults, ...origOpt }; + const throwError = this.#onError(opt.silent, opt.async, callback); + + // throw error in case of non string input + if (typeof src === 'undefined' || src === null) { + return throwError(new Error('marked(): input parameter is undefined or null')); + } + if (typeof src !== 'string') { + return throwError(new Error('marked(): input parameter is of type ' + + Object.prototype.toString.call(src) + ', string expected')); + } + + checkDeprecations(opt, callback); + + if (opt.hooks) { + opt.hooks.options = opt; + } + + if (callback) { + const highlight = opt.highlight; + let tokens; + + try { + if (opt.hooks) { + src = opt.hooks.preprocess(src); + } + tokens = lexer(src, opt); + } catch (e) { + return throwError(e); + } + + const done = (err) => { + let out; + + if (!err) { + try { + if (opt.walkTokens) { + this.walkTokens(tokens, opt.walkTokens); + } + out = parser(tokens, opt); + if (opt.hooks) { + out = opt.hooks.postprocess(out); + } + } catch (e) { + err = e; + } + } + + opt.highlight = highlight; + + return err + ? throwError(err) + : callback(null, out); + }; + + if (!highlight || highlight.length < 3) { + return done(); + } + + delete opt.highlight; + + if (!tokens.length) return done(); + + let pending = 0; + this.walkTokens(tokens, (token) => { + if (token.type === 'code') { + pending++; + setTimeout(() => { + highlight(token.text, token.lang, (err, code) => { + if (err) { + return done(err); + } + if (code != null && code !== token.text) { + token.text = code; + token.escaped = true; + } + + pending--; + if (pending === 0) { + done(); + } + }); + }, 0); + } + }); + + if (pending === 0) { + done(); + } + + return; + } + + if (opt.async) { + return Promise.resolve(opt.hooks ? opt.hooks.preprocess(src) : src) + .then(src => lexer(src, opt)) + .then(tokens => opt.walkTokens ? Promise.all(this.walkTokens(tokens, opt.walkTokens)).then(() => tokens) : tokens) + .then(tokens => parser(tokens, opt)) + .then(html => opt.hooks ? opt.hooks.postprocess(html) : html) + .catch(throwError); + } + + try { + if (opt.hooks) { + src = opt.hooks.preprocess(src); + } + const tokens = lexer(src, opt); + if (opt.walkTokens) { + this.walkTokens(tokens, opt.walkTokens); + } + let html = parser(tokens, opt); + if (opt.hooks) { + html = opt.hooks.postprocess(html); + } + return html; + } catch (e) { + return throwError(e); + } + }; + } + + #onError(silent, async, callback) { + return (e) => { + e.message += '\nPlease report this to https://github.com/markedjs/this.'; + + if (silent) { + const msg = '

    An error occurred:

    '
    +          + escape(e.message + '', true)
    +          + '
    '; + if (async) { + return Promise.resolve(msg); + } + if (callback) { + callback(null, msg); + return; + } + return msg; + } + + if (async) { + return Promise.reject(e); + } + if (callback) { + callback(e); + return; + } + throw e; + }; + } +} + +const markedInstance = new Marked(defaults); + +/** + * Marked + */ +function marked(src, opt, callback) { + return markedInstance.parse(src, opt, callback); +} + +/** + * Options + */ + +marked.options = +marked.setOptions = function(opt) { + markedInstance.setOptions(opt); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; +}; + +marked.getDefaults = getDefaults; + +marked.defaults = defaults; + +/** + * Use Extension + */ + +marked.use = function(...args) { + markedInstance.use(...args); + marked.defaults = markedInstance.defaults; + changeDefaults(marked.defaults); + return marked; +}; + +/** + * Run callback for every token + */ + +marked.walkTokens = function(tokens, callback) { + return markedInstance.walkTokens(tokens, callback); +}; + +/** + * Parse Inline + * @param {string} src + */ +marked.parseInline = markedInstance.parseInline; + +/** + * Expose + */ +marked.Parser = Parser; +marked.parser = Parser.parse; +marked.Renderer = Renderer; +marked.TextRenderer = TextRenderer; +marked.Lexer = Lexer; +marked.lexer = Lexer.lex; +marked.Tokenizer = Tokenizer; +marked.Slugger = Slugger; +marked.Hooks = Hooks; +marked.parse = marked; + +const options = marked.options; +const setOptions = marked.setOptions; +const use = marked.use; +const walkTokens = marked.walkTokens; +const parseInline = marked.parseInline; +const parse = marked; +const parser = Parser.parse; +const lexer = Lexer.lex; + +export { Hooks, Lexer, Marked, Parser, Renderer, Slugger, TextRenderer, Tokenizer, defaults, getDefaults, lexer, marked, options, parse, parseInline, parser, setOptions, use, walkTokens }; diff --git a/src/ffi_buffer.mjs b/src/ffi_buffer.mjs new file mode 100644 index 0000000..dd93afb --- /dev/null +++ b/src/ffi_buffer.mjs @@ -0,0 +1,3 @@ +export function to_string(buffer) { + return buffer.toString("utf8"); +} diff --git a/src/ffi_exceptions.mjs b/src/ffi_exceptions.mjs new file mode 100644 index 0000000..1f7718f --- /dev/null +++ b/src/ffi_exceptions.mjs @@ -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); + } +} diff --git a/src/ffi_fs.mjs b/src/ffi_fs.mjs new file mode 100644 index 0000000..08c371b --- /dev/null +++ b/src/ffi_fs.mjs @@ -0,0 +1,5 @@ +import { mkdirSync } from "node:fs"; + +export function mkdirP(path) { + return mkdirSync(path, { recursive: true }); +} diff --git a/src/ffi_luxon.mjs b/src/ffi_luxon.mjs new file mode 100644 index 0000000..9883980 --- /dev/null +++ b/src/ffi_luxon.mjs @@ -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(); +} diff --git a/src/ffi_meta_url.mjs b/src/ffi_meta_url.mjs new file mode 100644 index 0000000..2135c75 --- /dev/null +++ b/src/ffi_meta_url.mjs @@ -0,0 +1,3 @@ +export function metaURL() { + return import.meta.url; +} diff --git a/src/ffi_object.mjs b/src/ffi_object.mjs new file mode 100644 index 0000000..dc278c4 --- /dev/null +++ b/src/ffi_object.mjs @@ -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]; +} diff --git a/src/ffi_url.mjs b/src/ffi_url.mjs new file mode 100644 index 0000000..3c180b6 --- /dev/null +++ b/src/ffi_url.mjs @@ -0,0 +1,3 @@ +export function fromString(str) { + return new URL(str); +} diff --git a/src/gloss/builder.gleam b/src/gloss/builder.gleam new file mode 100644 index 0000000..4bd951b --- /dev/null +++ b/src/gloss/builder.gleam @@ -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) +} diff --git a/src/gloss/config.gleam b/src/gloss/config.gleam new file mode 100644 index 0000000..c9d7592 --- /dev/null +++ b/src/gloss/config.gleam @@ -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, + ) +} diff --git a/src/gloss/defaults.gleam b/src/gloss/defaults.gleam new file mode 100644 index 0000000..ad0bd84 --- /dev/null +++ b/src/gloss/defaults.gleam @@ -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, + ) +} diff --git a/src/gloss/models/database.gleam b/src/gloss/models/database.gleam new file mode 100644 index 0000000..474d897 --- /dev/null +++ b/src/gloss/models/database.gleam @@ -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) +} diff --git a/src/gloss/models/post.gleam b/src/gloss/models/post.gleam new file mode 100644 index 0000000..d0938ea --- /dev/null +++ b/src/gloss/models/post.gleam @@ -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) + } + } +} diff --git a/src/gloss/parser.gleam b/src/gloss/parser.gleam new file mode 100644 index 0000000..184a7a8 --- /dev/null +++ b/src/gloss/parser.gleam @@ -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" +} diff --git a/src/gloss/parser/post.gleam b/src/gloss/parser/post.gleam new file mode 100644 index 0000000..40c6378 --- /dev/null +++ b/src/gloss/parser/post.gleam @@ -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 = "" + +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) +} diff --git a/src/gloss/paths.gleam b/src/gloss/paths.gleam new file mode 100644 index 0000000..ccdeaca --- /dev/null +++ b/src/gloss/paths.gleam @@ -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" +} diff --git a/src/gloss/paths/post.gleam b/src/gloss/paths/post.gleam new file mode 100644 index 0000000..d33b9f1 --- /dev/null +++ b/src/gloss/paths/post.gleam @@ -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") +} diff --git a/src/gloss/renderer.gleam b/src/gloss/renderer.gleam new file mode 100644 index 0000000..e698aeb --- /dev/null +++ b/src/gloss/renderer.gleam @@ -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) + }) +} diff --git a/src/gloss/rendering/database.gleam b/src/gloss/rendering/database.gleam new file mode 100644 index 0000000..8d6ec58 --- /dev/null +++ b/src/gloss/rendering/database.gleam @@ -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)), + ) +} diff --git a/src/gloss/rendering/templates.gleam b/src/gloss/rendering/templates.gleam new file mode 100644 index 0000000..599ad1e --- /dev/null +++ b/src/gloss/rendering/templates.gleam @@ -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) diff --git a/src/gloss/rendering/templates/base.gleam b/src/gloss/rendering/templates/base.gleam new file mode 100644 index 0000000..d99a917 --- /dev/null +++ b/src/gloss/rendering/templates/base.gleam @@ -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([], _) +} diff --git a/src/gloss/rendering/templates/list_page.gleam b/src/gloss/rendering/templates/list_page.gleam new file mode 100644 index 0000000..0e33bdc --- /dev/null +++ b/src/gloss/rendering/templates/list_page.gleam @@ -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([], [])], + ]), + ) + } +} diff --git a/src/gloss/rendering/templates/single_post.gleam b/src/gloss/rendering/templates/single_post.gleam new file mode 100644 index 0000000..9a3bcf3 --- /dev/null +++ b/src/gloss/rendering/templates/single_post.gleam @@ -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)]) +} diff --git a/src/gloss/utils/buffer.gleam b/src/gloss/utils/buffer.gleam new file mode 100644 index 0000000..01fd842 --- /dev/null +++ b/src/gloss/utils/buffer.gleam @@ -0,0 +1,4 @@ +pub type Buffer + +@external(javascript, "../../ffi_buffer.mjs", "to_string") +pub fn to_string(buf buf: Buffer) -> String diff --git a/src/gloss/utils/date.gleam b/src/gloss/utils/date.gleam new file mode 100644 index 0000000..4fa1042 --- /dev/null +++ b/src/gloss/utils/date.gleam @@ -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") +} diff --git a/src/gloss/utils/exceptions.gleam b/src/gloss/utils/exceptions.gleam new file mode 100644 index 0000000..6c9d977 --- /dev/null +++ b/src/gloss/utils/exceptions.gleam @@ -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) diff --git a/src/gloss/utils/fs.gleam b/src/gloss/utils/fs.gleam new file mode 100644 index 0000000..730f653 --- /dev/null +++ b/src/gloss/utils/fs.gleam @@ -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) diff --git a/src/gloss/utils/ints/day.gleam b/src/gloss/utils/ints/day.gleam new file mode 100644 index 0000000..88d3f34 --- /dev/null +++ b/src/gloss/utils/ints/day.gleam @@ -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) +} diff --git a/src/gloss/utils/ints/hour.gleam b/src/gloss/utils/ints/hour.gleam new file mode 100644 index 0000000..b807fcc --- /dev/null +++ b/src/gloss/utils/ints/hour.gleam @@ -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) +} diff --git a/src/gloss/utils/ints/minute.gleam b/src/gloss/utils/ints/minute.gleam new file mode 100644 index 0000000..85d7c47 --- /dev/null +++ b/src/gloss/utils/ints/minute.gleam @@ -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) +} diff --git a/src/gloss/utils/luxon.gleam b/src/gloss/utils/luxon.gleam new file mode 100644 index 0000000..a35921c --- /dev/null +++ b/src/gloss/utils/luxon.gleam @@ -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 diff --git a/src/gloss/utils/marked.gleam b/src/gloss/utils/marked.gleam new file mode 100644 index 0000000..a6d15c4 --- /dev/null +++ b/src/gloss/utils/marked.gleam @@ -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 diff --git a/src/gloss/utils/meta_url.gleam b/src/gloss/utils/meta_url.gleam new file mode 100644 index 0000000..fb8eec2 --- /dev/null +++ b/src/gloss/utils/meta_url.gleam @@ -0,0 +1,2 @@ +@external(javascript, "../../ffi_meta_url.mjs", "metaURL") +pub fn get() -> String diff --git a/src/gloss/utils/object.gleam b/src/gloss/utils/object.gleam new file mode 100644 index 0000000..67248e3 --- /dev/null +++ b/src/gloss/utils/object.gleam @@ -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 diff --git a/src/gloss/utils/ordered_tree.gleam b/src/gloss/utils/ordered_tree.gleam new file mode 100644 index 0000000..0fe8e03 --- /dev/null +++ b/src/gloss/utils/ordered_tree.gleam @@ -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] }, + ) +} diff --git a/src/gloss/utils/path.gleam b/src/gloss/utils/path.gleam new file mode 100644 index 0000000..77c1269 --- /dev/null +++ b/src/gloss/utils/path.gleam @@ -0,0 +1,2 @@ +@external(javascript, "path", "dirname") +pub fn dirname(filename: String) -> String diff --git a/src/gloss/utils/priv.gleam b/src/gloss/utils/priv.gleam new file mode 100644 index 0000000..5ca2b54 --- /dev/null +++ b/src/gloss/utils/priv.gleam @@ -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" +} diff --git a/src/gloss/utils/string.gleam b/src/gloss/utils/string.gleam new file mode 100644 index 0000000..4df7b43 --- /dev/null +++ b/src/gloss/utils/string.gleam @@ -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) +} diff --git a/src/gloss/utils/time.gleam b/src/gloss/utils/time.gleam new file mode 100644 index 0000000..6606afc --- /dev/null +++ b/src/gloss/utils/time.gleam @@ -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") +} diff --git a/src/gloss/utils/uniqid.gleam b/src/gloss/utils/uniqid.gleam new file mode 100644 index 0000000..7f655b3 --- /dev/null +++ b/src/gloss/utils/uniqid.gleam @@ -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)) +} diff --git a/src/gloss/writer.gleam b/src/gloss/writer.gleam new file mode 100644 index 0000000..546d42f --- /dev/null +++ b/src/gloss/writer.gleam @@ -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) +} diff --git a/src/gloss2.gleam b/src/gloss2.gleam new file mode 100644 index 0000000..69730f7 --- /dev/null +++ b/src/gloss2.gleam @@ -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) +} diff --git a/test/gloss2_test.gleam b/test/gloss2_test.gleam new file mode 100644 index 0000000..3831e7a --- /dev/null +++ b/test/gloss2_test.gleam @@ -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) +}