Переглянути джерело

Raw port of contacts.app from Python Flask to Go. First compiling version.

Frédéric G. MARAND 10 місяців тому
батько
коміт
d6575bd3a6
46 змінених файлів з 5277 додано та 211 видалено
  1. 7 0
      .idea/demo-htmx.iml
  2. 6 0
      .idea/misc.xml
  3. 1 0
      .idea/modules.xml
  4. 21 0
      .idea/runConfigurations/go_build_code_osinet_fr_fgm_demo_htmx_contactsapp.xml
  5. 1 0
      .idea/vcs.xml
  6. 379 0
      contactsapp/app.go
  7. 181 0
      contactsapp/app.py
  8. 138 0
      contactsapp/contacts.json
  9. 138 0
      contactsapp/contacts0.json
  10. 334 0
      contactsapp/contacts_model.go
  11. 145 0
      contactsapp/contacts_model.py
  12. 55 0
      contactsapp/static/img/spinning-circles.svg
  13. 0 0
      contactsapp/static/js/_hyperscript-0.9.7.js
  14. 3295 0
      contactsapp/static/js/htmx-1.8.0.js
  15. 68 0
      contactsapp/static/js/rsjs-menu.js
  16. 65 0
      contactsapp/static/site.css
  17. 19 0
      contactsapp/templates/archive_ui.gohtml
  18. 53 0
      contactsapp/templates/edit.gohtml
  19. 64 0
      contactsapp/templates/index.gohtml
  20. 26 0
      contactsapp/templates/layout.gohtml
  21. 41 0
      contactsapp/templates/new.gohtml
  22. 28 0
      contactsapp/templates/rows.gohtml
  23. 20 0
      contactsapp/templates/show.gohtml
  24. 1 1
      doc/htmx-ch04.md
  25. 7 0
      doc/htmx-ch05.md
  26. 15 0
      go.mod
  27. 62 0
      go.sum
  28. 10 10
      web/public/00.html
  29. 11 11
      web/public/01.html
  30. 20 11
      web/public/02.html
  31. 0 26
      web/public/03.html
  32. 0 26
      web/public/04.html
  33. 0 26
      web/public/05.html
  34. 0 27
      web/public/06.html
  35. 0 32
      web/public/07.html
  36. 0 34
      web/public/08.html
  37. BIN
      web/public/blank.pdf
  38. 0 0
      web/public/css/styles.css
  39. 0 0
      web/public/js/head-support.js
  40. 0 0
      web/public/js/htmx-1-9-12.min.js
  41. 12 0
      web/public/layout.html
  42. 0 0
      web/templates/contacts.gohtml
  43. 3 0
      web/templates/messages.gohtml
  44. 0 0
      web/templates/nav.gohtml
  45. 15 0
      web/templates/settings.gohtml
  46. 36 7
      web/web.go

+ 7 - 0
.idea/demo-htmx.iml

@@ -1,10 +1,17 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <module type="WEB_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="Python" name="Python facet">
+      <configuration sdkName="Python 3.9" />
+    </facet>
+  </component>
   <component name="Go" enabled="true" />
   <component name="NewModuleRootManager">
     <content url="file://$MODULE_DIR$" />
     <orderEntry type="inheritedJdk" />
     <orderEntry type="sourceFolder" forTests="false" />
     <orderEntry type="library" name="htmx.org" level="application" />
+    <orderEntry type="module" module-name="htmx-contact-app" />
+    <orderEntry type="library" name="Python 3.9 interpreter library" level="application" />
   </component>
 </module>

+ 6 - 0
.idea/misc.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Black">
+    <option name="sdkName" value="Python 3.9" />
+  </component>
+</project>

+ 1 - 0
.idea/modules.xml

@@ -3,6 +3,7 @@
   <component name="ProjectModuleManager">
     <modules>
       <module fileurl="file://$PROJECT_DIR$/.idea/demo-htmx.iml" filepath="$PROJECT_DIR$/.idea/demo-htmx.iml" />
+      <module fileurl="file://$PROJECT_DIR$/../htmx-contact-app/.idea/htmx-contact-app.iml" filepath="$PROJECT_DIR$/../htmx-contact-app/.idea/htmx-contact-app.iml" />
     </modules>
   </component>
 </project>

+ 21 - 0
.idea/runConfigurations/go_build_code_osinet_fr_fgm_demo_htmx_contactsapp.xml

@@ -0,0 +1,21 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="go build code.osinet.fr/fgm/demo-htmx/contactsapp" type="GoApplicationRunConfiguration" factoryName="Go Application" nameIsGenerated="true">
+    <module name="demo-htmx" />
+    <working_directory value="$PROJECT_DIR$/contactsapp" />
+    <EXTENSION ID="net.ashald.envfile">
+      <option name="IS_ENABLED" value="false" />
+      <option name="IS_SUBST" value="false" />
+      <option name="IS_PATH_MACRO_SUPPORTED" value="false" />
+      <option name="IS_IGNORE_MISSING_FILES" value="false" />
+      <option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
+      <ENTRIES>
+        <ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
+      </ENTRIES>
+    </EXTENSION>
+    <kind value="PACKAGE" />
+    <package value="code.osinet.fr/fgm/demo-htmx/contactsapp" />
+    <directory value="$PROJECT_DIR$" />
+    <filePath value="$PROJECT_DIR$" />
+    <method v="2" />
+  </configuration>
+</component>

+ 1 - 0
.idea/vcs.xml

@@ -2,5 +2,6 @@
 <project version="4">
   <component name="VcsDirectoryMappings">
     <mapping directory="" vcs="Git" />
+    <mapping directory="$PROJECT_DIR$/../htmx-contact-app" vcs="Git" />
   </component>
 </project>

+ 379 - 0
contactsapp/app.go

@@ -0,0 +1,379 @@
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"html/template"
+	"log"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"github.com/Masterminds/sprig/v3"
+)
+
+const (
+	SecretKey = "hypermedia rocks"
+)
+
+var (
+	/*
+	   @app.route("/")
+	   def index():
+	*/
+	RouteFront = http.RedirectHandler("/contacts", http.StatusFound)
+
+	/*
+	   @app.route("/contacts")
+	   def contacts():
+	*/
+	MakeRouteContacts = func(templates *template.Template) http.HandlerFunc {
+		return func(w http.ResponseWriter, r *http.Request) {
+			search := r.URL.Query().Get("q")
+			page, err := strconv.Atoi(r.URL.Query().Get("page"))
+			if err != nil || page == 0 {
+				page = 1
+			}
+			var contactsSet []Contact
+			if search != "" {
+				contactsSet = (&Contact{}).Search(search)
+				if r.Header.Get("HX-Trigger") == "search" {
+					templates.ExecuteTemplate(w, "rows.html", anyMap{"contacts": contactsSet})
+					return
+				}
+			} else {
+				contactsSet = (&Contact{}).All(page)
+			}
+			templates.ExecuteTemplate(w, "index.html", anyMap{
+				"contacts": contactsSet,
+				"archiver": NewArchiver(),
+			})
+		}
+	}
+
+	/*
+		@app.route("/contacts/archive", methods=["POST"])
+		def start_archive():
+	*/
+	MakeStartArchive = func(templates *template.Template) http.HandlerFunc {
+		return func(w http.ResponseWriter, r *http.Request) {
+			archiver := NewArchiver()
+			archiver.Run()
+			templates.ExecuteTemplate(w, "archive_ui.html", anyMap{"archiver": archiver})
+		}
+	}
+
+	/*
+		@app.route("/contacts/archive", methods=["GET"])
+		def archive_status():
+	*/
+	MakeArchiveStatus = func(templates *template.Template) http.HandlerFunc {
+		return func(w http.ResponseWriter, r *http.Request) {
+			archiver := NewArchiver()
+			templates.ExecuteTemplate(w, "archive_ui.html", anyMap{"archiver": archiver})
+		}
+	}
+
+	/*
+		@app.route("/contacts/archive/file", methods=["GET"])
+		def archive_content():
+	*/
+	ArchiveContent http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
+		archiver := NewArchiver()
+		fileName := archiver.ArchiveFile()
+		w.Header().Set("Content-Disposition", fmt.Sprintf(`attachement; filename="%s"`, fileName))
+		http.ServeFile(w, r, fileName)
+	}
+
+	/*
+		@app.route("/contacts/archive", methods=["DELETE"])
+		def reset_archive():
+	*/
+	MakeResetArchive = func(templates *template.Template) http.HandlerFunc {
+		return func(w http.ResponseWriter, r *http.Request) {
+			archiver := NewArchiver()
+			archiver.Reset()
+			templates.ExecuteTemplate(w, "archive_ui.html", anyMap{"archiver": archiver})
+		}
+	}
+
+	/*
+		@app.route("/contacts/count")
+		def contacts_count():
+	*/
+	ContactsCount http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
+		count := (&Contact{}).Count()
+		_, _ = fmt.Fprintf(w, "(%d total Contacts)", count)
+	}
+
+	/*
+		@app.route("/contacts/new", methods=['GET'])
+		def contacts_new_get():
+	*/
+	MakeContactsNewGet = func(templates *template.Template) http.HandlerFunc {
+		return func(w http.ResponseWriter, r *http.Request) {
+			templates.ExecuteTemplate(w, "new.html", anyMap{"contact": NewContact(nil)})
+		}
+	}
+
+	/*
+		@app.route("/contacts/new", methods=['POST'])
+		def contacts_new():
+	*/
+	MakeContactsNew = func(templates *template.Template) http.HandlerFunc {
+		return func(w http.ResponseWriter, r *http.Request) {
+			c := NewContact(anyMap{
+				"first": r.FormValue("first_name"),
+				"last":  r.FormValue("last_name"),
+				"email": r.FormValue("phone"),
+				"phone": r.FormValue("email"),
+			})
+			if c.Save() {
+				flash("Created New Contact!")
+				http.Redirect(w, r, "/contacts", http.StatusFound)
+				return
+			} else {
+				templates.ExecuteTemplate(w, "new.html", anyMap{"contact": c})
+			}
+		}
+	}
+
+	/*
+		@app.route("/contacts/<contact_id>")
+		def contacts_view(contact_id=0):
+	*/
+	MakeContactsView = func(templates *template.Template) http.HandlerFunc {
+		return func(w http.ResponseWriter, r *http.Request) {
+			contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
+			contact := (&Contact{}).Find(uint64(contact_id))
+			templates.ExecuteTemplate(w, "show.html", anyMap{"contact": contact})
+		}
+	}
+
+	/*
+		@app.route("/contacts/<contact_id>/edit", methods=["GET"])
+		def contacts_edit_get(contact_id=0):
+	*/
+	MakeContactsEditGet = func(templates *template.Template) http.HandlerFunc {
+		return func(w http.ResponseWriter, r *http.Request) {
+			contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
+			contact := (&Contact{}).Find(uint64(contact_id))
+			templates.ExecuteTemplate(w, "edit.html", anyMap{"contact": contact})
+		}
+	}
+
+	/*
+		@app.route("/contacts/<contact_id>/edit", methods=["POST"])
+		def contacts_edit_post(contact_id=0):
+	*/
+	MakeContactsEditPost = func(templates *template.Template) http.HandlerFunc {
+		return func(w http.ResponseWriter, request *http.Request) {
+			contact_id, _ := strconv.Atoi(request.PathValue("contact_id"))
+			c := (&Contact{}).Find(uint64(contact_id))
+			c.Update(
+				request.FormValue("first_name"),
+				request.FormValue("last_name"),
+				request.FormValue("phone"),
+				request.FormValue("email"),
+			)
+			if c.Save() {
+				flash("Updated Contact!")
+				http.Redirect(w, request, fmt.Sprintf("/contacts/%d", contact_id), http.StatusFound)
+				return
+			} else {
+				templates.ExecuteTemplate(w, "edit.html", anyMap{"contact": c})
+			}
+		}
+	}
+
+	/*
+		@app.route("/contacts/<contact_id>/email", methods=["GET"])
+		def contacts_email_get(contact_id=0):
+	*/
+	ContactsEmailGet http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
+		contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
+		c := (&Contact{}).Find(uint64(contact_id))
+		c.Email = r.FormValue("email")
+		c.validate()
+		// FIXME find what this is really expected to do in the original Python.
+		for _, err := range c.Errors {
+			err := err.Error()
+			if strings.Contains(err, "email") {
+				fmt.Fprintln(w, err)
+			}
+		}
+	}
+
+	/*
+		@app.route("/contacts/<contact_id>", methods=["DELETE"])
+		def contacts_delete(contact_id=0):
+	*/
+	ContactsDelete http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
+		contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
+		contact := (&Contact{}).Find(uint64(contact_id))
+		contact.Delete()
+		if r.Header.Get("HX-Trigger") == "delete-btn" {
+			flash("Deleted Contact!")
+			http.Redirect(w, r, "/contacts", http.StatusSeeOther)
+			return
+		} else {
+			return
+		}
+	}
+
+	/*
+		@app.route("/contacts/", methods=["DELETE"])
+		def contacts_delete_all():
+	*/
+	MakeContactsDeleteAll = func(templates *template.Template) http.HandlerFunc {
+		return func(w http.ResponseWriter, r *http.Request) {
+			sContactIDs := strings.Split(r.FormValue("selected_contact_ids"), ",")
+			for _, sContactID := range sContactIDs {
+				if contactID, err := strconv.Atoi(sContactID); contactID > 0 && err == nil {
+					contact := (&Contact{}).Find(uint64(contactID))
+					contact.Delete()
+				}
+			}
+			flash("Deleted contacts!")
+			contacts_set := (&Contact{}).All(1)
+			templates.ExecuteTemplate(w, "index.html", anyMap{"contacts": contacts_set})
+		}
+	}
+
+	/*
+		# ===========================================================
+		# JSON Data API
+		# ===========================================================
+	*/
+	/*
+	   @app.route("/api/v1/contacts", methods=["GET"])
+	   def json_contacts():
+	*/
+	JSONContacts http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
+		contacts_set := (&Contact{}).All(1)
+		enc := json.NewEncoder(w)
+		if err := enc.Encode(anyMap{"contacts": contacts_set}); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+	}
+
+	/*
+		@app.route("/api/v1/contacts", methods=["POST"])
+		def json_contacts_new():
+	*/
+	JSONContactsNew http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
+		c := NewContact(anyMap{
+			"first": r.FormValue("first_°name"),
+			"last":  r.FormValue("last_name"),
+			"phone": r.FormValue("phone"),
+			"email": r.FormValue("email"),
+		})
+		enc := json.NewEncoder(w)
+		if c.Save() {
+			if err := enc.Encode(c); err != nil {
+				http.Error(w, err.Error(), http.StatusInternalServerError)
+				return
+			}
+		} else {
+			w.WriteHeader(http.StatusBadRequest)
+			if err := enc.Encode(anyMap{"errors": c.Errors}); err != nil {
+				http.Error(w, err.Error(), http.StatusInternalServerError)
+				return
+			}
+		}
+	}
+
+	/*
+		@app.route("/api/v1/contacts/<contact_id>", methods=["GET"])
+		def json_contacts_view(contact_id=0):
+	*/
+	JSONContactsEdit http.HandlerFunc = func(w http.ResponseWriter, request *http.Request) {
+		contact_id, _ := strconv.Atoi(request.PathValue("contact_id"))
+		c := (&Contact{}).Find(uint64(contact_id))
+		c.Update(
+			request.FormValue("first_name"),
+			request.FormValue("last_name"),
+			request.FormValue("phone"),
+			request.FormValue("email"),
+		)
+		enc := json.NewEncoder(w)
+		if c.Save() {
+			if err := enc.Encode(c); err != nil {
+				http.Error(w, err.Error(), http.StatusInternalServerError)
+				return
+			}
+		} else {
+			w.WriteHeader(http.StatusBadRequest)
+			if err := enc.Encode(anyMap{"errors": c.Errors}); err != nil {
+				http.Error(w, err.Error(), http.StatusInternalServerError)
+				return
+			}
+		}
+	}
+
+	/*
+		@app.route("/api/v1/contacts/<contact_id>", methods=["DELETE"])
+		def json_contacts_delete(contact_id=0):
+	*/
+	JSONContactsDelete http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
+		contact_id, _ := strconv.Atoi(r.PathValue("contact_id"))
+		contact := (&Contact{}).Find(uint64(contact_id))
+		contact.Delete()
+		enc := json.NewEncoder(w)
+		_ = enc.Encode(anyMap{"success": true})
+	}
+)
+
+/*
+*
+TODO implement me
+*/
+func flash(message string) {
+
+}
+
+func setupRoutes(mux *http.ServeMux, templates *template.Template) {
+	mux.Handle("/", RouteFront)
+	mux.Handle("/contacts", MakeRouteContacts(templates))
+	mux.Handle("POST /contacts/archive", MakeStartArchive(templates))
+	mux.Handle("GET /contacts/archive", MakeArchiveStatus(templates))
+	mux.Handle("GET /contacts/archive/file", ArchiveContent)
+	mux.Handle("DELETE /contacts/archive", MakeResetArchive(templates))
+	mux.Handle("GET /contacts/count", ContactsCount)
+	mux.Handle("GET /contacts/new", MakeContactsNewGet(templates))
+	mux.Handle("POST /contacts/new", MakeContactsNew(templates))
+	mux.Handle("GET /contacts/{contact_id}", MakeContactsView(templates))
+	mux.Handle("GET /contacts/{contact_id}/edit", MakeContactsEditGet(templates))
+	mux.Handle("POST /contacts/{contact_id}/edit", MakeContactsEditPost(templates))
+	mux.Handle("GET /contacts/{contact_id}/email", ContactsEmailGet)
+	mux.Handle("DELETE /contacts/{contact_id}", ContactsDelete)
+	mux.Handle("DELETE /contacts/", MakeContactsDeleteAll(templates))
+	mux.Handle("GET /api/v1/contacts", JSONContacts)
+	mux.Handle("POST /api/v1/contacts", JSONContactsNew)
+	mux.Handle("GET /api/v1/contacts/{contact_id}", JSONContactsEdit)
+	mux.Handle("DELETE /api/v1/contacts/{contact_id}", JSONContactsDelete)
+}
+
+func main() {
+	mux := http.NewServeMux()
+	templates := template.Must(
+		template.
+			New("htmx-app").
+			Funcs(sprig.FuncMap()). // Arithmetic, etc.
+			Funcs(template.FuncMap{
+				"get_flashed_messages": func() string { return "some message" },
+			}).
+			ParseGlob("./templates/*.gohtml"),
+	)
+	setupRoutes(mux, templates)
+	(&Contact{}).LoadDB()
+	if err := http.ListenAndServe(":8080", mux); err != nil {
+		if !errors.Is(err, http.ErrServerClosed) {
+			log.Printf("Server error: %v\n", err)
+			return
+		}
+	}
+	log.Println("Server shutdown")
+}

+ 181 - 0
contactsapp/app.py

@@ -0,0 +1,181 @@
+from flask import (
+    Flask, redirect, render_template, request, flash, jsonify, send_file
+)
+from contacts_model import Contact, Archiver
+import time
+
+Contact.load_db()
+
+# ========================================================
+# Flask App
+# ========================================================
+
+app = Flask(__name__)
+
+app.secret_key = b'hypermedia rocks'
+
+
+@app.route("/")
+def index():
+    return redirect("/contacts")
+
+
+@app.route("/contacts")
+def contacts():
+    search = request.args.get("q")
+    page = int(request.args.get("page", 1))
+    if search is not None:
+        contacts_set = Contact.search(search)
+        if request.headers.get('HX-Trigger') == 'search':
+            return render_template("rows.html", contacts=contacts_set)
+    else:
+        contacts_set = Contact.all()
+    return render_template("index.html", contacts=contacts_set, archiver=Archiver.get())
+
+
+@app.route("/contacts/archive", methods=["POST"])
+def start_archive():
+    archiver = Archiver.get()
+    archiver.run()
+    return render_template("archive_ui.html", archiver=archiver)
+
+
+@app.route("/contacts/archive", methods=["GET"])
+def archive_status():
+    archiver = Archiver.get()
+    return render_template("archive_ui.html", archiver=archiver)
+
+
+@app.route("/contacts/archive/file", methods=["GET"])
+def archive_content():
+    archiver = Archiver.get()
+    return send_file(archiver.archive_file(), "archive.json", as_attachment=True)
+
+
+@app.route("/contacts/archive", methods=["DELETE"])
+def reset_archive():
+    archiver = Archiver.get()
+    archiver.reset()
+    return render_template("archive_ui.html", archiver=archiver)
+
+
+@app.route("/contacts/count")
+def contacts_count():
+    count = Contact.count()
+    return "(" + str(count) + " total Contacts)"
+
+
+@app.route("/contacts/new", methods=['GET'])
+def contacts_new_get():
+    return render_template("new.html", contact=Contact())
+
+
+@app.route("/contacts/new", methods=['POST'])
+def contacts_new():
+    c = Contact(None, request.form['first_name'], request.form['last_name'], request.form['phone'],
+                request.form['email'])
+    if c.save():
+        flash("Created New Contact!")
+        return redirect("/contacts")
+    else:
+        return render_template("new.html", contact=c)
+
+
+@app.route("/contacts/<contact_id>")
+def contacts_view(contact_id=0):
+    contact = Contact.find(contact_id)
+    return render_template("show.html", contact=contact)
+
+
+@app.route("/contacts/<contact_id>/edit", methods=["GET"])
+def contacts_edit_get(contact_id=0):
+    contact = Contact.find(contact_id)
+    return render_template("edit.html", contact=contact)
+
+
+@app.route("/contacts/<contact_id>/edit", methods=["POST"])
+def contacts_edit_post(contact_id=0):
+    c = Contact.find(contact_id)
+    c.update(request.form['first_name'], request.form['last_name'], request.form['phone'], request.form['email'])
+    if c.save():
+        flash("Updated Contact!")
+        return redirect("/contacts/" + str(contact_id))
+    else:
+        return render_template("edit.html", contact=c)
+
+
+@app.route("/contacts/<contact_id>/email", methods=["GET"])
+def contacts_email_get(contact_id=0):
+    c = Contact.find(contact_id)
+    c.email = request.args.get('email')
+    c.validate()
+    return c.errors.get('email') or ""
+
+
+@app.route("/contacts/<contact_id>", methods=["DELETE"])
+def contacts_delete(contact_id=0):
+    contact = Contact.find(contact_id)
+    contact.delete()
+    if request.headers.get('HX-Trigger') == 'delete-btn':
+        flash("Deleted Contact!")
+        return redirect("/contacts", 303)
+    else:
+        return ""
+
+
+@app.route("/contacts/", methods=["DELETE"])
+def contacts_delete_all():
+    contact_ids = list(map(int, request.form.getlist("selected_contact_ids")))
+    for contact_id in contact_ids:
+        contact = Contact.find(contact_id)
+        contact.delete()
+    flash("Deleted Contacts!")
+    contacts_set = Contact.all(1)
+    return render_template("index.html", contacts=contacts_set)
+
+
+# ===========================================================
+# JSON Data API
+# ===========================================================
+
+@app.route("/api/v1/contacts", methods=["GET"])
+def json_contacts():
+    contacts_set = Contact.all()
+    return {"contacts": [c.__dict__ for c in contacts_set]}
+
+
+@app.route("/api/v1/contacts", methods=["POST"])
+def json_contacts_new():
+    c = Contact(None, request.form.get('first_name'), request.form.get('last_name'), request.form.get('phone'),
+                request.form.get('email'))
+    if c.save():
+        return c.__dict__
+    else:
+        return {"errors": c.errors}, 400
+
+
+@app.route("/api/v1/contacts/<contact_id>", methods=["GET"])
+def json_contacts_view(contact_id=0):
+    contact = Contact.find(contact_id)
+    return contact.__dict__
+
+
+@app.route("/api/v1/contacts/<contact_id>", methods=["PUT"])
+def json_contacts_edit(contact_id):
+    c = Contact.find(contact_id)
+    c.update(request.form['first_name'], request.form['last_name'], request.form['phone'], request.form['email'])
+    if c.save():
+        return c.__dict__
+    else:
+        return {"errors": c.errors}, 400
+
+
+@app.route("/api/v1/contacts/<contact_id>", methods=["DELETE"])
+def json_contacts_delete(contact_id=0):
+    contact = Contact.find(contact_id)
+    contact.delete()
+    return jsonify({"success": True})
+
+
+if __name__ == "__main__":
+    app.run()

