feat(frameworks): add htmx detector (#188)

This commit is contained in:
Tigah
2026-06-22 17:28:24 -07:00
committed by GitHub
parent c03fa7d336
commit 657cb0cbb8
3 changed files with 85 additions and 0 deletions
+52
View File
@@ -704,3 +704,55 @@ func TestDetectorRegistry(t *testing.T) {
}
}
}
func TestExtractVersion_Htmx(t *testing.T) {
tests := []struct {
body string
expected string
}{
{`<script src="https://unpkg.com/htmx.org@1.9.10"></script>`, "1.9.10"},
{`https://cdn.jsdelivr.net/npm/htmx@2.0.3/dist/htmx.min.js`, "2.0.3"},
{`"htmx.org": "^1.9.12"`, "1.9.12"},
{"no version", "unknown"},
}
for _, tt := range tests {
result := frameworks.ExtractVersionOptimized(tt.body, "htmx").Version
if result != tt.expected {
t.Errorf("ExtractVersionOptimized(%q, 'htmx') = %q, want %q", tt.body, result, tt.expected)
}
}
}
func TestDetectFramework_Htmx(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head>
<body>
<button hx-get="/clicked" hx-target="#out" hx-swap="outerHTML">Click</button>
<form hx-post="/submit" hx-boost="true"></form>
<div id="out"></div>
</body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "htmx" {
t.Errorf("expected framework 'htmx', got '%s'", result.Name)
}
if result.Version != "1.9.10" {
t.Errorf("expected version '1.9.10', got '%s'", result.Version)
}
}
@@ -34,6 +34,7 @@ func init() {
fw.Register(&emberDetector{})
fw.Register(&backboneDetector{})
fw.Register(&meteorDetector{})
fw.Register(&htmxDetector{})
}
// reactDetector detects React framework.
@@ -195,6 +196,34 @@ func (d *backboneDetector) Detect(body string, headers http.Header) (float32, st
return confidence, version
}
// htmxDetector detects the htmx library.
type htmxDetector struct{}
func (d *htmxDetector) Name() string { return "htmx" }
func (d *htmxDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "hx-get", Weight: 0.5},
{Pattern: "hx-post", Weight: 0.5},
{Pattern: "hx-swap", Weight: 0.4},
{Pattern: "hx-target", Weight: 0.4},
{Pattern: "hx-boost", Weight: 0.4},
{Pattern: "htmx.org", Weight: 0.5},
}
}
func (d *htmxDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// meteorDetector detects Meteor framework.
type meteorDetector struct{}
+4
View File
@@ -107,6 +107,10 @@ func init() {
"SvelteKit": {
{`"@sveltejs/kit":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"htmx": {
{`htmx(?:\.org)?@(\d+\.\d+(?:\.\d+)?)`, 0.85, "CDN reference"},
{`"htmx\.org":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"WordPress": {
{`<meta name="generator" content="WordPress (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
{`WordPress (\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},