Compare commits

...

8 Commits
0.30 ... master

Author SHA1 Message Date
Alberto Bertogli
dff4ff6757 css: Work around Pygments' element layout changes
Pygments 2.12 changes the element layout slightly, adding a wrapper
<div> that was accidentally removed before:
https://github.com/pygments/pygments/issues/632.

This patch adds a workaround, so the styling is consistent on both 2.11
and 2.12.
2022-10-13 22:32:21 +01:00
Alberto Bertogli
6ea59bad51 css: Dark mode for Pygments' syntax highlight
This patch updates Pygments' syntax highlight CSS to support dark mode.
It uses two themes from Pygments: `default` for light (same as before),
and `native` for dark.
2022-10-13 22:32:21 +01:00
Alberto Bertogli
4b1e1eb84c css: Introduce dark mode
This patch extends our CSS to introduce dark mode, so the style shown
matches the user media preference.

It is very analogous to the previous one, only minor adjustments have
been made to make the contrast levels pass the accessibility standards.

No changes have been made to the pygments CSS. It works surprisingly
well as-is, but there are some minor changes that may be needed. Those
will be done in subsequent patches.
2022-10-13 22:32:21 +01:00
Alberto Bertogli
518188288e Cache some (possibly) expensive function calls
This patch memoizes some of the functions to help speed up execution.
The speedup is quite variable, but ~30% is normal when generating a
medium size repository, and the output is byte-for-byte identical.
2022-08-31 23:15:16 +01:00
Alberto Bertogli
15547b2796 utils: Update Markdown local links extension to the new API
The Markdown extension for rewriting local links was using an API that
is now deprecated, and as of python-markdown 3.4 it is no longer
available.

This patch adjusts the code to use the new API which should be available
from 3.0 onwards.
2022-08-31 21:07:53 +01:00
Alberto Bertogli
9f3df4899f css: Improve handling of text overflow in <pre>
When a <pre> section (commit message, blob, diff) has a very long line,
today it makes the entire page very wide, causing usability issues.

This patch makes <pre> have a horizontal scroll in those cases, which is
easier to use.
2021-05-15 01:24:58 +01:00
Alberto Bertogli
bc1ee87dfe css: Reduce commit-message left padding
The commit message has a very large left and right padding, but doesn't
improve readability and might make the commit message more difficult to
read on smaller screens.

This patch shortens the padding.
2021-05-15 00:58:11 +01:00
Alberto Bertogli
0d61bbf7f5 css: Auto-format git-arr.css
Auto-format static/git-arr.css with https://blitiri.com.ar/git/r/css3fmt/
for consistency.
2021-05-15 00:53:42 +01:00
7 changed files with 336 additions and 105 deletions

View File

@ -186,7 +186,7 @@ bottle.app.push(app)
def with_utils(f): def with_utils(f):
"""Decorator to add the utilities to the return value. """Decorator to add the utilities to the return value.
Used to wrap functions that return dictionaries which are then passed to Used to wrap functions that return dictionaries which are then passed to
templates. templates.
""" """

57
git.py
View File