+ 138 - 0
contactsapp/contacts.json

@@ -0,0 +1,138 @@
+[
+  {
+    "id": 2,
+    "first": "Carson",
+    "last": "Gross",
+    "phone": "123-456-7890",
+    "email": "carson@example.comz",
+    "errors": {}
+  },
+  {
+    "id": 3,
+    "first": "",
+    "last": "",
+    "phone": "",
+    "email": "joe@example2.com",
+    "errors": {}
+  },
+  {
+    "id": 5,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe@example.com",
+    "errors": {}
+  },
+  {
+    "id": 6,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe1@example.com",
+    "errors": {}
+  },
+  {
+    "id": 7,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe2@example.com",
+    "errors": {}
+  },
+  {
+    "id": 8,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe3@example.com",
+    "errors": {}
+  },
+  {
+    "id": 9,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe4@example.com",
+    "errors": {}
+  },
+  {
+    "id": 10,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe5@example.com",
+    "errors": {}
+  },
+  {
+    "id": 11,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe6@example.com",
+    "errors": {}
+  },
+  {
+    "id": 12,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe7@example.com",
+    "errors": {}
+  },
+  {
+    "id": 13,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe8@example.com",
+    "errors": {}
+  },
+  {
+    "id": 14,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe9@example.com",
+    "errors": {}
+  },
+  {
+    "id": 15,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe10@example.com",
+    "errors": {}
+  },
+  {
+    "id": 16,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe11@example.com",
+    "errors": {}
+  },
+  {
+    "id": 17,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe12@example.com",
+    "errors": {}
+  },
+  {
+    "id": 18,
+    "first": null,
+    "last": null,
+    "phone": null,
+    "email": "restexample1@example.com",
+    "errors": {}
+  },
+  {
+    "id": 19,
+    "first": null,
+    "last": null,
+    "phone": null,
+    "email": "restexample2@example.com",
+    "errors": {}
+  }
+]

+ 138 - 0
contactsapp/contacts0.json

@@ -0,0 +1,138 @@
+[
+  {
+    "id": 2,
+    "first": "Carson",
+    "last": "Gross",
+    "phone": "123-456-7890",
+    "email": "carson@example.comz",
+    "errors": {}
+  },
+  {
+    "id": 3,
+    "first": "",
+    "last": "",
+    "phone": "",
+    "email": "joe@example2.com",
+    "errors": {}
+  },
+  {
+    "id": 5,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe@example.com",
+    "errors": {}
+  },
+  {
+    "id": 6,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe1@example.com",
+    "errors": {}
+  },
+  {
+    "id": 7,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe2@example.com",
+    "errors": {}
+  },
+  {
+    "id": 8,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe3@example.com",
+    "errors": {}
+  },
+  {
+    "id": 9,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe4@example.com",
+    "errors": {}
+  },
+  {
+    "id": 10,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe5@example.com",
+    "errors": {}
+  },
+  {
+    "id": 11,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe6@example.com",
+    "errors": {}
+  },
+  {
+    "id": 12,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe7@example.com",
+    "errors": {}
+  },
+  {
+    "id": 13,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe8@example.com",
+    "errors": {}
+  },
+  {
+    "id": 14,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe9@example.com",
+    "errors": {}
+  },
+  {
+    "id": 15,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe10@example.com",
+    "errors": {}
+  },
+  {
+    "id": 16,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe11@example.com",
+    "errors": {}
+  },
+  {
+    "id": 17,
+    "first": "Joe",
+    "last": "Blow",
+    "phone": "123-456-7890",
+    "email": "joe12@example.com",
+    "errors": {}
+  },
+  {
+    "id": 18,
+    "first": null,
+    "last": null,
+    "phone": null,
+    "email": "restexample1@example.com",
+    "errors": {}
+  },
+  {
+    "id": 19,
+    "first": null,
+    "last": null,
+    "phone": null,
+    "email": "restexample2@example.com",
+    "errors": {}
+  }
+]

+ 334 - 0
contactsapp/contacts_model.go

@@ -0,0 +1,334 @@
+package main
+
+import (
+	"cmp"
+	"encoding/json"
+	"errors"
+	"log"
+	"math/rand/v2"
+	"os"
+	"slices"
+	"strings"
+	"sync"
+	"time"
+)
+
+// Contact model
+
+const (
+	PageSize = 100
+)
+
+type anyMap map[string]any
+
+/*
+# mock contacts database
+*/
+var db = make(map[uint64]Contact, 0)
+
+/*
+class Contact:
+*/
+type Contact struct {
+	ID     uint64
+	First  string
+	Last   string
+	Phone  string
+	Email  string
+	Errors []error `json:"-"`
+}
+
+/*
+def __init__(self, id_=None, first=None, last=None, phone=None, email=None):
+*/
+func NewContact(args anyMap) *Contact {
+	c := Contact{
+		Errors: make([]error, 0),
+	}
+	// Note: the "ok" are for the type assertions. The values are already set
+	// to the 0V (or a slice for errors) at this point anyway.
+	if id, ok := args["ID"].(uint64); ok {
+		c.ID = id
+	}
+	if first, ok := args["First"].(string); ok {
+		c.First = first
+	}
+	if last, ok := args["list"].(string); ok {
+		c.Last = last
+	}
+	if phone, ok := args["Phone"].(string); ok {
+		c.Phone = phone
+	}
+	if email, ok := args["Email"].(string); ok {
+		c.Email = email
+	}
+	return &c
+}
+
+/*
+def __str__(self):
+*/
+func (self *Contact) String() string {
+	bs, err := json.Marshal(*self)
+	if err != nil {
+		log.Println(err)
+	}
+	return string(bs)
+}
+
+/*
+def update(self, first, last, phone, email):
+*/
+func (self *Contact) Update(first, last, phone, email string) {
+	self.First = first
+	self.Last = last
+	self.Phone = phone
+	self.Email = email
+}
+
+/*
+def validate(self):
+*/
+func (self *Contact) validate() bool {
+	if self.Email == "" {
+		self.Errors = append(self.Errors, errors.New("Email required"))
+	}
+	for id, contact := range db {
+		if id != self.ID && contact.Email == self.Email {
+			self.Errors = append(self.Errors, errors.New("Email Must Be Unique"))
+			break
+		}
+	}
+	return len(self.Errors) == 0
+}
+
+/*
+def save(self):
+*/
+func (self *Contact) Save() bool {
+	if !self.validate() {
+		return false
+	}
+	var maxID uint64
+	if self.ID == 0 {
+		if len(db) == 0 {
+			maxID = 1
+		} else {
+			for id := range db {
+				if id > maxID {
+					maxID = id
+				}
+			}
+		}
+		self.ID = maxID + 1
+	}
+	SaveDB()
+	return true
+}
+
+/*
+def delete(self):
+*/
+func (self *Contact) Delete() {
+	delete(db, self.ID)
+}
+
+/*
+@classmethod
+def count(cls):
+*/
+func (*Contact) Count() int {
+	time.Sleep(2 * time.Second)
+	return len(db)
+}
+
+/*
+@classmethod
+def all(cls, page=1):
+*/
+func (*Contact) All(page int) []Contact {
+	if page == 0 {
+		page = 1
+	}
+	start := (page - 1) * PageSize
+	end := start + PageSize
+	ids := make([]uint64, 0, len(db))
+	for id := range db {
+		ids = append(ids, id)
+	}
+	slices.SortStableFunc(ids, cmp.Compare[uint64])
+	pageIDs := ids[start:end]
+	contacts := make([]Contact, 0, len(pageIDs))
+	for _, id := range pageIDs {
+		contacts = append(contacts, db[id])
+	}
+	return contacts
+}
+
+/*
+@classmethod
+def search(cls, text):
+*/
+func (*Contact) Search(text string) []Contact {
+	var result []Contact
+	for _, c := range db {
+		matchFirst := c.First != "" && strings.Contains(c.First, text)
+		matchLast := c.Last != "" && strings.Contains(c.Last, text)
+		matchEmail := c.Email != "" && strings.Contains(c.Email, text)
+		matchPhone := c.Phone != "" && strings.Contains(c.Phone, text)
+		if matchFirst || matchLast || matchEmail || matchPhone {
+			result = append(result, c)
+		}
+	}
+	return result
+}
+
+/*
+@classmethod
+def load_db(cls):
+*/
+func (*Contact) LoadDB() {
+	db = make(map[uint64]Contact, 0)
+	contacts := make([]Contact, 0)
+	bs, err := os.ReadFile((&Archiver{}).ArchiveFile())
+	if err != nil {
+		panic(err)
+	}
+	if err := json.Unmarshal(bs, &contacts); err != nil {
+		panic(err)
+	}
+	for _, c := range contacts {
+		db[c.ID] = c
+	}
+}
+
+/*
+@staticmethod
+def save_db():
+*/
+func SaveDB() {
+	contacts := make([]Contact, 0, len(db))
+	for _, contact := range db {
+		contacts = append(contacts, contact)
+	}
+	slices.SortStableFunc(contacts, func(a, b Contact) int {
+		return cmp.Compare(a.ID, b.ID)
+	})
+	f, err := os.Create((&Archiver{}).ArchiveFile())
+	if err != nil {
+		panic(err)
+	}
+	defer f.Close()
+	enc := json.NewEncoder(f)
+	if err := enc.Encode(contacts); err != nil {
+		panic(err)
+	}
+}
+
+/*
+@classmethod
+def find(cls, id_):
+*/
+func (*Contact) Find(id uint64) Contact {
+	c, ok := db[id]
+	// TODO shouldn't it be if !ok ? Original Python logic seems wrong.
+	if ok {
+		c.Errors = make([]error, 0)
+	}
+	return c
+}
+
+type Archiver struct {
+	archiveStatus   string
+	archiveProgress float64
+	sync.RWMutex
+}
+
+const (
+	statusWaiting  = "Waiting"
+	statusRunning  = "Running"
+	statusComplete = "Complete"
+)
+
+/*
+def status(self):
+*/
+func (self *Archiver) Status() string {
+	self.RLock()
+	defer self.RUnlock()
+	return self.archiveStatus
+}
+
+/*
+def progress(self):
+*/
+func (self *Archiver) Progress() float64 {
+	self.RLock()
+	defer self.RUnlock()
+	return self.archiveProgress
+}
+
+/*
+def run(self):
+*/
+func (self *Archiver) Run() {
+	self.Lock()
+	defer self.Unlock()
+	if self.archiveStatus == statusWaiting {
+		self.archiveStatus = statusRunning
+		self.archiveProgress = 0.0
+		go self.RunImpl()
+	}
+}
+
+/*
+def run_impl(self):
+*/
+func (self *Archiver) RunImpl() {
+	for i := range 10 {
+		time.Sleep(time.Duration(rand.Float64()) * time.Second)
+		self.Lock()
+		if self.archiveStatus != statusRunning {
+			self.Unlock()
+			return
+		}
+		self.archiveProgress = float64(i+1) / 10.0
+		log.Println("Here... ", self.archiveProgress)
+		self.Unlock()
+	}
+	time.Sleep(time.Second)
+	self.Lock()
+	if self.archiveStatus != statusRunning {
+		self.Unlock()
+		return
+	}
+	self.archiveStatus = statusComplete
+	self.Unlock()
+}
+
+/*
+def archive_file(self):
+*/
+func (self *Archiver) ArchiveFile() string {
+	return "contacts.json"
+}
+
+/*
+def reset(self):
+*/
+func (self *Archiver) Reset() {
+	self.Lock()
+	defer self.Unlock()
+	self.archiveStatus = statusWaiting
+	self.archiveProgress = 0.0
+}
+
+/*
+@classmethod
+def get(cls):
+*/
+func NewArchiver() *Archiver {
+	return &Archiver{
+		archiveStatus: statusWaiting,
+	}
+}

+ 145 - 0
contactsapp/contacts_model.py

@@ -0,0 +1,145 @@
+import json
+from operator import attrgetter
+import time
+from threading import Thread
+from random import random
+
+
+# ========================================================
+# Contact Model
+# ========================================================
+PAGE_SIZE = 100
+
+class Contact:
+    # mock contacts database
+    db = {}
+
+    def __init__(self, id_=None, first=None, last=None, phone=None, email=None):
+        self.id = id_
+        self.first = first
+        self.last = last
+        self.phone = phone
+        self.email = email
+        self.errors = {}
+
+    def __str__(self):
+        return json.dumps(self.__dict__, ensure_ascii=False)
+
+    def update(self, first, last, phone, email):
+        self.first = first
+        self.last = last
+        self.phone = phone
+        self.email = email
+
+    def validate(self):
+        if not self.email:
+            self.errors['email'] = "Email Required"
+        existing_contact = next(filter(lambda c: c.id != self.id and c.email == self.email, Contact.db.values()), None)
+        if existing_contact:
+            self.errors['email'] = "Email Must Be Unique"
+        return len(self.errors) == 0
+
+    def save(self):
+        if not self.validate():
+            return False
+        if self.id is None:
+            if len(Contact.db) == 0:
+                max_id = 1
+            else:
+                max_id = max(contact.id for contact in Contact.db.values())
+            self.id = max_id + 1
+            Contact.db[self.id] = self
+        Contact.save_db()
+        return True
+
+    def delete(self):
+        del Contact.db[self.id]
+        Contact.save_db()
+
+    @classmethod
+    def count(cls):
+        time.sleep(2)
+        return len(cls.db)
+
+    @classmethod
+    def all(cls, page=1):
+        page = int(page)
+        start = (page - 1) * PAGE_SIZE
+        end = start + PAGE_SIZE
+        return list(cls.db.values())[start:end]
+
+    @classmethod
+    def search(cls, text):
+        result = []
+        for c in cls.db.values():
+            match_first = c.first is not None and text in c.first
+            match_last = c.last is not None and text in c.last
+            match_email = c.email is not None and text in c.email
+            match_phone = c.phone is not None and text in c.phone
+            if match_first or match_last or match_email or match_phone:
+                result.append(c)
+        return result
+
+    @classmethod
+    def load_db(cls):
+        with open('contacts.json', 'r') as contacts_file:
+            contacts = json.load(contacts_file)
+            cls.db.clear()
+            for c in contacts:
+                cls.db[c['id']] = Contact(c['id'], c['first'], c['last'], c['phone'], c['email'])
+
+    @staticmethod
+    def save_db():
+        out_arr = [c.__dict__ for c in Contact.db.values()]
+        with open("contacts.json", "w") as f:
+            json.dump(out_arr, f, indent=2)
+
+    @classmethod
+    def find(cls, id_):
+        id_ = int(id_)
+        c = cls.db.get(id_)
+        if c is not None:
+            c.errors = {}
+
+        return c
+
+
+class Archiver:
+    archive_status = "Waiting"
+    archive_progress = 0
+    thread = None
+
+    def status(self):
+        return Archiver.archive_status
+
+    def progress(self):
+        return Archiver.archive_progress
+
+    def run(self):
+        if Archiver.archive_status == "Waiting":
+            Archiver.archive_status = "Running"
+            Archiver.archive_progress = 0
+            Archiver.thread = Thread(target=self.run_impl)
+            Archiver.thread.start()
+
+    def run_impl(self):
+        for i in range(10):
+            time.sleep(1 * random())
+            if Archiver.archive_status != "Running":
+                return
+            Archiver.archive_progress = (i + 1) / 10
+            print("Here... " + str(Archiver.archive_progress))
+        time.sleep(1)
+        if Archiver.archive_status != "Running":
+            return
+        Archiver.archive_status = "Complete"
+
+    def archive_file(self):
+        return "contacts.json"
+
+    def reset(self):
+        Archiver.archive_status = "Waiting"
+
+    @classmethod
+    def get(cls):
+        return Archiver()

+ 55 - 0
contactsapp/static/img/spinning-circles.svg

@@ -0,0 +1,55 @@
+<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
+<svg width="58" height="58" viewBox="0 0 58 58" xmlns="http://www.w3.org/2000/svg">
+    <g fill="none" fill-rule="evenodd">
+        <g transform="translate(2 1)" stroke="#000" stroke-width="1.5">
+            <circle cx="42.601" cy="11.462" r="5" fill-opacity="1" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="1;0;0;0;0;0;0;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="49.063" cy="27.063" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;1;0;0;0;0;0;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="42.601" cy="42.663" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;0;1;0;0;0;0;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="27" cy="49.125" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;0;0;1;0;0;0;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="11.399" cy="42.663" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;0;0;0;1;0;0;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="4.938" cy="27.063" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;0;0;0;0;1;0;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="11.399" cy="11.462" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;0;0;0;0;0;1;0" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+            <circle cx="27" cy="5" r="5" fill-opacity="0" fill="#000">
+                <animate attributeName="fill-opacity"
+                     begin="0s" dur="1.3s"
+                     values="0;0;0;0;0;0;0;1" calcMode="linear"
+                     repeatCount="indefinite" />
+            </circle>
+        </g>
+    </g>
+</svg>

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
contactsapp/static/js/_hyperscript-0.9.7.js


+ 3295 - 0
contactsapp/static/js/htmx-1.8.0.js

@@ -0,0 +1,3295 @@
+//AMD insanity
+(function (root, factory) {
+    //@ts-ignore
+    if (typeof define === 'function' && define.amd) {
+        // AMD. Register as an anonymous module.
+        //@ts-ignore
+        define([], factory);
+    } else {
+        // Browser globals
+        root.htmx = root.htmx || factory();
+    }
+}(typeof self !== 'undefined' ? self : this, function () {
+return (function () {
+        'use strict';
+
+        // Public API
+        //** @type {import("./htmx").HtmxApi} */
+        // TODO: list all methods in public API
+        var htmx = {
+            onLoad: onLoadHelper,
+            process: processNode,
+            on: addEventListenerImpl,
+            off: removeEventListenerImpl,
+            trigger : triggerEvent,
+            ajax : ajaxHelper,
+            find : find,
+            findAll : findAll,
+            closest : closest,
+            values : function(elt, type){
+                var inputValues = getInputValues(elt, type || "post");
+                return inputValues.values;
+            },
+            remove : removeElement,
+            addClass : addClassToElement,
+            removeClass : removeClassFromElement,
+            toggleClass : toggleClassOnElement,
+            takeClass : takeClassForElement,
+            defineExtension : defineExtension,
+            removeExtension : removeExtension,
+            logAll : logAll,
+            logger : null,
+            config : {
+                historyEnabled:true,
+                historyCacheSize:10,
+                refreshOnHistoryMiss:false,
+                defaultSwapStyle:'innerHTML',
+                defaultSwapDelay:0,
+                defaultSettleDelay:20,
+                includeIndicatorStyles:true,
+                indicatorClass:'htmx-indicator',
+                requestClass:'htmx-request',
+                addedClass:'htmx-added',
+                settlingClass:'htmx-settling',
+                swappingClass:'htmx-swapping',
+                allowEval:true,
+                inlineScriptNonce:'',
+                attributesToSettle:["class", "style", "width", "height"],
+                withCredentials:false,
+                timeout:0,
+                wsReconnectDelay: 'full-jitter',
+                disableSelector: "[hx-disable], [data-hx-disable]",
+                useTemplateFragments: false,
+                scrollBehavior: 'smooth',
+                defaultFocusScroll: false,
+            },
+            parseInterval:parseInterval,
+            _:internalEval,
+            createEventSource: function(url){
+                return new EventSource(url, {withCredentials:true})
+            },
+            createWebSocket: function(url){
+                return new WebSocket(url, []);
+            },
+            version: "1.8.0"
+        };
+
+        /** @type {import("./htmx").HtmxInternalApi} */
+        var internalAPI = {
+            addTriggerHandler: addTriggerHandler,
+            bodyContains: bodyContains,
+            canAccessLocalStorage: canAccessLocalStorage,
+            filterValues: filterValues,
+            hasAttribute: hasAttribute,
+            getAttributeValue: getAttributeValue,
+            getClosestMatch: getClosestMatch,
+            getExpressionVars: getExpressionVars,
+            getHeaders: getHeaders,
+            getInputValues: getInputValues,
+            getInternalData: getInternalData,
+            getSwapSpecification: getSwapSpecification,
+            getTriggerSpecs: getTriggerSpecs,
+            getTarget: getTarget,
+            makeFragment: makeFragment,
+            mergeObjects: mergeObjects,
+            makeSettleInfo: makeSettleInfo,
+            oobSwap: oobSwap,
+            selectAndSwap: selectAndSwap,
+            settleImmediately: settleImmediately,
+            shouldCancel: shouldCancel,
+            triggerEvent: triggerEvent,
+            triggerErrorEvent: triggerErrorEvent,
+            withExtensions: withExtensions,
+        }
+
+        var VERBS = ['get', 'post', 'put', 'delete', 'patch'];
+        var VERB_SELECTOR = VERBS.map(function(verb){
+            return "[hx-" + verb + "], [data-hx-" + verb + "]"
+        }).join(", ");
+
+        //====================================================================
+        // Utilities
+        //====================================================================
+
+        function parseInterval(str) {
+            if (str == undefined)  {
+                return undefined
+            }
+            if (str.slice(-2) == "ms") {
+                return parseFloat(str.slice(0,-2)) || undefined
+            }
+            if (str.slice(-1) == "s") {
+                return (parseFloat(str.slice(0,-1)) * 1000) || undefined
+            }
+            if (str.slice(-1) == "m") {
+                return (parseFloat(str.slice(0,-1)) * 1000 * 60) || undefined
+            }
+            return parseFloat(str) || undefined
+        }
+
+        /**
+         * @param {HTMLElement} elt
+         * @param {string} name
+         * @returns {(string | null)}
+         */
+        function getRawAttribute(elt, name) {
+            return elt.getAttribute && elt.getAttribute(name);
+        }
+
+        // resolve with both hx and data-hx prefixes
+        function hasAttribute(elt, qualifiedName) {
+            return elt.hasAttribute && (elt.hasAttribute(qualifiedName) ||
+                elt.hasAttribute("data-" + qualifiedName));
+        }
+
+        /**
+         *
+         * @param {HTMLElement} elt
+         * @param {string} qualifiedName
+         * @returns {(string | null)}
+         */
+        function getAttributeValue(elt, qualifiedName) {
+            return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName);
+        }
+
+        /**
+         * @param {HTMLElement} elt
+         * @returns {HTMLElement | null}
+         */
+        function parentElt(elt) {
+            return elt.parentElement;
+        }
+
+        /**
+         * @returns {Document}
+         */
+        function getDocument() {
+            return document;
+        }
+
+        /**
+         * @param {HTMLElement} elt
+         * @param {(e:HTMLElement) => boolean} condition
+         * @returns {HTMLElement | null}
+         */
+        function getClosestMatch(elt, condition) {
+            while (elt && !condition(elt)) {
+                elt = parentElt(elt);
+            }
+
+            return elt ? elt : null;
+        }
+
+        function getAttributeValueWithDisinheritance(initialElement, ancestor, attributeName){
+            var attributeValue = getAttributeValue(ancestor, attributeName);
+            var disinherit = getAttributeValue(ancestor, "hx-disinherit");
+            if (initialElement !== ancestor && disinherit && (disinherit === "*" || disinherit.split(" ").indexOf(attributeName) >= 0)) {
+                return "unset";
+            } else {
+                return attributeValue
+            }
+        }
+
+        /**
+         * @param {HTMLElement} elt
+         * @param {string} attributeName
+         * @returns {string | null}
+         */
+        function getClosestAttributeValue(elt, attributeName) {
+            var closestAttr = null;
+            getClosestMatch(elt, function (e) {
+                return closestAttr = getAttributeValueWithDisinheritance(elt, e, attributeName);
+            });
+            if (closestAttr !== "unset") {
+                return closestAttr;
+            }
+        }
+
+        /**
+         * @param {HTMLElement} elt
+         * @param {string} selector
+         * @returns {boolean}
+         */
+        function matches(elt, selector) {
+            // @ts-ignore: non-standard properties for browser compatability
+            // noinspection JSUnresolvedVariable
+            var matchesFunction = elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector;
+            return matchesFunction && matchesFunction.call(elt, selector);
+        }
+
+        /**
+         * @param {string} str
+         * @returns {string}
+         */
+        function getStartTag(str) {
+            var tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i
+            var match = tagMatcher.exec( str );
+            if (match) {
+                return match[1].toLowerCase();
+            } else {
+                return "";
+            }
+        }
+
+        /**
+         *
+         * @param {string} resp
+         * @param {number} depth
+         * @returns {Element}
+         */
+        function parseHTML(resp, depth) {
+            var parser = new DOMParser();
+            var responseDoc = parser.parseFromString(resp, "text/html");
+
+            /** @type {Element} */
+            var responseNode = responseDoc.body;
+            while (depth > 0) {
+                depth--;
+                // @ts-ignore
+                responseNode = responseNode.firstChild;
+            }
+            if (responseNode == null) {
+                // @ts-ignore
+                responseNode = getDocument().createDocumentFragment();
+            }
+            return responseNode;
+        }
+
+        /**
+         *
+         * @param {string} resp
+         * @returns {Element}
+         */
+        function makeFragment(resp) {
+            if (htmx.config.useTemplateFragments) {
+                var documentFragment = parseHTML("<body><template>" + resp + "</template></body>", 0);
+                // @ts-ignore type mismatch between DocumentFragment and Element.
+                // TODO: Are these close enough for htmx to use interchangably?
+                return documentFragment.querySelector('template').content;
+            } else {
+                var startTag = getStartTag(resp);
+                switch (startTag) {
+                    case "thead":
+                    case "tbody":
+                    case "tfoot":
+                    case "colgroup":
+                    case "caption":
+                        return parseHTML("<table>" + resp + "</table>", 1);
+                    case "col":
+                        return parseHTML("<table><colgroup>" + resp + "</colgroup></table>", 2);
+                    case "tr":
+                        return parseHTML("<table><tbody>" + resp + "</tbody></table>", 2);
+                    case "td":
+                    case "th":
+                        return parseHTML("<table><tbody><tr>" + resp + "</tr></tbody></table>", 3);
+                    case "script":
+                        return parseHTML("<div>" + resp + "</div>", 1);
+                    default:
+                        return parseHTML(resp, 0);
+                }
+            }
+        }
+
+        /**
+         * @param {Function} func
+         */
+        function maybeCall(func){
+            if(func) {
+                func();
+            }
+        }
+
+        /**
+         * @param {any} o
+         * @param {string} type
+         * @returns
+         */
+        function isType(o, type) {
+            return Object.prototype.toString.call(o) === "[object " + type + "]";
+        }
+
+        /**
+         * @param {*} o
+         * @returns {o is Function}
+         */
+        function isFunction(o) {
+            return isType(o, "Function");
+        }
+
+        /**
+         * @param {*} o
+         * @returns {o is Object}
+         */
+        function isRawObject(o) {
+            return isType(o, "Object");
+        }
+
+        /**
+         * getInternalData retrieves "private" data stored by htmx within an element
+         * @param {HTMLElement} elt
+         * @returns {*}
+         */
+        function getInternalData(elt) {
+            var dataProp = 'htmx-internal-data';
+            var data = elt[dataProp];
+            if (!data) {
+                data = elt[dataProp] = {};
+            }
+            return data;
+        }
+
+        /**
+         * toArray converts an ArrayLike object into a real array.
+         * @param {ArrayLike} arr
+         * @returns {any[]}
+         */
+        function toArray(arr) {
+            var returnArr = [];
+            if (arr) {
+                for (var i = 0; i < arr.length; i++) {
+                    returnArr.push(arr[i]);
+                }
+            }
+            return returnArr
+        }
+
+        function forEach(arr, func) {
+            if (arr) {
+                for (var i = 0; i < arr.length; i++) {
+                    func(arr[i]);
+                }
+            }
+        }
+
+        function isScrolledIntoView(el) {
+            var rect = el.getBoundingClientRect();
+            var elemTop = rect.top;
+            var elemBottom = rect.bottom;
+            return elemTop < window.innerHeight && elemBottom >= 0;
+        }
+
+	function bodyContains(elt) {
+	    if (elt.getRootNode() instanceof ShadowRoot) {
+		return getDocument().body.contains(elt.getRootNode().host);
+	    } else {
+		return getDocument().body.contains(elt);
+	    }
+	}
+
+        function splitOnWhitespace(trigger) {
+            return trigger.trim().split(/\s+/);
+        }
+
+        /**
+         * mergeObjects takes all of the keys from
+         * obj2 and duplicates them into obj1
+         * @param {Object} obj1
+         * @param {Object} obj2
+         * @returns {Object}
+         */
+        function mergeObjects(obj1, obj2) {
+            for (var key in obj2) {
+                if (obj2.hasOwnProperty(key)) {
+                    obj1[key] = obj2[key];
+                }
+            }
+            return obj1;
+        }
+
+        function parseJSON(jString) {
+            try {
+                return JSON.parse(jString);
+            } catch(error) {
+                logError(error);
+                return null;
+            }
+        }
+
+        function canAccessLocalStorage() {
+            var test = 'htmx:localStorageTest';
+            try {
+                localStorage.setItem(test, test);
+                localStorage.removeItem(test);
+                return true;
+            } catch(e) {
+                return false;
+            }
+        }
+
+        //==========================================================================================
+        // public API
+        //==========================================================================================
+
+        function internalEval(str){
+            return maybeEval(getDocument().body, function () {
+                return eval(str);
+            });
+        }
+
+        function onLoadHelper(callback) {
+            var value = htmx.on("htmx:load", function(evt) {
+                callback(evt.detail.elt);
+            });
+            return value;
+        }
+
+        function logAll(){
+            htmx.logger = function(elt, event, data) {
+                if(console) {
+                    console.log(event, elt, data);
+                }
+            }
+        }
+
+        function find(eltOrSelector, selector) {
+            if (selector) {
+                return eltOrSelector.querySelector(selector);
+            } else {
+                return find(getDocument(), eltOrSelector);
+            }
+        }
+
+        function findAll(eltOrSelector, selector) {
+            if (selector) {
+                return eltOrSelector.querySelectorAll(selector);
+            } else {
+                return findAll(getDocument(), eltOrSelector);
+            }
+        }
+
+        function removeElement(elt, delay) {
+            elt = resolveTarget(elt);
+            if (delay) {
+                setTimeout(function(){removeElement(elt);}, delay)
+            } else {
+                elt.parentElement.removeChild(elt);
+            }
+        }
+
+        function addClassToElement(elt, clazz, delay) {
+            elt = resolveTarget(elt);
+            if (delay) {
+                setTimeout(function(){addClassToElement(elt, clazz);}, delay)
+            } else {
+                elt.classList && elt.classList.add(clazz);
+            }
+        }
+
+        function removeClassFromElement(elt, clazz, delay) {
+            elt = resolveTarget(elt);
+            if (delay) {
+                setTimeout(function(){removeClassFromElement(elt, clazz);}, delay)
+            } else {
+                if (elt.classList) {
+                    elt.classList.remove(clazz);
+                    // if there are no classes left, remove the class attribute
+                    if (elt.classList.length === 0) {
+                        elt.removeAttribute("class");
+                    }
+                }
+            }
+        }
+
+        function toggleClassOnElement(elt, clazz) {
+            elt = resolveTarget(elt);
+            elt.classList.toggle(clazz);
+        }
+
+        function takeClassForElement(elt, clazz) {
+            elt = resolveTarget(elt);
+            forEach(elt.parentElement.children, function(child){
+                removeClassFromElement(child, clazz);
+            })
+            addClassToElement(elt, clazz);
+        }
+
+        function closest(elt, selector) {
+            elt = resolveTarget(elt);
+            if (elt.closest) {
+                return elt.closest(selector);
+            } else {
+                do{
+                    if (elt == null || matches(elt, selector)){
+                        return elt;
+                    }
+                }
+                while (elt = elt && parentElt(elt));
+            }
+        }
+
+        function querySelectorAllExt(elt, selector) {
+            if (selector.indexOf("closest ") === 0) {
+                return [closest(elt, selector.substr(8))];
+            } else if (selector.indexOf("find ") === 0) {
+                return [find(elt, selector.substr(5))];
+            } else if (selector.indexOf("next ") === 0) {
+                return [scanForwardQuery(elt, selector.substr(5))];
+            } else if (selector.indexOf("previous ") === 0) {
+                return [scanBackwardsQuery(elt, selector.substr(9))];
+            } else if (selector === 'document') {
+                return [document];
+            } else if (selector === 'window') {
+                return [window];
+            } else {
+                return getDocument().querySelectorAll(selector);
+            }
+        }
+
+        var scanForwardQuery = function(start, match) {
+            var results = getDocument().querySelectorAll(match);
+            for (var i = 0; i < results.length; i++) {
+                var elt = results[i];
+                if (elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_PRECEDING) {
+                    return elt;
+                }
+            }
+        }
+
+        var scanBackwardsQuery = function(start, match) {
+            var results = getDocument().querySelectorAll(match);
+            for (var i = results.length - 1; i >= 0; i--) {
+                var elt = results[i];
+                if (elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_FOLLOWING) {
+                    return elt;
+                }
+            }
+        }
+
+        function querySelectorExt(eltOrSelector, selector) {
+            if (selector) {
+                return querySelectorAllExt(eltOrSelector, selector)[0];
+            } else {
+                return querySelectorAllExt(getDocument().body, eltOrSelector)[0];
+            }
+        }
+
+        function resolveTarget(arg2) {
+            if (isType(arg2, 'String')) {
+                return find(arg2);
+            } else {
+                return arg2;
+            }
+        }
+
+        function processEventArgs(arg1, arg2, arg3) {
+            if (isFunction(arg2)) {
+                return {
+                    target: getDocument().body,
+                    event: arg1,
+                    listener: arg2
+                }
+            } else {
+                return {
+                    target: resolveTarget(arg1),
+                    event: arg2,
+                    listener: arg3
+                }
+            }
+
+        }
+
+        function addEventListenerImpl(arg1, arg2, arg3) {
+            ready(function(){
+                var eventArgs = processEventArgs(arg1, arg2, arg3);
+                eventArgs.target.addEventListener(eventArgs.event, eventArgs.listener);
+            })
+            var b = isFunction(arg2);
+            return b ? arg2 : arg3;
+        }
+
+        function removeEventListenerImpl(arg1, arg2, arg3) {
+            ready(function(){
+                var eventArgs = processEventArgs(arg1, arg2, arg3);
+                eventArgs.target.removeEventListener(eventArgs.event, eventArgs.listener);
+            })
+            return isFunction(arg2) ? arg2 : arg3;
+        }
+
+        //====================================================================
+        // Node processing
+        //====================================================================
+
+        var DUMMY_ELT = getDocument().createElement("output"); // dummy element for bad selectors
+        function findAttributeTargets(elt, attrName) {
+            var attrTarget = getClosestAttributeValue(elt, attrName);
+            if (attrTarget) {
+                if (attrTarget === "this") {
+                    return [findThisElement(elt, attrName)];
+                } else {
+                    var result = querySelectorAllExt(elt, attrTarget);
+                    if (result.length === 0) {
+                        logError('The selector "' + attrTarget + '" on ' + attrName + " returned no matches!");
+                        return [DUMMY_ELT]
+                    } else {
+                        return result;
+                    }
+                }
+            }
+        }
+
+        function findThisElement(elt, attribute){
+            return getClosestMatch(elt, function (elt) {
+                return getAttributeValue(elt, attribute) != null;
+            })
+        }
+
+        function getTarget(elt) {
+            var targetStr = getClosestAttributeValue(elt, "hx-target");
+            if (targetStr) {
+                if (targetStr === "this") {
+                    return findThisElement(elt,'hx-target');
+                } else {
+                    return querySelectorExt(elt, targetStr)
+                }
+            } else {
+                var data = getInternalData(elt);
+                if (data.boosted) {
+                    return getDocument().body;
+                } else {
+                    return elt;
+                }
+            }
+        }
+
+        function shouldSettleAttribute(name) {
+            var attributesToSettle = htmx.config.attributesToSettle;
+            for (var i = 0; i < attributesToSettle.length; i++) {
+                if (name === attributesToSettle[i]) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        function cloneAttributes(mergeTo, mergeFrom) {
+            forEach(mergeTo.attributes, function (attr) {
+                if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) {
+                    mergeTo.removeAttribute(attr.name)
+                }
+            });
+            forEach(mergeFrom.attributes, function (attr) {
+                if (shouldSettleAttribute(attr.name)) {
+                    mergeTo.setAttribute(attr.name, attr.value);
+                }
+            });
+        }
+
+        function isInlineSwap(swapStyle, target) {
+            var extensions = getExtensions(target);
+            for (var i = 0; i < extensions.length; i++) {
+                var extension = extensions[i];
+                try {
+                    if (extension.isInlineSwap(swapStyle)) {
+                        return true;
+                    }
+                } catch(e) {
+                    logError(e);
+                }
+            }
+            return swapStyle === "outerHTML";
+        }
+
+        /**
+         *
+         * @param {string} oobValue
+         * @param {HTMLElement} oobElement
+         * @param {*} settleInfo
+         * @returns
+         */
+        function oobSwap(oobValue, oobElement, settleInfo) {
+            var selector = "#" + oobElement.id;
+            var swapStyle = "outerHTML";
+            if (oobValue === "true") {
+                // do nothing
+            } else if (oobValue.indexOf(":") > 0) {
+                swapStyle = oobValue.substr(0, oobValue.indexOf(":"));
+                selector  = oobValue.substr(oobValue.indexOf(":") + 1, oobValue.length);
+            } else {
+                swapStyle = oobValue;
+            }
+
+            var targets = getDocument().querySelectorAll(selector);
+            if (targets) {
+                forEach(
+                    targets,
+                    function (target) {
+                        var fragment;
+                        var oobElementClone = oobElement.cloneNode(true);
+                        fragment = getDocument().createDocumentFragment();
+                        fragment.appendChild(oobElementClone);
+                        if (!isInlineSwap(swapStyle, target)) {
+                            fragment = oobElementClone; // if this is not an inline swap, we use the content of the node, not the node itself
+                        }
+
+                        var beforeSwapDetails = {shouldSwap: true, target: target, fragment:fragment };
+                        if (!triggerEvent(target, 'htmx:oobBeforeSwap', beforeSwapDetails)) return;
+
+                        target = beforeSwapDetails.target; // allow re-targeting
+                        if (beforeSwapDetails['shouldSwap']){
+                            swap(swapStyle, target, target, fragment, settleInfo);
+                        }
+                        forEach(settleInfo.elts, function (elt) {
+                            triggerEvent(elt, 'htmx:oobAfterSwap', beforeSwapDetails);
+                        });
+                    }
+                );
+                oobElement.parentNode.removeChild(oobElement);
+            } else {
+                oobElement.parentNode.removeChild(oobElement);
+                triggerErrorEvent(getDocument().body, "htmx:oobErrorNoTarget", {content: oobElement});
+            }
+            return oobValue;
+        }
+
+        function handleOutOfBandSwaps(elt, fragment, settleInfo) {
+            var oobSelects = getClosestAttributeValue(elt, "hx-select-oob");
+            if (oobSelects) {
+                var oobSelectValues = oobSelects.split(",");
+                for (let i = 0; i < oobSelectValues.length; i++) {
+                    var oobSelectValue = oobSelectValues[i].split(":", 2);
+                    var id = oobSelectValue[0];
+                    if (id.indexOf("#") === 0) {
+                        id = id.substring(1);
+                    }
+                    var oobValue = oobSelectValue[1] || "true";
+                    var oobElement = fragment.querySelector("#" + id);
+                    if (oobElement) {
+                        oobSwap(oobValue, oobElement, settleInfo);
+                    }
+                }
+            }
+            forEach(findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]'), function (oobElement) {
+                var oobValue = getAttributeValue(oobElement, "hx-swap-oob");
+                if (oobValue != null) {
+                    oobSwap(oobValue, oobElement, settleInfo);
+                }
+            });
+        }
+
+        function handlePreservedElements(fragment) {
+            forEach(findAll(fragment, '[hx-preserve], [data-hx-preserve]'), function (preservedElt) {
+                var id = getAttributeValue(preservedElt, "id");
+                var oldElt = getDocument().getElementById(id);
+                if (oldElt != null) {
+                    preservedElt.parentNode.replaceChild(oldElt, preservedElt);
+                }
+            });
+        }
+
+        function handleAttributes(parentNode, fragment, settleInfo) {
+            forEach(fragment.querySelectorAll("[id]"), function (newNode) {
+                if (newNode.id && newNode.id.length > 0) {
+                    var oldNode = parentNode.querySelector(newNode.tagName + "[id='" + newNode.id + "']");
+                    if (oldNode && oldNode !== parentNode) {
+                        var newAttributes = newNode.cloneNode();
+                        cloneAttributes(newNode, oldNode);
+                        settleInfo.tasks.push(function () {
+                            cloneAttributes(newNode, newAttributes);
+                        });
+                    }
+                }
+            });
+        }
+
+        function makeAjaxLoadTask(child) {
+            return function () {
+                removeClassFromElement(child, htmx.config.addedClass);
+                processNode(child);
+                processScripts(child);
+                processFocus(child)
+                triggerEvent(child, 'htmx:load');
+            };
+        }
+
+        function processFocus(child) {
+            var autofocus = "[autofocus]";
+            var autoFocusedElt = matches(child, autofocus) ? child : child.querySelector(autofocus)
+            if (autoFocusedElt != null) {
+                autoFocusedElt.focus();
+            }
+        }
+
+        function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
+            handleAttributes(parentNode, fragment, settleInfo);
+            while(fragment.childNodes.length > 0){
+                var child = fragment.firstChild;
+                addClassToElement(child, htmx.config.addedClass);
+                parentNode.insertBefore(child, insertBefore);
+                if (child.nodeType !== Node.TEXT_NODE && child.nodeType !== Node.COMMENT_NODE) {
+                    settleInfo.tasks.push(makeAjaxLoadTask(child));
+                }
+            }
+        }
+
+        function cleanUpElement(element) {
+            var internalData = getInternalData(element);
+            if (internalData.webSocket) {
+                internalData.webSocket.close();
+            }
+            if (internalData.sseEventSource) {
+                internalData.sseEventSource.close();
+            }
+
+            triggerEvent(element, "htmx:beforeCleanupElement")
+
+            if (internalData.listenerInfos) {
+                forEach(internalData.listenerInfos, function(info) {
+                    if (element !== info.on) {
+                        info.on.removeEventListener(info.trigger, info.listener);
+                    }
+                });
+            }
+            if (element.children) { // IE
+                forEach(element.children, function(child) { cleanUpElement(child) });
+            }
+        }
+
+        function swapOuterHTML(target, fragment, settleInfo) {
+            if (target.tagName === "BODY") {
+                return swapInnerHTML(target, fragment, settleInfo);
+            } else {
+                // @type {HTMLElement}
+                var newElt
+                var eltBeforeNewContent = target.previousSibling;
+                insertNodesBefore(parentElt(target), target, fragment, settleInfo);
+                if (eltBeforeNewContent == null) {
+                    newElt = parentElt(target).firstChild;
+                } else {
+                    newElt = eltBeforeNewContent.nextSibling;
+                }
+                getInternalData(target).replacedWith = newElt; // tuck away so we can fire events on it later
+                settleInfo.elts = [] // clear existing elements
+                while(newElt && newElt !== target) {
+                    if (newElt.nodeType === Node.ELEMENT_NODE) {
+                        settleInfo.elts.push(newElt);
+                    }
+                    newElt = newElt.nextElementSibling;
+                }
+                cleanUpElement(target);
+                parentElt(target).removeChild(target);
+            }
+        }
+
+        function swapAfterBegin(target, fragment, settleInfo) {
+            return insertNodesBefore(target, target.firstChild, fragment, settleInfo);
+        }
+
+        function swapBeforeBegin(target, fragment, settleInfo) {
+            return insertNodesBefore(parentElt(target), target, fragment, settleInfo);
+        }
+
+        function swapBeforeEnd(target, fragment, settleInfo) {
+            return insertNodesBefore(target, null, fragment, settleInfo);
+        }
+
+        function swapAfterEnd(target, fragment, settleInfo) {
+            return insertNodesBefore(parentElt(target), target.nextSibling, fragment, settleInfo);
+        }
+        function swapDelete(target, fragment, settleInfo) {
+            cleanUpElement(target);
+            return parentElt(target).removeChild(target);
+        }
+
+        function swapInnerHTML(target, fragment, settleInfo) {
+            var firstChild = target.firstChild;
+            insertNodesBefore(target, firstChild, fragment, settleInfo);
+            if (firstChild) {
+                while (firstChild.nextSibling) {
+                    cleanUpElement(firstChild.nextSibling)
+                    target.removeChild(firstChild.nextSibling);
+                }
+                cleanUpElement(firstChild)
+                target.removeChild(firstChild);
+            }
+        }
+
+        function maybeSelectFromResponse(elt, fragment) {
+            var selector = getClosestAttributeValue(elt, "hx-select");
+            if (selector) {
+                var newFragment = getDocument().createDocumentFragment();
+                forEach(fragment.querySelectorAll(selector), function (node) {
+                    newFragment.appendChild(node);
+                });
+                fragment = newFragment;
+            }
+            return fragment;
+        }
+
+        function swap(swapStyle, elt, target, fragment, settleInfo) {
+            switch (swapStyle) {
+                case "none":
+                    return;
+                case "outerHTML":
+                    swapOuterHTML(target, fragment, settleInfo);
+                    return;
+                case "afterbegin":
+                    swapAfterBegin(target, fragment, settleInfo);
+                    return;
+                case "beforebegin":
+                    swapBeforeBegin(target, fragment, settleInfo);
+                    return;
+                case "beforeend":
+                    swapBeforeEnd(target, fragment, settleInfo);
+                    return;
+                case "afterend":
+                    swapAfterEnd(target, fragment, settleInfo);
+                    return;
+                case "delete":
+                    swapDelete(target, fragment, settleInfo);
+                    return;
+                default:
+                    var extensions = getExtensions(elt);
+                    for (var i = 0; i < extensions.length; i++) {
+                        var ext = extensions[i];
+                        try {
+                            var newElements = ext.handleSwap(swapStyle, target, fragment, settleInfo);
+                            if (newElements) {
+                                if (typeof newElements.length !== 'undefined') {
+                                    // if handleSwap returns an array (like) of elements, we handle them
+                                    for (var j = 0; j < newElements.length; j++) {
+                                        var child = newElements[j];
+                                        if (child.nodeType !== Node.TEXT_NODE && child.nodeType !== Node.COMMENT_NODE) {
+                                            settleInfo.tasks.push(makeAjaxLoadTask(child));
+                                        }
+                                    }
+                                }
+                                return;
+                            }
+                        } catch (e) {
+                            logError(e);
+                        }
+                    }
+                    if (swapStyle === "innerHTML") {
+                        swapInnerHTML(target, fragment, settleInfo);
+                    } else {
+                        swap(htmx.config.defaultSwapStyle, elt, target, fragment, settleInfo);
+                    }
+            }
+        }
+
+        function findTitle(content) {
+            if (content.indexOf('<title') > -1) {
+                var contentWithSvgsRemoved = content.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
+                var result = contentWithSvgsRemoved.match(/<title(\s[^>]*>|>)([\s\S]*?)<\/title>/im);
+
+                if (result) {
+                    return result[2];
+                }
+            }
+        }
+
+        function selectAndSwap(swapStyle, target, elt, responseText, settleInfo) {
+            settleInfo.title = findTitle(responseText);
+            var fragment = makeFragment(responseText);
+            if (fragment) {
+                handleOutOfBandSwaps(elt, fragment, settleInfo);
+                fragment = maybeSelectFromResponse(elt, fragment);
+                handlePreservedElements(fragment);
+                return swap(swapStyle, elt, target, fragment, settleInfo);
+            }
+        }
+
+        function handleTrigger(xhr, header, elt) {
+            var triggerBody = xhr.getResponseHeader(header);
+            if (triggerBody.indexOf("{") === 0) {
+                var triggers = parseJSON(triggerBody);
+                for (var eventName in triggers) {
+                    if (triggers.hasOwnProperty(eventName)) {
+                        var detail = triggers[eventName];
+                        if (!isRawObject(detail)) {
+                            detail = {"value": detail}
+                        }
+                        triggerEvent(elt, eventName, detail);
+                    }
+                }
+            } else {
+                triggerEvent(elt, triggerBody, []);
+            }
+        }
+
+        var WHITESPACE = /\s/;
+        var WHITESPACE_OR_COMMA = /[\s,]/;
+        var SYMBOL_START = /[_$a-zA-Z]/;
+        var SYMBOL_CONT = /[_$a-zA-Z0-9]/;
+        var STRINGISH_START = ['"', "'", "/"];
+        var NOT_WHITESPACE = /[^\s]/;
+        function tokenizeString(str) {
+            var tokens = [];
+            var position = 0;
+            while (position < str.length) {
+                if(SYMBOL_START.exec(str.charAt(position))) {
+                    var startPosition = position;
+                    while (SYMBOL_CONT.exec(str.charAt(position + 1))) {
+                        position++;
+                    }
+                    tokens.push(str.substr(startPosition, position - startPosition + 1));
+                } else if (STRINGISH_START.indexOf(str.charAt(position)) !== -1) {
+                    var startChar = str.charAt(position);
+                    var startPosition = position;
+                    position++;
+                    while (position < str.length && str.charAt(position) !== startChar ) {
+                        if (str.charAt(position) === "\\") {
+                            position++;
+                        }
+                        position++;
+                    }
+                    tokens.push(str.substr(startPosition, position - startPosition + 1));
+                } else {
+                    var symbol = str.charAt(position);
+                    tokens.push(symbol);
+                }
+                position++;
+            }
+            return tokens;
+        }
+
+        function isPossibleRelativeReference(token, last, paramName) {
+            return SYMBOL_START.exec(token.charAt(0)) &&
+                token !== "true" &&
+                token !== "false" &&
+                token !== "this" &&
+                token !== paramName &&
+                last !== ".";
+        }
+
+        function maybeGenerateConditional(elt, tokens, paramName) {
+            if (tokens[0] === '[') {
+                tokens.shift();
+                var bracketCount = 1;
+                var conditionalSource = " return (function(" + paramName + "){ return (";
+                var last = null;
+                while (tokens.length > 0) {
+                    var token = tokens[0];
+                    if (token === "]") {
+                        bracketCount--;
+                        if (bracketCount === 0) {
+                            if (last === null) {
+                                conditionalSource = conditionalSource + "true";
+                            }
+                            tokens.shift();
+                            conditionalSource += ")})";
+                            try {
+                                var conditionFunction = maybeEval(elt,function () {
+                                    return Function(conditionalSource)();
+                                    },
+                                    function(){return true})
+                                conditionFunction.source = conditionalSource;
+                                return conditionFunction;
+                            } catch (e) {
+                                triggerErrorEvent(getDocument().body, "htmx:syntax:error", {error:e, source:conditionalSource})
+                                return null;
+                            }
+                        }
+                    } else if (token === "[") {
+                        bracketCount++;
+                    }
+                    if (isPossibleRelativeReference(token, last, paramName)) {
+                            conditionalSource += "((" + paramName + "." + token + ") ? (" + paramName + "." + token + ") : (window." + token + "))";
+                    } else {
+                        conditionalSource = conditionalSource + token;
+                    }
+                    last = tokens.shift();
+                }
+            }
+        }
+
+        function consumeUntil(tokens, match) {
+            var result = "";
+            while (tokens.length > 0 && !tokens[0].match(match)) {
+                result += tokens.shift();
+            }
+            return result;
+        }
+
+        var INPUT_SELECTOR = 'input, textarea, select';
+
+        /**
+         * @param {HTMLElement} elt
+         * @returns {import("./htmx").HtmxTriggerSpecification[]}
+         */
+        function getTriggerSpecs(elt) {
+            var explicitTrigger = getAttributeValue(elt, 'hx-trigger');
+            var triggerSpecs = [];
+            if (explicitTrigger) {
+                var tokens = tokenizeString(explicitTrigger);
+                do {
+                    consumeUntil(tokens, NOT_WHITESPACE);
+                    var initialLength = tokens.length;
+                    var trigger = consumeUntil(tokens, /[,\[\s]/);
+                    if (trigger !== "") {
+                        if (trigger === "every") {
+                            var every = {trigger: 'every'};
+                            consumeUntil(tokens, NOT_WHITESPACE);
+                            every.pollInterval = parseInterval(consumeUntil(tokens, /[,\[\s]/));
+                            consumeUntil(tokens, NOT_WHITESPACE);
+                            var eventFilter = maybeGenerateConditional(elt, tokens, "event");
+                            if (eventFilter) {
+                                every.eventFilter = eventFilter;
+                            }
+                            triggerSpecs.push(every);
+                        } else if (trigger.indexOf("sse:") === 0) {
+                            triggerSpecs.push({trigger: 'sse', sseEvent: trigger.substr(4)});
+                        } else {
+                            var triggerSpec = {trigger: trigger};
+                            var eventFilter = maybeGenerateConditional(elt, tokens, "event");
+                            if (eventFilter) {
+                                triggerSpec.eventFilter = eventFilter;
+                            }
+                            while (tokens.length > 0 && tokens[0] !== ",") {
+                                consumeUntil(tokens, NOT_WHITESPACE)
+                                var token = tokens.shift();
+                                if (token === "changed") {
+                                    triggerSpec.changed = true;
+                                } else if (token === "once") {
+                                    triggerSpec.once = true;
+                                } else if (token === "consume") {
+                                    triggerSpec.consume = true;
+                                } else if (token === "delay" && tokens[0] === ":") {
+                                    tokens.shift();
+                                    triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
+                                } else if (token === "from" && tokens[0] === ":") {
+                                    tokens.shift();
+                                    var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
+                                    if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
+                                        tokens.shift();
+                                        from_arg +=
+                                            " " +
+                                            consumeUntil(
+                                                tokens,
+                                                WHITESPACE_OR_COMMA
+                                            );
+                                    }
+                                    triggerSpec.from = from_arg;
+                                } else if (token === "target" && tokens[0] === ":") {
+                                    tokens.shift();
+                                    triggerSpec.target = consumeUntil(tokens, WHITESPACE_OR_COMMA);
+                                } else if (token === "throttle" && tokens[0] === ":") {
+                                    tokens.shift();
+                                    triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
+                                } else if (token === "queue" && tokens[0] === ":") {
+                                    tokens.shift();
+                                    triggerSpec.queue = consumeUntil(tokens, WHITESPACE_OR_COMMA);
+                                } else if ((token === "root" || token === "threshold") && tokens[0] === ":") {
+                                    tokens.shift();
+                                    triggerSpec[token] = consumeUntil(tokens, WHITESPACE_OR_COMMA);
+                                } else {
+                                    triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
+                                }
+                            }
+                            triggerSpecs.push(triggerSpec);
+                        }
+                    }
+                    if (tokens.length === initialLength) {
+                        triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
+                    }
+                    consumeUntil(tokens, NOT_WHITESPACE);
+                } while (tokens[0] === "," && tokens.shift())
+            }
+
+            if (triggerSpecs.length > 0) {
+                return triggerSpecs;
+            } else if (matches(elt, 'form')) {
+                return [{trigger: 'submit'}];
+            } else if (matches(elt, 'input[type="button"]')){
+                return [{trigger: 'click'}];
+            } else if (matches(elt, INPUT_SELECTOR)) {
+                return [{trigger: 'change'}];
+            } else {
+                return [{trigger: 'click'}];
+            }
+        }
+
+        function cancelPolling(elt) {
+            getInternalData(elt).cancelled = true;
+        }
+
+        function processPolling(elt, handler, spec) {
+            var nodeData = getInternalData(elt);
+            nodeData.timeout = setTimeout(function () {
+                if (bodyContains(elt) && nodeData.cancelled !== true) {
+                    if (!maybeFilterEvent(spec, makeEvent('hx:poll:trigger', {triggerSpec:spec, target:elt}))) {
+                        handler(elt);
+                    }
+                    processPolling(elt, handler, spec);
+                }
+            }, spec.pollInterval);
+        }
+
+        function isLocalLink(elt) {
+            return location.hostname === elt.hostname &&
+                getRawAttribute(elt,'href') &&
+                getRawAttribute(elt,'href').indexOf("#") !== 0;
+        }
+
+        function boostElement(elt, nodeData, triggerSpecs) {
+            if ((elt.tagName === "A" && isLocalLink(elt) && (elt.target === "" || elt.target === "_self")) || elt.tagName === "FORM") {
+                nodeData.boosted = true;
+                var verb, path;
+                if (elt.tagName === "A") {
+                    verb = "get";
+                    path = getRawAttribute(elt, 'href');
+                } else {
+                    var rawAttribute = getRawAttribute(elt, "method");
+                    verb = rawAttribute ? rawAttribute.toLowerCase() : "get";
+                    if (verb === "get") {
+                    }
+                    path = getRawAttribute(elt, 'action');
+                }
+                triggerSpecs.forEach(function(triggerSpec) {
+                    addEventListener(elt, function(evt) {
+                        issueAjaxRequest(verb, path, elt, evt)
+                    }, nodeData, triggerSpec, true);
+                });
+            }
+        }
+
+        /**
+         *
+         * @param {Event} evt
+         * @param {HTMLElement} elt
+         * @returns
+         */
+        function shouldCancel(evt, elt) {
+            if (evt.type === "submit" || evt.type === "click") {
+                if (elt.tagName === "FORM") {
+                    return true;
+                }
+                if (matches(elt, 'input[type="submit"], button') && closest(elt, 'form') !== null) {
+                    return true;
+                }
+                if (elt.tagName === "A" && elt.href &&
+                    (elt.getAttribute('href') === '#' || elt.getAttribute('href').indexOf("#") !== 0)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        function ignoreBoostedAnchorCtrlClick(elt, evt) {
+            return getInternalData(elt).boosted && elt.tagName === "A" && evt.type === "click" && (evt.ctrlKey || evt.metaKey);
+        }
+
+        function maybeFilterEvent(triggerSpec, evt) {
+            var eventFilter = triggerSpec.eventFilter;
+            if(eventFilter){
+                try {
+                    return eventFilter(evt) !== true;
+                } catch(e) {
+                    triggerErrorEvent(getDocument().body, "htmx:eventFilter:error", {error: e, source:eventFilter.source});
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        function addEventListener(elt, handler, nodeData, triggerSpec, explicitCancel) {
+            var eltsToListenOn;
+            if (triggerSpec.from) {
+                eltsToListenOn = querySelectorAllExt(elt, triggerSpec.from);
+            } else {
+                eltsToListenOn = [elt];
+            }
+            forEach(eltsToListenOn, function (eltToListenOn) {
+                var eventListener = function (evt) {
+                    if (!bodyContains(elt)) {
+                        eltToListenOn.removeEventListener(triggerSpec.trigger, eventListener);
+                        return;
+                    }
+                    if (ignoreBoostedAnchorCtrlClick(elt, evt)) {
+                        return;
+                    }
+                    if (explicitCancel || shouldCancel(evt, elt)) {
+                        evt.preventDefault();
+                    }
+                    if (maybeFilterEvent(triggerSpec, evt)) {
+                        return;
+                    }
+                    var eventData = getInternalData(evt);
+                    eventData.triggerSpec = triggerSpec;
+                    if (eventData.handledFor == null) {
+                        eventData.handledFor = [];
+                    }
+                    var elementData = getInternalData(elt);
+                    if (eventData.handledFor.indexOf(elt) < 0) {
+                        eventData.handledFor.push(elt);
+                        if (triggerSpec.consume) {
+                            evt.stopPropagation();
+                        }
+                        if (triggerSpec.target && evt.target) {
+                            if (!matches(evt.target, triggerSpec.target)) {
+                                return;
+                            }
+                        }
+                        if (triggerSpec.once) {
+                            if (elementData.triggeredOnce) {
+                                return;
+                            } else {
+                                elementData.triggeredOnce = true;
+                            }
+                        }
+                        if (triggerSpec.changed) {
+                            if (elementData.lastValue === elt.value) {
+                                return;
+                            } else {
+                                elementData.lastValue = elt.value;
+                            }
+                        }
+                        if (elementData.delayed) {
+                            clearTimeout(elementData.delayed);
+                        }
+                        if (elementData.throttle) {
+                            return;
+                        }
+
+                        if (triggerSpec.throttle) {
+                            if (!elementData.throttle) {
+                                handler(elt, evt);
+                                elementData.throttle = setTimeout(function () {
+                                    elementData.throttle = null;
+                                }, triggerSpec.throttle);
+                            }
+                        } else if (triggerSpec.delay) {
+                            elementData.delayed = setTimeout(function() { handler(elt, evt) }, triggerSpec.delay);
+                        } else {
+                            handler(elt, evt);
+                        }
+                    }
+                };
+                if (nodeData.listenerInfos == null) {
+                    nodeData.listenerInfos = [];
+                }
+                nodeData.listenerInfos.push({
+                    trigger: triggerSpec.trigger,
+                    listener: eventListener,
+                    on: eltToListenOn
+                })
+                eltToListenOn.addEventListener(triggerSpec.trigger, eventListener);
+            })
+        }
+
+        var windowIsScrolling = false // used by initScrollHandler
+        var scrollHandler = null;
+        function initScrollHandler() {
+            if (!scrollHandler) {
+                scrollHandler = function() {
+                    windowIsScrolling = true
+                };
+                window.addEventListener("scroll", scrollHandler)
+                setInterval(function() {
+                    if (windowIsScrolling) {
+                        windowIsScrolling = false;
+                        forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) {
+                            maybeReveal(elt);
+                        })
+                    }
+                }, 200);
+            }
+        }
+
+        function maybeReveal(elt) {
+            if (!hasAttribute(elt,'data-hx-revealed') && isScrolledIntoView(elt)) {
+                elt.setAttribute('data-hx-revealed', 'true');
+                var nodeData = getInternalData(elt);
+                if (nodeData.initialized) {
+                    triggerEvent(elt, 'revealed');
+                } else {
+                    // if the node isn't initialized, wait for it before triggering the request
+                    elt.addEventListener("htmx:afterProcessNode", function(evt) { triggerEvent(elt, 'revealed') }, {once: true});
+                }
+            }
+        }
+
+        //====================================================================
+        // Web Sockets
+        //====================================================================
+
+        function processWebSocketInfo(elt, nodeData, info) {
+            var values = splitOnWhitespace(info);
+            for (var i = 0; i < values.length; i++) {
+                var value = values[i].split(/:(.+)/);
+                if (value[0] === "connect") {
+                    ensureWebSocket(elt, value[1], 0);
+                }
+                if (value[0] === "send") {
+                    processWebSocketSend(elt);
+                }
+            }
+        }
+
+        function ensureWebSocket(elt, wssSource, retryCount) {
+            if (!bodyContains(elt)) {
+                return;  // stop ensuring websocket connection when socket bearing element ceases to exist
+            }
+
+            if (wssSource.indexOf("/") == 0) {  // complete absolute paths only
+                var base_part = location.hostname + (location.port ? ':'+location.port: '');
+                if (location.protocol == 'https:') {
+                    wssSource = "wss://" + base_part + wssSource;
+                } else if (location.protocol == 'http:') {
+                    wssSource = "ws://" + base_part + wssSource;
+                }
+            }
+            var socket = htmx.createWebSocket(wssSource);
+            socket.onerror = function (e) {
+                triggerErrorEvent(elt, "htmx:wsError", {error:e, socket:socket});
+                maybeCloseWebSocketSource(elt);
+            };
+
+            socket.onclose = function (e) {
+                if ([1006, 1012, 1013].indexOf(e.code) >= 0) {  // Abnormal Closure/Service Restart/Try Again Later
+                    var delay = getWebSocketReconnectDelay(retryCount);
+                    setTimeout(function() {
+                        ensureWebSocket(elt, wssSource, retryCount+1);  // creates a websocket with a new timeout
+                    }, delay);
+                }
+            };
+            socket.onopen = function (e) {
+                retryCount = 0;
+            }
+
+            getInternalData(elt).webSocket = socket;
+            socket.addEventListener('message', function (event) {
+                if (maybeCloseWebSocketSource(elt)) {
+                    return;
+                }
+
+                var response = event.data;
+                withExtensions(elt, function(extension){
+                    response = extension.transformResponse(response, null, elt);
+                });
+
+                var settleInfo = makeSettleInfo(elt);
+                var fragment = makeFragment(response);
+                var children = toArray(fragment.children);
+                for (var i = 0; i < children.length; i++) {
+                    var child = children[i];
+                    oobSwap(getAttributeValue(child, "hx-swap-oob") || "true", child, settleInfo);
+                }
+
+                settleImmediately(settleInfo.tasks);
+            });
+        }
+
+        function maybeCloseWebSocketSource(elt) {
+            if (!bodyContains(elt)) {
+                getInternalData(elt).webSocket.close();
+                return true;
+            }
+        }
+
+        function processWebSocketSend(elt) {
+            var webSocketSourceElt = getClosestMatch(elt, function (parent) {
+                return getInternalData(parent).webSocket != null;
+            });
+            if (webSocketSourceElt) {
+                elt.addEventListener(getTriggerSpecs(elt)[0].trigger, function (evt) {
+                    var webSocket = getInternalData(webSocketSourceElt).webSocket;
+                    var headers = getHeaders(elt, webSocketSourceElt);
+                    var results = getInputValues(elt, 'post');
+                    var errors = results.errors;
+                    var rawParameters = results.values;
+                    var expressionVars = getExpressionVars(elt);
+                    var allParameters = mergeObjects(rawParameters, expressionVars);
+                    var filteredParameters = filterValues(allParameters, elt);
+                    filteredParameters['HEADERS'] = headers;
+                    if (errors && errors.length > 0) {
+                        triggerEvent(elt, 'htmx:validation:halted', errors);
+                        return;
+                    }
+                    webSocket.send(JSON.stringify(filteredParameters));
+                    if(shouldCancel(evt, elt)){
+                        evt.preventDefault();
+                    }
+                });
+            } else {
+                triggerErrorEvent(elt, "htmx:noWebSocketSourceError");
+            }
+        }
+
+        function getWebSocketReconnectDelay(retryCount) {
+            var delay = htmx.config.wsReconnectDelay;
+            if (typeof delay === 'function') {
+                // @ts-ignore
+                return delay(retryCount);
+            }
+            if (delay === 'full-jitter') {
+                var exp = Math.min(retryCount, 6);
+                var maxDelay = 1000 * Math.pow(2, exp);
+                return maxDelay * Math.random();
+            }
+            logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
+        }
+
+        //====================================================================
+        // Server Sent Events
+        //====================================================================
+
+        function processSSEInfo(elt, nodeData, info) {
+            var values = splitOnWhitespace(info);
+            for (var i = 0; i < values.length; i++) {
+                var value = values[i].split(/:(.+)/);
+                if (value[0] === "connect") {
+                    processSSESource(elt, value[1]);
+                }
+
+                if ((value[0] === "swap")) {
+                    processSSESwap(elt, value[1])
+                }
+            }
+        }
+
+        function processSSESource(elt, sseSrc) {
+            var source = htmx.createEventSource(sseSrc);
+            source.onerror = function (e) {
+                triggerErrorEvent(elt, "htmx:sseError", {error:e, source:source});
+                maybeCloseSSESource(elt);
+            };
+            getInternalData(elt).sseEventSource = source;
+        }
+
+        function processSSESwap(elt, sseEventName) {
+            var sseSourceElt = getClosestMatch(elt, hasEventSource);
+            if (sseSourceElt) {
+                var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
+                var sseListener = function (event) {
+                    if (maybeCloseSSESource(sseSourceElt)) {
+                        sseEventSource.removeEventListener(sseEventName, sseListener);
+                        return;
+                    }
+
+                    ///////////////////////////
+                    // TODO: merge this code with AJAX and WebSockets code in the future.
+
+                    var response = event.data;
+                    withExtensions(elt, function(extension){
+                        response = extension.transformResponse(response, null, elt);
+                    });
+
+                    var swapSpec = getSwapSpecification(elt)
+                    var target = getTarget(elt)
+                    var settleInfo = makeSettleInfo(elt);
+
+                    selectAndSwap(swapSpec.swapStyle, elt, target, response, settleInfo)
+                    settleImmediately(settleInfo.tasks)
+                    triggerEvent(elt, "htmx:sseMessage", event)
+                };
+
+                getInternalData(elt).sseListener = sseListener;
+                sseEventSource.addEventListener(sseEventName, sseListener);
+            } else {
+                triggerErrorEvent(elt, "htmx:noSSESourceError");
+            }
+        }
+
+        function processSSETrigger(elt, handler, sseEventName) {
+            var sseSourceElt = getClosestMatch(elt, hasEventSource);
+            if (sseSourceElt) {
+                var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
+                var sseListener = function () {
+                    if (!maybeCloseSSESource(sseSourceElt)) {
+                        if (bodyContains(elt)) {
+                            handler(elt);
+                        } else {
+                            sseEventSource.removeEventListener(sseEventName, sseListener);
+                        }
+                    }
+                };
+                getInternalData(elt).sseListener = sseListener;
+                sseEventSource.addEventListener(sseEventName, sseListener);
+            } else {
+                triggerErrorEvent(elt, "htmx:noSSESourceError");
+            }
+        }
+
+        function maybeCloseSSESource(elt) {
+            if (!bodyContains(elt)) {
+                getInternalData(elt).sseEventSource.close();
+                return true;
+            }
+        }
+
+        function hasEventSource(node) {
+            return getInternalData(node).sseEventSource != null;
+        }
+
+        //====================================================================
+
+        function loadImmediately(elt, handler, nodeData, delay) {
+            var load = function(){
+                if (!nodeData.loaded) {
+                    nodeData.loaded = true;
+                    handler(elt);
+                }
+            }
+            if (delay) {
+                setTimeout(load, delay);
+            } else {
+                load();
+            }
+        }
+
+        function processVerbs(elt, nodeData, triggerSpecs) {
+            var explicitAction = false;
+            forEach(VERBS, function (verb) {
+                if (hasAttribute(elt,'hx-' + verb)) {
+                    var path = getAttributeValue(elt, 'hx-' + verb);
+                    explicitAction = true;
+                    nodeData.path = path;
+                    nodeData.verb = verb;
+                    triggerSpecs.forEach(function(triggerSpec) {
+                        addTriggerHandler(elt, triggerSpec, nodeData, function (elt, evt) {
+                            issueAjaxRequest(verb, path, elt, evt)
+                        })
+                    });
+                }
+            });
+            return explicitAction;
+        }
+
+        function addTriggerHandler(elt, triggerSpec, nodeData, handler) {
+            if (triggerSpec.sseEvent) {
+                processSSETrigger(elt, handler, triggerSpec.sseEvent);
+            } else if (triggerSpec.trigger === "revealed") {
+                initScrollHandler();
+                addEventListener(elt, handler, nodeData, triggerSpec);
+                maybeReveal(elt);
+            } else if (triggerSpec.trigger === "intersect") {
+                var observerOptions = {};
+                if (triggerSpec.root) {
+                    observerOptions.root = querySelectorExt(elt, triggerSpec.root)
+                }
+                if (triggerSpec.threshold) {
+                    observerOptions.threshold = parseFloat(triggerSpec.threshold);
+                }
+                var observer = new IntersectionObserver(function (entries) {
+                    for (var i = 0; i < entries.length; i++) {
+                        var entry = entries[i];
+                        if (entry.isIntersecting) {
+                            triggerEvent(elt, "intersect");
+                            break;
+                        }
+                    }
+                }, observerOptions);
+                observer.observe(elt);
+                addEventListener(elt, handler, nodeData, triggerSpec);
+            } else if (triggerSpec.trigger === "load") {
+                if (!maybeFilterEvent(triggerSpec, makeEvent("load", {elt:elt}))) {
+                                loadImmediately(elt, handler, nodeData, triggerSpec.delay);
+                            }
+            } else if (triggerSpec.pollInterval) {
+                nodeData.polling = true;
+                processPolling(elt, handler, triggerSpec);
+            } else {
+                addEventListener(elt, handler, nodeData, triggerSpec);
+            }
+        }
+
+        function evalScript(script) {
+            if (script.type === "text/javascript" || script.type === "module" || script.type === "") {
+                var newScript = getDocument().createElement("script");
+                forEach(script.attributes, function (attr) {
+                    newScript.setAttribute(attr.name, attr.value);
+                });
+                newScript.textContent = script.textContent;
+                newScript.async = false;
+                if (htmx.config.inlineScriptNonce) {
+                    newScript.nonce = htmx.config.inlineScriptNonce;
+                }
+                var parent = script.parentElement;
+
+                try {
+                    parent.insertBefore(newScript, script);
+                } catch (e) {
+                    logError(e);
+                } finally {
+                    parent.removeChild(script);
+                }
+            }
+        }
+
+        function processScripts(elt) {
+            if (matches(elt, "script")) {
+                evalScript(elt);
+            }
+            forEach(findAll(elt, "script"), function (script) {
+                evalScript(script);
+            });
+        }
+
+        function hasChanceOfBeingBoosted() {
+            return document.querySelector("[hx-boost], [data-hx-boost]");
+        }
+
+        function findElementsToProcess(elt) {
+            if (elt.querySelectorAll) {
+                var boostedElts = hasChanceOfBeingBoosted() ? ", a, form" : "";
+                var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", [hx-sse], [data-hx-sse], [hx-ws]," +
+                    " [data-hx-ws], [hx-ext], [hx-data-ext]");
+                return results;
+            } else {
+                return [];
+            }
+        }
+
+        function initButtonTracking(form){
+            var maybeSetLastButtonClicked = function(evt){
+                if (matches(evt.target, "button, input[type='submit']")) {
+                    var internalData = getInternalData(form);
+                    internalData.lastButtonClicked = evt.target;
+                }
+            };
+
+            // need to handle both click and focus in:
+            //   focusin - in case someone tabs in to a button and hits the space bar
+            //   click - on OSX buttons do not focus on click see https://bugs.webkit.org/show_bug.cgi?id=13724
+
+            form.addEventListener('click', maybeSetLastButtonClicked)
+            form.addEventListener('focusin', maybeSetLastButtonClicked)
+            form.addEventListener('focusout', function(evt){
+                var internalData = getInternalData(form);
+                internalData.lastButtonClicked = null;
+            })
+        }
+
+        function initNode(elt) {
+            if (elt.closest && elt.closest(htmx.config.disableSelector)) {
+                return;
+            }
+            var nodeData = getInternalData(elt);
+            if (!nodeData.initialized) {
+                nodeData.initialized = true;
+                triggerEvent(elt, "htmx:beforeProcessNode")
+
+                if (elt.value) {
+                    nodeData.lastValue = elt.value;
+                }
+
+                var triggerSpecs = getTriggerSpecs(elt);
+                var explicitAction = processVerbs(elt, nodeData, triggerSpecs);
+
+                if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") {
+                    boostElement(elt, nodeData, triggerSpecs);
+                }
+
+                if (elt.tagName === "FORM") {
+                    initButtonTracking(elt);
+                }
+
+                var sseInfo = getAttributeValue(elt, 'hx-sse');
+                if (sseInfo) {
+                    processSSEInfo(elt, nodeData, sseInfo);
+                }
+
+                var wsInfo = getAttributeValue(elt, 'hx-ws');
+                if (wsInfo) {
+                    processWebSocketInfo(elt, nodeData, wsInfo);
+                }
+                triggerEvent(elt, "htmx:afterProcessNode");
+            }
+        }
+
+        function processNode(elt) {
+            elt = resolveTarget(elt);
+            initNode(elt);
+            forEach(findElementsToProcess(elt), function(child) { initNode(child) });
+        }
+
+        //====================================================================
+        // Event/Log Support
+        //====================================================================
+
+        function kebabEventName(str) {
+            return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
+        }
+
+        function makeEvent(eventName, detail) {
+            var evt;
+            if (window.CustomEvent && typeof window.CustomEvent === 'function') {
+                evt = new CustomEvent(eventName, {bubbles: true, cancelable: true, detail: detail});
+            } else {
+                evt = getDocument().createEvent('CustomEvent');
+                evt.initCustomEvent(eventName, true, true, detail);
+            }
+            return evt;
+        }
+
+        function triggerErrorEvent(elt, eventName, detail) {
+            triggerEvent(elt, eventName, mergeObjects({error:eventName}, detail));
+        }
+
+        function ignoreEventForLogging(eventName) {
+            return eventName === "htmx:afterProcessNode"
+        }
+
+        /**
+         * `withExtensions` locates all active extensions for a provided element, then
+         * executes the provided function using each of the active extensions.  It should
+         * be called internally at every extendable execution point in htmx.
+         *
+         * @param {HTMLElement} elt
+         * @param {(extension:import("./htmx").HtmxExtension) => void} toDo
+         * @returns void
+         */
+        function withExtensions(elt, toDo) {
+            forEach(getExtensions(elt), function(extension){
+                try {
+                    toDo(extension);
+                } catch (e) {
+                    logError(e);
+                }
+            });
+        }
+
+        function logError(msg) {
+            if(console.error) {
+                console.error(msg);
+            } else if (console.log) {
+                console.log("ERROR: ", msg);
+            }
+        }
+
+        function triggerEvent(elt, eventName, detail) {
+            elt = resolveTarget(elt);
+            if (detail == null) {
+                detail = {};
+            }
+            detail["elt"] = elt;
+            var event = makeEvent(eventName, detail);
+            if (htmx.logger && !ignoreEventForLogging(eventName)) {
+                htmx.logger(elt, eventName, detail);
+            }
+            if (detail.error) {
+                logError(detail.error);
+                triggerEvent(elt, "htmx:error", {errorInfo:detail})
+            }
+            var eventResult = elt.dispatchEvent(event);
+            var kebabName = kebabEventName(eventName);
+            if (eventResult && kebabName !== eventName) {
+                var kebabedEvent = makeEvent(kebabName, event.detail);
+                eventResult = eventResult && elt.dispatchEvent(kebabedEvent)
+            }
+            withExtensions(elt, function (extension) {
+                eventResult = eventResult && (extension.onEvent(eventName, event) !== false)
+            });
+            return eventResult;
+        }
+
+        //====================================================================
+        // History Support
+        //====================================================================
+        var currentPathForHistory = location.pathname+location.search;
+
+        function getHistoryElement() {
+            var historyElt = getDocument().querySelector('[hx-history-elt],[data-hx-history-elt]');
+            return historyElt || getDocument().body;
+        }
+
+        function saveToHistoryCache(url, content, title, scroll) {
+            if (!canAccessLocalStorage()) {
+                return;
+            }
+
+            var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || [];
+            for (var i = 0; i < historyCache.length; i++) {
+                if (historyCache[i].url === url) {
+                    historyCache.splice(i, 1);
+                    break;
+                }
+            }
+            historyCache.push({url:url, content: content, title:title, scroll:scroll})
+            while (historyCache.length > htmx.config.historyCacheSize) {
+                historyCache.shift();
+            }
+            while(historyCache.length > 0){
+                try {
+                    localStorage.setItem("htmx-history-cache", JSON.stringify(historyCache));
+                    break;
+                } catch (e) {
+                    triggerErrorEvent(getDocument().body, "htmx:historyCacheError", {cause:e, cache: historyCache})
+                    historyCache.shift(); // shrink the cache and retry
+                }
+            }
+        }
+
+        function getCachedHistory(url) {
+            if (!canAccessLocalStorage()) {
+                return null;
+            }
+
+            var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || [];
+            for (var i = 0; i < historyCache.length; i++) {
+                if (historyCache[i].url === url) {
+                    return historyCache[i];
+                }
+            }
+            return null;
+        }
+
+        function cleanInnerHtmlForHistory(elt) {
+            var className = htmx.config.requestClass;
+            var clone = elt.cloneNode(true);
+            forEach(findAll(clone, "." + className), function(child){
+                removeClassFromElement(child, className);
+            });
+            return clone.innerHTML;
+        }
+
+        function saveCurrentPageToHistory() {
+            var elt = getHistoryElement();
+            var path = currentPathForHistory || location.pathname+location.search;
+            triggerEvent(getDocument().body, "htmx:beforeHistorySave", {path:path, historyElt:elt});
+            if(htmx.config.historyEnabled) history.replaceState({htmx:true}, getDocument().title, window.location.href);
+            saveToHistoryCache(path, cleanInnerHtmlForHistory(elt), getDocument().title, window.scrollY);
+        }
+
+        function pushUrlIntoHistory(path) {
+            if(htmx.config.historyEnabled)  history.pushState({htmx:true}, "", path);
+            currentPathForHistory = path;
+        }
+
+        function replaceUrlInHistory(path) {
+            if(htmx.config.historyEnabled)  history.replaceState({htmx:true}, "", path);
+            currentPathForHistory = path;
+        }
+
+        function settleImmediately(tasks) {
+            forEach(tasks, function (task) {
+                task.call();
+            });
+        }
+
+        function loadHistoryFromServer(path) {
+            var request = new XMLHttpRequest();
+            var details = {path: path, xhr:request};
+            triggerEvent(getDocument().body, "htmx:historyCacheMiss", details);
+            request.open('GET', path, true);
+            request.setRequestHeader("HX-History-Restore-Request", "true");
+            request.onload = function () {
+                if (this.status >= 200 && this.status < 400) {
+                    triggerEvent(getDocument().body, "htmx:historyCacheMissLoad", details);
+                    var fragment = makeFragment(this.response);
+                    // @ts-ignore
+                    fragment = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment;
+                    var historyElement = getHistoryElement();
+                    var settleInfo = makeSettleInfo(historyElement);
+                    // @ts-ignore
+                    swapInnerHTML(historyElement, fragment, settleInfo)
+                    settleImmediately(settleInfo.tasks);
+                    currentPathForHistory = path;
+                    triggerEvent(getDocument().body, "htmx:historyRestore", {path:path});
+                } else {
+                    triggerErrorEvent(getDocument().body, "htmx:historyCacheMissLoadError", details);
+                }
+            };
+            request.send();
+        }
+
+        function restoreHistory(path) {
+            saveCurrentPageToHistory();
+            path = path || location.pathname+location.search;
+            var cached = getCachedHistory(path);
+            if (cached) {
+                var fragment = makeFragment(cached.content);
+                var historyElement = getHistoryElement();
+                var settleInfo = makeSettleInfo(historyElement);
+                swapInnerHTML(historyElement, fragment, settleInfo)
+                settleImmediately(settleInfo.tasks);
+                document.title = cached.title;
+                window.scrollTo(0, cached.scroll);
+                currentPathForHistory = path;
+                triggerEvent(getDocument().body, "htmx:historyRestore", {path:path});
+            } else {
+                if (htmx.config.refreshOnHistoryMiss) {
+
+                    // @ts-ignore: optional parameter in reload() function throws error
+                    window.location.reload(true);
+                } else {
+                    loadHistoryFromServer(path);
+                }
+            }
+        }
+
+        function addRequestIndicatorClasses(elt) {
+            var indicators = findAttributeTargets(elt, 'hx-indicator');
+            if (indicators == null) {
+                indicators = [elt];
+            }
+            forEach(indicators, function (ic) {
+                ic.classList["add"].call(ic.classList, htmx.config.requestClass);
+            });
+            return indicators;
+        }
+
+        function removeRequestIndicatorClasses(indicators) {
+            forEach(indicators, function (ic) {
+                ic.classList["remove"].call(ic.classList, htmx.config.requestClass);
+            });
+        }
+
+        //====================================================================
+        // Input Value Processing
+        //====================================================================
+
+        function haveSeenNode(processed, elt) {
+            for (var i = 0; i < processed.length; i++) {
+                var node = processed[i];
+                if (node.isSameNode(elt)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        function shouldInclude(elt) {
+            if(elt.name === "" || elt.name == null || elt.disabled) {
+                return false;
+            }
+            // ignore "submitter" types (see jQuery src/serialize.js)
+            if (elt.type === "button" || elt.type === "submit" || elt.tagName === "image" || elt.tagName === "reset" || elt.tagName === "file" ) {
+                return false;
+            }
+            if (elt.type === "checkbox" || elt.type === "radio" ) {
+                return elt.checked;
+            }
+            return true;
+        }
+
+        function processInputValue(processed, values, errors, elt, validate) {
+            if (elt == null || haveSeenNode(processed, elt)) {
+                return;
+            } else {
+                processed.push(elt);
+            }
+            if (shouldInclude(elt)) {
+                var name = getRawAttribute(elt,"name");
+                var value = elt.value;
+                if (elt.multiple) {
+                    value = toArray(elt.querySelectorAll("option:checked")).map(function (e) { return e.value });
+                }
+                // include file inputs
+                if (elt.files) {
+                    value = toArray(elt.files);
+                }
+                // This is a little ugly because both the current value of the named value in the form
+                // and the new value could be arrays, so we have to handle all four cases :/
+                if (name != null && value != null) {
+                    var current = values[name];
+                    if(current) {
+                        if (Array.isArray(current)) {
+                            if (Array.isArray(value)) {
+                                values[name] = current.concat(value);
+                            } else {
+                                current.push(value);
+                            }
+                        } else {
+                            if (Array.isArray(value)) {
+                                values[name] = [current].concat(value);
+                            } else {
+                                values[name] = [current, value];
+                            }
+                        }
+                    } else {
+                        values[name] = value;
+                    }
+                }
+                if (validate) {
+                    validateElement(elt, errors);
+                }
+            }
+            if (matches(elt, 'form')) {
+                var inputs = elt.elements;
+                forEach(inputs, function(input) {
+                    processInputValue(processed, values, errors, input, validate);
+                });
+            }
+        }
+
+        function validateElement(element, errors) {
+            if (element.willValidate) {
+                triggerEvent(element, "htmx:validation:validate")
+                if (!element.checkValidity()) {
+                    errors.push({elt: element, message:element.validationMessage, validity:element.validity});
+                    triggerEvent(element, "htmx:validation:failed", {message:element.validationMessage, validity:element.validity})
+                }
+            }
+        }
+
+        /**
+         * @param {HTMLElement} elt
+         * @param {string} verb
+         */
+        function getInputValues(elt, verb) {
+            var processed = [];
+            var values = {};
+            var formValues = {};
+            var errors = [];
+            var internalData = getInternalData(elt);
+
+            // only validate when form is directly submitted and novalidate or formnovalidate are not set
+            var validate = matches(elt, 'form') && elt.noValidate !== true;
+            if (internalData.lastButtonClicked) {
+                validate = validate && internalData.lastButtonClicked.formNoValidate !== true;
+            }
+
+            // for a non-GET include the closest form
+            if (verb !== 'get') {
+                processInputValue(processed, formValues, errors, closest(elt, 'form'), validate);
+            }
+
+            // include the element itself
+            processInputValue(processed, values, errors, elt, validate);
+
+            // if a button or submit was clicked last, include its value
+            if (internalData.lastButtonClicked) {
+                var name = getRawAttribute(internalData.lastButtonClicked,"name");
+                if (name) {
+                    values[name] = internalData.lastButtonClicked.value;
+                }
+            }
+
+            // include any explicit includes
+            var includes = findAttributeTargets(elt, "hx-include");
+            forEach(includes, function(node) {
+                processInputValue(processed, values, errors, node, validate);
+                // if a non-form is included, include any input values within it
+                if (!matches(node, 'form')) {
+                    forEach(node.querySelectorAll(INPUT_SELECTOR), function (descendant) {
+                        processInputValue(processed, values, errors, descendant, validate);
+                    })
+                }
+            });
+
+            // form values take precedence, overriding the regular values
+            values = mergeObjects(values, formValues);
+
+            return {errors:errors, values:values};
+        }
+
+        function appendParam(returnStr, name, realValue) {
+            if (returnStr !== "") {
+                returnStr += "&";
+            }
+            if (String(realValue) === "[object Object]") {
+                realValue = JSON.stringify(realValue);
+            }
+            var s = encodeURIComponent(realValue);
+            returnStr += encodeURIComponent(name) + "=" + s;
+            return returnStr;
+        }
+
+        function urlEncode(values) {
+            var returnStr = "";
+            for (var name in values) {
+                if (values.hasOwnProperty(name)) {
+                    var value = values[name];
+                    if (Array.isArray(value)) {
+                        forEach(value, function(v) {
+                            returnStr = appendParam(returnStr, name, v);
+                        });
+                    } else {
+                        returnStr = appendParam(returnStr, name, value);
+                    }
+                }
+            }
+            return returnStr;
+        }
+
+        function makeFormData(values) {
+            var formData = new FormData();
+            for (var name in values) {
+                if (values.hasOwnProperty(name)) {
+                    var value = values[name];
+                    if (Array.isArray(value)) {
+                        forEach(value, function(v) {
+                            formData.append(name, v);
+                        });
+                    } else {
+                        formData.append(name, value);
+                    }
+                }
+            }
+            return formData;
+        }
+
+        //====================================================================
+        // Ajax
+        //====================================================================
+
+        /**
+         * @param {HTMLElement} elt
+         * @param {HTMLElement} target
+         * @param {string} prompt
+         * @returns {Object} // TODO: Define/Improve HtmxHeaderSpecification
+         */
+        function getHeaders(elt, target, prompt) {
+            var headers = {
+                "HX-Request" : "true",
+                "HX-Trigger" : getRawAttribute(elt, "id"),
+                "HX-Trigger-Name" : getRawAttribute(elt, "name"),
+                "HX-Target" : getAttributeValue(target, "id"),
+                "HX-Current-URL" : getDocument().location.href,
+            }
+            getValuesForElement(elt, "hx-headers", false, headers)
+            if (prompt !== undefined) {
+                headers["HX-Prompt"] = prompt;
+            }
+            if (getInternalData(elt).boosted) {
+                headers["HX-Boosted"] = "true";
+            }
+            return headers;
+        }
+
+        /**
+         * filterValues takes an object containing form input values
+         * and returns a new object that only contains keys that are
+         * specified by the closest "hx-params" attribute
+         * @param {Object} inputValues
+         * @param {HTMLElement} elt
+         * @returns {Object}
+         */
+        function filterValues(inputValues, elt) {
+            var paramsValue = getClosestAttributeValue(elt, "hx-params");
+            if (paramsValue) {
+                if (paramsValue === "none") {
+                    return {};
+                } else if (paramsValue === "*") {
+                    return inputValues;
+                } else if(paramsValue.indexOf("not ") === 0) {
+                    forEach(paramsValue.substr(4).split(","), function (name) {
+                        name = name.trim();
+                        delete inputValues[name];
+                    });
+                    return inputValues;
+                } else {
+                    var newValues = {}
+                    forEach(paramsValue.split(","), function (name) {
+                        name = name.trim();
+                        newValues[name] = inputValues[name];
+                    });
+                    return newValues;
+                }
+            } else {
+                return inputValues;
+            }
+        }
+
+        function isAnchorLink(elt) {
+          return getRawAttribute(elt, 'href') && getRawAttribute(elt, 'href').indexOf("#") >=0
+        }
+
+        /**
+         *
+         * @param {HTMLElement} elt
+         * @param {string} swapInfoOverride
+         * @returns {import("./htmx").HtmxSwapSpecification}
+         */
+        function getSwapSpecification(elt, swapInfoOverride) {
+            var swapInfo = swapInfoOverride ? swapInfoOverride : getClosestAttributeValue(elt, "hx-swap");
+            var swapSpec = {
+                "swapStyle" : getInternalData(elt).boosted ? 'innerHTML' : htmx.config.defaultSwapStyle,
+                "swapDelay" : htmx.config.defaultSwapDelay,
+                "settleDelay" : htmx.config.defaultSettleDelay
+            }
+            if (getInternalData(elt).boosted && !isAnchorLink(elt)) {
+              swapSpec["show"] = "top"
+            }
+            if (swapInfo) {
+                var split = splitOnWhitespace(swapInfo);
+                if (split.length > 0) {
+                    swapSpec["swapStyle"] = split[0];
+                    for (var i = 1; i < split.length; i++) {
+                        var modifier = split[i];
+                        if (modifier.indexOf("swap:") === 0) {
+                            swapSpec["swapDelay"] = parseInterval(modifier.substr(5));
+                        }
+                        if (modifier.indexOf("settle:") === 0) {
+                            swapSpec["settleDelay"] = parseInterval(modifier.substr(7));
+                        }
+                        if (modifier.indexOf("scroll:") === 0) {
+                            var scrollSpec = modifier.substr(7);
+                            var splitSpec = scrollSpec.split(":");
+                            var scrollVal = splitSpec.pop();
+                            var selectorVal = splitSpec.length > 0 ? splitSpec.join(":") : null;
+                            swapSpec["scroll"] = scrollVal;
+                            swapSpec["scrollTarget"] = selectorVal;
+                        }
+                        if (modifier.indexOf("show:") === 0) {
+                            var showSpec = modifier.substr(5);
+                            var splitSpec = showSpec.split(":");
+                            var showVal = splitSpec.pop();
+                            var selectorVal = splitSpec.length > 0 ? splitSpec.join(":") : null;
+                            swapSpec["show"] = showVal;
+                            swapSpec["showTarget"] = selectorVal;
+                        }
+                        if (modifier.indexOf("focus-scroll:") === 0) {
+                            var focusScrollVal = modifier.substr("focus-scroll:".length);
+                            swapSpec["focusScroll"] = focusScrollVal == "true";
+                        }
+                    }
+                }
+            }
+            return swapSpec;
+        }
+
+        function usesFormData(elt) {
+            return getClosestAttributeValue(elt, "hx-encoding") === "multipart/form-data" ||
+                (matches(elt, "form") && getRawAttribute(elt, 'enctype') === "multipart/form-data");
+        }
+
+        function encodeParamsForBody(xhr, elt, filteredParameters) {
+            var encodedParameters = null;
+            withExtensions(elt, function (extension) {
+                if (encodedParameters == null) {
+                    encodedParameters = extension.encodeParameters(xhr, filteredParameters, elt);
+                }
+            });
+            if (encodedParameters != null) {
+                return encodedParameters;
+            } else {
+                if (usesFormData(elt)) {
+                    return makeFormData(filteredParameters);
+                } else {
+                    return urlEncode(filteredParameters);
+                }
+            }
+        }
+
+        /**
+         *
+         * @param {Element} target
+         * @returns {import("./htmx").HtmxSettleInfo}
+         */
+        function makeSettleInfo(target) {
+            return {tasks: [], elts: [target]};
+        }
+
+        function updateScrollState(content, swapSpec) {
+            var first = content[0];
+            var last = content[content.length - 1];
+            if (swapSpec.scroll) {
+                var target = null;
+                if (swapSpec.scrollTarget) {
+                    target = querySelectorExt(first, swapSpec.scrollTarget);
+                }
+                if (swapSpec.scroll === "top" && (first || target)) {
+                    target = target || first;
+                    target.scrollTop = 0;
+                }
+                if (swapSpec.scroll === "bottom" && (last || target)) {
+                    target = target || last;
+                    target.scrollTop = target.scrollHeight;
+                }
+            }
+            if (swapSpec.show) {
+                var target = null;
+                if (swapSpec.showTarget) {
+                    var targetStr = swapSpec.showTarget;
+                    if (swapSpec.showTarget === "window") {
+                        targetStr = "body";
+                    }
+                    target = querySelectorExt(first, targetStr);
+                }
+                if (swapSpec.show === "top" && (first || target)) {
+                    target = target || first;
+                    target.scrollIntoView({block:'start', behavior: htmx.config.scrollBehavior});
+                }
+                if (swapSpec.show === "bottom" && (last || target)) {
+                    target = target || last;
+                    target.scrollIntoView({block:'end', behavior: htmx.config.scrollBehavior});
+                }
+            }
+        }
+
+        /**
+         * @param {HTMLElement} elt
+         * @param {string} attr
+         * @param {boolean=} evalAsDefault
+         * @param {Object=} values
+         * @returns {Object}
+         */
+        function getValuesForElement(elt, attr, evalAsDefault, values) {
+            if (values == null) {
+                values = {};
+            }
+            if (elt == null) {
+                return values;
+            }
+            var attributeValue = getAttributeValue(elt, attr);
+            if (attributeValue) {
+                var str = attributeValue.trim();
+                var evaluateValue = evalAsDefault;
+                if (str.indexOf("javascript:") === 0) {
+                    str = str.substr(11);
+                    evaluateValue = true;
+                } else if (str.indexOf("js:") === 0) {
+                    str = str.substr(3);
+                    evaluateValue = true;
+                }
+                if (str.indexOf('{') !== 0) {
+                    str = "{" + str + "}";
+                }
+                var varsValues;
+                if (evaluateValue) {
+                    varsValues = maybeEval(elt,function () {return Function("return (" + str + ")")();}, {});
+                } else {
+                    varsValues = parseJSON(str);
+                }
+                for (var key in varsValues) {
+                    if (varsValues.hasOwnProperty(key)) {
+                        if (values[key] == null) {
+                            values[key] = varsValues[key];
+                        }
+                    }
+                }
+            }
+            return getValuesForElement(parentElt(elt), attr, evalAsDefault, values);
+        }
+
+        function maybeEval(elt, toEval, defaultVal) {
+            if (htmx.config.allowEval) {
+                return toEval();
+            } else {
+                triggerErrorEvent(elt, 'htmx:evalDisallowedError');
+                return defaultVal;
+            }
+        }
+
+        /**
+         * @param {HTMLElement} elt
+         * @param {*} expressionVars
+         * @returns
+         */
+        function getHXVarsForElement(elt, expressionVars) {
+            return getValuesForElement(elt, "hx-vars", true, expressionVars);
+        }
+
+        /**
+         * @param {HTMLElement} elt
+         * @param {*} expressionVars
+         * @returns
+         */
+        function getHXValsForElement(elt, expressionVars) {
+            return getValuesForElement(elt, "hx-vals", false, expressionVars);
+        }
+
+        /**
+         * @param {HTMLElement} elt
+         * @returns {Object}
+         */
+        function getExpressionVars(elt) {
+            return mergeObjects(getHXVarsForElement(elt), getHXValsForElement(elt));
+        }
+
+        function safelySetHeaderValue(xhr, header, headerValue) {
+            if (headerValue !== null) {
+                try {
+                    xhr.setRequestHeader(header, headerValue);
+                } catch (e) {
+                    // On an exception, try to set the header URI encoded instead
+                    xhr.setRequestHeader(header, encodeURIComponent(headerValue));
+                    xhr.setRequestHeader(header + "-URI-AutoEncoded", "true");
+                }
+            }
+        }
+
+        function getPathFromResponse(xhr) {
+            // NB: IE11 does not support this stuff
+            if (xhr.responseURL && typeof(URL) !== "undefined") {
+                try {
+                    var url = new URL(xhr.responseURL);
+                    return url.pathname + url.search;
+                } catch (e) {
+                    triggerErrorEvent(getDocument().body, "htmx:badResponseUrl", {url: xhr.responseURL});
+                }
+            }
+        }
+
+        function hasHeader(xhr, regexp) {
+            return xhr.getAllResponseHeaders().match(regexp);
+        }
+
+        function ajaxHelper(verb, path, context) {
+            verb = verb.toLowerCase();
+            if (context) {
+                if (context instanceof Element || isType(context, 'String')) {
+                    return issueAjaxRequest(verb, path, null, null, {
+                        targetOverride: resolveTarget(context),
+                        returnPromise: true
+                    });
+                } else {
+                    return issueAjaxRequest(verb, path, resolveTarget(context.source), context.event,
+                        {
+                            handler : context.handler,
+                            headers : context.headers,
+                            values : context.values,
+                            targetOverride: resolveTarget(context.target),
+                            swapOverride: context.swap,
+                            returnPromise: true
+                        });
+                }
+            } else {
+                return issueAjaxRequest(verb, path, null, null, {
+                        returnPromise: true
+                });
+            }
+        }
+
+        function hierarchyForElt(elt) {
+            var arr = [];
+            while (elt) {
+                arr.push(elt);
+                elt = elt.parentElement;
+            }
+            return arr;
+        }
+
+        function issueAjaxRequest(verb, path, elt, event, etc) {
+            var resolve = null;
+            var reject = null;
+            etc = etc != null ? etc : {};
+            if(etc.returnPromise && typeof Promise !== "undefined"){
+                var promise = new Promise(function (_resolve, _reject) {
+                    resolve = _resolve;
+                    reject = _reject;
+                });
+            }
+            if(elt == null) {
+                elt = getDocument().body;
+            }
+            var responseHandler = etc.handler || handleAjaxResponse;
+
+            if (!bodyContains(elt)) {
+                return; // do not issue requests for elements removed from the DOM
+            }
+            var target = etc.targetOverride || getTarget(elt);
+            if (target == null || target == DUMMY_ELT) {
+                triggerErrorEvent(elt, 'htmx:targetError', {target: getAttributeValue(elt, "hx-target")});
+                return;
+            }
+
+            var syncElt = elt;
+            var eltData = getInternalData(elt);
+            var syncStrategy = getClosestAttributeValue(elt, "hx-sync");
+            var queueStrategy = null;
+            var abortable = false;
+            if (syncStrategy) {
+                var syncStrings = syncStrategy.split(":");
+                var selector = syncStrings[0].trim();
+                if (selector === "this") {
+                    syncElt = findThisElement(elt, 'hx-sync');
+                } else {
+                    syncElt = querySelectorExt(elt, selector);
+                }
+                // default to the drop strategy
+                syncStrategy = (syncStrings[1] || 'drop').trim();
+                eltData = getInternalData(syncElt);
+                if (syncStrategy === "drop" && eltData.xhr && eltData.abortable !== true) {
+                    return;
+                } else if (syncStrategy === "abort") {
+                    if (eltData.xhr) {
+                        return;
+                    } else {
+                        abortable = true;
+                    }
+                } else if (syncStrategy === "replace") {
+                    triggerEvent(syncElt, 'htmx:abort'); // abort the current request and continue
+                } else if (syncStrategy.indexOf("queue") === 0) {
+                    var queueStrArray = syncStrategy.split(" ");
+                    queueStrategy = (queueStrArray[1] || "last").trim();
+                }
+            }
+
+            if (eltData.xhr) {
+                if (eltData.abortable) {
+                    triggerEvent(syncElt, 'htmx:abort'); // abort the current request and continue
+                } else {
+                    if(queueStrategy == null){
+                        if (event) {
+                            var eventData = getInternalData(event);
+                            if (eventData && eventData.triggerSpec && eventData.triggerSpec.queue) {
+                                queueStrategy = eventData.triggerSpec.queue;
+                            }
+                        }
+                        if (queueStrategy == null) {
+                            queueStrategy = "last";
+                        }
+                    }
+                    if (eltData.queuedRequests == null) {
+                        eltData.queuedRequests = [];
+                    }
+                    if (queueStrategy === "first" && eltData.queuedRequests.length === 0) {
+                        eltData.queuedRequests.push(function () {
+                            issueAjaxRequest(verb, path, elt, event, etc)
+                        });
+                    } else if (queueStrategy === "all") {
+                        eltData.queuedRequests.push(function () {
+                            issueAjaxRequest(verb, path, elt, event, etc)
+                        });
+                    } else if (queueStrategy === "last") {
+                        eltData.queuedRequests = []; // dump existing queue
+                        eltData.queuedRequests.push(function () {
+                            issueAjaxRequest(verb, path, elt, event, etc)
+                        });
+                    }
+                    return;
+                }
+            }
+
+            var xhr = new XMLHttpRequest();
+            eltData.xhr = xhr;
+            eltData.abortable = abortable;
+            var endRequestLock = function(){
+                eltData.xhr = null;
+                eltData.abortable = false;
+                if (eltData.queuedRequests != null &&
+                    eltData.queuedRequests.length > 0) {
+                    var queuedRequest = eltData.queuedRequests.shift();
+                    queuedRequest();
+                }
+            }
+            var promptQuestion = getClosestAttributeValue(elt, "hx-prompt");
+            if (promptQuestion) {
+                var promptResponse = prompt(promptQuestion);
+                // prompt returns null if cancelled and empty string if accepted with no entry
+                if (promptResponse === null ||
+                    !triggerEvent(elt, 'htmx:prompt', {prompt: promptResponse, target:target})) {
+                    maybeCall(resolve);
+                    endRequestLock();
+                    return promise;
+                }
+            }
+
+            var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
+            if (confirmQuestion) {
+                if(!confirm(confirmQuestion)) {
+                    maybeCall(resolve);
+                    endRequestLock()
+                    return promise;
+                }
+            }
+
+
+            var headers = getHeaders(elt, target, promptResponse);
+            if (etc.headers) {
+                headers = mergeObjects(headers, etc.headers);
+            }
+            var results = getInputValues(elt, verb);
+            var errors = results.errors;
+            var rawParameters = results.values;
+            if (etc.values) {
+                rawParameters = mergeObjects(rawParameters, etc.values);
+            }
+            var expressionVars = getExpressionVars(elt);
+            var allParameters = mergeObjects(rawParameters, expressionVars);
+            var filteredParameters = filterValues(allParameters, elt);
+
+            if (verb !== 'get' && !usesFormData(elt)) {
+                headers['Content-Type'] = 'application/x-www-form-urlencoded';
+            }
+
+            // behavior of anchors w/ empty href is to use the current URL
+            if (path == null || path === "") {
+                path = getDocument().location.href;
+            }
+
+
+            var requestAttrValues = getValuesForElement(elt, 'hx-request');
+
+            var requestConfig = {
+                parameters: filteredParameters,
+                unfilteredParameters: allParameters,
+                headers:headers,
+                target:target,
+                verb:verb,
+                errors:errors,
+                withCredentials: etc.credentials || requestAttrValues.credentials || htmx.config.withCredentials,
+                timeout:  etc.timeout || requestAttrValues.timeout || htmx.config.timeout,
+                path:path,
+                triggeringEvent:event
+            };
+
+            if(!triggerEvent(elt, 'htmx:configRequest', requestConfig)){
+                maybeCall(resolve);
+                endRequestLock();
+                return promise;
+            }
+
+            // copy out in case the object was overwritten
+            path = requestConfig.path;
+            verb = requestConfig.verb;
+            headers = requestConfig.headers;
+            filteredParameters = requestConfig.parameters;
+            errors = requestConfig.errors;
+
+            if(errors && errors.length > 0){
+                triggerEvent(elt, 'htmx:validation:halted', requestConfig)
+                maybeCall(resolve);
+                endRequestLock();
+                return promise;
+            }
+
+            var splitPath = path.split("#");
+            var pathNoAnchor = splitPath[0];
+            var anchor = splitPath[1];
+            var finalPathForGet = null;
+            if (verb === 'get') {
+                finalPathForGet = pathNoAnchor;
+                var values = Object.keys(filteredParameters).length !== 0;
+                if (values) {
+                    if (finalPathForGet.indexOf("?") < 0) {
+                        finalPathForGet += "?";
+                    } else {
+                        finalPathForGet += "&";
+                    }
+                    finalPathForGet += urlEncode(filteredParameters);
+                    if (anchor) {
+                        finalPathForGet += "#" + anchor;
+                    }
+                }
+                xhr.open('GET', finalPathForGet, true);
+            } else {
+                xhr.open(verb.toUpperCase(), path, true);
+            }
+
+            xhr.overrideMimeType("text/html");
+            xhr.withCredentials = requestConfig.withCredentials;
+            xhr.timeout = requestConfig.timeout;
+
+            // request headers
+            if (requestAttrValues.noHeaders) {
+                // ignore all headers
+            } else {
+                for (var header in headers) {
+                    if (headers.hasOwnProperty(header)) {
+                        var headerValue = headers[header];
+                        safelySetHeaderValue(xhr, header, headerValue);
+                    }
+                }
+            }
+
+            var responseInfo = {
+                xhr: xhr, target: target, requestConfig: requestConfig, etc: etc,
+                pathInfo: {
+                    requestPath: path,
+                    finalRequestPath: finalPathForGet || path,
+                    anchor: anchor
+                }
+            };
+
+            xhr.onload = function () {
+                try {
+                    var hierarchy = hierarchyForElt(elt);
+                    responseInfo.pathInfo.responsePath = getPathFromResponse(xhr);
+                    responseHandler(elt, responseInfo);
+                    removeRequestIndicatorClasses(indicators);
+                    triggerEvent(elt, 'htmx:afterRequest', responseInfo);
+                    triggerEvent(elt, 'htmx:afterOnLoad', responseInfo);
+                    // if the body no longer contains the element, trigger the even on the closest parent
+                    // remaining in the DOM
+                    if (!bodyContains(elt)) {
+                        var secondaryTriggerElt = null;
+                        while (hierarchy.length > 0 && secondaryTriggerElt == null) {
+                            var parentEltInHierarchy = hierarchy.shift();
+                            if (bodyContains(parentEltInHierarchy)) {
+                                secondaryTriggerElt = parentEltInHierarchy;
+                            }
+                        }
+                        if (secondaryTriggerElt) {
+                            triggerEvent(secondaryTriggerElt, 'htmx:afterRequest', responseInfo);
+                            triggerEvent(secondaryTriggerElt, 'htmx:afterOnLoad', responseInfo);
+                        }
+                    }
+                    maybeCall(resolve);
+                    endRequestLock();
+                } catch (e) {
+                    triggerErrorEvent(elt, 'htmx:onLoadError', mergeObjects({error:e}, responseInfo));
+                    throw e;
+                }
+            }
+            xhr.onerror = function () {
+                removeRequestIndicatorClasses(indicators);
+                triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo);
+                triggerErrorEvent(elt, 'htmx:sendError', responseInfo);
+                maybeCall(reject);
+                endRequestLock();
+            }
+            xhr.onabort = function() {
+                removeRequestIndicatorClasses(indicators);
+                triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo);
+                triggerErrorEvent(elt, 'htmx:sendAbort', responseInfo);
+                maybeCall(reject);
+                endRequestLock();
+            }
+            xhr.ontimeout = function() {
+                removeRequestIndicatorClasses(indicators);
+                triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo);
+                triggerErrorEvent(elt, 'htmx:timeout', responseInfo);
+                maybeCall(reject);
+                endRequestLock();
+            }
+            if(!triggerEvent(elt, 'htmx:beforeRequest', responseInfo)){
+                maybeCall(resolve);
+                endRequestLock()
+                return promise
+            }
+            var indicators = addRequestIndicatorClasses(elt);
+
+            forEach(['loadstart', 'loadend', 'progress', 'abort'], function(eventName) {
+                forEach([xhr, xhr.upload], function (target) {
+                    target.addEventListener(eventName, function(event){
+                        triggerEvent(elt, "htmx:xhr:" + eventName, {
+                            lengthComputable:event.lengthComputable,
+                            loaded:event.loaded,
+                            total:event.total
+                        });
+                    })
+                });
+            });
+            triggerEvent(elt, 'htmx:beforeSend', responseInfo);
+            xhr.send(verb === 'get' ? null : encodeParamsForBody(xhr, elt, filteredParameters));
+            return promise;
+        }
+
+        function determineHistoryUpdates(elt, responseInfo) {
+
+            var xhr = responseInfo.xhr;
+
+            //===========================================
+            // First consult response headers
+            //===========================================
+            var pathFromHeaders = null;
+            var typeFromHeaders = null;
+            if (hasHeader(xhr,/HX-Push:/i)) {
+                pathFromHeaders = xhr.getResponseHeader("HX-Push");
+                typeFromHeaders = "push";
+            } else if (hasHeader(xhr,/HX-Push-Url:/i)) {
+                pathFromHeaders = xhr.getResponseHeader("HX-Push-Url");
+                typeFromHeaders = "push";
+            } else if (hasHeader(xhr,/HX-Replace-Url:/i)) {
+                pathFromHeaders = xhr.getResponseHeader("HX-Replace-Url");
+                typeFromHeaders = "replace";
+            }
+
+            // if there was a response header, that has priority
+            if (pathFromHeaders) {
+                if (pathFromHeaders === "false") {
+                    return {}
+                } else {
+                    return {
+                        type: typeFromHeaders,
+                        path : pathFromHeaders
+                    }
+                }
+            }
+
+            //===========================================
+            // Next resolve via DOM values
+            //===========================================
+            var requestPath =  responseInfo.pathInfo.finalRequestPath;
+            var responsePath =  responseInfo.pathInfo.responsePath;
+
+            var pushUrl = getClosestAttributeValue(elt, "hx-push-url");
+            var replaceUrl = getClosestAttributeValue(elt, "hx-replace-url");
+            var elementIsBoosted = getInternalData(elt).boosted;
+
+            var saveType = null;
+            var path = null;
+
+            if (pushUrl) {
+                saveType = "push";
+                path = pushUrl;
+            } else if (replaceUrl) {
+                saveType = "replace";
+                path = replaceUrl;
+            } else if (elementIsBoosted) {
+                saveType = "push";
+                path = responsePath || requestPath; // if there is no response path, go with the original request path
+            }
+
+            if (path) {
+                // false indicates no push, return empty object
+                if (path === "false") {
+                    return {};
+                }
+
+                // true indicates we want to follow wherever the server ended up sending us
+                if (path === "true") {
+                    path = responsePath || requestPath; // if there is no response path, go with the original request path
+                }
+
+                // restore any anchor associated with the request
+                if (responseInfo.pathInfo.anchor &&
+                    path.indexOf("#") === -1) {
+                    path = path + "#" + responseInfo.pathInfo.anchor;
+                }
+
+                return {
+                    type:saveType,
+                    path: path
+                }
+            } else {
+                return {};
+            }
+        }
+
+        function handleAjaxResponse(elt, responseInfo) {
+            var xhr = responseInfo.xhr;
+            var target = responseInfo.target;
+            var etc = responseInfo.etc;
+
+            if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return;
+
+            if (hasHeader(xhr, /HX-Trigger:/i)) {
+                handleTrigger(xhr, "HX-Trigger", elt);
+            }
+
+            if (hasHeader(xhr, /HX-Location:/i)) {
+                saveCurrentPageToHistory();
+                var redirectPath = xhr.getResponseHeader("HX-Location");
+                var swapSpec;
+                if (redirectPath.indexOf("{") === 0) {
+                    swapSpec = parseJSON(redirectPath);
+                    // what's the best way to throw an error if the user didn't include this
+                    redirectPath = swapSpec['path'];
+                    delete swapSpec['path'];
+                }
+                ajaxHelper('GET', redirectPath, swapSpec).then(() =>{
+                    pushUrlIntoHistory(redirectPath);
+                });
+                return;
+            }
+
+            if (hasHeader(xhr, /HX-Redirect:/i)) {
+                location.href = xhr.getResponseHeader("HX-Redirect");
+                return;
+            }
+
+            if (hasHeader(xhr,/HX-Refresh:/i)) {
+                if ("true" === xhr.getResponseHeader("HX-Refresh")) {
+                    location.reload();
+                    return;
+                }
+            }
+
+            if (hasHeader(xhr,/HX-Retarget:/i)) {
+                responseInfo.target = getDocument().querySelector(xhr.getResponseHeader("HX-Retarget"));
+            }
+
+            var historyUpdate = determineHistoryUpdates(elt, responseInfo);
+
+            // by default htmx only swaps on 200 return codes and does not swap
+            // on 204 'No Content'
+            // this can be ovverriden by responding to the htmx:beforeSwap event and
+            // overriding the detail.shouldSwap property
+            var shouldSwap = xhr.status >= 200 && xhr.status < 400 && xhr.status !== 204;
+            var serverResponse = xhr.response;
+            var isError = xhr.status >= 400;
+            var beforeSwapDetails = mergeObjects({shouldSwap: shouldSwap, serverResponse:serverResponse, isError:isError}, responseInfo);
+            if (!triggerEvent(target, 'htmx:beforeSwap', beforeSwapDetails)) return;
+
+            target = beforeSwapDetails.target; // allow re-targeting
+            serverResponse = beforeSwapDetails.serverResponse; // allow updating content
+            isError = beforeSwapDetails.isError; // allow updating error
+
+            responseInfo.failed = isError; // Make failed property available to response events
+            responseInfo.successful = !isError; // Make successful property available to response events
+
+            if (beforeSwapDetails.shouldSwap) {
+                if (xhr.status === 286) {
+                    cancelPolling(elt);
+                }
+
+                withExtensions(elt, function (extension) {
+                    serverResponse = extension.transformResponse(serverResponse, xhr, elt);
+                });
+
+                // Save current page if there will be a history update
+                if (historyUpdate.type) {
+                    saveCurrentPageToHistory();
+                }
+
+                var swapOverride = etc.swapOverride;
+                if (hasHeader(xhr,/HX-Reswap:/i)) {
+                    swapOverride = xhr.getResponseHeader("HX-Reswap");
+                }
+                var swapSpec = getSwapSpecification(elt, swapOverride);
+
+                target.classList.add(htmx.config.swappingClass);
+                var doSwap = function () {
+                    try {
+
+                        var activeElt = document.activeElement;
+                        var selectionInfo = {};
+                        try {
+                            selectionInfo = {
+                                elt: activeElt,
+                                // @ts-ignore
+                                start: activeElt ? activeElt.selectionStart : null,
+                                // @ts-ignore
+                                end: activeElt ? activeElt.selectionEnd : null
+                            };
+                        } catch (e) {
+                            // safari issue - see https://github.com/microsoft/playwright/issues/5894
+                        }
+
+                        var settleInfo = makeSettleInfo(target);
+                        selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo);
+
+                        if (selectionInfo.elt &&
+                            !bodyContains(selectionInfo.elt) &&
+                            selectionInfo.elt.id) {
+                            var newActiveElt = document.getElementById(selectionInfo.elt.id);
+                            var focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll };
+                            if (newActiveElt) {
+                                // @ts-ignore
+                                if (selectionInfo.start && newActiveElt.setSelectionRange) {
+                                    // @ts-ignore
+                                    newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end);
+                                }
+                                newActiveElt.focus(focusOptions);
+                            }
+                        }
+
+                        target.classList.remove(htmx.config.swappingClass);
+                        forEach(settleInfo.elts, function (elt) {
+                            if (elt.classList) {
+                                elt.classList.add(htmx.config.settlingClass);
+                            }
+                            triggerEvent(elt, 'htmx:afterSwap', responseInfo);
+                        });
+
+                        if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
+                            var finalElt = elt;
+                            if (!bodyContains(elt)) {
+                                finalElt = getDocument().body;
+                            }
+                            handleTrigger(xhr, "HX-Trigger-After-Swap", finalElt);
+                        }
+
+                        var doSettle = function () {
+                            forEach(settleInfo.tasks, function (task) {
+                                task.call();
+                            });
+                            forEach(settleInfo.elts, function (elt) {
+                                if (elt.classList) {
+                                    elt.classList.remove(htmx.config.settlingClass);
+                                }
+                                triggerEvent(elt, 'htmx:afterSettle', responseInfo);
+                            });
+
+                            // if we need to save history, do so
+                            if (historyUpdate.type) {
+                                if (historyUpdate.type === "push") {
+                                    pushUrlIntoHistory(historyUpdate.path);
+                                    triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
+                                } else {
+                                    replaceUrlInHistory(historyUpdate.path);
+                                    triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path});
+                                }
+                            }
+                            if (responseInfo.pathInfo.anchor) {
+                                var anchorTarget = find("#" + responseInfo.pathInfo.anchor);
+                                if(anchorTarget) {
+                                    anchorTarget.scrollIntoView({block:'start', behavior: "auto"});
+                                }
+                            }
+
+                            if(settleInfo.title) {
+                                var titleElt = find("title");
+                                if(titleElt) {
+                                    titleElt.innerHTML = settleInfo.title;
+                                } else {
+                                    window.document.title = settleInfo.title;
+                                }
+                            }
+
+                            updateScrollState(settleInfo.elts, swapSpec);
+
+                            if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
+                                var finalElt = elt;
+                                if (!bodyContains(elt)) {
+                                    finalElt = getDocument().body;
+                                }
+                                handleTrigger(xhr, "HX-Trigger-After-Settle", finalElt);
+                            }
+                        }
+
+                        if (swapSpec.settleDelay > 0) {
+                            setTimeout(doSettle, swapSpec.settleDelay)
+                        } else {
+                            doSettle();
+                        }
+                    } catch (e) {
+                        triggerErrorEvent(elt, 'htmx:swapError', responseInfo);
+                        throw e;
+                    }
+                };
+
+                if (swapSpec.swapDelay > 0) {
+                    setTimeout(doSwap, swapSpec.swapDelay)
+                } else {
+                    doSwap();
+                }
+            }
+            if (isError) {
+                triggerErrorEvent(elt, 'htmx:responseError', mergeObjects({error: "Response Status Error Code " + xhr.status + " from " + responseInfo.pathInfo.path}, responseInfo));
+            }
+        }
+
+        //====================================================================
+        // Extensions API
+        //====================================================================
+
+        /** @type {Object<string, import("./htmx").HtmxExtension>} */
+        var extensions = {};
+
+        /**
+         * extensionBase defines the default functions for all extensions.
+         * @returns {import("./htmx").HtmxExtension}
+         */
+        function extensionBase() {
+            return {
+                init: function(api) {return null;},
+                onEvent : function(name, evt) {return true;},
+                transformResponse : function(text, xhr, elt) {return text;},
+                isInlineSwap : function(swapStyle) {return false;},
+                handleSwap : function(swapStyle, target, fragment, settleInfo) {return false;},
+                encodeParameters : function(xhr, parameters, elt) {return null;}
+            }
+        }
+
+        /**
+         * defineExtension initializes the extension and adds it to the htmx registry
+         *
+         * @param {string} name
+         * @param {import("./htmx").HtmxExtension} extension
+         */
+        function defineExtension(name, extension) {
+            if(extension.init) {
+                extension.init(internalAPI)
+            }
+            extensions[name] = mergeObjects(extensionBase(), extension);
+        }
+
+        /**
+         * removeExtension removes an extension from the htmx registry
+         *
+         * @param {string} name
+         */
+        function removeExtension(name) {
+            delete extensions[name];
+        }
+
+        /**
+         * getExtensions searches up the DOM tree to return all extensions that can be applied to a given element
+         *
+         * @param {HTMLElement} elt
+         * @param {import("./htmx").HtmxExtension[]=} extensionsToReturn
+         * @param {import("./htmx").HtmxExtension[]=} extensionsToIgnore
+         */
+         function getExtensions(elt, extensionsToReturn, extensionsToIgnore) {
+
+            if (elt == undefined) {
+                return extensionsToReturn;
+            }
+            if (extensionsToReturn == undefined) {
+                extensionsToReturn = [];
+            }
+            if (extensionsToIgnore == undefined) {
+                extensionsToIgnore = [];
+            }
+            var extensionsForElement = getAttributeValue(elt, "hx-ext");
+            if (extensionsForElement) {
+                forEach(extensionsForElement.split(","), function(extensionName){
+                    extensionName = extensionName.replace(/ /g, '');
+                    if (extensionName.slice(0, 7) == "ignore:") {
+                        extensionsToIgnore.push(extensionName.slice(7));
+                        return;
+                    }
+                    if (extensionsToIgnore.indexOf(extensionName) < 0) {
+                        var extension = extensions[extensionName];
+                        if (extension && extensionsToReturn.indexOf(extension) < 0) {
+                            extensionsToReturn.push(extension);
+                        }
+                    }
+                });
+            }
+            return getExtensions(parentElt(elt), extensionsToReturn, extensionsToIgnore);
+        }
+
+        //====================================================================
+        // Initialization
+        //====================================================================
+
+        function ready(fn) {
+            if (getDocument().readyState !== 'loading') {
+                fn();
+            } else {
+                getDocument().addEventListener('DOMContentLoaded', fn);
+            }
+        }
+
+        function insertIndicatorStyles() {
+            if (htmx.config.includeIndicatorStyles !== false) {
+                getDocument().head.insertAdjacentHTML("beforeend",
+                    "<style>\
+                      ." + htmx.config.indicatorClass + "{opacity:0;transition: opacity 200ms ease-in;}\
+                      ." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1}\
+                      ." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1}\
+                    </style>");
+            }
+        }
+
+        function getMetaConfig() {
+            var element = getDocument().querySelector('meta[name="htmx-config"]');
+            if (element) {
+                // @ts-ignore
+                return parseJSON(element.content);
+            } else {
+                return null;
+            }
+        }
+
+        function mergeMetaConfig() {
+            var metaConfig = getMetaConfig();
+            if (metaConfig) {
+                htmx.config = mergeObjects(htmx.config , metaConfig)
+            }
+        }
+
+        // initialize the document
+        ready(function () {
+            mergeMetaConfig();
+            insertIndicatorStyles();
+            var body = getDocument().body;
+            processNode(body);
+            var restoredElts = getDocument().querySelectorAll(
+                "[hx-trigger='restored'],[data-hx-trigger='restored']"
+            );
+            body.addEventListener("htmx:abort", function (evt) {
+                var target = evt.target;
+                var internalData = getInternalData(target);
+                if (internalData && internalData.xhr) {
+                    internalData.xhr.abort();
+                }
+            });
+            window.onpopstate = function (event) {
+                if (event.state && event.state.htmx) {
+                    restoreHistory();
+                    forEach(restoredElts, function(elt){
+                        triggerEvent(elt, 'htmx:restored', {
+                            'document': getDocument(),
+                            'triggerEvent': triggerEvent
+                        });
+                    });
+                }
+            };
+            setTimeout(function () {
+                triggerEvent(body, 'htmx:load', {}); // give ready handlers a chance to load up before firing this event
+            }, 0);
+        })
+
+        return htmx;
+    }
+)()
+}));

