mirror of
https://github.com/tiennm99/thptqg.git
synced 2026-08-14 01:24:20 +00:00
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.
106 lines
2.9 KiB
Go
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() }
|