// Port of ~/code/experiments/hpe/extractors/xe_page.py -- see that file's
// docstring for the two table shapes this parses.
//
// Note on traversal: the switch firmware's HTML puts
/ | as direct
// children of with no wrapper. A strict browser-grade HTML5
// parser (which golang.org/x/net/html, underlying goquery, implements) would
// normally insert a synthetic around those rows, which would break a
// "direct children only" traversal the way the Python version does (Python's
// html.parser is lenient and doesn't insert one). We sidestep this by using
// Find() (any descendant) instead of direct-children selectors -- safe here
// because tables in this firmware are never nested inside one another.
package client
import (
"strings"
"github.com/PuerkitoBio/goquery"
)
type Table struct {
Caption string
Kind string // "scalar" | "tabular"
Record map[string]string
FieldNames map[string]string
Rows []map[string]string
ID string
}
func extractCellValue(td *goquery.Selection) string {
if inp := td.Find("input").First(); inp.Length() > 0 {
if v, exists := inp.Attr("value"); exists {
return v
}
return ""
}
return strings.TrimSpace(td.Text())
}
func parseTable(table *goquery.Selection) Table {
id, _ := table.Attr("id")
caption := strings.TrimSpace(table.Find("caption").First().Text())
var headers []string
table.Find("th").Each(func(_ int, th *goquery.Selection) {
headers = append(headers, strings.TrimSpace(th.Text()))
})
if len(headers) > 0 {
var rows []map[string]string
table.Find("tr").Each(func(_ int, tr *goquery.Selection) {
tds := tr.Find("td")
if tds.Length() != len(headers) {
return
}
row := make(map[string]string, len(headers))
tds.Each(func(i int, td *goquery.Selection) {
// Later (visible) column wins on a duplicate header label --
// see the same caveat noted in extractors/xe_page.py.
row[headers[i]] = extractCellValue(td)
})
rows = append(rows, row)
})
return Table{Caption: caption, Kind: "tabular", Rows: rows, ID: id}
}
record := make(map[string]string)
fieldNames := make(map[string]string)
table.Find("tr").Each(func(_ int, tr *goquery.Selection) {
defleft := tr.Find("td.defleft").First()
defright := tr.Find("td.defright").First()
if defleft.Length() == 0 || defright.Length() == 0 {
return
}
label := strings.TrimSpace(defleft.Text())
if label == "" {
return
}
record[label] = extractCellValue(defright)
if inp := defright.Find("input").First(); inp.Length() > 0 {
if name, exists := inp.Attr("name"); exists && name != "" {
fieldNames[label] = name
}
}
})
return Table{
Caption: caption,
Kind: "scalar",
Record: record,
FieldNames: fieldNames,
ID: id,
}
}
// ParseXETables parses every in html, in document order.
func ParseXETables(html string) ([]Table, error) {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
return nil, err
}
var tables []Table
doc.Find("table").Each(func(_ int, table *goquery.Selection) {
tables = append(tables, parseTable(table))
})
return tables, nil
}
// FetchTables fetches path and returns {caption: table} for every captioned
// table. Tables with no/blank caption are dropped -- use FetchAllTables +
// Table.ID for those (e.g. the MAC Table entries table on FDBSearch.html).
func (c *Client) FetchTables(path string) (map[string]Table, error) {
html, err := c.Fetch(path)
if err != nil {
return nil, err
}
tables, err := ParseXETables(html)
if err != nil {
return nil, err
}
result := make(map[string]Table, len(tables))
for _, t := range tables {
if t.Caption != "" {
result[t.Caption] = t
}
}
return result, nil
}
// FetchAllTables fetches path and returns every table, including
// uncaptioned ones, in document order.
func (c *Client) FetchAllTables(path string) ([]Table, error) {
html, err := c.Fetch(path)
if err != nil {
return nil, err
}
return ParseXETables(html)
}