Przeglądaj źródła

修改渲染方式

fancl 1 rok temu
rodzic
commit
ab5d5679b8
12 zmienionych plików z 531 dodań i 335 usunięć
  1. 40 0
      element/action.go
  2. 43 0
      element/button.go
  3. 48 0
      element/element.go
  4. 53 0
      element/entry.go
  5. 21 0
      element/style.go
  6. 67 0
      element/table.go
  7. 92 0
      element/text.go
  8. 56 0
      element/timeline.go
  9. 23 0
      internal/pool/buffer.go
  10. 30 331
      pgenr.go
  11. 10 4
      render_test.go
  12. 48 0
      theme/default.go

+ 40 - 0
element/action.go

@@ -0,0 +1,40 @@
+package element
+
+import (
+	"git.nspix.com/golang/pgenr/internal/pool"
+	"golang.org/x/net/html/atom"
+	"html"
+)
+
+type Action struct {
+	Instructions Element
+	Button       *Button
+	InviteCode   string
+}
+
+func (element *Action) Html() string {
+	return element.String()
+}
+
+func (element *Action) String() string {
+	br := pool.Get()
+	defer pool.Put(br)
+	if element.Instructions != nil {
+		br.WriteString(element.Instructions.Html())
+	}
+	br.WriteString(beginTag(atom.Div.String(), Attrs{"class": "action-block"}))
+	if element.InviteCode != "" {
+		br.WriteString(beginTag(atom.Span.String(), Attrs{"class": "invite-code"}))
+		br.WriteString(html.EscapeString(element.InviteCode))
+		br.WriteString(endTag(atom.Span.String()))
+	}
+	if element.Button != nil {
+		br.WriteString(element.Button.String())
+	}
+	br.WriteString(endTag(atom.Div.String()))
+	return br.String()
+}
+
+func NewAction() *Action {
+	return &Action{}
+}

+ 43 - 0
element/button.go

@@ -0,0 +1,43 @@
+package element
+
+import (
+	"git.nspix.com/golang/pgenr/internal/pool"
+	"golang.org/x/net/html/atom"
+	"html"
+)
+
+type (
+	ButtonOption func(btn *Button)
+
+	Button struct {
+		Url   string
+		Text  string
+		Color string
+		Style Style
+	}
+)
+
+func (element *Button) Html() string {
+	return element.String()
+}
+
+func (element *Button) String() string {
+	br := pool.Get()
+	defer pool.Put(br)
+	if element.Style == nil {
+		element.Style = make(map[string]string)
+	}
+	if element.Color != "" {
+		element.Style["color"] = element.Color
+	}
+	br.WriteString(renderTag(atom.A.String(), Attrs{"class": "button", "href": element.Url, "style": element.Style.String()}, html.EscapeString(element.Text)))
+	return br.String()
+}
+
+func NewButton(label, link string, opts ...ButtonOption) *Button {
+	btn := &Button{Text: label, Url: link}
+	for _, cb := range opts {
+		cb(btn)
+	}
+	return btn
+}

+ 48 - 0
element/element.go

@@ -0,0 +1,48 @@
+package element
+
+import (
+	"git.nspix.com/golang/pgenr/internal/pool"
+)
+
+type (
+	Element interface {
+		Html() string
+	}
+
+	Attrs map[string]string
+)
+
+func renderTag(tag string, attrs Attrs, content string) string {
+	br := pool.Get()
+	defer pool.Put(br)
+	br.WriteString("<" + tag)
+	if len(attrs) > 0 {
+		for k, v := range attrs {
+			br.WriteString(" " + k + "=\"" + v + "\" ")
+		}
+	}
+	br.WriteString(">")
+	br.WriteString(content)
+	br.WriteString("</" + tag + ">")
+	return br.String()
+}
+
+func beginTag(tag string, attrs Attrs) string {
+	br := pool.Get()
+	defer pool.Put(br)
+	br.WriteString("<" + tag)
+	if len(attrs) > 0 {
+		for k, v := range attrs {
+			br.WriteString(" " + k + "=\"" + v + "\" ")
+		}
+	}
+	br.WriteString(">")
+	return br.String()
+}
+
+func endTag(tag string) string {
+	br := pool.Get()
+	defer pool.Put(br)
+	br.WriteString("</" + tag + ">")
+	return br.String()
+}