@ -6,6 +6,7 @@ command line tool directly, so please be careful with using untrusted
parameters. parameters.
""" """
import functools
import sys import sys
import io import io
import subprocess import subprocess
@ -199,7 +200,8 @@ class Repo:
"""Returns a GitCommand() on our path.""" """Returns a GitCommand() on our path."""
return GitCommand(self.path, cmd) return GitCommand(self.path, cmd)
def for_each_ref(self, pattern=None, sort=None, count=None): @functools.lru_cache
def _for_each_ref(self, pattern=None, sort=None, count=None):
"""Returns a list of references.""" """Returns a list of references."""
cmd = self.cmd("for-each-ref") cmd = self.cmd("for-each-ref")
if sort: if sort:
@ -209,26 +211,25 @@ class Repo:
if pattern: if pattern:
cmd.arg(pattern) cmd.arg(pattern)
refs = []
for l in cmd.run(): for l in cmd.run():
obj_id, obj_type, ref = l.split() obj_id, obj_type, ref = l.split()
yield obj_id, obj_type, ref refs.append((obj_id, obj_type, ref))
return refs
def branches(self, sort="-authordate"):
"""Get the (name, obj_id) of the branches."""
refs = self.for_each_ref(pattern="refs/heads/", sort=sort)
for obj_id, _, ref in refs:
yield ref[len("refs/heads/") :], obj_id
@functools.cache
def branch_names(self): def branch_names(self):
"""Get the names of the branches.""" """Get the names of the branches."""
return (name for name, _ in self.branches()) refs = self._for_each_ref(pattern="refs/heads/", sort="-authordate")
return [ref[len("refs/heads/") :] for _, _, ref in refs]
@functools.cache
def tags(self, sort="-taggerdate"): def tags(self, sort="-taggerdate"):
"""Get the (name, obj_id) of the tags.""" """Get the (name, obj_id) of the tags."""
refs = self.for_each_ref(pattern="refs/tags/", sort=sort) refs = self._for_each_ref(pattern="refs/tags/", sort=sort)
for obj_id, _, ref in refs: return [(ref[len("refs/tags/") :], obj_id) for obj_id, _, ref in refs]
yield ref[len("refs/tags/") :], obj_id
@functools.lru_cache
def commit_ids(self, ref, limit=None): def commit_ids(self, ref, limit=None):
"""Generate commit ids.""" """Generate commit ids."""
cmd = self.cmd("rev-list") cmd = self.cmd("rev-list")
@ -238,9 +239,9 @@ class Repo:
cmd.arg(ref) cmd.arg(ref)
cmd.arg("--") cmd.arg("--")
for l in cmd.run(): return [l.rstrip("\n") for l in cmd.run()]
yield l.rstrip("\n")
@functools.lru_cache
def commit(self, commit_id): def commit(self, commit_id):
"""Return a single commit.""" """Return a single commit."""
cs = list(self.commits(commit_id, limit=1)) cs = list(self.commits(commit_id, limit=1))
@ -248,11 +249,11 @@ class Repo:
return None return None
return cs[0] return cs[0]
def commits(self, ref, limit=None, offset=0): @functools.lru_cache
def commits(self, ref, limit, offset=0):
"""Generate commit objects for the ref.""" """Generate commit objects for the ref."""
cmd = self.cmd("rev-list") cmd = self.cmd("rev-list")
if limit: cmd.max_count = limit + offset
cmd.max_count = limit + offset
cmd.header = None cmd.header = None
@ -261,6 +262,7 @@ class Repo:
info_buffer = "" info_buffer = ""
count = 0 count = 0
commits = []
for l in cmd.run(): for l in cmd.run():
if "\0" in l: if "\0" in l:
pre, post = l.split("\0", 1) pre, post = l.split("\0", 1)
@ -268,7 +270,7 @@ class Repo:
count += 1 count += 1
if count > offset: if count > offset:
yield Commit.from_str(self, info_buffer) commits.append(Commit.from_str(self, info_buffer))
# Start over. # Start over.
info_buffer = post info_buffer = post
@ -278,8 +280,11 @@ class Repo:
if info_buffer: if info_buffer:
count += 1 count += 1
if count > offset: if count > offset:
yield Commit.from_str(self, info_buffer) commits.append(Commit.from_str(self, info_buffer))
return commits
@functools.lru_cache
def diff(self, ref): def diff(self, ref):
"""Return a Diff object for the ref.""" """Return a Diff object for the ref."""
cmd = self.cmd("diff-tree") cmd = self.cmd("diff-tree")
@ -295,6 +300,7 @@ class Repo:
return Diff.from_str(cmd.run()) return Diff.from_str(cmd.run())
@functools.lru_cache
def refs(self): def refs(self):
"""Return a dict of obj_id -> ref.""" """Return a dict of obj_id -> ref."""
cmd = self.cmd("show-ref") cmd = self.cmd("show-ref")
@ -308,10 +314,12 @@ class Repo:
return r return r
@functools.lru_cache
def tree(self, ref): def tree(self, ref):
"""Returns a Tree instance for the given ref.""" """Returns a Tree instance for the given ref."""
return Tree(self, ref) return Tree(self, ref)
@functools.lru_cache
def blob(self, path, ref): def blob(self, path, ref):
"""Returns a Blob instance for the given path.""" """Returns a Blob instance for the given path."""
cmd = self.cmd("cat-file") cmd = self.cmd("cat-file")
@ -329,9 +337,10 @@ class Repo:
return Blob(out.read()[: int(head)]) return Blob(out.read()[: int(head)])
@functools.cache
def last_commit_timestamp(self): def last_commit_timestamp(self):
"""Return the timestamp of the last commit.""" """Return the timestamp of the last commit."""
refs = self.for_each_ref( refs = self._for_each_ref(
pattern="refs/heads/", sort="-committerdate", count=1 pattern="refs/heads/", sort="-committerdate", count=1
) )
for obj_id, _, _ in refs: for obj_id, _, _ in refs:
@ -515,12 +524,13 @@ class Diff:
class Tree: class Tree:
""" A git tree.""" """A git tree."""
def __init__(self, repo: Repo, ref: str): def __init__(self, repo: Repo, ref: str):
self.repo = repo self.repo = repo
self.ref = ref self.ref = ref
@functools.lru_cache
def ls( def ls(
self, path, recursive=False self, path, recursive=False
) -> Iterable[Tuple[str, smstr, Optional[int]]]: ) -> Iterable[Tuple[str, smstr, Optional[int]]]:
@ -537,6 +547,7 @@ class Tree:
else: else:
cmd.arg(path) cmd.arg(path)
files = []
for l in cmd.run(): for l in cmd.run():
_mode, otype, _oid, size, name = l.split(None, 4) _mode, otype, _oid, size, name = l.split(None, 4)
if size == "-": if size == "-":
@ -553,7 +564,9 @@ class Tree:
# We use a smart string for the name, as it's often tricky to # We use a smart string for the name, as it's often tricky to
# manipulate otherwise. # manipulate otherwise.
yield otype, smstr(name), size files.append((otype, smstr(name), size))
return files
class Blob: class Blob:

