├── LICENSE ├── README.md ├── api.go ├── elements.go ├── example_test.go ├── go.mod ├── go.sum ├── if.go ├── modd.conf ├── tag.go ├── tag_test.go └── utils.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 The Plant 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## htmlgo 4 | 5 | Type safe and modularize way to generate html on server side. 6 | Download the package with `go get -v github.com/theplant/htmlgo` and import the package with `.` gives you simpler code: 7 | 8 | 9 | ```go 10 | import ( 11 | . "github.com/theplant/htmlgo" 12 | ) 13 | ``` 14 | 15 | also checkout full API documentation at: https://godoc.org/github.com/theplant/htmlgo 16 | 17 | 18 | 19 | Create a simple div, Text will be escaped by html 20 | ```go 21 | banner := "We write html in Go" 22 | comp := Div( 23 | Text("123

"), 24 | Textf("Hello, %s", banner), 25 | Br(), 26 | ) 27 | Fprint(os.Stdout, comp, context.TODO()) 28 | //Output: 29 | //
123<h1>Hello, We write html in Go 30 | //
31 | ``` 32 | 33 | Create a full html page 34 | ```go 35 | comp := HTML( 36 | Head( 37 | Meta().Charset("utf8"), 38 | Title("My test page"), 39 | ), 40 | Body( 41 | Img("images/firefox-icon.png").Alt("My test image"), 42 | ), 43 | ) 44 | Fprint(os.Stdout, comp, context.TODO()) 45 | //Output: 46 | // 47 | // 48 | // 49 | // 50 | // 51 | // 52 | // My test page 53 | // 54 | // 55 | // 56 | // My test image 57 | // 58 | // 59 | ``` 60 | 61 | Use RawHTML and Component 62 | ```go 63 | userProfile := func(username string, avatarURL string) HTMLComponent { 64 | return ComponentFunc(func(ctx context.Context) (r []byte, err error) { 65 | return Div( 66 | H1(username).Class("profileName"), 67 | Img(avatarURL).Class("profileImage"), 68 | RawHTML("complicated svg\n"), 69 | ).Class("userProfile").MarshalHTML(ctx) 70 | }) 71 | } 72 | 73 | comp := Ul( 74 | Li( 75 | userProfile("felix