+ 53 - 0
element/entry.go

@@ -0,0 +1,53 @@
+package element
+
+import (
+	"git.nspix.com/golang/pgenr/internal/pool"
+	"golang.org/x/net/html/atom"
+)
+
+type (
+	Entry struct {
+		Title Element
+		Items map[string]Element
+	}
+)
+
+func (element *Entry) Html() string {
+	return element.String()
+}
+
+func (element *Entry) String() string {
+	br := pool.Get()
+	defer pool.Put(br)
+	br.WriteString(beginTag(atom.Div.String(), Attrs{"class": "grid"}))
+	if element.Title != nil {
+		br.WriteString(element.Title.Html())
+	}
+	for k, v := range element.Items {
+		br.WriteString(beginTag(atom.Div.String(), Attrs{"class": "row"}))
+		br.WriteString(beginTag(atom.Div.String(), Attrs{"class": "preview-label"}))
+		br.WriteString(k)
+		br.WriteString(endTag(atom.Div.String()))
+		br.WriteString(beginTag(atom.Div.String(), Attrs{"class": "preview-value"}))
+		br.WriteString(v.Html())
+		br.WriteString(endTag(atom.Div.String()))
+		br.WriteString(endTag(atom.Div.String()))
+	}
+	br.WriteString(endTag(atom.Div.String()))
+	return br.String()
+}
+
+func (element *Entry) AddItem(label string, txt *Text) *Entry {
+	if element.Items == nil {
+		element.Items = make(map[string]Element)
+	}
+	element.Items[label] = txt
+	return element
+}
+
+func NewEntry(title string) *Entry {
+	return &Entry{
+		Title: NewText(title, WithTextTag(atom.P)),
+		Items: make(map[string]Element),
+	}
+}

+ 21 - 0
element/style.go

@@ -0,0 +1,21 @@
+package element
+
+import "git.nspix.com/golang/pgenr/internal/pool"
+
+type Style map[string]string
+
+func (s Style) Css(name string, value string) {
+	s[name] = value
+}
+
+func (s Style) String() string {
+	br := pool.Get()
+	defer pool.Put(br)
+	for k, v := range s {
+		br.WriteString(k)
+		br.WriteString(":")
+		br.WriteString(v)
+		br.WriteString(";")
+	}
+	return br.String()
+}

+ 67 - 0
element/table.go

@@ -0,0 +1,67 @@
+package element
+
+import (
+	"git.nspix.com/golang/pgenr/internal/pool"
+	"golang.org/x/net/html/atom"
+)
+
+type (
+	Table struct {
+		Title  Element
+		Header []Element
+		Body   [][]Element
+	}
+)
+
+func (table *Table) SetHead(elements ...Element) {
+	table.Header = elements
+}
+
+func (table *Table) AddCell(elements ...Element) {
+	if table.Body == nil {
+		table.Body = make([][]Element, 0)
+	}
+	table.Body = append(table.Body, elements)
+}
+
+func (table *Table) String() string {
+	br := pool.Get()
+	defer pool.Put(br)
+	br.WriteString(beginTag(atom.Div.String(), Attrs{"class": "table-wrapper"}))
+	if table.Title != nil {
+		br.WriteString(renderTag(atom.Div.String(), Attrs{"class": "table-title"}, table.Title.Html()))
+	}
+	br.WriteString(beginTag(atom.Table.String(), Attrs{"class": "table"}))
+	if len(table.Header) > 0 {
+		br.WriteString(beginTag(atom.Thead.String(), nil))
+		br.WriteString(beginTag(atom.Tr.String(), nil))
+		for _, text := range table.Header {
+			br.WriteString(renderTag(atom.Th.String(), nil, text.Html()))
+		}
+		br.WriteString(endTag(atom.Tr.String()))
+		br.WriteString(endTag(atom.Thead.String()))
+	}
+	if len(table.Body) > 0 {
+		br.WriteString(beginTag(atom.Tbody.String(), nil))
+		for _, cell := range table.Body {
+			br.WriteString(beginTag(atom.Tr.String(), nil))
+			for _, text := range cell {
+				br.WriteString(renderTag(atom.Td.String(), nil, text.Html()))
+			}
+			br.WriteString(endTag(atom.Tr.String()))
+		}
+		br.WriteString(endTag(atom.Tbody.String()))
+	}
+	br.WriteString(endTag(atom.Table.String()))
+	br.WriteString(endTag(atom.Div.String()))
+	return br.String()
+}
+
+func NewTable(title *Text) *Table {
+	table := &Table{
+		Title:  title,
+		Header: make([]Element, 0),
+		Body:   make([][]Element, 0),
+	}
+	return table
+}