+ 68 - 0
contactsapp/static/js/rsjs-menu.js

@@ -0,0 +1,68 @@
+// @ts-nocheck
+
+function overflowMenu(subtree = document) {
+  subtree.querySelectorAll("[data-overflow-menu]").forEach(menuRoot => {
+    const
+    button = menuRoot.querySelector("[aria-haspopup]"),
+    menu = menuRoot.querySelector("[role=menu]"),
+    items = [...menu.querySelectorAll("[role=menuitem]")];
+
+    const isOpen = () => !menu.hidden;
+
+    items.forEach(item => item.setAttribute("tabindex", "-1"));
+
+    function toggleMenu(open = !isOpen()) {
+      if (open) {
+        menu.hidden = false;
+        button.setAttribute("aria-expanded", "true");
+        items[0].focus();
+      } else {
+        menu.hidden = true;
+        button.setAttribute("aria-expanded", "false");
+      }
+    }
+
+    toggleMenu(isOpen());
+    button.addEventListener("click", () => toggleMenu());
+    menuRoot.addEventListener("blur", e => console.log(e) || toggleMenu(false));
+
+    window.addEventListener("click", function clickAway(event) {
+      if (!menuRoot.isConnected) window.removeEventListener("click", clickAway);
+      if (!menuRoot.contains(event.target)) toggleMenu(false);
+    })
+
+    const currentIndex = () => {
+      const idx = items.indexOf(document.activeElement);
+      if (idx === -1) return 0;
+      return idx;
+    }
+
+    menuRoot.addEventListener("keydown", e => {
+      if (e.key === "ArrowUp") {
+        items[currentIndex() - 1]?.focus();
+
+      } else if (e.key === "ArrowDown") {
+        items[currentIndex() + 1]?.focus();
+
+      } else if (e.key === "Space") {
+        items[currentIndex()].click();
+
+      } else if (e.key === "Home") {
+        items[0].focus();
+
+      } else if (e.key === "End") {
+        items[items.length - 1].focus();
+
+      } else if (e.key === "Escape") {
+        toggleMenu(false);
+        button.focus();
+
+      } else if (e.key === "Tab") {
+        toggleMenu(false);
+      }
+    })
+  })
+}
+
+addEventListener("htmx:load", e => overflowMenu(e.target));
+