", "http://image.com/img1.png"), 76 | ), 77 | Li( 78 | userProfile("john", "http://image.com/img2.png"), 79 | ), 80 | ) 81 | Fprint(os.Stdout, comp, context.TODO()) 82 | //Output: 83 | // 102 | ``` 103 | 104 | More complicated customized component 105 | ```go 106 | /* 107 | Define MySelect as follows: 108 | 109 | type MySelectBuilder struct { 110 | options [][]string 111 | selected string 112 | } 113 | 114 | func MySelect() *MySelectBuilder { 115 | return &MySelectBuilder{} 116 | } 117 | 118 | func (b *MySelectBuilder) Options(opts [][]string) (r *MySelectBuilder) { 119 | b.options = opts 120 | return b 121 | } 122 | 123 | func (b *MySelectBuilder) Selected(selected string) (r *MySelectBuilder) { 124 | b.selected = selected 125 | return b 126 | } 127 | 128 | func (b *MySelectBuilder) MarshalHTML(ctx context.Context) (r []byte, err error) { 129 | opts := []HTMLComponent{} 130 | for _, op := range b.options { 131 | var opt HTMLComponent 132 | if op[0] == b.selected { 133 | opt = Option(op[1]).Value(op[0]).Attr("selected", "true") 134 | } else { 135 | opt = Option(op[1]).Value(op[0]) 136 | } 137 | opts = append(opts, opt) 138 | } 139 | return Select(opts...).MarshalHTML(ctx) 140 | } 141 | */ 142 | 143 | comp := MySelect().Options([][]string{ 144 | {"1", "label 1"}, 145 | {"2", "label 2"}, 146 | {"3", "label 3"}, 147 | }).Selected("2") 148 | 149 | Fprint(os.Stdout, comp, context.TODO()) 150 | //Output: 151 | // 158 | ``` 159 | 160 | Write a little bit of JavaScript and stylesheet 161 | ```go 162 | comp := Div( 163 | Button("Hello").Id("hello"), 164 | Style(` 165 | .container { 166 | background-color: red; 167 | } 168 | `), 169 | 170 | Script(` 171 | var b = document.getElementById("hello") 172 | b.onclick = function(e){ 173 | alert("Hello"); 174 | } 175 | `), 176 | ).Class("container") 177 | 178 | Fprint(os.Stdout, comp, context.TODO()) 179 | //Output: 180 | //
181 | // 182 | // 183 | // 188 | // 189 | // 195 | //
196 | ``` 197 | 198 | An example about how to integrate into http.Handler, and how to do layout, and how to use context. 199 | ```go 200 | type User struct { 201 | Name string 202 | } 203 | 204 | userStatus := func() HTMLComponent { 205 | return ComponentFunc(func(ctx context.Context) (r []byte, err error) { 206 | 207 | if currentUser, ok := ctx.Value("currentUser").(*User); ok { 208 | return Div( 209 | Text(currentUser.Name), 210 | ).Class("username").MarshalHTML(ctx) 211 | } 212 | 213 | return Div(Text("Login")).Class("login").MarshalHTML(ctx) 214 | }) 215 | } 216 | 217 | myHeader := func() HTMLComponent { 218 | return Div( 219 | Text("header"), 220 | userStatus(), 221 | ).Class("header") 222 | } 223 | myFooter := func() HTMLComponent { 224 | return Div(Text("footer")).Class("footer") 225 | } 226 | 227 | layout := func(in HTMLComponent) (out HTMLComponent) { 228 | out = HTML( 229 | Head( 230 | Meta().Charset("utf8"), 231 | ), 232 | Body( 233 | myHeader(), 234 | in, 235 | myFooter(), 236 | ), 237 | ) 238 | return 239 | } 240 | 241 | getLoginUserFromCookie := func(r *http.Request) *User { 242 | return &User{Name: "felix"} 243 | } 244 | 245 | homeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 246 | user := getLoginUserFromCookie(r) 247 | ctx := context.WithValue(context.TODO(), "currentUser", user) 248 | 249 | root := Div( 250 | Text("This is my home page"), 251 | ) 252 | 253 | Fprint(w, layout(root), ctx) 254 | }) 255 | 256 | w := httptest.NewRecorder() 257 | r := httptest.NewRequest("GET", "/", nil) 258 | homeHandler.ServeHTTP(w, r) 259 | 260 | fmt.Println(w.Body.String()) 261 | 262 | //Output: 263 | // 264 | // 265 | // 266 | // 267 | // 268 | // 269 | // 270 | // 271 | //
header 272 | //
felix
273 | //
274 | // 275 | //
This is my home page
276 | // 277 | // 278 | // 279 | // 280 | ``` 281 | 282 | An example show how to set different type of attributes 283 | ```go 284 | type MoreData struct { 285 | Name string 286 | Count int 287 | } 288 | comp := Div( 289 | Input("username"). 290 | Type("checkbox"). 291 | Attr("checked", true). 292 | Attr("more-data", &MoreData{Name: "felix", Count: 100}). 293 | Attr("max-length", 10), 294 | Input("username2"). 295 | Type("checkbox"). 296 | Attr("checked", false), 297 | ) 298 | Fprint(os.Stdout, comp, context.TODO()) 299 | //Output: 300 | //
301 | // 302 | // 303 | // 304 | //
305 | ``` 306 | 307 | An example show how to set styles 308 | ```go 309 | comp := Div(). 310 | StyleIf("background-color:red; border:1px solid red;", true). 311 | StyleIf("color:blue", true) 312 | Fprint(os.Stdout, comp, context.TODO()) 313 | //Output: 314 | //
315 | ``` 316 | 317 | An example to use If, `Iff` is for body to passed in as an func for the body depends on if condition not to be nil, `If` is for directly passed in HTMLComponent 318 | ```go 319 | type Person struct { 320 | Age int 321 | } 322 | var p *Person 323 | 324 | name := "Leon" 325 | comp := Div( 326 | Iff(p != nil && p.Age > 18, func() HTMLComponent { 327 | return Div().Text(name + ": Age > 18") 328 | }).ElseIf(p == nil, func() HTMLComponent { 329 | return Div().Text("No person named " + name) 330 | }).Else(func() HTMLComponent { 331 | return Div().Text(name + ":Age <= 18") 332 | }), 333 | ) 334 | Fprint(os.Stdout, comp, context.TODO()) 335 | //Output: 336 | //
337 | //
No person named Leon
338 | //
339 | ``` 340 | 341 | 342 | 343 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | ## htmlgo 4 | 5 | Type safe and modularize way to generate html on server side. 6 | Download the package with `go get -v github.com/theplant/htmlgo` and import the package with `.` gives you simpler code: 7 | 8 | import ( 9 | . "github.com/theplant/htmlgo" 10 | ) 11 | 12 | also checkout full API documentation at: https://godoc.org/github.com/theplant/htmlgo 13 | 14 | */ 15 | package htmlgo 16 | 17 | import ( 18 | "context" 19 | ) 20 | 21 | type HTMLComponent interface { 22 | MarshalHTML(ctx context.Context) ([]byte, error) 23 | } 24 | 25 | type ComponentFunc func(ctx context.Context) (r []byte, err error) 26 | 27 | func (f ComponentFunc) MarshalHTML(ctx context.Context) (r []byte, err error) { 28 | return f(ctx) 29 | } 30 | 31 | type MutableAttrHTMLComponent interface { 32 | HTMLComponent 33 | SetAttr(k string, v interface{}) 34 | } 35 | -------------------------------------------------------------------------------- /elements.go: -------------------------------------------------------------------------------- 1 | package htmlgo 2 | 3 | // "a": HTMLAnchorElement; 4 | func A(children ...HTMLComponent) (r *HTMLTagBuilder) { 5 | return Tag("a").Children(children...) 6 | } 7 | 8 | // "abbr": HTMLElement; 9 | func Abbr(text string) (r *HTMLTagBuilder) { 10 | return Tag("abbr").Text(text) 11 | } 12 | 13 | // "address": HTMLElement; 14 | func Address(children ...HTMLComponent) (r *HTMLTagBuilder) { 15 | return Tag("address").Children(children...) 16 | } 17 | 18 | // "applet": HTMLAppletElement; 19 | // Not support 20 | 21 | // "area": HTMLAreaElement; 22 | func Area() (r *HTMLTagBuilder) { 23 | return Tag("area").OmitEndTag() 24 | } 25 | 26 | // "article": HTMLElement; 27 | func Article(children ...HTMLComponent) (r *HTMLTagBuilder) { 28 | return Tag("article").Children(children...) 29 | } 30 | 31 | // "aside": HTMLElement; 32 | func Aside(children ...HTMLComponent) (r *HTMLTagBuilder) { 33 | return Tag("aside").Children(children...) 34 | } 35 | 36 | // "audio": HTMLAudioElement; 37 | func Audio(children ...HTMLComponent) (r *HTMLTagBuilder) { 38 | return Tag("audio").Children(children...) 39 | } 40 | 41 | // "b": HTMLElement; 42 | func B(text string) (r *HTMLTagBuilder) { 43 | return Tag("b").Text(text) 44 | } 45 | 46 | // "base": HTMLBaseElement; 47 | func Base() (r *HTMLTagBuilder) { 48 | return Tag("base").OmitEndTag() 49 | } 50 | 51 | // "basefont": HTMLBaseFontElement; 52 | // Not Support 53 | 54 | // "bdi": HTMLElement; 55 | func Bdi(text string) (r *HTMLTagBuilder) { 56 | return Tag("bdi").Text(text) 57 | } 58 | 59 | // "bdo": HTMLElement; 60 | func Bdo(text string) (r *HTMLTagBuilder) { 61 | return Tag("bdo").Text(text) 62 | } 63 | 64 | // "blockquote": HTMLQuoteElement; 65 | func Blockquote(children ...HTMLComponent) (r *HTMLTagBuilder) { 66 | return Tag("blockquote").Children(children...) 67 | } 68 | 69 | // "body": HTMLBodyElement; 70 | func Body(children ...HTMLComponent) (r *HTMLTagBuilder) { 71 | return Tag("body").Children(children...) 72 | } 73 | 74 | // "br": HTMLBRElement; 75 | func Br() (r *HTMLTagBuilder) { 76 | return Tag("br").OmitEndTag() 77 | } 78 | 79 | // "button": HTMLButtonElement; 80 | func Button(label string) (r *HTMLTagBuilder) { 81 | return Tag("button").Text(label) 82 | } 83 | 84 | // "canvas": HTMLCanvasElement; 85 | func Canvas(children ...HTMLComponent) (r *HTMLTagBuilder) { 86 | return Tag("canvas").Children(children...) 87 | } 88 | 89 | // "caption": HTMLTableCaptionElement; 90 | func Caption(text string) (r *HTMLTagBuilder) { 91 | return Tag("caption").Text(text) 92 | } 93 | 94 | // "cite": HTMLElement; 95 | func Cite(children ...HTMLComponent) (r *HTMLTagBuilder) { 96 | return Tag("cite").Children(children...) 97 | } 98 | 99 | // "code": HTMLElement; 100 | func Code(text string) (r *HTMLTagBuilder) { 101 | return Tag("code").Text(text) 102 | } 103 | 104 | // "col": HTMLTableColElement; 105 | func Col() (r *HTMLTagBuilder) { 106 | return Tag("col").OmitEndTag() 107 | } 108 | 109 | // "colgroup": HTMLTableColElement; 110 | func Colgroup(children ...HTMLComponent) (r *HTMLTagBuilder) { 111 | return Tag("colgroup").Children(children...) 112 | } 113 | 114 | // "data": HTMLDataElement; 115 | func Data(children ...HTMLComponent) (r *HTMLTagBuilder) { 116 | return Tag("data").Children(children...) 117 | } 118 | 119 | // "datalist": HTMLDataListElement; 120 | func Datalist(children ...HTMLComponent) (r *HTMLTagBuilder) { 121 | return Tag("datalist").Children(children...) 122 | } 123 | 124 | // "dd": HTMLElement; 125 | func Dd(children ...HTMLComponent) (r *HTMLTagBuilder) { 126 | return Tag("dd").Children(children...) 127 | } 128 | 129 | // "del": HTMLModElement; 130 | func Del(text string) (r *HTMLTagBuilder) { 131 | return Tag("del").Text(text) 132 | } 133 | 134 | // "details": HTMLDetailsElement; 135 | 136 | func Details(children ...HTMLComponent) (r *HTMLTagBuilder) { 137 | return Tag("details").Children(children...) 138 | } 139 | 140 | // "dfn": HTMLElement; 141 | func Dfn(text string) (r *HTMLTagBuilder) { 142 | return Tag("dfn").Text(text) 143 | } 144 | 145 | // "dialog": HTMLDialogElement; 146 | func Dialog(children ...HTMLComponent) (r *HTMLTagBuilder) { 147 | return Tag("dialog").Children(children...) 148 | } 149 | 150 | // "dir": HTMLDirectoryElement; 151 | // Not Support 152 | 153 | // "div": HTMLDivElement; 154 | func Div(children ...HTMLComponent) (r *HTMLTagBuilder) { 155 | return Tag("div").Children(children...) 156 | } 157 | 158 | // "dl": HTMLDListElement; 159 | func Dl(children ...HTMLComponent) (r *HTMLTagBuilder) { 160 | return Tag("dl").Children(children...) 161 | } 162 | 163 | // "dt": HTMLElement; 164 | func Dt(children ...HTMLComponent) (r *HTMLTagBuilder) { 165 | return Tag("dt").Children(children...) 166 | } 167 | 168 | // "em": HTMLElement; 169 | func Em(text string) (r *HTMLTagBuilder) { 170 | return Tag("em").Text(text) 171 | } 172 | 173 | // "embed": HTMLEmbedElement; 174 | func Embed() (r *HTMLTagBuilder) { 175 | return Tag("embed").OmitEndTag() 176 | } 177 | 178 | // "fieldset": HTMLFieldSetElement; 179 | func Fieldset(children ...HTMLComponent) (r *HTMLTagBuilder) { 180 | return Tag("fieldset").Children(children...) 181 | } 182 | 183 | // "figcaption": HTMLElement; 184 | func Figcaption(text string) (r *HTMLTagBuilder) { 185 | return Tag("figcaption").Text(text) 186 | } 187 | 188 | // "figure": HTMLElement; 189 | func Figure(children ...HTMLComponent) (r *HTMLTagBuilder) { 190 | return Tag("figure").Children(children...) 191 | } 192 | 193 | // "font": HTMLFontElement; 194 | // Not Support 195 | 196 | // "footer": HTMLElement; 197 | func Footer(children ...HTMLComponent) (r *HTMLTagBuilder) { 198 | return Tag("footer").Children(children...) 199 | } 200 | 201 | // "form": HTMLFormElement; 202 | func Form(children ...HTMLComponent) (r *HTMLTagBuilder) { 203 | return Tag("form").Children(children...) 204 | } 205 | 206 | // "frame": HTMLFrameElement; 207 | // Not Support 208 | 209 | // "frameset": HTMLFrameSetElement; 210 | // Not Support 211 | 212 | // "h1": HTMLHeadingElement; 213 | func H1(text string) (r *HTMLTagBuilder) { 214 | return Tag("h1").Text(text) 215 | } 216 | 217 | // "h2": HTMLHeadingElement; 218 | func H2(text string) (r *HTMLTagBuilder) { 219 | return Tag("h2").Text(text) 220 | } 221 | 222 | // "h3": HTMLHeadingElement; 223 | func H3(text string) (r *HTMLTagBuilder) { 224 | return Tag("h3").Text(text) 225 | } 226 | 227 | // "h4": HTMLHeadingElement; 228 | func H4(text string) (r *HTMLTagBuilder) { 229 | return Tag("h4").Text(text) 230 | } 231 | 232 | // "h5": HTMLHeadingElement; 233 | func H5(text string) (r *HTMLTagBuilder) { 234 | return Tag("h5").Text(text) 235 | } 236 | 237 | // "h6": HTMLHeadingElement; 238 | func H6(text string) (r *HTMLTagBuilder) { 239 | return Tag("h6").Text(text) 240 | } 241 | 242 | // "head": HTMLHeadElement; 243 | 244 | func Head(children ...HTMLComponent) (r *HTMLTagBuilder) { 245 | return Tag("head").Children(children...) 246 | } 247 | 248 | // "header": HTMLElement; 249 | func Header(children ...HTMLComponent) (r *HTMLTagBuilder) { 250 | return Tag("header").Children(children...) 251 | } 252 | 253 | // "hgroup": HTMLElement; 254 | func Hgroup(children ...HTMLComponent) (r *HTMLTagBuilder) { 255 | return Tag("hgroup").Children(children...) 256 | } 257 | 258 | // "hr": HTMLHRElement; 259 | func Hr() (r *HTMLTagBuilder) { 260 | return Tag("hr").OmitEndTag() 261 | } 262 | 263 | // "html": HTMLHtmlElement; 264 | func HTML(children ...HTMLComponent) (r HTMLComponent) { 265 | return HTMLComponents{ 266 | RawHTML("\n"), 267 | Tag("html").Children(children...), 268 | } 269 | } 270 | 271 | // "i": HTMLElement; 272 | func I(text string) (r *HTMLTagBuilder) { 273 | return Tag("i").Text(text) 274 | } 275 | 276 | // "iframe": HTMLIFrameElement; 277 | func Iframe(children ...HTMLComponent) (r *HTMLTagBuilder) { 278 | return Tag("iframe").Children(children...) 279 | } 280 | 281 | // "img": HTMLImageElement; 282 | func Img(src string) (r *HTMLTagBuilder) { 283 | return Tag("img").OmitEndTag().Attr("src", src) 284 | } 285 | 286 | // "input": HTMLInputElement; 287 | 288 | func Input(name string) (r *HTMLTagBuilder) { 289 | return Tag("input").OmitEndTag().Attr("name", name) 290 | } 291 | 292 | // "ins": HTMLModElement; 293 | func Ins(children ...HTMLComponent) (r *HTMLTagBuilder) { 294 | return Tag("ins").Children(children...) 295 | } 296 | 297 | // "kbd": HTMLElement; 298 | func Kbd(text string) (r *HTMLTagBuilder) { 299 | return Tag("kbd").Text(text) 300 | } 301 | 302 | // "label": HTMLLabelElement; 303 | func Label(text string) (r *HTMLTagBuilder) { 304 | return Tag("label").Text(text) 305 | } 306 | 307 | // "legend": HTMLLegendElement; 308 | func Legend(text string) (r *HTMLTagBuilder) { 309 | return Tag("legend").Text(text) 310 | } 311 | 312 | // "li": HTMLLIElement; 313 | func Li(children ...HTMLComponent) (r *HTMLTagBuilder) { 314 | return Tag("li").Children(children...) 315 | } 316 | 317 | // "link": HTMLLinkElement; 318 | func Link(href string) (r *HTMLTagBuilder) { 319 | return Tag("link").OmitEndTag().Attr("href", href) 320 | } 321 | 322 | // "main": HTMLElement; 323 | func Main(children ...HTMLComponent) (r *HTMLTagBuilder) { 324 | return Tag("main").Children(children...) 325 | } 326 | 327 | // "map": HTMLMapElement; 328 | func Map(children ...HTMLComponent) (r *HTMLTagBuilder) { 329 | return Tag("map").Children(children...) 330 | } 331 | 332 | // "mark": HTMLElement; 333 | func Mark(text string) (r *HTMLTagBuilder) { 334 | return Tag("mark").Text(text) 335 | } 336 | 337 | // "marquee": HTMLMarqueeElement; 338 | // Not Support 339 | 340 | // "menu": HTMLMenuElement; 341 | func Menu(children ...HTMLComponent) (r *HTMLTagBuilder) { 342 | return Tag("menu").Children(children...) 343 | } 344 | 345 | // "meta": HTMLMetaElement; 346 | 347 | func Meta() (r *HTMLTagBuilder) { 348 | return Tag("meta").OmitEndTag() 349 | } 350 | 351 | // "meter": HTMLMeterElement; 352 | func Meter(children ...HTMLComponent) (r *HTMLTagBuilder) { 353 | return Tag("meter").Children(children...) 354 | } 355 | 356 | // "nav": HTMLElement; 357 | func Nav(children ...HTMLComponent) (r *HTMLTagBuilder) { 358 | return Tag("nav").Children(children...) 359 | } 360 | 361 | // "noscript": HTMLElement; 362 | func Noscript(children ...HTMLComponent) (r *HTMLTagBuilder) { 363 | return Tag("noscript").Children(children...) 364 | } 365 | 366 | // "object": HTMLObjectElement; 367 | func Object(data string) (r *HTMLTagBuilder) { 368 | return Tag("object").Attr("data", data) 369 | } 370 | 371 | // "ol": HTMLOListElement; 372 | func Ol(children ...HTMLComponent) (r *HTMLTagBuilder) { 373 | return Tag("ol").Children(children...) 374 | } 375 | 376 | // "optgroup": HTMLOptGroupElement; 377 | func Optgroup(children ...HTMLComponent) (r *HTMLTagBuilder) { 378 | return Tag("optgroup").Children(children...) 379 | } 380 | 381 | // "option": HTMLOptionElement; 382 | func Option(text string) (r *HTMLTagBuilder) { 383 | return Tag("option").Text(text) 384 | } 385 | 386 | // "output": HTMLOutputElement; 387 | func Output(children ...HTMLComponent) (r *HTMLTagBuilder) { 388 | return Tag("output").Children(children...) 389 | } 390 | 391 | // "p": HTMLParagraphElement; 392 | func P(children ...HTMLComponent) (r *HTMLTagBuilder) { 393 | return Tag("p").Children(children...) 394 | } 395 | 396 | // "param": HTMLParamElement; 397 | func Param(name string) (r *HTMLTagBuilder) { 398 | return Tag("param").OmitEndTag().Attr("name", name) 399 | } 400 | 401 | // "picture": HTMLPictureElement; 402 | func Picture(children ...HTMLComponent) (r *HTMLTagBuilder) { 403 | return Tag("picture").Children(children...) 404 | } 405 | 406 | // "pre": HTMLPreElement; 407 | func Pre(text string) (r *HTMLTagBuilder) { 408 | return Tag("pre").Text(text) 409 | } 410 | 411 | // "progress": HTMLProgressElement; 412 | func Progress(children ...HTMLComponent) (r *HTMLTagBuilder) { 413 | return Tag("progress").Children(children...) 414 | } 415 | 416 | // "q": HTMLQuoteElement; 417 | func Q(text string) (r *HTMLTagBuilder) { 418 | return Tag("q").Text(text) 419 | } 420 | 421 | // "rp": HTMLElement; 422 | func Rp(text string) (r *HTMLTagBuilder) { 423 | return Tag("rp").Text(text) 424 | } 425 | 426 | // "rt": HTMLElement; 427 | func Rt(text string) (r *HTMLTagBuilder) { 428 | return Tag("rt").Text(text) 429 | } 430 | 431 | // "ruby": HTMLElement; 432 | func Ruby(children ...HTMLComponent) (r *HTMLTagBuilder) { 433 | return Tag("ruby").Children(children...) 434 | } 435 | 436 | // "s": HTMLElement; 437 | func S(text string) (r *HTMLTagBuilder) { 438 | return Tag("s").Text(text) 439 | } 440 | 441 | // "samp": HTMLElement; 442 | 443 | func Samp(children ...HTMLComponent) (r *HTMLTagBuilder) { 444 | return Tag("samp").Children(children...) 445 | } 446 | 447 | // "script": HTMLScriptElement; 448 | func Script(script string) (r *HTMLTagBuilder) { 449 | return Tag("script"). 450 | Attr("type", "text/javascript"). 451 | Children(RawHTML(script)) 452 | } 453 | 454 | // "section": HTMLElement; 455 | func Section(children ...HTMLComponent) (r *HTMLTagBuilder) { 456 | return Tag("section").Children(children...) 457 | } 458 | 459 | // "select": HTMLSelectElement; 460 | func Select(children ...HTMLComponent) (r *HTMLTagBuilder) { 461 | return Tag("select").Children(children...) 462 | } 463 | 464 | // "slot": HTMLSlotElement; 465 | func Slot(children ...HTMLComponent) (r *HTMLTagBuilder) { 466 | return Tag("slot").Children(children...) 467 | } 468 | 469 | // "small": HTMLElement; 470 | func Small(text string) (r *HTMLTagBuilder) { 471 | return Tag("small").Text(text) 472 | } 473 | 474 | // "source": HTMLSourceElement; 475 | func Source(src string) (r *HTMLTagBuilder) { 476 | return Tag("source").OmitEndTag().Attr("src", src) 477 | } 478 | 479 | // "span": HTMLSpanElement; 480 | func Span(text string) (r *HTMLTagBuilder) { 481 | return Tag("span").Text(text) 482 | } 483 | 484 | // "strong": HTMLElement; 485 | func Strong(text string) (r *HTMLTagBuilder) { 486 | return Tag("strong").Text(text) 487 | } 488 | 489 | // "style": HTMLStyleElement; 490 | func Style(style string) (r *HTMLTagBuilder) { 491 | return Tag("style"). 492 | Attr("type", "text/css"). 493 | Children(RawHTML(style)) 494 | } 495 | 496 | // "sub": HTMLElement; 497 | func Sub(text string) (r *HTMLTagBuilder) { 498 | return Tag("sub").Text(text) 499 | } 500 | 501 | // "summary": HTMLElement; 502 | func Summary(children ...HTMLComponent) (r *HTMLTagBuilder) { 503 | return Tag("summary").Children(children...) 504 | } 505 | 506 | // "sup": HTMLElement; 507 | func Sup(text string) (r *HTMLTagBuilder) { 508 | return Tag("sup").Text(text) 509 | } 510 | 511 | // "table": HTMLTableElement; 512 | func Table(children ...HTMLComponent) (r *HTMLTagBuilder) { 513 | return Tag("table").Children(children...) 514 | } 515 | 516 | // "tbody": HTMLTableSectionElement; 517 | func Tbody(children ...HTMLComponent) (r *HTMLTagBuilder) { 518 | return Tag("tbody").Children(children...) 519 | } 520 | 521 | // "td": HTMLTableDataCellElement; 522 | func Td(children ...HTMLComponent) (r *HTMLTagBuilder) { 523 | return Tag("td").Children(children...) 524 | } 525 | 526 | // "template": HTMLTemplateElement; 527 | func Template(children ...HTMLComponent) (r *HTMLTagBuilder) { 528 | return Tag("template").Children(children...) 529 | } 530 | 531 | // "textarea": HTMLTextAreaElement; 532 | func Textarea(text string) (r *HTMLTagBuilder) { 533 | return Tag("textarea").Text(text) 534 | } 535 | 536 | // "tfoot": HTMLTableSectionElement; 537 | func Tfoot(children ...HTMLComponent) (r *HTMLTagBuilder) { 538 | return Tag("tfoot").Children(children...) 539 | } 540 | 541 | // "th": HTMLTableHeaderCellElement; 542 | func Th(text string) (r *HTMLTagBuilder) { 543 | return Tag("th").Text(text) 544 | } 545 | 546 | // "thead": HTMLTableSectionElement; 547 | func Thead(children ...HTMLComponent) (r *HTMLTagBuilder) { 548 | return Tag("thead").Children(children...) 549 | } 550 | 551 | // "time": HTMLTimeElement; 552 | func Time(datetime string) (r *HTMLTagBuilder) { 553 | return Tag("time").Attr("datetime", datetime) 554 | } 555 | 556 | // "title": HTMLTitleElement; 557 | func Title(text string) (r *HTMLTagBuilder) { 558 | return Tag("title").Text(text) 559 | } 560 | 561 | // "tr": HTMLTableRowElement; 562 | func Tr(children ...HTMLComponent) (r *HTMLTagBuilder) { 563 | return Tag("tr").Children(children...) 564 | } 565 | 566 | // "track": HTMLTrackElement; 567 | func Track(src string) (r *HTMLTagBuilder) { 568 | return Tag("track").OmitEndTag().Attr("src", src) 569 | } 570 | 571 | // "u": HTMLElement; 572 | func U(text string) (r *HTMLTagBuilder) { 573 | return Tag("u").Text(text) 574 | } 575 | 576 | // "ul": HTMLUListElement; 577 | func Ul(children ...HTMLComponent) (r *HTMLTagBuilder) { 578 | return Tag("ul").Children(children...) 579 | } 580 | 581 | // "var": HTMLElement; 582 | func Var(text string) (r *HTMLTagBuilder) { 583 | return Tag("var").Text(text) 584 | } 585 | 586 | // "video": HTMLVideoElement; 587 | func Video(children ...HTMLComponent) (r *HTMLTagBuilder) { 588 | return Tag("video").Children(children...) 589 | } 590 | 591 | // "wbr": HTMLElement; 592 | func Wbr() (r *HTMLTagBuilder) { 593 | return Tag("wbr").OmitEndTag() 594 | } 595 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package htmlgo_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | 10 | . "github.com/theplant/htmlgo" 11 | ) 12 | 13 | /* 14 | Create a simple div, Text will be escaped by html 15 | */ 16 | func ExampleTag_01simplediv() { 17 | banner := "We write html in Go" 18 | comp := Div( 19 | Text("123

"), 20 | Textf("Hello, %s", banner), 21 | Br(), 22 | ) 23 | Fprint(os.Stdout, comp, context.TODO()) 24 | //Output: 25 | //
123<h1>Hello, We write html in Go 26 | //
27 | //
28 | } 29 | 30 | /* 31 | Create a full html page 32 | */ 33 | func ExampleTag_02fullhtml() { 34 | comp := HTML( 35 | Head( 36 | Meta().Charset("utf8"), 37 | Title("My test page"), 38 | ), 39 | Body( 40 | Img("images/firefox-icon.png").Alt("My test image"), 41 | ), 42 | ) 43 | Fprint(os.Stdout, comp, context.TODO()) 44 | //Output: 45 | // 46 | // 47 | // 48 | // 49 | // 50 | // 51 | // My test page 52 | // 53 | // 54 | // 55 | // My test image 56 | // 57 | // 58 | } 59 | 60 | /* 61 | Use RawHTML and Component 62 | */ 63 | func ExampleTag_03rawhtmlandcomponent() { 64 | userProfile := func(username string, avatarURL string) HTMLComponent { 65 | return ComponentFunc(func(ctx context.Context) (r []byte, err error) { 66 | return Div( 67 | H1(username).Class("profileName"), 68 | Img(avatarURL).Class("profileImage"), 69 | RawHTML("complicated svg\n"), 70 | ).Class("userProfile").MarshalHTML(ctx) 71 | }) 72 | } 73 | 74 | comp := Ul( 75 | Li( 76 | userProfile("felix

", "http://image.com/img1.png"), 77 | ), 78 | Li( 79 | userProfile("john", "http://image.com/img2.png"), 80 | ), 81 | ) 82 | Fprint(os.Stdout, comp, context.TODO()) 83 | //Output: 84 | // 103 | } 104 | 105 | type MySelectBuilder struct { 106 | options [][]string 107 | selected string 108 | } 109 | 110 | func MySelect() *MySelectBuilder { 111 | return &MySelectBuilder{} 112 | } 113 | 114 | func (b *MySelectBuilder) Options(opts [][]string) (r *MySelectBuilder) { 115 | b.options = opts 116 | return b 117 | } 118 | 119 | func (b *MySelectBuilder) Selected(selected string) (r *MySelectBuilder) { 120 | b.selected = selected 121 | return b 122 | } 123 | 124 | func (b *MySelectBuilder) MarshalHTML(ctx context.Context) (r []byte, err error) { 125 | opts := []HTMLComponent{} 126 | for _, op := range b.options { 127 | var opt HTMLComponent 128 | if op[0] == b.selected { 129 | opt = Option(op[1]).Value(op[0]).Attr("selected", "true") 130 | } else { 131 | opt = Option(op[1]).Value(op[0]) 132 | } 133 | opts = append(opts, opt) 134 | } 135 | return Select(opts...).MarshalHTML(ctx) 136 | } 137 | 138 | /* 139 | More complicated customized component 140 | */ 141 | func ExampleTag_04newcomponentstyle() { 142 | 143 | /* 144 | Define MySelect as follows: 145 | 146 | type MySelectBuilder struct { 147 | options [][]string 148 | selected string 149 | } 150 | 151 | func MySelect() *MySelectBuilder { 152 | return &MySelectBuilder{} 153 | } 154 | 155 | func (b *MySelectBuilder) Options(opts [][]string) (r *MySelectBuilder) { 156 | b.options = opts 157 | return b 158 | } 159 | 160 | func (b *MySelectBuilder) Selected(selected string) (r *MySelectBuilder) { 161 | b.selected = selected 162 | return b 163 | } 164 | 165 | func (b *MySelectBuilder) MarshalHTML(ctx context.Context) (r []byte, err error) { 166 | opts := []HTMLComponent{} 167 | for _, op := range b.options { 168 | var opt HTMLComponent 169 | if op[0] == b.selected { 170 | opt = Option(op[1]).Value(op[0]).Attr("selected", "true") 171 | } else { 172 | opt = Option(op[1]).Value(op[0]) 173 | } 174 | opts = append(opts, opt) 175 | } 176 | return Select(opts...).MarshalHTML(ctx) 177 | } 178 | */ 179 | 180 | comp := MySelect().Options([][]string{ 181 | {"1", "label 1"}, 182 | {"2", "label 2"}, 183 | {"3", "label 3"}, 184 | }).Selected("2") 185 | 186 | Fprint(os.Stdout, comp, context.TODO()) 187 | //Output: 188 | // 195 | } 196 | 197 | /* 198 | Write a little bit of JavaScript and stylesheet 199 | */ 200 | func ExampleTag_05javascript() { 201 | 202 | comp := Div( 203 | Button("Hello").Id("hello"), 204 | Style(` 205 | .container { 206 | background-color: red; 207 | } 208 | `), 209 | 210 | Script(` 211 | var b = document.getElementById("hello") 212 | b.onclick = function(e){ 213 | alert("Hello"); 214 | } 215 | `), 216 | ).Class("container") 217 | 218 | Fprint(os.Stdout, comp, context.TODO()) 219 | //Output: 220 | //
221 | // 222 | // 223 | // 228 | // 229 | // 235 | //
236 | } 237 | 238 | /* 239 | An example about how to integrate into http.Handler, and how to do layout, and how to use context. 240 | */ 241 | func ExampleTag_06httphandler() { 242 | type User struct { 243 | Name string 244 | } 245 | 246 | userStatus := func() HTMLComponent { 247 | return ComponentFunc(func(ctx context.Context) (r []byte, err error) { 248 | 249 | if currentUser, ok := ctx.Value("currentUser").(*User); ok { 250 | return Div( 251 | Text(currentUser.Name), 252 | ).Class("username").MarshalHTML(ctx) 253 | } 254 | 255 | return Div(Text("Login")).Class("login").MarshalHTML(ctx) 256 | }) 257 | } 258 | 259 | myHeader := func() HTMLComponent { 260 | return Div( 261 | Text("header"), 262 | userStatus(), 263 | ).Class("header") 264 | } 265 | myFooter := func() HTMLComponent { 266 | return Div(Text("footer")).Class("footer") 267 | } 268 | 269 | layout := func(in HTMLComponent) (out HTMLComponent) { 270 | out = HTML( 271 | Head( 272 | Meta().Charset("utf8"), 273 | ), 274 | Body( 275 | myHeader(), 276 | in, 277 | myFooter(), 278 | ), 279 | ) 280 | return 281 | } 282 | 283 | getLoginUserFromCookie := func(r *http.Request) *User { 284 | return &User{Name: "felix"} 285 | } 286 | 287 | homeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 288 | user := getLoginUserFromCookie(r) 289 | ctx := context.WithValue(context.TODO(), "currentUser", user) 290 | 291 | root := Div( 292 | Text("This is my home page"), 293 | ) 294 | 295 | Fprint(w, layout(root), ctx) 296 | }) 297 | 298 | w := httptest.NewRecorder() 299 | r := httptest.NewRequest("GET", "/", nil) 300 | homeHandler.ServeHTTP(w, r) 301 | 302 | fmt.Println(w.Body.String()) 303 | 304 | //Output: 305 | // 306 | // 307 | // 308 | // 309 | // 310 | // 311 | // 312 | // 313 | //
header 314 | //
felix
315 | //
316 | // 317 | //
This is my home page
318 | // 319 | // 320 | // 321 | // 322 | } 323 | 324 | /* 325 | An example show how to set different type of attributes 326 | */ 327 | func ExampleTag_07MutipleTypeAttrs() { 328 | type MoreData struct { 329 | Name string 330 | Count int 331 | } 332 | comp := Div( 333 | Input("username"). 334 | Type("checkbox"). 335 | Attr("checked", true). 336 | Attr("more-data", &MoreData{Name: "felix", Count: 100}). 337 | Attr("max-length", 10), 338 | Input("username2"). 339 | Type("checkbox"). 340 | Attr("checked", false), 341 | ) 342 | Fprint(os.Stdout, comp, context.TODO()) 343 | //Output: 344 | //
345 | // 346 | // 347 | // 348 | //
349 | } 350 | 351 | /* 352 | An example show how to set styles 353 | */ 354 | func ExampleTag_08styles() { 355 | comp := Div(). 356 | StyleIf("background-color:red; border:1px solid red;", true). 357 | StyleIf("color:blue", true) 358 | Fprint(os.Stdout, comp, context.TODO()) 359 | //Output: 360 | //
361 | } 362 | 363 | /* 364 | An example to use If, `Iff` is for body to passed in as an func for the body depends on if condition not to be nil, `If` is for directly passed in HTMLComponent 365 | */ 366 | func ExampleTag_09iff() { 367 | type Person struct { 368 | Age int 369 | } 370 | var p *Person 371 | 372 | name := "Leon" 373 | comp := Div( 374 | Iff(p != nil && p.Age > 18, func() HTMLComponent { 375 | return Div().Text(name + ": Age > 18") 376 | }).ElseIf(p == nil, func() HTMLComponent { 377 | return Div().Text("No person named " + name) 378 | }).Else(func() HTMLComponent { 379 | return Div().Text(name + ":Age <= 18") 380 | }), 381 | ) 382 | Fprint(os.Stdout, comp, context.TODO()) 383 | //Output: 384 | //
385 | //
No person named Leon
386 | //
387 | } 388 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/theplant/htmlgo 2 | 3 | go 1.18 4 | 5 | require github.com/theplant/testingutils v0.0.0-20190603093022-26d8b4d95c61 6 | 7 | require github.com/pmezard/go-difflib v1.0.0 // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 2 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 3 | github.com/theplant/testingutils v0.0.0-20190603093022-26d8b4d95c61 h1:757/ruZNgTsOf5EkQBo0i3Bx/P2wgF5ljVkODeUX/uA= 4 | github.com/theplant/testingutils v0.0.0-20190603093022-26d8b4d95c61/go.mod h1:p22Q3Bg5ML+hdI3QSQkB/pZ2+CjfOnGugoQIoyE2Ub8= 5 | -------------------------------------------------------------------------------- /if.go: -------------------------------------------------------------------------------- 1 | package htmlgo 2 | 3 | import "context" 4 | 5 | type IfBuilder struct { 6 | comps []HTMLComponent 7 | set bool 8 | } 9 | 10 | func If(v bool, comps ...HTMLComponent) (r *IfBuilder) { 11 | r = &IfBuilder{} 12 | if v { 13 | r.comps = comps 14 | r.set = true 15 | } 16 | return 17 | } 18 | 19 | func (b *IfBuilder) ElseIf(v bool, comps ...HTMLComponent) (r *IfBuilder) { 20 | if b.set { 21 | return b 22 | } 23 | if v { 24 | b.comps = comps 25 | b.set = true 26 | } 27 | return b 28 | } 29 | 30 | func (b *IfBuilder) Else(comps ...HTMLComponent) (r *IfBuilder) { 31 | if b.set { 32 | return b 33 | } 34 | b.set = true 35 | b.comps = comps 36 | return b 37 | } 38 | 39 | func (b *IfBuilder) MarshalHTML(ctx context.Context) (r []byte, err error) { 40 | if len(b.comps) == 0 { 41 | return 42 | } 43 | return HTMLComponents(b.comps).MarshalHTML(ctx) 44 | } 45 | 46 | type IfFuncBuilder struct { 47 | f func() HTMLComponent 48 | set bool 49 | } 50 | 51 | func Iff(v bool, f func() HTMLComponent) (r *IfFuncBuilder) { 52 | r = &IfFuncBuilder{} 53 | if v { 54 | r.f = f 55 | r.set = true 56 | } 57 | return 58 | } 59 | 60 | func (b *IfFuncBuilder) ElseIf(v bool, f func() HTMLComponent) (r *IfFuncBuilder) { 61 | if b.set { 62 | return b 63 | } 64 | if v { 65 | b.f = f 66 | b.set = true 67 | } 68 | return b 69 | } 70 | 71 | func (b *IfFuncBuilder) Else(f func() HTMLComponent) (r *IfFuncBuilder) { 72 | if b.set { 73 | return b 74 | } 75 | b.set = true 76 | b.f = f 77 | return b 78 | } 79 | 80 | func (b *IfFuncBuilder) MarshalHTML(ctx context.Context) (r []byte, err error) { 81 | if b.f == nil { 82 | return 83 | } 84 | return b.f().MarshalHTML(ctx) 85 | } 86 | -------------------------------------------------------------------------------- /modd.conf: -------------------------------------------------------------------------------- 1 | **/*.go { 2 | prep: "godoc2readme . > ./README.md" 3 | prep: go test -v ./... 4 | } 5 | -------------------------------------------------------------------------------- /tag.go: -------------------------------------------------------------------------------- 1 | package htmlgo 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "html" 9 | "strings" 10 | "sync" 11 | ) 12 | 13 | type tagAttr struct { 14 | key string 15 | value interface{} 16 | } 17 | 18 | type HTMLTagBuilder struct { 19 | tag string 20 | omitEndTag bool 21 | attrs []*tagAttr 22 | styles []string 23 | classNames []string 24 | children []HTMLComponent 25 | } 26 | 27 | func Tag(tag string) (r *HTMLTagBuilder) { 28 | r = &HTMLTagBuilder{} 29 | 30 | if r.attrs == nil { 31 | r.attrs = []*tagAttr{} 32 | } 33 | 34 | r.Tag(tag) 35 | 36 | return 37 | } 38 | 39 | func (b *HTMLTagBuilder) Tag(v string) (r *HTMLTagBuilder) { 40 | b.tag = v 41 | return b 42 | } 43 | 44 | func (b *HTMLTagBuilder) OmitEndTag() (r *HTMLTagBuilder) { 45 | b.omitEndTag = true 46 | return b 47 | } 48 | 49 | func (b *HTMLTagBuilder) Text(v string) (r *HTMLTagBuilder) { 50 | b.Children(Text(v)) 51 | return b 52 | } 53 | 54 | func (b *HTMLTagBuilder) Children(comps ...HTMLComponent) (r *HTMLTagBuilder) { 55 | b.children = comps 56 | return b 57 | } 58 | 59 | func (b *HTMLTagBuilder) SetAttr(k string, v interface{}) { 60 | for _, at := range b.attrs { 61 | if at.key == k { 62 | at.value = v 63 | return 64 | } 65 | } 66 | b.attrs = append(b.attrs, &tagAttr{k, v}) 67 | } 68 | 69 | func (b *HTMLTagBuilder) Attr(vs ...interface{}) (r *HTMLTagBuilder) { 70 | if len(vs)%2 != 0 { 71 | vs = append(vs, "") 72 | } 73 | 74 | for i := 0; i < len(vs); i = i + 2 { 75 | if key, ok := vs[i].(string); ok { 76 | b.SetAttr(key, vs[i+1]) 77 | } else { 78 | panic(fmt.Sprintf("Attr key must be string, but was %#+v", vs[i])) 79 | } 80 | } 81 | return b 82 | } 83 | 84 | func (b *HTMLTagBuilder) AttrIf(key, value interface{}, add bool) (r *HTMLTagBuilder) { 85 | if !add { 86 | return b 87 | } 88 | 89 | return b.Attr(key, value) 90 | } 91 | 92 | func (b *HTMLTagBuilder) Class(names ...string) (r *HTMLTagBuilder) { 93 | b.addClass(names...) 94 | return b 95 | } 96 | 97 | func (b *HTMLTagBuilder) addClass(names ...string) (r *HTMLTagBuilder) { 98 | for _, n := range names { 99 | ins := strings.Split(n, " ") 100 | for _, in := range ins { 101 | tin := strings.TrimSpace(in) 102 | if len(tin) > 0 { 103 | b.classNames = append(b.classNames, tin) 104 | } 105 | } 106 | } 107 | return b 108 | } 109 | 110 | func (b *HTMLTagBuilder) ClassIf(name string, add bool) (r *HTMLTagBuilder) { 111 | if !add { 112 | return b 113 | } 114 | b.addClass(name) 115 | return b 116 | } 117 | 118 | func (b *HTMLTagBuilder) Data(vs ...string) (r *HTMLTagBuilder) { 119 | for i := 0; i < len(vs); i = i + 2 { 120 | b.Attr(fmt.Sprintf("data-%s", vs[i]), vs[i+1]) 121 | } 122 | return b 123 | } 124 | 125 | func (b *HTMLTagBuilder) Id(v string) (r *HTMLTagBuilder) { 126 | b.Attr("id", v) 127 | return b 128 | } 129 | 130 | func (b *HTMLTagBuilder) Href(v string) (r *HTMLTagBuilder) { 131 | b.Attr("href", v) 132 | return b 133 | } 134 | 135 | func (b *HTMLTagBuilder) Rel(v string) (r *HTMLTagBuilder) { 136 | b.Attr("rel", v) 137 | return b 138 | } 139 | 140 | func (b *HTMLTagBuilder) Title(v string) (r *HTMLTagBuilder) { 141 | b.Attr("title", html.EscapeString(v)) 142 | return b 143 | } 144 | 145 | func (b *HTMLTagBuilder) TabIndex(v int) (r *HTMLTagBuilder) { 146 | b.Attr("tabindex", v) 147 | return b 148 | } 149 | 150 | func (b *HTMLTagBuilder) Required(v bool) (r *HTMLTagBuilder) { 151 | b.Attr("required", v) 152 | return b 153 | } 154 | 155 | func (b *HTMLTagBuilder) Readonly(v bool) (r *HTMLTagBuilder) { 156 | b.Attr("readonly", v) 157 | return b 158 | } 159 | 160 | func (b *HTMLTagBuilder) Role(v string) (r *HTMLTagBuilder) { 161 | b.Attr("role", v) 162 | return b 163 | } 164 | 165 | func (b *HTMLTagBuilder) Alt(v string) (r *HTMLTagBuilder) { 166 | b.Attr("alt", v) 167 | return b 168 | } 169 | 170 | func (b *HTMLTagBuilder) Target(v string) (r *HTMLTagBuilder) { 171 | b.Attr("target", v) 172 | return b 173 | } 174 | 175 | func (b *HTMLTagBuilder) Name(v string) (r *HTMLTagBuilder) { 176 | b.Attr("name", v) 177 | return b 178 | } 179 | 180 | func (b *HTMLTagBuilder) Value(v string) (r *HTMLTagBuilder) { 181 | b.Attr("value", v) 182 | return b 183 | } 184 | 185 | func (b *HTMLTagBuilder) For(v string) (r *HTMLTagBuilder) { 186 | b.Attr("for", v) 187 | return b 188 | } 189 | 190 | func (b *HTMLTagBuilder) Style(v string) (r *HTMLTagBuilder) { 191 | b.addStyle(strings.Trim(v, ";")) 192 | return b 193 | } 194 | 195 | func (b *HTMLTagBuilder) StyleIf(v string, add bool) (r *HTMLTagBuilder) { 196 | if !add { 197 | return b 198 | } 199 | b.Style(v) 200 | return b 201 | } 202 | 203 | func (b *HTMLTagBuilder) addStyle(v string) (r *HTMLTagBuilder) { 204 | if len(v) > 0 { 205 | b.styles = append(b.styles, v) 206 | } 207 | 208 | return b 209 | } 210 | 211 | func (b *HTMLTagBuilder) Type(v string) (r *HTMLTagBuilder) { 212 | b.Attr("type", v) 213 | return b 214 | } 215 | 216 | func (b *HTMLTagBuilder) Placeholder(v string) (r *HTMLTagBuilder) { 217 | b.Attr("placeholder", v) 218 | return b 219 | } 220 | 221 | func (b *HTMLTagBuilder) Src(v string) (r *HTMLTagBuilder) { 222 | b.Attr("src", v) 223 | return b 224 | } 225 | 226 | func (b *HTMLTagBuilder) Property(v string) (r *HTMLTagBuilder) { 227 | b.Attr("property", v) 228 | return b 229 | } 230 | 231 | func (b *HTMLTagBuilder) Action(v string) (r *HTMLTagBuilder) { 232 | b.Attr("action", v) 233 | return b 234 | } 235 | 236 | func (b *HTMLTagBuilder) Method(v string) (r *HTMLTagBuilder) { 237 | b.Attr("method", v) 238 | return b 239 | } 240 | 241 | func (b *HTMLTagBuilder) Content(v string) (r *HTMLTagBuilder) { 242 | b.Attr("content", v) 243 | return b 244 | } 245 | 246 | func (b *HTMLTagBuilder) Charset(v string) (r *HTMLTagBuilder) { 247 | b.Attr("charset", v) 248 | return b 249 | } 250 | 251 | func (b *HTMLTagBuilder) Disabled(v bool) (r *HTMLTagBuilder) { 252 | b.Attr("disabled", v) 253 | return b 254 | } 255 | 256 | func (b *HTMLTagBuilder) Checked(v bool) (r *HTMLTagBuilder) { 257 | b.Attr("checked", v) 258 | return b 259 | } 260 | 261 | func (b *HTMLTagBuilder) AppendChildren(c ...HTMLComponent) (r *HTMLTagBuilder) { 262 | b.children = append(b.children, c...) 263 | return b 264 | } 265 | 266 | func (b *HTMLTagBuilder) PrependChildren(c ...HTMLComponent) (r *HTMLTagBuilder) { 267 | b.children = append(c, b.children...) 268 | return b 269 | } 270 | 271 | var bufPool = sync.Pool{ 272 | New: func() any { 273 | return &bytes.Buffer{} 274 | }, 275 | } 276 | 277 | func (b *HTMLTagBuilder) MarshalHTML(ctx context.Context) (r []byte, err error) { 278 | class := strings.TrimSpace(strings.Join(b.classNames, " ")) 279 | if len(class) > 0 { 280 | b.Attr("class", class) 281 | } 282 | 283 | styles := strings.TrimSpace(strings.Join(b.styles, "; ")) 284 | if len(styles) > 0 { 285 | b.Attr("style", styles+";") 286 | } 287 | 288 | // remove empty 289 | var cs []HTMLComponent 290 | for _, c := range b.children { 291 | if c == nil { 292 | continue 293 | } 294 | cs = append(cs, c) 295 | } 296 | 297 | var attrSegs []string 298 | for _, at := range b.attrs { 299 | var val string 300 | var isBool bool 301 | var boolVal bool 302 | switch v := at.value.(type) { 303 | case string: 304 | val = v 305 | case []byte: 306 | val = string(v) 307 | case []rune: 308 | val = string(v) 309 | case int, int8, int16, int32, int64, 310 | uint, uint8, uint16, uint32, uint64: 311 | val = fmt.Sprintf(`%d`, v) 312 | case float32, float64: 313 | val = fmt.Sprintf(`%f`, v) 314 | case bool: 315 | boolVal = v 316 | isBool = true 317 | default: 318 | val = JSONString(v) 319 | } 320 | 321 | if len(val) == 0 && !isBool { 322 | continue 323 | } 324 | 325 | if isBool && !boolVal { 326 | continue 327 | } 328 | 329 | seg := fmt.Sprintf(`%s='%s'`, escapeAttr(at.key), escapeAttr(val)) 330 | if isBool && boolVal { 331 | seg = escapeAttr(at.key) 332 | } 333 | attrSegs = append(attrSegs, seg) 334 | } 335 | 336 | attrStr := "" 337 | if len(attrSegs) > 0 { 338 | attrStr = " " + strings.Join(attrSegs, " ") 339 | } 340 | 341 | buf := bufPool.Get().(*bytes.Buffer) 342 | defer bufPool.Put(buf) 343 | buf.Reset() 344 | 345 | newline := "" 346 | 347 | if b.omitEndTag { 348 | newline = "\n" 349 | } 350 | buf.WriteString(fmt.Sprintf("\n<%s%s>%s", b.tag, attrStr, newline)) 351 | if !b.omitEndTag { 352 | if len(cs) > 0 { 353 | // buf.WriteString("\n") 354 | for _, c := range cs { 355 | var child []byte 356 | child, err = c.MarshalHTML(ctx) 357 | if err != nil { 358 | return 359 | } 360 | buf.Write(child) 361 | } 362 | } 363 | buf.WriteString(fmt.Sprintf("\n", b.tag)) 364 | } 365 | r = make([]byte, buf.Len()) 366 | copy(r, buf.Bytes()) 367 | return 368 | } 369 | 370 | func JSONString(v interface{}) (r string) { 371 | b, err := json.Marshal(v) 372 | if err != nil { 373 | panic(err) 374 | } 375 | r = string(b) 376 | return 377 | } 378 | 379 | func escapeAttr(str string) (r string) { 380 | r = strings.Replace(str, "'", "'", -1) 381 | //r = strings.Replace(r, "\n", "", -1) 382 | return 383 | } 384 | -------------------------------------------------------------------------------- /tag_test.go: -------------------------------------------------------------------------------- 1 | package htmlgo_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | . "github.com/theplant/htmlgo" 8 | "github.com/theplant/testingutils" 9 | ) 10 | 11 | var htmltagCases = []struct { 12 | name string 13 | tag *HTMLTagBuilder 14 | expected string 15 | }{ 16 | { 17 | name: "case 1", 18 | tag: Div( 19 | Div().Text("Hello"), 20 | ), 21 | expected: ` 22 |
23 |
Hello
24 |
25 | `, 26 | }, 27 | { 28 | name: "case 2", 29 | tag: Div( 30 | Div().Text("Hello"). 31 | Attr("class", "menu", 32 | "id", "the-menu", 33 | "style"). 34 | Attr("id", "menu-id"), 35 | ), 36 | expected: ` 37 |
38 | 39 |
40 | `, 41 | }, 42 | { 43 | name: "escape 1", 44 | tag: Div( 45 | Div().Text("Hello"). 46 | Attr("class", "menu", 47 | "id", "the><&\"'-menu", 48 | "style"), 49 | ), 50 | expected: ` 51 |
52 | 53 |
54 | `, 55 | }, 56 | } 57 | 58 | func TestHtmlTag(t *testing.T) { 59 | for _, c := range htmltagCases { 60 | r, err := c.tag.MarshalHTML(context.TODO()) 61 | if err != nil { 62 | panic(err) 63 | } 64 | diff := testingutils.PrettyJsonDiff(c.expected, string(r)) 65 | if len(diff) > 0 { 66 | t.Error(c.name, diff) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package htmlgo 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "html" 8 | "io" 9 | ) 10 | 11 | type RawHTML string 12 | 13 | func (s RawHTML) MarshalHTML(ctx context.Context) (r []byte, err error) { 14 | r = []byte(s) 15 | return 16 | } 17 | 18 | func Text(text string) (r HTMLComponent) { 19 | return RawHTML(html.EscapeString(text)) 20 | } 21 | 22 | func Textf(format string, a ...interface{}) (r HTMLComponent) { 23 | return Text(fmt.Sprintf(format, a...)) 24 | } 25 | 26 | type HTMLComponents []HTMLComponent 27 | 28 | func Components(comps ...HTMLComponent) HTMLComponents { 29 | return HTMLComponents(comps) 30 | } 31 | 32 | func (hcs HTMLComponents) MarshalHTML(ctx context.Context) (r []byte, err error) { 33 | buf := bytes.NewBuffer(nil) 34 | for _, h := range hcs { 35 | if h == nil { 36 | continue 37 | } 38 | var b []byte 39 | b, err = h.MarshalHTML(ctx) 40 | if err != nil { 41 | return 42 | } 43 | buf.Write(b) 44 | } 45 | r = buf.Bytes() 46 | return 47 | } 48 | 49 | func Fprint(w io.Writer, root HTMLComponent, ctx context.Context) (err error) { 50 | if root == nil { 51 | return 52 | } 53 | var b []byte 54 | b, err = root.MarshalHTML(ctx) 55 | if err != nil { 56 | return 57 | } 58 | _, err = fmt.Fprint(w, string(b)) 59 | return 60 | } 61 | 62 | func MustString(root HTMLComponent, ctx context.Context) string { 63 | b := bytes.NewBuffer(nil) 64 | err := Fprint(b, root, ctx) 65 | if err != nil { 66 | panic(err) 67 | } 68 | return b.String() 69 | } 70 | --------------------------------------------------------------------------------