+ 92 - 0
element/text.go

@@ -0,0 +1,92 @@
+package element
+
+import (
+	"git.nspix.com/golang/pgenr/internal/pool"
+	"golang.org/x/net/html/atom"
+	"html"
+)
+
+const (
+	TextThemeSuccess = "success"
+	TextThemeDanger  = "danger"
+)
+
+type (
+	TextOption func(t *Text)
+
+	Text struct {
+		Tag     atom.Atom
+		Content string
+		Theme   string
+		Color   string
+		Style   Style
+	}
+)
+
+func WithTextTheme(s string) TextOption {
+	return func(t *Text) {
+		t.Theme = s
+	}
+}
+
+func WithTextStyle(ms map[string]string) TextOption {
+	return func(t *Text) {
+		t.Style = ms
+	}
+}
+
+func WithTextTag(tag atom.Atom) TextOption {
+	return func(t *Text) {
+		t.Tag = tag
+	}
+}
+
+func WithTextColor(color string) TextOption {
+	return func(t *Text) {
+		t.Color = color
+	}
+}
+
+func (text *Text) Html() string {
+	return text.String()
+}
+
+func (text *Text) String() string {
+	br := pool.Get()
+	defer pool.Put(br)
+	if text.Style == nil {
+		text.Style = make(map[string]string)
+	}
+	if text.Color != "" {
+		text.Style["color"] = text.Color
+	}
+	if text.Tag == 0 {
+		text.Tag = atom.Span
+	}
+	br.WriteString("<")
+	br.WriteString(text.Tag.String())
+	if text.Theme != "" {
+		br.WriteString(" class='text-")
+		br.WriteString(text.Theme)
+		br.WriteString("'")
+	}
+	if len(text.Style) > 0 {
+		br.WriteString(" style='")
+		br.WriteString(text.Style.String())
+		br.WriteString("'")
+	}
+	br.WriteString(">")
+	br.WriteString(html.EscapeString(text.Content))
+	br.WriteString("</")
+	br.WriteString(text.Tag.String())
+	br.WriteString(">")
+	return br.String()
+}
+
+func NewText(s string, opts ...TextOption) *Text {
+	txt := &Text{Content: s, Tag: atom.Span}
+	for _, cb := range opts {
+		cb(txt)
+	}
+	return txt
+}

+ 56 - 0
element/timeline.go

@@ -0,0 +1,56 @@
+package element
+
+import (
+	"git.nspix.com/golang/pgenr/internal/pool"
+	"golang.org/x/net/html/atom"
+	"time"
+)
+
+type (
+	TimelineItem struct {
+		Datetime time.Time
+		Content  Element
+	}
+
+	Timeline struct {
+		Layout string
+		Items  []TimelineItem
+	}
+)
+
+func (element *Timeline) AddItem(tm time.Time, content Element) *Timeline {
+	if element.Items == nil {
+		element.Items = make([]TimelineItem, 0)
+	}
+	element.Items = append(element.Items, TimelineItem{
+		Datetime: tm,
+		Content:  content,
+	})
+	return element
+}
+
+func (element *Timeline) Html() string {
+	return element.String()
+}
+
+func (element *Timeline) String() string {
+	br := pool.Get()
+	defer pool.Put(br)
+	br.WriteString(beginTag(atom.Ul.String(), Attrs{"class": "timeline"}))
+	for _, item := range element.Items {
+		br.WriteString(beginTag(atom.Li.String(), Attrs{"class": "timeline-item"}))
+		br.WriteString(renderTag(atom.Div.String(), Attrs{"class": "timeline-item-tail"}, ""))
+		br.WriteString(renderTag(atom.Div.String(), Attrs{"class": "timeline-item-node"}, ""))
+		br.WriteString(beginTag(atom.Div.String(), Attrs{"class": "timeline-item-wrapper"}))
+		br.WriteString(renderTag(atom.Div.String(), Attrs{"class": "timeline-item-timestamp"}, item.Datetime.Format("2006-01-02")))
+		br.WriteString(renderTag(atom.Div.String(), Attrs{"class": "timeline-item-content"}, item.Content.Html()))
+		br.WriteString(endTag(atom.Div.String()))
+		br.WriteString(endTag(atom.Li.String()))
+	}
+	br.WriteString(endTag(atom.Ul.String()))
+	return br.String()
+}
+
+func NewTimeline() *Timeline {
+	return &Timeline{Layout: "2006-01-02", Items: make([]TimelineItem, 0)}
+}

