├── 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 | //
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 | //
84 | //
85 | //
86 | //
felix<h1>
87 | //
88 | //
89 | //
complicated svg
90 | //
91 | //
92 | //
93 | //
94 | //
95 | //
john
96 | //
97 | //
98 | //
complicated svg
99 | //
100 | //
101 | //
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 | //
152 | // label 1
153 | //
154 | // label 2
155 | //
156 | // label 3
157 | //
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 | // Hello
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 | //
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 | //
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 | //
85 | //
86 | //
87 | //
felix<h1>
88 | //
89 | //
90 | //
complicated svg
91 | //
92 | //
93 | //
94 | //
95 | //
96 | //
john
97 | //
98 | //
99 | //
complicated svg
100 | //
101 | //
102 | //
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 | //
189 | // label 1
190 | //
191 | // label 2
192 | //
193 | // label 3
194 | //
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 | // Hello
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 | //
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("%s>\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 |
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 |
--------------------------------------------------------------------------------