22 Commits

Author SHA1 Message Date
1d861d7ba8 exclude readme from home search results 2025-12-10 13:10:39 +00:00
0d98335078 order tags browser, exclude include readme files 2025-12-10 13:03:33 +00:00
245dcb2f96 use readme uuids to permalink directories 2025-12-10 12:53:15 +00:00
5133e08afb make sound less like ai because it's not 😠 2025-12-10 12:05:38 +00:00
Akbar Rahman
fa8840d25e Update indexsearch.js 2024-11-13 12:14:19 +00:00
Akbar Rahman
07ece47980 fix indexsearch.js using wrong variable 2024-11-13 12:13:40 +00:00
6638c72230 include styling to permalink redirect page 2024-04-14 16:01:30 +01:00
44e5da27cc feed at second url 2024-04-13 21:15:42 +01:00
41cadb0354 update blog_index title handling 2024-04-13 21:02:53 +01:00
a7fc67ee00 styling tweaks 2024-04-13 20:51:38 +01:00
f94fd769cc make start and end of posts more clear 2024-04-13 20:49:41 +01:00
2b2cdcef79 update readme 2024-04-13 20:37:53 +01:00
aa159369fd fix sorting 2024-04-13 20:36:24 +01:00
7bee57bcd5 sort posts in rss feed by date 2024-04-13 20:28:46 +01:00
8300c4ba45 add base_url documentation for blog mode 2024-04-13 20:16:23 +01:00
390f5a789f fix double h1 on index with readme contents 2024-04-13 20:14:03 +01:00
f23f99aeec add blog mode 2024-04-13 19:53:51 +01:00
c59283b3fc 🦫 2024-01-06 20:17:04 +00:00
662dd48e9c update screenshot 2024-01-02 20:00:43 +00:00
309f201de6 fix readme showing in listings 2024-01-02 19:38:25 +00:00
e0277641a2 fix plaintext files not showing properly 2024-01-02 19:32:00 +00:00
b00973d18d add old name to readme 2024-01-02 18:38:35 +00:00
12 changed files with 483 additions and 175 deletions

View File