+ 23 - 0
internal/pool/buffer.go

@@ -0,0 +1,23 @@
+package pool
+
+import (
+	"bytes"
+	"sync"
+)
+
+var (
+	bufferPool sync.Pool
+)
+
+func Get() *bytes.Buffer {
+	if v := bufferPool.Get(); v == nil {
+		return new(bytes.Buffer)
+	} else {
+		return v.(*bytes.Buffer)
+	}
+}
+
+func Put(sb *bytes.Buffer) {
+	sb.Reset()
+	bufferPool.Put(sb)
+}

+ 30 - 331
pgenr.go

@@ -1,161 +1,25 @@
 package pgenr
 
 import (
-	"bytes"
+	"git.nspix.com/golang/pgenr/element"
 	"golang.org/x/net/html/atom"
 	"html"
-	"sync"
 )
 
-const (
-	TextThemeSuccess = "success"
-	TextThemeDanger  = "danger"
-)
-
-var (
-	bufferPool sync.Pool
-)
-
-func getBuffer() *bytes.Buffer {
-	if v := bufferPool.Get(); v == nil {
-		return new(bytes.Buffer)
-	} else {
-		return v.(*bytes.Buffer)
-	}
-}
-
-func releaseBuffer(sb *bytes.Buffer) {
-	sb.Reset()
-	bufferPool.Put(sb)
-}
-
 type (
-	Element interface {
-		Html() string
-	}
-
-	Style map[string]string
-
-	TextOption func(t *Text)
-
-	ButtonOption func(btn *Button)
-
-	Text struct {
-		Tag     atom.Atom
-		Content string
-		Theme   string
-		Color   string
-		Style   Style
-	}
-
-	Button struct {
-		Url   string
-		Text  string
-		Color string
-		Style Style
-	}
-
-	Entry struct {
-		Title Element
-		Items map[string]Element
-	}
-
-	Action struct {
-		Instructions Element
-		Button       *Button
-		InviteCode   string
-	}
-
-	Table struct {
-		Title  Element
-		Header []Element
-		Body   [][]Element
-	}
-
 	Page struct {
 		Title     string
 		Head      string
-		Intros    []Element
-		Entries   []*Entry
-		Actions   []*Action
-		Tables    []*Table
-		Outros    []Element
+		Intros    []element.Element
+		Entries   []*element.Entry
+		Actions   []*element.Action
+		Timelines []*element.Timeline
+		Tables    []*element.Table
+		Outros    []element.Element
 		Copyright string
 	}
 )
 
