Changed: DB Params
0
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# Learnlytics - in GoLang
|
||||
|
||||
## Links
|
||||
|
||||
- [Postgres as Auth](https://medium.com/@ValentinMouret/simple-authentication-with-only-postgresql-ff38f5bf8b0d)
|
||||
- [templ](https://templ.guide/)
|
||||
- [htmx](https://htmx.org/docs/#inheritance)
|
9
assets/css/colors.css
Normal file
@ -0,0 +1,9 @@
|
||||
:root {
|
||||
--text-color: #E0E1DD;
|
||||
--text-color-inverted: #1f1e22;
|
||||
--background-color: #0d1b2a;
|
||||
--focused: #f4a260;
|
||||
--unfocused: #2ec4b6;
|
||||
--menu-bg: #1b3857;
|
||||
--menu-border: #668580;
|
||||
}
|
36
assets/css/grid_layout.css
Normal file
@ -0,0 +1,36 @@
|
||||
.two-split {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 4fr;
|
||||
grid-auto-rows: 75px;
|
||||
}
|
||||
|
||||
.three-split {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 4fr 1fr;
|
||||
grid-auto-rows: 75px;
|
||||
}
|
||||
|
||||
.grid-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 10%;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.grid-item-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.one-row {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.two-row {
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
|
||||
.three-row {
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
}
|
319
assets/css/style.css
Normal file
@ -0,0 +1,319 @@
|
||||
@font-face {
|
||||
font-family: "Lato";
|
||||
src:
|
||||
url("/assets/fonts/lato/Lato-BlackItalic.ttf") format("truetype"),
|
||||
url("/assets/fonts/lato/Lato-Black.ttf") format("truetype"),
|
||||
url("/assets/fonts/lato/Lato-BoldItalic.ttf") format("truetype"),
|
||||
url("/assets/fonts/lato/Lato-Bold.ttf") format("truetype"),
|
||||
url("/assets/fonts/lato/Lato-Italic.ttf") format("truetype"),
|
||||
url("/assets/fonts/lato/Lato-LightItalic.ttf") format("truetype"),
|
||||
url("/assets/fonts/lato/Lato-Regular.ttf") format("truetype"),
|
||||
url("/assets/fonts/lato/Lato-ThinItalic.ttf") format("truetype"),
|
||||
url("/assets/fonts/lato/Lato-Thin.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.lato-thin {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.lato-light {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.lato-regular {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.lato-bold {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.lato-black {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-weight: 900;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.lato-thin-italic {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-weight: 100;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.lato-light-italic {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.lato-regular-italic {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.lato-bold-italic {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.lato-black-italic {
|
||||
font-family: "Lato", sans-serif;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
* {
|
||||
color: var(--text-color);
|
||||
font-family: lato-regular, sans-serif;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
main {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0%);
|
||||
min-height: 110vh;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--menu-bg);
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--unfocused);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--focused);
|
||||
}
|
||||
|
||||
button {
|
||||
color: var(--text-color-inverted);
|
||||
background-color: var(--unfocused);
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
color: var(--text-color);
|
||||
background-color: var(--focused);
|
||||
}
|
||||
|
||||
.content_container {
|
||||
}
|
||||
|
||||
|
||||
.login {
|
||||
zoom: 150%;
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.login h1 {
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
margin-top: -20px;
|
||||
margin-bottom: 1%;
|
||||
}
|
||||
|
||||
.login img {
|
||||
position: relative;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.login form {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login input {
|
||||
text-align: left;
|
||||
font-size: 15px;
|
||||
background-color: var(--background-color);
|
||||
border: none;
|
||||
border-bottom: 2px solid var(--unfocused);
|
||||
transition: border-bottom 0.2s ease-out;
|
||||
}
|
||||
|
||||
.login input:focus {
|
||||
outline: none;
|
||||
border-bottom: 2px solid var(--focused);
|
||||
}
|
||||
|
||||
.login input:required {
|
||||
border-bottom: 2px solid var(--focused);
|
||||
}
|
||||
|
||||
.login input[required]:invalid {
|
||||
border-bottom: 2px solid var(--unfocused);
|
||||
}
|
||||
|
||||
.login input[type=text] {
|
||||
background-image: url("/assets/img/id-card-negated.png");
|
||||
background-position: 5% center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 15px 15px;
|
||||
text-indent: 15%;
|
||||
}
|
||||
|
||||
.login input[type=password] {
|
||||
background-image: url("/assets/img/key-negated.png");
|
||||
background-position: 5% center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 15px 15px;
|
||||
text-indent: 15%;
|
||||
}
|
||||
|
||||
.login input[type=submit] {
|
||||
text-align: center;
|
||||
width: 30%;
|
||||
background-color: var(--background-color);
|
||||
transition: border-bottom 0.2s ease-out;
|
||||
}
|
||||
|
||||
.login input[type=submit]:hover {
|
||||
border-bottom: 2px solid var(--focused);
|
||||
}
|
||||
|
||||
.side_by_side {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error h1 {
|
||||
font-size: 300%;
|
||||
}
|
||||
|
||||
.error h2 {
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
.error p {
|
||||
font-size: 150%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.navbar ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: var(--menu-bg);
|
||||
}
|
||||
|
||||
.navbar li {
|
||||
float: left;
|
||||
border-right: 1px solid var(--menu-border);
|
||||
}
|
||||
|
||||
.navbar li:first-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.navbar li:last-child {
|
||||
float: right;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.navbar img {
|
||||
object-fit: contain;
|
||||
width: 80px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.navbar a {
|
||||
display: block;
|
||||
padding: 8px;
|
||||
font-size: 130%;
|
||||
text-align: center;
|
||||
color: var(--text-color-inverted);
|
||||
background-color: var(--unfocused);
|
||||
}
|
||||
|
||||
.navbar a:hover {
|
||||
color: var(--text-color);
|
||||
background-color: var(--focused);
|
||||
}
|
||||
|
||||
.usercard {
|
||||
border-radius: 10px;
|
||||
border: 3px solid var(--unfocused);
|
||||
}
|
||||
|
||||
.usercard img {
|
||||
display: block;
|
||||
margin-top: 2%;
|
||||
margin-bottom: 2%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-radius: 50%;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.usercard h1 {
|
||||
margin-top: 0%;
|
||||
text-align: center;
|
||||
font-size: 150%;
|
||||
background-color: var(--unfocused);
|
||||
color: var(--text-color-inverted);
|
||||
}
|
||||
|
||||
.usercard p {
|
||||
font-size: 90%;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.button_row {
|
||||
text-align: left;
|
||||
padding-top: 1%;
|
||||
padding-bottom: 1%;
|
||||
}
|
||||
|
||||
.button_row button {
|
||||
font-size: 100%;
|
||||
margin-left: 5%;
|
||||
width: 15%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
0
assets/fonts/.gitignore
vendored
Normal file
BIN
assets/fonts/lato/Lato-Black.ttf
Normal file
BIN
assets/fonts/lato/Lato-BlackItalic.ttf
Normal file
BIN
assets/fonts/lato/Lato-Bold.ttf
Normal file
BIN
assets/fonts/lato/Lato-BoldItalic.ttf
Normal file
BIN
assets/fonts/lato/Lato-Italic.ttf
Normal file
BIN
assets/fonts/lato/Lato-Light.ttf
Normal file
BIN
assets/fonts/lato/Lato-LightItalic.ttf
Normal file
BIN
assets/fonts/lato/Lato-Regular.ttf
Normal file
BIN
assets/fonts/lato/Lato-Thin.ttf
Normal file
BIN
assets/fonts/lato/Lato-ThinItalic.ttf
Normal file
BIN
assets/fonts/lato/MPLUSRounded1c-Black.ttf
Normal file
BIN
assets/fonts/lato/MPLUSRounded1c-Bold.ttf
Normal file
BIN
assets/fonts/lato/MPLUSRounded1c-ExtraBold.ttf
Normal file
BIN
assets/fonts/lato/MPLUSRounded1c-Light.ttf
Normal file
BIN
assets/fonts/lato/MPLUSRounded1c-Medium.ttf
Normal file
BIN
assets/fonts/lato/MPLUSRounded1c-Regular.ttf
Normal file
BIN
assets/fonts/lato/MPLUSRounded1c-Thin.ttf
Normal file
18
assets/img/failed-exam.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg fill="#E0E1DD" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="128px" height="128px" viewBox="0 0 893.4 893.4" xml:space="preserve"
|
||||
>
|
||||
<g>
|
||||
<path d="M747.3,0H146.101c-13.8,0-25,11.2-25,25v700.5H234c30.3,0,55,24.7,55,55v112.9H747.3c13.801,0,25-11.2,25-25V25
|
||||
C772.3,11.2,761.101,0,747.3,0z M636.901,655.6H256.5c-13.8,0-25-11.199-25-25c0-13.8,11.2-25,25-25h380.401
|
||||
c13.799,0,25,11.2,25,25C661.901,644.4,650.7,655.6,636.901,655.6z M636.901,543.5H256.5c-13.8,0-25-11.2-25-25s11.2-25,25-25
|
||||
h380.401c13.799,0,25,11.2,25,25S650.7,543.5,636.901,543.5z M661.901,406.4c0,13.8-11.201,25-25,25H256.5c-13.8,0-25-11.2-25-25
|
||||
l0,0c0-13.801,11.2-25,25-25h380.401C650.7,381.4,661.901,392.6,661.901,406.4L661.901,406.4z M661.901,98.3
|
||||
c0,12.5-10.102,22.6-22.602,22.6l-97.6,1v55.7l81.201-1c12.5,0,22.6,10.1,22.6,22.6l0,0c0,12.5-10.1,22.6-22.6,22.6l-81.201,1v82
|
||||
c0,12.5-10.1,22.6-22.6,22.6h-8.1c-12.5,0-22.6-10.1-22.6-22.6V98.3c0-12.5,10.1-22.6,22.6-22.6H639.3
|
||||
C651.8,75.7,661.901,85.8,661.901,98.3L661.901,98.3z"/>
|
||||
<path d="M234,755.5h-110.9h-2L259,893.4v-0.7V780.5C259,766.7,247.801,755.5,234,755.5z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/img/icon/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
assets/img/icon/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
assets/img/icon/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
assets/img/icon/favicon-16x16.png
Normal file
After Width: | Height: | Size: 354 B |
BIN
assets/img/icon/favicon-32x32.png
Normal file
After Width: | Height: | Size: 676 B |
BIN
assets/img/icon/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
1
assets/img/icon/site.webmanifest
Normal file
@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
BIN
assets/img/id-card-negated.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
assets/img/id-card.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
assets/img/key-negated.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
assets/img/key.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
assets/img/learnlytics.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
28
assets/img/learnlytics.svg
Normal file
@ -0,0 +1,28 @@
|
||||
<svg
|
||||
width="300" height="150" viewBox="0 0 300 150"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" stroke-linecap="round" stroke-linejoin="round"
|
||||
>
|
||||
<!-- Centering Group -->
|
||||
<g transform="translate(25, 0)">
|
||||
<!-- Bar Chart -->
|
||||
<rect x="0" y="90" width="30" height="30" fill="#2EC4B6" />
|
||||
<rect x="45" y="60" width="30" height="60" fill="#2EC4B6" />
|
||||
<rect x="90" y="80" width="30" height="40" fill="#2EC4B6" />
|
||||
<rect x="135" y="40" width="30" height="80" fill="#2EC4B6" />
|
||||
<rect x="180" y="70" width="30" height="50" fill="#2EC4B6" />
|
||||
<rect x="225" y="30" width="30" height="90" fill="#2EC4B6" />
|
||||
|
||||
<!-- Analytics Chart - Dynamic Graph Lines -->
|
||||
<polyline points="15,90 60,60 105,80 150,40 195,70 240,30" stroke="#F4A261" stroke-width="3" stroke-dasharray="8 4" />
|
||||
<circle cx="15" cy="90" r="5" fill="#F4A261" />
|
||||
<circle cx="60" cy="60" r="5" fill="#F4A261" />
|
||||
<circle cx="105" cy="80" r="5" fill="#F4A261" />
|
||||
<circle cx="150" cy="40" r="5" fill="#F4A261" />
|
||||
<circle cx="195" cy="70" r="5" fill="#F4A261" />
|
||||
<circle cx="240" cy="30" r="5" fill="#F4A261" />
|
||||
|
||||
<!-- Centered Text -->
|
||||
<!-- text x="75" y="140" fill="#E0E1DD" font="lato-regular" font-size="20" font-weight="bold">Learnlytics</text-->
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
4
assets/img/smiley-x.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#E0E1DD" width="128px" height="128px" viewBox="0 0 256 256" id="Flat" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M128,28A100,100,0,1,0,228,128,100.11332,100.11332,0,0,0,128,28Zm0,192a92,92,0,1,1,92-92A92.10416,92.10416,0,0,1,128,220ZM186.82812,98.82812,173.65674,112l13.17138,13.17188a3.99957,3.99957,0,1,1-5.65624,5.65624L168,117.65674l-13.17188,13.17138a3.99957,3.99957,0,0,1-5.65624-5.65624L162.34326,112,149.17188,98.82812a3.99957,3.99957,0,0,1,5.65624-5.65624L168,106.34326l13.17188-13.17138a3.99957,3.99957,0,1,1,5.65624,5.65624Zm-80,0L93.65674,112l13.17138,13.17188a3.99957,3.99957,0,1,1-5.65624,5.65624L88,117.65674,74.82812,130.82812a3.99957,3.99957,0,0,1-5.65624-5.65624L82.34326,112,69.17188,98.82812a3.99957,3.99957,0,0,1,5.65624-5.65624L88,106.34326l13.17188-13.17138a3.99957,3.99957,0,0,1,5.65624,5.65624ZM136,180a8,8,0,1,1-8-8A8.00917,8.00917,0,0,1,136,180Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1017 B |
BIN
assets/img/user.jpeg
Normal file
After Width: | Height: | Size: 352 KiB |
20
assets/js/chart.js
Normal file
293
assets/js/chartUtils.js
Normal file
@ -0,0 +1,293 @@
|
||||
function barChart(id, data, labels, tooltip, title, scale_label_x, scale_label_y) {
|
||||
const canvas = document.getElementById('bar_chart'+id)
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
data = data.map((item) => DecimalPrecision.round(item, 2));
|
||||
|
||||
// Gradient
|
||||
var gradient = ctx.createLinearGradient(0, 0, 0, 500);
|
||||
gradient.addColorStop(0, '#f4a260');
|
||||
gradient.addColorStop(1, '#2ec4b6');
|
||||
|
||||
// Data
|
||||
var displayData = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: tooltip,
|
||||
data: data,
|
||||
backgroundColor: gradient,
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Options
|
||||
var options = {
|
||||
responsive: true,
|
||||
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
text: scale_label_x,
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
text: scale_label_y,
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: title,
|
||||
font: {
|
||||
size: 20,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
var config = {
|
||||
type: 'bar',
|
||||
data: displayData,
|
||||
options: options
|
||||
};
|
||||
|
||||
new Chart(ctx, config);
|
||||
}
|
||||
|
||||
function barLineChart(id, data, labels, tooltip, title, scale_label_x, scale_label_y) {
|
||||
const canvas = document.getElementById('bar_line_chart'+id)
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, 500);
|
||||
gradient.addColorStop(0, '#f4a260');
|
||||
gradient.addColorStop(1, '#2ec4b6');
|
||||
|
||||
const sum = data.reduce((partialSum, a) => partialSum + a, 0);
|
||||
const percentage = data.map((item) => DecimalPrecision.round(item/sum*100, 2));
|
||||
var percentageTick = 100;
|
||||
var percentageTickSize = 20;
|
||||
|
||||
// Data
|
||||
const displayData = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Percentage",
|
||||
data: percentage,
|
||||
borderColor: '#E0E1DD',
|
||||
backgroundColor: '#2ec4b6',
|
||||
yAxisID: 'y1',
|
||||
type: 'line',
|
||||
},
|
||||
{
|
||||
label: tooltip,
|
||||
data: data,
|
||||
backgroundColor: gradient,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
|
||||
]
|
||||
};
|
||||
|
||||
// Options
|
||||
const options = {
|
||||
responsive: true,
|
||||
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
text: scale_label_x,
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
text: scale_label_y,
|
||||
beginAtZero: true,
|
||||
max: Math.max.apply(null, data) + 1,
|
||||
},
|
||||
|
||||
y1: {
|
||||
display: true,
|
||||
position: 'right',
|
||||
beginAtZero: true,
|
||||
max: percentageTick,
|
||||
ticks: {
|
||||
stepSize: percentageTickSize,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: title,
|
||||
font: {
|
||||
size: 20,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
const config = {
|
||||
type: 'bar',
|
||||
data: displayData,
|
||||
options: options
|
||||
};
|
||||
|
||||
let barlinechart = new Chart(ctx, config);
|
||||
|
||||
// Actions
|
||||
const actions = [
|
||||
{
|
||||
name: "Toggle Tick",
|
||||
handler(chart) {
|
||||
if (percentageTick == 100 ) {
|
||||
percentageTick = Math.trunc(Math.max.apply(null, percentage) + 1);
|
||||
percentageTickSize = Math.trunc(percentageTick / 5);
|
||||
}
|
||||
else {
|
||||
percentageTick = 100;
|
||||
percentageTickSize = 20;
|
||||
}
|
||||
barlinechart.options.scales.y1.max = percentageTick;
|
||||
barlinechart.options.scales.y1.ticks.stepSize = percentageTickSize;
|
||||
chart.update();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
actions.forEach((a, i) => {
|
||||
let button = document.createElement("button");
|
||||
button.id = "button"+i;
|
||||
button.innerText = a.name;
|
||||
button.onclick = () => a.handler(barlinechart);
|
||||
document.querySelector(".button_row").appendChild(button);
|
||||
});
|
||||
}
|
||||
|
||||
function pieChart(id, data, labels, tooltip, title, scale_label_x, scale_label_y) {
|
||||
const canvas = document.getElementById('pie_chart'+id)
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Data
|
||||
var displayData = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: tooltip,
|
||||
data: data,
|
||||
backgroundColor: [
|
||||
'#f4a260',
|
||||
'#e77f7a',
|
||||
'#be6d8e',
|
||||
'#856490',
|
||||
'#4f597b',
|
||||
'#2f4858',
|
||||
],
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Options
|
||||
var options = {
|
||||
responsive: true,
|
||||
};
|
||||
|
||||
var config = {
|
||||
type: 'pie',
|
||||
data: displayData,
|
||||
options: options
|
||||
};
|
||||
|
||||
new Chart(ctx, config);
|
||||
}
|
||||
|
||||
function doughnutChart(id, data, labels, tooltip, title, scale_label_x, scale_label_y) {
|
||||
const canvas = document.getElementById('doughnut_chart'+id)
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Data
|
||||
var displayData = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: tooltip,
|
||||
data: data,
|
||||
backgroundColor: [
|
||||
'#f4a260',
|
||||
'#e77f7a',
|
||||
'#be6d8e',
|
||||
'#856490',
|
||||
'#4f597b',
|
||||
'#2f4858',
|
||||
],
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Options
|
||||
var options = {
|
||||
responsive: true,
|
||||
};
|
||||
|
||||
var config = {
|
||||
type: 'doughnut',
|
||||
data: displayData,
|
||||
options: options
|
||||
};
|
||||
|
||||
new Chart(ctx, config);
|
||||
}
|
||||
|
||||
function polarChart(id, data, labels, tooltip, title, scale_label_x, scale_label_y) {
|
||||
const canvas = document.getElementById('polar_chart'+id)
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Data
|
||||
var displayData = {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: tooltip,
|
||||
data: data,
|
||||
backgroundColor: [
|
||||
'#f4a260',
|
||||
'#e77f7a',
|
||||
'#be6d8e',
|
||||
'#856490',
|
||||
'#4f597b',
|
||||
'#2f4858',
|
||||
],
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Options
|
||||
var options = {
|
||||
responsive: true,
|
||||
|
||||
scales: {
|
||||
x: {
|
||||
border: { display: true },
|
||||
grid: {
|
||||
display: false,
|
||||
drawOnChartArea: true,
|
||||
drawTicks: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var config = {
|
||||
type: 'polarArea',
|
||||
data: displayData,
|
||||
options: config,
|
||||
};
|
||||
|
||||
new Chart(ctx, config);
|
||||
}
|
1
assets/js/htmx.min.js
vendored
Normal file
39
assets/js/utils.js
Normal file
@ -0,0 +1,39 @@
|
||||
var DecimalPrecision = (function() {
|
||||
if (Math.trunc === undefined) {
|
||||
Math.trunc = function(v) {
|
||||
return v < 0 ? Math.ceil(v) : Math.floor(v);
|
||||
};
|
||||
}
|
||||
var decimalAdjust = function myself(type, num, decimalPlaces) {
|
||||
if (type === 'round' && num < 0)
|
||||
return -myself(type, -num, decimalPlaces);
|
||||
var shift = function(value, exponent) {
|
||||
value = (value + 'e').split('e');
|
||||
return +(value[0] + 'e' + (+value[1] + (exponent || 0)));
|
||||
};
|
||||
var n = shift(num, +decimalPlaces);
|
||||
return shift(Math[type](n), -decimalPlaces);
|
||||
};
|
||||
return {
|
||||
// Decimal round (half away from zero)
|
||||
round: function(num, decimalPlaces) {
|
||||
return decimalAdjust('round', num, decimalPlaces);
|
||||
},
|
||||
// Decimal ceil
|
||||
ceil: function(num, decimalPlaces) {
|
||||
return decimalAdjust('ceil', num, decimalPlaces);
|
||||
},
|
||||
// Decimal floor
|
||||
floor: function(num, decimalPlaces) {
|
||||
return decimalAdjust('floor', num, decimalPlaces);
|
||||
},
|
||||
// Decimal trunc
|
||||
trunc: function(num, decimalPlaces) {
|
||||
return decimalAdjust('trunc', num, decimalPlaces);
|
||||
},
|
||||
// Format using fixed-point notation
|
||||
toFixed: function(num, decimalPlaces) {
|
||||
return decimalAdjust('round', num, decimalPlaces).toFixed(decimalPlaces);
|
||||
}
|
||||
};
|
||||
})();
|
BIN
assets/learnlytics.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
82
components/charts.templ
Normal file
@ -0,0 +1,82 @@
|
||||
package components
|
||||
|
||||
templ barChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) {
|
||||
<div class="chart">
|
||||
<canvas id={ "bar_chart" + id }></canvas>
|
||||
@templ.JSFuncCall(
|
||||
"barChart",
|
||||
id,
|
||||
data,
|
||||
labels,
|
||||
tooltip,
|
||||
title,
|
||||
scaleLabelX,
|
||||
scaleLabelY,
|
||||
)
|
||||
</div>
|
||||
}
|
||||
|
||||
templ barLineChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) {
|
||||
<div class="chart">
|
||||
<canvas id={ "bar_line_chart" + id }></canvas>
|
||||
<section class="button_row"></section>
|
||||
@templ.JSFuncCall(
|
||||
"barLineChart",
|
||||
id,
|
||||
data,
|
||||
labels,
|
||||
tooltip,
|
||||
title,
|
||||
scaleLabelX,
|
||||
scaleLabelY,
|
||||
)
|
||||
</div>
|
||||
}
|
||||
|
||||
templ pieChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) {
|
||||
<div class="chart">
|
||||
<canvas id={ "pie_chart" + id }></canvas>
|
||||
@templ.JSFuncCall(
|
||||
"pieChart",
|
||||
id,
|
||||
data,
|
||||
labels,
|
||||
tooltip,
|
||||
title,
|
||||
scaleLabelX,
|
||||
scaleLabelY,
|
||||
)
|
||||
</div>
|
||||
}
|
||||
|
||||
templ doughnutChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) {
|
||||
<div class="chart">
|
||||
<canvas id={ "doughnut_chart" + id }></canvas>
|
||||
@templ.JSFuncCall(
|
||||
"doughnutChart",
|
||||
id,
|
||||
data,
|
||||
labels,
|
||||
tooltip,
|
||||
title,
|
||||
scaleLabelX,
|
||||
scaleLabelY,
|
||||
)
|
||||
</div>
|
||||
}
|
||||
|
||||
templ polarChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) {
|
||||
<div class="chart">
|
||||
<canvas id={ "polar_chart" + id }></canvas>
|
||||
@templ.JSFuncCall(
|
||||
"polarChart",
|
||||
id,
|
||||
data,
|
||||
labels,
|
||||
tooltip,
|
||||
title,
|
||||
scaleLabelX,
|
||||
scaleLabelY,
|
||||
)
|
||||
</div>
|
||||
}
|
306
components/charts_templ.go
Normal file
@ -0,0 +1,306 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func barChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"chart\"><canvas id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs("bar_chart" + id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/charts.templ`, Line: 5, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"></canvas>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.JSFuncCall(
|
||||
"barChart",
|
||||
id,
|
||||
data,
|
||||
labels,
|
||||
tooltip,
|
||||
title,
|
||||
scaleLabelX,
|
||||
scaleLabelY,
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func barLineChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var3 == nil {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"chart\"><canvas id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("bar_line_chart" + id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/charts.templ`, Line: 21, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"></canvas><section class=\"button_row\"></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.JSFuncCall(
|
||||
"barLineChart",
|
||||
id,
|
||||
data,
|
||||
labels,
|
||||
tooltip,
|
||||
title,
|
||||
scaleLabelX,
|
||||
scaleLabelY,
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func pieChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var5 == nil {
|
||||
templ_7745c5c3_Var5 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"chart\"><canvas id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("pie_chart" + id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/charts.templ`, Line: 38, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"></canvas>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.JSFuncCall(
|
||||
"pieChart",
|
||||
id,
|
||||
data,
|
||||
labels,
|
||||
tooltip,
|
||||
title,
|
||||
scaleLabelX,
|
||||
scaleLabelY,
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func doughnutChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var7 == nil {
|
||||
templ_7745c5c3_Var7 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"chart\"><canvas id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs("doughnut_chart" + id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/charts.templ`, Line: 54, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"></canvas>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.JSFuncCall(
|
||||
"doughnutChart",
|
||||
id,
|
||||
data,
|
||||
labels,
|
||||
tooltip,
|
||||
title,
|
||||
scaleLabelX,
|
||||
scaleLabelY,
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func polarChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var9 == nil {
|
||||
templ_7745c5c3_Var9 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"chart\"><canvas id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs("polar_chart" + id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/charts.templ`, Line: 70, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"></canvas>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.JSFuncCall(
|
||||
"polarChart",
|
||||
id,
|
||||
data,
|
||||
labels,
|
||||
tooltip,
|
||||
title,
|
||||
scaleLabelX,
|
||||
scaleLabelY,
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
240
components/components.templ
Normal file
@ -0,0 +1,240 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"time"
|
||||
"strconv"
|
||||
"math/rand/v2"
|
||||
)
|
||||
|
||||
func getCurrentTime() string {
|
||||
loc, err := time.LoadLocation("Europe/Berlin")
|
||||
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
|
||||
t := time.Now().In(loc)
|
||||
layout := "02.01.2006 15:04"
|
||||
return t.Format(layout)
|
||||
}
|
||||
|
||||
func genRandomData(count int) []float64 {
|
||||
data := make([]float64, count)
|
||||
for i := 0; i < count; i++ {
|
||||
data[i] = rand.NormFloat64() * 30 + 50
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
templ base(title string) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Learnlytics: WebApp to manage classrooms"
|
||||
/>
|
||||
<meta name="google" content="notranslate"/>
|
||||
|
||||
<link rel="icon" href="assets/img/icon/favicon.ico" type="image/x-icon"/>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/img/icon/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/img/icon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="assets/img/icon/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
|
||||
<link rel="stylesheet" href="assets/css/colors.css" />
|
||||
<link rel="stylesheet" href="assets/css/style.css" />
|
||||
<link rel="stylesheet" href="assets/css/grid_layout.css" />
|
||||
|
||||
<title>Learnlytics - { title }</title>
|
||||
|
||||
<script src="assets/js/utils.js"></script>
|
||||
<script src="assets/js/chartUtils.js"></script>
|
||||
<script src="assets/js/htmx.min.js"></script>
|
||||
<script src="assets/js/chart.js"></script>
|
||||
<script>
|
||||
Chart.defaults.color = "#E0E1DD";
|
||||
Chart.defaults.backgroundColor = "#2ec4b6";
|
||||
Chart.defaults.borderColor = "#00000000";
|
||||
Chart.defaults.font.family = "'lato-regular', sans-serif";
|
||||
|
||||
Chart.defaults.plugins.align = 'center';
|
||||
Chart.defaults.plugins.display = true;
|
||||
Chart.defaults.plugins.padding = 10;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@navbar()
|
||||
<div class="content_container">
|
||||
{ children... }
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ footer() {
|
||||
<footer>
|
||||
<p>Author: @DerGrumpf</p>
|
||||
</footer>
|
||||
}
|
||||
|
||||
templ navbar() {
|
||||
<div class="navbar">
|
||||
<ul>
|
||||
<li><img src="assets/img/learnlytics.svg" alt="Learnlytics Logo"></li>
|
||||
<li><a href="/">Dashboard</a></li>
|
||||
<li><a href="/">Dashboard</a></li>
|
||||
<li><a href="/">Dashboard</a></li>
|
||||
<li><a href="/">Dashboard</a></li>
|
||||
<li><a href="/">About</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ selectList(labels []string) {
|
||||
<div class="select_list">
|
||||
<form>
|
||||
<fieldset>
|
||||
<legend>Students:</legend>
|
||||
for index, label := range labels {
|
||||
<label for={ strconv.Itoa(index) }>{ label }</label>
|
||||
<input type="radio" id={ strconv.Itoa(index) }><br><br>
|
||||
}
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ usercard(username string) {
|
||||
<div class="usercard">
|
||||
<img src="assets/img/user.jpeg" alt="Avatar">
|
||||
<h1>{ username }</h1>
|
||||
<div class="two-split">
|
||||
<div class="grid-item-left">
|
||||
<p>Insitution:</p>
|
||||
</div>
|
||||
<div class="grid-item-left">
|
||||
<p>IFN @ TU BS</p>
|
||||
</div>
|
||||
|
||||
<div class="grid-item-left">
|
||||
<p>Mail:</p>
|
||||
</div>
|
||||
<div class="grid-item-left">
|
||||
<p>p.keier@beyerstedt-it.de</p>
|
||||
</div>
|
||||
|
||||
<div class="grid-item-left">
|
||||
<p>Created:</p>
|
||||
</div>
|
||||
<div class="grid-item-left">
|
||||
<p>{ getCurrentTime() }</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ NotFound() {
|
||||
@base("Error") {
|
||||
<div class="error">
|
||||
<h1>404 - Not Found</h1>
|
||||
<div class="side_by_side">
|
||||
<img src="assets/img/smiley-x.svg" alt="Dead Smiley">
|
||||
<img src="assets/img/failed-exam.svg" alt="Failed Exam">
|
||||
</div>
|
||||
<h2>This Page Didn't Pass the Exam</h2>
|
||||
<p>It tried, but it didn’t make the cut.</p>
|
||||
<p>Better check the <a href="/">Dashboard</a> instead!</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ Test() {
|
||||
@base("Test") {
|
||||
<div class="two-split two-row">
|
||||
<div class="grid-item-center">
|
||||
Test
|
||||
</div>
|
||||
<div class="grid-item-center">
|
||||
@polarChart(
|
||||
"1",
|
||||
genRandomData(6),
|
||||
[]string{"Klasse 8a", "Klasse 5b", "Klasse 6c", "Klasse 10d", "Englisch LK 12", "Geschickte GK 11"},
|
||||
"Points scored",
|
||||
"Classes",
|
||||
"Classes",
|
||||
"",
|
||||
)
|
||||
</div>
|
||||
<div class="grid-item-center">
|
||||
Test
|
||||
</div>
|
||||
<div class="grid-item-center">
|
||||
@doughnutChart(
|
||||
"1",
|
||||
genRandomData(6),
|
||||
[]string{"Klasse 8a", "Klasse 5b", "Klasse 6c", "Klasse 10d", "Englisch LK 12", "Geschickte GK 11"},
|
||||
"Points scored",
|
||||
"Classes",
|
||||
"Classes",
|
||||
"",
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ Dashboard(username string) {
|
||||
@base("Dashboard") {
|
||||
<div class="two-split three-row">
|
||||
<div class="grid-item-center">
|
||||
@usercard(username)
|
||||
</div>
|
||||
<div class="grid-item-center">
|
||||
@barChart(
|
||||
"2",
|
||||
genRandomData(6),
|
||||
[]string{"Klasse 8a", "Klasse 5b", "Klasse 6c", "Klasse 10d", "Englisch LK 12", "Geschickte GK 11"},
|
||||
"Points scored",
|
||||
"Classes",
|
||||
"Classes",
|
||||
"",
|
||||
)
|
||||
</div>
|
||||
|
||||
<div class="grid-item-center">
|
||||
@selectList([]string{"Phil Keier", "Calvin Brandt", "Nova Eib"})
|
||||
</div>
|
||||
|
||||
<div class="grid-item-center">
|
||||
@barLineChart(
|
||||
"1",
|
||||
[]float64{31, 15, 18, 35, 20, 20, 22, 27, 24, 30},
|
||||
[]string{"Tutorial 1", "Tutorial 2", "Extended Applications", "Numpy & MatPlotLib", "SciPy", "MonteCarlo", "Pandas & Seaborn", "Folium", "Statistical Test Methods", "Data Analysis"},
|
||||
"Points scored",
|
||||
"Lectures",
|
||||
"Lectures",
|
||||
"Points",
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ Login() {
|
||||
@base("Login") {
|
||||
<div class="login">
|
||||
<img src="assets/img/learnlytics.svg" alt="Learnlytics Logo">
|
||||
<h1>Learnlytics</h1>
|
||||
<form action="/" method="POST">
|
||||
<input type="text" id="username" name="username" placeholder="Username" required><br><br>
|
||||
|
||||
<input type="password" id="password" name="password" placeholder="Password" required><br><br>
|
||||
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
}
|
554
components/components_templ.go
Normal file
@ -0,0 +1,554 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package components
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getCurrentTime() string {
|
||||
loc, err := time.LoadLocation("Europe/Berlin")
|
||||
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
|
||||
t := time.Now().In(loc)
|
||||
layout := "02.01.2006 15:04"
|
||||
return t.Format(layout)
|
||||
}
|
||||
|
||||
func genRandomData(count int) []float64 {
|
||||
data := make([]float64, count)
|
||||
for i := 0; i < count; i++ {
|
||||
data[i] = rand.NormFloat64()*30 + 50
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func base(title string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" data-theme=\"dark\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"description\" content=\"Learnlytics: WebApp to manage classrooms\"><meta name=\"google\" content=\"notranslate\"><link rel=\"icon\" href=\"assets/img/icon/favicon.ico\" type=\"image/x-icon\"><link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"assets/img/icon/apple-touch-icon.png\"><link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"assets/img/icon/favicon-32x32.png\"><link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"assets/img/icon/favicon-16x16.png\"><link rel=\"manifest\" href=\"/site.webmanifest\"><link rel=\"stylesheet\" href=\"assets/css/colors.css\"><link rel=\"stylesheet\" href=\"assets/css/style.css\"><link rel=\"stylesheet\" href=\"assets/css/grid_layout.css\"><title>Learnlytics - ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/components.templ`, Line: 51, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><script src=\"assets/js/utils.js\"></script><script src=\"assets/js/chartUtils.js\"></script><script src=\"assets/js/htmx.min.js\"></script><script src=\"assets/js/chart.js\"></script><script>\n Chart.defaults.color = \"#E0E1DD\";\n Chart.defaults.backgroundColor = \"#2ec4b6\";\n Chart.defaults.borderColor = \"#00000000\";\n Chart.defaults.font.family = \"'lato-regular', sans-serif\";\n\n Chart.defaults.plugins.align = 'center';\n Chart.defaults.plugins.display = true;\n Chart.defaults.plugins.padding = 10;\n </script></head><body>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = navbar().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"content_container\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func footer() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var3 == nil {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<footer><p>Author: @DerGrumpf</p></footer>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func navbar() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"navbar\"><ul><li><img src=\"assets/img/learnlytics.svg\" alt=\"Learnlytics Logo\"></li><li><a href=\"/\">Dashboard</a></li><li><a href=\"/\">Dashboard</a></li><li><a href=\"/\">Dashboard</a></li><li><a href=\"/\">Dashboard</a></li><li><a href=\"/\">About</a></li></ul></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func selectList(labels []string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var5 == nil {
|
||||
templ_7745c5c3_Var5 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"select_list\"><form><fieldset><legend>Students:</legend> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for index, label := range labels {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<label for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(index))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/components.templ`, Line: 102, Col: 52}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/components.templ`, Line: 102, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</label> <input type=\"radio\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(index))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/components.templ`, Line: 103, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"><br><br>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</fieldset></form></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func usercard(username string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var9 == nil {
|
||||
templ_7745c5c3_Var9 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"usercard\"><img src=\"assets/img/user.jpeg\" alt=\"Avatar\"><h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/components.templ`, Line: 113, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</h1><div class=\"two-split\"><div class=\"grid-item-left\"><p>Insitution:</p></div><div class=\"grid-item-left\"><p>IFN @ TU BS</p></div><div class=\"grid-item-left\"><p>Mail:</p></div><div class=\"grid-item-left\"><p>p.keier@beyerstedt-it.de</p></div><div class=\"grid-item-left\"><p>Created:</p></div><div class=\"grid-item-left\"><p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(getCurrentTime())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/components.templ`, Line: 133, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</p></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func NotFound() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var12 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var12 == nil {
|
||||
templ_7745c5c3_Var12 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var13 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"error\"><h1>404 - Not Found</h1><div class=\"side_by_side\"><img src=\"assets/img/smiley-x.svg\" alt=\"Dead Smiley\"> <img src=\"assets/img/failed-exam.svg\" alt=\"Failed Exam\"></div><h2>This Page Didn't Pass the Exam</h2><p>It tried, but it didn’t make the cut.</p><p>Better check the <a href=\"/\">Dashboard</a> instead!</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = base("Error").Render(templ.WithChildren(ctx, templ_7745c5c3_Var13), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func Test() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var14 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var14 == nil {
|
||||
templ_7745c5c3_Var14 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var15 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"two-split two-row\"><div class=\"grid-item-center\">Test</div><div class=\"grid-item-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = polarChart(
|
||||
"1",
|
||||
genRandomData(6),
|
||||
[]string{"Klasse 8a", "Klasse 5b", "Klasse 6c", "Klasse 10d", "Englisch LK 12", "Geschickte GK 11"},
|
||||
"Points scored",
|
||||
"Classes",
|
||||
"Classes",
|
||||
"",
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div><div class=\"grid-item-center\">Test</div><div class=\"grid-item-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = doughnutChart(
|
||||
"1",
|
||||
genRandomData(6),
|
||||
[]string{"Klasse 8a", "Klasse 5b", "Klasse 6c", "Klasse 10d", "Englisch LK 12", "Geschickte GK 11"},
|
||||
"Points scored",
|
||||
"Classes",
|
||||
"Classes",
|
||||
"",
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = base("Test").Render(templ.WithChildren(ctx, templ_7745c5c3_Var15), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func Dashboard(username string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var16 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var16 == nil {
|
||||
templ_7745c5c3_Var16 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var17 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"two-split three-row\"><div class=\"grid-item-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = usercard(username).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div><div class=\"grid-item-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = barChart(
|
||||
"2",
|
||||
genRandomData(6),
|
||||
[]string{"Klasse 8a", "Klasse 5b", "Klasse 6c", "Klasse 10d", "Englisch LK 12", "Geschickte GK 11"},
|
||||
"Points scored",
|
||||
"Classes",
|
||||
"Classes",
|
||||
"",
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div><div class=\"grid-item-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = selectList([]string{"Phil Keier", "Calvin Brandt", "Nova Eib"}).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div><div class=\"grid-item-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = barLineChart(
|
||||
"1",
|
||||
[]float64{31, 15, 18, 35, 20, 20, 22, 27, 24, 30},
|
||||
[]string{"Tutorial 1", "Tutorial 2", "Extended Applications", "Numpy & MatPlotLib", "SciPy", "MonteCarlo", "Pandas & Seaborn", "Folium", "Statistical Test Methods", "Data Analysis"},
|
||||
"Points scored",
|
||||
"Lectures",
|
||||
"Lectures",
|
||||
"Points",
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = base("Dashboard").Render(templ.WithChildren(ctx, templ_7745c5c3_Var17), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func Login() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var18 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var18 == nil {
|
||||
templ_7745c5c3_Var18 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var19 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"login\"><img src=\"assets/img/learnlytics.svg\" alt=\"Learnlytics Logo\"><h1>Learnlytics</h1><form action=\"/\" method=\"POST\"><input type=\"text\" id=\"username\" name=\"username\" placeholder=\"Username\" required><br><br><input type=\"password\" id=\"password\" name=\"password\" placeholder=\"Password\" required><br><br><input type=\"submit\" value=\"Login\"></form></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = base("Login").Render(templ.WithChildren(ctx, templ_7745c5c3_Var19), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
5
components/go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module components
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require github.com/a-h/templ v0.3.833
|
4
components/go.sum
Normal file
@ -0,0 +1,4 @@
|
||||
github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
|
||||
github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
20
db/go.mod
Normal file
@ -0,0 +1,20 @@
|
||||
module db
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
gorm.io/driver/postgres v1.5.11
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.2 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
)
|
36
db/go.sum
Normal file
@ -0,0 +1,36 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
||||
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
78
db/model.go
Normal file
@ -0,0 +1,78 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Class struct {
|
||||
id uint "gorm:primaryKey"
|
||||
name string
|
||||
created_at time.Time
|
||||
}
|
||||
|
||||
type Student struct {
|
||||
id uint "gorm:primaryKey"
|
||||
prename string
|
||||
surname string
|
||||
sex string
|
||||
study_id uint
|
||||
class_id uint
|
||||
group_id uint
|
||||
created_at time.Time
|
||||
}
|
||||
|
||||
type Study struct {
|
||||
id uint "gorm:primaryKey"
|
||||
name string
|
||||
created_at time.Time
|
||||
}
|
||||
|
||||
type Lecture struct {
|
||||
id uint "gorm:primaryKey"
|
||||
title string
|
||||
points uint
|
||||
class_id uint
|
||||
created_at time.Time
|
||||
}
|
||||
|
||||
type Submission struct {
|
||||
id uint "gorm:primaryKey"
|
||||
student_id uint
|
||||
lecture_id uint
|
||||
class_id uint
|
||||
points float32
|
||||
created_at time.Time
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
id uint "gorm:primaryKey"
|
||||
name string
|
||||
project string
|
||||
has_passed bool
|
||||
presentation string
|
||||
class_id uint
|
||||
created_at time.Time
|
||||
}
|
||||
|
||||
func ConnectDB() *gorm.DB {
|
||||
dsn := "host=postgres.cyperpunk.de user=dergrumpf password=1P2h3i4lon$% dbname=phil port=5432 sslmode=disable TimeZone=Europe/Berlin"
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
db.AutoMigrate(
|
||||
&Class{},
|
||||
&Student{},
|
||||
&Study{},
|
||||
&Lecture{},
|
||||
&Submission{},
|
||||
&Group{},
|
||||
)
|
||||
|
||||
return db
|
||||
}
|
32
go.mod
Normal file
@ -0,0 +1,32 @@
|
||||
module learnlytics-go
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
components v0.0.0-00010101000000-000000000000
|
||||
github.com/a-h/templ v0.3.833
|
||||
)
|
||||
|
||||
require (
|
||||
db v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/alexedwards/scs/v2 v2.8.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.2 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
gorm.io/driver/postgres v1.5.11 // indirect
|
||||
gorm.io/gorm v1.25.12 // indirect
|
||||
)
|
||||
|
||||
replace github.com/a-h/templ => ./templ
|
||||
|
||||
replace components => ./components
|
||||
|
||||
replace db => ./db
|
||||
|
||||
replace handlers => ./handlers
|
33
go.sum
Normal file
@ -0,0 +1,33 @@
|
||||
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
|
||||
github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
||||
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
3
handlers/go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module handlers
|
||||
|
||||
go 1.24.1
|
11
handlers/handler.go
Normal file
@ -0,0 +1,11 @@
|
||||
package handlers
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type handler struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
func New(db *gorm.DB) handler {
|
||||
return handler{db}
|
||||
}
|
82
main.go
Normal file
@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
"components"
|
||||
//"db"
|
||||
"github.com/a-h/templ"
|
||||
"github.com/alexedwards/scs/v2"
|
||||
)
|
||||
|
||||
var sessionManager *scs.SessionManager
|
||||
|
||||
func getHandler(w http.ResponseWriter, r *http.Request) {
|
||||
component := components.Dashboard("DerGrumpf")
|
||||
component.Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func postHandler(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
|
||||
username := r.Form["username"][0]
|
||||
component := components.Dashboard(username)
|
||||
component.Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func errorHandler(w http.ResponseWriter, r *http.Request, status int) {
|
||||
w.WriteHeader(status)
|
||||
if status == http.StatusNotFound {
|
||||
component := components.NotFound()
|
||||
component.Render(r.Context(), w)
|
||||
}
|
||||
}
|
||||
|
||||
func testHandler(w http.ResponseWriter, r *http.Request) {
|
||||
component := components.Test()
|
||||
component.Render(r.Context(), w)
|
||||
}
|
||||
|
||||
func initMux() *http.ServeMux {
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Index Handle
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
errorHandler(w, r, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
postHandler(w, r)
|
||||
return
|
||||
}
|
||||
getHandler(w, r)
|
||||
})
|
||||
|
||||
// File Server
|
||||
mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets"))))
|
||||
|
||||
mux.HandleFunc("/test", testHandler)
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
sessionManager := scs.New()
|
||||
sessionManager.Lifetime = 24 * time.Hour
|
||||
sessionManager.Cookie.Persist = false
|
||||
|
||||
mux := initMux()
|
||||
|
||||
component := components.Login()
|
||||
http.Handle("/", templ.Handler(component))
|
||||
|
||||
fmt.Println("Listening on http://127.0.0.1:3000")
|
||||
if err := http.ListenAndServe(":3000", sessionManager.LoadAndSave(mux)); err != nil {
|
||||
fmt.Println("Error listening: %v", err)
|
||||
}
|
||||
}
|
3
templ/.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
.git
|
||||
Dockerfile
|
||||
.dockerignore
|
1
templ/.envrc
Normal file
@ -0,0 +1 @@
|
||||
use flake
|
1
templ/.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
github: [a-h, joerdav]
|
38
templ/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Before you begin**
|
||||
Please make sure you're using the latest version of the templ CLI (`go install github.com/a-h/templ/cmd/templ@latest`), and have upgraded your project to use the latest version of the templ runtime (`go get -u github.com/a-h/templ@latest`)
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
A small, self-container, complete reproduction, uploaded to a GitHub repo, containing the minimum amount of files required to reproduce the behaviour, along with a list of commands that need to be run. Keep it simple.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots or screen captures to help explain your problem.
|
||||
|
||||
**Logs**
|
||||
If the issue is related to IDE support, run through the LSP troubleshooting section at https://templ.guide/commands-and-tools/ide-support/#troubleshooting-1 and include logs from templ
|
||||
|
||||
**`templ info` output**
|
||||
Run `templ info` and include the output.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. MacOS, Linux, Windows, WSL]
|
||||
- templ CLI version (`templ version`)
|
||||
- Go version (`go version`)
|
||||
- `gopls` version (`gopls version`)
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
83
templ/.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,83 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@v16
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@v8
|
||||
|
||||
- name: Test
|
||||
run: nix develop --command xc test-cover
|
||||
|
||||
- name: Copy coverage.out to temp
|
||||
run: cp coverage.out $RUNNER_TEMP
|
||||
|
||||
- name: Update coverage report
|
||||
uses: ncruces/go-coverage-report@57ac6f0f19874f7afbab596105154f08004f482e
|
||||
with:
|
||||
coverage-file: ${{ runner.temp }}/coverage.out
|
||||
report: 'true'
|
||||
chart: 'true'
|
||||
reuse-go: 'true'
|
||||
if: |
|
||||
github.event_name == 'push'
|
||||
|
||||
- name: Build
|
||||
run: nix build
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@v16
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@v8
|
||||
|
||||
- name: Lint
|
||||
run: nix develop --command xc lint
|
||||
|
||||
ensure-generated:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@v16
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@v8
|
||||
|
||||
- name: Generate
|
||||
run: nix develop --command xc ensure-generated
|
||||
|
||||
ensure-fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@v16
|
||||
with:
|
||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: DeterminateSystems/magic-nix-cache-action@v8
|
||||
|
||||
- name: Fmt
|
||||
run: nix develop --command xc fmt
|
||||
|
||||
- name: Ensure clean
|
||||
run: git diff --exit-code
|
60
templ/.github/workflows/docs.yaml
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
build-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Setup Pages
|
||||
id: pages
|
||||
uses: actions/configure-pages@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: npm
|
||||
cache-dependency-path: "./docs/package-lock.json"
|
||||
- name: Install Node.js dependencies
|
||||
run: |
|
||||
cd docs
|
||||
npm ci
|
||||
- name: Build
|
||||
run: |
|
||||
cd docs
|
||||
npm run build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./docs/build
|
||||
|
||||
deploy-docs:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-docs
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
36
templ/.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.22
|
||||
cache: true
|
||||
- uses: ko-build/setup-ko@v0.7
|
||||
- uses: sigstore/cosign-installer@v3.7.0
|
||||
with:
|
||||
cosign-release: v2.2.3
|
||||
- uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: v1.24.0
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
COSIGN_PASSWORD: '${{ secrets.COSIGN_PASSWORD }}'
|
||||
COSIGN_PRIVATE_KEY: '${{ secrets.COSIGN_PRIVATE_KEY }}'
|
||||
COSIGN_PUBLIC_KEY: '${{ secrets.COSIGN_PUBLIC_KEY }}'
|
34
templ/.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
# Output.
|
||||
cmd/templ/templ
|
||||
|
||||
# Logs.
|
||||
cmd/templ/lspcmd/*log.txt
|
||||
|
||||
# Go code coverage.
|
||||
coverage.out
|
||||
coverage
|
||||
|
||||
# Mac filesystem jank.
|
||||
.DS_Store
|
||||
|
||||
# Docusaurus.
|
||||
docs/build/
|
||||
docs/resources/_gen/
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
# Nix artifacts.
|
||||
result
|
||||
|
||||
# Editors
|
||||
## nvim
|
||||
.null-ls*
|
||||
|
||||
# Go workspace.
|
||||
go.work
|
||||
|
||||
# direnv
|
||||
.direnv
|
||||
|
||||
# templ txt files.
|
||||
*_templ.txt
|
72
templ/.goreleaser.yaml
Normal file
@ -0,0 +1,72 @@
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
dir: cmd/templ
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
|
||||
signs:
|
||||
- id: checksums
|
||||
cmd: cosign
|
||||
stdin: '{{ .Env.COSIGN_PASSWORD }}'
|
||||
output: true
|
||||
artifacts: checksum
|
||||
args:
|
||||
- sign-blob
|
||||
- --yes
|
||||
- --key
|
||||
- env://COSIGN_PRIVATE_KEY
|
||||
- '--output-certificate=${certificate}'
|
||||
- '--output-signature=${signature}'
|
||||
- '${artifact}'
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
|
||||
kos:
|
||||
- repository: ghcr.io/a-h/templ
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
tags:
|
||||
- latest
|
||||
- '{{.Tag}}'
|
||||
bare: true
|
||||
|
||||
docker_signs:
|
||||
- cmd: cosign
|
||||
artifacts: all
|
||||
output: true
|
||||
args:
|
||||
- sign
|
||||
- --yes
|
||||
- --key
|
||||
- env://COSIGN_PRIVATE_KEY
|
||||
- '${artifact}'
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
9
templ/.ignore
Normal file
@ -0,0 +1,9 @@
|
||||
*_templ.go
|
||||
examples/integration-ct/static/index.js
|
||||
examples/counter/assets/css/bulma.*
|
||||
examples/counter/assets/js/htmx.min.js
|
||||
examples/counter-basic/assets/css/bulma.*
|
||||
examples/typescript/assets/index.js
|
||||
package-lock.json
|
||||
go.sum
|
||||
docs/static/llms.md
|
1
templ/.version
Normal file
@ -0,0 +1 @@
|
||||
0.3.833
|
12
templ/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"blockquote",
|
||||
"fieldset",
|
||||
"figcaption",
|
||||
"formatstring",
|
||||
"goexpression",
|
||||
"keygen",
|
||||
"strs",
|
||||
"templ"
|
||||
]
|
||||
}
|
128
templ/CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
adrianhesketh@hushail.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
244
templ/CONTRIBUTING.md
Normal file
@ -0,0 +1,244 @@
|
||||
# Contributing to templ
|
||||
|
||||
## Vision
|
||||
|
||||
Enable Go developers to build strongly typed, component-based HTML user interfaces with first-class developer tooling, and a short learning curve.
|
||||
|
||||
## Come up with a design and share it
|
||||
|
||||
Before starting work on any major pull requests or code changes, start a discussion at https://github.com/a-h/templ/discussions or raise an issue.
|
||||
|
||||
We don't want you to spend time on a PR or feature that ultimately doesn't get merged because it doesn't fit with the project goals, or the design doesn't work for some reason.
|
||||
|
||||
For issues, it really helps if you provide a reproduction repo, or can create a failing unit test to describe the behaviour.
|
||||
|
||||
In designs, we need to consider:
|
||||
|
||||
* Backwards compatibility - Not changing the public API between releases, introducing gradual deprecation - don't break people's code.
|
||||
* Correctness over time - How can we reduce the risk of defects both now, and in future releases?
|
||||
* Threat model - How could each change be used to inject vulnerabilities into web pages?
|
||||
* Go version - We target the oldest supported version of Go as per https://go.dev/doc/devel/release
|
||||
* Automatic migration - If we need to force through a change.
|
||||
* Compile time vs runtime errors - Prefer compile time.
|
||||
* Documentation - New features are only useful if people can understand the new feature, what would the documentation look like?
|
||||
* Examples - How will we demonstrate the feature?
|
||||
|
||||
## Project structure
|
||||
|
||||
templ is structured into a few areas:
|
||||
|
||||
### Parser `./parser`
|
||||
|
||||
The parser directory currently contains both v1 and v2 parsers.
|
||||
|
||||
The v1 parser is not maintained, it's only used to migrate v1 code over to the v2 syntax.
|
||||
|
||||
The parser is responsible for parsing templ files into an object model. The types that make up the object model are in `types.go`. Automatic formatting of the types is tested in `types_test.go`.
|
||||
|
||||
A templ file is parsed into the `TemplateFile` struct object model.
|
||||
|
||||
```go
|
||||
type TemplateFile struct {
|
||||
// Header contains comments or whitespace at the top of the file.
|
||||
Header []GoExpression
|
||||
// Package expression.
|
||||
Package Package
|
||||
// Nodes in the file.
|
||||
Nodes []TemplateFileNode
|
||||
}
|
||||
```
|
||||
|
||||
Parsers are individually tested using two types of unit test.
|
||||
|
||||
One test covers the successful parsing of text into an object. For example, the `HTMLCommentParser` test checks for successful patterns.
|
||||
|
||||
```go
|
||||
func TestHTMLCommentParser(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected HTMLComment
|
||||
}{
|
||||
{
|
||||
name: "comment - single line",
|
||||
input: `<!-- single line comment -->`,
|
||||
expected: HTMLComment{
|
||||
Contents: " single line comment ",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comment - no whitespace",
|
||||
input: `<!--no whitespace between sequence open and close-->`,
|
||||
expected: HTMLComment{
|
||||
Contents: "no whitespace between sequence open and close",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comment - multiline",
|
||||
input: `<!-- multiline
|
||||
comment
|
||||
-->`,
|
||||
expected: HTMLComment{
|
||||
Contents: ` multiline
|
||||
comment
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comment - with tag",
|
||||
input: `<!-- <p class="test">tag</p> -->`,
|
||||
expected: HTMLComment{
|
||||
Contents: ` <p class="test">tag</p> `,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comments can contain tags",
|
||||
input: `<!-- <div> hello world </div> -->`,
|
||||
expected: HTMLComment{
|
||||
Contents: ` <div> hello world </div> `,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
result, ok, err := htmlComment.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("parser error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse at %d", input.Index())
|
||||
}
|
||||
if diff := cmp.Diff(tt.expected, result); diff != "" {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Alongside each success test, is a similar test to check that invalid syntax is detected.
|
||||
|
||||
```go
|
||||
func TestHTMLCommentParserErrors(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
input string
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
name: "unclosed HTML comment",
|
||||
input: `<!-- unclosed HTML comment`,
|
||||
expected: parse.Error("expected end comment literal '-->' not found",
|
||||
parse.Position{
|
||||
Index: 26,
|
||||
Line: 0,
|
||||
Col: 26,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "comment in comment",
|
||||
input: `<!-- <-- other --> -->`,
|
||||
expected: parse.Error("comment contains invalid sequence '--'", parse.Position{
|
||||
Index: 8,
|
||||
Line: 0,
|
||||
Col: 8,
|
||||
}),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input := parse.NewInput(tt.input)
|
||||
_, _, err := htmlComment.Parse(input)
|
||||
if diff := cmp.Diff(tt.expected, err); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Generator
|
||||
|
||||
The generator takes the object model and writes out Go code that produces the expected output. Any changes to Go code output by templ are made in this area.
|
||||
|
||||
Testing of the generator is carried out by creating a templ file, and a matching expected output file.
|
||||
|
||||
For example, `./generator/test-a-href` contains a templ file of:
|
||||
|
||||
```templ
|
||||
package testahref
|
||||
|
||||
templ render() {
|
||||
<a href="javascript:alert('unaffected');">Ignored</a>
|
||||
<a href={ templ.URL("javascript:alert('should be sanitized')") }>Sanitized</a>
|
||||
<a href={ templ.SafeURL("javascript:alert('should not be sanitized')") }>Unsanitized</a>
|
||||
}
|
||||
```
|
||||
|
||||
It also contains an expected output file.
|
||||
|
||||
```html
|
||||
<a href="javascript:alert('unaffected');">Ignored</a>
|
||||
<a href="about:invalid#TemplFailedSanitizationURL">Sanitized</a>
|
||||
<a href="javascript:alert('should not be sanitized')">Unsanitized</a>
|
||||
```
|
||||
|
||||
These tests contribute towards the code coverage metrics by building an instrumented test CLI program. See the `test-cover` task in the `README.md` file.
|
||||
|
||||
### CLI
|
||||
|
||||
The command line interface for templ is used to generate Go code from templ files, format templ files, and run the LSP.
|
||||
|
||||
The code for this is at `./cmd/templ`.
|
||||
|
||||
Testing of the templ command line is done with unit tests to check the argument parsing.
|
||||
|
||||
The `templ generate` command is tested by generating templ files in the project, and testing that the expected output HTML is present.
|
||||
|
||||
### Runtime
|
||||
|
||||
The runtime is used by generated code, and by template authors, to serve template content over HTTP, and to carry out various operations.
|
||||
|
||||
It is in the root directory of the project at `./runtime.go`. The runtime is unit tested, as well as being tested as part of the `generate` tests.
|
||||
|
||||
### LSP
|
||||
|
||||
The LSP is structured within the command line interface, and proxies commands through to the `gopls` LSP.
|
||||
|
||||
### Docs
|
||||
|
||||
The docs are a Docusaurus project at `./docs`.
|
||||
|
||||
## Coding
|
||||
|
||||
### Build tasks
|
||||
|
||||
templ uses the `xc` task runner - https://github.com/joerdav/xc
|
||||
|
||||
If you run `xc` you can get see a list of the development tasks that can be run, or you can read the `README.md` file and see the `Tasks` section.
|
||||
|
||||
The most useful tasks for local development are:
|
||||
|
||||
* `install-snapshot` - this builds the templ CLI and installs it into `~/bin`. Ensure that this is in your path.
|
||||
* `test` - this regenerates all templates, and runs the unit tests.
|
||||
* `fmt` - run the `gofmt` tool to format all Go code.
|
||||
* `lint` - run the same linting as run in the CI process.
|
||||
* `docs-run` - run the Docusaurus documentation site.
|
||||
|
||||
### Commit messages
|
||||
|
||||
The project using https://www.conventionalcommits.org/en/v1.0.0/
|
||||
|
||||
Examples:
|
||||
|
||||
* `feat: support Go comments in templates, fixes #234"`
|
||||
|
||||
### Coding style
|
||||
|
||||
* Reduce nesting - i.e. prefer early returns over an `else` block, as per https://danp.net/posts/reducing-go-nesting/ or https://go.dev/doc/effective_go#if
|
||||
* Use line breaks to separate "paragraphs" of code - don't use line breaks in between lines, or at the start/end of functions etc.
|
||||
* Use the `fmt` and `lint` build tasks to format and lint your code before submitting a PR.
|
||||
|
21
templ/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Adrian Hesketh
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
184
templ/README.md
Normal file
@ -0,0 +1,184 @@
|
||||

|
||||
|
||||
## An HTML templating language for Go that has great developer tooling.
|
||||
|
||||

|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
See user documentation at https://templ.guide
|
||||
|
||||
<p align="center">
|
||||
<a href="https://pkg.go.dev/github.com/a-h/templ"><img src="https://pkg.go.dev/badge/github.com/a-h/templ.svg" alt="Go Reference" /></a>
|
||||
<a href="https://xcfile.dev"><img src="https://xcfile.dev/badge.svg" alt="xc compatible" /></a>
|
||||
<a href="https://raw.githack.com/wiki/a-h/templ/coverage.html"><img src="https://github.com/a-h/templ/wiki/coverage.svg" alt="Go Coverage" /></a>
|
||||
<a href="https://goreportcard.com/report/github.com/a-h/templ"><img src="https://goreportcard.com/badge/github.com/a-h/templ" alt="Go Report Card" /></a>
|
||||
</p>
|
||||
|
||||
## Tasks
|
||||
|
||||
### build
|
||||
|
||||
Build a local version.
|
||||
|
||||
```sh
|
||||
go run ./get-version > .version
|
||||
cd cmd/templ
|
||||
go build
|
||||
```
|
||||
|
||||
### install-snapshot
|
||||
|
||||
Build and install current version.
|
||||
|
||||
```sh
|
||||
# Remove templ from the non-standard ~/bin/templ path
|
||||
# that this command previously used.
|
||||
rm -f ~/bin/templ
|
||||
# Clear LSP logs.
|
||||
rm -f cmd/templ/lspcmd/*.txt
|
||||
# Update version.
|
||||
go run ./get-version > .version
|
||||
# Install to $GOPATH/bin or $HOME/go/bin
|
||||
cd cmd/templ && go install
|
||||
```
|
||||
|
||||
### build-snapshot
|
||||
|
||||
Use goreleaser to build the command line binary using goreleaser.
|
||||
|
||||
```sh
|
||||
goreleaser build --snapshot --clean
|
||||
```
|
||||
|
||||
### generate
|
||||
|
||||
Run templ generate using local version.
|
||||
|
||||
```sh
|
||||
go run ./cmd/templ generate -include-version=false
|
||||
```
|
||||
|
||||
### test
|
||||
|
||||
Run Go tests.
|
||||
|
||||
```sh
|
||||
go run ./get-version > .version
|
||||
go run ./cmd/templ generate -include-version=false
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### test-short
|
||||
|
||||
Run Go tests.
|
||||
|
||||
```sh
|
||||
go run ./get-version > .version
|
||||
go run ./cmd/templ generate -include-version=false
|
||||
go test ./... -short
|
||||
```
|
||||
|
||||
### test-cover
|
||||
|
||||
Run Go tests.
|
||||
|
||||
```sh
|
||||
# Create test profile directories.
|
||||
mkdir -p coverage/fmt
|
||||
mkdir -p coverage/generate
|
||||
mkdir -p coverage/version
|
||||
mkdir -p coverage/unit
|
||||
# Build the test binary.
|
||||
go build -cover -o ./coverage/templ-cover ./cmd/templ
|
||||
# Run the covered generate command.
|
||||
GOCOVERDIR=coverage/fmt ./coverage/templ-cover fmt .
|
||||
GOCOVERDIR=coverage/generate ./coverage/templ-cover generate -include-version=false
|
||||
GOCOVERDIR=coverage/version ./coverage/templ-cover version
|
||||
# Run the unit tests.
|
||||
go test -cover ./... -coverpkg ./... -args -test.gocoverdir="$PWD/coverage/unit"
|
||||
# Display the combined percentage.
|
||||
go tool covdata percent -i=./coverage/fmt,./coverage/generate,./coverage/version,./coverage/unit
|
||||
# Generate a text coverage profile for tooling to use.
|
||||
go tool covdata textfmt -i=./coverage/fmt,./coverage/generate,./coverage/version,./coverage/unit -o coverage.out
|
||||
# Print total
|
||||
go tool cover -func coverage.out | grep total
|
||||
```
|
||||
|
||||
### test-cover-watch
|
||||
|
||||
```sh
|
||||
gotestsum --watch -- -coverprofile=coverage.out
|
||||
```
|
||||
|
||||
### test-fuzz
|
||||
|
||||
```sh
|
||||
./parser/v2/fuzz.sh
|
||||
./parser/v2/goexpression/fuzz.sh
|
||||
```
|
||||
|
||||
### benchmark
|
||||
|
||||
Run benchmarks.
|
||||
|
||||
```sh
|
||||
go run ./cmd/templ generate -include-version=false && go test ./... -bench=. -benchmem
|
||||
```
|
||||
|
||||
### fmt
|
||||
|
||||
Format all Go and templ code.
|
||||
|
||||
```sh
|
||||
gofmt -s -w .
|
||||
go run ./cmd/templ fmt .
|
||||
```
|
||||
|
||||
### lint
|
||||
|
||||
Run the lint operations that are run as part of the CI.
|
||||
|
||||
```sh
|
||||
golangci-lint run --verbose
|
||||
```
|
||||
|
||||
### ensure-generated
|
||||
|
||||
Ensure that templ files have been generated with the local version of templ, and that those files have been added to git.
|
||||
|
||||
Requires: generate
|
||||
|
||||
```sh
|
||||
git diff --exit-code
|
||||
```
|
||||
|
||||
### push-release-tag
|
||||
|
||||
Push a semantic version number to GitHub to trigger the release process.
|
||||
|
||||
```sh
|
||||
./push-tag.sh
|
||||
```
|
||||
|
||||
### docs-run
|
||||
|
||||
Run the development server.
|
||||
|
||||
Directory: docs
|
||||
|
||||
```sh
|
||||
npm run start
|
||||
```
|
||||
|
||||
### docs-build
|
||||
|
||||
Build production docs site.
|
||||
|
||||
Directory: docs
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
9
templ/SECURITY.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
The latest version of templ is supported.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use the "Security" tab in GitHub and fill out the "Report a vulnerability" form.
|
3
templ/benchmarks/react/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
index.js
|
||||
node_modules
|
||||
|
23
templ/benchmarks/react/README.md
Normal file
@ -0,0 +1,23 @@
|
||||
# React benchmark
|
||||
|
||||
## Tasks
|
||||
|
||||
### install
|
||||
|
||||
```
|
||||
npm i
|
||||
```
|
||||
|
||||
### build
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### run
|
||||
|
||||
requires: build
|
||||
|
||||
```sh
|
||||
npm start
|
||||
```
|
719
templ/benchmarks/react/package-lock.json
generated
Normal file
@ -0,0 +1,719 @@
|
||||
{
|
||||
"name": "react-benchmark",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "react-benchmark",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"benchmark": "^2.1.4",
|
||||
"esbuild": "0.19.2",
|
||||
"microtime": "^3.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.2.tgz",
|
||||
"integrity": "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.2.tgz",
|
||||
"integrity": "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.2.tgz",
|
||||
"integrity": "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.2.tgz",
|
||||
"integrity": "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.2.tgz",
|
||||
"integrity": "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.2.tgz",
|
||||
"integrity": "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.2.tgz",
|
||||
"integrity": "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.2.tgz",
|
||||
"integrity": "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.2.tgz",
|
||||
"integrity": "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/benchmark": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
|
||||
"integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.4",
|
||||
"platform": "^1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.2.tgz",
|
||||
"integrity": "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.19.2",
|
||||
"@esbuild/android-arm64": "0.19.2",
|
||||
"@esbuild/android-x64": "0.19.2",
|
||||
"@esbuild/darwin-arm64": "0.19.2",
|
||||
"@esbuild/darwin-x64": "0.19.2",
|
||||
"@esbuild/freebsd-arm64": "0.19.2",
|
||||
"@esbuild/freebsd-x64": "0.19.2",
|
||||
"@esbuild/linux-arm": "0.19.2",
|
||||
"@esbuild/linux-arm64": "0.19.2",
|
||||
"@esbuild/linux-ia32": "0.19.2",
|
||||
"@esbuild/linux-loong64": "0.19.2",
|
||||
"@esbuild/linux-mips64el": "0.19.2",
|
||||
"@esbuild/linux-ppc64": "0.19.2",
|
||||
"@esbuild/linux-riscv64": "0.19.2",
|
||||
"@esbuild/linux-s390x": "0.19.2",
|
||||
"@esbuild/linux-x64": "0.19.2",
|
||||
"@esbuild/netbsd-x64": "0.19.2",
|
||||
"@esbuild/openbsd-x64": "0.19.2",
|
||||
"@esbuild/sunos-x64": "0.19.2",
|
||||
"@esbuild/win32-arm64": "0.19.2",
|
||||
"@esbuild/win32-ia32": "0.19.2",
|
||||
"@esbuild/win32-x64": "0.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/microtime": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/microtime/-/microtime-3.0.0.tgz",
|
||||
"integrity": "sha512-SirJr7ZL4ow2iWcb54bekS4aWyBQNVcEDBiwAz9D/sTgY59A+uE8UJU15cp5wyZmPBwg/3zf8lyCJ5NUe1nVlQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^1.2.0",
|
||||
"node-gyp-build": "^3.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
|
||||
"integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.9.0.tgz",
|
||||
"integrity": "sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A==",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/platform": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
|
||||
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
||||
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@esbuild/android-arm": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.2.tgz",
|
||||
"integrity": "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/android-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/darwin-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/darwin-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/freebsd-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/freebsd-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-arm": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.2.tgz",
|
||||
"integrity": "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-ia32": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.2.tgz",
|
||||
"integrity": "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-loong64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.2.tgz",
|
||||
"integrity": "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-mips64el": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.2.tgz",
|
||||
"integrity": "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-ppc64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.2.tgz",
|
||||
"integrity": "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-riscv64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.2.tgz",
|
||||
"integrity": "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-s390x": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.2.tgz",
|
||||
"integrity": "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/linux-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/netbsd-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/openbsd-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/sunos-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-ia32": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.2.tgz",
|
||||
"integrity": "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==",
|
||||
"optional": true
|
||||
},
|
||||
"@esbuild/win32-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==",
|
||||
"optional": true
|
||||
},
|
||||
"benchmark": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
|
||||
"integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=",
|
||||
"requires": {
|
||||
"lodash": "^4.17.4",
|
||||
"platform": "^1.3.3"
|
||||
}
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.2.tgz",
|
||||
"integrity": "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==",
|
||||
"requires": {
|
||||
"@esbuild/android-arm": "0.19.2",
|
||||
"@esbuild/android-arm64": "0.19.2",
|
||||
"@esbuild/android-x64": "0.19.2",
|
||||
"@esbuild/darwin-arm64": "0.19.2",
|
||||
"@esbuild/darwin-x64": "0.19.2",
|
||||
"@esbuild/freebsd-arm64": "0.19.2",
|
||||
"@esbuild/freebsd-x64": "0.19.2",
|
||||
"@esbuild/linux-arm": "0.19.2",
|
||||
"@esbuild/linux-arm64": "0.19.2",
|
||||
"@esbuild/linux-ia32": "0.19.2",
|
||||
"@esbuild/linux-loong64": "0.19.2",
|
||||
"@esbuild/linux-mips64el": "0.19.2",
|
||||
"@esbuild/linux-ppc64": "0.19.2",
|
||||
"@esbuild/linux-riscv64": "0.19.2",
|
||||
"@esbuild/linux-s390x": "0.19.2",
|
||||
"@esbuild/linux-x64": "0.19.2",
|
||||
"@esbuild/netbsd-x64": "0.19.2",
|
||||
"@esbuild/openbsd-x64": "0.19.2",
|
||||
"@esbuild/sunos-x64": "0.19.2",
|
||||
"@esbuild/win32-arm64": "0.19.2",
|
||||
"@esbuild/win32-ia32": "0.19.2",
|
||||
"@esbuild/win32-x64": "0.19.2"
|
||||
}
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"requires": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"microtime": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/microtime/-/microtime-3.0.0.tgz",
|
||||
"integrity": "sha512-SirJr7ZL4ow2iWcb54bekS4aWyBQNVcEDBiwAz9D/sTgY59A+uE8UJU15cp5wyZmPBwg/3zf8lyCJ5NUe1nVlQ==",
|
||||
"requires": {
|
||||
"node-addon-api": "^1.2.0",
|
||||
"node-gyp-build": "^3.8.0"
|
||||
}
|
||||
},
|
||||
"node-addon-api": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
|
||||
"integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="
|
||||
},
|
||||
"node-gyp-build": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.9.0.tgz",
|
||||
"integrity": "sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A=="
|
||||
},
|
||||
"platform": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
|
||||
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
|
||||
},
|
||||
"react": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.0"
|
||||
}
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
||||
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
templ/benchmarks/react/package.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "react-benchmark",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "./src/index.jsx",
|
||||
"scripts": {
|
||||
"build": "esbuild ./src/index.jsx --bundle --outfile=index.js",
|
||||
"start": "node index.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"benchmark": "^2.1.4",
|
||||
"esbuild": "0.19.2",
|
||||
"microtime": "^3.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
}
|
||||
}
|
34
templ/benchmarks/react/src/index.jsx
Normal file
@ -0,0 +1,34 @@
|
||||
import * as React from 'react'
|
||||
import * as Server from 'react-dom/server'
|
||||
import Benchmark from 'benchmark';
|
||||
|
||||
const component = (p) =>
|
||||
<div>
|
||||
<h1>{p.Name}</h1>
|
||||
<div style={{ fontFamily: "sans-serif" }} id="test" data-contents="something with "quotes" and a <tag>">
|
||||
<div>email:<a href="mailto: luiz@example.com">luiz@example.com</a></div>
|
||||
</div>
|
||||
<hr noshade /><hr optionA optionB optionC="other" /><hr noshade />
|
||||
</div>;
|
||||
|
||||
const p = {
|
||||
Name: "Luiz Bonfa",
|
||||
Email: "luiz@example.com",
|
||||
};
|
||||
|
||||
// Benchmark.
|
||||
// Outputs...
|
||||
// Render test x 114,131 ops/sec ±0.27% (97 runs sampled)
|
||||
// There are 1,000,000,000 nanoseconds in a second.
|
||||
// 1,000,000,000ns / 114,131 ops = 8,757.5 ns per operation.
|
||||
// The templ equivalent is 340 ns per operation.
|
||||
const suite = new Benchmark.Suite;
|
||||
|
||||
const test = suite.add('Render test',
|
||||
() => Server.renderToString(component(p)))
|
||||
|
||||
test.on('cycle', (event) => {
|
||||
console.log(String(event.target));
|
||||
});
|
||||
|
||||
test.run();
|
27
templ/benchmarks/templ/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# templ benchmark
|
||||
|
||||
Used to test code generation strategies for improvements to render time.
|
||||
|
||||
## Tasks
|
||||
|
||||
### run
|
||||
|
||||
```
|
||||
go test -bench .
|
||||
```
|
||||
|
||||
## Results as of 2023-08-17
|
||||
|
||||
```
|
||||
go test -bench .
|
||||
goos: darwin
|
||||
goarch: arm64
|
||||
pkg: github.com/a-h/templ/benchmarks/templ
|
||||
BenchmarkTempl-10 3291883 369.1 ns/op 536 B/op 6 allocs/op
|
||||
BenchmarkGoTemplate-10 481052 2475 ns/op 1400 B/op 38 allocs/op
|
||||
BenchmarkIOWriteString-10 20353198 56.64 ns/op 320 B/op 1 allocs/op
|
||||
PASS
|
||||
ok github.com/a-h/templ/benchmarks/templ 4.650s
|
||||
```
|
||||
|
||||
React comes in at 1,000,000,000ns / 114,131 ops/s = 8,757.5 ns per operation.
|
6
templ/benchmarks/templ/data.go
Normal file
@ -0,0 +1,6 @@
|
||||
package testhtml
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Email string
|
||||
}
|
87
templ/benchmarks/templ/render_test.go
Normal file
@ -0,0 +1,87 @@
|
||||
package testhtml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/a-h/templ/parser/v2"
|
||||
)
|
||||
|
||||
func BenchmarkTemplRender(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
t := Render(Person{
|
||||
Name: "Luiz Bonfa",
|
||||
Email: "luiz@example.com",
|
||||
})
|
||||
|
||||
w := new(strings.Builder)
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := t.Render(context.Background(), w)
|
||||
if err != nil {
|
||||
b.Errorf("failed to render: %v", err)
|
||||
}
|
||||
w.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed template.templ
|
||||
var parserBenchmarkTemplate string
|
||||
|
||||
func BenchmarkTemplParser(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
tf, err := parser.ParseString(parserBenchmarkTemplate)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if tf.Package.Expression.Value == "" {
|
||||
b.Fatal("unexpected nil template")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var goTemplate = template.Must(template.New("example").Parse(`<div>
|
||||
<h1>{{.Name}}</h1>
|
||||
<div style="font-family: 'sans-serif'" id="test" data-contents="something with "quotes" and a <tag>">
|
||||
<div>
|
||||
email:<a href="mailto: {{.Email}}">{{.Email}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<hr noshade>
|
||||
<hr optionA optionB optionC="other">
|
||||
<hr noshade>
|
||||
`))
|
||||
|
||||
func BenchmarkGoTemplateRender(b *testing.B) {
|
||||
w := new(strings.Builder)
|
||||
person := Person{
|
||||
Name: "Luiz Bonfa",
|
||||
Email: "luiz@exapmle.com",
|
||||
}
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := goTemplate.Execute(w, person)
|
||||
if err != nil {
|
||||
b.Errorf("failed to render: %v", err)
|
||||
}
|
||||
w.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
const html = `<div><h1>Luiz Bonfa</h1><div style="font-family: 'sans-serif'" id="test" data-contents="something with "quotes" and a <tag>"><div>email:<a href="mailto: luiz@example.com">luiz@example.com</a></div></div></div><hr noshade><hr optionA optionB optionC="other"><hr noshade>`
|
||||
|
||||
func BenchmarkIOWriteString(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
w := new(strings.Builder)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := io.WriteString(w, html)
|
||||
if err != nil {
|
||||
b.Errorf("failed to render: %v", err)
|
||||
}
|
||||
w.Reset()
|
||||
}
|
||||
}
|
13
templ/benchmarks/templ/template.templ
Normal file
@ -0,0 +1,13 @@
|
||||
package testhtml
|
||||
|
||||
templ Render(p Person) {
|
||||
<div>
|
||||
<h1>{ p.Name }</h1>
|
||||
<div style="font-family: 'sans-serif'" id="test" data-contents={ `something with "quotes" and a <tag>` }>
|
||||
<div>email:<a href={ templ.URL("mailto: " + p.Email) }>{ p.Email }</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<hr noshade?={ true }/>
|
||||
<hr optionA optionB?={ true } optionC="other" optionD?={ false }/>
|
||||
<hr noshade/>
|
||||
}
|
118
templ/benchmarks/templ/template_templ.go
Normal file
@ -0,0 +1,118 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package testhtml
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func Render(p Person) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div><h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(p.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/benchmarks/templ/template.templ`, Line: 5, Col: 14}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h1><div style=\"font-family: 'sans-serif'\" id=\"test\" data-contents=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(`something with "quotes" and a <tag>`)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/benchmarks/templ/template.templ`, Line: 6, Col: 104}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><div>email:<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 templ.SafeURL = templ.URL("mailto: " + p.Email)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(p.Email)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templ/benchmarks/templ/template.templ`, Line: 7, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</a></div></div></div><hr")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if true {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " noshade")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "><hr optionA")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if true {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " optionB")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " optionC=\"other\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if false {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " optionD")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "><hr noshade>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
20
templ/cfg/cfg.go
Normal file
@ -0,0 +1,20 @@
|
||||
// This package is inspired by the GOEXPERIMENT approach of allowing feature flags for experimenting with breaking changes.
|
||||
package cfg
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Flags struct{}
|
||||
|
||||
var Experiment = parse()
|
||||
|
||||
func parse() *Flags {
|
||||
m := map[string]bool{}
|
||||
for _, f := range strings.Split(os.Getenv("TEMPL_EXPERIMENT"), ",") {
|
||||
m[strings.ToLower(f)] = true
|
||||
}
|
||||
|
||||
return &Flags{}
|
||||
}
|
166
templ/cmd/templ/fmtcmd/main.go
Normal file
@ -0,0 +1,166 @@
|
||||
package fmtcmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/a-h/templ/cmd/templ/imports"
|
||||
"github.com/a-h/templ/cmd/templ/processor"
|
||||
parser "github.com/a-h/templ/parser/v2"
|
||||
"github.com/natefinch/atomic"
|
||||
)
|
||||
|
||||
type Arguments struct {
|
||||
FailIfChanged bool
|
||||
ToStdout bool
|
||||
StdinFilepath string
|
||||
Files []string
|
||||
WorkerCount int
|
||||
}
|
||||
|
||||
func Run(log *slog.Logger, stdin io.Reader, stdout io.Writer, args Arguments) (err error) {
|
||||
// If no files are provided, read from stdin and write to stdout.
|
||||
if len(args.Files) == 0 {
|
||||
out, _ := format(writeToWriter(stdout), readFromReader(stdin, args.StdinFilepath), true)
|
||||
return out
|
||||
}
|
||||
process := func(fileName string) (error, bool) {
|
||||
read := readFromFile(fileName)
|
||||
write := writeToFile
|
||||
if args.ToStdout {
|
||||
write = writeToWriter(stdout)
|
||||
}
|
||||
writeIfUnchanged := args.ToStdout
|
||||
return format(write, read, writeIfUnchanged)
|
||||
}
|
||||
dir := args.Files[0]
|
||||
return NewFormatter(log, dir, process, args.WorkerCount, args.FailIfChanged).Run()
|
||||
}
|
||||
|
||||
type Formatter struct {
|
||||
Log *slog.Logger
|
||||
Dir string
|
||||
Process func(fileName string) (error, bool)
|
||||
WorkerCount int
|
||||
FailIfChange bool
|
||||
}
|
||||
|
||||
func NewFormatter(log *slog.Logger, dir string, process func(fileName string) (error, bool), workerCount int, failIfChange bool) *Formatter {
|
||||
f := &Formatter{
|
||||
Log: log,
|
||||
Dir: dir,
|
||||
Process: process,
|
||||
WorkerCount: workerCount,
|
||||
FailIfChange: failIfChange,
|
||||
}
|
||||
if f.WorkerCount == 0 {
|
||||
f.WorkerCount = runtime.NumCPU()
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *Formatter) Run() (err error) {
|
||||
changesMade := 0
|
||||
start := time.Now()
|
||||
results := make(chan processor.Result)
|
||||
f.Log.Debug("Walking directory", slog.String("path", f.Dir))
|
||||
go processor.Process(f.Dir, f.Process, f.WorkerCount, results)
|
||||
var successCount, errorCount int
|
||||
for r := range results {
|
||||
if r.ChangesMade {
|
||||
changesMade += 1
|
||||
}
|
||||
if r.Error != nil {
|
||||
f.Log.Error(r.FileName, slog.Any("error", r.Error))
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
f.Log.Debug(r.FileName, slog.Duration("duration", r.Duration))
|
||||
successCount++
|
||||
}
|
||||
|
||||
if f.FailIfChange && changesMade > 0 {
|
||||
f.Log.Error("Templates were valid but not properly formatted", slog.Int("count", successCount+errorCount), slog.Int("changed", changesMade), slog.Int("errors", errorCount), slog.Duration("duration", time.Since(start)))
|
||||
return fmt.Errorf("templates were not formatted properly")
|
||||
}
|
||||
|
||||
f.Log.Info("Format Complete", slog.Int("count", successCount+errorCount), slog.Int("errors", errorCount), slog.Int("changed", changesMade), slog.Duration("duration", time.Since(start)))
|
||||
|
||||
if errorCount > 0 {
|
||||
return fmt.Errorf("formatting failed")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type reader func() (fileName, src string, err error)
|
||||
|
||||
func readFromReader(r io.Reader, stdinFilepath string) func() (fileName, src string, err error) {
|
||||
return func() (fileName, src string, err error) {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to read stdin: %w", err)
|
||||
}
|
||||
return stdinFilepath, string(b), nil
|
||||
}
|
||||
}
|
||||
|
||||
func readFromFile(name string) reader {
|
||||
return func() (fileName, src string, err error) {
|
||||
b, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to read file %q: %w", fileName, err)
|
||||
}
|
||||
return name, string(b), nil
|
||||
}
|
||||
}
|
||||
|
||||
type writer func(fileName, tgt string) error
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
func writeToWriter(w io.Writer) func(fileName, tgt string) error {
|
||||
return func(fileName, tgt string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
_, err := w.Write([]byte(tgt))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func writeToFile(fileName, tgt string) error {
|
||||
return atomic.WriteFile(fileName, bytes.NewBufferString(tgt))
|
||||
}
|
||||
|
||||
func format(write writer, read reader, writeIfUnchanged bool) (err error, fileChanged bool) {
|
||||
fileName, src, err := read()
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
t, err := parser.ParseString(src)
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
t.Filepath = fileName
|
||||
t, err = imports.Process(t)
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
w := new(bytes.Buffer)
|
||||
if err = t.Write(w); err != nil {
|
||||
return fmt.Errorf("formatting error: %w", err), false
|
||||
}
|
||||
|
||||
fileChanged = (src != w.String())
|
||||
|
||||
if !writeIfUnchanged && !fileChanged {
|
||||
return nil, fileChanged
|
||||
}
|
||||
return write(fileName, w.String()), fileChanged
|
||||
}
|
163
templ/cmd/templ/fmtcmd/main_test.go
Normal file
@ -0,0 +1,163 @@
|
||||
package fmtcmd
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/tools/txtar"
|
||||
)
|
||||
|
||||
//go:embed testdata.txtar
|
||||
var testDataTxTar []byte
|
||||
|
||||
type testProject struct {
|
||||
dir string
|
||||
cleanup func()
|
||||
testFiles map[string]testFile
|
||||
}
|
||||
|
||||
type testFile struct {
|
||||
name string
|
||||
input, expected string
|
||||
}
|
||||
|
||||
func setupProjectDir() (tp testProject, err error) {
|
||||
tp.dir, err = os.MkdirTemp("", "fmtcmd_test_*")
|
||||
if err != nil {
|
||||
return tp, fmt.Errorf("failed to make test dir: %w", err)
|
||||
}
|
||||
tp.testFiles = make(map[string]testFile)
|
||||
testData := txtar.Parse(testDataTxTar)
|
||||
for i := 0; i < len(testData.Files); i += 2 {
|
||||
file := testData.Files[i]
|
||||
err = os.WriteFile(filepath.Join(tp.dir, file.Name), file.Data, 0660)
|
||||
if err != nil {
|
||||
return tp, fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
tp.testFiles[file.Name] = testFile{
|
||||
name: filepath.Join(tp.dir, file.Name),
|
||||
input: string(file.Data),
|
||||
expected: string(testData.Files[i+1].Data),
|
||||
}
|
||||
}
|
||||
tp.cleanup = func() {
|
||||
os.RemoveAll(tp.dir)
|
||||
}
|
||||
return tp, nil
|
||||
}
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
t.Run("can format a single file from stdin to stdout", func(t *testing.T) {
|
||||
tp, err := setupProjectDir()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to setup project dir: %v", err)
|
||||
}
|
||||
defer tp.cleanup()
|
||||
stdin := strings.NewReader(tp.testFiles["a.templ"].input)
|
||||
stdout := new(strings.Builder)
|
||||
if err = Run(log, stdin, stdout, Arguments{
|
||||
ToStdout: true,
|
||||
}); err != nil {
|
||||
t.Fatalf("failed to run format command: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tp.testFiles["a.templ"].expected, stdout.String()); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
t.Run("can process a single file to stdout", func(t *testing.T) {
|
||||
tp, err := setupProjectDir()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to setup project dir: %v", err)
|
||||
}
|
||||
defer tp.cleanup()
|
||||
stdout := new(strings.Builder)
|
||||
if err = Run(log, nil, stdout, Arguments{
|
||||
ToStdout: true,
|
||||
Files: []string{
|
||||
tp.testFiles["a.templ"].name,
|
||||
},
|
||||
FailIfChanged: false,
|
||||
}); err != nil {
|
||||
t.Fatalf("failed to run format command: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tp.testFiles["a.templ"].expected, stdout.String()); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
t.Run("can process a single file in place", func(t *testing.T) {
|
||||
tp, err := setupProjectDir()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to setup project dir: %v", err)
|
||||
}
|
||||
defer tp.cleanup()
|
||||
if err = Run(log, nil, nil, Arguments{
|
||||
Files: []string{
|
||||
tp.testFiles["a.templ"].name,
|
||||
},
|
||||
FailIfChanged: false,
|
||||
}); err != nil {
|
||||
t.Fatalf("failed to run format command: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(tp.testFiles["a.templ"].name)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read file: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tp.testFiles["a.templ"].expected, string(data)); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fails when fail flag used and change occurs", func(t *testing.T) {
|
||||
tp, err := setupProjectDir()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to setup project dir: %v", err)
|
||||
}
|
||||
defer tp.cleanup()
|
||||
if err = Run(log, nil, nil, Arguments{
|
||||
Files: []string{
|
||||
tp.testFiles["a.templ"].name,
|
||||
},
|
||||
FailIfChanged: true,
|
||||
}); err == nil {
|
||||
t.Fatal("command should have exited with an error and did not")
|
||||
}
|
||||
data, err := os.ReadFile(tp.testFiles["a.templ"].name)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read file: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tp.testFiles["a.templ"].expected, string(data)); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("passes when fail flag used and no change occurs", func(t *testing.T) {
|
||||
tp, err := setupProjectDir()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to setup project dir: %v", err)
|
||||
}
|
||||
defer tp.cleanup()
|
||||
if err = Run(log, nil, nil, Arguments{
|
||||
Files: []string{
|
||||
tp.testFiles["c.templ"].name,
|
||||
},
|
||||
FailIfChanged: true,
|
||||
}); err != nil {
|
||||
t.Fatalf("failed to run format command: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(tp.testFiles["c.templ"].name)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read file: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tp.testFiles["c.templ"].expected, string(data)); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
54
templ/cmd/templ/fmtcmd/testdata.txtar
Normal file
@ -0,0 +1,54 @@
|
||||
-- a.templ --
|
||||
package test
|
||||
|
||||
templ a() {
|
||||
<div><p class={templ.Class("mapped")}>A
|
||||
</p></div>
|
||||
}
|
||||
-- a.templ --
|
||||
package test
|
||||
|
||||
templ a() {
|
||||
<div>
|
||||
<p class={ templ.Class("mapped") }>
|
||||
A
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
-- b.templ --
|
||||
package test
|
||||
|
||||
templ b() {
|
||||
<div><p>B
|
||||
</p></div>
|
||||
}
|
||||
-- b.templ --
|
||||
package test
|
||||
|
||||
templ b() {
|
||||
<div>
|
||||
<p>
|
||||
B
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
-- c.templ --
|
||||
package test
|
||||
|
||||
templ c() {
|
||||
<div>
|
||||
<p>
|
||||
C
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
-- c.templ --
|
||||
package test
|
||||
|
||||
templ c() {
|
||||
<div>
|
||||
<p>
|
||||
C
|
||||
</p>
|
||||
</div>
|
||||
}
|
403
templ/cmd/templ/generatecmd/cmd.go
Normal file
@ -0,0 +1,403 @@
|
||||
package generatecmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"github.com/a-h/templ/cmd/templ/generatecmd/modcheck"
|
||||
"github.com/a-h/templ/cmd/templ/generatecmd/proxy"
|
||||
"github.com/a-h/templ/cmd/templ/generatecmd/run"
|
||||
"github.com/a-h/templ/cmd/templ/generatecmd/watcher"
|
||||
"github.com/a-h/templ/generator"
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/cli/browser"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
const defaultWatchPattern = `(.+\.go$)|(.+\.templ$)|(.+_templ\.txt$)`
|
||||
|
||||
func NewGenerate(log *slog.Logger, args Arguments) (g *Generate, err error) {
|
||||
g = &Generate{
|
||||
Log: log,
|
||||
Args: &args,
|
||||
}
|
||||
if g.Args.WorkerCount == 0 {
|
||||
g.Args.WorkerCount = runtime.NumCPU()
|
||||
}
|
||||
if g.Args.WatchPattern == "" {
|
||||
g.Args.WatchPattern = defaultWatchPattern
|
||||
}
|
||||
g.WatchPattern, err = regexp.Compile(g.Args.WatchPattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compile watch pattern %q: %w", g.Args.WatchPattern, err)
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
type Generate struct {
|
||||
Log *slog.Logger
|
||||
Args *Arguments
|
||||
WatchPattern *regexp.Regexp
|
||||
}
|
||||
|
||||
type GenerationEvent struct {
|
||||
Event fsnotify.Event
|
||||
Updated bool
|
||||
GoUpdated bool
|
||||
TextUpdated bool
|
||||
}
|
||||
|
||||
func (cmd Generate) Run(ctx context.Context) (err error) {
|
||||
if cmd.Args.NotifyProxy {
|
||||
return proxy.NotifyProxy(cmd.Args.ProxyBind, cmd.Args.ProxyPort)
|
||||
}
|
||||
if cmd.Args.Watch && cmd.Args.FileName != "" {
|
||||
return fmt.Errorf("cannot watch a single file, remove the -f or -watch flag")
|
||||
}
|
||||
writingToWriter := cmd.Args.FileWriter != nil
|
||||
if cmd.Args.FileName == "" && writingToWriter {
|
||||
return fmt.Errorf("only a single file can be output to stdout, add the -f flag to specify the file to generate code for")
|
||||
}
|
||||
// Default to writing to files.
|
||||
if cmd.Args.FileWriter == nil {
|
||||
cmd.Args.FileWriter = FileWriter
|
||||
}
|
||||
if cmd.Args.PPROFPort > 0 {
|
||||
go func() {
|
||||
_ = http.ListenAndServe(fmt.Sprintf("localhost:%d", cmd.Args.PPROFPort), nil)
|
||||
}()
|
||||
}
|
||||
|
||||
// Use absolute path.
|
||||
if !path.IsAbs(cmd.Args.Path) {
|
||||
cmd.Args.Path, err = filepath.Abs(cmd.Args.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Configure generator.
|
||||
var opts []generator.GenerateOpt
|
||||
if cmd.Args.IncludeVersion {
|
||||
opts = append(opts, generator.WithVersion(templ.Version()))
|
||||
}
|
||||
if cmd.Args.IncludeTimestamp {
|
||||
opts = append(opts, generator.WithTimestamp(time.Now()))
|
||||
}
|
||||
|
||||
// Check the version of the templ module.
|
||||
if err := modcheck.Check(cmd.Args.Path); err != nil {
|
||||
cmd.Log.Warn("templ version check: " + err.Error())
|
||||
}
|
||||
|
||||
fseh := NewFSEventHandler(
|
||||
cmd.Log,
|
||||
cmd.Args.Path,
|
||||
cmd.Args.Watch,
|
||||
opts,
|
||||
cmd.Args.GenerateSourceMapVisualisations,
|
||||
cmd.Args.KeepOrphanedFiles,
|
||||
cmd.Args.FileWriter,
|
||||
cmd.Args.Lazy,
|
||||
)
|
||||
|
||||
// If we're processing a single file, don't bother setting up the channels/multithreaing.
|
||||
if cmd.Args.FileName != "" {
|
||||
_, err = fseh.HandleEvent(ctx, fsnotify.Event{
|
||||
Name: cmd.Args.FileName,
|
||||
Op: fsnotify.Create,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Start timer.
|
||||
start := time.Now()
|
||||
|
||||
// Create channels:
|
||||
// For the initial filesystem walk and subsequent (optional) fsnotify events.
|
||||
events := make(chan fsnotify.Event)
|
||||
// Count of events currently being processed by the event handler.
|
||||
var eventsWG sync.WaitGroup
|
||||
// Used to check that the event handler has completed.
|
||||
var eventHandlerWG sync.WaitGroup
|
||||
// For errs from the watcher.
|
||||
errs := make(chan error)
|
||||
// Tracks whether errors occurred during the generation process.
|
||||
var errorCount atomic.Int64
|
||||
// For triggering actions after generation has completed.
|
||||
postGeneration := make(chan *GenerationEvent, 256)
|
||||
// Used to check that the post-generation handler has completed.
|
||||
var postGenerationWG sync.WaitGroup
|
||||
var postGenerationEventsWG sync.WaitGroup
|
||||
|
||||
// Waitgroup for the push process.
|
||||
var pushHandlerWG sync.WaitGroup
|
||||
|
||||
// Start process to push events into the channel.
|
||||
pushHandlerWG.Add(1)
|
||||
go func() {
|
||||
defer pushHandlerWG.Done()
|
||||
defer close(events)
|
||||
cmd.Log.Debug(
|
||||
"Walking directory",
|
||||
slog.String("path", cmd.Args.Path),
|
||||
slog.Bool("devMode", cmd.Args.Watch),
|
||||
)
|
||||
if err := watcher.WalkFiles(ctx, cmd.Args.Path, cmd.WatchPattern, events); err != nil {
|
||||
cmd.Log.Error("WalkFiles failed, exiting", slog.Any("error", err))
|
||||
errs <- FatalError{Err: fmt.Errorf("failed to walk files: %w", err)}
|
||||
return
|
||||
}
|
||||
if !cmd.Args.Watch {
|
||||
cmd.Log.Debug("Dev mode not enabled, process can finish early")
|
||||
return
|
||||
}
|
||||
cmd.Log.Info("Watching files")
|
||||
rw, err := watcher.Recursive(ctx, cmd.Args.Path, cmd.WatchPattern, events, errs)
|
||||
if err != nil {
|
||||
cmd.Log.Error("Recursive watcher setup failed, exiting", slog.Any("error", err))
|
||||
errs <- FatalError{Err: fmt.Errorf("failed to setup recursive watcher: %w", err)}
|
||||
return
|
||||
}
|
||||
cmd.Log.Debug("Waiting for context to be cancelled to stop watching files")
|
||||
<-ctx.Done()
|
||||
cmd.Log.Debug("Context cancelled, closing watcher")
|
||||
if err := rw.Close(); err != nil {
|
||||
cmd.Log.Error("Failed to close watcher", slog.Any("error", err))
|
||||
}
|
||||
cmd.Log.Debug("Waiting for events to be processed")
|
||||
eventsWG.Wait()
|
||||
cmd.Log.Debug(
|
||||
"All pending events processed, waiting for pending post-generation events to complete",
|
||||
)
|
||||
postGenerationEventsWG.Wait()
|
||||
cmd.Log.Debug(
|
||||
"All post-generation events processed, deleting watch mode text files",
|
||||
slog.Int64("errorCount", errorCount.Load()),
|
||||
)
|
||||
|
||||
fileEvents := make(chan fsnotify.Event)
|
||||
go func() {
|
||||
if err := watcher.WalkFiles(ctx, cmd.Args.Path, cmd.WatchPattern, fileEvents); err != nil {
|
||||
cmd.Log.Error("Post dev mode WalkFiles failed", slog.Any("error", err))
|
||||
errs <- FatalError{Err: fmt.Errorf("failed to walk files: %w", err)}
|
||||
return
|
||||
}
|
||||
close(fileEvents)
|
||||
}()
|
||||
for event := range fileEvents {
|
||||
if strings.HasSuffix(event.Name, "_templ.txt") {
|
||||
if err = os.Remove(event.Name); err != nil {
|
||||
cmd.Log.Warn("Failed to remove watch mode text file", slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Start process to handle events.
|
||||
eventHandlerWG.Add(1)
|
||||
sem := make(chan struct{}, cmd.Args.WorkerCount)
|
||||
go func() {
|
||||
defer eventHandlerWG.Done()
|
||||
defer close(postGeneration)
|
||||
cmd.Log.Debug("Starting event handler")
|
||||
for event := range events {
|
||||
eventsWG.Add(1)
|
||||
sem <- struct{}{}
|
||||
go func(event fsnotify.Event) {
|
||||
cmd.Log.Debug("Processing file", slog.String("file", event.Name))
|
||||
defer eventsWG.Done()
|
||||
defer func() { <-sem }()
|
||||
r, err := fseh.HandleEvent(ctx, event)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
}
|
||||
if !(r.GoUpdated || r.TextUpdated) {
|
||||
cmd.Log.Debug("File not updated", slog.String("file", event.Name))
|
||||
return
|
||||
}
|
||||
e := &GenerationEvent{
|
||||
Event: event,
|
||||
Updated: r.Updated,
|
||||
GoUpdated: r.GoUpdated,
|
||||
TextUpdated: r.TextUpdated,
|
||||
}
|
||||
cmd.Log.Debug("File updated", slog.String("file", event.Name))
|
||||
postGeneration <- e
|
||||
}(event)
|
||||
}
|
||||
// Wait for all events to be processed before closing.
|
||||
eventsWG.Wait()
|
||||
}()
|
||||
|
||||
// Start process to handle post-generation events.
|
||||
var updates int
|
||||
postGenerationWG.Add(1)
|
||||
var firstPostGenerationExecuted bool
|
||||
go func() {
|
||||
defer close(errs)
|
||||
defer postGenerationWG.Done()
|
||||
cmd.Log.Debug("Starting post-generation handler")
|
||||
timeout := time.NewTimer(time.Hour * 24 * 365)
|
||||
var goUpdated, textUpdated bool
|
||||
var p *proxy.Handler
|
||||
for {
|
||||
select {
|
||||
case ge := <-postGeneration:
|
||||
if ge == nil {
|
||||
cmd.Log.Debug("Post-generation event channel closed, exiting")
|
||||
return
|
||||
}
|
||||
goUpdated = goUpdated || ge.GoUpdated
|
||||
textUpdated = textUpdated || ge.TextUpdated
|
||||
if goUpdated || textUpdated {
|
||||
updates++
|
||||
}
|
||||
// Reset timer.
|
||||
if !timeout.Stop() {
|
||||
<-timeout.C
|
||||
}
|
||||
timeout.Reset(time.Millisecond * 100)
|
||||
case <-timeout.C:
|
||||
if !goUpdated && !textUpdated {
|
||||
// Nothing to process, reset timer and wait again.
|
||||
timeout.Reset(time.Hour * 24 * 365)
|
||||
break
|
||||
}
|
||||
postGenerationEventsWG.Add(1)
|
||||
if cmd.Args.Command != "" && goUpdated {
|
||||
cmd.Log.Debug("Executing command", slog.String("command", cmd.Args.Command))
|
||||
if cmd.Args.Watch {
|
||||
os.Setenv("TEMPL_DEV_MODE", "true")
|
||||
}
|
||||
if _, err := run.Run(ctx, cmd.Args.Path, cmd.Args.Command); err != nil {
|
||||
cmd.Log.Error("Error executing command", slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
if !firstPostGenerationExecuted {
|
||||
cmd.Log.Debug("First post-generation event received, starting proxy")
|
||||
firstPostGenerationExecuted = true
|
||||
p, err = cmd.StartProxy(ctx)
|
||||
if err != nil {
|
||||
cmd.Log.Error("Failed to start proxy", slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
// Send server-sent event.
|
||||
if p != nil && (textUpdated || goUpdated) {
|
||||
cmd.Log.Debug("Sending reload event")
|
||||
p.SendSSE("message", "reload")
|
||||
}
|
||||
postGenerationEventsWG.Done()
|
||||
// Reset timer.
|
||||
timeout.Reset(time.Millisecond * 100)
|
||||
textUpdated = false
|
||||
goUpdated = false
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read errors.
|
||||
for err := range errs {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, FatalError{}) {
|
||||
cmd.Log.Debug("Fatal error, exiting")
|
||||
return err
|
||||
}
|
||||
cmd.Log.Error("Error", slog.Any("error", err))
|
||||
errorCount.Add(1)
|
||||
}
|
||||
|
||||
// Wait for everything to complete.
|
||||
cmd.Log.Debug("Waiting for push handler to complete")
|
||||
pushHandlerWG.Wait()
|
||||
cmd.Log.Debug("Waiting for event handler to complete")
|
||||
eventHandlerWG.Wait()
|
||||
cmd.Log.Debug("Waiting for post-generation handler to complete")
|
||||
postGenerationWG.Wait()
|
||||
if cmd.Args.Command != "" {
|
||||
cmd.Log.Debug("Killing command", slog.String("command", cmd.Args.Command))
|
||||
if err := run.KillAll(); err != nil {
|
||||
cmd.Log.Error("Error killing command", slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Check for errors after everything has completed.
|
||||
if errorCount.Load() > 0 {
|
||||
return fmt.Errorf("generation completed with %d errors", errorCount.Load())
|
||||
}
|
||||
|
||||
cmd.Log.Info(
|
||||
"Complete",
|
||||
slog.Int("updates", updates),
|
||||
slog.Duration("duration", time.Since(start)),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmd *Generate) StartProxy(ctx context.Context) (p *proxy.Handler, err error) {
|
||||
if cmd.Args.Proxy == "" {
|
||||
cmd.Log.Debug("No proxy URL specified, not starting proxy")
|
||||
return nil, nil
|
||||
}
|
||||
var target *url.URL
|
||||
target, err = url.Parse(cmd.Args.Proxy)
|
||||
if err != nil {
|
||||
return nil, FatalError{Err: fmt.Errorf("failed to parse proxy URL: %w", err)}
|
||||
}
|
||||
if cmd.Args.ProxyPort == 0 {
|
||||
cmd.Args.ProxyPort = 7331
|
||||
}
|
||||
if cmd.Args.ProxyBind == "" {
|
||||
cmd.Args.ProxyBind = "127.0.0.1"
|
||||
}
|
||||
p = proxy.New(cmd.Log, cmd.Args.ProxyBind, cmd.Args.ProxyPort, target)
|
||||
go func() {
|
||||
cmd.Log.Info("Proxying", slog.String("from", p.URL), slog.String("to", p.Target.String()))
|
||||
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", cmd.Args.ProxyBind, cmd.Args.ProxyPort), p); err != nil {
|
||||
cmd.Log.Error("Proxy failed", slog.Any("error", err))
|
||||
}
|
||||
}()
|
||||
if !cmd.Args.OpenBrowser {
|
||||
cmd.Log.Debug("Not opening browser")
|
||||
return p, nil
|
||||
}
|
||||
go func() {
|
||||
cmd.Log.Debug("Waiting for proxy to be ready", slog.String("url", p.URL))
|
||||
backoff := backoff.NewExponentialBackOff()
|
||||
backoff.InitialInterval = time.Second
|
||||
var client http.Client
|
||||
client.Timeout = 1 * time.Second
|
||||
for {
|
||||
if _, err := client.Get(p.URL); err == nil {
|
||||
break
|
||||
}
|
||||
d := backoff.NextBackOff()
|
||||
cmd.Log.Debug(
|
||||
"Proxy not ready, retrying",
|
||||
slog.String("url", p.URL),
|
||||
slog.Any("backoff", d),
|
||||
)
|
||||
time.Sleep(d)
|
||||
}
|
||||
if err := browser.OpenURL(p.URL); err != nil {
|
||||
cmd.Log.Error("Failed to open browser", slog.Any("error", err))
|
||||
}
|
||||
}()
|
||||
return p, nil
|
||||
}
|
366
templ/cmd/templ/generatecmd/eventhandler.go
Normal file
@ -0,0 +1,366 @@
|
||||
package generatecmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/a-h/templ/cmd/templ/visualize"
|
||||
"github.com/a-h/templ/generator"
|
||||
"github.com/a-h/templ/parser/v2"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
type FileWriterFunc func(name string, contents []byte) error
|
||||
|
||||
func FileWriter(fileName string, contents []byte) error {
|
||||
return os.WriteFile(fileName, contents, 0o644)
|
||||
}
|
||||
|
||||
func WriterFileWriter(w io.Writer) FileWriterFunc {
|
||||
return func(_ string, contents []byte) error {
|
||||
_, err := w.Write(contents)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func NewFSEventHandler(
|
||||
log *slog.Logger,
|
||||
dir string,
|
||||
devMode bool,
|
||||
genOpts []generator.GenerateOpt,
|
||||
genSourceMapVis bool,
|
||||
keepOrphanedFiles bool,
|
||||
fileWriter FileWriterFunc,
|
||||
lazy bool,
|
||||
) *FSEventHandler {
|
||||
if !path.IsAbs(dir) {
|
||||
dir, _ = filepath.Abs(dir)
|
||||
}
|
||||
fseh := &FSEventHandler{
|
||||
Log: log,
|
||||
dir: dir,
|
||||
fileNameToLastModTime: make(map[string]time.Time),
|
||||
fileNameToLastModTimeMutex: &sync.Mutex{},
|
||||
fileNameToError: make(map[string]struct{}),
|
||||
fileNameToErrorMutex: &sync.Mutex{},
|
||||
fileNameToOutput: make(map[string]generator.GeneratorOutput),
|
||||
fileNameToOutputMutex: &sync.Mutex{},
|
||||
devMode: devMode,
|
||||
hashes: make(map[string][sha256.Size]byte),
|
||||
hashesMutex: &sync.Mutex{},
|
||||
genOpts: genOpts,
|
||||
genSourceMapVis: genSourceMapVis,
|
||||
keepOrphanedFiles: keepOrphanedFiles,
|
||||
writer: fileWriter,
|
||||
lazy: lazy,
|
||||
}
|
||||
return fseh
|
||||
}
|
||||
|
||||
type FSEventHandler struct {
|
||||
Log *slog.Logger
|
||||
// dir is the root directory being processed.
|
||||
dir string
|
||||
fileNameToLastModTime map[string]time.Time
|
||||
fileNameToLastModTimeMutex *sync.Mutex
|
||||
fileNameToError map[string]struct{}
|
||||
fileNameToErrorMutex *sync.Mutex
|
||||
fileNameToOutput map[string]generator.GeneratorOutput
|
||||
fileNameToOutputMutex *sync.Mutex
|
||||
devMode bool
|
||||
hashes map[string][sha256.Size]byte
|
||||
hashesMutex *sync.Mutex
|
||||
genOpts []generator.GenerateOpt
|
||||
genSourceMapVis bool
|
||||
Errors []error
|
||||
keepOrphanedFiles bool
|
||||
writer func(string, []byte) error
|
||||
lazy bool
|
||||
}
|
||||
|
||||
type GenerateResult struct {
|
||||
// Updated indicates that the file was updated.
|
||||
Updated bool
|
||||
// GoUpdated indicates that Go expressions were updated.
|
||||
GoUpdated bool
|
||||
// TextUpdated indicates that text literals were updated.
|
||||
TextUpdated bool
|
||||
}
|
||||
|
||||
func (h *FSEventHandler) HandleEvent(ctx context.Context, event fsnotify.Event) (result GenerateResult, err error) {
|
||||
// Handle _templ.go files.
|
||||
if !event.Has(fsnotify.Remove) && strings.HasSuffix(event.Name, "_templ.go") {
|
||||
_, err = os.Stat(strings.TrimSuffix(event.Name, "_templ.go") + ".templ")
|
||||
if !os.IsNotExist(err) {
|
||||
return GenerateResult{}, err
|
||||
}
|
||||
// File is orphaned.
|
||||
if h.keepOrphanedFiles {
|
||||
return GenerateResult{}, nil
|
||||
}
|
||||
h.Log.Debug("Deleting orphaned Go file", slog.String("file", event.Name))
|
||||
if err = os.Remove(event.Name); err != nil {
|
||||
h.Log.Warn("Failed to remove orphaned file", slog.Any("error", err))
|
||||
}
|
||||
return GenerateResult{Updated: true, GoUpdated: true, TextUpdated: false}, nil
|
||||
}
|
||||
// Handle _templ.txt files.
|
||||
if !event.Has(fsnotify.Remove) && strings.HasSuffix(event.Name, "_templ.txt") {
|
||||
if h.devMode {
|
||||
// Don't delete the file in dev mode, ignore changes to it, since the .templ file
|
||||
// must have been updated in order to trigger a change in the _templ.txt file.
|
||||
return GenerateResult{Updated: false, GoUpdated: false, TextUpdated: false}, nil
|
||||
}
|
||||
h.Log.Debug("Deleting watch mode file", slog.String("file", event.Name))
|
||||
if err = os.Remove(event.Name); err != nil {
|
||||
h.Log.Warn("Failed to remove watch mode text file", slog.Any("error", err))
|
||||
return GenerateResult{}, nil
|
||||
}
|
||||
return GenerateResult{}, nil
|
||||
}
|
||||
|
||||
// If the file hasn't been updated since the last time we processed it, ignore it.
|
||||
lastModTime, updatedModTime := h.UpsertLastModTime(event.Name)
|
||||
if !updatedModTime {
|
||||
h.Log.Debug("Skipping file because it wasn't updated", slog.String("file", event.Name))
|
||||
return GenerateResult{}, nil
|
||||
}
|
||||
|
||||
// Process anything that isn't a templ file.
|
||||
if !strings.HasSuffix(event.Name, ".templ") {
|
||||
// If it's a Go file, mark it as updated.
|
||||
if strings.HasSuffix(event.Name, ".go") {
|
||||
result.GoUpdated = true
|
||||
}
|
||||
result.Updated = true
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Handle templ files.
|
||||
|
||||
// If the go file is newer than the templ file, skip generation, because it's up-to-date.
|
||||
if h.lazy && goFileIsUpToDate(event.Name, lastModTime) {
|
||||
h.Log.Debug("Skipping file because the Go file is up-to-date", slog.String("file", event.Name))
|
||||
return GenerateResult{}, nil
|
||||
}
|
||||
|
||||
// Start a processor.
|
||||
start := time.Now()
|
||||
var diag []parser.Diagnostic
|
||||
result, diag, err = h.generate(ctx, event.Name)
|
||||
if err != nil {
|
||||
h.SetError(event.Name, true)
|
||||
return result, fmt.Errorf("failed to generate code for %q: %w", event.Name, err)
|
||||
}
|
||||
if len(diag) > 0 {
|
||||
for _, d := range diag {
|
||||
h.Log.Warn(d.Message,
|
||||
slog.String("from", fmt.Sprintf("%d:%d", d.Range.From.Line, d.Range.From.Col)),
|
||||
slog.String("to", fmt.Sprintf("%d:%d", d.Range.To.Line, d.Range.To.Col)),
|
||||
)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
if errorCleared, errorCount := h.SetError(event.Name, false); errorCleared {
|
||||
h.Log.Info("Error cleared", slog.String("file", event.Name), slog.Int("errors", errorCount))
|
||||
}
|
||||
h.Log.Debug("Generated code", slog.String("file", event.Name), slog.Duration("in", time.Since(start)))
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func goFileIsUpToDate(templFileName string, templFileLastMod time.Time) (upToDate bool) {
|
||||
goFileName := strings.TrimSuffix(templFileName, ".templ") + "_templ.go"
|
||||
goFileInfo, err := os.Stat(goFileName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return goFileInfo.ModTime().After(templFileLastMod)
|
||||
}
|
||||
|
||||
func (h *FSEventHandler) SetError(fileName string, hasError bool) (previouslyHadError bool, errorCount int) {
|
||||
h.fileNameToErrorMutex.Lock()
|
||||
defer h.fileNameToErrorMutex.Unlock()
|
||||
_, previouslyHadError = h.fileNameToError[fileName]
|
||||
delete(h.fileNameToError, fileName)
|
||||
if hasError {
|
||||
h.fileNameToError[fileName] = struct{}{}
|
||||
}
|
||||
return previouslyHadError, len(h.fileNameToError)
|
||||
}
|
||||
|
||||
func (h *FSEventHandler) UpsertLastModTime(fileName string) (modTime time.Time, updated bool) {
|
||||
fileInfo, err := os.Stat(fileName)
|
||||
if err != nil {
|
||||
return modTime, false
|
||||
}
|
||||
h.fileNameToLastModTimeMutex.Lock()
|
||||
defer h.fileNameToLastModTimeMutex.Unlock()
|
||||
previousModTime := h.fileNameToLastModTime[fileName]
|
||||
currentModTime := fileInfo.ModTime()
|
||||
if !currentModTime.After(previousModTime) {
|
||||
return currentModTime, false
|
||||
}
|
||||
h.fileNameToLastModTime[fileName] = currentModTime
|
||||
return currentModTime, true
|
||||
}
|
||||
|
||||
func (h *FSEventHandler) UpsertHash(fileName string, hash [sha256.Size]byte) (updated bool) {
|
||||
h.hashesMutex.Lock()
|
||||
defer h.hashesMutex.Unlock()
|
||||
lastHash := h.hashes[fileName]
|
||||
if lastHash == hash {
|
||||
return false
|
||||
}
|
||||
h.hashes[fileName] = hash
|
||||
return true
|
||||
}
|
||||
|
||||
// generate Go code for a single template.
|
||||
// If a basePath is provided, the filename included in error messages is relative to it.
|
||||
func (h *FSEventHandler) generate(ctx context.Context, fileName string) (result GenerateResult, diagnostics []parser.Diagnostic, err error) {
|
||||
t, err := parser.Parse(fileName)
|
||||
if err != nil {
|
||||
return GenerateResult{}, nil, fmt.Errorf("%s parsing error: %w", fileName, err)
|
||||
}
|
||||
targetFileName := strings.TrimSuffix(fileName, ".templ") + "_templ.go"
|
||||
|
||||
// Only use relative filenames to the basepath for filenames in runtime error messages.
|
||||
absFilePath, err := filepath.Abs(fileName)
|
||||
if err != nil {
|
||||
return GenerateResult{}, nil, fmt.Errorf("failed to get absolute path for %q: %w", fileName, err)
|
||||
}
|
||||
relFilePath, err := filepath.Rel(h.dir, absFilePath)
|
||||
if err != nil {
|
||||
return GenerateResult{}, nil, fmt.Errorf("failed to get relative path for %q: %w", fileName, err)
|
||||
}
|
||||
// Convert Windows file paths to Unix-style for consistency.
|
||||
relFilePath = filepath.ToSlash(relFilePath)
|
||||
|
||||
var b bytes.Buffer
|
||||
generatorOutput, err := generator.Generate(t, &b, append(h.genOpts, generator.WithFileName(relFilePath))...)
|
||||
if err != nil {
|
||||
return GenerateResult{}, nil, fmt.Errorf("%s generation error: %w", fileName, err)
|
||||
}
|
||||
|
||||
formattedGoCode, err := format.Source(b.Bytes())
|
||||
if err != nil {
|
||||
err = remapErrorList(err, generatorOutput.SourceMap, fileName)
|
||||
return GenerateResult{}, nil, fmt.Errorf("%s source formatting error %w", fileName, err)
|
||||
}
|
||||
|
||||
// Hash output, and write out the file if the goCodeHash has changed.
|
||||
goCodeHash := sha256.Sum256(formattedGoCode)
|
||||
if h.UpsertHash(targetFileName, goCodeHash) {
|
||||
result.Updated = true
|
||||
if err = h.writer(targetFileName, formattedGoCode); err != nil {
|
||||
return result, nil, fmt.Errorf("failed to write target file %q: %w", targetFileName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the txt file if it has changed.
|
||||
if h.devMode {
|
||||
txtFileName := strings.TrimSuffix(fileName, ".templ") + "_templ.txt"
|
||||
joined := strings.Join(generatorOutput.Literals, "\n")
|
||||
txtHash := sha256.Sum256([]byte(joined))
|
||||
if h.UpsertHash(txtFileName, txtHash) {
|
||||
result.TextUpdated = true
|
||||
if err = os.WriteFile(txtFileName, []byte(joined), 0o644); err != nil {
|
||||
return result, nil, fmt.Errorf("failed to write string literal file %q: %w", txtFileName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the change would require a recompilation to take effect.
|
||||
h.fileNameToOutputMutex.Lock()
|
||||
defer h.fileNameToOutputMutex.Unlock()
|
||||
previous := h.fileNameToOutput[fileName]
|
||||
if generator.HasChanged(previous, generatorOutput) {
|
||||
result.GoUpdated = true
|
||||
}
|
||||
h.fileNameToOutput[fileName] = generatorOutput
|
||||
}
|
||||
|
||||
parsedDiagnostics, err := parser.Diagnose(t)
|
||||
if err != nil {
|
||||
return result, nil, fmt.Errorf("%s diagnostics error: %w", fileName, err)
|
||||
}
|
||||
|
||||
if h.genSourceMapVis {
|
||||
err = generateSourceMapVisualisation(ctx, fileName, targetFileName, generatorOutput.SourceMap)
|
||||
}
|
||||
|
||||
return result, parsedDiagnostics, err
|
||||
}
|
||||
|
||||
// Takes an error from the formatter and attempts to convert the positions reported in the target file to their positions
|
||||
// in the source file.
|
||||
func remapErrorList(err error, sourceMap *parser.SourceMap, fileName string) error {
|
||||
list, ok := err.(scanner.ErrorList)
|
||||
if !ok || len(list) == 0 {
|
||||
return err
|
||||
}
|
||||
for i, e := range list {
|
||||
// The positions in the source map are off by one line because of the package definition.
|
||||
srcPos, ok := sourceMap.SourcePositionFromTarget(uint32(e.Pos.Line-1), uint32(e.Pos.Column))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
list[i].Pos = token.Position{
|
||||
Filename: fileName,
|
||||
Offset: int(srcPos.Index),
|
||||
Line: int(srcPos.Line) + 1,
|
||||
Column: int(srcPos.Col),
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func generateSourceMapVisualisation(ctx context.Context, templFileName, goFileName string, sourceMap *parser.SourceMap) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
var templContents, goContents []byte
|
||||
var templErr, goErr error
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
templContents, templErr = os.ReadFile(templFileName)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
goContents, goErr = os.ReadFile(goFileName)
|
||||
}()
|
||||
wg.Wait()
|
||||
if templErr != nil {
|
||||
return templErr
|
||||
}
|
||||
if goErr != nil {
|
||||
return templErr
|
||||
}
|
||||
|
||||
targetFileName := strings.TrimSuffix(templFileName, ".templ") + "_templ_sourcemap.html"
|
||||
w, err := os.Create(targetFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s sourcemap visualisation error: %w", templFileName, err)
|
||||
}
|
||||
defer w.Close()
|
||||
b := bufio.NewWriter(w)
|
||||
defer b.Flush()
|
||||
|
||||
return visualize.HTML(templFileName, string(templContents), string(goContents), sourceMap).Render(ctx, b)
|
||||
}
|
23
templ/cmd/templ/generatecmd/fatalerror.go
Normal file
@ -0,0 +1,23 @@
|
||||
package generatecmd
|
||||
|
||||
type FatalError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e FatalError) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
func (e FatalError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
func (e FatalError) Is(target error) bool {
|
||||
_, ok := target.(FatalError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (e FatalError) As(target any) bool {
|
||||
_, ok := target.(*FatalError)
|
||||
return ok
|
||||
}
|
39
templ/cmd/templ/generatecmd/main.go
Normal file
@ -0,0 +1,39 @@
|
||||
package generatecmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"log/slog"
|
||||
|
||||
_ "net/http/pprof"
|
||||
)
|
||||
|
||||
type Arguments struct {
|
||||
FileName string
|
||||
FileWriter FileWriterFunc
|
||||
Path string
|
||||
Watch bool
|
||||
WatchPattern string
|
||||
OpenBrowser bool
|
||||
Command string
|
||||
ProxyBind string
|
||||
ProxyPort int
|
||||
Proxy string
|
||||
NotifyProxy bool
|
||||
WorkerCount int
|
||||
GenerateSourceMapVisualisations bool
|
||||
IncludeVersion bool
|
||||
IncludeTimestamp bool
|
||||
// PPROFPort is the port to run the pprof server on.
|
||||
PPROFPort int
|
||||
KeepOrphanedFiles bool
|
||||
Lazy bool
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, log *slog.Logger, args Arguments) (err error) {
|
||||
g, err := NewGenerate(log, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return g.Run(ctx)
|
||||
}
|
170
templ/cmd/templ/generatecmd/main_test.go
Normal file
@ -0,0 +1,170 @@
|
||||
package generatecmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/a-h/templ/cmd/templ/testproject"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
t.Run("can generate a file in place", func(t *testing.T) {
|
||||
// templ generate -f templates.templ
|
||||
dir, err := testproject.Create("github.com/a-h/templ/cmd/templ/testproject")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test project: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// Delete the templates_templ.go file to ensure it is generated.
|
||||
err = os.Remove(path.Join(dir, "templates_templ.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to remove templates_templ.go: %v", err)
|
||||
}
|
||||
|
||||
// Run the generate command.
|
||||
err = Run(context.Background(), log, Arguments{
|
||||
FileName: path.Join(dir, "templates.templ"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run generate command: %v", err)
|
||||
}
|
||||
|
||||
// Check the templates_templ.go file was created.
|
||||
_, err = os.Stat(path.Join(dir, "templates_templ.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("templates_templ.go was not created: %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("can generate a file in watch mode", func(t *testing.T) {
|
||||
// templ generate -f templates.templ
|
||||
dir, err := testproject.Create("github.com/a-h/templ/cmd/templ/testproject")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test project: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// Delete the templates_templ.go file to ensure it is generated.
|
||||
err = os.Remove(path.Join(dir, "templates_templ.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to remove templates_templ.go: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var eg errgroup.Group
|
||||
eg.Go(func() error {
|
||||
// Run the generate command.
|
||||
return Run(ctx, log, Arguments{
|
||||
Path: dir,
|
||||
Watch: true,
|
||||
})
|
||||
})
|
||||
|
||||
// Check the templates_templ.go file was created, with backoff.
|
||||
for i := 0; i < 5; i++ {
|
||||
time.Sleep(time.Second * time.Duration(i))
|
||||
_, err = os.Stat(path.Join(dir, "templates_templ.go"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, err = os.Stat(path.Join(dir, "templates_templ.txt"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("template files were not created: %v", err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
if err := eg.Wait(); err != nil {
|
||||
t.Fatalf("generate command failed: %v", err)
|
||||
}
|
||||
|
||||
// Check the templates_templ.txt file was removed.
|
||||
_, err = os.Stat(path.Join(dir, "templates_templ.txt"))
|
||||
if err == nil {
|
||||
t.Fatalf("templates_templ.txt was not removed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefaultWatchPattern(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
matches bool
|
||||
}{
|
||||
{
|
||||
name: "empty file names do not match",
|
||||
input: "",
|
||||
matches: false,
|
||||
},
|
||||
{
|
||||
name: "*_templ.txt matches, Windows",
|
||||
input: `C:\Users\adrian\github.com\a-h\templ\cmd\templ\testproject\strings_templ.txt`,
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*_templ.txt matches, Unix",
|
||||
input: "/Users/adrian/github.com/a-h/templ/cmd/templ/testproject/strings_templ.txt",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*.templ files match, Windows",
|
||||
input: `C:\Users\adrian\github.com\a-h\templ\cmd\templ\testproject\templates.templ`,
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*.templ files match, Unix",
|
||||
input: "/Users/adrian/github.com/a-h/templ/cmd/templ/testproject/templates.templ",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*_templ.go files match, Windows",
|
||||
input: `C:\Users\adrian\github.com\a-h\templ\cmd\templ\testproject\templates_templ.go`,
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*_templ.go files match, Unix",
|
||||
input: "/Users/adrian/github.com/a-h/templ/cmd/templ/testproject/templates_templ.go",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*.go files match, Windows",
|
||||
input: `C:\Users\adrian\github.com\a-h\templ\cmd\templ\testproject\templates.go`,
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*.go files match, Unix",
|
||||
input: "/Users/adrian/github.com/a-h/templ/cmd/templ/testproject/templates.go",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "*.css files do not match",
|
||||
input: "/Users/adrian/github.com/a-h/templ/cmd/templ/testproject/templates.css",
|
||||
matches: false,
|
||||
},
|
||||
}
|
||||
wpRegexp, err := regexp.Compile(defaultWatchPattern)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile default watch pattern: %v", err)
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if wpRegexp.MatchString(test.input) != test.matches {
|
||||
t.Fatalf("expected match of %q to be %v", test.input, test.matches)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
82
templ/cmd/templ/generatecmd/modcheck/modcheck.go
Normal file
@ -0,0 +1,82 @@
|
||||
package modcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"golang.org/x/mod/modfile"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// WalkUp the directory tree, starting at dir, until we find a directory containing
|
||||
// a go.mod file.
|
||||
func WalkUp(dir string) (string, error) {
|
||||
dir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
var modFile string
|
||||
for {
|
||||
modFile = filepath.Join(dir, "go.mod")
|
||||
_, err := os.Stat(modFile)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("failed to stat go.mod file: %w", err)
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
// Move up.
|
||||
prev := dir
|
||||
dir = filepath.Dir(dir)
|
||||
if dir == prev {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// No file found.
|
||||
if modFile == "" {
|
||||
return dir, fmt.Errorf("could not find go.mod file")
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
func Check(dir string) error {
|
||||
dir, err := WalkUp(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Found a go.mod file.
|
||||
// Read it and find the templ version.
|
||||
modFile := filepath.Join(dir, "go.mod")
|
||||
m, err := os.ReadFile(modFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read go.mod file: %w", err)
|
||||
}
|
||||
|
||||
mf, err := modfile.Parse(modFile, m, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse go.mod file: %w", err)
|
||||
}
|
||||
if mf.Module.Mod.Path == "github.com/a-h/templ" {
|
||||
// The go.mod file is for templ itself.
|
||||
return nil
|
||||
}
|
||||
for _, r := range mf.Require {
|
||||
if r.Mod.Path == "github.com/a-h/templ" {
|
||||
cmp := semver.Compare(r.Mod.Version, templ.Version())
|
||||
if cmp < 0 {
|
||||
return fmt.Errorf("generator %v is newer than templ version %v found in go.mod file, consider running `go get -u github.com/a-h/templ` to upgrade", templ.Version(), r.Mod.Version)
|
||||
}
|
||||
if cmp > 0 {
|
||||
return fmt.Errorf("generator %v is older than templ version %v found in go.mod file, consider upgrading templ CLI", templ.Version(), r.Mod.Version)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("templ not found in go.mod file, run `go get github.com/a-h/templ` to install it")
|
||||
}
|
47
templ/cmd/templ/generatecmd/modcheck/modcheck_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
package modcheck
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/mod/modfile"
|
||||
)
|
||||
|
||||
func TestPatchGoVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
input: "go 1.20",
|
||||
expected: "1.20",
|
||||
},
|
||||
{
|
||||
input: "go 1.20.123",
|
||||
expected: "1.20.123",
|
||||
},
|
||||
{
|
||||
input: "go 1.20.1",
|
||||
expected: "1.20.1",
|
||||
},
|
||||
{
|
||||
input: "go 1.20rc1",
|
||||
expected: "1.20rc1",
|
||||
},
|
||||
{
|
||||
input: "go 1.15",
|
||||
expected: "1.15",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
input := "module github.com/a-h/templ\n\n" + string(test.input) + "\n" + "toolchain go1.27.9\n"
|
||||
mf, err := modfile.Parse("go.mod", []byte(input), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse go.mod: %v", err)
|
||||
}
|
||||
if test.expected != mf.Go.Version {
|
||||
t.Errorf("expected %q, got %q", test.expected, mf.Go.Version)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
284
templ/cmd/templ/generatecmd/proxy/proxy.go
Normal file
@ -0,0 +1,284 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
stdlog "log"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/a-h/templ/cmd/templ/generatecmd/sse"
|
||||
"github.com/andybalholm/brotli"
|
||||
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed script.js
|
||||
var script string
|
||||
|
||||
type Handler struct {
|
||||
log *slog.Logger
|
||||
URL string
|
||||
Target *url.URL
|
||||
p *httputil.ReverseProxy
|
||||
sse *sse.Handler
|
||||
}
|
||||
|
||||
func getScriptTag(nonce string) string {
|
||||
if nonce != "" {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`<script src="/_templ/reload/script.js" nonce="`)
|
||||
sb.WriteString(html.EscapeString(nonce))
|
||||
sb.WriteString(`"></script>`)
|
||||
return sb.String()
|
||||
}
|
||||
return `<script src="/_templ/reload/script.js"></script>`
|
||||
}
|
||||
|
||||
func insertScriptTagIntoBody(nonce, body string) (updated string) {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(body))
|
||||
if err != nil {
|
||||
return strings.Replace(body, "</body>", getScriptTag(nonce)+"</body>", -1)
|
||||
}
|
||||
doc.Find("body").AppendHtml(getScriptTag(nonce))
|
||||
r, err := doc.Html()
|
||||
if err != nil {
|
||||
return strings.Replace(body, "</body>", getScriptTag(nonce)+"</body>", -1)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type passthroughWriteCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (pwc passthroughWriteCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const unsupportedContentEncoding = "Unsupported content encoding, hot reload script not inserted."
|
||||
|
||||
func (h *Handler) modifyResponse(r *http.Response) error {
|
||||
log := h.log.With(slog.String("url", r.Request.URL.String()))
|
||||
if r.Header.Get("templ-skip-modify") == "true" {
|
||||
log.Debug("Skipping response modification because templ-skip-modify header is set")
|
||||
return nil
|
||||
}
|
||||
if contentType := r.Header.Get("Content-Type"); !strings.HasPrefix(contentType, "text/html") {
|
||||
log.Debug("Skipping response modification because content type is not text/html", slog.String("content-type", contentType))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set up readers and writers.
|
||||
newReader := func(in io.Reader) (out io.Reader, err error) {
|
||||
return in, nil
|
||||
}
|
||||
newWriter := func(out io.Writer) io.WriteCloser {
|
||||
return passthroughWriteCloser{out}
|
||||
}
|
||||
switch r.Header.Get("Content-Encoding") {
|
||||
case "gzip":
|
||||
newReader = func(in io.Reader) (out io.Reader, err error) {
|
||||
return gzip.NewReader(in)
|
||||
}
|
||||
newWriter = func(out io.Writer) io.WriteCloser {
|
||||
return gzip.NewWriter(out)
|
||||
}
|
||||
case "br":
|
||||
newReader = func(in io.Reader) (out io.Reader, err error) {
|
||||
return brotli.NewReader(in), nil
|
||||
}
|
||||
newWriter = func(out io.Writer) io.WriteCloser {
|
||||
return brotli.NewWriter(out)
|
||||
}
|
||||
case "":
|
||||
log.Debug("No content encoding header found")
|
||||
default:
|
||||
h.log.Warn(unsupportedContentEncoding, slog.String("encoding", r.Header.Get("Content-Encoding")))
|
||||
}
|
||||
|
||||
// Read the encoded body.
|
||||
encr, err := newReader(r.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
body, err := io.ReadAll(encr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update it.
|
||||
csp := r.Header.Get("Content-Security-Policy")
|
||||
updated := insertScriptTagIntoBody(parseNonce(csp), string(body))
|
||||
if log.Enabled(r.Request.Context(), slog.LevelDebug) {
|
||||
if len(updated) == len(body) {
|
||||
log.Debug("Reload script not inserted")
|
||||
} else {
|
||||
log.Debug("Reload script inserted")
|
||||
}
|
||||
}
|
||||
|
||||
// Encode the response.
|
||||
var buf bytes.Buffer
|
||||
encw := newWriter(&buf)
|
||||
_, err = encw.Write([]byte(updated))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = encw.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the response.
|
||||
r.Body = io.NopCloser(&buf)
|
||||
r.ContentLength = int64(buf.Len())
|
||||
r.Header.Set("Content-Length", strconv.Itoa(buf.Len()))
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseNonce(csp string) (nonce string) {
|
||||
outer:
|
||||
for _, rawDirective := range strings.Split(csp, ";") {
|
||||
parts := strings.Fields(rawDirective)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
if parts[0] != "script-src" {
|
||||
continue
|
||||
}
|
||||
for _, source := range parts[1:] {
|
||||
source = strings.TrimPrefix(source, "'")
|
||||
source = strings.TrimSuffix(source, "'")
|
||||
if strings.HasPrefix(source, "nonce-") {
|
||||
nonce = source[6:]
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
return nonce
|
||||
}
|
||||
|
||||
func New(log *slog.Logger, bind string, port int, target *url.URL) (h *Handler) {
|
||||
p := httputil.NewSingleHostReverseProxy(target)
|
||||
p.ErrorLog = stdlog.New(os.Stderr, "Proxy to target error: ", 0)
|
||||
p.Transport = &roundTripper{
|
||||
maxRetries: 20,
|
||||
initialDelay: 100 * time.Millisecond,
|
||||
backoffExponent: 1.5,
|
||||
}
|
||||
h = &Handler{
|
||||
log: log,
|
||||
URL: fmt.Sprintf("http://%s:%d", bind, port),
|
||||
Target: target,
|
||||
p: p,
|
||||
sse: sse.New(),
|
||||
}
|
||||
p.ModifyResponse = h.modifyResponse
|
||||
return h
|
||||
}
|
||||
|
||||
func (p *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/_templ/reload/script.js" {
|
||||
// Provides a script that reloads the page.
|
||||
w.Header().Add("Content-Type", "text/javascript")
|
||||
_, err := io.WriteString(w, script)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to write script: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/_templ/reload/events" {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
// Provides a list of messages including a reload message.
|
||||
p.sse.ServeHTTP(w, r)
|
||||
return
|
||||
case http.MethodPost:
|
||||
// Send a reload message to all connected clients.
|
||||
p.sse.Send("message", "reload")
|
||||
return
|
||||
}
|
||||
http.Error(w, "only GET or POST method allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
p.p.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (p *Handler) SendSSE(eventType string, data string) {
|
||||
p.sse.Send(eventType, data)
|
||||
}
|
||||
|
||||
type roundTripper struct {
|
||||
maxRetries int
|
||||
initialDelay time.Duration
|
||||
backoffExponent float64
|
||||
}
|
||||
|
||||
func (rt *roundTripper) setShouldSkipResponseModificationHeader(r *http.Request, resp *http.Response) {
|
||||
// Instruct the modifyResponse function to skip modifying the response if the
|
||||
// HTTP request has come from HTMX.
|
||||
if r.Header.Get("HX-Request") != "true" {
|
||||
return
|
||||
}
|
||||
resp.Header.Set("templ-skip-modify", "true")
|
||||
}
|
||||
|
||||
func (rt *roundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
// Read and buffer the body.
|
||||
var bodyBytes []byte
|
||||
if r.Body != nil && r.Body != http.NoBody {
|
||||
var err error
|
||||
bodyBytes, err = io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Body.Close()
|
||||
}
|
||||
|
||||
// Retry logic.
|
||||
var resp *http.Response
|
||||
var err error
|
||||
for retries := 0; retries < rt.maxRetries; retries++ {
|
||||
// Clone the request and set the body.
|
||||
req := r.Clone(r.Context())
|
||||
if bodyBytes != nil {
|
||||
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||
}
|
||||
|
||||
// Execute the request.
|
||||
resp, err = http.DefaultTransport.RoundTrip(req)
|
||||
if err != nil {
|
||||
time.Sleep(rt.initialDelay * time.Duration(math.Pow(rt.backoffExponent, float64(retries))))
|
||||
continue
|
||||
}
|
||||
|
||||
rt.setShouldSkipResponseModificationHeader(r, resp)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("max retries reached: %q", r.URL.String())
|
||||
}
|
||||
|
||||
func NotifyProxy(host string, port int) error {
|
||||
urlStr := fmt.Sprintf("http://%s:%d/_templ/reload/events", host, port)
|
||||
req, err := http.NewRequest(http.MethodPost, urlStr, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = http.DefaultClient.Do(req)
|
||||
return err
|
||||
}
|
627
templ/cmd/templ/generatecmd/proxy/proxy_test.go
Normal file
@ -0,0 +1,627 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestRoundTripper(t *testing.T) {
|
||||
t.Run("if the HX-Request header is present, set the templ-skip-modify header on the response", func(t *testing.T) {
|
||||
rt := &roundTripper{}
|
||||
req, err := http.NewRequest("GET", "http://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating request: %v", err)
|
||||
}
|
||||
req.Header.Set("HX-Request", "true")
|
||||
resp := &http.Response{Header: make(http.Header)}
|
||||
rt.setShouldSkipResponseModificationHeader(req, resp)
|
||||
if resp.Header.Get("templ-skip-modify") != "true" {
|
||||
t.Errorf("expected templ-skip-modify header to be true, got %v", resp.Header.Get("templ-skip-modify"))
|
||||
}
|
||||
})
|
||||
t.Run("if the HX-Request header is not present, do not set the templ-skip-modify header on the response", func(t *testing.T) {
|
||||
rt := &roundTripper{}
|
||||
req, err := http.NewRequest("GET", "http://example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating request: %v", err)
|
||||
}
|
||||
resp := &http.Response{Header: make(http.Header)}
|
||||
rt.setShouldSkipResponseModificationHeader(req, resp)
|
||||
if resp.Header.Get("templ-skip-modify") != "" {
|
||||
t.Errorf("expected templ-skip-modify header to be empty, got %v", resp.Header.Get("templ-skip-modify"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestProxy(t *testing.T) {
|
||||
t.Run("plain: non-html content is not modified", func(t *testing.T) {
|
||||
// Arrange
|
||||
r := &http.Response{
|
||||
Body: io.NopCloser(strings.NewReader(`{"key": "value"}`)),
|
||||
Header: make(http.Header),
|
||||
Request: &http.Request{
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
r.Header.Set("Content-Length", "16")
|
||||
|
||||
// Act
|
||||
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
|
||||
err := h.modifyResponse(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Assert
|
||||
if r.Header.Get("Content-Length") != "16" {
|
||||
t.Errorf("expected content length to be 16, got %v", r.Header.Get("Content-Length"))
|
||||
}
|
||||
actualBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading response: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(`{"key": "value"}`, string(actualBody)); diff != "" {
|
||||
t.Errorf("unexpected response body (-got +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
t.Run("plain: if the response contains templ-skip-modify header, it is not modified", func(t *testing.T) {
|
||||
// Arrange
|
||||
r := &http.Response{
|
||||
Body: io.NopCloser(strings.NewReader(`Hello`)),
|
||||
Header: make(http.Header),
|
||||
Request: &http.Request{
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
r.Header.Set("Content-Type", "text/html")
|
||||
r.Header.Set("Content-Length", "5")
|
||||
r.Header.Set("templ-skip-modify", "true")
|
||||
|
||||
// Act
|
||||
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
|
||||
err := h.modifyResponse(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Assert
|
||||
if r.Header.Get("Content-Length") != "5" {
|
||||
t.Errorf("expected content length to be 5, got %v", r.Header.Get("Content-Length"))
|
||||
}
|
||||
actualBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading response: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(`Hello`, string(actualBody)); diff != "" {
|
||||
t.Errorf("unexpected response body (-got +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
t.Run("plain: body tags get the script inserted", func(t *testing.T) {
|
||||
// Arrange
|
||||
r := &http.Response{
|
||||
Body: io.NopCloser(strings.NewReader(`<html><body></body></html>`)),
|
||||
Header: make(http.Header),
|
||||
Request: &http.Request{
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
r.Header.Set("Content-Type", "text/html, charset=utf-8")
|
||||
r.Header.Set("Content-Length", "26")
|
||||
|
||||
expectedString := insertScriptTagIntoBody("", `<html><body></body></html>`)
|
||||
if !strings.Contains(expectedString, getScriptTag("")) {
|
||||
t.Fatalf("expected the script tag to be inserted, but it wasn't: %q", expectedString)
|
||||
}
|
||||
|
||||
// Act
|
||||
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
|
||||
err := h.modifyResponse(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Assert
|
||||
if r.Header.Get("Content-Length") != fmt.Sprintf("%d", len(expectedString)) {
|
||||
t.Errorf("expected content length to be %d, got %v", len(expectedString), r.Header.Get("Content-Length"))
|
||||
}
|
||||
actualBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading response: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(expectedString, string(actualBody)); diff != "" {
|
||||
t.Errorf("unexpected response body (-got +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
t.Run("plain: body tags get the script inserted with nonce", func(t *testing.T) {
|
||||
// Arrange
|
||||
r := &http.Response{
|
||||
Body: io.NopCloser(strings.NewReader(`<html><body></body></html>`)),
|
||||
Header: make(http.Header),
|
||||
Request: &http.Request{
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
r.Header.Set("Content-Type", "text/html, charset=utf-8")
|
||||
r.Header.Set("Content-Length", "26")
|
||||
const nonce = "this-is-the-nonce"
|
||||
r.Header.Set("Content-Security-Policy", fmt.Sprintf("script-src 'nonce-%s'", nonce))
|
||||
|
||||
expectedString := insertScriptTagIntoBody(nonce, `<html><body></body></html>`)
|
||||
if !strings.Contains(expectedString, getScriptTag(nonce)) {
|
||||
t.Fatalf("expected the script tag to be inserted, but it wasn't: %q", expectedString)
|
||||
}
|
||||
|
||||
// Act
|
||||
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
|
||||
err := h.modifyResponse(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Assert
|
||||
if r.Header.Get("Content-Length") != fmt.Sprintf("%d", len(expectedString)) {
|
||||
t.Errorf("expected content length to be %d, got %v", len(expectedString), r.Header.Get("Content-Length"))
|
||||
}
|
||||
actualBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading response: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(expectedString, string(actualBody)); diff != "" {
|
||||
t.Errorf("unexpected response body (-got +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
t.Run("plain: body tags get the script inserted ignoring js with body tags", func(t *testing.T) {
|
||||
// Arrange
|
||||
r := &http.Response{
|
||||
Body: io.NopCloser(strings.NewReader(`<html><body><script>console.log("<body></body>")</script></body></html>`)),
|
||||
Header: make(http.Header),
|
||||
Request: &http.Request{
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
r.Header.Set("Content-Type", "text/html, charset=utf-8")
|
||||
r.Header.Set("Content-Length", "26")
|
||||
|
||||
expectedString := insertScriptTagIntoBody("", `<html><body><script>console.log("<body></body>")</script></body></html>`)
|
||||
if !strings.Contains(expectedString, getScriptTag("")) {
|
||||
t.Fatalf("expected the script tag to be inserted, but it wasn't: %q", expectedString)
|
||||
}
|
||||
if !strings.Contains(expectedString, `console.log("<body></body>")`) {
|
||||
t.Fatalf("expected the script tag to be inserted, but mangled the html: %q", expectedString)
|
||||
}
|
||||
|
||||
// Act
|
||||
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
|
||||
err := h.modifyResponse(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Assert
|
||||
if r.Header.Get("Content-Length") != fmt.Sprintf("%d", len(expectedString)) {
|
||||
t.Errorf("expected content length to be %d, got %v", len(expectedString), r.Header.Get("Content-Length"))
|
||||
}
|
||||
actualBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading response: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(expectedString, string(actualBody)); diff != "" {
|
||||
t.Errorf("unexpected response body (-got +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
t.Run("gzip: non-html content is not modified", func(t *testing.T) {
|
||||
// Arrange
|
||||
r := &http.Response{
|
||||
Body: io.NopCloser(strings.NewReader(`{"key": "value"}`)),
|
||||
Header: make(http.Header),
|
||||
Request: &http.Request{
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
// It's not actually gzipped here, but it doesn't matter, it shouldn't get that far.
|
||||
r.Header.Set("Content-Encoding", "gzip")
|
||||
// Similarly, this is not the actual length of the gzipped content.
|
||||
r.Header.Set("Content-Length", "16")
|
||||
|
||||
// Act
|
||||
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
|
||||
err := h.modifyResponse(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Assert
|
||||
if r.Header.Get("Content-Length") != "16" {
|
||||
t.Errorf("expected content length to be 16, got %v", r.Header.Get("Content-Length"))
|
||||
}
|
||||
actualBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading response: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(`{"key": "value"}`, string(actualBody)); diff != "" {
|
||||
t.Errorf("unexpected response body (-got +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
t.Run("gzip: body tags get the script inserted", func(t *testing.T) {
|
||||
// Arrange
|
||||
body := `<html><body></body></html>`
|
||||
var buf bytes.Buffer
|
||||
gzw := gzip.NewWriter(&buf)
|
||||
_, err := gzw.Write([]byte(body))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error writing gzip: %v", err)
|
||||
}
|
||||
gzw.Close()
|
||||
|
||||
expectedString := insertScriptTagIntoBody("", body)
|
||||
|
||||
var expectedBytes bytes.Buffer
|
||||
gzw = gzip.NewWriter(&expectedBytes)
|
||||
_, err = gzw.Write([]byte(expectedString))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error writing gzip: %v", err)
|
||||
}
|
||||
gzw.Close()
|
||||
expectedLength := len(expectedBytes.Bytes())
|
||||
|
||||
r := &http.Response{
|
||||
Body: io.NopCloser(&buf),
|
||||
Header: make(http.Header),
|
||||
Request: &http.Request{
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
r.Header.Set("Content-Type", "text/html, charset=utf-8")
|
||||
r.Header.Set("Content-Encoding", "gzip")
|
||||
r.Header.Set("Content-Length", fmt.Sprintf("%d", expectedLength))
|
||||
|
||||
// Act
|
||||
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
|
||||
err = h.modifyResponse(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Assert
|
||||
if r.Header.Get("Content-Length") != fmt.Sprintf("%d", expectedLength) {
|
||||
t.Errorf("expected content length to be %d, got %v", expectedLength, r.Header.Get("Content-Length"))
|
||||
}
|
||||
|
||||
gr, err := gzip.NewReader(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading response: %v", err)
|
||||
}
|
||||
actualBody, err := io.ReadAll(gr)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading response: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(expectedString, string(actualBody)); diff != "" {
|
||||
t.Errorf("unexpected response body (-got +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
t.Run("brotli: body tags get the script inserted", func(t *testing.T) {
|
||||
// Arrange
|
||||
body := `<html><body></body></html>`
|
||||
var buf bytes.Buffer
|
||||
brw := brotli.NewWriter(&buf)
|
||||
_, err := brw.Write([]byte(body))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error writing gzip: %v", err)
|
||||
}
|
||||
brw.Close()
|
||||
|
||||
expectedString := insertScriptTagIntoBody("", body)
|
||||
|
||||
var expectedBytes bytes.Buffer
|
||||
brw = brotli.NewWriter(&expectedBytes)
|
||||
_, err = brw.Write([]byte(expectedString))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error writing gzip: %v", err)
|
||||
}
|
||||
brw.Close()
|
||||
expectedLength := len(expectedBytes.Bytes())
|
||||
|
||||
r := &http.Response{
|
||||
Body: io.NopCloser(&buf),
|
||||
Header: make(http.Header),
|
||||
Request: &http.Request{
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
r.Header.Set("Content-Type", "text/html, charset=utf-8")
|
||||
r.Header.Set("Content-Encoding", "br")
|
||||
r.Header.Set("Content-Length", fmt.Sprintf("%d", expectedLength))
|
||||
|
||||
// Act
|
||||
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
|
||||
err = h.modifyResponse(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Assert
|
||||
if r.Header.Get("Content-Length") != fmt.Sprintf("%d", expectedLength) {
|
||||
t.Errorf("expected content length to be %d, got %v", expectedLength, r.Header.Get("Content-Length"))
|
||||
}
|
||||
|
||||
actualBody, err := io.ReadAll(brotli.NewReader(r.Body))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading response: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(expectedString, string(actualBody)); diff != "" {
|
||||
t.Errorf("unexpected response body (-got +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
t.Run("notify-proxy: sending POST request to /_templ/reload/events should receive reload sse event", func(t *testing.T) {
|
||||
// Arrange 1: create a test proxy server.
|
||||
dummyHandler := func(w http.ResponseWriter, r *http.Request) {}
|
||||
dummyServer := httptest.NewServer(http.HandlerFunc(dummyHandler))
|
||||
defer dummyServer.Close()
|
||||
|
||||
u, err := url.Parse(dummyServer.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error parsing URL: %v", err)
|
||||
}
|
||||
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
|
||||
handler := New(log, "0.0.0.0", 0, u)
|
||||
proxyServer := httptest.NewServer(handler)
|
||||
defer proxyServer.Close()
|
||||
|
||||
u2, err := url.Parse(proxyServer.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error parsing URL: %v", err)
|
||||
}
|
||||
port, err := strconv.Atoi(u2.Port())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error parsing port: %v", err)
|
||||
}
|
||||
|
||||
// Arrange 2: start a goroutine to listen for sse events.
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
errChan := make(chan error)
|
||||
sseRespCh := make(chan string)
|
||||
sseListening := make(chan bool) // Coordination channel that ensures the SSE listener is started before notifying the proxy.
|
||||
go func() {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/_templ/reload/events", proxyServer.URL), nil)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
sseListening <- true
|
||||
lines := []string{}
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
if scanner.Text() == "data: reload" {
|
||||
sseRespCh <- strings.Join(lines, "\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
err = scanner.Err()
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// Act: notify the proxy.
|
||||
select { // Either SSE is listening or an error occurred.
|
||||
case <-sseListening:
|
||||
err = NotifyProxy(u2.Hostname(), port)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error notifying proxy: %v", err)
|
||||
}
|
||||
case err := <-errChan:
|
||||
if err == nil {
|
||||
t.Fatalf("unexpected sse response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Assert.
|
||||
select { // Either SSE has a expected response or an error or timeout occurred.
|
||||
case resp := <-sseRespCh:
|
||||
if !strings.Contains(resp, "event: message\ndata: reload") {
|
||||
t.Errorf("expected sse reload event to be received, got: %q", resp)
|
||||
}
|
||||
case err := <-errChan:
|
||||
if err == nil {
|
||||
t.Fatalf("unexpected sse response: %v", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("timeout waiting for sse response")
|
||||
}
|
||||
})
|
||||
t.Run("unsupported encodings result in a warning", func(t *testing.T) {
|
||||
// Arrange
|
||||
r := &http.Response{
|
||||
Body: io.NopCloser(bytes.NewReader([]byte("<p>Data</p>"))),
|
||||
Header: make(http.Header),
|
||||
Request: &http.Request{
|
||||
URL: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
r.Header.Set("Content-Type", "text/html, charset=utf-8")
|
||||
r.Header.Set("Content-Encoding", "weird-encoding")
|
||||
|
||||
// Act
|
||||
lh := newTestLogHandler(slog.LevelInfo)
|
||||
log := slog.New(lh)
|
||||
h := New(log, "127.0.0.1", 7474, &url.URL{Scheme: "http", Host: "example.com"})
|
||||
err := h.modifyResponse(r)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Assert
|
||||
if len(lh.records) != 1 {
|
||||
var sb strings.Builder
|
||||
for _, record := range lh.records {
|
||||
sb.WriteString(record.Message)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
t.Fatalf("expected 1 log entry, but got %d: \n%s", len(lh.records), sb.String())
|
||||
}
|
||||
record := lh.records[0]
|
||||
if record.Message != unsupportedContentEncoding {
|
||||
t.Errorf("expected warning message %q, got %q", unsupportedContentEncoding, record.Message)
|
||||
}
|
||||
if record.Level != slog.LevelWarn {
|
||||
t.Errorf("expected warning, got level %v", record.Level)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func newTestLogHandler(level slog.Level) *testLogHandler {
|
||||
return &testLogHandler{
|
||||
m: new(sync.Mutex),
|
||||
records: nil,
|
||||
level: level,
|
||||
}
|
||||
}
|
||||
|
||||
type testLogHandler struct {
|
||||
m *sync.Mutex
|
||||
records []slog.Record
|
||||
level slog.Level
|
||||
}
|
||||
|
||||
func (h *testLogHandler) Enabled(ctx context.Context, l slog.Level) bool {
|
||||
return l >= h.level
|
||||
}
|
||||
|
||||
func (h *testLogHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
h.m.Lock()
|
||||
defer h.m.Unlock()
|
||||
if r.Level < h.level {
|
||||
return nil
|
||||
}
|
||||
h.records = append(h.records, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *testLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *testLogHandler) WithGroup(name string) slog.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
func TestParseNonce(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
csp string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty csp",
|
||||
csp: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "simple csp",
|
||||
csp: "script-src 'nonce-oLhVst3hTAcxI734qtB0J9Qc7W4qy09C'",
|
||||
expected: "oLhVst3hTAcxI734qtB0J9Qc7W4qy09C",
|
||||
},
|
||||
{
|
||||
name: "simple csp without single quote",
|
||||
csp: "script-src nonce-oLhVst3hTAcxI734qtB0J9Qc7W4qy09C",
|
||||
expected: "oLhVst3hTAcxI734qtB0J9Qc7W4qy09C",
|
||||
},
|
||||
{
|
||||
name: "complete csp",
|
||||
csp: "default-src 'self'; frame-ancestors 'self'; form-action 'self'; script-src 'strict-dynamic' 'nonce-4VOtk0Uo1l7pwtC';",
|
||||
expected: "4VOtk0Uo1l7pwtC",
|
||||
},
|
||||
{
|
||||
name: "mdn example 1",
|
||||
csp: "default-src 'self'",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "mdn example 2",
|
||||
csp: "default-src 'self' *.trusted.com",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "mdn example 3",
|
||||
csp: "default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "mdn example 3 multiple sources",
|
||||
csp: "default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com foo.com 'strict-dynamic' 'nonce-4VOtk0Uo1l7pwtC'",
|
||||
expected: "4VOtk0Uo1l7pwtC",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
nonce := parseNonce(tc.csp)
|
||||
if nonce != tc.expected {
|
||||
t.Errorf("expected nonce to be %s, but got %s", tc.expected, nonce)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
10
templ/cmd/templ/generatecmd/proxy/script.js
Normal file
@ -0,0 +1,10 @@
|
||||
(function() {
|
||||
let templ_reloadSrc = window.templ_reloadSrc || new EventSource("/_templ/reload/events");
|
||||
templ_reloadSrc.onmessage = (event) => {
|
||||
if (event && event.data === "reload") {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
window.templ_reloadSrc = templ_reloadSrc;
|
||||
window.onbeforeunload = () => window.templ_reloadSrc.close();
|
||||
})();
|
108
templ/cmd/templ/generatecmd/run/run_test.go
Normal file
@ -0,0 +1,108 @@
|
||||
package run_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/a-h/templ/cmd/templ/generatecmd/run"
|
||||
)
|
||||
|
||||
//go:embed testprogram/*
|
||||
var testprogram embed.FS
|
||||
|
||||
func TestGoRun(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping test in short mode.")
|
||||
}
|
||||
|
||||
// Copy testprogram to a temporary directory.
|
||||
dir, err := os.MkdirTemp("", "testprogram")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to make test dir: %v", err)
|
||||
}
|
||||
files, err := testprogram.ReadDir("testprogram")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read embedded dir: %v", err)
|
||||
}
|
||||
for _, file := range files {
|
||||
srcFileName := "testprogram/" + file.Name()
|
||||
srcData, err := testprogram.ReadFile(srcFileName)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read src file %q: %v", srcFileName, err)
|
||||
}
|
||||
tgtFileName := filepath.Join(dir, file.Name())
|
||||
tgtFile, err := os.Create(tgtFileName)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create tgt file %q: %v", tgtFileName, err)
|
||||
}
|
||||
defer tgtFile.Close()
|
||||
if _, err := tgtFile.Write(srcData); err != nil {
|
||||
t.Fatalf("failed to write to tgt file %q: %v", tgtFileName, err)
|
||||
}
|
||||
}
|
||||
// Rename the go.mod.embed file to go.mod.
|
||||
if err := os.Rename(filepath.Join(dir, "go.mod.embed"), filepath.Join(dir, "go.mod")); err != nil {
|
||||
t.Fatalf("failed to rename go.mod.embed: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cmd string
|
||||
}{
|
||||
{
|
||||
name: "Well behaved programs get shut down",
|
||||
cmd: "go run .",
|
||||
},
|
||||
{
|
||||
name: "Badly behaved programs get shut down",
|
||||
cmd: "go run . -badly-behaved",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cmd, err := run.Run(ctx, dir, tt.cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run program: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
pid := cmd.Process.Pid
|
||||
|
||||
if err := run.KillAll(); err != nil {
|
||||
t.Fatalf("failed to kill all: %v", err)
|
||||
}
|
||||
|
||||
// Check the parent process is no longer running.
|
||||
if err := cmd.Process.Signal(os.Signal(syscall.Signal(0))); err == nil {
|
||||
t.Fatalf("process %d is still running", pid)
|
||||
}
|
||||
// Check that the child was stopped.
|
||||
body, err := readResponse("http://localhost:7777")
|
||||
if err == nil {
|
||||
t.Fatalf("child process is still running: %s", body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func readResponse(url string) (body string, err error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return body, err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|