Files
tiennm99 c359a0b444 refactor: one directory per pipeline stage, and an assembler to drive them
The repository now reads as the pipeline it is: crawler fetches, parser
converts, assembler verifies and publishes, with data/ and web/ as the stores
they hand work through. go-parser is renamed parser now that there is no other.

The assembler replaces build-db.js and assemble-site.js. It compiles the
parser, builds and verifies each database, compresses it, runs the Vite build
and assembles _site — one command, and the only place that knows the order.

It also closes a real hole: nothing previously asserted that a database reached
the site. An empty staging directory assembled happily, so every page rendered,
every query 404d and CI stayed green. The row-count and size guards could not
catch that, since they only run when a database was built at all.

Removing Node from the root forced the dataset list out of web/src/datasets.js,
which the assembler cannot import. datasets.json is now the registry both sides
read — JSON because Go and the browser both parse it without a dependency —
while presentation stays in the web app, keyed by id and cross-checked against
the registry so a half-added dataset fails instead of half-working.

Guards verified by making each one fail: a missing database, and an expected
row count one higher than the truth.
2026-08-13 22:50:05 +07:00

106 lines
2.9 KiB
Go

package reader
import (
"fmt"
"github.com/xuri/excelize/v2"
)
// xlsxWorkbook reads OOXML through excelize.
//
// Two excelize behaviours must be corrected to match calamine:
//
// 1. GetRows applies the cell number format by default, while calamine renders
// the underlying value. RawCellValue: true disables that.
// 2. GetRows trims trailing blank cells, so rows are ragged; calamine returns a
// rectangular used range. Rows are padded back out to the sheet width.
type xlsxWorkbook struct {
f *excelize.File
sheets []Sheet
rows [][][]Cell // [sheetIdx][rowIdx][colIdx]
}
func openXLSX(path string) (Workbook, error) {
f, err := excelize.OpenFile(path)
if err != nil {
return nil, fmt.Errorf("excelize open %s: %w", path, err)
}
wb := &xlsxWorkbook{f: f}
crFixups := buildCRFixups(path)
for idx, name := range f.GetSheetList() {
raw, err := f.GetRows(name, excelize.Options{RawCellValue: true})
if err != nil {
f.Close()
return nil, fmt.Errorf("excelize GetRows %s/%s: %w", path, name, err)
}
// Do NOT trim trailing blank rows: calamine's used range keeps them, and
// excelize's GetRows already drops trailing fully-empty rows itself.
//
// One correction is needed. 63 sheets in 2017-old and 53 in 2017-old2
// hold a single empty shared-string cell at A1; calamine reports those
// as a 1x1 range, while GetRows returns nothing. A genuinely empty sheet
// (230 of them in 2016) is height 0 on both sides. GetCellType tells the
// two apart: the empty-shared-string cell exists in the XML and types as
// CellTypeSharedString, an absent cell types as CellTypeUnset.
if len(raw) == 0 {
if t, terr := f.GetCellType(name, "A1"); terr == nil && t != excelize.CellTypeUnset {
raw = [][]string{{""}}
}
}
height := len(raw)
width := 0
for _, r := range raw {
if len(r) > width {
width = len(r)
}
}
cells := make([][]Cell, len(raw))
for i, r := range raw {
row := make([]Cell, len(r))
for j, v := range r {
if fixed, ok := crFixups[v]; ok {
v = fixed
} else {
v = normalizeNumeric(f, name, j, i, v)
}
row[j] = Cell{Str: v, IsEmpty: v == ""}
}
cells[i] = padRow(row, width)
}
wb.sheets = append(wb.sheets, Sheet{Index: idx, Name: name, Height: height, Width: width})
wb.rows = append(wb.rows, cells)
}
return wb, nil
}
func rowAllBlank(r []string) bool {
for _, v := range r {
if v != "" {
return false
}
}
return true
}
func (w *xlsxWorkbook) Sheets() []Sheet { return w.sheets }
func (w *xlsxWorkbook) EachRow(sheetIdx int, fn RowFunc) error {
if sheetIdx < 0 || sheetIdx >= len(w.sheets) {
return fmt.Errorf("sheet index %d out of range", sheetIdx)
}
sh := w.sheets[sheetIdx]
for i, row := range w.rows[sheetIdx] {
if err := fn(sh, i, row); err != nil {
return err
}
}
return nil
}
func (w *xlsxWorkbook) Close() error { return w.f.Close() }