-func (s Style) Css(name string, value string) {
-	s[name] = value
-}
-
-func (s Style) String() string {
-	br := getBuffer()
-	defer releaseBuffer(br)
-	for k, v := range s {
-		br.WriteString(k)
-		br.WriteString(":")
-		br.WriteString(v)
-		br.WriteString(";")
-	}
-	return br.String()
-}
-
-func (a *Action) Html() string {
-	return a.String()
-}
-
-func (a *Action) String() string {
-	br := getBuffer()
-	defer releaseBuffer(br)
-	if a.Instructions != nil {
-		br.WriteString(a.Instructions.Html())
-	}
-	br.WriteString("<div class='action-block'>")
-	if a.InviteCode != "" {
-		br.WriteString("<span class='invite-code'>")
-		br.WriteString(html.EscapeString(a.InviteCode))
-		br.WriteString("</span>")
-	}
-	if a.Button != nil {
-		br.WriteString(a.Button.String())
-	}
-	br.WriteString("</div>")
-	return br.String()
-}
-
-func (e *Entry) Html() string {
-	return e.String()
-}
-
-func (e *Entry) String() string {
-	br := getBuffer()
-	defer releaseBuffer(br)
-	br.WriteString("<div class='grid'>")
-	if e.Title != nil {
-		br.WriteString(e.Title.Html())
-	}
-	for k, v := range e.Items {
-		br.WriteString("<div class='row'>")
-		br.WriteString("<div class='preview-label'>")
-		br.WriteString(k)
-		br.WriteString("</div>")
-		br.WriteString("<div class='preview-value'>")
-		br.WriteString(v.Html())
-		br.WriteString("</div>")
-		br.WriteString("</div>")
-	}
-	br.WriteString("</div>")
-	return br.String()
-}
-
-func (e *Entry) AddItem(label string, txt *Text) *Entry {
-	if e.Items == nil {
-		e.Items = make(map[string]Element)
-	}
-	e.Items[label] = txt
-	return e
-}
-
 func (page *Page) SetHead(s string) *Page {
 	page.Head = s
 	return page
@@ -167,42 +31,47 @@ func (page *Page) SetCopyright(s string) *Page {
 }
 
 func (page *Page) AddPlainIntro(s string) *Page {
-	page.Intros = append(page.Intros, NewText(s, WithTextTag(atom.P)))
+	page.Intros = append(page.Intros, element.NewText(s, element.WithTextTag(atom.P)))
 	return page
 }
 
-func (page *Page) AddIntro(t *Text) *Page {
-	page.Intros = append(page.Intros, t)
+func (page *Page) AddIntro(ele element.Element) *Page {
+	page.Intros = append(page.Intros, ele)
 	return page
 }
 
-func (page *Page) AddEntry(e *Entry) *Page {
+func (page *Page) AddEntry(e *element.Entry) *Page {
 	page.Entries = append(page.Entries, e)
 	return page
 }
 
-func (page *Page) AddTable(t *Table) *Page {
+func (page *Page) AddTable(t *element.Table) *Page {
 	page.Tables = append(page.Tables, t)
 	return page
 }
 
-func (page *Page) AddButtonAction(s string, btn *Button) *Page {
-	page.Actions = append(page.Actions, &Action{Instructions: NewText(s, WithTextTag(atom.P)), Button: btn})
+func (page *Page) AddTimeline(t *element.Timeline) *Page {
+	page.Timelines = append(page.Timelines, t)
+	return page
+}
+
+func (page *Page) AddButtonAction(s string, btn *element.Button) *Page {
+	page.Actions = append(page.Actions, &element.Action{Instructions: element.NewText(s, element.WithTextTag(atom.P)), Button: btn})
 	return page
 }
 
 func (page *Page) AddInviteCodeAction(s string, code string) *Page {
-	page.Actions = append(page.Actions, &Action{Instructions: NewText(s, WithTextTag(atom.P)), InviteCode: code})
+	page.Actions = append(page.Actions, &element.Action{Instructions: element.NewText(s, element.WithTextTag(atom.P)), InviteCode: code})
 	return page
 }
 
-func (page *Page) AddOutro(t *Text) *Page {
-	page.Outros = append(page.Outros, t)
+func (page *Page) AddOutro(ele element.Element) *Page {
+	page.Outros = append(page.Outros, ele)
 	return page
 }
 
 func (page *Page) AddPlainOutro(s string) *Page {
-	page.Outros = append(page.Outros, NewText(s, WithTextTag(atom.P)))
+	page.Outros = append(page.Outros, element.NewText(s, element.WithTextTag(atom.P)))
 	return page
 }
 
@@ -213,182 +82,12 @@ func (page *Page) Escape() {
 
 func NewPage(title string) *Page {
 	return &Page{
-		Title:   title,
-		Intros:  make([]Element, 0),
-		Entries: make([]*Entry, 0),
-		Actions: make([]*Action, 0),
-		Outros:  make([]Element, 0),
-		Tables:  make([]*Table, 0),
-	}
-}
-
-func NewEntry(title string) *Entry {
-	return &Entry{
-		Title: NewText(title, WithTextTag(atom.P)),
-		Items: make(map[string]Element),
-	}
-}
-
-func WithTextTheme(s string) TextOption {
-	return func(t *Text) {
-		t.Theme = s
-	}
-}
-
-func WithTextStyle(ms map[string]string) TextOption {
-	return func(t *Text) {
-		t.Style = ms
-	}
-}
-
-func WithTextTag(tag atom.Atom) TextOption {
-	return func(t *Text) {
-		t.Tag = tag
-	}
-}
-
-func WithTextColor(color string) TextOption {
-	return func(t *Text) {
-		t.Color = color
-	}
-}
-
-func (button *Button) Html() string {
-	return button.String()
-}
-
-func (button *Button) String() string {
-	br := getBuffer()
-	defer releaseBuffer(br)
-	if button.Style == nil {
-		button.Style = make(map[string]string)
-	}
-	if button.Color != "" {
-		button.Style["color"] = button.Color
-	}
-	br.WriteString("<a class='button'")
-	if button.Url != "" {
-		br.WriteString(" href='" + button.Url + "'")
-	}
-	if len(button.Style) > 0 {
-		br.WriteString(" style='")
-		br.WriteString(button.Style.String())
-		br.WriteString("'")
-	}
-	br.WriteString(">")
-	br.WriteString(html.EscapeString(button.Text))
-	br.WriteString("</a>")
-	return br.String()
-}
-
-func (table *Table) SetHead(elements ...Element) {
-	table.Header = elements
-}
-
-func (table *Table) AddCell(elements ...Element) {
-	if table.Body == nil {
-		table.Body = make([][]Element, 0)
-	}
-	table.Body = append(table.Body, elements)
-}
-
-func (table *Table) String() string {
-	br := getBuffer()
-	defer releaseBuffer(br)
-	br.WriteString("<div class='table-wrapper'>")
-	if table.Title != nil {
-		br.WriteString("<div class='table-title'>")
-		br.WriteString(table.Title.Html())
-		br.WriteString("</div>")
-	}
-	br.WriteString("<table class='table'>")
-
-	if len(table.Header) > 0 {
-		br.WriteString("<thead>")
-		br.WriteString("<tr>")
-		for _, text := range table.Header {
-			br.WriteString("<th>")
-			br.WriteString(text.Html())
-			br.WriteString("</th>")
-		}
-		br.WriteString("</tr>")
-		br.WriteString("</thead>")
-	}
-	if len(table.Body) > 0 {
-		br.WriteString("<tbody>")
-		for _, cell := range table.Body {
-			br.WriteString("<tr>")
-			for _, text := range cell {
-				br.WriteString("<td>")
-				br.WriteString(text.Html())
-				br.WriteString("</td>")
-			}
-			br.WriteString("</tr>")
-		}
-		br.WriteString("</tbody>")
-	}
-	br.WriteString("</table>")
-	br.WriteString("</div>")
-	return br.String()
-}
-
-func (text *Text) Html() string {
-	return text.String()
-}
-
-func (text *Text) String() string {
-	br := getBuffer()
-	defer releaseBuffer(br)
-	if text.Style == nil {
-		text.Style = make(map[string]string)
-	}
-	if text.Color != "" {
-		text.Style["color"] = text.Color
-	}
-	if text.Tag == 0 {
-		text.Tag = atom.Span
-	}
-	br.WriteString("<")
-	br.WriteString(text.Tag.String())
-	if text.Theme != "" {
-		br.WriteString(" class='text-")
-		br.WriteString(text.Theme)
-		br.WriteString("'")
-	}
-	if len(text.Style) > 0 {
-		br.WriteString(" style='")
-		br.WriteString(text.Style.String())
-		br.WriteString("'")
-	}
-	br.WriteString(">")
-	br.WriteString(html.EscapeString(text.Content))
-	br.WriteString("</")
-	br.WriteString(text.Tag.String())
-	br.WriteString(">")
-	return br.String()
-}
-
-func NewButton(label, link string, opts ...ButtonOption) *Button {
-	btn := &Button{Text: label, Url: link}
-	for _, cb := range opts {
-		cb(btn)
-	}
-	return btn
-}
-
-func NewText(s string, opts ...TextOption) *Text {
-	txt := &Text{Content: s, Tag: atom.Span}
-	for _, cb := range opts {
-		cb(txt)
-	}
-	return txt
-}
-
-func NewTable(title *Text) *Table {
-	table := &Table{
-		Title:  title,
-		Header: make([]Element, 0),
-		Body:   make([][]Element, 0),
+		Title:     title,
+		Intros:    make([]element.Element, 0),
+		Entries:   make([]*element.Entry, 0),
+		Actions:   make([]*element.Action, 0),
+		Timelines: make([]*element.Timeline, 0),
+		Outros:    make([]element.Element, 0),
+		Tables:    make([]*element.Table, 0),
 	}
-	return table
 }

+ 10 - 4
render_test.go

@@ -2,6 +2,7 @@ package pgenr
 
 import (
 	"fmt"
+	"git.nspix.com/golang/pgenr/element"
 	"io/ioutil"
 	"testing"
 	"time"
@@ -20,10 +21,15 @@ func TestRender(t *testing.T) {
 	page.AddPlainOutro("Need help, or have questions? Just reply to this email, we'd love to help.")
 	page.AddPlainOutro("Yours truly,")
 	page.AddPlainOutro("Hermes - https://google.com")
-	table := NewTable(NewText("This year sale table", WithTextStyle(map[string]string{"font-size": "1.06rem", "font-weight": "550"})))
-	table.SetHead(NewText("Name"), NewText("Age"), NewText("Price"))
-	table.AddCell(NewText("ZhanSan"), NewText("31"), NewText("185.6"))
-	table.AddCell(NewText("Lisi"), NewText("35"), NewText("102.6"))
+	timeline := element.NewTimeline()
+	timeline.AddItem(time.Now().Add(-2*time.Minute), element.NewText("Event start"))
+	timeline.AddItem(time.Now().Add(-1*time.Minute), element.NewText("Event end"))
+	timeline.AddItem(time.Now().Add(time.Minute), element.NewText("Event closed"))
+	page.AddTimeline(timeline)
+	table := element.NewTable(element.NewText("This year sale table", element.WithTextStyle(map[string]string{"font-size": "1.06rem", "font-weight": "550"})))
+	table.SetHead(element.NewText("Name"), element.NewText("Age"), element.NewText("Price"))
+	table.AddCell(element.NewText("ZhanSan"), element.NewText("31"), element.NewText("185.6"))
+	table.AddCell(element.NewText("Lisi"), element.NewText("35"), element.NewText("102.6"))
 	page.AddTable(table)
 	//page.AddButtonAction("To get started with Hermes, please click here:", NewButton("Confirm your account", "https://example-hermes.com/"))
 	page.AddInviteCodeAction("To get started with Hermes, please click here:", "950038")

+ 48 - 0
theme/default.go

@@ -117,6 +117,47 @@ func (theme *Default) Template() string {
       line-height: 2;
     }
 	
+	.timeline {
+		margin: 0;
+		font-size: 12px;
+		list-style: none;
+	}
+
+	.timeline-item {
+		position: relative;
+    	padding-bottom: 20px;
+		box-sizing: border-box;
+	}
+
+	.timeline-item-tail {
+		top: 0;
+		position: absolute;
+		left: 4px;
+		height: 100%;
+		border-left: 2px solid #e4e7ed;
+	}
+	
+	.timeline-item-node {
+		left: -1px;
+		width: 12px;
+		height: 12px;
+		position: absolute;
+		background-color: #e4e7ed;
+		border-color: #e4e7ed;
+		border-radius: 50%;
+		box-sizing: border-box;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+	}
+	
+	.timeline-item-wrapper {
+		width: 100%;
+		position: relative;
+		padding-left: 28px;
+		top: -3px;
+	}
+	
 	.table {
       border: 1px solid #ddd;
       width: 100%;
@@ -211,6 +252,13 @@ func (theme *Default) Template() string {
 				{{ end }}
 			{{ end }}
 		{{ end }}
+		{{ with .Page.Timelines }}
+			{{ if gt (len .) 0 }}
+				{{ range $timeline := . }}
+					{{ $timeline }}
+				{{ end }}
+			{{ end }}
+		{{ end }}
 		{{ with .Page.Tables }}
 			{{ if gt (len .) 0 }}
 				{{ range $table := . }}