@@ -104,6 +104,12 @@ mjx-container {
p.metadata { margin: 0 } p.metadata { margin: 0 }
.blog_inline_post {
border-left: 1em solid var(--fg-lc);
padding: 0 1em 0 1em;
margin-bottom: 5em;
}
@media (max-width: 80em) { @media (max-width: 80em) {
/* CSS that should be displayed if width is equal to or less than 60em goes here */ /* CSS that should be displayed if width is equal to or less than 60em goes here */
#contentWrapper { flex-direction: column } #contentWrapper { flex-direction: column }

441
gronk.py
View File

@@ -13,7 +13,8 @@ import copy
import time import time
import magic import magic
import regex as re import regex as re
import pprint from datetime import datetime as dt
import json
import frontmatter import frontmatter
import jinja2 import jinja2
@@ -25,12 +26,12 @@ PANDOC_SERVER_URL = os.getenv("PANDOC_SERVER_URL", r"http://localhost:3030/")
PANDOC_TIMEOUT = int(os.getenv("PANDOC_TIMEOUT", "120")) PANDOC_TIMEOUT = int(os.getenv("PANDOC_TIMEOUT", "120"))
GRONK_CSS_DIR = Path(os.getenv("GRONK_CSS_DIR", "/opt/gronk/css")) GRONK_CSS_DIR = Path(os.getenv("GRONK_CSS_DIR", "/opt/gronk/css"))
GRONK_JS_DIR = Path(os.getenv("GRONK_JS_DIR", "/opt/gronk/js")) GRONK_JS_DIR = Path(os.getenv("GRONK_JS_DIR", "/opt/gronk/js"))
GRONK_TEMPLATES_DIR = Path( GRONK_TEMPLATES_DIR = Path(os.getenv("GRONK_TEMPLATES_DIR", "/opt/gronk/templates/"))
os.getenv("GRONK_TEMPLATES_DIR", "/opt/gronk/templates/"))
JINJA_ENV = jinja2.Environment( JINJA_ENV = jinja2.Environment(
loader=jinja2.FileSystemLoader(searchpath=GRONK_TEMPLATES_DIR), loader=jinja2.FileSystemLoader(searchpath=GRONK_TEMPLATES_DIR),
autoescape=jinja2.select_autoescape) autoescape=jinja2.select_autoescape,
)
JINJA_TEMPLATE_TEXTARTICLE = JINJA_ENV.get_template("article-text.html") JINJA_TEMPLATE_TEXTARTICLE = JINJA_ENV.get_template("article-text.html")
JINJA_TEMPLATE_HOME_INDEX = JINJA_ENV.get_template("home.html") JINJA_TEMPLATE_HOME_INDEX = JINJA_ENV.get_template("home.html")
@@ -38,6 +39,10 @@ JINJA_TEMPLATE_INDEX = JINJA_ENV.get_template("index.html")
JINJA_TEMPLATE_ARTICLE = JINJA_ENV.get_template("article.html") JINJA_TEMPLATE_ARTICLE = JINJA_ENV.get_template("article.html")
JINJA_TEMPLATE_PERMALINK = JINJA_ENV.get_template("permalink.html") JINJA_TEMPLATE_PERMALINK = JINJA_ENV.get_template("permalink.html")
JINJA_TEMPLATE_BLOGINDEX = JINJA_ENV.get_template("blog_index.html")
JINJA_TEMPLATE_BLOG_INLINE_POST = JINJA_ENV.get_template("blog_inline_post.html")
JINJA_TEMPLATE_BLOG_FEED = JINJA_ENV.get_template("rss.xml")
LICENSE = None LICENSE = None
FILEMAP = None FILEMAP = None
@@ -53,14 +58,19 @@ class FileMap:
self.input_dir = Path(input_dir) self.input_dir = Path(input_dir)
self.output_dir = Path(output_dir) self.output_dir = Path(output_dir)
def get_base_url(self):
props = self.get(self.input_dir.joinpath("readme.md"))
return props["base_url"]
@staticmethod @staticmethod
def _path_to_key(path): def _path_to_key(path):
return str(path) return str(path)
@staticmethod @staticmethod
def is_plaintext(filename): def is_plaintext(filename):
return re.match(r'^text/', magic.from_file(str(filename), return (
mime=True)) is not None re.match(r"^text/", magic.from_file(str(filename), mime=True)) is not None
)
def add(self, filepath): def add(self, filepath):
filepath = Path(filepath) filepath = Path(filepath)
@@ -69,8 +79,8 @@ class FileMap:
else: else:
properties = self._get_file_properties(filepath) properties = self._get_file_properties(filepath)
properties['src_path'] = filepath properties["src_path"] = filepath
properties['dst_path'] = self._get_output_filepath(filepath) properties["dst_path"] = self._get_output_filepath(filepath)
self._map[self._path_to_key(filepath)] = properties self._map[self._path_to_key(filepath)] = properties
@@ -84,8 +94,7 @@ class FileMap:
if self._path_to_key(filepath) not in self._map.keys(): if self._path_to_key(filepath) not in self._map.keys():
self.add(filepath) self.add(filepath)
properties = copy.deepcopy( properties = copy.deepcopy(self._map.get(self._path_to_key(filepath), default))
self._map.get(self._path_to_key(filepath), default))
if raw: if raw:
return properties return properties
@@ -93,45 +102,46 @@ class FileMap:
parent = filepath parent = filepath
while True: while True:
parent = parent.parent parent = parent.parent
if parent == Path('.'): if parent == Path("."):
break break
parent_properties = self.get(parent, raw=True) parent_properties = self.get(parent, raw=True)
# TODO inherit any property that isn't defined, append any lists # TODO inherit any property that isn't defined, append any lists
# that exist # that exist
properties['tags'] = properties.get( properties["tags"] = properties.get("tags", []) + parent_properties.get(
'tags', []) + parent_properties.get('tags', []) "tags", []
)
if parent == self.input_dir: if parent == self.input_dir:
break break
return properties return properties
def _get_directory_properties(self, def _get_directory_properties(self, filepath: Path, include_index_entries=True):
filepath: Path,
include_index_entries=True):
post = { post = {
'title': filepath.name, "title": filepath.name,
'content_after_search': False, "blog": False,
'automatic_index': True, "content_after_search": None,
'search_bar': True, "automatic_index": True,
'tags': [], "search_bar": True,
"tags": [],
} }
if 'readme.md' in [f.name for f in filepath.iterdir()]: if "readme.md" in [f.name for f in filepath.iterdir()]:
with open(filepath.joinpath('readme.md'), with open(filepath.joinpath("readme.md"), encoding="utf-8") as file_pointer:
encoding='utf-8') as file_pointer: for key, val in frontmatter.load(file_pointer).to_dict().items():
for key, val in frontmatter.load(
file_pointer).to_dict().items():
post[key] = val post[key] = val
if 'content' in post.keys(): if post["content_after_search"] is None:
post['content'] = render_markdown(post['content']) post["content_after_search"] = post["blog"]
post['is_dir'] = True if "content" in post.keys():
post["content"] = render_markdown(post["content"])
post["is_dir"] = True
if include_index_entries: if include_index_entries:
post['index_entries'] = self._get_index_entries(filepath) post["index_entries"] = self._get_index_entries(filepath)
return post return post
@@ -139,65 +149,67 @@ class FileMap:
entries = [] entries = []
for path in filepath.iterdir(): for path in filepath.iterdir():
if '.git' in path.parts: if ".git" in path.parts:
continue
if "readme.md" == path.name:
continue continue
if path.is_dir(): if path.is_dir():
entry = self._get_directory_properties( entry = self._get_directory_properties(
path, include_index_entries=False) path, include_index_entries=False
)
else: else:
entry = self._get_file_properties(path) entry = self._get_file_properties(path)
entry['path'] = self._get_output_filepath(path)['web'] entry["path"] = self._get_output_filepath(path)["web"]
entries.append(entry) entries.append(entry)
entries.sort(key=lambda entry: str(entry.get('title', '')).lower()) entries.sort(key=lambda entry: str(entry.get("title", "")).lower())
entries.sort(key=lambda entry: entry['is_dir'], reverse=True) entries.sort(key=lambda entry: entry["is_dir"], reverse=True)
return entries return entries
def _get_file_properties(self, filepath): def _get_file_properties(self, filepath):
post = {'title': filepath.name} post = {"title": filepath.name, "pub_date": False}
if filepath.suffix == '.md': if filepath.suffix == ".md":
with open(filepath, encoding='utf-8') as file_pointer: with open(filepath, encoding="utf-8") as file_pointer:
post = frontmatter.load(file_pointer).to_dict() post = frontmatter.load(file_pointer).to_dict()
# don't store file contents in memory # don't store file contents in memory
if 'content' in post.keys(): if "content" in post.keys():
del post['content'] del post["content"]
post['is_dir'] = False post["is_dir"] = False
return post return post
def _get_output_filepath(self, input_filepath): def _get_output_filepath(self, input_filepath):
def webpath(filepath): def webpath(filepath):
return Path('/notes').joinpath( return Path("/notes").joinpath(filepath.relative_to(self.output_dir))
filepath.relative_to(self.output_dir))
r = {} r = {}
r['raw'] = self.output_dir.joinpath( r["raw"] = self.output_dir.joinpath(input_filepath.relative_to(self.input_dir))
input_filepath.relative_to(self.input_dir)) r["web"] = webpath(r["raw"])
r['web'] = webpath(r['raw'])
if input_filepath.is_dir(): if input_filepath.is_dir():
return r return r
if input_filepath.suffix == '.md': if input_filepath.suffix == ".md":
r['html'] = self.output_dir.joinpath( r["html"] = self.output_dir.joinpath(
input_filepath.relative_to( input_filepath.relative_to(self.input_dir)
self.input_dir)).with_suffix('.html') ).with_suffix(".html")
r['web'] = webpath(r['html']) r["web"] = webpath(r["html"])
elif self.is_plaintext(input_filepath): elif self.is_plaintext(input_filepath):
r['html'] = self.output_dir.joinpath( r["html"] = self.output_dir.joinpath(
input_filepath.relative_to( input_filepath.relative_to(self.input_dir)
self.input_dir)).with_suffix(input_filepath.suffix + ).with_suffix(input_filepath.suffix + ".html")
'.html') r["raw"] = self.output_dir.joinpath(
r['raw'] = self.output_dir.joinpath( input_filepath.relative_to(self.input_dir)
input_filepath.relative_to(self.input_dir)) )
r['web'] = webpath(r['html']) r["web"] = webpath(r["html"])
r["web_raw"] = webpath(r["raw"])
return r return r
@@ -210,57 +222,108 @@ class FileMap:
""" """
r = [] r = []
for _, val in self._map.items(): for _, val in self._map.items():
r.append({ if val["src_path"].name == "readme.md":
'title': val.get('title', ''), continue
'tags': val.get('tags', []),
'path': str(val['dst_path']['web']), r.append(
'is_dir': val['is_dir'] {
}) "title": val.get("title", ""),
"tags": val.get("tags", []),
"path": str(val["dst_path"]["web"]),
"is_dir": val["is_dir"],
}
)
return r return r
def get_uuid_map(self): def get_uuid_map(self):
d = {} d = {}
for _, val in self._map.items(): for _, val in self._map.items():
if 'uuid' not in val.keys(): if "uuid" not in val.keys():
continue continue
d[val['uuid']] = str(val['dst_path']['web'])
if val["uuid"] in d.keys():
if not val["is_dir"]:
# only allow directories to overwrite conflicting UUIDs
# ensures that a readme's folder gets the permalink
continue
d[val["uuid"]] = str(val["dst_path"]["web"])
return d return d
def rfc822_date_sorter_key(date):
if date is None:
ret = 0
else:
ret = int(dt.strptime(date, "%a, %d %b %Y %H:%M:%S %z").timestamp())
return ret
def update_required(src_filepath, output_filepath): def update_required(src_filepath, output_filepath):
""" """
check if file requires an update, check if file requires an update,
return boolean return boolean
""" """
return not output_filepath.exists() or src_filepath.stat( return (
).st_mtime > output_filepath.stat().st_mtimeme() not output_filepath.exists()
or src_filepath.stat().st_mtime > output_filepath.stat().st_mtimeme()
)
def get_args(): def get_args():
""" Get command line arguments """ """Get command line arguments"""
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('notes', type=Path) parser.add_argument("notes", type=Path)
parser.add_argument('-o', '--output-dir', type=Path, default='web') parser.add_argument("-o", "--output-dir", type=Path, default="web")
parser.add_argument( parser.add_argument(
'-F', "-F",
'--force', "--force",
action="store_true", action="store_true",
help= help="Generate new output html even if source file was modified before output html",
"Generate new output html even if source file was modified before output html"
) )
return parser.parse_args() return parser.parse_args()
def render_inline_blog_post(input_filepath):
"""
render markdown file as blog post for inlinining into blog index
returns html
"""
with open(input_filepath, encoding="utf-8") as file_pointer:
content = frontmatter.load(file_pointer).content
properties = FILEMAP.get(input_filepath)
html = render_markdown(content)
html = JINJA_TEMPLATE_BLOG_INLINE_POST.render(
license=LICENSE,
content=html,
lecture_slides=properties.get("lecture_slides"),
lecture_notes=properties.get("lecture_notes"),
uuid=properties.get("uuid"),
tags=properties.get("tags"),
author=properties.get("author"),
title=properties.get("title"),
published=properties.get("pub_date"),
base_url=FILEMAP.get_base_url(),
)
properties["dst_path"]["html"].write_text(html)
return html
def render_markdown_file(input_filepath): def render_markdown_file(input_filepath):
""" """
render markdown file to file render markdown file to file
write markdown file to args.output_dir in html, write markdown file to args.output_dir in html,
return list of tuple of output filepath, frontmatter post return list of tuple of output filepath, frontmatter post
""" """
with open(input_filepath, encoding='utf-8') as file_pointer: with open(input_filepath, encoding="utf-8") as file_pointer:
content = frontmatter.load(file_pointer).content content = frontmatter.load(file_pointer).content
properties = FILEMAP.get(input_filepath) properties = FILEMAP.get(input_filepath)
@@ -274,9 +337,11 @@ def render_markdown_file(input_filepath):
uuid=properties.get("uuid"), uuid=properties.get("uuid"),
tags=properties.get("tags"), tags=properties.get("tags"),
author=properties.get("author"), author=properties.get("author"),
title=properties.get("title")) title=properties.get("title"),
published=properties.get("pub_date"),
)
properties['dst_path']['html'].write_text(html) properties["dst_path"]["html"].write_text(html)
def render_plaintext_file(input_filepath): def render_plaintext_file(input_filepath):
@@ -288,9 +353,14 @@ def render_plaintext_file(input_filepath):
raw_content = input_filepath.read_text() raw_content = input_filepath.read_text()
properties = FILEMAP.get(input_filepath) properties = FILEMAP.get(input_filepath)
html = JINJA_TEMPLATE_TEXTARTICLE.render(license=LICENSE, **properties) html = JINJA_TEMPLATE_TEXTARTICLE.render(
properties['dst_path']['raw'].write_text(raw_content) license=LICENSE,
properties['dst_path']['html'].write_text(html) **properties,
raw_link=properties["dst_path"]["web_raw"],
raw_content=raw_content,
)
properties["dst_path"]["raw"].write_text(raw_content)
properties["dst_path"]["html"].write_text(html)
def render_generic_file(input_filepath): def render_generic_file(input_filepath):
@@ -300,7 +370,7 @@ def render_generic_file(input_filepath):
return list of tuple of output filepath, empty dict return list of tuple of output filepath, empty dict
""" """
properties = FILEMAP.get(input_filepath) properties = FILEMAP.get(input_filepath)
output_filepath = properties['dst_path']['raw'] output_filepath = properties["dst_path"]["raw"]
shutil.copyfile(input_filepath, output_filepath) shutil.copyfile(input_filepath, output_filepath)
@@ -311,7 +381,7 @@ def render_file(input_filepath):
return list of tuples of output filepath, frontmatter post return list of tuples of output filepath, frontmatter post
""" """
if input_filepath.suffix == '.md': if input_filepath.suffix == ".md":
return render_markdown_file(input_filepath) return render_markdown_file(input_filepath)
if FileMap.is_plaintext(input_filepath): if FileMap.is_plaintext(input_filepath):
@@ -326,30 +396,29 @@ def render_markdown(content):
""" """
post_body = { post_body = {
'text': content, "text": content,
'toc-depth': 6, "toc-depth": 6,
'highlight-style': 'pygments', "highlight-style": "pygments",
'html-math-method': 'mathml', "html-math-method": "mathml",
'to': 'html', "to": "html",
'files': { "files": {
'data/data/abbreviations': '', "data/data/abbreviations": "",
}, },
'standalone': False, "standalone": False,
} }
headers = {'Accept': 'application/json'} headers = {"Accept": "application/json"}
response = requests.post(PANDOC_SERVER_URL, response = requests.post(
headers=headers, PANDOC_SERVER_URL, headers=headers, json=post_body, timeout=PANDOC_TIMEOUT
json=post_body, )
timeout=PANDOC_TIMEOUT)
response = response.json() response = response.json()
# TODO look at response['messages'] and log them maybe? # TODO look at response['messages'] and log them maybe?
# https://github.com/jgm/pandoc/blob/main/doc/pandoc-server.md#response # https://github.com/jgm/pandoc/blob/main/doc/pandoc-server.md#response
return response['output'] return response["output"]
def process_home_index(args, notes_git_head_sha1=None): def process_home_index(args, notes_git_head_sha1=None):
@@ -357,22 +426,23 @@ def process_home_index(args, notes_git_head_sha1=None):
create home index.html in output_dir create home index.html in output_dir
""" """
post = {'title': 'gronk', 'content': ''} post = {"title": "gronk", "content": ""}
custom_content_file = args.notes.joinpath('readme.md') custom_content_file = args.notes.joinpath("readme.md")
if custom_content_file.is_file(): if custom_content_file.is_file():
fmpost = frontmatter.loads(custom_content_file.read_text()).to_dict() fmpost = frontmatter.loads(custom_content_file.read_text()).to_dict()
for key, val in fmpost.items(): for key, val in fmpost.items():
post[key] = val post[key] = val
post['content'] = render_markdown(post['content']) post["content"] = render_markdown(post["content"])
html = JINJA_TEMPLATE_HOME_INDEX.render( html = JINJA_TEMPLATE_HOME_INDEX.render(
gronk_commit=GRONK_COMMIT, gronk_commit=GRONK_COMMIT,
search_data=FILEMAP.to_search_data(), search_data=FILEMAP.to_search_data(),
notes_git_head_sha1=notes_git_head_sha1, notes_git_head_sha1=notes_git_head_sha1,
post=post) post=post,
)
args.output_dir.joinpath('index.html').write_text(html) args.output_dir.joinpath("index.html").write_text(html)
def generate_permalink_page(output_dir): def generate_permalink_page(output_dir):
@@ -380,11 +450,15 @@ def generate_permalink_page(output_dir):
create the directory and index.html for redirecting permalinks create the directory and index.html for redirecting permalinks
""" """
dir = output_dir.joinpath('permalink') dir = output_dir.joinpath("permalink")
dir.mkdir(exist_ok=True) dir.mkdir(exist_ok=True)
dir.joinpath('index.html').write_text( dir.joinpath("index.html").write_text(
JINJA_TEMPLATE_PERMALINK.render(gronk_commit=GRONK_COMMIT, JINJA_TEMPLATE_PERMALINK.render(
data=FILEMAP.get_uuid_map())) title="redirecting... | gronk",
gronk_commit=GRONK_COMMIT,
data=FILEMAP.get_uuid_map(),
)
)
def generate_tag_browser(output_dir): def generate_tag_browser(output_dir):
@@ -394,19 +468,19 @@ def generate_tag_browser(output_dir):
tags = {} tags = {}
for post in FILEMAP.to_list(): for post in FILEMAP.to_list():
post['path'] = post['dst_path']['web'] post["path"] = post["dst_path"]["web"]
if 'tags' not in post.keys(): if "tags" not in post.keys():
continue continue
for tag in post['tags']: for tag in post["tags"]:
if tag not in tags.keys(): if tag not in tags.keys():
tags[tag] = [] tags[tag] = []
tags[tag].append(post) tags[tag].append(post)
for tag, index_entries in tags.items(): for tag, index_entries in tags.items():
output_file = output_dir.joinpath(tag, 'index.html') output_file = output_dir.joinpath(tag, "index.html")
output_file.parent.mkdir(exist_ok=True, parents=True) output_file.parent.mkdir(exist_ok=True, parents=True)
output_file.write_text( output_file.write_text(
JINJA_TEMPLATE_INDEX.render( JINJA_TEMPLATE_INDEX.render(
@@ -414,36 +488,47 @@ def generate_tag_browser(output_dir):
automatic_index=True, automatic_index=True,
search_bar=True, search_bar=True,
title=tag, title=tag,
index_entries=[{ index_entries=[
'title': entry.get('title', ''), {
'is_dir': entry.get('is_dir', False), "title": entry.get("title", ""),
'path': str(entry.get('path', Path(''))), "is_dir": entry.get("is_dir", False),
} for entry in index_entries], "path": str(entry.get("path", Path(""))),
)) }
for entry in index_entries
if entry.get("src_path").name != "readme.md"
],
)
)
output_file = output_dir.joinpath('index.html') output_file = output_dir.joinpath("index.html")
output_file.parent.mkdir(exist_ok=True, parents=True) output_file.parent.mkdir(exist_ok=True, parents=True)
output_file.write_text( output_file.write_text(
JINJA_TEMPLATE_INDEX.render(automatic_index=True, JINJA_TEMPLATE_INDEX.render(
gronk_commit=GRONK_COMMIT, automatic_index=True,
search_bar=True, gronk_commit=GRONK_COMMIT,
title='tags', search_bar=True,
index_entries=[{ title="tags",
'path': tag, index_entries=[
'title': tag, {
'is_dir': False, "path": tag,
} for tag in tags.keys()])) "title": tag,
"is_dir": True,
}
for tag in sorted(tags.keys())
],
)
)
def main(args): def main(args):
""" Entry point for script """ """Entry point for script"""
start_time = time.time() start_time = time.time()
global LICENSE global LICENSE
global FILEMAP global FILEMAP
FILEMAP = FileMap(args.notes, args.output_dir.joinpath('notes')) FILEMAP = FileMap(args.notes, args.output_dir.joinpath("notes"))
# TODO have some sort of 'site rebuild in progress - come back in a minute # TODO have some sort of 'site rebuild in progress - come back in a minute
# or two!' or auto checking/refreshing page for when site is being built # or two!' or auto checking/refreshing page for when site is being built
@@ -462,52 +547,91 @@ def main(args):
for root_str, _, files in os.walk(args.notes): for root_str, _, files in os.walk(args.notes):
root = Path(root_str) root = Path(root_str)
if '.git' in root.parts: if ".git" in root.parts:
continue continue
root_properties = FILEMAP.get(root) root_properties = FILEMAP.get(root)
root_properties['dst_path']['raw'].mkdir(parents=True, exist_ok=True) root_properties["dst_path"]["raw"].mkdir(parents=True, exist_ok=True)
#pprint.pprint(root_properties) posts = []
html = JINJA_TEMPLATE_INDEX.render( if root_properties["blog"]:
for file in files:
props = FILEMAP.get(root.joinpath(file))
post = {
"title": props["title"],
"link": props["dst_path"]["web"],
"pub_date": props.get("pub_date"),
"description": render_inline_blog_post(root.joinpath(file)),
}
posts.append(post)
posts.sort(
key=lambda p: rfc822_date_sorter_key(p.get("pub_date")), reverse=True
)
# render rss feed
rss = JINJA_TEMPLATE_BLOG_FEED.render(
title=root_properties.get("title", ""),
description=root_properties.get("content", ""),
base_url=FILEMAP.get_base_url(),
link=f"{FILEMAP.get_base_url()}{root_properties['dst_path']['web']}",
language="en-GB",
posts=posts,
)
root_properties["dst_path"]["raw"].joinpath("feed.xml").write_text(rss)
root_properties["dst_path"]["raw"].joinpath("rss.xml").write_text(rss)
# pprint.pprint(root_properties)
# render index
html = (
JINJA_TEMPLATE_BLOGINDEX
if root_properties["blog"]
else JINJA_TEMPLATE_INDEX
).render(
gronk_commit=GRONK_COMMIT, gronk_commit=GRONK_COMMIT,
title=root_properties.get('title', ''), title=root_properties.get("title", ""),
content=root_properties.get('content', ''), content=root_properties.get("content", ""),
content_after_search=root_properties['content_after_search'], content_after_search=root_properties["content_after_search"],
automatic_index=root_properties['automatic_index'], automatic_index=root_properties["automatic_index"],
search_bar=root_properties['search_bar'], search_bar=root_properties["search_bar"],
index_entries=[{ posts=posts,
'title': entry.get('title', ''), uuid=root_properties.get("uuid"),
'is_dir': entry.get('is_dir', False), index_entries=[
'path': str(entry.get('path', Path(''))), {
} for entry in root_properties.get('index_entries', '')], "title": entry.get("title", ""),
"is_dir": entry.get("is_dir", False),
"path": str(entry.get("path", Path(""))),
}
for entry in root_properties.get("index_entries", "")
],
) )
root_properties['dst_path']['raw'].joinpath('index.html').write_text( root_properties["dst_path"]["raw"].joinpath("index.html").write_text(html)
html)
# render each file # render each file
for file in files: for file in files:
# don't render readme.md as index as it is used for directory
if file == "readme.md":
continue
render_file(root.joinpath(file)) render_file(root.joinpath(file))
process_home_index(args) process_home_index(args)
# copy styling and js scripts necessary for function # copy styling and js scripts necessary for function
shutil.copytree(GRONK_CSS_DIR, shutil.copytree(GRONK_CSS_DIR, args.output_dir.joinpath("css"), dirs_exist_ok=True)
args.output_dir.joinpath('css'), shutil.copytree(GRONK_JS_DIR, args.output_dir.joinpath("js"), dirs_exist_ok=True)
dirs_exist_ok=True)
shutil.copytree(GRONK_JS_DIR,
args.output_dir.joinpath('js'),
dirs_exist_ok=True)
generate_tag_browser(args.output_dir.joinpath('tags')) generate_tag_browser(args.output_dir.joinpath("tags"))
generate_permalink_page(args.output_dir) generate_permalink_page(args.output_dir)
elapsed_time = time.time() - start_time elapsed_time = time.time() - start_time
print(f"generated notes {elapsed_time=}") print(f"generated notes {elapsed_time=}")
with open(args.output_dir.joinpath("fart.json"), "w+") as fp:
json.dump(
{
k: {prop: str(propval) for prop, propval in v.items()}
for k, v in FILEMAP._map.items()
},
fp,
)
return 0 return 0
@@ -517,14 +641,13 @@ def start_pandoc_server():
successful and return version as string successful and return version as string
""" """
start_time = time.time() start_time = time.time()
process = subprocess.Popen(["/usr/bin/pandoc-server"], process = subprocess.Popen(["/usr/bin/pandoc-server"], stdout=subprocess.PIPE)
stdout=subprocess.PIPE)
version = None version = None
while True: while True:
try: try:
resp = requests.get(f"{PANDOC_SERVER_URL}/version") resp = requests.get(f"{PANDOC_SERVER_URL}/version")
version = resp.content.decode('utf-8') version = resp.content.decode("utf-8")
break break
except requests.ConnectionError: except requests.ConnectionError:
time.sleep(0.1) time.sleep(0.1)
@@ -541,7 +664,7 @@ def start_pandoc_server():
# TODO implement useful logging and debug printing # TODO implement useful logging and debug printing
if __name__ == '__main__': if __name__ == "__main__":
pandoc_process = start_pandoc_server() pandoc_process = start_pandoc_server()
try: try:

View File

@@ -6,7 +6,7 @@ const TITLE = "title"
const SEARCH_TIMEOUT_MS = 100 const SEARCH_TIMEOUT_MS = 100
var SEARCH_TIMEOUT_ID = -1 var SEARCH_TIMEOUT_ID = -1
const fuse = new Fuse(data, { const fuse = new Fuse(search_data, {
keys: [ 'title' ], keys: [ 'title' ],
ignoreLocation: true, ignoreLocation: true,
threshhold: 0.4, threshhold: 0.4,
@@ -30,7 +30,7 @@ function updateResults() {
console.log("updating results") console.log("updating results")
resultsDiv.innerHTML = '' resultsDiv.innerHTML = ''
if (searchBar.value) results = fuse.search(searchBar.value, { limit: RESULTS_MAX }).map(r => r.item) if (searchBar.value) results = fuse.search(searchBar.value, { limit: RESULTS_MAX }).map(r => r.item)
else results = data else results = search_data
results.forEach(r => { results.forEach(r => {
wrapper = document.createElement('li') wrapper = document.createElement('li')

View File

@@ -1,11 +1,11 @@
# gronk # :beaver: gronk
View your notes as a static html site. Browse a live sample of it [here](https://notes.alv.cx). Formerly notes2web.
View your notes as a static HTML site. Powers [notes.alv.cx](https://notes.alv.cx).
![](./screenshot.png) ![](./screenshot.png)
Tested with [pandoc v2.19.2](https://github.com/jgm/pandoc/releases/tag/2.19.2).
## Why? ## Why?
@@ -57,13 +57,14 @@ To add custom content to a directory index, put it in a file called `readme.md`
You can set the following frontmatter variables to customise the directory index of a directory: You can set the following frontmatter variables to customise the directory index of a directory:
| variable | default value | description | | variable | default value | description |
|------------------------|-------------------|--------------------------------------------------------------------------------------------| |------------------------|----------------|--------------------------------------------------------------------------------------------|
| `tags` | `[]` | list of tags, used by search and inherited by any notes and subdirectories | | `blog` | `false` | enable [blog mode](#blog-mode) for this directory |
| `uuid` | none | unique id to reference directory, used for permalinking | | `tags` | `[]` | list of tags, used by search and inherited by any notes and subdirectories |
| `content_after_search` | `false` | show custom content in `readme.md` after search bar and directory index | | `uuid` | none | unique id to reference directory, used for permalinking |
| `automatic_index` | `true` | show the automatically generated directory index. required for search bar to function. | | `content_after_search` | same as `blog` | show custom content in `readme.md` after search bar and directory index |
| `search_bar` | `true` | show search bar to search directory items. requires `automatic_index` (enabled by default) | | `automatic_index` | `true` | show the automatically generated directory index. required for search bar to function. |
| `search_bar` | `true` | show search bar to search directory items. requires `automatic_index` (enabled by default) |
## Notes Metadata ## Notes Metadata
@@ -73,12 +74,31 @@ gronk reads the following YAML [frontmatter](https://jekyllrb.com/docs/front-mat
| variable | description | | variable | description |
|------------------|---------------------------------------------------------------------------------------| |------------------|---------------------------------------------------------------------------------------|
| `author` | The person(s) who wrote the article | | `author` | The person(s) who wrote the article |
| `pub_date` | (for blog mode) set the publish date of an article/post/note (MUST be RFC822 format) |
| `tags` | A YAML list of tags which the article relates to - this is used for browsing and also | | `tags` | A YAML list of tags which the article relates to - this is used for browsing and also |
| `title` | The title of the article | | `title` | The title of the article |
| `uuid` | A unique identifier used for permalinks. | | `uuid` | A unique identifier used for permalinks. |
| `lecture_slides` | a list of paths pointing to lecture slides used while taking notes | | `lecture_slides` | a list of paths pointing to lecture slides used while taking notes |
| `lecture_notes` | a list of paths pointing to other notes used while taking notes | | `lecture_notes` | a list of paths pointing to other notes used while taking notes |
## Blog Mode
A directory can be turned into a blog by enabling blog mode.
This can be done by setting the `blog` variable to `true` in the `readme.md` [custom directory metadata](#custom-directory-index-and-metadata).
Notes under this directory will be published to a blog, whose feed is accesible at `https://notes.alv.cx/notes/<directory..>/feed.xml`.
When blog mode is enabled, it is required that the `base_url` property is set in the top level `readme.md` file.
Note that there should be no trailing slash.
If a `readme.md` file does not exist, then an empty one can be created:
```md
---
base_url: https://notes.alv.cx
---
```
## Permalinks ## Permalinks
Permalinks are currently rather basic and requires JavaScript to be enabled on the local computer. Permalinks are currently rather basic and requires JavaScript to be enabled on the local computer.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 172 KiB

View File

@@ -32,6 +32,10 @@
<p class="smallText metadata"> uuid: {{ uuid }} (<a href="/permalink?uuid={{ uuid }}">permalink</a>) </p> <p class="smallText metadata"> uuid: {{ uuid }} (<a href="/permalink?uuid={{ uuid }}">permalink</a>) </p>
{% endif %} {% endif %}
{% if published %}
<p class="smallText metadata"> published: {{ published }} </p>
{% endif %}
<p class="smallText metadata"> tags: [ <p class="smallText metadata"> tags: [
{% for tag in tags %} {% for tag in tags %}
<a href="/tags/{{ tag }}">{{ tag }}</a>{% if loop.nextitem %},{% endif %} <a href="/tags/{{ tag }}">{{ tag }}</a>{% if loop.nextitem %},{% endif %}
@@ -49,7 +53,7 @@ written by: [ {% for auth in author %}{{ auth }}{% if loop.nextitem %}, {% endif
colors colors
</p> </p>
<p class="smallText metadata"> <p class="smallText metadata">
page generated by <a href="https://git.alv.cx/alvierahman90/gronk">gronk</a> page rendered by <a href="https://git.alv.cx/alvierahman90/gronk">gronk</a>
</p> </p>
{% if license %} {% if license %}
<details id="license"> <details id="license">

View File

@@ -13,6 +13,6 @@
<div id="content"> <div id="content">
{% block content %} {% block content %}
{% endblock %} {% endblock %}
<p class="smallText"> page generated by <a href="https://github.com/alvierahman90/gronk">gronk</a> <!--(commit {{ gronk_commit }}) {% if notes_git_head_sha1 %}notes commit {{ notes_git_head_sha1 }}{% endif %}--></p> <p class="smallText"> page rendered by <a href="https://github.com/alvierahman90/gronk">gronk</a> <!--(commit {{ gronk_commit }}) {% if notes_git_head_sha1 %}notes commit {{ notes_git_head_sha1 }}{% endif %}--></p>
</div> </div>
</body> </body>

83
templates/blog_index.html Normal file
View File

@@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block head %}
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script>
MathJax = {
tex: {
tags: 'ams'
}
}
</script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
{% endblock %}
{% block content %}
<p class="smallText metadata">
syntax highlighting based on <a href="https://pygments.org/">Pygments'</a> default
colors
</p>
<p class="smallText metadata">
page rendered by <a href="https://git.alv.cx/alvierahman90/gronk">gronk</a>
</p>
<p class="smallText metadata">
<a href="./feed.xml">rss feed</a>
</p>
{% if license %}
<details id="license">
<summary class="smallText">
License
</summary>
<pre>{{ license }}</pre>
</details>
{% endif %}
{% if not content %}
<h1>{{ title }}</h1>
{% endif %}
{% if not content_after_search %}
{{ content|safe }}
{% for post in posts %}
{{ post['description']|safe }}
{% endfor %}
{% endif %}
{% if automatic_index %}
<details>
<summary>
<h4> List of posts </h4>
</summary>
{% if search_bar %}
<div id="searchWrapper">
<input id="search" placeholder="search" autocomplete="off" autofocus>
</div>
<p class="searchSmallText" style="margin-top: 0; text-align: center">
Press (<kbd>Shift</kbd>+)<kbd>Enter</kbd> to open first result (in new tab)
</p>
{% endif %}
<ul id="searchResults" class="buttonlist">
<li class="article"><a href=".."><p>../</p></a></li>
{% for entry in index_entries %}
<li class="article"><a href="{{ entry['path'] }}"><p>{{ entry['title'] }}{% if entry['is_dir'] %}/{% endif %}</p></a></li>
{% endfor %}
</ul>
</details>
{% endif %}
{% if content_after_search %}
{{ content|safe }}
{% for post in posts %}
<div class="blog_inline_post">
{{ post['description']|safe }}
</div>
{% endfor %}
{% endif %}
<script src="/js/fuse.js"> </script>
<script> const search_data = {{ index_entries|tojson }} </script>
<script src="/js/indexsearch.js"> </script>
{% endblock %}

View File

@@ -0,0 +1,42 @@
{% block content %}
<p class="smallText metadata"> title: {{ title }} </p>
{% if lecture_slides %}
<p class="smallText metadata"> lecture_slides: [
{% for slide in lecture_slides %}
<a href="{{ base_url}}{{ slide }}">{{ slide }}</a>{% if loop.nextitem %},{% endif %}
{% endfor %}
]</p>
{% endif %}
{% if lecture_notes %}
<p class="smallText metadata"> lecture_notes: [
{% for note in lecture_notes %}
<a href="{{ base_url }}{{ note }}">{{ note }}</a>{% if loop.nextitem %},{% endif %}
{% endfor %}
]</p>
{% endif %}
{% if uuid %}
<p class="smallText metadata"> uuid: {{ uuid }} (<a href="{{ base_url }}/permalink?uuid={{ uuid }}">permalink</a>) </p>
{% endif %}
{% if published %}
<p class="smallText metadata"> published: {{ published }} </p>
{% endif %}
<p class="smallText metadata"> tags: [
{% for tag in tags %}
<a href="{{ base_url }}/tags/{{ tag }}">{{ tag }}</a>{% if loop.nextitem %},{% endif %}
{% endfor %}
]</p>
<p class="smallText metadata">
{% if author is string %}
written by: {{ author }}
{% elif author is iterable %}
written by: [ {% for auth in author %}{{ auth }}{% if loop.nextitem %}, {% endif %}{% endfor %} ]
{% endif %}
</p>
{% block body_content %}
{{ content|safe }}
{% endblock %}
{% endblock %}

View File

@@ -1,6 +1,12 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
{% if not content %}
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
{% endif %}
{% if uuid %}
<p class="smallText metadata"> uuid: {{ uuid }} (<a href="/permalink?uuid={{ uuid }}">permalink</a>) </p>
{% endif %}
{% if not content_after_search %} {% if not content_after_search %}
{{ content|safe }} {{ content|safe }}

View File

@@ -1,10 +1,11 @@
{% extends "base.html" %}
{% block content %} {% block content %}
<p> <p>
You should be being redirected... You should be being redirected...
Otherwise, click <a id="manual_redirect">here</a>. Otherwise, click <a id="manual_redirect">here</a>.
</p> </p>
<p class="smallText"> page generated by <a href="https://github.com/alvierahman90/gronk">gronk</a></p> <p class="smallText"><a href="https://github.com/alvierahman90/gronk">gronk</a></p>
<script> const data = {{ data|tojson }} </script> <script> const data = {{ data|tojson }} </script>
<script src="/js/permalink.js"> </script> <script src="/js/permalink.js"> </script>
{% endblock %} {% endblock %}

23
templates/rss.xml Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ title }}</title>
<description>{{ description }}</description>
<language>{{ language }}</language>
<link>{{ link }}</link>
<atom:link href="{{ link }}/feed.xml" rel="self" type="application/rss+xml" />
{% for post in posts %}
<item>
<title>{{ post['title']}}</title>
<guid>{{ base_url }}{{ post['link'] }}</guid>
{% if post['pub_date'] %}
<pubDate>{{ post['pub_date'] }}</pubDate>
{% endif %}
<description><![CDATA[{{ post['description']|safe }}]]></description>
</item>
{% endfor %}
</channel>
</rss>