View File

@ -1,15 +1,53 @@
/* /*
* git-arr style sheet * git-arr style sheet
*/ */
:root {
--body-bg: white;
--text-fg: black;
--h1-bg: #ddd;
--hr-bg: #e3e3e3;
--text-lowcontrast-fg: grey;
--a-fg: #800;
--a-explicit-fg: #038;
--table-hover-bg: #eee;
--head-bg: #88ff88;
--tag-bg: #ffff88;
--age-fg0: darkgreen;
--age-fg1: green;
--age-fg2: seagreen;
--diff-added-fg: green;
--diff-deleted-fg: red;
}
@media (prefers-color-scheme: dark) {
:root {
--body-bg: #121212;
--text-fg: #c9d1d9;
--h1-bg: #2f2f2f;
--hr-bg: #e3e3e3;
--text-lowcontrast-fg: grey;
--a-fg: #d4b263;
--a-explicit-fg: #44b4ec;
--table-hover-bg: #313131;
--head-bg: #020;
--tag-bg: #333000;
--age-fg0: #51a552;
--age-fg1: #468646;
--age-fg2: #2f722f;
--diff-added-fg: #00A000;
--diff-deleted-fg: #A00000;
}
}
body { body {
font-family: sans-serif; font-family: sans-serif;
padding: 0 1em 1em 1em; padding: 0 1em 1em 1em;
color: var(--text-fg);
background: var(--body-bg);
} }
h1 { h1 {
background: #ddd; background: var(--h1-bg);
padding: 0.3em; padding: 0.3em;
} }
@ -21,26 +59,29 @@ h2, h3 {
hr { hr {
border: none; border: none;
background-color: #e3e3e3; background-color: var(--hr-bg);
height: 1px; height: 1px;
} }
/* By default, use implied links, more discrete for increased readability. */ /* By default, use implied links, more discrete for increased readability. */
a { a {
text-decoration: none; text-decoration: none;
color: black; color: var(--text-fg);
} }
a:hover { a:hover {
text-decoration: underline; color: var(--a-fg);
color: #800;
} }
/* Explicit links */ /* Explicit links */
a.explicit { a.explicit {
color: #038; color: var(--a-explicit-fg);
} }
a.explicit:hover, a.explicit:active { a.explicit:hover, a.explicit:active {
color: #880000; color: var(--a-fg);
} }
@ -48,22 +89,27 @@ a.explicit:hover, a.explicit:active {
table.nice { table.nice {
text-align: left; text-align: left;
} }
table.nice td { table.nice td {
padding: 0.15em 0.5em; padding: 0.15em 0.5em;
} }
table.nice td.links { table.nice td.links {
} }
table.nice td.main { table.nice td.main {
min-width: 10em; min-width: 10em;
} }
table.nice tr:hover { table.nice tr:hover {
background: #eee; background: var(--table-hover-bg);
} }
/* Table for commits. */ /* Table for commits. */
table.commits td.date { table.commits td.date {
font-style: italic; font-style: italic;
color: gray; color: var(--text-lowcontrast-fg);
} }
@media (min-width: 600px) { @media (min-width: 600px) {
@ -71,106 +117,134 @@ table.commits td.date {
min-width: 32em; min-width: 32em;
} }
} }
table.commits td.author { table.commits td.author {
color: gray; color: var(--text-lowcontrast-fg);
} }
/* Table for commit information. */ /* Table for commit information. */
table.commit-info tr:hover { table.commit-info tr:hover {
background: inherit; background: inherit;
} }
table.commit-info td { table.commit-info td {
vertical-align: top; vertical-align: top;
} }
table.commit-info span.date, span.email { table.commit-info span.date, span.email {
color: gray; color: var(--text-lowcontrast-fg);
} }
/* Reference annotations. */ /* Reference annotations. */
span.refs { span.refs {
margin: 0px 0.5em; margin: 0px 0.5em;
padding: 0px 0.25em; padding: 0px 0.25em;
border: solid 1px gray; border: solid 1px var(--text-lowcontrast-fg);
} }
span.head { span.head {
background-color: #88ff88; background-color: var(--head-bg);
} }
span.tag { span.tag {
background-color: #ffff88; background-color: var(--tag-bg);
} }
/* Projects table */ /* Projects table */
table.projects td.name a { table.projects td.name a {
color: #038; color: var(--a-explicit-fg);
} }
/* Age of an object. /* Age of an object.
* Note this is hidden by default as we rely on javascript to show it. */ * Note this is hidden by default as we rely on javascript to show it. */
span.age { span.age {
display: none; display: none;
color: gray; color: var(--text-lowcontrast-fg);
font-size: smaller; font-size: smaller;
} }
span.age-band0 { span.age-band0 {
color: darkgreen; color: var(--age-fg0);
} }
span.age-band1 { span.age-band1 {
color: green; color: var(--age-fg1);
} }
span.age-band2 { span.age-band2 {
color: seagreen; color: var(--age-fg2);
} }
/* Toggable titles */ /* Toggable titles */
div.toggable-title { div.toggable-title {
font-weight: bold; font-weight: bold;
margin-bottom: 0.3em; margin-bottom: 0.3em;
} }
pre {
/* Sometimes, <pre> elements (commit messages, diffs, blobs) have very
* long lines. In those case, use automatic overflow, which will
* introduce a horizontal scroll bar for this element only (more
* comfortable than stretching the page, which is the default). */
overflow: auto;
}
/* Commit message and diff. */ /* Commit message and diff. */
pre.commit-message { pre.commit-message {
font-size: large; font-size: large;
padding: 0.2em 2em; padding: 0.2em 0.5em;
} }
pre.diff-body { pre.diff-body {
/* Note this is only used as a fallback if pygments is not available. */ /* Note this is only used as a fallback if pygments is not available. */
} }
table.changed-files { table.changed-files {
font-family: monospace; font-family: monospace;
} }
table.changed-files span.lines-added { table.changed-files span.lines-added {
color: green; color: var(--diff-added-fg);
} }
table.changed-files span.lines-deleted { table.changed-files span.lines-deleted {
color: red; color: var(--diff-deleted-fg);
} }
/* Pagination. */ /* Pagination. */
div.paginate { div.paginate {
padding-bottom: 1em; padding-bottom: 1em;
} }
div.paginate span.inactive { div.paginate span.inactive {
color: gray; color: var(--text-lowcontrast-fg);
} }
/* Directory listing. */ /* Directory listing. */
@media (min-width: 600px) { @media (min-width: 600px) {
table.ls td.name { table.ls td.name {
min-width: 20em; min-width: 20em;
} }
} }
table.ls { table.ls {
font-family: monospace; font-family: monospace;
font-size: larger; font-size: larger;
} }
table.ls tr.blob td.size { table.ls tr.blob td.size {
color: gray; color: var(--text-lowcontrast-fg);
} }
/* Blob. */ /* Blob. */
pre.blob-body { pre.blob-body {
/* Note this is only used as a fallback if pygments is not available. */ /* Note this is only used as a fallback if pygments is not available. */
@ -184,60 +258,79 @@ table.blob-binary pre {
table.blob-binary .offset { table.blob-binary .offset {
text-align: right; text-align: right;
font-size: x-small; font-size: x-small;
color: darkgray; color: var(--text-lowcontrast-fg);
border-right: 1px solid #eee; border-right: 1px solid var(--text-lowcontrast-fg);
} }
table.blob-binary tr.etc { table.blob-binary tr.etc {
text-align: center; text-align: center;
} }
/* Pygments overrides. */ /* Pygments overrides. */
div.linenodiv { div.colorized-src {
padding-right: 0.5em;
font-size: larger; /* must match div.source_code */
}
div.linenodiv a {
color: gray;
}
div.source_code {
background: inherit;
font-size: larger; font-size: larger;
} }
div.colorized-src .source_code {
/* Ignore pygments style's background. */
background: var(--body-bg);
}
td.code > div.source_code {
/* This is a workaround, in pygments 2.11 there's a bug where the wrapper
* div is inside the table, so we need to override the descendant (because
* the style sets it on ".source_code" and the most specific value wins).
* Once we no longer support 2.11, we can remove this. */
background: var(--body-bg);
}
div.linenodiv {
padding-right: 0.5em;
}
div.linenodiv a {
color: var(--text-lowcontrast-fg);
}
/* Repository information table. */ /* Repository information table. */
table.repo_info tr:hover { table.repo_info tr:hover {
background: inherit; background: inherit;
} }
table.repo_info td.category { table.repo_info td.category {
font-weight: bold; font-weight: bold;
/* So we can copy-paste rows and preserve spaces, useful for the row: /* So we can copy-paste rows and preserve spaces, useful for the row:
* git clone | url * git clone | url */
*/
white-space: pre-wrap; white-space: pre-wrap;
} }
table.repo_info td { table.repo_info td {
vertical-align: top; vertical-align: top;
} }
span.ctrlchr { span.ctrlchr {
color: gray; color: var(--text-lowcontrast-fg);
padding: 0 0.2ex 0 0.1ex; padding: 0 0.2ex 0 0.1ex;
margin: 0 0.2ex 0 0.1ex; margin: 0 0.2ex 0 0.1ex;
} }
/* /*
* Markdown overrides * Markdown overrides
*/ */
/* Colored links (same as explicit links above) */ /* Colored links (same as explicit links above) */
div.markdown a { div.markdown a {
color: #038; color: var(--a-explicit-fg);
} }
div.markdown a:hover, div.markdown a:active { div.markdown a:hover, div.markdown a:active {
color: #880000; color: var(--a-fg);
} }
/* Restrict max width for readability */ /* Restrict max width for readability */
div.markdown { div.markdown {
max-width: 55em; max-width: 55em;

View File

@ -1,30 +1,37 @@
/* CSS for syntax highlighting. /* CSS for syntax highlighting.
* Generated by pygments (what we use for syntax highlighting): * Generated by pygments (what we use for syntax highlighting).
* *
* $ pygmentize -S default -f html -a .source_code * Light mode: pygmentize -S default -f html -a .source_code
*/ */
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.source_code .hll { background-color: #ffffcc } .source_code .hll { background-color: #ffffcc }
.source_code { background: #f8f8f8; } .source_code { background: #f8f8f8; }
.source_code .c { color: #408080; font-style: italic } /* Comment */ .source_code .c { color: #3D7B7B; font-style: italic } /* Comment */
.source_code .err { border: 1px solid #FF0000 } /* Error */ .source_code .err { border: 1px solid #FF0000 } /* Error */
.source_code .k { color: #008000; font-weight: bold } /* Keyword */ .source_code .k { color: #008000; font-weight: bold } /* Keyword */
.source_code .o { color: #666666 } /* Operator */ .source_code .o { color: #666666 } /* Operator */
.source_code .cm { color: #408080; font-style: italic } /* Comment.Multiline */ .source_code .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
.source_code .cp { color: #BC7A00 } /* Comment.Preproc */ .source_code .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
.source_code .c1 { color: #408080; font-style: italic } /* Comment.Single */ .source_code .cp { color: #9C6500 } /* Comment.Preproc */
.source_code .cs { color: #408080; font-style: italic } /* Comment.Special */ .source_code .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
.source_code .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
.source_code .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
.source_code .gd { color: #A00000 } /* Generic.Deleted */ .source_code .gd { color: #A00000 } /* Generic.Deleted */
.source_code .ge { font-style: italic } /* Generic.Emph */ .source_code .ge { font-style: italic } /* Generic.Emph */
.source_code .gr { color: #FF0000 } /* Generic.Error */ .source_code .gr { color: #E40000 } /* Generic.Error */
.source_code .gh { color: #000080; font-weight: bold } /* Generic.Heading */ .source_code .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.source_code .gi { color: #00A000 } /* Generic.Inserted */ .source_code .gi { color: #008400 } /* Generic.Inserted */
.source_code .go { color: #808080 } /* Generic.Output */ .source_code .go { color: #717171 } /* Generic.Output */
.source_code .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ .source_code .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.source_code .gs { font-weight: bold } /* Generic.Strong */ .source_code .gs { font-weight: bold } /* Generic.Strong */
.source_code .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ .source_code .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.source_code .gt { color: #0040D0 } /* Generic.Traceback */ .source_code .gt { color: #0044DD } /* Generic.Traceback */
.source_code .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ .source_code .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
.source_code .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ .source_code .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
.source_code .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ .source_code .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
@ -33,38 +40,139 @@
.source_code .kt { color: #B00040 } /* Keyword.Type */ .source_code .kt { color: #B00040 } /* Keyword.Type */
.source_code .m { color: #666666 } /* Literal.Number */ .source_code .m { color: #666666 } /* Literal.Number */
.source_code .s { color: #BA2121 } /* Literal.String */ .source_code .s { color: #BA2121 } /* Literal.String */
.source_code .na { color: #7D9029 } /* Name.Attribute */ .source_code .na { color: #687822 } /* Name.Attribute */
.source_code .nb { color: #008000 } /* Name.Builtin */ .source_code .nb { color: #008000 } /* Name.Builtin */
.source_code .nc { color: #0000FF; font-weight: bold } /* Name.Class */ .source_code .nc { color: #0000FF; font-weight: bold } /* Name.Class */
.source_code .no { color: #880000 } /* Name.Constant */ .source_code .no { color: #880000 } /* Name.Constant */
.source_code .nd { color: #AA22FF } /* Name.Decorator */ .source_code .nd { color: #AA22FF } /* Name.Decorator */
.source_code .ni { color: #999999; font-weight: bold } /* Name.Entity */ .source_code .ni { color: #717171; font-weight: bold } /* Name.Entity */
.source_code .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ .source_code .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
.source_code .nf { color: #0000FF } /* Name.Function */ .source_code .nf { color: #0000FF } /* Name.Function */
.source_code .nl { color: #A0A000 } /* Name.Label */ .source_code .nl { color: #767600 } /* Name.Label */
.source_code .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ .source_code .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
.source_code .nt { color: #008000; font-weight: bold } /* Name.Tag */ .source_code .nt { color: #008000; font-weight: bold } /* Name.Tag */
.source_code .nv { color: #19177C } /* Name.Variable */ .source_code .nv { color: #19177C } /* Name.Variable */
.source_code .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ .source_code .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
.source_code .w { color: #bbbbbb } /* Text.Whitespace */ .source_code .w { color: #bbbbbb } /* Text.Whitespace */
.source_code .mb { color: #666666 } /* Literal.Number.Bin */
.source_code .mf { color: #666666 } /* Literal.Number.Float */ .source_code .mf { color: #666666 } /* Literal.Number.Float */
.source_code .mh { color: #666666 } /* Literal.Number.Hex */ .source_code .mh { color: #666666 } /* Literal.Number.Hex */
.source_code .mi { color: #666666 } /* Literal.Number.Integer */ .source_code .mi { color: #666666 } /* Literal.Number.Integer */
.source_code .mo { color: #666666 } /* Literal.Number.Oct */ .source_code .mo { color: #666666 } /* Literal.Number.Oct */
.source_code .sa { color: #BA2121 } /* Literal.String.Affix */
.source_code .sb { color: #BA2121 } /* Literal.String.Backtick */ .source_code .sb { color: #BA2121 } /* Literal.String.Backtick */
.source_code .sc { color: #BA2121 } /* Literal.String.Char */ .source_code .sc { color: #BA2121 } /* Literal.String.Char */
.source_code .dl { color: #BA2121 } /* Literal.String.Delimiter */
.source_code .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ .source_code .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
.source_code .s2 { color: #BA2121 } /* Literal.String.Double */ .source_code .s2 { color: #BA2121 } /* Literal.String.Double */
.source_code .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ .source_code .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
.source_code .sh { color: #BA2121 } /* Literal.String.Heredoc */ .source_code .sh { color: #BA2121 } /* Literal.String.Heredoc */
.source_code .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ .source_code .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
.source_code .sx { color: #008000 } /* Literal.String.Other */ .source_code .sx { color: #008000 } /* Literal.String.Other */
.source_code .sr { color: #BB6688 } /* Literal.String.Regex */ .source_code .sr { color: #A45A77 } /* Literal.String.Regex */
.source_code .s1 { color: #BA2121 } /* Literal.String.Single */ .source_code .s1 { color: #BA2121 } /* Literal.String.Single */
.source_code .ss { color: #19177C } /* Literal.String.Symbol */ .source_code .ss { color: #19177C } /* Literal.String.Symbol */
.source_code .bp { color: #008000 } /* Name.Builtin.Pseudo */ .source_code .bp { color: #008000 } /* Name.Builtin.Pseudo */
.source_code .fm { color: #0000FF } /* Name.Function.Magic */
.source_code .vc { color: #19177C } /* Name.Variable.Class */ .source_code .vc { color: #19177C } /* Name.Variable.Class */
.source_code .vg { color: #19177C } /* Name.Variable.Global */ .source_code .vg { color: #19177C } /* Name.Variable.Global */
.source_code .vi { color: #19177C } /* Name.Variable.Instance */ .source_code .vi { color: #19177C } /* Name.Variable.Instance */
.source_code .vm { color: #19177C } /* Name.Variable.Magic */
.source_code .il { color: #666666 } /* Literal.Number.Integer.Long */ .source_code .il { color: #666666 } /* Literal.Number.Integer.Long */
/*
* Dark mode: pygmentize -S native -f html -a .source_code
*/
@media (prefers-color-scheme: dark) {
pre { line-height: 125%; }
td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.source_code .hll { background-color: #404040 }
.source_code { background: #202020; color: #d0d0d0 }
.source_code .c { color: #ababab; font-style: italic } /* Comment */
.source_code .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.source_code .esc { color: #d0d0d0 } /* Escape */
.source_code .g { color: #d0d0d0 } /* Generic */
.source_code .k { color: #6ebf26; font-weight: bold } /* Keyword */
.source_code .l { color: #d0d0d0 } /* Literal */
.source_code .n { color: #d0d0d0 } /* Name */
.source_code .o { color: #d0d0d0 } /* Operator */
.source_code .x { color: #d0d0d0 } /* Other */
.source_code .p { color: #d0d0d0 } /* Punctuation */
.source_code .ch { color: #ababab; font-style: italic } /* Comment.Hashbang */
.source_code .cm { color: #ababab; font-style: italic } /* Comment.Multiline */
.source_code .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */
.source_code .cpf { color: #ababab; font-style: italic } /* Comment.PreprocFile */
.source_code .c1 { color: #ababab; font-style: italic } /* Comment.Single */
.source_code .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
.source_code .gd { color: #d22323 } /* Generic.Deleted */
.source_code .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
.source_code .gr { color: #d22323 } /* Generic.Error */
.source_code .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
.source_code .gi { color: #589819 } /* Generic.Inserted */
.source_code .go { color: #cccccc } /* Generic.Output */
.source_code .gp { color: #aaaaaa } /* Generic.Prompt */
.source_code .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
.source_code .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
.source_code .gt { color: #d22323 } /* Generic.Traceback */
.source_code .kc { color: #6ebf26; font-weight: bold } /* Keyword.Constant */
.source_code .kd { color: #6ebf26; font-weight: bold } /* Keyword.Declaration */
.source_code .kn { color: #6ebf26; font-weight: bold } /* Keyword.Namespace */
.source_code .kp { color: #6ebf26 } /* Keyword.Pseudo */
.source_code .kr { color: #6ebf26; font-weight: bold } /* Keyword.Reserved */
.source_code .kt { color: #6ebf26; font-weight: bold } /* Keyword.Type */
.source_code .ld { color: #d0d0d0 } /* Literal.Date */
.source_code .m { color: #51b2fd } /* Literal.Number */
.source_code .s { color: #ed9d13 } /* Literal.String */
.source_code .na { color: #bbbbbb } /* Name.Attribute */
.source_code .nb { color: #2fbccd } /* Name.Builtin */
.source_code .nc { color: #71adff; text-decoration: underline } /* Name.Class */
.source_code .no { color: #40ffff } /* Name.Constant */
.source_code .nd { color: #ffa500 } /* Name.Decorator */
.source_code .ni { color: #d0d0d0 } /* Name.Entity */
.source_code .ne { color: #bbbbbb } /* Name.Exception */
.source_code .nf { color: #71adff } /* Name.Function */
.source_code .nl { color: #d0d0d0 } /* Name.Label */
.source_code .nn { color: #71adff; text-decoration: underline } /* Name.Namespace */
.source_code .nx { color: #d0d0d0 } /* Name.Other */
.source_code .py { color: #d0d0d0 } /* Name.Property */
.source_code .nt { color: #6ebf26; font-weight: bold } /* Name.Tag */
.source_code .nv { color: #40ffff } /* Name.Variable */
.source_code .ow { color: #6ebf26; font-weight: bold } /* Operator.Word */
.source_code .w { color: #666666 } /* Text.Whitespace */
.source_code .mb { color: #51b2fd } /* Literal.Number.Bin */
.source_code .mf { color: #51b2fd } /* Literal.Number.Float */
.source_code .mh { color: #51b2fd } /* Literal.Number.Hex */
.source_code .mi { color: #51b2fd } /* Literal.Number.Integer */
.source_code .mo { color: #51b2fd } /* Literal.Number.Oct */
.source_code .sa { color: #ed9d13 } /* Literal.String.Affix */
.source_code .sb { color: #ed9d13 } /* Literal.String.Backtick */
.source_code .sc { color: #ed9d13 } /* Literal.String.Char */
.source_code .dl { color: #ed9d13 } /* Literal.String.Delimiter */
.source_code .sd { color: #ed9d13 } /* Literal.String.Doc */
.source_code .s2 { color: #ed9d13 } /* Literal.String.Double */
.source_code .se { color: #ed9d13 } /* Literal.String.Escape */
.source_code .sh { color: #ed9d13 } /* Literal.String.Heredoc */
.source_code .si { color: #ed9d13 } /* Literal.String.Interpol */
.source_code .sx { color: #ffa500 } /* Literal.String.Other */
.source_code .sr { color: #ed9d13 } /* Literal.String.Regex */
.source_code .s1 { color: #ed9d13 } /* Literal.String.Single */
.source_code .ss { color: #ed9d13 } /* Literal.String.Symbol */
.source_code .bp { color: #2fbccd } /* Name.Builtin.Pseudo */
.source_code .fm { color: #71adff } /* Name.Function.Magic */
.source_code .vc { color: #40ffff } /* Name.Variable.Class */
.source_code .vg { color: #40ffff } /* Name.Variable.Global */
.source_code .vi { color: #40ffff } /* Name.Variable.Instance */
.source_code .vm { color: #40ffff } /* Name.Variable.Magic */
.source_code .il { color: #51b2fd } /* Literal.Number.Integer.Long */
/* Dark mode - my overrides, because the defaults are too bright. */
.source_code .gh { color: rgb(189, 193, 198); }
.source_code .gu { color: rgb(189, 193, 198); }
}

View File

@ -9,6 +9,14 @@ try:
from pygments import highlight # type: ignore from pygments import highlight # type: ignore
from pygments import lexers # type: ignore from pygments import lexers # type: ignore
from pygments.formatters import HtmlFormatter # type: ignore from pygments.formatters import HtmlFormatter # type: ignore
_html_formatter = HtmlFormatter(
encoding="utf-8",
cssclass="source_code",
linenos="table",
anchorlinenos=True,
lineanchors="line",
)
except ImportError: except ImportError:
pygments = None pygments = None
@ -19,6 +27,7 @@ except ImportError:
markdown = None markdown = None
import base64 import base64
import functools
import mimetypes import mimetypes
import string import string
import os.path import os.path
@ -32,6 +41,7 @@ def shorten(s: str, width=60):
return s[:57] + "..." return s[:57] + "..."
@functools.lru_cache
def can_colorize(s: str): def can_colorize(s: str):
"""True if we can colorize the string, False otherwise.""" """True if we can colorize the string, False otherwise."""
if pygments is None: if pygments is None:
@ -77,6 +87,7 @@ def can_embed_image(repo, fname):
) )
@functools.lru_cache
def colorize_diff(s: str) -> str: def colorize_diff(s: str) -> str:
lexer = lexers.DiffLexer(encoding="utf-8") lexer = lexers.DiffLexer(encoding="utf-8")
formatter = HtmlFormatter(encoding="utf-8", cssclass="source_code") formatter = HtmlFormatter(encoding="utf-8", cssclass="source_code")
@ -84,6 +95,7 @@ def colorize_diff(s: str) -> str:
return highlight(s, lexer, formatter) return highlight(s, lexer, formatter)
@functools.lru_cache
def colorize_blob(fname, s: str) -> str: def colorize_blob(fname, s: str) -> str:
try: try:
lexer = lexers.guess_lexer_for_filename(fname, s, encoding="utf-8") lexer = lexers.guess_lexer_for_filename(fname, s, encoding="utf-8")
@ -98,24 +110,7 @@ def colorize_blob(fname, s: str) -> str:
except lexers.ClassNotFound: except lexers.ClassNotFound:
pass pass
formatter = HtmlFormatter( return highlight(s, lexer, _html_formatter)
encoding="utf-8",
cssclass="source_code",
linenos="table",
anchorlinenos=True,
lineanchors="line",
)
return highlight(s, lexer, formatter)
def markdown_blob(s: str) -> str:
extensions = [
"markdown.extensions.fenced_code",
"markdown.extensions.tables",
RewriteLocalLinksExtension(),
]
return markdown.markdown(s, extensions=extensions)
def embed_image_blob(fname: str, image_data: bytes) -> str: def embed_image_blob(fname: str, image_data: bytes) -> str:
@ -126,11 +121,13 @@ def embed_image_blob(fname: str, image_data: bytes) -> str:
) )
@functools.lru_cache
def is_binary(b: bytes): def is_binary(b: bytes):
# Git considers a blob binary if NUL in first ~8KB, so do the same. # Git considers a blob binary if NUL in first ~8KB, so do the same.
return b"\0" in b[:8192] return b"\0" in b[:8192]
@functools.lru_cache
def hexdump(s: bytes): def hexdump(s: bytes):
graph = string.ascii_letters + string.digits + string.punctuation + " " graph = string.ascii_letters + string.digits + string.punctuation + " "
b = s.decode("latin1") b = s.decode("latin1")
@ -177,7 +174,23 @@ if markdown:
tag.set("href", new_target) tag.set("href", new_target)
class RewriteLocalLinksExtension(markdown.Extension): class RewriteLocalLinksExtension(markdown.Extension):
def extendMarkdown(self, md, md_globals): def extendMarkdown(self, md):
md.treeprocessors.add( md.treeprocessors.register(
"RewriteLocalLinks", RewriteLocalLinks(), "_end" RewriteLocalLinks(), "RewriteLocalLinks", 1000
) )
_md_extensions = [
"markdown.extensions.fenced_code",
"markdown.extensions.tables",
RewriteLocalLinksExtension(),
]
@functools.lru_cache
def markdown_blob(s: str) -> str:
return markdown.markdown(s, extensions=_md_extensions)
else:
@functools.lru_cache
def markdown_blob(s: str) -> str:
raise RuntimeError("markdown_blob() called without markdown support")

View File

@ -77,7 +77,9 @@
{{!markdown_blob(blob.utf8_content)}} {{!markdown_blob(blob.utf8_content)}}
</div> </div>
% elif can_colorize(blob.utf8_content): % elif can_colorize(blob.utf8_content):
<div class="colorized-src">
{{!colorize_blob(fname.raw, blob.utf8_content)}} {{!colorize_blob(fname.raw, blob.utf8_content)}}
</div>
% else: % else:
<pre class="blob-body"> <pre class="blob-body">
{{blob.utf8_content}} {{blob.utf8_content}}

View File

@ -56,7 +56,9 @@
<hr/> <hr/>
% if can_colorize(c.diff.body): % if can_colorize(c.diff.body):
<div class="colorized-src">
{{!colorize_diff(c.diff.body)}} {{!colorize_diff(c.diff.body)}}
</div>
% else: % else:
<pre class="diff-body"> <pre class="diff-body">
{{c.diff.body}} {{c.diff.body}}