diff options
context:
space:
mode:
-rw-r--r--assets/cgit.css127
-rw-r--r--source/ui-shared.c150
-rwxr-xr-xtests/t0010-validate-html.sh11
-rwxr-xr-xtests/t0102-summary.sh4
4 files changed, 189 insertions, 103 deletions
diff --git a/assets/cgit.css b/assets/cgit.css
index d10a24d..e02d551 100644
--- a/assets/cgit.css
+++ b/assets/cgit.css
@@ -21,99 +21,142 @@ div#cgit table {
border-collapse: collapse;
}
-div#cgit table#header {
+div#cgit #header {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ grid-template-areas:
+ "logo heading switch"
+ "logo desc owner";
+ align-items: center;
+ column-gap: 10px;
width: 100%;
margin-bottom: 1em;
}
-div#cgit table#header td.logo {
- width: 96px;
- vertical-align: top;
+div#cgit #header .logo {
+ grid-area: logo;
+ align-self: start;
+}
+
+div#cgit #header .logo img {
+ display: block;
+ max-width: 96px;
}
-div#cgit table#header td.main {
+div#cgit #header h1.main {
+ grid-area: heading;
+ margin: 0;
font-size: 250%;
- padding-left: 10px;
+ font-weight: normal;
white-space: nowrap;
}
-div#cgit table#header td.main a {
+div#cgit #header h1.main a {
color: #000;
}
-div#cgit table#header td.form {
- text-align: right;
- vertical-align: bottom;
- padding-right: 1em;
- padding-bottom: 2px;
- white-space: nowrap;
-}
-
-div#cgit table#header td.form form,
-div#cgit table#header td.form input,
-div#cgit table#header td.form select {
+div#cgit #header .branch-switch {
+ grid-area: switch;
+ justify-self: end;
+ align-self: end;
font-size: 90%;
}
-div#cgit table#header td.sub {
+div#cgit #header .sub {
+ grid-area: desc;
+ margin: 0;
color: #777;
border-top: solid 1px #ccc;
- padding-left: 10px;
}
-div#cgit table.tabs {
+div#cgit #header .owner {
+ grid-area: owner;
+ justify-self: end;
+ text-align: right;
+}
+
+@media (max-width: 640px) {
+ div#cgit #header {
+ grid-template-columns: auto 1fr;
+ grid-template-areas:
+ "logo heading"
+ "desc desc"
+ "owner owner"
+ "switch switch";
+ row-gap: 4px;
+ }
+ div#cgit #header .branch-switch,
+ div#cgit #header .owner {
+ justify-self: start;
+ text-align: left;
+ }
+ div#cgit #header h1.main {
+ white-space: normal;
+ }
+}
+
+div#cgit .tabs {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ column-gap: 0.5em;
border-bottom: solid 3px #ccc;
- border-collapse: collapse;
margin-top: 2em;
- margin-bottom: 0px;
- width: 100%;
}
-div#cgit table.tabs td {
- padding: 0px 1em;
- vertical-align: bottom;
+div#cgit .tabs ul {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ flex: 1 1 auto;
+ list-style: none;
+ margin: 0;
+ padding: 0;
}
-div#cgit table.tabs td a {
+div#cgit .tabs li {
+ margin: 0;
+ padding: 0 1em;
+}
+
+div#cgit .tabs a {
+ display: inline-block;
padding: 2px 0.75em;
color: #777;
font-size: 110%;
}
-div#cgit table.tabs td a.active {
+div#cgit .tabs a.active {
color: #000;
background-color: #ccc;
}
-div#cgit table.tabs a[href^="http://"]:after, div#cgit table.tabs a[href^="https://"]:after {
+div#cgit .tabs a.external:after {
content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfgAhcJDQY+gm2TAAAAHWlUWHRDb21tZW50AAAAAABDcmVhdGVkIHdpdGggR0lNUGQuZQcAAABbSURBVAhbY2BABs4MU4CwhYHBh2Erww4wrGFQZHjI8B8IgUIscJWyDHcggltQhI4zGDCcRwhChPggHIggP1QoAVmQkSETrGoHsiAEsACtBYN0oDAMbgU6EBcAAL2eHUt4XUU4AAAAAElFTkSuQmCC);
opacity: 0.5;
margin: 0 0 0 5px;
}
-div#cgit table.tabs td.form {
- text-align: right;
-}
-
-div#cgit table.tabs td.form form {
+div#cgit .tabs .search {
padding-bottom: 2px;
font-size: 90%;
white-space: nowrap;
}
-div#cgit table.tabs td.form input,
-div#cgit table.tabs td.form select {
+div#cgit .tabs .search input,
+div#cgit .tabs .search select {
font-size: 90%;
}
-div#cgit div.path {
+div#cgit .path {
margin: 0px;
padding: 5px 2em 2px 2em;
color: #000;
background-color: #eee;
+ overflow-wrap: anywhere;
}
-div#cgit div.content {
+div#cgit .content {
margin: 0px;
padding: 2em;
border-bottom: solid 3px #ccc;
@@ -662,19 +705,19 @@ div#cgit span.deletions {
color: #800;
}
-div#cgit div.footer {
+div#cgit .footer {
margin-top: 0.5em;
text-align: center;
font-size: 80%;
color: #ccc;
}
-div#cgit div.footer a {
+div#cgit .footer a {
color: #ccc;
text-decoration: none;
}
-div#cgit div.footer a:hover {
+div#cgit .footer a:hover {
text-decoration: underline;
}
diff --git a/source/ui-shared.c b/source/ui-shared.c
index eb68ab7..69d2cdf 100644
--- a/source/ui-shared.c
+++ b/source/ui-shared.c
@@ -867,7 +867,7 @@ void cgit_print_docstart(void)
void cgit_print_docend(void)
{
- html("</div> <!-- class=content -->\n");
+ html("</main>\n");
if (ctx.cfg.embedded) {
html("</div> <!-- id=cgit -->\n");
if (ctx.cfg.footer)
@@ -877,10 +877,10 @@ void cgit_print_docend(void)
if (ctx.cfg.footer)
html_include(ctx.cfg.footer);
else {
- htmlf("<div class='footer'>generated by <a href='https://git.zx2c4.com/cgit/about/'>cgit %s</a> "
+ htmlf("<footer class='footer'>generated by <a href='https://git.zx2c4.com/cgit/about/'>cgit %s</a> "
"(<a href='https://git-scm.com/'>git %s</a>) at ", cgit_version, git_version_string);
html_txt(show_date(time(NULL), 0, cgit_date_mode(DATE_ISO8601)));
- html("</div>\n");
+ html("</footer>\n");
}
html("</div> <!-- id=cgit -->\n");
html("</body>\n</html>\n");
@@ -1015,9 +1015,6 @@ static void print_header(void)
{
char *logo = NULL, *logo_link = NULL;
- html("<table id='header'>\n");
- html("<tr>\n");
-
if (ctx.repo && ctx.repo->logo && *ctx.repo->logo)
logo = ctx.repo->logo;
else
@@ -1026,44 +1023,47 @@ static void print_header(void)
logo_link = ctx.repo->logo_link;
else
logo_link = ctx.cfg.logo_link;
+
+ html("<header id='header'>\n");
if (logo && *logo) {
- html("<td class='logo' rowspan='2'><a href='");
+ html("<a class='logo' href='");
if (logo_link && *logo_link)
html_attr(logo_link);
else
html_attr(cgit_rooturl());
html("'><img src='");
html_attr(logo);
- html("' alt='cgit logo'/></a></td>\n");
+ html("' alt='cgit logo'/></a>\n");
}
- html("<td class='main'>");
+ html("<h1 class='main'>");
if (ctx.repo) {
cgit_index_link("index", NULL, NULL, NULL, NULL, 0, 1);
html(" : ");
cgit_summary_link(ctx.repo->name, NULL, NULL, NULL);
- if (ctx.env.authenticated) {
- html("</td><td class='form'>");
- html("<form method='get'>\n");
- cgit_add_hidden_formfields(0, 1, ctx.qry.page);
- html("<select name='h' onchange='this.form.submit();'>\n");
- refs_for_each_branch_ref(get_main_ref_store(the_repository),
- print_branch_option, ctx.qry.head);
- if (ctx.repo->enable_remote_branches)
- refs_for_each_remote_ref(get_main_ref_store(the_repository),
- print_branch_option, ctx.qry.head);
- html("</select> ");
- html("<input type='submit' value='switch'/>");
- html("</form>");
- }
} else
html_txt(ctx.cfg.root_title);
- html("</td></tr>\n");
+ html("</h1>\n");
+
+ if (ctx.repo && ctx.env.authenticated) {
+ html("<form class='branch-switch' method='get'>\n");
+ cgit_add_hidden_formfields(0, 1, ctx.qry.page);
+ html("<select name='h' aria-label='Branch' onchange='this.form.submit();'>\n");
+ refs_for_each_branch_ref(get_main_ref_store(the_repository),
+ print_branch_option, ctx.qry.head);
+ if (ctx.repo->enable_remote_branches)
+ refs_for_each_remote_ref(get_main_ref_store(the_repository),
+ print_branch_option, ctx.qry.head);
+ html("</select> ");
+ html("<input type='submit' value='switch'/>");
+ html("</form>\n");
+ }
- html("<tr><td class='sub'>");
if (ctx.repo) {
+ html("<p class='sub desc'>");
html_txt(ctx.repo->desc);
- html("</td><td class='sub right'>");
+ html("</p>\n");
+ html("<p class='sub owner'>");
if (ctx.repo->owner_filter) {
cgit_open_filter(ctx.repo->owner_filter);
html_txt(ctx.repo->owner);
@@ -1071,11 +1071,13 @@ static void print_header(void)
} else {
html_txt(ctx.repo->owner);
}
- } else {
- if (ctx.cfg.root_desc)
- html_txt(ctx.cfg.root_desc);
+ html("</p>\n");
+ } else if (ctx.cfg.root_desc) {
+ html("<p class='sub desc'>");
+ html_txt(ctx.cfg.root_desc);
+ html("</p>\n");
}
- html("</td></tr></table>\n");
+ html("</header>\n");
}
void cgit_print_pageheader(void)
@@ -1084,39 +1086,63 @@ void cgit_print_pageheader(void)
if (!ctx.env.authenticated || !ctx.cfg.noheader)
print_header();
- html("<table class='tabs'><tr><td>\n");
+ if (ctx.env.authenticated)
+ html("<nav class='tabs'>\n");
if (ctx.env.authenticated && ctx.repo) {
- if (ctx.repo->readme.nr)
- reporevlink("about", "about", NULL,
+ const char *vp = ctx.qry.vpath;
+ html("<ul>\n");
+ if (ctx.repo->readme.nr) {
+ html("<li>");
+ reporevlink("about", "about", "About this repository",
hc("about"), ctx.qry.head, NULL,
NULL);
- cgit_summary_link("summary", NULL, hc("summary"),
+ html("</li>\n");
+ }
+ html("<li>");
+ cgit_summary_link("summary", "Repository summary", hc("summary"),
ctx.qry.head);
- cgit_refs_link("refs", NULL, hc("refs"), ctx.qry.head,
+ html("</li>\n<li>");
+ cgit_refs_link("refs", "Branches and tags", hc("refs"), ctx.qry.head,
ctx.qry.oid, NULL);
- cgit_log_link("log", NULL, hc("log"), ctx.qry.head,
+ html("</li>\n<li>");
+ cgit_log_link("log",
+ vp && *vp ? fmt("Commit history of %s", vp) : "Commit history",
+ hc("log"), ctx.qry.head,
NULL, ctx.qry.vpath, 0, NULL, NULL,
ctx.qry.showmsg, ctx.qry.follow);
+ html("</li>\n<li>");
if (ctx.qry.page && !strcmp(ctx.qry.page, "blame"))
- cgit_blame_link("blame", NULL, hc("blame"), ctx.qry.head,
+ cgit_blame_link("blame",
+ vp && *vp ? fmt("Line-by-line blame of %s", vp) : "Blame",
+ hc("blame"), ctx.qry.head,
ctx.qry.oid, ctx.qry.vpath);
else
- cgit_tree_link("tree", NULL, hc("tree"), ctx.qry.head,
+ cgit_tree_link("tree",
+ vp && *vp ? fmt("Browse the tree at %s", vp) : "Browse the file tree",
+ hc("tree"), ctx.qry.head,
ctx.qry.oid, ctx.qry.vpath);
- cgit_commit_link("commit", NULL, hc("commit"),
+ html("</li>\n<li>");
+ cgit_commit_link("commit", "The current commit", hc("commit"),
ctx.qry.head, ctx.qry.oid, ctx.qry.vpath);
- cgit_diff_link("diff", NULL, hc("diff"), ctx.qry.head,
+ html("</li>\n<li>");
+ cgit_diff_link("diff",
+ vp && *vp ? fmt("Changes to %s", vp) : "Changes in the current commit",
+ hc("diff"), ctx.qry.head,
ctx.qry.oid, ctx.qry.oid2, ctx.qry.vpath);
- if (ctx.repo->max_stats)
- cgit_stats_link("stats", NULL, hc("stats"),
+ html("</li>\n");
+ if (ctx.repo->max_stats) {
+ html("<li>");
+ cgit_stats_link("stats", "Commit activity", hc("stats"),
ctx.qry.head, ctx.qry.vpath);
+ html("</li>\n");
+ }
if (ctx.repo->homepage) {
- html("<a href='");
+ html("<li><a class='external' href='");
html_attr(ctx.repo->homepage);
- html("'>homepage</a>");
+ html("'>homepage</a></li>\n");
}
- html("</td><td class='form'>");
- html("<form class='right' method='get' action='");
+ html("</ul>\n");
+ html("<form class='search' method='get' action='");
if (ctx.cfg.virtual_root) {
char *fileurl = cgit_fileurl(ctx.qry.repo, "log",
ctx.qry.vpath, NULL);
@@ -1125,37 +1151,43 @@ void cgit_print_pageheader(void)
}
html("'>\n");
cgit_add_hidden_formfields(1, 0, "log");
- html("<select name='qt'>\n");
+ html("<select name='qt' aria-label='Search type'>\n");
html_option("grep", "log msg", ctx.qry.grep);
html_option("author", "author", ctx.qry.grep);
html_option("committer", "committer", ctx.qry.grep);
html_option("range", "range", ctx.qry.grep);
html("</select>\n");
- html("<input class='txt' type='search' size='10' name='q' value='");
+ html("<input class='txt' type='search' name='q' aria-label='Search' value='");
html_attr(ctx.qry.search);
html("'/>\n");
html("<input type='submit' value='search'/>\n");
html("</form>\n");
} else if (ctx.env.authenticated) {
char *currenturl = cgit_currenturl();
- site_link(NULL, "index", NULL, hc("repolist"), NULL, NULL, 0, 1);
- if (ctx.cfg.root_readme)
- site_link("about", "about", NULL, hc("about"),
+ html("<ul>\n<li>");
+ site_link(NULL, "index", "Repository index", hc("repolist"), NULL, NULL, 0, 1);
+ html("</li>\n");
+ if (ctx.cfg.root_readme) {
+ html("<li>");
+ site_link("about", "about", "About this site", hc("about"),
NULL, NULL, 0, 1);
- html("</td><td class='form'>");
- html("<form method='get' action='");
+ html("</li>\n");
+ }
+ html("</ul>\n");
+ html("<form class='search' method='get' action='");
html_attr(currenturl);
html("'>\n");
- html("<input type='search' name='q' size='10' value='");
+ html("<input class='txt' type='search' name='q' aria-label='Search' value='");
html_attr(ctx.qry.search);
html("'/>\n");
html("<input type='submit' value='search'/>\n");
- html("</form>");
+ html("</form>\n");
free(currenturl);
}
- html("</td></tr></table>\n");
+ if (ctx.env.authenticated)
+ html("</nav>\n");
if (ctx.env.authenticated && ctx.repo && ctx.qry.vpath) {
- html("<div class='path'>");
+ html("<nav class='path' aria-label='breadcrumb'>");
html("path: ");
cgit_print_path_crumbs(ctx.qry.vpath);
if (ctx.repo->enable_follow_links && !strcmp(ctx.qry.page, "log")) {
@@ -1166,9 +1198,9 @@ void cgit_print_pageheader(void)
ctx.qry.follow = !ctx.qry.follow;
html(")");
}
- html("</div>");
+ html("</nav>");
}
- html("<div class='content'>");
+ html("<main class='content'>\n");
}
void cgit_print_filemode(unsigned short mode)
diff --git a/tests/t0010-validate-html.sh b/tests/t0010-validate-html.sh
index ca08d69..91433f8 100755
--- a/tests/t0010-validate-html.sh
+++ b/tests/t0010-validate-html.sh
@@ -29,6 +29,17 @@ test -n "$tidy" || {
exit
}
+# Releases of tidy that predate HTML5 reject the semantic elements (header,
+# nav, main, footer) that cgit emits, reporting them as errors. Skip the
+# validation unless tidy is new enough to understand them.
+printf '<!DOCTYPE html>\n<html lang="en"><head><title>t</title></head><body><header>x</header></body></html>\n' \
+ | "$tidy" -q -e >/dev/null 2>&1
+test $? -ge 2 && {
+ skip_all='Skipping html validation tests: tidy is too old for HTML5'
+ test_done
+ exit
+}
+
test_expect_success 'index page' 'test_url ""'
test_expect_success 'foo' 'test_url "foo"'
test_expect_success 'foo/log' 'test_url "foo/log"'
diff --git a/tests/t0102-summary.sh b/tests/t0102-summary.sh
index b8864cb..f1c1e78 100755
--- a/tests/t0102-summary.sh
+++ b/tests/t0102-summary.sh
@@ -7,7 +7,7 @@ test_expect_success 'generate foo summary' 'cgit_url "foo" >tmp'
test_expect_success 'find commit 1' 'grep "commit 1" tmp'
test_expect_success 'find commit 5' 'grep "commit 5" tmp'
test_expect_success 'find branch master' 'grep "master" tmp'
-test_expect_success 'no tags' '! grep "tags" tmp'
+test_expect_success 'no tags' '! grep "Tag</th>" tmp'
test_expect_success 'clone-url expanded correctly' '
grep "git://example.org/foo.git" tmp
'
@@ -17,7 +17,7 @@ test_expect_success 'no commit 45' '! grep "commit 45" tmp'
test_expect_success 'find commit 46' 'grep "commit 46" tmp'
test_expect_success 'find commit 50' 'grep "commit 50" tmp'
test_expect_success 'find branch master' 'grep "master" tmp'
-test_expect_success 'no tags' '! grep "tags" tmp'
+test_expect_success 'no tags' '! grep "Tag</th>" tmp'
test_expect_success 'clone-url expanded correctly' '
grep "git://example.org/bar.git" tmp
'