+ 65 - 0
contactsapp/static/site.css

@@ -0,0 +1,65 @@
+.flash {
+    display: block;
+    background-color: #2fdc2f !important;
+    color: white;
+    font-weight: bold;
+    padding: 12px;
+    border: 1px solid black;
+    border-radius: 8px;
+    margin: 16px;
+}
+
+table {
+    width: 100%;
+    margin-bottom: 12px;
+}
+
+.error {
+    display: inline-block;
+    color: darkred;
+}
+
+tr.htmx-swapping {
+  opacity: 0;
+  transition: opacity 1s ease-out;
+}
+
+td {
+    vertical-align: middle;
+}
+
+#download-ui {
+    margin-bottom: 16px;
+}
+
+.progress {
+    height: 20px;
+    margin-bottom: 20px;
+    overflow: hidden;
+    background-color: #f5f5f5;
+    border-radius: 4px;
+    box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
+}
+
+.progress-bar {
+    float: left;
+    width: 0%;
+    height: 100%;
+    font-size: 12px;
+    line-height: 20px;
+    color: #fff;
+    text-align: center;
+    background-color: #337ab7;
+    -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
+    box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
+    -webkit-transition: width .6s ease;
+    -o-transition: width .6s ease;
+    transition: width .6s ease;
+}
+
+[data-overflow-menu] {
+    visibility: hidden;
+}
+    tr:is(:hover, :focus-within) [data-overflow-menu] {
+        visibility: visible;
+    }

