0){E().setTimeout(e,x.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ce({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Mn={};function Xn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Fn(e,t){if(t.init){t.init(n)}Mn[e]=ce(Xn(),t)}function Bn(e){delete Mn[e]}function Un(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Mn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Un(ue(c(e)),n,r)}var jn=false;ne().addEventListener("DOMContentLoaded",function(){jn=true});function Vn(e){if(jn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function _n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend","")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function $n(){const e=zn();if(e){Q.config=ce(Q.config,e)}}Vn(function(){$n();_n();let e=ne().body;kt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Wt();se(t,function(e){he(e,"htmx:restored",{document:ne(),triggerEvent:he})})}else{if(n){n(e)}}};E().setTimeout(function(){he(e,"htmx:load",{});e=null},0)});return Q}();
\ No newline at end of file
diff --git a/assets/js/utils.js b/assets/js/utils.js
new file mode 100644
index 0000000..901393e
--- /dev/null
+++ b/assets/js/utils.js
@@ -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);
+ }
+ };
+})();
diff --git a/assets/learnlytics.png b/assets/learnlytics.png
new file mode 100644
index 0000000..9281f10
Binary files /dev/null and b/assets/learnlytics.png differ
diff --git a/components/charts.templ b/components/charts.templ
new file mode 100644
index 0000000..4076cc7
--- /dev/null
+++ b/components/charts.templ
@@ -0,0 +1,82 @@
+package components
+
+templ barChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) {
+
+
+ @templ.JSFuncCall(
+ "barChart",
+ id,
+ data,
+ labels,
+ tooltip,
+ title,
+ scaleLabelX,
+ scaleLabelY,
+ )
+
+}
+
+templ barLineChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) {
+
+
+
+ @templ.JSFuncCall(
+ "barLineChart",
+ id,
+ data,
+ labels,
+ tooltip,
+ title,
+ scaleLabelX,
+ scaleLabelY,
+ )
+
+}
+
+templ pieChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) {
+
+
+ @templ.JSFuncCall(
+ "pieChart",
+ id,
+ data,
+ labels,
+ tooltip,
+ title,
+ scaleLabelX,
+ scaleLabelY,
+ )
+
+}
+
+templ doughnutChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) {
+
+
+ @templ.JSFuncCall(
+ "doughnutChart",
+ id,
+ data,
+ labels,
+ tooltip,
+ title,
+ scaleLabelX,
+ scaleLabelY,
+ )
+
+}
+
+templ polarChart(id string, data []float64, labels []string, tooltip string, title string, scaleLabelX string, scaleLabelY string) {
+
+
+ @templ.JSFuncCall(
+ "polarChart",
+ id,
+ data,
+ labels,
+ tooltip,
+ title,
+ scaleLabelX,
+ scaleLabelY,
+ )
+
+}
diff --git a/components/charts_templ.go b/components/charts_templ.go
new file mode 100644
index 0000000..af54ab3
--- /dev/null
+++ b/components/charts_templ.go
@@ -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, " ")
+ 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, "
")
+ 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, " ")
+ 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, "
")
+ 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, " ")
+ 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, "
")
+ 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, " ")
+ 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, "
")
+ 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, " ")
+ 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, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/components/components.templ b/components/components.templ
new file mode 100644
index 0000000..cbfac29
--- /dev/null
+++ b/components/components.templ
@@ -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) {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Learnlytics - { title }
+
+
+
+
+
+
+
+
+ @navbar()
+
+ { children... }
+
+
+
+}
+
+templ footer() {
+
+}
+
+templ navbar() {
+
+}
+
+templ selectList(labels []string) {
+
+
+
+}
+
+templ usercard(username string) {
+
+
+
{ username }
+
+
+
+
+
+
+
p.keier@beyerstedt-it.de
+
+
+
+
+
+
+}
+
+templ NotFound() {
+ @base("Error") {
+
+
404 - Not Found
+
+
+
+
+
This Page Didn't Pass the Exam
+
It tried, but it didn’t make the cut.
+
Better check the Dashboard instead!
+
+ }
+}
+
+templ Test() {
+ @base("Test") {
+
+
+ Test
+
+
+ @polarChart(
+ "1",
+ genRandomData(6),
+ []string{"Klasse 8a", "Klasse 5b", "Klasse 6c", "Klasse 10d", "Englisch LK 12", "Geschickte GK 11"},
+ "Points scored",
+ "Classes",
+ "Classes",
+ "",
+ )
+
+
+ Test
+
+
+ @doughnutChart(
+ "1",
+ genRandomData(6),
+ []string{"Klasse 8a", "Klasse 5b", "Klasse 6c", "Klasse 10d", "Englisch LK 12", "Geschickte GK 11"},
+ "Points scored",
+ "Classes",
+ "Classes",
+ "",
+ )
+
+
+ }
+}
+
+templ Dashboard(username string) {
+ @base("Dashboard") {
+
+
+ @usercard(username)
+
+
+ @barChart(
+ "2",
+ genRandomData(6),
+ []string{"Klasse 8a", "Klasse 5b", "Klasse 6c", "Klasse 10d", "Englisch LK 12", "Geschickte GK 11"},
+ "Points scored",
+ "Classes",
+ "Classes",
+ "",
+ )
+
+
+
+ @selectList([]string{"Phil Keier", "Calvin Brandt", "Nova Eib"})
+
+
+
+ @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",
+ )
+
+
+ }
+}
+
+templ Login() {
+ @base("Login") {
+
+
+
Learnlytics
+
+
+ }
+}
diff --git a/components/components_templ.go b/components/components_templ.go
new file mode 100644
index 0000000..64e04ff
--- /dev/null
+++ b/components/components_templ.go
@@ -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, "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, " ")
+ 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, "")
+ 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, "
")
+ 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, "")
+ 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, "")
+ 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, "
")
+ 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, "")
+ 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, " ")
+ 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, "
")
+ 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, "404 - Not Found This Page Didn't Pass the Exam It tried, but it didn’t make the cut.
Better check the Dashboard instead!
")
+ 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, "Test
")
+ 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, "
Test
")
+ 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, "
")
+ 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, "")
+ 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, "
")
+ 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, "
")
+ 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, "
")
+ 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, "
")
+ 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, "Learnlytics ")
+ 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
diff --git a/components/go.mod b/components/go.mod
new file mode 100644
index 0000000..1b619b0
--- /dev/null
+++ b/components/go.mod
@@ -0,0 +1,5 @@
+module components
+
+go 1.24.1
+
+require github.com/a-h/templ v0.3.833
diff --git a/components/go.sum b/components/go.sum
new file mode 100644
index 0000000..bbe292f
--- /dev/null
+++ b/components/go.sum
@@ -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=
diff --git a/db/go.mod b/db/go.mod
new file mode 100644
index 0000000..f75d104
--- /dev/null
+++ b/db/go.mod
@@ -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
+)
diff --git a/db/go.sum b/db/go.sum
new file mode 100644
index 0000000..3a8514c
--- /dev/null
+++ b/db/go.sum
@@ -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=
diff --git a/db/model.go b/db/model.go
new file mode 100644
index 0000000..709c2fa
--- /dev/null
+++ b/db/model.go
@@ -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
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..861e5da
--- /dev/null
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..1febbf6
--- /dev/null
+++ b/go.sum
@@ -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=
diff --git a/handlers/go.mod b/handlers/go.mod
new file mode 100644
index 0000000..5df149a
--- /dev/null
+++ b/handlers/go.mod
@@ -0,0 +1,3 @@
+module handlers
+
+go 1.24.1
diff --git a/handlers/handler.go b/handlers/handler.go
new file mode 100644
index 0000000..4d943f2
--- /dev/null
+++ b/handlers/handler.go
@@ -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}
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..3758828
--- /dev/null
+++ b/main.go
@@ -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)
+ }
+}
diff --git a/templ/.dockerignore b/templ/.dockerignore
new file mode 100644
index 0000000..17896fe
--- /dev/null
+++ b/templ/.dockerignore
@@ -0,0 +1,3 @@
+.git
+Dockerfile
+.dockerignore
diff --git a/templ/.envrc b/templ/.envrc
new file mode 100644
index 0000000..8392d15
--- /dev/null
+++ b/templ/.envrc
@@ -0,0 +1 @@
+use flake
\ No newline at end of file
diff --git a/templ/.github/FUNDING.yml b/templ/.github/FUNDING.yml
new file mode 100644
index 0000000..22f305d
--- /dev/null
+++ b/templ/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: [a-h, joerdav]
diff --git a/templ/.github/ISSUE_TEMPLATE/bug_report.md b/templ/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..cf03af0
--- /dev/null
+++ b/templ/.github/ISSUE_TEMPLATE/bug_report.md
@@ -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.
diff --git a/templ/.github/workflows/ci.yml b/templ/.github/workflows/ci.yml
new file mode 100644
index 0000000..56722a4
--- /dev/null
+++ b/templ/.github/workflows/ci.yml
@@ -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
diff --git a/templ/.github/workflows/docs.yaml b/templ/.github/workflows/docs.yaml
new file mode 100644
index 0000000..2dff2bc
--- /dev/null
+++ b/templ/.github/workflows/docs.yaml
@@ -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
diff --git a/templ/.github/workflows/release.yml b/templ/.github/workflows/release.yml
new file mode 100644
index 0000000..c19a3ab
--- /dev/null
+++ b/templ/.github/workflows/release.yml
@@ -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 }}'
diff --git a/templ/.gitignore b/templ/.gitignore
new file mode 100644
index 0000000..0420c67
--- /dev/null
+++ b/templ/.gitignore
@@ -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
diff --git a/templ/.goreleaser.yaml b/templ/.goreleaser.yaml
new file mode 100644
index 0000000..456187c
--- /dev/null
+++ b/templ/.goreleaser.yaml
@@ -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:'
diff --git a/templ/.ignore b/templ/.ignore
new file mode 100644
index 0000000..9377535
--- /dev/null
+++ b/templ/.ignore
@@ -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
diff --git a/templ/.version b/templ/.version
new file mode 100644
index 0000000..46a3c4a
--- /dev/null
+++ b/templ/.version
@@ -0,0 +1 @@
+0.3.833
\ No newline at end of file
diff --git a/templ/.vscode/settings.json b/templ/.vscode/settings.json
new file mode 100644
index 0000000..7ccb52a
--- /dev/null
+++ b/templ/.vscode/settings.json
@@ -0,0 +1,12 @@
+{
+ "cSpell.words": [
+ "blockquote",
+ "fieldset",
+ "figcaption",
+ "formatstring",
+ "goexpression",
+ "keygen",
+ "strs",
+ "templ"
+ ]
+}
\ No newline at end of file
diff --git a/templ/CODE_OF_CONDUCT.md b/templ/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..08340d3
--- /dev/null
+++ b/templ/CODE_OF_CONDUCT.md
@@ -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.
diff --git a/templ/CONTRIBUTING.md b/templ/CONTRIBUTING.md
new file mode 100644
index 0000000..e98d31f
--- /dev/null
+++ b/templ/CONTRIBUTING.md
@@ -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: ``,
+ expected: HTMLComment{
+ Contents: " single line comment ",
+ },
+ },
+ {
+ name: "comment - no whitespace",
+ input: ``,
+ expected: HTMLComment{
+ Contents: "no whitespace between sequence open and close",
+ },
+ },
+ {
+ name: "comment - multiline",
+ input: ``,
+ expected: HTMLComment{
+ Contents: ` multiline
+ comment
+ `,
+ },
+ },
+ {
+ name: "comment - with tag",
+ input: ``,
+ expected: HTMLComment{
+ Contents: ` tag
`,
+ },
+ },
+ {
+ name: "comments can contain tags",
+ input: ``,
+ expected: HTMLComment{
+ Contents: ` hello world
`,
+ },
+ },
+ }
+ 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: `' not found",
+ parse.Position{
+ Index: 26,
+ Line: 0,
+ Col: 26,
+ }),
+ },
+ {
+ name: "comment in comment",
+ input: ` -->`,
+ 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() {
+ Ignored
+ Sanitized
+ Unsanitized
+}
+```
+
+It also contains an expected output file.
+
+```html
+Ignored
+Sanitized
+Unsanitized
+```
+
+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.
+
diff --git a/templ/LICENSE b/templ/LICENSE
new file mode 100644
index 0000000..15e6fb8
--- /dev/null
+++ b/templ/LICENSE
@@ -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.
diff --git a/templ/README.md b/templ/README.md
new file mode 100644
index 0000000..60d2f67
--- /dev/null
+++ b/templ/README.md
@@ -0,0 +1,184 @@
+
+
+## An HTML templating language for Go that has great developer tooling.
+
+
+
+
+## Documentation
+
+See user documentation at https://templ.guide
+
+
+
+
+
+
+
+
+## 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
+```
+
diff --git a/templ/SECURITY.md b/templ/SECURITY.md
new file mode 100644
index 0000000..e8c820e
--- /dev/null
+++ b/templ/SECURITY.md
@@ -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.
diff --git a/templ/benchmarks/react/.gitignore b/templ/benchmarks/react/.gitignore
new file mode 100644
index 0000000..36abe80
--- /dev/null
+++ b/templ/benchmarks/react/.gitignore
@@ -0,0 +1,3 @@
+index.js
+node_modules
+
diff --git a/templ/benchmarks/react/README.md b/templ/benchmarks/react/README.md
new file mode 100644
index 0000000..409c608
--- /dev/null
+++ b/templ/benchmarks/react/README.md
@@ -0,0 +1,23 @@
+# React benchmark
+
+## Tasks
+
+### install
+
+```
+npm i
+```
+
+### build
+
+```sh
+npm run build
+```
+
+### run
+
+requires: build
+
+```sh
+npm start
+```
diff --git a/templ/benchmarks/react/package-lock.json b/templ/benchmarks/react/package-lock.json
new file mode 100644
index 0000000..1209fc9
--- /dev/null
+++ b/templ/benchmarks/react/package-lock.json
@@ -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"
+ }
+ }
+ }
+}
diff --git a/templ/benchmarks/react/package.json b/templ/benchmarks/react/package.json
new file mode 100644
index 0000000..d234ac7
--- /dev/null
+++ b/templ/benchmarks/react/package.json
@@ -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"
+ }
+}
diff --git a/templ/benchmarks/react/src/index.jsx b/templ/benchmarks/react/src/index.jsx
new file mode 100644
index 0000000..b72b305
--- /dev/null
+++ b/templ/benchmarks/react/src/index.jsx
@@ -0,0 +1,34 @@
+import * as React from 'react'
+import * as Server from 'react-dom/server'
+import Benchmark from 'benchmark';
+
+const component = (p) =>
+ ;
+
+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();
diff --git a/templ/benchmarks/templ/README.md b/templ/benchmarks/templ/README.md
new file mode 100644
index 0000000..9275b01
--- /dev/null
+++ b/templ/benchmarks/templ/README.md
@@ -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.
diff --git a/templ/benchmarks/templ/data.go b/templ/benchmarks/templ/data.go
new file mode 100644
index 0000000..adc32dc
--- /dev/null
+++ b/templ/benchmarks/templ/data.go
@@ -0,0 +1,6 @@
+package testhtml
+
+type Person struct {
+ Name string
+ Email string
+}
diff --git a/templ/benchmarks/templ/render_test.go b/templ/benchmarks/templ/render_test.go
new file mode 100644
index 0000000..cecb523
--- /dev/null
+++ b/templ/benchmarks/templ/render_test.go
@@ -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(`
+
+
+
+`))
+
+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 = ` `
+
+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()
+ }
+}
diff --git a/templ/benchmarks/templ/template.templ b/templ/benchmarks/templ/template.templ
new file mode 100644
index 0000000..54ac5f2
--- /dev/null
+++ b/templ/benchmarks/templ/template.templ
@@ -0,0 +1,13 @@
+package testhtml
+
+templ Render(p Person) {
+
+
+
+
+}
diff --git a/templ/benchmarks/templ/template_templ.go b/templ/benchmarks/templ/template_templ.go
new file mode 100644
index 0000000..2638d39
--- /dev/null
+++ b/templ/benchmarks/templ/template_templ.go
@@ -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, "")
+ 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, " `)
+ 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, "\">
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/templ/cfg/cfg.go b/templ/cfg/cfg.go
new file mode 100644
index 0000000..0ea880d
--- /dev/null
+++ b/templ/cfg/cfg.go
@@ -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{}
+}
diff --git a/templ/cmd/templ/fmtcmd/main.go b/templ/cmd/templ/fmtcmd/main.go
new file mode 100644
index 0000000..2f2b7fc
--- /dev/null
+++ b/templ/cmd/templ/fmtcmd/main.go
@@ -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
+}
diff --git a/templ/cmd/templ/fmtcmd/main_test.go b/templ/cmd/templ/fmtcmd/main_test.go
new file mode 100644
index 0000000..9a70f36
--- /dev/null
+++ b/templ/cmd/templ/fmtcmd/main_test.go
@@ -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)
+ }
+ })
+}
diff --git a/templ/cmd/templ/fmtcmd/testdata.txtar b/templ/cmd/templ/fmtcmd/testdata.txtar
new file mode 100644
index 0000000..8041fce
--- /dev/null
+++ b/templ/cmd/templ/fmtcmd/testdata.txtar
@@ -0,0 +1,54 @@
+-- a.templ --
+package test
+
+templ a() {
+
+}
+-- a.templ --
+package test
+
+templ a() {
+
+}
+-- b.templ --
+package test
+
+templ b() {
+
+}
+-- b.templ --
+package test
+
+templ b() {
+
+}
+-- c.templ --
+package test
+
+templ c() {
+
+}
+-- c.templ --
+package test
+
+templ c() {
+
+}
diff --git a/templ/cmd/templ/generatecmd/cmd.go b/templ/cmd/templ/generatecmd/cmd.go
new file mode 100644
index 0000000..169cc06
--- /dev/null
+++ b/templ/cmd/templ/generatecmd/cmd.go
@@ -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
+}
diff --git a/templ/cmd/templ/generatecmd/eventhandler.go b/templ/cmd/templ/generatecmd/eventhandler.go
new file mode 100644
index 0000000..93ce332
--- /dev/null
+++ b/templ/cmd/templ/generatecmd/eventhandler.go
@@ -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)
+}
diff --git a/templ/cmd/templ/generatecmd/fatalerror.go b/templ/cmd/templ/generatecmd/fatalerror.go
new file mode 100644
index 0000000..1081659
--- /dev/null
+++ b/templ/cmd/templ/generatecmd/fatalerror.go
@@ -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
+}
diff --git a/templ/cmd/templ/generatecmd/main.go b/templ/cmd/templ/generatecmd/main.go
new file mode 100644
index 0000000..4d2cbf5
--- /dev/null
+++ b/templ/cmd/templ/generatecmd/main.go
@@ -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)
+}
diff --git a/templ/cmd/templ/generatecmd/main_test.go b/templ/cmd/templ/generatecmd/main_test.go
new file mode 100644
index 0000000..b13c8eb
--- /dev/null
+++ b/templ/cmd/templ/generatecmd/main_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/templ/cmd/templ/generatecmd/modcheck/modcheck.go b/templ/cmd/templ/generatecmd/modcheck/modcheck.go
new file mode 100644
index 0000000..bc3fc03
--- /dev/null
+++ b/templ/cmd/templ/generatecmd/modcheck/modcheck.go
@@ -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")
+}
diff --git a/templ/cmd/templ/generatecmd/modcheck/modcheck_test.go b/templ/cmd/templ/generatecmd/modcheck/modcheck_test.go
new file mode 100644
index 0000000..544642b
--- /dev/null
+++ b/templ/cmd/templ/generatecmd/modcheck/modcheck_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/templ/cmd/templ/generatecmd/proxy/proxy.go b/templ/cmd/templ/generatecmd/proxy/proxy.go
new file mode 100644
index 0000000..f8d4ccd
--- /dev/null
+++ b/templ/cmd/templ/generatecmd/proxy/proxy.go
@@ -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(``)
+ return sb.String()
+ }
+ return ``
+}
+
+func insertScriptTagIntoBody(nonce, body string) (updated string) {
+ doc, err := goquery.NewDocumentFromReader(strings.NewReader(body))
+ if err != nil {
+ return strings.Replace(body, "", getScriptTag(nonce)+"