commit 163796828c599ae93fd57574fd2b7559920041f4
parent 53804cf99c498fff23b1da692a6eec221e899df7
Author: Jack Sangdahl <jack@pngu.org>
Date: Fri, 10 Jul 2026 09:22:34 -0600
Add syntax highlighting
Diffstat:
| A | .gitignore | | | 4 | ++++ |
| A | highlight | | | 50 | ++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | requirements.txt | | | 3 | +++ |
| M | stagit.c | | | 65 | +++++++++++++++++++++++++++++++++++++++-------------------------- |
| M | style.css | | | 159 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------- |
5 files changed, 205 insertions(+), 76 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,4 @@
+.venv
+*.o
+stagit
+stagit-index
diff --git a/highlight b/highlight
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+
+from sys import stdin
+
+import pygments
+from markdown import markdown
+from pygments import highlight
+from pygments.formatters import HtmlFormatter
+from pygments.lexers import TextLexer, guess_lexer, guess_lexer_for_filename
+
+filename = stdin.readline().strip()
+contents = stdin.read()
+lexer = None
+rendered = None
+
+try:
+ lexer = guess_lexer_for_filename(filename, contents)
+except pygments.util.ClassNotFound:
+ try:
+ lexer = guess_lexer(contents)
+ except pygments.util.ClassNotFound:
+ pass
+
+if lexer is None:
+ lexer = TextLexer()
+
+if lexer.__class__ is pygments.lexers.MarkdownLexer:
+ rendered = markdown(
+ contents,
+ extensions=["codehilite", "extra", "sane_lists", "smarty", "pymdownx.tasklist"],
+ )
+
+format = HtmlFormatter(
+ style="murphy",
+ cssclass="highlight",
+ linenos="table",
+ lineanchors="loc",
+ anchorlinenos=True,
+)
+
+if rendered:
+ print('<article class="markup markdown">')
+ print(rendered)
+ print("</article>")
+print('<div id="blob">')
+print(highlight(contents, lexer, format))
+print("</div>")
+print("<style>")
+print(format.get_style_defs(""))
+print("</style>")
diff --git a/requirements.txt b/requirements.txt
@@ -0,0 +1,3 @@
+markdown
+pygments
+pymdown-extensions
diff --git a/stagit.c b/stagit.c
@@ -69,7 +69,7 @@ static char cloneurl[1024];
static char *submodules;
static char *licensefiles[] = { "HEAD:LICENSE", "HEAD:LICENSE.md", "HEAD:LICENSE.txt", "HEAD:COPYING" };
static char *license;
-static char *readmefiles[] = { "HEAD:README", "HEAD:README.md" };
+static char *readmefiles[] = { "HEAD:readme.md" };
static char *readme;
static long long nlogcommits = -1; /* -1 indicates not used */
@@ -556,37 +556,50 @@ writefooter(FILE *fp)
fputs("</div>\n</body>\n</html>\n", fp);
}
+
size_t
-writeblobhtml(FILE *fp, const git_blob *blob)
+syntax_highlight(const char *filename, FILE *fp, const char *s, size_t len)
{
- size_t n = 0, i, len, prev;
- const char *nfmt = "<a href=\"#l%zu\" class=\"line\" id=\"l%zu\">%7zu</a> ";
- const char *s = git_blob_rawcontent(blob);
+ fflush(fp);
+ int stdout_copy = dup(1);
+ dup2(fileno(fp), 1);
+
+ FILE *child = popen("./highlight", "w");
+ if (child == NULL) {
+ fprintf(stderr, "child is null: %s\n", strerror(errno));
+ exit(1);
+ }
- len = git_blob_rawsize(blob);
- fputs("<pre id=\"blob\">\n", fp);
+ // Pass filename first so Python knows the extension
+ fprintf(child, "%s\n", filename);
- if (len > 0) {
- for (i = 0, prev = 0; i < len; i++) {
- if (s[i] != '\n')
- continue;
- n++;
- fprintf(fp, nfmt, n, n, n);
- xmlencodeline(fp, &s[prev], i - prev + 1);
- putc('\n', fp);
- prev = i + 1;
- }
- /* trailing data */
- if ((len - prev) > 0) {
- n++;
- fprintf(fp, nfmt, n, n, n);
- xmlencodeline(fp, &s[prev], len - prev);
- }
+ // Feed the raw code block to Python
+ size_t lc = 0;
+ for (size_t i = 0; *s && i < len; s++, i++) {
+ if (*s == '\n') lc++;
+ fprintf(child, "%c", *s);
}
- fputs("</pre>\n", fp);
+ pclose(child);
+ fflush(stdout);
+ dup2(stdout_copy, 1);
+ close(stdout_copy);
+
+ return lc;
+}
+
+size_t
+writeblobhtml(const char *filename, FILE *fp, const git_blob *blob)
+{
+ size_t lc = 0;
+ const char *s = git_blob_rawcontent(blob);
+ git_off_t len = git_blob_rawsize(blob);
+
+ if (len > 0) {
+ lc = syntax_highlight(filename, fp, s, len);
+ }
- return n;
+ return lc;
}
void
@@ -977,7 +990,7 @@ writeblob(git_object *obj, const char *fpath, const char *filename, size_t files
if (git_blob_is_binary((git_blob *)obj))
fputs("<p>Binary file.</p>\n", fp);
else
- lc = writeblobhtml(fp, (git_blob *)obj);
+ lc = writeblobhtml(filename, fp, (git_blob *)obj);
writefooter(fp);
checkfileerror(fp, fpath, 'w');
diff --git a/style.css b/style.css
@@ -1,9 +1,19 @@
+html {
+ font-size: 12px;
+ height: 100%;
+}
+
body {
- color: #000;
+ margin: 5rem auto;
+ color: #222;
background-color: #fff;
font-family: monospace;
+ width: 66rem;
}
+pre { -moz-tab-size: 4; }
+pre { tab-size: 4; }
+
h1, h2, h3, h4, h5, h6 {
font-size: 1em;
margin: 0;
@@ -28,8 +38,89 @@ a.line {
text-decoration: none;
}
+#blob {
+ display: block;
+ max-width: 100%;
+ overflow-x: scroll;
+}
+
+article.markup {
+ font-size: 15px;
+ border: 2px solid #00000017;
+ border-radius: 10px;
+ font-family: sans-serif;
+ padding: 2.5em;
+ margin: 2em 0;
+}
+
+article.markup code {
+ font-size: 0.9em;
+ border: 1px solid #dbdbdb;
+ background-color: #f7f7f7;
+ padding: 0 0.3em;
+ border-radius: 0.3em
+}
+
+article.markup pre code {
+ border: none;
+ background: none;
+ padding: 0;
+ border-radius: 0;
+}
+
+article.markup pre {
+ background-color: #f7f7f7;
+ padding: 1em;
+ border: 1px solid #dbdbdb;
+ border-radius: 0.3em;
+}
+
+article.markup h1 {
+ font-size: 2.4em;
+ padding-bottom: 6px;
+ border-bottom: 5px solid #0000000a;
+}
+
+article.markup h2 {
+ font-size: 1.9em;
+ padding-bottom: 5px;
+ border-bottom: 2px solid #00000014;
+}
+
+article.markup h3 {
+ font-size: 1.5em;
+}
+
+article.markup h4 {
+ font-size: 1.3em;
+}
+
+article.markup h5 {
+ font-size: 1.1em;
+}
+
+article.markup h6 {
+ font-size: 1em;
+}
+
+article img {
+ max-width: 100%;
+}
+
+.linenos {
+ margin-right: 0;
+ border-right: 1px solid rgb(0 0 0 / 8%);
+ user-select: none;
+}
+
+.linenos a {
+ margin-right: 0.9em;
+ user-select: none;
+ text-decoration: none;
+}
+
#blob a {
- color: #555;
+ color: #777;
}
#blob a:hover {
@@ -70,12 +161,12 @@ td.num {
}
.desc {
- color: #555;
+ color: #777;
}
hr {
border: 0;
- border-top: 1px solid #555;
+ border-top: 1px solid #777;
height: 1px;
}
@@ -105,50 +196,18 @@ pre a.d:hover {
text-decoration: none;
}
-@media (prefers-color-scheme: dark) {
- body {
- background-color: #000;
- color: #bdbdbd;
- }
- hr {
- border-color: #222;
- }
- a {
- color: #56c8ff;
- }
- a:target {
- background-color: #222;
- }
- .desc {
- color: #aaa;
- }
- #blob a {
- color: #555;
- }
- #blob a:target {
- color: #eee;
- }
- #blob a:hover {
- color: #56c8ff;
- }
- pre a.h {
- color: #00cdcd;
- }
- .A,
- span.i,
- pre a.i {
- color: #00cd00;
- }
- .D,
- span.d,
- pre a.d {
- color: #cd0000;
- }
- #branches tr:hover td,
- #tags tr:hover td,
- #index tr:hover td,
- #log tr:hover td,
- #files tr:hover td {
- background-color: #111;
- }
+.url td:nth-child(2) {
+ padding-top: 0.2em;
+ padding-bottom: 0.9em;
+}
+
+.url td:nth-child(2) span {
+ padding: 1px 5px;
+ background-color: #eee;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+}
+
+.url td:nth-child(2) span a {
+ color: #444;
}