+ 19 - 0
contactsapp/templates/archive_ui.gohtml

@@ -0,0 +1,19 @@
+{{define "archive_ui.html"}}
+<div id="archive-ui" hx-target="this" hx-swap="outerHTML">
+    {% if archiver.status() == "Waiting" %}
+        <button hx-post="/contacts/archive">
+            Download Contact Archive
+        </button>
+    {% elif archiver.status() == "Running" %}
+        <div hx-get="/contacts/archive" hx-trigger="load delay:500ms">
+            Creating Archive...
+            <div class="progress" >
+                <div id="archive-progress" class="progress-bar" style="width:{{ mul .archiver.progress 100 }}%"></div>
+            </div>
+        </div>
+    {% elif archiver.status() == "Complete" %}
+        <a hx-boost="false" href="/contacts/archive/file" _="on load click() me">Archive Downloading!  Click here if the download does not start.</a>
+        <button hx-delete="/contacts/archive">Clear Download</button>
+    {% endif %}
+</div>
+{{end}}

+ 53 - 0
contactsapp/templates/edit.gohtml

@@ -0,0 +1,53 @@
+{{define "edit.html"}}
+{% extends 'layout.html' %}
+
+{{ block "content" . }}
+
+    <form action="/contacts/{{ .contact.id }}/edit" method="post">
+        <fieldset>
+            <legend>Contact Values</legend>
+            <div class="table rows">
+                <p>
+                    <label for="email">Email</label>
+                    <input name="email" id="email" type="email"
+                           hx-get="/contacts/{{ .contact.id }}/email" hx-target="next .error"
+                           hx-trigger="change, keyup delay:200ms"
+                           placeholder="Email" value="{{ .contact.email }}">
+                    <span class="error">{{ .contact.errors.email }}</span>
+                </p>
+                <p>
+                    <label for="first_name">First Name</label>
+                    <input name="first_name" id="first_name" type="text" placeholder="First Name"
+                           value="{{ .contact.first }}">
+                    <span class="error">{{ .contact.errors.first }}</span>
+                </p>
+                <p>
+                    <label for="last_name">Last Name</label>
+                    <input name="last_name" id="last_name" type="text" placeholder="Last Name"
+                           value="{{ .contact.last }}">
+                    <span class="error">{{ .contact.errors.last }}</span>
+                </p>
+                <p>
+                    <label for="phone">Phone</label>
+                    <input name="phone" id="phone" type="text" placeholder="Phone" value="{{ .contact.phone }}">
+                    <span class="error">{{ .contact.errors.phone }}</span>
+                </p>
+            </div>
+            <button>Save</button>
+        </fieldset>
+    </form>
+
+    <button id="delete-btn"
+            hx-delete="/contacts/{{ .contact.id }}"
+            hx-push-url="true"
+            hx-confirm="Are you sure you want to delete this contact?"
+            hx-target="body">
+        Delete Contact
+    </button>
+
+    <p>
+        <a href="/contacts">Back</a>
+    </p>
+
+{{ end }}
+{{end}}

