package main import ( "context" "errors" "flag" "fmt" "io" "net/http" "net/url" "os" "regexp" "sort" "strconv" "strings" "time" ) const ( kindCPU = "cpu" kindGPU = "gpu" ) type options struct { kind string queries []string } type result struct { Kind string Name string URL string Stats []stat } type stat struct { Name string Value string } type client struct { http *http.Client } var ( letterNumberRe = regexp.MustCompile(`([a-z])([0-9])`) numberLetterRe = regexp.MustCompile(`([0-9])([a-z])`) nonWordRe = regexp.MustCompile(`[^a-z0-9]+`) ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*m`) scriptRe = regexp.MustCompile(`(?is)]*>.*?`) styleRe = regexp.MustCompile(`(?is)]*>.*?`) tagRe = regexp.MustCompile(`(?s)<[^>]+>`) tokenReplacer = strings.NewReplacer("+", " plus ", "-", " ", "_", " ", "(", " ", ")", " ", "/", " ") ) const ( colorReset = "\x1b[0m" colorBold = "\x1b[1m" colorDim = "\x1b[2m" colorGreen = "\x1b[32m" colorRed = "\x1b[31m" ) func main() { opts, err := parseArgs(os.Args[1:]) if err != nil { printUsage(os.Stderr) exitErr(err) } ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() c := &client{http: &http.Client{Timeout: 15 * time.Second}} results := make([]result, 0, len(opts.queries)) for _, query := range opts.queries { res, err := c.passmark(ctx, opts.kind, query) if err != nil { exitErr(err) } results = append(results, res) } if len(results) > 1 { printComparison(os.Stdout, results...) return } printResult(os.Stdout, results[0]) } func parseArgs(args []string) (options, error) { fs := flag.NewFlagSet("passctl", flag.ContinueOnError) fs.SetOutput(io.Discard) cpu := fs.Bool("cpu", false, "search PassMark CPUs") gpu := fs.Bool("gpu", false, "search PassMark GPUs") longHelp := fs.Bool("help", false, "show help") shortHelp := fs.Bool("h", false, "show help") if err := fs.Parse(args); err != nil { return options{}, err } if *shortHelp || *longHelp { printUsage(os.Stdout) os.Exit(0) } rest := fs.Args() if len(rest) < 1 { return options{}, errors.New("expected at least one search query") } var opts options if *cpu && *gpu { return options{}, errors.New("choose only one target: --cpu or --gpu") } if *cpu { opts.kind = kindCPU } if *gpu { opts.kind = kindGPU } if opts.kind == "" { return options{}, errors.New("missing target: use --cpu or --gpu") } for _, query := range rest { query = strings.TrimSpace(query) if query == "" { return options{}, errors.New("query cannot be empty") } opts.queries = append(opts.queries, query) } return opts, nil } func printUsage(w io.Writer) { fmt.Fprintln(w, "Usage:") fmt.Fprintln(w, ` passctl --cpu "Ryzen 3600x"`) fmt.Fprintln(w, ` passctl --cpu "Ryzen 3600x" "Core i7-10700"`) fmt.Fprintln(w, ` passctl --cpu "Ryzen 3600x" "Core i7-10700" "Core i5-12400"`) fmt.Fprintln(w, ` passctl --gpu "Radeon RX 6650 XT"`) fmt.Fprintln(w, ` passctl --gpu "Radeon RX 6650 XT" "GeForce RTX 3060"`) fmt.Fprintln(w) fmt.Fprintln(w, "Options:") fmt.Fprintln(w, " --cpu Search PassMark CPU benchmarks") fmt.Fprintln(w, " --gpu Search PassMark GPU benchmarks") fmt.Fprintln(w) fmt.Fprintln(w, "Passing multiple queries prints a color-coded comparison.") } func exitErr(err error) { fmt.Fprintf(os.Stderr, "passctl: %v\n", err) os.Exit(1) } func printResult(w io.Writer, res result) { fmt.Fprintf(w, "PassMark %s: %s\n", strings.ToUpper(res.Kind), res.Name) if res.URL != "" { fmt.Fprintf(w, "URL: %s\n", res.URL) } fmt.Fprintln(w) width := 0 for _, s := range res.Stats { if len(s.Name) > width { width = len(s.Name) } } for _, s := range res.Stats { fmt.Fprintf(w, "%-*s %s\n", width, s.Name+":", s.Value) } } func printComparison(w io.Writer, results ...result) { if len(results) == 0 { return } fmt.Fprintf(w, "%sPassMark %s Comparison%s\n", colorBold, strings.ToUpper(results[0].Kind), colorReset) names := make([]string, 0, len(results)) for _, res := range results { names = append(names, res.Name) } fmt.Fprintf(w, "%s\n", strings.Join(names, " vs ")) for i, res := range results { if res.URL != "" { fmt.Fprintf(w, "%s%s:%s %s\n", colorDim, resultLabel(i), colorReset, res.URL) } } fmt.Fprintln(w) rows := comparisonRows(results) if len(rows) == 0 { fmt.Fprintln(w, "No comparable stats found.") return } headers := []string{"Metric"} for _, res := range results { headers = append(headers, res.Name) } headers = append(headers, "Better") widths := make([]int, len(headers)) for i, header := range headers { widths[i] = visibleLen(header) } for _, row := range rows { for i, cell := range row { if l := visibleLen(cell); l > widths[i] { widths[i] = l } } } printTableRule(w, widths) printTableRow(w, headers, widths) printTableRule(w, widths) for _, row := range rows { printTableRow(w, row, widths) } printTableRule(w, widths) } func resultLabel(i int) string { if i < 26 { return string(rune('A' + i)) } return fmt.Sprintf("Q%d", i+1) } func comparisonRows(results []result) [][]string { stats := make([]map[string]string, 0, len(results)) for _, res := range results { stats = append(stats, statsMap(res.Stats)) } ordered := comparisonStatOrder(stats...) var rows [][]string for _, name := range ordered { row, ok := compareRow(name, stats, results) if !ok { continue } rows = append(rows, row) } return rows } func statsMap(stats []stat) map[string]string { out := make(map[string]string) for _, s := range stats { out[strings.ToLower(s.Name)] = s.Value } addAlias := func(from, to string) { if v, ok := out[from]; ok { if _, exists := out[to]; !exists { out[to] = v } } } addAlias("average cpu mark", "passmark score") addAlias("average g3d mark", "passmark score") addAlias("average g2d mark", "passmark score") addAlias("cpumark/$price", "value") addAlias("single thread", "single thread rating") return out } func comparisonStatOrder(stats ...map[string]string) []string { preferred := []string{ "PassMark Score", "Single Thread Rating", "Value", "Price", "Rank", "Samples", "CPU First Seen on Charts", "CPU Released", "Release Date", "Launched", } seen := make(map[string]bool) var out []string for _, name := range preferred { key := strings.ToLower(name) for _, statMap := range stats { if _, ok := statMap[key]; ok { out = append(out, name) seen[key] = true break } } } var rest []string for _, statMap := range stats { for key := range statMap { if !seen[key] && !representedAlias(key, seen) { rest = append(rest, key) } } } sort.Strings(rest) for _, key := range rest { if seen[key] { continue } seen[key] = true out = append(out, titleStatName(key, stats...)) } return out } func representedAlias(key string, seen map[string]bool) bool { aliases := map[string]string{ "average cpu mark": "passmark score", "average g3d mark": "passmark score", "average g2d mark": "passmark score", "cpumark/$price": "value", "single thread": "single thread rating", } canonical, ok := aliases[key] return ok && seen[canonical] } func titleStatName(key string, stats ...map[string]string) string { for _, s := range stats { for name := range s { if name == key { parts := strings.Fields(name) for i := range parts { parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:] } return strings.Join(parts, " ") } } } return key } func compareRow(name string, stats []map[string]string, results []result) ([]string, bool) { key := strings.ToLower(name) values := make([]string, len(stats)) present := make([]bool, len(stats)) anyPresent := false for i, statMap := range stats { values[i], present[i] = statMap[key] anyPresent = anyPresent || present[i] } if !anyPresent { return nil, false } cells := make([]string, len(values)) copy(cells, values) for i := range cells { if !present[i] { cells[i] = colorDim + "n/a" + colorReset } } winners, comparable := winningIndexes(name, values, present) better := colorDim + "n/a" + colorReset if comparable { if len(winners) == countPresent(present) { better = colorDim + "same" + colorReset } else { for i := range cells { if !present[i] { continue } if containsIndex(winners, i) { cells[i] = colorGreen + cells[i] + colorReset } else { cells[i] = colorRed + cells[i] + colorReset } } names := make([]string, 0, len(winners)) for _, idx := range winners { names = append(names, results[idx].Name) } better = strings.Join(names, ", ") } } row := []string{name} row = append(row, cells...) row = append(row, better) return row, true } func winningIndexes(name string, values []string, present []bool) ([]int, bool) { best := -1 for i := range values { if !present[i] { continue } if best == -1 { best = i continue } cmp, ok := compareStat(name, values[i], values[best]) if !ok { return nil, false } if cmp > 0 { best = i } } if best == -1 { return nil, false } winners := []int{best} for i := range values { if i == best || !present[i] { continue } cmp, ok := compareStat(name, values[i], values[best]) if !ok { return nil, false } if cmp == 0 { winners = append(winners, i) } } sort.Ints(winners) return winners, true } func countPresent(present []bool) int { count := 0 for _, ok := range present { if ok { count++ } } return count } func containsIndex(indexes []int, needle int) bool { for _, idx := range indexes { if idx == needle { return true } } return false } func compareCells(name, lv, rv, leftName, rightName string, lok, rok bool) (string, string, string) { if !lok { return colorDim + "n/a" + colorReset, rv, colorDim + "n/a" + colorReset } if !rok { return lv, colorDim + "n/a" + colorReset, colorDim + "n/a" + colorReset } cmp, ok := compareStat(name, lv, rv) if !ok || cmp == 0 { better := colorDim + "n/a" + colorReset if strings.EqualFold(lv, rv) { better = colorDim + "same" + colorReset } return lv, rv, better } if cmp > 0 { return colorGreen + lv + colorReset, colorRed + rv + colorReset, leftName } return colorRed + lv + colorReset, colorGreen + rv + colorReset, rightName } func compareStat(name, left, right string) (int, bool) { lower := strings.ToLower(name) if strings.Contains(lower, "date") || strings.Contains(lower, "released") || strings.Contains(lower, "launched") || strings.Contains(lower, "first seen") { lv, lok := parseDateValue(left) rv, rok := parseDateValue(right) if lok && rok { return compareTime(lv, rv), true } } if !numericComparableStat(lower) { return 0, false } lv, lok := parseNumberValue(left) rv, rok := parseNumberValue(right) if !lok || !rok { return 0, false } cmp := compareFloat(lv, rv) if lowerBetterStat(lower) { cmp = -cmp } return cmp, true } func numericComparableStat(lower string) bool { terms := []string{ "score", "mark", "rating", "value", "price", "rank", "samples", "clock", "speed", "core", "thread", "tdp", "power", "compression", "encryption", "instructions", "math", "physics", "sorting", "prime", "fps", "frames", "watt", } for _, term := range terms { if strings.Contains(lower, term) { return true } } return false } func lowerBetterStat(lower string) bool { return strings.Contains(lower, "price") || strings.Contains(lower, "rank") || strings.Contains(lower, "cost") || strings.Contains(lower, "tdp") || strings.Contains(lower, "power") } func parseNumberValue(s string) (float64, bool) { re := regexp.MustCompile(`-?\d[\d,]*(?:\.\d+)?`) match := re.FindString(s) if match == "" { return 0, false } match = strings.ReplaceAll(match, ",", "") v, err := strconv.ParseFloat(match, 64) return v, err == nil } func parseDateValue(s string) (time.Time, bool) { s = strings.TrimSpace(s) quarterRe := regexp.MustCompile(`(?i)^q([1-4])\s+(\d{4})$`) if m := quarterRe.FindStringSubmatch(s); len(m) == 3 { quarter, _ := strconv.Atoi(m[1]) year, _ := strconv.Atoi(m[2]) month := time.Month(((quarter - 1) * 3) + 1) return time.Date(year, month, 1, 0, 0, 0, 0, time.UTC), true } layouts := []string{ "Jan 2006", "January 2006", "Jan 2, 2006", "January 2, 2006", "2006-01-02", "2006", } for _, layout := range layouts { if t, err := time.Parse(layout, s); err == nil { return t, true } } return time.Time{}, false } func compareFloat(left, right float64) int { if left > right { return 1 } if left < right { return -1 } return 0 } func compareTime(left, right time.Time) int { if left.After(right) { return 1 } if left.Before(right) { return -1 } return 0 } func printTableRule(w io.Writer, widths []int) { fmt.Fprint(w, "+") for _, width := range widths { fmt.Fprint(w, strings.Repeat("-", width+2), "+") } fmt.Fprintln(w) } func printTableRow(w io.Writer, cells []string, widths []int) { fmt.Fprint(w, "|") for i, cell := range cells { fmt.Fprintf(w, " %s%s |", cell, strings.Repeat(" ", widths[i]-visibleLen(cell))) } fmt.Fprintln(w) } func visibleLen(s string) int { return len(ansiRe.ReplaceAllString(s, "")) } func (c *client) passmark(ctx context.Context, kind, query string) (result, error) { baseHost := "www.cpubenchmark.net" listPath := "/cpu-list/all" detailPaths := []string{"/cpu.php", "/cpu_lookup.php"} if kind == kindGPU { baseHost = "www.videocardbenchmark.net" listPath = "/gpu_list.php" detailPaths = []string{"/gpu.php", "/gpu_lookup.php", "video_lookup.php", "/video_lookup.php"} } if kind == kindCPU { directURL := (&url.URL{Scheme: "https", Host: baseHost, Path: "/cpu.php", RawQuery: "cpu=" + url.QueryEscape(query)}).String() if direct, err := c.get(ctx, directURL); err == nil { name, stats := parsePassMarkDetail(direct) if name != "" && len(stats) > 0 { return result{Kind: kind, Name: name, URL: directURL, Stats: stats}, nil } } } listURL := (&url.URL{Scheme: "https", Host: baseHost, Path: listPath}).String() body, err := c.get(ctx, listURL) if err != nil { return result{}, err } candidates := passmarkCandidates(body, baseHost, detailPaths) best, ok := bestCandidate(query, candidates) if !ok { return result{}, fmt.Errorf("no PassMark %s match found for %q", kind, query) } if len(best.Stats) > 0 { detailURL := canonicalDetailURL(kind, best.URL) detail, err := c.get(ctx, detailURL) if err != nil { detailURL = best.URL detail, err = c.get(ctx, detailURL) if err != nil { return result{Kind: kind, Name: best.Name, URL: best.URL, Stats: best.Stats}, nil } } name, stats := parsePassMarkDetail(detail) if name == "" { name = best.Name } return result{Kind: kind, Name: name, URL: detailURL, Stats: mergeStats(best.Stats, stats)}, nil } detail, err := c.get(ctx, best.URL) if err != nil { return result{}, err } name, stats := parsePassMarkDetail(detail) if name == "" { name = best.Name } if len(stats) == 0 { return result{}, fmt.Errorf("PassMark page was fetched but no benchmark stats could be parsed: %s", best.URL) } return result{Kind: kind, Name: name, URL: best.URL, Stats: stats}, nil } func canonicalDetailURL(kind, rawURL string) string { u, err := url.Parse(rawURL) if err != nil { return rawURL } switch kind { case kindCPU: u.Path = "/cpu.php" case kindGPU: u.Path = "/gpu.php" } return u.String() } func (c *client) get(ctx context.Context, rawURL string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil) if err != nil { return "", err } req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36 passctl/0.1") req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") req.Header.Set("Accept-Language", "en-US,en;q=0.9") resp, err := c.http.Do(req) if err != nil { return "", err } defer resp.Body.Close() data, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) if err != nil { return "", err } body := string(data) if resp.StatusCode >= 400 { return "", fmt.Errorf("%s returned %s", rawURL, resp.Status) } if isBlocked(body) { return "", fmt.Errorf("%s returned an anti-bot/challenge page; try again later or from a network/browser session the site accepts", rawURL) } return body, nil } func isBlocked(body string) bool { lower := strings.ToLower(body) return strings.Contains(lower, "403 forbidden") || strings.TrimSpace(lower) == "forbidden" || strings.Contains(lower, "just a moment") || strings.Contains(lower, "enable javascript and cookies") || strings.Contains(lower, "cf-chl") } type candidate struct { Name string URL string Stats []stat } func passmarkCandidates(body, host string, detailPaths []string) []candidate { var pathAlternates []string for _, p := range detailPaths { pathAlternates = append(pathAlternates, regexp.QuoteMeta(p)) } pattern := fmt.Sprintf(`(?is)]*>\s*]*>\s*]+href=["']([^"']*(?:%s)\?[^"']*(?:cpu|gpu)=[^"']*(?:&|&)id=\d+[^"']*)["'][^>]*>(.*?)\s*\s*]*>(.*?)\s*]*>(.*?)\s*]*>(.*?)\s*]*>(.*?)\s*`, strings.Join(pathAlternates, "|")) re := regexp.MustCompile(pattern) seen := make(map[string]bool) var out []candidate for _, m := range re.FindAllStringSubmatch(body, -1) { href := htmlUnescape(m[1]) name := cleanText(stripTags(m[2])) if name == "" { if u, err := url.Parse(href); err == nil { name = u.Query().Get("cpu") if name == "" { name = u.Query().Get("gpu") } } } if strings.HasPrefix(href, "/") { href = "https://" + host + href } if !strings.HasPrefix(href, "http") { href = "https://" + host + "/" + strings.TrimPrefix(href, "/") } key := strings.ToLower(name + "|" + href) if name != "" && !seen[key] { seen[key] = true stats := []stat{ {Name: "PassMark Score", Value: cleanText(stripTags(m[3]))}, {Name: "Rank", Value: cleanText(stripTags(m[4]))}, {Name: "Value", Value: cleanText(stripTags(m[5]))}, {Name: "Price", Value: cleanText(stripTags(m[6]))}, } out = append(out, candidate{Name: name, URL: href, Stats: compactStats(stats)}) } } return out } func compactStats(stats []stat) []stat { var out []stat for _, s := range stats { s.Name = strings.TrimSpace(s.Name) s.Value = strings.TrimSpace(s.Value) if s.Name == "" || s.Value == "" { continue } out = append(out, s) } return out } func bestCandidate(query string, candidates []candidate) (candidate, bool) { if len(candidates) == 0 { return candidate{}, false } normalizedQuery := normalizeTokens(query) type scored struct { candidate score int } var scoredList []scored for _, c := range candidates { score := matchScoreNormalized(normalizedQuery, c.Name) if score > 0 { scoredList = append(scoredList, scored{candidate: c, score: score}) } } if len(scoredList) == 0 { return candidates[0], true } sort.SliceStable(scoredList, func(i, j int) bool { return scoredList[i].score > scoredList[j].score }) return scoredList[0].candidate, true } func matchScore(query, name string) int { return matchScoreNormalized(normalizeTokens(query), name) } func matchScoreNormalized(q, name string) int { n := normalizeTokens(name) if q == "" || n == "" { return 0 } if q == n { return 1000 } score := 0 if strings.Contains(n, q) { score += 500 } for _, tok := range strings.Fields(q) { if strings.Contains(n, tok) { score += 50 + len(tok) } } return score } func normalizeTokens(s string) string { s = strings.ToLower(s) s = tokenReplacer.Replace(s) s = letterNumberRe.ReplaceAllString(s, "$1 $2") s = numberLetterRe.ReplaceAllString(s, "$1 $2") return strings.Join(strings.Fields(nonWordRe.ReplaceAllString(s, " ")), " ") } func parsePassMarkDetail(body string) (string, []stat) { name := firstMatch(body, `(?is)]*>(.*?)`) if name == "" { name = firstMatch(body, `(?is)]*>(.*?)`) } name = strings.TrimSuffix(cleanText(stripTags(name)), " PassMark CPU Mark") name = strings.TrimSuffix(name, " Videocard Benchmarks") var stats []stat add := func(k, v string) { k, v = cleanText(stripTags(k)), cleanText(stripTags(v)) if k == "" || v == "" || len(k) > 80 || len(v) > 200 { return } if skipPassMarkStat(k, v) { return } stats = appendUnique(stats, stat{Name: k, Value: v}) } for _, s := range passmarkSelectedStats(body) { add(s.Name, s.Value) } directScoreRe := regexp.MustCompile(`(?is)Average\s+(CPU|G3D|G2D)\s+Mark.*?]*>\s*([^<]*Rating)\s*\s*]*>\s*([0-9,]+)\s*`) for _, m := range directScoreRe.FindAllStringSubmatch(body, -1) { add("Average "+strings.ToUpper(m[1])+" Mark", m[3]) } singleRe := regexp.MustCompile(`(?is)Single\s+Thread\s+Rating\s*\s*]*>\s*([0-9,]+)\s*`) if m := singleRe.FindStringSubmatch(body); len(m) == 2 { add("Single Thread Rating", m[1]) } rowRe := regexp.MustCompile(`(?is)]*>\s*]*>(.*?)\s*]*>(.*?)`) for _, m := range rowRe.FindAllStringSubmatch(body, -1) { add(m[1], m[2]) } strongRe := regexp.MustCompile(`(?is)<(?:strong|b)[^>]*>([^:<]{2,80}:?)\s*([^<\n]{1,200})`) for _, m := range strongRe.FindAllStringSubmatch(body, -1) { add(strings.TrimSuffix(m[1], ":"), m[2]) } return name, stats } func skipPassMarkStat(k, v string) bool { lk := strings.ToLower(k) lv := strings.ToLower(v) switch lk { case "merchant", "processor", "baseline": return true } if strings.Contains(k, `"`) || strings.Contains(v, `" +`) { return true } if strings.Contains(lk, "detected and enabled") || strings.Contains(lk, "view current prices") { return true } if strings.Contains(lv, "for our website") || strings.Contains(lv, "selected cpu") { return true } return false } func passmarkSelectedStats(body string) []stat { id := firstMatch(body, `(?is)var\s+(?:cpu|gpu|videocard)DetailsId\s*=\s*["']?(\d+)`) if id == "" { id = firstMatch(body, `(?is)["'](?:cpu|gpu|videocard)DetailsId["']\s*:\s*["']?(\d+)`) } if id == "" { return nil } var stats []stat row := firstMatch(body, `(?is)]+id=["']pk`+regexp.QuoteMeta(id)+`["'][^>]*>(.*?)`) if row != "" { if product := cleanText(stripTags(firstMatch(row, `(?is)]+class=["'][^"']*prdname[^"']*["'][^>]*>(.*?)`))); product != "" { stats = appendUnique(stats, stat{Name: "Matched Product", Value: product}) } if score := cleanText(stripTags(firstMatch(row, `(?is)]+class=["'][^"']*mark-neww[^"']*["'][^>]*>(.*?)`))); score != "" { stats = appendUnique(stats, stat{Name: "PassMark Score", Value: score}) } if value := cleanText(stripTags(firstMatch(row, `(?is)]+class=["'][^"']*count[^"']*["'][^>]*>(.*?)`))); value != "" { stats = appendUnique(stats, stat{Name: "Value", Value: value}) } if price := cleanText(stripTags(firstMatch(row, `(?is)]+class=["'][^"']*price-neww[^"']*["'][^>]*>(.*?)`))); price != "" { stats = appendUnique(stats, stat{Name: "Price", Value: price}) } score := cleanText(stripTags(firstMatch(row, `(?is)]+class=["'][^"']*mark-neww[^"']*["'][^>]*>(.*?)`))) nums := onclickNumbers(row) if len(nums) >= 3 { if sameNumber(nums[0], score) { stats = appendUnique(stats, stat{Name: "Rank", Value: nums[1]}) stats = appendUnique(stats, stat{Name: "Samples", Value: nums[2]}) } else { stats = appendUnique(stats, stat{Name: "Rank", Value: nums[0]}) stats = appendUnique(stats, stat{Name: "Samples", Value: nums[2]}) } } } rankRow := firstMatch(body, `(?is)]+id=["']rk`+regexp.QuoteMeta(id)+`["'][^>]*>(.*?)`) if rankRow != "" { if score := cleanText(stripTags(firstMatch(rankRow, `(?is)]+class=["'][^"']*count[^"']*["'][^>]*>(.*?)`))); score != "" { stats = appendUnique(stats, stat{Name: "PassMark Score", Value: score}) } } return stats } func onclickNumbers(row string) []string { payload := firstMatch(row, `(?is)onclick=["'][^(]*\((.*?)\)`) if payload == "" { return nil } re := regexp.MustCompile(`['"]?(-?(?:\d{1,3},)*\d+(?:\.\d+)?)['"]?`) var nums []string for _, m := range re.FindAllStringSubmatch(payload, -1) { nums = append(nums, m[1]) } return nums } func sameNumber(a, b string) bool { clean := func(s string) string { s = strings.ReplaceAll(s, ",", "") s = strings.TrimSpace(s) return strings.TrimSuffix(s, ".0") } return clean(a) == clean(b) } func firstMatch(s, pattern string) string { re := regexp.MustCompile(pattern) m := re.FindStringSubmatch(s) if len(m) < 2 { return "" } return m[1] } func stripScripts(s string) string { s = scriptRe.ReplaceAllString(s, " ") return styleRe.ReplaceAllString(s, " ") } func stripTags(s string) string { s = stripScripts(s) return tagRe.ReplaceAllString(s, " ") } func cleanText(s string) string { s = htmlUnescape(s) s = strings.ReplaceAll(s, "\u00a0", " ") return strings.Join(strings.Fields(s), " ") } func htmlUnescape(s string) string { repl := strings.NewReplacer( "&", "&", "<", "<", ">", ">", """, `"`, "'", "'", "'", "'", " ", " ", ) return repl.Replace(s) } func appendUnique(stats []stat, next stat) []stat { key := strings.ToLower(next.Name) for _, s := range stats { if strings.ToLower(s.Name) == key { return stats } } return append(stats, next) } func mergeStats(first, second []stat) []stat { out := append([]stat{}, first...) for _, s := range second { out = appendUnique(out, s) } return out }