diff --git a/markup/lexer.go b/markup/lexer.go index b1c3abc..b69a3cf 100644 --- a/markup/lexer.go +++ b/markup/lexer.go @@ -27,7 +27,8 @@ type GemLexerState struct { id int buf string // Temporaries - img *Img + img *Img + table *Table } type Line struct { @@ -80,6 +81,8 @@ func lineToAST(line string, state *GemLexerState, ast *[]Line) { switch state.where { case "img": goto imgState + case "table": + goto tableState case "pre": goto preformattedState case "list": @@ -99,6 +102,13 @@ imgState: } return +tableState: + if shouldGoBackToNormal := state.table.Process(line); shouldGoBackToNormal { + state.where = "" + addLine(*state.table) + } + return + preformattedState: switch { case startsWith("```"): @@ -209,6 +219,9 @@ normalState: state.where = "img" state.img = img } + case MatchesTable(line): + state.where = "table" + state.table = TableFromFirstLine(line, state.name) default: addLine(fmt.Sprintf("

%s

", state.id, ParagraphToHtml(state.name, line))) } diff --git a/markup/parser.go b/markup/parser.go index 50ff158..665f8ba 100644 --- a/markup/parser.go +++ b/markup/parser.go @@ -13,6 +13,8 @@ func Parse(ast []Line, from, to int, recursionLevel int) (html string) { html += Transclude(v, recursionLevel) case Img: html += v.ToHtml() + case Table: + html += v.asHtml() case string: html += v default: diff --git a/markup/table.go b/markup/table.go new file mode 100644 index 0000000..f6c6886 --- /dev/null +++ b/markup/table.go @@ -0,0 +1,215 @@ +package markup + +import ( + "fmt" + "regexp" + "strings" + "unicode" + // "github.com/bouncepaw/mycorrhiza/util" +) + +var tableRe = regexp.MustCompile(`^table\s+{`) + +func MatchesTable(line string) bool { + return tableRe.MatchString(line) +} + +func TableFromFirstLine(line, hyphaName string) *Table { + return &Table{ + hyphaName: hyphaName, + caption: line[strings.IndexRune(line, '{')+1:], + rows: make([]*tableRow, 0), + } +} + +func (t *Table) Process(line string) (shouldGoBackToNormal bool) { + if strings.TrimSpace(line) == "}" && !t.inMultiline { + return true + } + if !t.inMultiline { + t.pushRow() + } + var ( + escaping bool + lookingForNonSpace = !t.inMultiline + countingColspan bool + ) + for i, r := range line { + switch { + case lookingForNonSpace && unicode.IsSpace(r): + case lookingForNonSpace && (r == '!' || r == '|'): + t.currCellMarker = r + t.currColspan = 1 + lookingForNonSpace = false + countingColspan = true + case lookingForNonSpace: + t.currCellMarker = '^' // ^ represents implicit |, not part of syntax + t.currColspan = 1 + lookingForNonSpace = false + t.currCellBuilder.WriteRune(r) + + case escaping: + t.currCellBuilder.WriteRune(r) + + case t.inMultiline && r == '}': + t.inMultiline = false + case t.inMultiline && i == len(line)-1: + t.currCellBuilder.WriteRune('\n') + case t.inMultiline: + t.currCellBuilder.WriteRune(r) + + // Not in multiline: + case (r == '|' || r == '!') && !countingColspan: + t.pushCell() + t.currCellMarker = r + t.currColspan = 1 + countingColspan = true + case r == t.currCellMarker && (r == '|' || r == '!') && countingColspan: + t.currColspan++ + case r == '{': + t.inMultiline = true + countingColspan = false + case i == len(line)-1: + t.pushCell() + default: + t.currCellBuilder.WriteRune(r) + countingColspan = false + } + } + return false +} + +type Table struct { + // data + hyphaName string + caption string + rows []*tableRow + // state + inMultiline bool + // tmp + currCellMarker rune + currColspan uint + currCellBuilder strings.Builder +} + +func (t *Table) pushRow() { + t.rows = append(t.rows, &tableRow{ + cells: make([]*tableCell, 0), + }) +} + +func (t *Table) pushCell() { + tc := &tableCell{ + content: t.currCellBuilder.String(), + colspan: t.currColspan, + } + switch t.currCellMarker { + case '|', '^': + tc.kind = tableCellDatum + case '!': + tc.kind = tableCellHeader + } + // We expect the table to have at least one row ready, so no nil-checking + tr := t.rows[len(t.rows)-1] + tr.cells = append(tr.cells, tc) + t.currCellBuilder = strings.Builder{} +} + +func (t *Table) asHtml() (html string) { + if t.caption != "" { + html += fmt.Sprintf("%s", t.caption) + } + if len(t.rows) > 0 && t.rows[0].looksLikeThead() { + html += fmt.Sprintf("%s", t.rows[0].asHtml(t.hyphaName)) + t.rows = t.rows[1:] + } + html += "\n\n" + for _, tr := range t.rows { + html += tr.asHtml(t.hyphaName) + } + return fmt.Sprintf(`%s
`, html) +} + +type tableRow struct { + cells []*tableCell +} + +func (tr *tableRow) asHtml(hyphaName string) (html string) { + for _, tc := range tr.cells { + html += tc.asHtml(hyphaName) + } + return fmt.Sprintf("%s\n", html) +} + +// Most likely, rows with more than two header cells are theads. I allow one extra datum cell for tables like this: +// | ! a ! b +// ! c | d | e +// ! f | g | h +func (tr *tableRow) looksLikeThead() bool { + var ( + headerAmount = 0 + datumAmount = 0 + ) + for _, tc := range tr.cells { + switch tc.kind { + case tableCellHeader: + headerAmount++ + case tableCellDatum: + datumAmount++ + } + } + return headerAmount >= 2 && datumAmount <= 1 +} + +type tableCell struct { + kind tableCellKind + colspan uint + content string +} + +func (tc *tableCell) asHtml(hyphaName string) string { + return fmt.Sprintf( + "<%[1]s %[2]s>%[3]s\n", + tc.kind.tagName(), + tc.colspanAttribute(), + tc.contentAsHtml(hyphaName), + ) +} + +func (tc *tableCell) colspanAttribute() string { + if tc.colspan <= 1 { + return "" + } + return fmt.Sprintf(`colspan="%d"`, tc.colspan) +} + +func (tc *tableCell) contentAsHtml(hyphaName string) (html string) { + for _, line := range strings.Split(tc.content, "\n") { + if line = strings.TrimSpace(line); line != "" { + if html != "" { + html += `
` + } + html += ParagraphToHtml(hyphaName, line) + } + } + return html +} + +type tableCellKind int + +const ( + tableCellUnknown tableCellKind = iota + tableCellHeader + tableCellDatum +) + +func (tck tableCellKind) tagName() string { + switch tck { + case tableCellHeader: + return "th" + case tableCellDatum: + return "td" + default: + return "p" + } +} diff --git a/metarrhiza b/metarrhiza index 7828352..95f48bf 160000 --- a/metarrhiza +++ b/metarrhiza @@ -1 +1 @@ -Subproject commit 7828352598c19afe5f2e13df0219656ac7b44c9c +Subproject commit 95f48bfd7a7cfef17d56cef207a770767d727950 diff --git a/templates/asset.qtpl.go b/templates/asset.qtpl.go index 0be6bb6..b0416a1 100644 --- a/templates/asset.qtpl.go +++ b/templates/asset.qtpl.go @@ -93,6 +93,10 @@ nav ul li {list-style-type:none;margin-right:1rem;} .history-entry__time { font-weight: bold; } .history-entry__author { font-style: italic; } +table { background-color: #eee; border: #ddd 1px solid; border-radius: .25rem; +min-width: 4rem; } +td { padding: .25rem; border: #ddd 1px solid; } +caption { caption-side: top; font-size: small; } `) //line templates/asset.qtpl:2 qw422016.N().S(` diff --git a/templates/default.css b/templates/default.css index edddd1a..1c041f9 100644 --- a/templates/default.css +++ b/templates/default.css @@ -68,3 +68,7 @@ nav ul li {list-style-type:none;margin-right:1rem;} .history-entry__time { font-weight: bold; } .history-entry__author { font-style: italic; } +table { background-color: #eee; border: #ddd 1px solid; border-radius: .25rem; +min-width: 4rem; } +td { padding: .25rem; border: #ddd 1px solid; } +caption { caption-side: top; font-size: small; }