1package fileio23import (4 "fmt"5 "html"6 "sort"7 "strconv"8 "strings"910 "src.rest/cat/012/internal/chart"11 "src.rest/cat/012/internal/sheet"12)1314// maxHTMLCells bounds the cells a page draws, blank ones included: a15// browser shows a table of about this many at once without trouble.16// Past it the page draws the first rows and says so.17const maxHTMLCells = 200_0001819// htmlLayout is where the grid's rows and columns go: the columns and20// rows drawn, in order, and the pixel offset of each from the grid's21// top-left corner, headers included.22type htmlLayout struct {23 snap *Snapshot24 cols, rows []int25 x, y map[int]int // left edge of a column, top edge of a row26 headW int // the row numbers' width27 cut int // rows left out past maxHTMLCells28 merged map[sheet.Addr]sheet.Rect29 covered map[sheet.Addr]bool30 tableRole map[sheet.Addr]string31}3233// layout places the snapshot's range, grown to hold its charts.34func layout(snap *Snapshot) *htmlLayout {35 r := snap.Range36 colsTo, rowsTo := r.To.Col, r.To.Row37 for _, c := range snap.Charts {38 colsTo = max(colsTo, chartEndCol(snap, c))39 rowsTo = max(rowsTo, chartEndRow(snap, c))40 }41 l := &htmlLayout{snap: snap, x: map[int]int{}, y: map[int]int{}}42 for col := r.From.Col; col <= min(colsTo, sheet.MaxCols-1); col++ {43 l.cols = append(l.cols, col)44 }45 maxRows := max(maxHTMLCells/max(len(l.cols), 1), 100)46 for row := r.From.Row; row <= min(rowsTo, sheet.MaxRows-1); row++ {47 if snap.HiddenRows[row] {48 continue49 }50 if len(l.rows) == maxRows {51 l.cut = rowsTo - row + 152 break53 }54 l.rows = append(l.rows, row)55 }56 last := r.From.Row + 157 if len(l.rows) > 0 {58 last = l.rows[len(l.rows)-1] + 159 }60 l.headW = (max(len(strconv.Itoa(last)), 3) + 2) * htmlCellW61 x := l.headW62 for _, col := range l.cols {63 l.x[col] = x64 x += htmlColWidth(snap, col) * htmlCellW65 }66 y := htmlCellH67 for _, row := range l.rows {68 l.y[row] = y69 y += htmlRowHeight(snap, row) * htmlCellH70 }71 l.mergesAndTables()72 return l73}7475func htmlColWidth(snap *Snapshot, col int) int {76 if w, ok := snap.Widths[col]; ok {77 return w78 }79 return sheet.DefaultWidth80}8182func htmlRowHeight(snap *Snapshot, row int) int {83 if h, ok := snap.Heights[row]; ok && h > 0 {84 return h85 }86 return 187}8889// chartEndCol is the last column a chart covers.90func chartEndCol(snap *Snapshot, c SnapChart) int {91 col, w := c.At.Col, c.W92 for w > htmlColWidth(snap, col) && col < sheet.MaxCols-1 {93 w -= htmlColWidth(snap, col)94 col++95 }96 return col97}9899// chartEndRow is the last row a chart covers.100func chartEndRow(snap *Snapshot, c SnapChart) int {101 row, h := c.At.Row, c.H102 for h > htmlRowHeight(snap, row) && row < sheet.MaxRows-1 {103 h -= htmlRowHeight(snap, row)104 row++105 }106 return row107}108109// mergesAndTables notes the merged cells and the cells tables style.110func (l *htmlLayout) mergesAndTables() {111 l.merged, l.covered, l.tableRole = map[sheet.Addr]sheet.Rect{}, map[sheet.Addr]bool{}, map[sheet.Addr]string{}112 for _, m := range l.snap.Merges {113 l.merged[m.From] = m114 for row := m.From.Row; row <= m.To.Row; row++ {115 for col := m.From.Col; col <= m.To.Col; col++ {116 if a := (sheet.Addr{Col: col, Row: row}); a != m.From {117 l.covered[a] = true118 }119 }120 }121 }122 for _, t := range l.snap.Tables {123 for col := t.Range.From.Col; col <= t.Range.To.Col; col++ {124 if t.Header {125 l.tableRole[sheet.Addr{Col: col, Row: t.Range.From.Row}] = "th"126 }127 for row := t.Range.From.Row + 2; t.Banded && row <= t.Range.To.Row; row += 2 {128 l.tableRole[sheet.Addr{Col: col, Row: row}] = "band"129 }130 }131 }132}133134// HTMLGrid draws the snapshot as 012's grid: column letters and row135// numbers, the cells as they show with their styles, borders and the136// looks of rules, and the charts over them. It returns the fragment and137// a note when rows were left out.138func HTMLGrid(snap *Snapshot) (string, string) {139 l := layout(snap)140 var b strings.Builder141 b.WriteString(`<div class="sheet"><table class="grid"><colgroup>`)142 fmt.Fprintf(&b, `<col style="width:%dpx">`, l.headW)143 for _, col := range l.cols {144 fmt.Fprintf(&b, `<col style="width:%dpx">`, htmlColWidth(snap, col)*htmlCellW)145 }146 b.WriteString(`</colgroup><thead><tr><th class="corner"></th>`)147 for _, col := range l.cols {148 fmt.Fprintf(&b, `<th data-c="%s">%s</th>`, sheet.ColName(col), sheet.ColName(col))149 }150 b.WriteString("</tr></thead><tbody>")151 for _, row := range l.rows {152 l.writeRow(&b, row)153 }154 b.WriteString("</tbody></table>")155 for _, c := range snap.Charts {156 l.writeChart(&b, c)157 }158 b.WriteString("</div>")159 note := ""160 if l.cut > 0 {161 note = fmt.Sprintf("HTML pages show the first %d rows: %d more are left out", len(l.rows), l.cut)162 }163 return b.String(), note164}165166func (l *htmlLayout) writeRow(b *strings.Builder, row int) {167 style := ""168 if h := htmlRowHeight(l.snap, row); h > 1 {169 style = fmt.Sprintf(` style="height:%dpx"`, h*htmlCellH)170 }171 fmt.Fprintf(b, `<tr%s><th data-r="%d">%d</th>`, style, row+1, row+1)172 for _, col := range l.cols {173 a := sheet.Addr{Col: col, Row: row}174 if l.covered[a] {175 continue176 }177 l.writeCell(b, a)178 }179 b.WriteString("</tr>")180}181182// writeChart draws a chart in its frame over the cells under it, as183// the screen does: the title in the top border, the range in the184// bottom one.185func (l *htmlLayout) writeChart(b *strings.Builder, c SnapChart) {186 x, y := l.at(c.At)187 fmt.Fprintf(b, `<figure class="chart" style="left:%dpx;top:%dpx;width:%dpx;height:%dpx">`,188 x, y, c.W*htmlCellW, c.H*htmlCellH)189 writeChartBody(b, c)190 b.WriteString("</figure>")191}192193// writeChartBody is a chart's frame labels and drawing, inside its194// figure.195func writeChartBody(b *strings.Builder, c SnapChart) {196 fmt.Fprintf(b, `<figcaption>%s</figcaption>`, html.EscapeString(chartTitle(c.Chart)))197 b.WriteString(chart.SVG(c.Type, c.Values, c.W-4, c.H-2, chart.Options{Chart: c.ChartOptions}, chartTitle(c.Chart)))198 fmt.Fprintf(b, `<span class="range">%s</span>`, c.Data.String())199}200201// chartTitle is a chart's title, or its type's name when it has none.202func chartTitle(c sheet.Chart) string {203 if c.Title != "" {204 return c.Title205 }206 return c.Type.Title() + " chart"207}208209// at is the pixel offset of a cell's top-left corner; a cell in a210// hidden row or past the drawn ones sits where the next drawn one does.211func (l *htmlLayout) at(a sheet.Addr) (int, int) {212 x, ok := l.x[a.Col]213 if !ok {214 x = l.headW215 }216 y, ok := l.y[a.Row]217 if !ok {218 i := sort.SearchInts(l.rows, a.Row)219 switch {220 case i < len(l.rows):221 y = l.y[l.rows[i]]222 case len(l.rows) > 0:223 last := l.rows[len(l.rows)-1]224 y = l.y[last] + htmlRowHeight(l.snap, last)*htmlCellH225 default:226 y = htmlCellH227 }228 }229 return x, y230}231232// HTMLChart draws one chart alone, in its frame.233func HTMLChart(c SnapChart) string {234 var b strings.Builder235 fmt.Fprintf(&b, `<figure class="chart solo" style="width:%dpx;height:%dpx">`, c.W*htmlCellW, c.H*htmlCellH)236 writeChartBody(&b, c)237 b.WriteString("</figure>")238 return b.String()239}