+ 64 - 0
contactsapp/templates/index.gohtml

@@ -0,0 +1,64 @@
+{{define "index.html"}}
+{% extends 'layout.html' %}
+
+{{ block "content" . }}
+
+    {% include 'archive_ui.html' %}
+
+    <form action="/contacts" method="get" class="tool-bar">
+        <label for="search">Search Term</label>
+        <input id="search" type="search" name="q" value="{{ .request.args.get 'q' }}"
+               hx-get="/contacts"
+               hx-trigger="search, keyup delay:200ms changed"
+               hx-target="tbody"
+               hx-push-url="true"
+               hx-indicator="#spinner"/>
+        <img style="height: 20px" id="spinner" class="htmx-indicator" src="/static/img/spinning-circles.svg"/>
+        <input type="submit" value="Search"/>
+    </form>
+
+    <form x-data="{ selected: [] }">
+    <template
+        x-if="selected.length > 0">
+        <div class="box info tool-bar flxed top">
+            <slot x-text="selected.length"></slot>
+            contacts selected
+            
+            <button type="button" class="bad bg color border"
+                @click="confirm(`Delete ${selected.length} contacts?`) &&
+                    htmx.ajax('DELETE', '/contacts', { source: $root, target: document.body })"
+            >Delete</button>
+            <hr aria-orientation="vertical">
+            <button type="button" @click="selected = []">Cancel</button> 
+        </div>
+    </template>
+    <table>
+        <thead>
+        <tr>
+            <th></th>
+            <th>First</th>
+            <th>Last</th>
+            <th>Phone</th>
+            <th>Email</th>
+            <th></th>
+        </tr>
+        </thead>
+        <tbody>
+        {% include 'rows.html' %}
+        </tbody>
+    </table>
+        <button hx-delete="/contacts"
+                hx-confirm="Are you sure you want to delete these contacts?"
+                hx-target="body">
+            Delete Selected Contacts
+        </button>
+    </form>
+    <p>
+        <a href="/contacts/new">Add Contact</a>
+        <span hx-get="/contacts/count" hx-trigger="revealed">
+          <img id="spinner" style="height: 20px"  class="htmx-indicator" src="/static/img/spinning-circles.svg"/>
+        </span>
+    </p>
+
+{{ end }}
+{{end}}

