33 |
37 |
38 | {{.Body}}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "github.com/avelino/awesome-go/pkg/markdown"
6 | "os"
7 | "regexp"
8 | "sort"
9 | "strings"
10 | "testing"
11 |
12 | "github.com/PuerkitoBio/goquery"
13 | )
14 |
15 | var (
16 | reContainsLink = regexp.MustCompile(`\* \[.*\]\(.*\)`)
17 | reOnlyLink = regexp.MustCompile(`\* \[.*\]\([^()]*\)$`)
18 | reLinkWithDescription = regexp.MustCompile(`\* \[.*\]\(.*\) - \S.*[\.\!]`)
19 | )
20 |
21 | func requireNoErr(t *testing.T, err error, msg string) {
22 | // FIXME: replace to github.com/stretchr/testify
23 | t.Helper()
24 |
25 | if msg == "" {
26 | msg = "unknown error"
27 | }
28 |
29 | if err != nil {
30 | t.Fatalf("Received unexpected error [%s]: %+v", msg, err)
31 | }
32 | }
33 |
34 | func goqueryFromReadme(t *testing.T) *goquery.Document {
35 | t.Helper()
36 |
37 | input, err := os.ReadFile(readmePath)
38 | requireNoErr(t, err, "readme file should be exists")
39 |
40 | html, err := markdown.ToHTML(input)
41 | requireNoErr(t, err, "markdown should be rendered to html")
42 |
43 | buf := bytes.NewBuffer(html)
44 | doc, err := goquery.NewDocumentFromReader(buf)
45 | requireNoErr(t, err, "html must be valid for goquery")
46 |
47 | return doc
48 | }
49 |
50 | func TestAlpha(t *testing.T) {
51 | doc := goqueryFromReadme(t)
52 | doc.Find("body > ul").Each(func(i int, s *goquery.Selection) {
53 | if i != 0 {
54 | // skip content menu
55 | // TODO: the sub items (with 3 hash marks `###`) are staying in
56 | // the main list, not respecting the hierarchy and making it
57 | // impossible to test the alphabetical order
58 | testList(t, s)
59 | }
60 | })
61 | }
62 |
63 | func TestDuplicatedLinks(t *testing.T) {
64 | doc := goqueryFromReadme(t)
65 | links := make(map[string]bool, 0)
66 | doc.Find("body li > a:first-child").Each(func(_ int, s *goquery.Selection) {
67 | t.Run(s.Text(), func(t *testing.T) {
68 | href, ok := s.Attr("href")
69 | if !ok {
70 | t.Error("expected to have href")
71 | }
72 | if links[href] {
73 | t.Fatalf("duplicated link '%s'", href)
74 | }
75 | links[href] = true
76 | })
77 | })
78 | }
79 |
80 | // Test if an entry has description, it must be separated from link with ` - `
81 | func TestSeparator(t *testing.T) {
82 | var matched, containsLink, noDescription bool
83 | input, err := os.ReadFile(readmePath)
84 | requireNoErr(t, err, "readme should be exists")
85 |
86 | lines := strings.Split(string(input), "\n")
87 | for _, line := range lines {
88 | line = strings.Trim(line, " ")
89 | containsLink = reContainsLink.MatchString(line)
90 | if containsLink {
91 | noDescription = reOnlyLink.MatchString(line)
92 | if noDescription {
93 | continue
94 | }
95 | matched = reLinkWithDescription.MatchString(line)
96 | if !matched {
97 | t.Errorf("expected entry to be in form of `* [link] - description.`, got '%s'", line)
98 | }
99 | }
100 | }
101 | }
102 |
103 | func TestRenderIndex(t *testing.T) {
104 | requireNoErr(t, mkdirAll(outDir), "output dir should exists")
105 |
106 | err := renderIndex(readmePath, outIndexFile)
107 | requireNoErr(t, err, "html should be rendered")
108 | }
109 |
110 | func testList(t *testing.T, list *goquery.Selection) {
111 | list.Find("ul").Each(func(_ int, items *goquery.Selection) {
112 | testList(t, items)
113 | items.RemoveFiltered("ul")
114 | })
115 | t.Run(list.Prev().Text(), func(t *testing.T) {
116 | checkAlphabeticOrder(t, list)
117 | })
118 | }
119 |
120 | func checkAlphabeticOrder(t *testing.T, s *goquery.Selection) {
121 | items := s.Find("li > a:first-child").Map(func(_ int, li *goquery.Selection) string {
122 | return strings.ToLower(li.Text())
123 | })
124 | sorted := make([]string, len(items))
125 | copy(sorted, items)
126 | sort.Strings(sorted)
127 | for k, item := range items {
128 | if item != sorted[k] {
129 | t.Errorf("expected '%s' but actual is '%s'", sorted[k], item)
130 | }
131 | }
132 | if t.Failed() {
133 | t.Logf("expected order is:\n%s", strings.Join(sorted, "\n"))
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/tmpl/assets/fonts/firasans.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Fira Sans';
3 | src: local('Fira Sans ExtraLight'),
4 | local('FiraSans-ExtraLight'),
5 | url('/assets/fonts/firasansextralight.woff2') format('woff2'),
6 | url('/assets/fonts/firasansextralight.woff') format('woff'),
7 | url('/assets/fonts/firasansextralight.ttf') format('truetype');
8 | font-weight: 100;
9 | font-style: normal;
10 | }
11 | @font-face {
12 | font-family: 'Fira Sans';
13 | src: local('Fira Sans ExtraLight Italic'),
14 | local('FiraSans-ExtraLightItalic'),
15 | url('/assets/fonts/firasansextralightitalic.woff2') format('woff2'),
16 | url('/assets/fonts/firasansextralightitalic.woff') format('woff'),
17 | url('/assets/fonts/firasansextralightitalic.ttf') format('truetype');
18 | font-weight: 100;
19 | font-style: italic;
20 | }
21 | @font-face {
22 | font-family: 'Fira Sans';
23 | src: local('Fira Sans Light'),
24 | local('FiraSans-Light'),
25 | url('/assets/fonts/firasanslight.woff2') format('woff2'),
26 | url('/assets/fonts/firasanslight.woff') format('woff'),
27 | url('/assets/fonts/firasanslight.ttf') format('truetype');
28 | font-weight: 200;
29 | font-style: normal;
30 | }
31 | @font-face {
32 | font-family: 'Fira Sans';
33 | src: local('Fira Sans Light Italic'),
34 | local('FiraSans-LightItalic'),
35 | url('/assets/fonts/firasanslightitalic.woff2') format('woff2'),
36 | url('/assets/fonts/firasanslightitalic.woff') format('woff'),
37 | url('/assets/fonts/firasanslightitalic.ttf') format('truetype');
38 | font-weight: 200;
39 | font-style: italic;
40 | }
41 | @font-face {
42 | font-family: 'Fira Sans';
43 | src: local('Fira Sans Book'),
44 | local('FiraSans-Book'),
45 | url('/assets/fonts/firasansbook.woff2') format('woff2'),
46 | url('/assets/fonts/firasansbook.woff') format('woff'),
47 | url('/assets/fonts/firasansbook.ttf') format('truetype');
48 | font-weight: 300;
49 | font-style: normal;
50 | }
51 | @font-face {
52 | font-family: 'Fira Sans';
53 | src: local('Fira Sans Book Italic'),
54 | local('FiraSans-BookItalic'),
55 | url('/assets/fonts/firasansbookitalic.woff2') format('woff2'),
56 | url('/assets/fonts/firasansbookitalic.woff') format('woff'),
57 | url('/assets/fonts/firasansbookitalic.ttf') format('truetype');
58 | font-weight: 300;
59 | font-style: italic;
60 | }
61 | @font-face {
62 | font-family: 'Fira Sans';
63 | src: local('Fira Sans'),
64 | local('FiraSans-Regular'),
65 | url('/assets/fonts/firasans.woff2') format('woff2'),
66 | url('/assets/fonts/firasans.woff') format('woff'),
67 | url('/assets/fonts/firasans.ttf') format('truetype');
68 | font-weight: 400;
69 | font-style: normal;
70 | }
71 | @font-face {
72 | font-family: 'Fira Sans';
73 | src: local('Fira Sans Italic'),
74 | local('FiraSans-Italic'),
75 | url('/assets/fonts/firasansitalic.woff2') format('woff2'),
76 | url('/assets/fonts/firasansitalic.woff') format('woff'),
77 | url('/assets/fonts/firasansitalic.ttf') format('truetype');
78 | font-weight: 400;
79 | font-style: italic;
80 | }
81 | @font-face {
82 | font-family: 'Fira Sans';
83 | src: local('Fira Sans Medium'),
84 | local('FiraSans-Medium'),
85 | url('/assets/fonts/firasansmedium.woff2') format('woff2'),
86 | url('/assets/fonts/firasansmedium.woff') format('woff'),
87 | url('/assets/fonts/firasansmedium.ttf') format('truetype');
88 | font-weight: 500;
89 | font-style: normal;
90 | }
91 | @font-face {
92 | font-family: 'Fira Sans';
93 | src: local('Fira Sans Medium Italic'),
94 | local('FiraSans-MediumItalic'),
95 | url('/assets/fonts/firasansmediumitalic.woff2') format('woff2'),
96 | url('/assets/fonts/firasansmediumitalic.woff') format('woff'),
97 | url('/assets/fonts/firasansmediumitalic.ttf') format('truetype');
98 | font-weight: 500;
99 | font-style: italic;
100 | }
101 | @font-face {
102 | font-family: 'Fira Sans';
103 | src: local('Fira Sans SemiBold'),
104 | local('FiraSans-SemiBold'),
105 | url('/assets/fonts/firasanssemibold.woff2') format('woff2'),
106 | url('/assets/fonts/firasanssemibold.woff') format('woff'),
107 | url('/assets/fonts/firasanssemibold.ttf') format('truetype');
108 | font-weight: 600;
109 | font-style: normal;
110 | }
111 | @font-face {
112 | font-family: 'Fira Sans';
113 | src: local('Fira Sans SemiBold Italic'),
114 | local('FiraSans-SemiBoldItalic'),
115 | url('/assets/fonts/firasanssemibolditalic.woff2') format('woff2'),
116 | url('/assets/fonts/firasanssemibolditalic.woff') format('woff'),
117 | url('/assets/fonts/firasanssemibolditalic.ttf') format('truetype');
118 | font-weight: 600;
119 | font-style: italic;
120 | }
121 | @font-face {
122 | font-family: 'Fira Sans';
123 | src: local('Fira Sans Bold'),
124 | local('FiraSans-Bold'),
125 | url('/assets/fonts/firasansbold.woff2') format('woff2'),
126 | url('/assets/fonts/firasansbold.woff') format('woff'),
127 | url('/assets/fonts/firasansbold.ttf') format('truetype');
128 | font-weight: 700;
129 | font-style: normal;
130 | }
131 | @font-face {
132 | font-family: 'Fira Sans';
133 | src: local('Fira Sans Bold Italic'),
134 | local('FiraSans-BoldItalic'),
135 | url('/assets/fonts/firasansbolditalic.woff2') format('woff2'),
136 | url('/assets/fonts/firasansbolditalic.woff') format('woff'),
137 | url('/assets/fonts/firasansbolditalic.ttf') format('truetype');
138 | font-weight: 700;
139 | font-style: italic;
140 | }
141 |
--------------------------------------------------------------------------------
/tmpl/category-index.tmpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |