Browse Source

修改渲染方式

fancl 2 years ago
parent
commit
ab5d5679b8
12 changed files with 531 additions and 335 deletions
  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
 package pgenr
 
 
 import (
 import (
-	"bytes"
+	"git.nspix.com/golang/pgenr/element"
 	"golang.org/x/net/html/atom"
 	"golang.org/x/net/html/atom"
 	"html"
 	"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 (
 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 {
 	Page struct {
 		Title     string
 		Title     string
 		Head      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
 		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 {
 func (page *Page) SetHead(s string) *Page {
 	page.Head = s
 	page.Head = s
 	return page
 	return page
@@ -167,42 +31,47 @@ func (page *Page) SetCopyright(s string) *Page {
 }
 }
 
 
 func (page *Page) AddPlainIntro(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
 	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
 	return page
 }
 }
 
 
-func (page *Page) AddEntry(e *Entry) *Page {
+func (page *Page) AddEntry(e *element.Entry) *Page {
 	page.Entries = append(page.Entries, e)
 	page.Entries = append(page.Entries, e)
 	return page
 	return page
 }
 }
 
 
-func (page *Page) AddTable(t *Table) *Page {
+func (page *Page) AddTable(t *element.Table) *Page {
 	page.Tables = append(page.Tables, t)
 	page.Tables = append(page.Tables, t)
 	return page
 	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
 	return page
 }
 }
 
 
 func (page *Page) AddInviteCodeAction(s string, code string) *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
 	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
 	return page
 }
 }
 
 
 func (page *Page) AddPlainOutro(s string) *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
 	return page
 }
 }
 
 
@@ -213,182 +82,12 @@ func (page *Page) Escape() {
 
 
 func NewPage(title string) *Page {
 func NewPage(title string) *Page {
 	return &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 (
 import (
 	"fmt"
 	"fmt"
+	"git.nspix.com/golang/pgenr/element"
 	"io/ioutil"
 	"io/ioutil"
 	"testing"
 	"testing"
 	"time"
 	"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("Need help, or have questions? Just reply to this email, we'd love to help.")
 	page.AddPlainOutro("Yours truly,")
 	page.AddPlainOutro("Yours truly,")
 	page.AddPlainOutro("Hermes - https://google.com")
 	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.AddTable(table)
 	//page.AddButtonAction("To get started with Hermes, please click here:", NewButton("Confirm your account", "https://example-hermes.com/"))
 	//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")
 	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;
       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 {
 	.table {
       border: 1px solid #ddd;
       border: 1px solid #ddd;
       width: 100%;
       width: 100%;
@@ -211,6 +252,13 @@ func (theme *Default) Template() string {
 				{{ end }}
 				{{ end }}
 			{{ end }}
 			{{ end }}
 		{{ end }}
 		{{ end }}
+		{{ with .Page.Timelines }}
+			{{ if gt (len .) 0 }}
+				{{ range $timeline := . }}
+					{{ $timeline }}
+				{{ end }}
+			{{ end }}
+		{{ end }}
 		{{ with .Page.Tables }}
 		{{ with .Page.Tables }}
 			{{ if gt (len .) 0 }}
 			{{ if gt (len .) 0 }}
 				{{ range $table := . }}
 				{{ range $table := . }}