+ 26 - 0
contactsapp/templates/layout.gohtml

@@ -0,0 +1,26 @@
+<!doctype html>
+<html lang="">
+<head>
+    <title>Contact App</title>
+    <link rel="stylesheet" href="https://the.missing.style/v0.2.0/missing.min.css">
+    <link rel="stylesheet" href="/static/site.css">
+    <script src="/static/js/htmx-1.8.0.js"></script>
+    <script src="/static/js/_hyperscript-0.9.7.js"></script>
+    <script src="/static/js/rsjs-menu.js" type="module"></script>
+    <script defer src="https://unpkg.com/alpinejs@3/dist/cdn.min.js"></script>
+</head>
+<body hx-boost="true">
+<main>
+    <header>
+        <h1>
+            <all-caps>contacts.app</all-caps>
+            <sub-title>A Demo Contacts Application</sub-title>
+        </h1>
+    </header>
+    {{ range $message := get_flashed_messages }}
+      <div class="flash">{{ $message }}</div>
+    {{ end }}
+    {{block "content" . }}{{ end }}
+</main>
+</body>
+</html>

+ 41 - 0
contactsapp/templates/new.gohtml

@@ -0,0 +1,41 @@
+{{define "new.html"}}
+{% extends 'layout.html' %}
+
+{{ block "content" . }}
+
+<form action="/contacts/new" method="post">
+    <fieldset>
+        <legend>Contact Values</legend>
+        <div class="table rows">
+            <p>
+                <label for="email">Email</label>
+                <input name="email" id="email" type="text" placeholder="Email" value="{{ .contact.email }}">
+                <span class="error">{{ .contact.errors.email }}</span>
+            </p>
+            <p>
+                <label for="first_name">First Name</label>
+                <input name="first_name" id="first_name" type="text" placeholder="First Name" value="{{ .contact.first }}">
+                <span class="error">{{ .contact.errors.first }}</span>
+            </p>
+            <p>
+                <label for="last_name">Last Name</label>
+                <input name="last_name" id="last_name" type="text" placeholder="Last Name" value="{{ .contact.last }}">
+                <span class="error">{{ .contact.errors.last }}</span>
+            </p>
+            <p>
+                <label for="phone">Phone</label>
+                <input name="phone" id="phone" type="text" placeholder="Phone" value="{{ .contact.phone }}">
+                <span class="error">{{ .contact.errors.phone }}</span>
+            </p>
+        </div>
+        <button>Save</button>
+    </fieldset>
+</form>
+
+<p>
+    <a href="/contacts">Back</a>
+</p>
+
+
+{{ end }}
+{{end}}

+ 28 - 0
contactsapp/templates/rows.gohtml

@@ -0,0 +1,28 @@
+{{define "rows.html"}}
+{% for contact in contacts %}
+    <tr>
+        <td><input type="checkbox" name="selected_contact_ids" value="{{ .contact.id }}"
+            x-model="selected"></td>
+        <td>{{ .contact.first }}</td>
+        <td>{{ .contact.last }}</td>
+        <td>{{ .contact.phone }}</td>
+        <td>{{ .contact.email }}</td>
+        <td>
+            <div data-overflow-menu>
+                <button type="button" aria-haspopup="menu"
+                    aria-controls="contact-menu-{{ .contact.id }}"
+                    >Options</button>
+                <div role="menu" hidden id="contact-menu-{{ .contact.id }}">
+                    <a role="menuitem" href="/contacts/{{ .contact.id }}/edit">Edit</a>
+                    <a role="menuitem" href="/contacts/{{ .contact.id }}">View</a>
+                    <a role="menuitem" href="#"
+                        hx-delete="/contacts/{{ .contact.id }}"
+                        hx-confirm="Are you sure you want to delete this contact?"
+                        hx-swap="outerHTML swap:1s"
+                        hx-target="closest tr">Delete</a>
+                </div>
+            </div>
+        </td>
+    </tr>
+{% endfor %}
+{{end}}

+ 20 - 0
contactsapp/templates/show.gohtml

@@ -0,0 +1,20 @@
+{{define "show.html"}}
+{% extends 'layout.html' %}
+
+{{ block "content" . }}
+
+<h1>{{ .contact.first}} {{ .contact.last}}</h1>
+
+<div>
+    <div>Phone: {{ .contact.phone}}</div>
+    <div>Email: {{ .contact.email}}</div>
+</div>
+
+<p>
+    <a href="/contacts/{{ .contact.id}}/edit">Edit</a>
+    <a href="/contacts">Back</a>
+</p>
+
+
+{{ end }}
+{{end}}

+ 1 - 1
doc/htmx-ch04.md

@@ -1,4 +1,4 @@
-# HTMX
+# HTMX chapter 4
 ## Attributes
 
 - `hx-[delete|get|patch|post|put]` - AJAX method to use

+ 7 - 0
doc/htmx-ch05.md

@@ -0,0 +1,7 @@
+# HTMX chapter 5
+## Attributes inheritance
+
+- "most" attributes in HTMX are inherited down the DOM, "like CSS styles".
+- children can override them
+
+

+ 15 - 0
go.mod

@@ -1,3 +1,18 @@
 module code.osinet.fr/fgm/demo-htmx
 
 go 1.22.2
+
+require github.com/Masterminds/sprig/v3 v3.2.3
+
+require (
+	github.com/Masterminds/goutils v1.1.1 // indirect
+	github.com/Masterminds/semver/v3 v3.2.0 // indirect
+	github.com/google/uuid v1.1.1 // indirect
+	github.com/huandu/xstrings v1.3.3 // indirect
+	github.com/imdario/mergo v0.3.11 // indirect
+	github.com/mitchellh/copystructure v1.0.0 // indirect
+	github.com/mitchellh/reflectwalk v1.0.0 // indirect
+	github.com/shopspring/decimal v1.2.0 // indirect
+	github.com/spf13/cast v1.3.1 // indirect
+	golang.org/x/crypto v0.3.0 // indirect
+)

+ 62 - 0
go.sum

@@ -0,0 +1,62 @@
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
+github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
+github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
+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/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
+github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+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/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
+golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

+ 10 - 10
web/public/00.html

@@ -1,19 +1,19 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-    <meta charset="UTF-8">
-    <title id="page-title">HTMX Demos</title>
-    <script src="htmx-1-9-12.min.js"></script>
-    <link rel="stylesheet" href="styles.css" />
+  <meta charset="UTF-8">
+  <title id="page-title">HTMX | Chapter 5 demos</title>
+  <script src="js/htmx-1-9-12.min.js"></script>
+  <link rel="stylesheet" href="css/styles.css"/>
 </head>
 <body>
-<h1 id="title">Demo</h1>
-<h2>Default: swap current element</h2>
-
+<h1 id="title">Chapter 5 demos</h1>
+<h2>Boost anchor tag</h2>
 <nav hx-get="/autonum/nav" hx-trigger="load"></nav>
 
-<button hx-post="/contacts" hx-swap="innerHTML">
-    Contacts
-</button>
+<div id="main">
+  <p><a href="/settings" hx-boost="true">Settings</a></p>
+  <p>Observe how the title is updated from the settings template, but the rest of the &lt;head&gt; is not.</p>
+</div>
 </body>
 </html>

+ 11 - 11
web/public/01.html

@@ -1,22 +1,22 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-    <meta charset="UTF-8">
-    <title id="page-title">HTMX Demos</title>
-    <script src="htmx-1-9-12.min.js"></script>
-    <link rel="stylesheet" href="styles.css" />
+  <meta charset="UTF-8">
+  <title id="page-title">HTMX | Chapter 5 demos</title>
+  <script src="js/htmx-1-9-12.min.js"></script>
+  <link rel="stylesheet" href="css/styles.css"/>
 </head>
 <body>
-<h1 id="title">Demo</h1>
-<h2>hx-target : target another element</h2>
-
+<h1 id="title">Chapter 5 demos</h1>
+<h2>Boost form</h2>
 <nav hx-get="/autonum/nav" hx-trigger="load"></nav>
 
 <div id="main">
-    <div class="list"></div>
-    <button hx-post="/contacts" hx-target="#main .list" hx-swap="innerHTML">
-        Contacts
-    </button>
+  <form action="/messages" method="post" hx-boost="true">
+    <input type="text" name="message" placeholder="Enter A Message...">
+    <button>Post Your Message</button>
+  </form>
+
 </div>
 </body>
 </html>

+ 20 - 11
web/public/02.html

@@ -1,22 +1,31 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-    <meta charset="UTF-8">
-    <title id="page-title">HTMX Demos</title>
-    <script src="htmx-1-9-12.min.js"></script>
-    <link rel="stylesheet" href="styles.css" />
+  <meta charset="UTF-8">
+  <title id="page-title">HTMX | Chapter 5 demos</title>
+  <script src="js/htmx-1-9-12.min.js"></script>
+  <link rel="stylesheet" href="css/styles.css"/>
 </head>
 <body>
-<h1 id="title">Demo</h1>
-<h2>hx-trigger: trigger on mouseenter</h2>
-
+<h1 id="title">Chapter 5 demos</h1>
+<h2>Attribute inheritance with <code>hx-boost</code></h2>
 <nav hx-get="/autonum/nav" hx-trigger="load"></nav>
 
 <div id="main">
-    <div class="list"></div>
-    <button hx-post="/contacts" hx-target="#main .list" hx-swap="innerHTML" hx-trigger="mouseenter">
-        Contacts
-    </button>
+  <ul hx-boost="true" hx-target="#main">
+    <li><a href="/contacts">Contacts</a></li>
+    <li><a href="/settings">Settings</a></li>
+    <li><a href="/help">Help</a></li>
+    <li><a href="/blank.pdf" hx-boost="false">Download Blank docs</a></li>
+  </ul>
 </div>
+<p>Notice :</p>
+<ul>
+  <li>how the successful first 2 links replace the div#main</li>
+  <li>how the 404 on Help does not replace it like a plain a.href normally would,
+    but the others do.
+  </li>
+  <li>How the PDF download is not an AJAX call</li>
+</ul>
 </body>
 </html>

+ 0 - 26
web/public/03.html

@@ -1,26 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title id="page-title">HTMX Demos</title>
-    <script src="htmx-1-9-12.min.js"></script>
-    <link rel="stylesheet" href="styles.css"/>
-</head>
-<body>
-<h1 id="title">Demo</h1>
-<h2>hx-trigger: <code>^L</code> keyboard shortcut</h2>
-
-<nav hx-get="/autonum/nav" hx-trigger="load"></nav>
-
-<div id="main">
-    <div class="list"></div>
-    <button
-            hx-post="/contacts"
-            hx-target="#main .list"
-            hx-swap="innerHTML"
-            hx-trigger="click,keyup[ctrlKey && key == 'l'] from:body">
-        Contacts
-    </button>
-</div>
-</body>
-</html>

+ 0 - 26
web/public/04.html

@@ -1,26 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title id="page-title">HTMX Demos</title>
-    <script src="htmx-1-9-12.min.js"></script>
-    <link rel="stylesheet" href="styles.css"/>
-</head>
-<body>
-<h1 id="title">Demo</h1>
-<h2>Forms</h2>
-
-<nav hx-get="/autonum/nav" hx-trigger="load"></nav>
-
-<div id="main">
-    <form>
-        <label for="search">Search Contacts:</label>
-        <input id="search" name="q" type="search" placeholder="Search Contacts">
-        <button hx-post="/search" hx-target="#main .results">
-            Search The Contacts
-        </button>
-    </form>
-    <div class="results"></div>
-</div>
-</body>
-</html>

+ 0 - 26
web/public/05.html

@@ -1,26 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title id="page-title">HTMX Demos</title>
-    <script src="htmx-1-9-12.min.js"></script>
-    <link rel="stylesheet" href="styles.css"/>
-</head>
-<body>
-<h1 id="title">Demo</h1>
-<h2>Including inputs without forms</h2>
-
-<nav hx-get="/autonum/nav" hx-trigger="load"></nav>
-
-<div id="main">
-    <label for="search">Search Contacts:</label>
-    <input id="search" name="q" type="search" placeholder="Search Contacts">
-    <button hx-get="/search"
-            hx-target="#main .results"
-            hx-include="#search">
-        Search The Contacts
-    </button>
-    <div class="results"></div>
-</div>
-</body>
-</html>

+ 0 - 27
web/public/06.html

@@ -1,27 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title id="page-title">HTMX Demos</title>
-    <script src="htmx-1-9-12.min.js"></script>
-    <link rel="stylesheet" href="styles.css"/>
-</head>
-<body>
-<h1 id="title">Demo</h1>
-<h2>Including static values with <code>hx-vals</code></h2>
-
-<nav hx-get="/autonum/nav" hx-trigger="load"></nav>
-
-<div id="main">
-    <label for="search">Search Contacts:</label>
-    <input id="search" name="q" type="search" placeholder="Search Contacts">
-    <button hx-get="/search"
-            hx-target="#main .results"
-            hx-include="#search"
-            hx-vals='{"state":"Montana"}'>
-        Search The Contacts
-    </button>
-    <div class="results"></div>
-</div>
-</body>
-</html>

+ 0 - 32
web/public/07.html

@@ -1,32 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-  <meta charset="UTF-8">
-  <title id="page-title">HTMX Demos</title>
-  <script src="htmx-1-9-12.min.js"></script>
-  <link rel="stylesheet" href="styles.css"/>
-  <script>
-    function getCurrentState() {
-      return "California";
-    }
-  </script>
-</head>
-<body>
-<h1 id="title">Demo</h1>
-<h2>Including JavaScript values with <code>hx-vals</code></h2>
-
-<nav hx-get="/autonum/nav" hx-trigger="load"></nav>
-
-<div id="main">
-  <label for="search">Search Contacts:</label>
-  <input id="search" name="q" type="search" placeholder="Search Contacts">
-  <button hx-get="/search"
-          hx-target="#main .results"
-          hx-include="#search"
-          hx-vals='js:{"state":getCurrentState()}'>
-    Search The Contacts
-  </button>
-  <div class="results"></div>
-</div>
-</body>
-</html>

+ 0 - 34
web/public/08.html

@@ -1,34 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-  <meta charset="UTF-8">
-  <title id="page-title">HTMX Demos</title>
-  <script src="htmx-1-9-12.min.js"></script>
-  <link rel="stylesheet" href="styles.css"/>
-</head>
-<body>
-<h1 id="title">Demo</h1>
-<h2>Using history with <code>hx-push-url</code></h2>
-
-<nav hx-get="/autonum/nav" hx-trigger="load"></nav>
-
-<div id="main">
-  <label for="search">Search Contacts:</label>
-  <input id="search" name="q" type="search" placeholder="Search Contacts">
-  <button hx-get="/search"
-          hx-target="#main .results"
-          hx-include="#search"
-          hx-push-url="true"
-  >
-    Search The Contacts
-  </button>
-  <div class="results"></div>
-  <p>Usage:</p>
-  <ul>
-    <li>search on some criteria a couple of times,</li>
-    <li>then use the browser &larr;/&rarr; buttons button. Yay ! 😊</li>
-    <li>Now refresh the page. Boo 😕 : we get only the partial response, because the pushed URL is the one for the <code>hx-post</code></li>
-  </ul>
-</div>
-</body>
-</html>

BIN
web/public/blank.pdf


+ 0 - 0
web/public/styles.css → web/public/css/styles.css


+ 0 - 0
web/public/head-support.js → web/public/js/head-support.js


+ 0 - 0
web/public/htmx-1-9-12.min.js → web/public/js/htmx-1-9-12.min.js


+ 12 - 0
web/public/layout.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <title>HTMX | Chapter 5 demos</title>
+  <script src="js/htmx-1-9-12.min.js"></script>
+  <link rel="stylesheet" href="css/styles.css"/>
+</head>
+<body hx-boost="true">
+<a href="/contacts/new">Add Contact</a>
+</body>
+</html>

+ 0 - 0
web/public/contacts.gohtml → web/templates/contacts.gohtml


+ 3 - 0
web/templates/messages.gohtml

@@ -0,0 +1,3 @@
+{{ define "messages" }}
+  You said {{ .message }}
+{{ end }}

+ 0 - 0
web/public/nav.gohtml → web/templates/nav.gohtml


+ 15 - 0
web/templates/settings.gohtml

@@ -0,0 +1,15 @@
+{{ define "settings" }}
+  <!DOCTYPE html>
+  <html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <title id="page-title">HTMX | Partial</title>
+  </head>
+  <body>
+    <form>
+      <label for="text1" title="Title for text1" content="Content" about="About" />
+      <input type="text" id="text1" name="text1" placeholder="some setting value" value="{{ .text1 }}" />
+      <button id="button1" name="button1" value="A button">Button text</button>
+    </form>
+  </body>
+{{ end }}

+ 36 - 7
web/web.go

@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 	"html/template"
+	"log"
 	"net/http"
 	"net/url"
 	"path/filepath"
@@ -11,12 +12,16 @@ import (
 	"strings"
 )
 
+type (
+	gen map[string]any
+)
+
 var (
 	counter int = 1
 )
 
-func getContacts() map[string]any {
-	return map[string]any{
+func getContacts() gen {
+	return gen{
 		"Joe":   "joe@example.com",
 		"Sarah": "sarah@example.com",
 		"Fred":  "fred@example.com",
@@ -25,7 +30,7 @@ func getContacts() map[string]any {
 
 func makeContacts1(templates *template.Template) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		templates.ExecuteTemplate(w, "contacts", map[string]any{
+		templates.ExecuteTemplate(w, "contacts", gen{
 			"contacts": getContacts(),
 			"counter":  counter,
 		})
@@ -51,7 +56,7 @@ func makeNav(templates *template.Template) http.HandlerFunc {
 		}
 		switch path {
 		case "nav":
-			templates.ExecuteTemplate(w, "nav", map[string]any{
+			templates.ExecuteTemplate(w, "nav", gen{
 				"num":  num,
 				"prev": fmt.Sprintf("%02d", num-1),
 				"curr": fmt.Sprintf("%2d", num),
@@ -69,28 +74,52 @@ func makeSearch4(templates *template.Template) http.HandlerFunc {
 		q := r.FormValue("q")
 		state := r.FormValue("state")
 		raw := getContacts()
-		filtered := map[string]any{}
+		filtered := gen{}
 		for k, v := range raw {
 			if strings.Contains(strings.ToUpper(k), strings.ToUpper(q)) {
 				filtered[k] = v
 			}
 		}
-		templates.ExecuteTemplate(w, "contacts", map[string]any{
+		templates.ExecuteTemplate(w, "contacts", gen{
 			"contacts": filtered,
 			"state":    state,
 		})
 	}
 }
 
+func makeMessages(templates *template.Template) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		message := r.FormValue("message")
+		log.Printf("%s %s: %q", r.Method, r.URL.Path, message)
+		if err := templates.ExecuteTemplate(w, "messages", gen{
+			"message": message,
+		}); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+	}
+}
+
+func makeSettings(templates *template.Template) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		if err := templates.ExecuteTemplate(w, "settings", gen{
+			"text1": "some value for text1",
+		}); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+	}
+}
+
 func SetupRoutes(mux *http.ServeMux, templates *template.Template) {
 	mux.Handle("/", http.FileServer(http.Dir("./web/public/")))
 	mux.HandleFunc("/autonum/", makeNav(templates))
 	mux.HandleFunc("/contacts", makeContacts1(templates))
+	mux.HandleFunc("/messages", makeMessages(templates))
 	mux.HandleFunc("/search", makeSearch4(templates))
+	mux.HandleFunc("/settings", makeSettings(templates))
 }
 
 func UI(addr string) error {
-	templates := template.Must(template.ParseGlob("./web/public/*.gohtml"))
+	templates := template.Must(template.ParseGlob("./web/templates/*.gohtml"))
 	mux := http.NewServeMux()
 	SetupRoutes(mux, templates)
 	if err := http.ListenAndServe(addr, mux); err != nil {

Деякі файли не було показано, через те що забагато файлів було змінено