73 Commits

Author SHA1 Message Date
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
0dd69c3fe3 fix requirements rebase errror 2024-01-02 18:29:02 +00:00
95003803eb run as non priveleged user in docker 2024-01-02 18:28:35 +00:00
fddbbf4c50 use readme.md instead of index.md 2024-01-02 18:24:33 +00:00
8b4765e9fb remove commit gronk commit information for now 2024-01-02 18:24:33 +00:00
d83611f12a dockerize 2024-01-02 18:24:27 +00:00
1eca81cd1d correct env variable names, jinja template loader 2024-01-02 18:23:26 +00:00
91a57aa8c9 check if pandoc-server fails to start 2024-01-02 18:23:26 +00:00
3a9cc4c0f8 measure and print how long notes generation takes 2024-01-02 18:23:26 +00:00
44bea9c982 check for pandoc start instead of wait 2024-01-02 18:23:26 +00:00
5c8e1e5253 don't include .git directories in index listings 2024-01-02 18:23:26 +00:00
451bc567e2 update readme 2024-01-02 18:23:26 +00:00
80dce5d4ed start pandoc web server inside python process 2024-01-02 18:23:26 +00:00
48f64bf3d4 render index pages properly 2024-01-02 18:23:26 +00:00
4e5bd41953 generate permalink page 2024-01-02 18:23:26 +00:00
de3d758005 render articles with jinja 2024-01-02 18:23:25 +00:00
444c777135 clean up code 2024-01-02 18:23:25 +00:00
9c6b2247a7 update environment variable names 2024-01-02 18:23:25 +00:00
09334831f7 formatting 2024-01-02 18:23:25 +00:00
af5bef0125 update requirements 2024-01-02 18:23:18 +00:00
b4aa431f4d update templates 2024-01-02 18:22:57 +00:00
49a215a587 make gronk.py single file 2024-01-02 18:22:57 +00:00
0a0bd542fc mark script as executable once copied 2024-01-02 18:22:56 +00:00
36f40f3269 update gronk_add_uuid.py 2024-01-02 18:22:56 +00:00
b61f1ba3b2 minor readme updates 2024-01-02 18:22:56 +00:00
bef8aeda52 rename to gronk 2024-01-02 18:22:56 +00:00
5bb40a57d1 add tag browser generation 2024-01-02 18:22:56 +00:00
29529cfd6a begin rewrite 2024-01-02 18:22:15 +00:00
8bd9173847 update makefile to install python packages, apply executable perms to script after install 2023-10-15 16:07:10 +01:00
dc4560c232 update readme to include other dependecies 2023-10-15 16:03:33 +01:00
da0d9db113 downgrade pyyaml 5.4.1 -> 5.3.1 (workaround)
https://github.com/yaml/pyyaml/issues/601
2023-10-15 16:01:23 +01:00
98a6649a39 add search bar to all browsing menus 2023-02-16 17:20:17 +00:00
0ae70aa2b3 lecture_slides, lecture_notes metadata support 2023-02-09 13:41:16 +00:00
a91f5dcdfc use --short flag on git rev-parse 2023-02-06 14:42:37 +00:00
df8febd3cf put versions and commits on landing page 2023-02-06 14:39:10 +00:00
32a32b873e sort tags in articles 2023-02-06 14:04:45 +00:00
82e5c3e254 minor metadata presentation change 2023-02-06 13:49:18 +00:00
c8e8ecd76c add title to metadata section 2023-02-06 13:46:10 +00:00
45e9e9a95e update gitignore 2023-02-06 13:43:11 +00:00
48268770b7 add timer to search to reduce cpu usage 2023-02-06 13:42:45 +00:00
79f5acd020 update metadata styling 2023-02-06 13:41:23 +00:00
a10bca718b change fuse options to improve search 2023-02-06 13:02:00 +00:00
18ea2a3b04 remove table of contents 2023-02-06 13:01:13 +00:00
8cc14c8a3e disable input suggestions in search bar 2023-02-06 12:37:22 +00:00
a17b475a4b fix duplicate tags on site pages 2023-02-06 12:14:13 +00:00
03fff83930 update search key weightings 2023-02-06 11:51:27 +00:00
2a35e967f0 tag deduplication 2023-02-06 11:50:58 +00:00
8ba82c2911 hide toc on smaller width devices (for now) 2022-11-11 12:36:54 +00:00
b050b36747 show non-plaintext files 2022-11-11 12:23:21 +00:00
8e09404520 tag inheritance 2022-11-11 12:06:46 +00:00
1c4dad1b2e update readme 2022-11-10 16:46:59 +00:00
9ccf00e78a use amsmath style equation tagging with mathjax 2022-11-10 16:36:22 +00:00
59eccec7c4 update search weights 2022-05-07 19:50:35 +01:00
adc2a36fc2 only show horizontal scroll for equations which are too long 2022-05-03 19:19:36 +01:00
3368bdb999 update styles to better use styles.alv.cx 2022-04-14 19:35:15 +01:00
8f5d1a1a3b update to new colors styling location 2022-04-13 17:33:36 +01:00
b6e69f5596 prepare for new styling update 2022-04-13 17:05:38 +01:00
bbb49d7a34 dark mode 2022-04-12 17:57:28 +01:00
35 changed files with 1281 additions and 659 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
.git
.env
env
**/__pycache__

3
.gitignore vendored
View File

@@ -1,4 +1,7 @@
*.swp *.swp
.env
env env
n n
web web
planning.txt
**/__pycache__

31
Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
FROM python:3.12-bookworm
ARG ARCH=
ENV BUILDARCH=${ARCH}
ENV GRONK_CSS_DIR=./css
ENV GRONK_JS_DIR=./js
ENV GRONK_TEMPLATES_DIR=./templates
WORKDIR /usr/src/app
RUN mkdir /notes
RUN mkdir /web
VOLUME /usr/src/app/notes
VOLUME /usr/src/app/web
RUN apt-get update \
&& wget -O ./pandoc.deb https://github.com/jgm/pandoc/releases/download/3.1.11/pandoc-3.1.11-1-${BUILDARCH}.deb \
&& apt install -y -f ./pandoc.deb \
&& rm ./pandoc.deb
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
RUN useradd -Ms /bin/nologin user
USER user
COPY . .
CMD [ "python3", "-u", "gronk.py", "--output-dir", "./web", "./notes" ]

View File

@@ -1,13 +1,11 @@
install: install:
cp notes2web.py n2w_add_uuid.py /usr/local/bin cp gronk_add_uuid.py /usr/local/bin
sed "s/GRONK_COMMIT = \"dev\"/GRONK_COMMIT = \"$$(git rev-parse --short HEAD)\"/" gronk.py > /usr/local/bin/gronk.py
chmod +x /usr/local/bin/gronk.py
chmod +x /usr/local/bin/gronk_add_uuid.py
mkdir -p /opt/gronk
cp -r templates js css /opt/gronk
pip3 install -r requirements.txt pip3 install -r requirements.txt
mkdir -p /opt/notes2web
cp -r templates /opt/notes2web
cp styles.css /opt/notes2web
cp fuse.js /opt/notes2web
cp search.js /opt/notes2web
cp toc_search.js /opt/notes2web
cp permalink.js /opt/notes2web
uninstall: uninstall:
rm -rf /usr/local/bin/notes2web.py /usr/local/bin/n2w_add_uuid.py /opt/notes2web rm -rf /usr/local/bin/gronk.py /usr/local/bin/gronk_add_uuid.py /opt/gronk

View File

@@ -1,6 +1,9 @@
@import url("https://styles.alv.cx/colors/gruvbox.css");
@import url("https://styles.alv.cx/base.css"); @import url("https://styles.alv.cx/base.css");
@import url("https://styles.alv.cx/modules/search.css"); @import url("https://styles.alv.cx/modules/search.css");
@import url("https://styles.alv.cx/modules/buttonlist.css"); @import url("https://styles.alv.cx/modules/buttonlist.css");
@import url("https://styles.alv.cx/modules/darkmode.css");
@import url("/notes/styles.css");
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
@@ -29,10 +32,10 @@ body {
} }
.matchHighlight { .matchHighlight {
background-color: #86c1b9; background-color: var(--blue);
} }
#sidebar > #header { padding-bottom: 1em; color: #656565;} #sidebar > #header { padding-bottom: 1em; color: var(--fg-lc);}
#header > * { #header > * {
margin: 0; margin: 0;
@@ -71,12 +74,12 @@ body {
#sidebar, #sidebar,
#toc li > a { #toc li > a {
color: #656565; color: var(--fg-lc);
} }
#toc li, ul#articlelist { list-style: none; } #toc li, ul#articlelist { list-style: none; }
#toc ul, ul#articlelist { margin-left: 0.75em ; padding-left: 0.75em; } #toc ul, ul#articlelist { margin-left: 0.75em ; padding-left: 0.75em; }
#toc ul, ul#articlelist { border-left: 1px solid #b3b3b3;} #toc ul, ul#articlelist { border-left: 1px solid var(--fg-lc);}
#toc > ul { padding; none; padding: 0; margin: 0; border: none;max-width: 100%;} #toc > ul { padding; none; padding: 0; margin: 0; border: none;max-width: 100%;}
li { padding: 0 !important; } li { padding: 0 !important; }
@@ -91,18 +94,27 @@ ul#articlelist > a{
} }
#toc li > a:hover, #toc li > a:hover,
ul#articlelist li a:hover { ul#articlelist li a:hover {
background: #d9d9d9; background: var(--bg-lc);
color: black; color: black;
} }
mjx-container { mjx-container {
overflow-x: scroll; overflow-x: auto;
} }
@media (max-width: 60em) { 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) {
/* 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 }
#sidebar { position: static; width: 100%; max-width: none; } #sidebar { position: static; width: 100%; max-width: none; height: auto; }
#tocWrapper { display: none }
} }

11
docker-compose.yml Normal file
View File

@@ -0,0 +1,11 @@
version: '3'
services:
gronk:
build:
context: '.'
args:
ARCH: ${ARCH}
volumes:
- '${SOURCE}:/usr/src/app/notes'
- '${OUTPUT}:/usr/src/app/web'

640
gronk.py Executable file
View File

@@ -0,0 +1,640 @@
#!/usr/bin/env python3
"""
gronk --- view your notes as a static html site
"""
import argparse
import os
from pathlib import Path
import shutil
import sys
import subprocess
import copy
import time
import magic
import regex as re
import pprint
from datetime import datetime as dt
import frontmatter
import jinja2
import requests
GRONK_COMMIT = "dev"
PANDOC_SERVER_URL = os.getenv("PANDOC_SERVER_URL", r"http://localhost:3030/")
PANDOC_TIMEOUT = int(os.getenv("PANDOC_TIMEOUT", "120"))
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_TEMPLATES_DIR = Path(
os.getenv("GRONK_TEMPLATES_DIR", "/opt/gronk/templates/"))
JINJA_ENV = jinja2.Environment(
loader=jinja2.FileSystemLoader(searchpath=GRONK_TEMPLATES_DIR),
autoescape=jinja2.select_autoescape)
JINJA_TEMPLATE_TEXTARTICLE = JINJA_ENV.get_template("article-text.html")
JINJA_TEMPLATE_HOME_INDEX = JINJA_ENV.get_template("home.html")
JINJA_TEMPLATE_INDEX = JINJA_ENV.get_template("index.html")
JINJA_TEMPLATE_ARTICLE = JINJA_ENV.get_template("article.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
FILEMAP = None
class FileMap:
"""
this class is used to read file properties, inherit properties,
and have a centralised place to access them
"""
def __init__(self, input_dir, output_dir):
self._map = {}
self.input_dir = Path(input_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
def _path_to_key(path):
return str(path)
@staticmethod
def is_plaintext(filename):
return re.match(r'^text/', magic.from_file(str(filename),
mime=True)) is not None
def add(self, filepath):
filepath = Path(filepath)
if filepath.is_dir():
properties = self._get_directory_properties(filepath)
else:
properties = self._get_file_properties(filepath)
properties['src_path'] = filepath
properties['dst_path'] = self._get_output_filepath(filepath)
self._map[self._path_to_key(filepath)] = properties
def get(self, filepath, default=None, raw=False):
"""
get the properties of a file at a filepath
raw=True to not inherit properties
"""
# TODO maybe store properties of a file once it's in built and mark it
# as built? might save time but also cba
if self._path_to_key(filepath) not in self._map.keys():
self.add(filepath)
properties = copy.deepcopy(
self._map.get(self._path_to_key(filepath), default))
if raw:
return properties
parent = filepath
while True:
parent = parent.parent
if parent == Path('.'):
break
parent_properties = self.get(parent, raw=True)
# TODO inherit any property that isn't defined, append any lists
# that exist
properties['tags'] = properties.get(
'tags', []) + parent_properties.get('tags', [])
if parent == self.input_dir:
break
return properties
def _get_directory_properties(self,
filepath: Path,
include_index_entries=True):
post = {
'title': filepath.name,
'blog': False,
'content_after_search': None,
'automatic_index': True,
'search_bar': True,
'tags': [],
}
if 'readme.md' in [f.name for f in filepath.iterdir()]:
with open(filepath.joinpath('readme.md'),
encoding='utf-8') as file_pointer:
for key, val in frontmatter.load(
file_pointer).to_dict().items():
post[key] = val
if post['content_after_search'] is None:
post['content_after_search'] = post['blog']
if 'content' in post.keys():
post['content'] = render_markdown(post['content'])
post['is_dir'] = True
if include_index_entries:
post['index_entries'] = self._get_index_entries(filepath)
return post
def _get_index_entries(self, filepath):
entries = []
for path in filepath.iterdir():
if '.git' in path.parts:
continue
if 'readme.md' == path.name:
continue
if path.is_dir():
entry = self._get_directory_properties(
path, include_index_entries=False)
else:
entry = self._get_file_properties(path)
entry['path'] = self._get_output_filepath(path)['web']
entries.append(entry)
entries.sort(key=lambda entry: str(entry.get('title', '')).lower())
entries.sort(key=lambda entry: entry['is_dir'], reverse=True)
return entries
def _get_file_properties(self, filepath):
post = {'title': filepath.name, 'pub_date': False}
if filepath.suffix == '.md':
with open(filepath, encoding='utf-8') as file_pointer:
post = frontmatter.load(file_pointer).to_dict()
# don't store file contents in memory
if 'content' in post.keys():
del post['content']
post['is_dir'] = False
return post
def _get_output_filepath(self, input_filepath):
def webpath(filepath):
return Path('/notes').joinpath(
filepath.relative_to(self.output_dir))
r = {}
r['raw'] = self.output_dir.joinpath(
input_filepath.relative_to(self.input_dir))
r['web'] = webpath(r['raw'])
if input_filepath.is_dir():
return r
if input_filepath.suffix == '.md':
r['html'] = self.output_dir.joinpath(
input_filepath.relative_to(
self.input_dir)).with_suffix('.html')
r['web'] = webpath(r['html'])
elif self.is_plaintext(input_filepath):
r['html'] = self.output_dir.joinpath(
input_filepath.relative_to(
self.input_dir)).with_suffix(input_filepath.suffix +
'.html')
r['raw'] = self.output_dir.joinpath(
input_filepath.relative_to(self.input_dir))
r['web'] = webpath(r['html'])
r['web_raw'] = webpath(r['raw'])
return r
def to_list(self):
return [val for _, val in self._map.items()]
def to_search_data(self):
"""
returns list of every file in map
"""
r = []
for _, val in self._map.items():
r.append({
'title': val.get('title', ''),
'tags': val.get('tags', []),
'path': str(val['dst_path']['web']),
'is_dir': val['is_dir']
})
return r
def get_uuid_map(self):
d = {}
for _, val in self._map.items():
if 'uuid' not in val.keys():
continue
d[val['uuid']] = str(val['dst_path']['web'])
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):
"""
check if file requires an update,
return boolean
"""
return not output_filepath.exists() or src_filepath.stat(
).st_mtime > output_filepath.stat().st_mtimeme()
def get_args():
""" Get command line arguments """
parser = argparse.ArgumentParser()
parser.add_argument('notes', type=Path)
parser.add_argument('-o', '--output-dir', type=Path, default='web')
parser.add_argument(
'-F',
'--force',
action="store_true",
help=
"Generate new output html even if source file was modified before output html"
)
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):
"""
render markdown file to file
write markdown file to args.output_dir in html,
return list of tuple of output filepath, frontmatter post
"""
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_ARTICLE.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")
)
properties['dst_path']['html'].write_text(html)
def render_plaintext_file(input_filepath):
"""
render plaintext file to file
copy plaintext file into a html preview, copy raw to output dir
return list of tuple of output filepath, empty dict
"""
raw_content = input_filepath.read_text()
properties = FILEMAP.get(input_filepath)
html = JINJA_TEMPLATE_TEXTARTICLE.render(
license=LICENSE,
**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):
"""
render generic file to file
copy generic file into to output_dir
return list of tuple of output filepath, empty dict
"""
properties = FILEMAP.get(input_filepath)
output_filepath = properties['dst_path']['raw']
shutil.copyfile(input_filepath, output_filepath)
def render_file(input_filepath):
"""
render any file by detecting type and applying appropriate type
write input_filepath to correct file in args.output_dir in appropriate formats,
return list of tuples of output filepath, frontmatter post
"""
if input_filepath.suffix == '.md':
return render_markdown_file(input_filepath)
if FileMap.is_plaintext(input_filepath):
return render_plaintext_file(input_filepath)
return render_generic_file(input_filepath)
def render_markdown(content):
"""
render markdown to html
"""
post_body = {
'text': content,
'toc-depth': 6,
'highlight-style': 'pygments',
'html-math-method': 'mathml',
'to': 'html',
'files': {
'data/data/abbreviations': '',
},
'standalone': False,
}
headers = {'Accept': 'application/json'}
response = requests.post(PANDOC_SERVER_URL,
headers=headers,
json=post_body,
timeout=PANDOC_TIMEOUT)
response = response.json()
# TODO look at response['messages'] and log them maybe?
# https://github.com/jgm/pandoc/blob/main/doc/pandoc-server.md#response
return response['output']
def process_home_index(args, notes_git_head_sha1=None):
"""
create home index.html in output_dir
"""
post = {'title': 'gronk', 'content': ''}
custom_content_file = args.notes.joinpath('readme.md')
if custom_content_file.is_file():
fmpost = frontmatter.loads(custom_content_file.read_text()).to_dict()
for key, val in fmpost.items():
post[key] = val
post['content'] = render_markdown(post['content'])
html = JINJA_TEMPLATE_HOME_INDEX.render(
gronk_commit=GRONK_COMMIT,
search_data=FILEMAP.to_search_data(),
notes_git_head_sha1=notes_git_head_sha1,
post=post)
args.output_dir.joinpath('index.html').write_text(html)
def generate_permalink_page(output_dir):
"""
create the directory and index.html for redirecting permalinks
"""
dir = output_dir.joinpath('permalink')
dir.mkdir(exist_ok=True)
dir.joinpath('index.html').write_text(
JINJA_TEMPLATE_PERMALINK.render(title="redirecting... | gronk",
gronk_commit=GRONK_COMMIT,
data=FILEMAP.get_uuid_map()))
def generate_tag_browser(output_dir):
"""
generate a directory that lets you groub by and browse by any given tag. e.g. tags, authors
"""
tags = {}
for post in FILEMAP.to_list():
post['path'] = post['dst_path']['web']
if 'tags' not in post.keys():
continue
for tag in post['tags']:
if tag not in tags.keys():
tags[tag] = []
tags[tag].append(post)
for tag, index_entries in tags.items():
output_file = output_dir.joinpath(tag, 'index.html')
output_file.parent.mkdir(exist_ok=True, parents=True)
output_file.write_text(
JINJA_TEMPLATE_INDEX.render(
gronk_commit=GRONK_COMMIT,
automatic_index=True,
search_bar=True,
title=tag,
index_entries=[{
'title': entry.get('title', ''),
'is_dir': entry.get('is_dir', False),
'path': str(entry.get('path', Path(''))),
} for entry in index_entries],
))
output_file = output_dir.joinpath('index.html')
output_file.parent.mkdir(exist_ok=True, parents=True)
output_file.write_text(
JINJA_TEMPLATE_INDEX.render(automatic_index=True,
gronk_commit=GRONK_COMMIT,
search_bar=True,
title='tags',
index_entries=[{
'path': tag,
'title': tag,
'is_dir': False,
} for tag in tags.keys()]))
def main(args):
""" Entry point for script """
start_time = time.time()
global LICENSE
global FILEMAP
FILEMAP = FileMap(args.notes, args.output_dir.joinpath('notes'))
# 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
if args.output_dir.is_file():
print(f"Output directory ({args.output_dir}) cannot be a file.")
args.output_dir.mkdir(parents=True, exist_ok=True)
# attempt to get licensing information
license_path = args.notes.joinpath("LICENSE")
if license_path.exists():
LICENSE = license_path.read_text()
# TODO git commit log integration
for root_str, _, files in os.walk(args.notes):
root = Path(root_str)
if '.git' in root.parts:
continue
root_properties = FILEMAP.get(root)
root_properties['dst_path']['raw'].mkdir(parents=True, exist_ok=True)
posts = []
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,
title=root_properties.get('title', ''),
content=root_properties.get('content', ''),
content_after_search=root_properties['content_after_search'],
automatic_index=root_properties['automatic_index'],
search_bar=root_properties['search_bar'],
posts=posts,
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(html)
# render each file
for file in files:
render_file(root.joinpath(file))
process_home_index(args)
# copy styling and js scripts necessary for function
shutil.copytree(GRONK_CSS_DIR,
args.output_dir.joinpath('css'),
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_permalink_page(args.output_dir)
elapsed_time = time.time() - start_time
print(f"generated notes {elapsed_time=}")
return 0
def start_pandoc_server():
"""
attempt to get the version of pandoc server in a loop until it is
successful and return version as string
"""
start_time = time.time()
process = subprocess.Popen(["/usr/bin/pandoc-server"],
stdout=subprocess.PIPE)
version = None
while True:
try:
resp = requests.get(f"{PANDOC_SERVER_URL}/version")
version = resp.content.decode('utf-8')
break
except requests.ConnectionError:
time.sleep(0.1)
rc = process.poll()
if rc is not None:
print(f"PANDOC SERVER FAILED TO START: {rc=}")
print(process.stdout.read().decode("utf-8"))
raise Exception("Pandoc server failed to start")
elapsed_time = time.time() - start_time
print(f"pandoc-server started {version=} {elapsed_time=}")
return process
# TODO implement useful logging and debug printing
if __name__ == '__main__':
pandoc_process = start_pandoc_server()
try:
sys.exit(main(get_args()))
except KeyboardInterrupt:
sys.exit(0)
finally:
pandoc_process.kill()

28
n2w_add_uuid.py → gronk_add_uuid.py Executable file → Normal file
View File

@@ -1,35 +1,41 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import editfrontmatter
import frontmatter
import pathlib import pathlib
import sys import sys
import uuid import uuid
import editfrontmatter
import frontmatter
def get_args(): def get_args():
""" Get command line arguments """ """ Get command line arguments """
import argparse import argparse
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('filename', type=pathlib.Path) parser.add_argument('filename', type=pathlib.Path)
parser.add_argument('--template', parser.add_argument('-w',
default=pathlib.Path("/opt/notes2web/templates/n2w_add_uuid_frontmatter_template"), '--write',
type=pathlib.Path action='store_true',
) help='write to file instead of stdout')
parser.add_argument('-w', '--write', action='store_true',
help='write to file instead of stdout')
return parser.parse_args() return parser.parse_args()
def main(args): def main(args):
""" Entry point for script """ """ Entry point for script """
with open(args.template) as fp: template_str = "\n".join([
template_str=fp.read() "author: {{ author }}"
"date: {{ date }}"
"title: {{ title }}"
"tags: {{ tags }}"
"uuid: {{ uuid }}"
])
with open(args.filename) as fp: with open(args.filename) as fp:
fm_pre = frontmatter.load(fp) fm_pre = frontmatter.load(fp)
processor = editfrontmatter.EditFrontMatter(file_path=args.filename, template_str=template_str) processor = editfrontmatter.EditFrontMatter(file_path=args.filename,
template_str=template_str)
fm_data = fm_pre.metadata fm_data = fm_pre.metadata
if 'uuid' not in fm_data.keys(): if 'uuid' not in fm_data.keys():
fm_data['uuid'] = str(uuid.uuid4()) fm_data['uuid'] = str(uuid.uuid4())

View File

75
js/indexsearch.js Normal file
View File

@@ -0,0 +1,75 @@
const HEADERS = "headers"
const PATH = "path"
const TAGS = "tags"
const TITLE = "title"
const SEARCH_TIMEOUT_MS = 100
var SEARCH_TIMEOUT_ID = -1
const fuse = new Fuse(data, {
keys: [ 'title' ],
ignoreLocation: true,
threshhold: 0.4,
minMatchCharLength: 0,
})
const RESULTS_MAX = 15
const searchBar = document.getElementById('search')
const resultsDiv = document.getElementById('searchResults')
var results = []
function updateResultsWithTimeout() {
console.log("clearing timeout")
if (SEARCH_TIMEOUT_ID) SEARCH_TIMEOUT_ID = clearTimeout(SEARCH_TIMEOUT_ID)
SEARCH_TIMEOUT_ID = setTimeout(updateResults, SEARCH_TIMEOUT_MS)
}
function updateResults() {
console.log("updating results")
resultsDiv.innerHTML = ''
if (searchBar.value) results = fuse.search(searchBar.value, { limit: RESULTS_MAX }).map(r => r.item)
else results = data
results.forEach(r => {
wrapper = document.createElement('li')
wrapper.className = "article"
atag = document.createElement('a')
atag.href = r.path
ptag = document.createElement('p')
ptag.innerHTML = r.title + (r.isdirectory ? '/' : '')
atag.appendChild(ptag)
wrapper.appendChild(atag)
resultsDiv.appendChild(wrapper)
})
}
searchBar.addEventListener('keyup', e => {
console.log(e)
// if user pressed enter
if (e.keyCode === 13) {
if (e.shiftKey) {
window.open(results[0].path, '_blank')
} else {
window.location.href = results[0].path
}
return
}
updateResultsWithTimeout()
})
searchBar.addEventListener('change', updateResultsWithTimeout)
const searchParams = new URL(window.location.href).searchParams;
searchBar.value = searchParams.get('q');
updateResults();
console.log(results)
if (searchParams.has('lucky')) {
window.location.href = results[0].path;
}

View File

@@ -1,39 +1,63 @@
/*
* search.js expects an array `data` containing objects with the following keys:
* headers: [string]
* path: string
* tags: [string]
* title: string
* uuid: string
*/
const HEADERS = "headers" const HEADERS = "headers"
const PATH = "path" const PATH = "path"
const TAGS = "tags" const TAGS = "tags"
const TITLE = "title" const TITLE = "title"
const fuse = new Fuse(data, { const SEARCH_TIMEOUT_MS = 100
var SEARCH_TIMEOUT_ID = -1
const fuse = new Fuse(search_data, {
keys: [ keys: [
{ {
name: HEADERS, name: HEADERS,
weight: 2 weight: 1
}, },
{ {
name: PATH, name: PATH,
weight: 0.5 weight: 0.1
}, },
{ {
name: TAGS, name: TAGS,
weight: 1.5 weight: 0.5
}, },
{ {
name: TITLE, name: TITLE,
weight: 1.5 weight: 2
} }
], ],
includeMatches: true includeMatches: true,
useExtendedSearch: true,
ignoreLocation: true,
threshhold: 0.4,
}) })
const RESULTS_MAX = 5
const searchBar = document.getElementById('search') const searchBar = document.getElementById('search')
const resultsMax = document.getElementById('resultsMax')
const resultsDiv = document.getElementById('results') const resultsDiv = document.getElementById('results')
var results = [] var results = []
function updateResultsWithTimeout() {
console.log("clearing timeout")
if (SEARCH_TIMEOUT_ID) SEARCH_TIMEOUT_ID = clearTimeout(SEARCH_TIMEOUT_ID)
SEARCH_TIMEOUT_ID = setTimeout(updateResults, SEARCH_TIMEOUT_MS)
}
function updateResults() { function updateResults() {
console.log("updating results")
resultsDiv.innerHTML = '' resultsDiv.innerHTML = ''
results = fuse.search(searchBar.value).slice(0, parseInt(resultsMax.value)) results = fuse.search(searchBar.value, { limit: RESULTS_MAX })
results.forEach(r => { results.forEach(r => {
wrapper = document.createElement('div') wrapper = document.createElement('div')
wrapper.className = "article" wrapper.className = "article"
@@ -95,12 +119,10 @@ searchBar.addEventListener('keyup', e => {
} }
return return
} }
updateResults() updateResultsWithTimeout()
}) })
searchBar.addEventListener('change', updateResults) searchBar.addEventListener('change', updateResultsWithTimeout)
resultsMax.addEventListener('keyup', updateResults)
resultsMax.addEventListener('change', updateResults)
const searchParams = new URL(window.location.href).searchParams; const searchParams = new URL(window.location.href).searchParams;
searchBar.value = searchParams.get('q'); searchBar.value = searchParams.get('q');

View File

@@ -1,384 +0,0 @@
#!/usr/bin/env python3
from bs4 import BeautifulSoup as bs
import subprocess
import frontmatter
import magic
import sys
import pathlib
import pypandoc
import shutil
import os
import regex as re
import json
TEXT_ARTICLE_TEMPLATE_FOOT = None
TEXT_ARTICLE_TEMPLATE_HEAD = None
INDEX_TEMPLATE_FOOT = None
INDEX_TEMPLATE_HEAD = None
EXTRA_INDEX_CONTENT = None
def get_files(folder):
markdown = []
plaintext = []
other = []
for root, folders, files in os.walk(folder):
for filename in files:
if '/.git' in root:
continue
name = os.path.join(root, filename)
if pathlib.Path(name).suffix == '.md':
markdown.append(name)
elif re.match(r'^text/', magic.from_file(name, mime=True)):
plaintext.append(name)
other.append(name)
else:
other.append(name)
return markdown, plaintext, other
def git_filehistory(working_dir, filename):
print(f"{pathlib.Path(filename).relative_to(working_dir)=}")
git_response = subprocess.run(
[
'git',
f"--git-dir={working_dir.joinpath('.git')}",
"log",
"-p",
"--",
pathlib.Path(filename).relative_to(working_dir)
],
stdout=subprocess.PIPE
)
filehistory = [f"File history not available: git log returned code {git_response.returncode}."
"\nIf this is not a git repository, this is not a problem."]
if git_response.returncode == 0:
filehistory = git_response.stdout.decode('utf-8')
temp = re.split(
r'(commit [a-f0-9]{40})',
filehistory,
flags=re.IGNORECASE
)
for t in temp:
if t == '':
temp.remove(t)
filehistory = []
for i in range(0, len(temp)-1, 2):
filehistory.append(f"{temp[i]}{temp[i+1]}")
if filehistory == "":
filehistory = ["This file has no history (it may not be part of the git repository)."]
filehistory = [ x.replace("<", "&lt;").replace(">", "&gt;") for x in filehistory]
filehistory = "<pre>\n" + "</pre><pre>\n".join(filehistory) + "</pre>"
return filehistory
def get_dirs_to_index(folder):
r = []
for root, folders, files in os.walk(folder):
if pathlib.Path(os.path.join(root, folder)).is_relative_to(folder.joinpath('permalink')):
continue
[r.append(os.path.join(root, folder)) for folder in folders]
return r
def update_required(src_filename, output_filename):
return not os.path.exists(output_filename) or os.path.getmtime(src_filename) > os.path.getmtime(output_filename)
def get_args():
""" Get command line arguments """
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('notes', type=pathlib.Path)
parser.add_argument('-o', '--output-dir', type=pathlib.Path, default='web')
parser.add_argument('-t', '--template', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/templates/article.html'))
parser.add_argument('-H', '--template-text-head', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/templates/textarticlehead.html'))
parser.add_argument('-f', '--template-text-foot', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/templates/textarticlefoot.html'))
parser.add_argument('-i', '--template-index-head', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/templates/indexhead.html'))
parser.add_argument('-I', '--template-index-foot', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/templates/indexfoot.html'))
parser.add_argument('-s', '--stylesheet', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/styles.css'))
parser.add_argument('--home_index', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/templates/home_index.html'))
parser.add_argument('--permalink_index', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/templates/permalink_index.html'))
parser.add_argument('-e', '--extra-index-content', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/templates/extra_index_content.html'))
parser.add_argument('-n', '--index-article-names', action='append', default=['index.md'])
parser.add_argument('-F', '--force', action="store_true", help="Generate new output html even if source file was modified before output html")
parser.add_argument('--fuse', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/fuse.js'))
parser.add_argument('--searchjs', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/search.js'))
parser.add_argument('--permalinkjs', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/permalink.js'))
parser.add_argument('--tocsearchjs', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/toc_search.js'))
parser.add_argument('--toc-depth', type=int, default=6, dest='toc_depth')
return parser.parse_args()
def main(args):
""" Entry point for script """
with open(args.template_text_foot) as fp:
TEXT_ARTICLE_TEMPLATE_FOOT = fp.read()
with open(args.template_text_head) as fp:
TEXT_ARTICLE_TEMPLATE_HEAD = fp.read()
with open(args.template_index_foot) as fp:
INDEX_TEMPLATE_FOOT = fp.read()
with open(args.template_index_head) as fp:
INDEX_TEMPLATE_HEAD = fp.read()
with open(args.extra_index_content) as fp:
EXTRA_INDEX_CONTENT = fp.read()
if args.output_dir.is_file():
print(f"Output directory ({args.output_dir}) cannot be a file.")
args.output_dir.mkdir(parents=True, exist_ok=True)
notes_license = "This note has no copyright license.",
print(f"{notes_license=}")
license_path = args.notes.joinpath("LICENSE")
if license_path.exists():
with open(license_path) as fp:
notes_license = fp.read()
markdown_files, plaintext_files, other_files = get_files(args.notes)
all_entries=[]
dirs_with_index_article = []
tag_dict = {}
permalink_to_filepath = {}
print(f"{markdown_files=}")
for filename in markdown_files:
print(f"{filename=}")
# calculate output filename
output_filename = args.output_dir.joinpath('notes').joinpath(
pathlib.Path(filename).relative_to(args.notes)
).with_suffix('.html')
if os.path.basename(filename) in args.index_article_names:
output_filename = output_filename.parent.joinpath('index.html')
dirs_with_index_article.append(str(output_filename.parent))
print(f"{output_filename=}")
# extract tags from frontmatter, save to tag_dict
fm = frontmatter.load(filename)
if isinstance(fm.get('tags'), list):
for tag in fm.get('tags'):
t = {
'path': str(pathlib.Path(output_filename).relative_to(args.output_dir)),
'title': fm.get('title') or pathlib.Path(filename).name
}
if tag in tag_dict.keys():
tag_dict[tag].append(t)
else:
tag_dict[tag] = [t]
# find headers in markdown
with open(filename) as fp:
lines = fp.read().split('\n')
header_lines = []
for line in lines:
if re.match('^#{1,6} \S', line):
header_lines.append(" ".join(line.split(" ")[1:]))
all_entries.append({
'path': '/' + str(pathlib.Path(*pathlib.Path(output_filename).parts[1:])),
'title': fm.get('title') or pathlib.Path(filename).name,
'tags': fm.get('tags'),
'headers': header_lines,
'uuid': fm.get('uuid')
})
if 'uuid' in fm.keys():
permalink_to_filepath[fm['uuid']] = all_entries[-1]['path']
# update file if required
if update_required(filename, output_filename) or args.force:
filehistory = git_filehistory(args.notes, filename)
html = pypandoc.convert_file(filename, 'html', extra_args=[
f'--template={args.template}',
'-V', f'filehistory={filehistory}',
'-V', f'licenseFull={notes_license}',
'--mathjax',
'--toc', f'--toc-depth={args.toc_depth}'
])
pathlib.Path(output_filename).parent.mkdir(parents=True, exist_ok=True)
with open(output_filename, 'w+') as fp:
fp.write(html)
print(f"{plaintext_files=}")
for filename in plaintext_files:
filehistory = git_filehistory(args.notes, filename)
title = os.path.basename(filename)
output_filename = str(
args.output_dir.joinpath('notes').joinpath(
pathlib.Path(filename).relative_to(args.notes)
)
) + '.html'
print(f"{output_filename=}")
pathlib.Path(output_filename).parent.mkdir(parents=True, exist_ok=True)
html = re.sub(r'\$title\$', title, TEXT_ARTICLE_TEMPLATE_HEAD)
html = re.sub(r'\$h1title\$', title, html)
html = re.sub(r'\$raw\$', os.path.basename(filename), html)
html = re.sub(r'\$licenseFull\$', notes_license, html)
html = html.replace('$filehistory$', filehistory)
with open(filename) as fp:
html += fp.read().replace("<", "&lt;").replace(">", "&gt;")
html += TEXT_ARTICLE_TEMPLATE_FOOT
with open(output_filename, 'w+') as fp:
fp.write(html)
all_entries.append({
'path': str(pathlib.Path(*pathlib.Path(output_filename).parts[1:])),
'title': title,
'tags': [],
'headers': []
})
print(f"{other_files=}")
for filename in other_files:
output_filename = str(
args.output_dir.joinpath('notes').joinpath(
pathlib.Path(filename).relative_to(args.notes)
)
)
pathlib.Path(output_filename).parent.mkdir(parents=True, exist_ok=True)
all_entries.append({
'path': str(pathlib.Path(*pathlib.Path(output_filename).parts[1:])),
'title': str(pathlib.Path(*pathlib.Path(output_filename).parts[1:])),
'tags': [],
'headers': []
})
shutil.copyfile(filename, output_filename)
tagdir = args.output_dir.joinpath('.tags')
tagdir.mkdir(parents=True, exist_ok=True)
for tag in tag_dict.keys():
html = re.sub(r'\$title\$', f'{tag}', INDEX_TEMPLATE_HEAD)
html = re.sub(r'\$h1title\$', f'tag: {tag}', html)
html = re.sub(r'\$extra_content\$', '', html)
for entry in tag_dict[tag]:
html += f"<div class=\"article\"><a href=\"/{entry['path']}\">{entry['title']}</a></div>"
html += INDEX_TEMPLATE_FOOT
with open(tagdir.joinpath(f'{tag}.html'), 'w+') as fp:
fp.write(html)
dirs_to_index = [args.output_dir.name] + get_dirs_to_index(args.output_dir)
print(f"{dirs_to_index=}")
print(f"{dirs_with_index_article=}")
for d in dirs_to_index:
print(f"{d in dirs_with_index_article=} {d=}")
if d in dirs_with_index_article:
continue
directory = pathlib.Path(d)
paths = os.listdir(directory)
#print(f"{paths=}")
indexentries = []
for p in paths:
path = pathlib.Path(p)
#print(f"{path=}")
if p in [ 'index.html', '.git' ]:
continue
fullpath = directory.joinpath(path)
if path.suffix == '.html':
with open(fullpath) as fp:
soup = bs(fp.read(), 'html.parser')
try:
title = soup.find('title').get_text() or pathlib.Path(path).name
except AttributeError:
title = pathlib.Path(path).stem
elif fullpath.is_dir():
title = path
else:
# don't add plaintext files to index, since they have a html wrapper
continue
if str(title).strip() == '':
title = path
indexentries.append({
'title': str(title),
'path': str(path),
'isdirectory': fullpath.is_dir()
})
indexentries.sort(key=lambda entry: str(entry['title']).lower())
indexentries.sort(key=lambda entry: entry['isdirectory'], reverse=True)
html = re.sub(r'\$title\$', str(directory), INDEX_TEMPLATE_HEAD)
html = re.sub(r'\$h1title\$', str(directory), html)
html = re.sub(r'\$extra_content\$',
EXTRA_INDEX_CONTENT if directory == args.notes else '',
html
)
for entry in indexentries:
html += (
'<li class="article">'
f'<a href="{entry["path"]}"><p>'
f'{entry["title"]}{"/" if entry["isdirectory"] else ""}'
'</p></a>'
'</li>'
)
html += INDEX_TEMPLATE_FOOT
with open(directory.joinpath('index.html'), 'w+') as fp:
fp.write(html)
shutil.copyfile(args.stylesheet, args.output_dir.joinpath('styles.css'))
shutil.copyfile(args.fuse, args.output_dir.joinpath('fuse.js'))
shutil.copyfile(args.searchjs, args.output_dir.joinpath('search.js'))
shutil.copyfile(args.tocsearchjs, args.output_dir.joinpath('toc_search.js'))
shutil.copyfile(args.permalinkjs, args.output_dir.joinpath('permalink.js'))
with open(args.output_dir.joinpath('index.html'), 'w+') as fp:
with open(args.home_index) as fp2:
html = re.sub(r'\$title\$', args.output_dir.parts[0], fp2.read())
html = re.sub(r'\$h1title\$', args.output_dir.parts[0], html)
html = re.sub(r'\$data\$', json.dumps(all_entries), html)
fp.write(html)
permalink_dir = args.output_dir.joinpath('permalink')
permalink_dir.mkdir(exist_ok=True)
with open(args.permalink_index) as fp:
html = re.sub(r'\$data\$', json.dumps(permalink_to_filepath), fp.read())
with open(permalink_dir.joinpath('index.html'), 'w+') as fp:
fp.write(html)
print(tag_dict)
return 0
if __name__ == '__main__':
try:
sys.exit(main(get_args()))
except KeyboardInterrupt:
sys.exit(0)

164
readme.md
View File

@@ -1,53 +1,105 @@
# notes2web # :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)
## Why? ## Why?
I want to be able to view my notes in a more convenient way. - View notes as a website, on any device
I was already writing them in Pandoc markdown and could view them as PDFs but that wasn't quite - Easily share notes
doing it for me: - Powered by Pandoc, and therefore supports [Pandoc's markdown](https://pandoc.org/MANUAL.html#pandocs-markdown) (I mainly care about equations)
- [flatnotes](https://github.com/Dullage/flatnotes) is cool but I really would rather type my notes in Vim
- It was inconvenient to flick through multiple files of notes to find the right PDF - Lightweight HTML generated
- It was annoying to sync to my phone - Minimal JavaScript
- PDFs do not scale so they were hard to read on smaller screens
- Probably more reasons I can't think of right now
- Fun
## Install ## Install
0. Install [Pandoc](https://pandoc.org/index.html) and [Pip](https://github.com/pypa/pip) ### Docker
On arch: Run the following, modifing the `-v` arguments as needed to mount the correct folders and
``` setting the value of `ARCH` to either `amd64` or `arm64` as appropriate.
# pacman -S pandoc python-pip
```
1. Run `make install` as root ```
docker build . -t gronk --build-arg ARCH=amd64
docker run -v ./n:/usr/src/app/notes -v ./web:/usr/src/app/web gronk
```
## Things to Remember Whilst Writing Notes #### Compose
- notes2web reads the following YAML [frontmatter](https://jekyllrb.com/docs/front-matter/) variable: A [docker compose file](./docker-compose.yml) file has been provided.
- `author` --- The person(s) who wrote the article Set the following environment variables (or create a .env file) and run `docker compose up`:
- `tags` --- A YAML list of tags which the article relates to - this is used for browsing and also
searching
- `title` --- The title of the article
- `uuid` --- A unique identifier used for permalinks. More below.
- notes2web indexes [ATX-style headings](https://pandoc.org/MANUAL.html#atx-style-headings) for - `ARCH`
- `SOURCE`
- `OUTPUT`
### Locally
0. Install [Pandoc](https://pandoc.org/index.html) and [Pip](https://github.com/pypa/pip), python3-dev, and a C compiler
1. `sudo make install`
## Other Things to Know
- gronk indexes [ATX-style headings](https://pandoc.org/MANUAL.html#atx-style-headings) for
searching searching
- notes2web attempts to display file history through the `git log` command - gronk looks for the plaintext file `LICENSE` in the root directory of your notes
- notes2web looks for the plaintext file `LICENSE` in the root directory of your notes
This is optional but if you would like to add a license you can find one
[here](https://choosealicense.com).
### Permalinks ## Custom Directory Index and Metadata
To add custom content to a directory index, put it in a file called `readme.md` under the directory.
You can set the following frontmatter variables to customise the directory index of a directory:
| variable | default value | description |
|------------------------|----------------|--------------------------------------------------------------------------------------------|
| `blog` | `false` | enable [blog mode](#blog-mode) for this directory |
| `tags` | `[]` | list of tags, used by search and inherited by any notes and subdirectories |
| `uuid` | none | unique id to reference directory, used for permalinking |
| `content_after_search` | same as `blog` | show custom content in `readme.md` after search bar and directory index |
| `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
gronk reads the following YAML [frontmatter](https://jekyllrb.com/docs/front-matter/) variables for metadata:
| variable | description |
|------------------|---------------------------------------------------------------------------------------|
| `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 |
| `title` | The title of the article |
| `uuid` | A unique identifier used for permalinks. |
| `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 |
## 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 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.
In order to identify documents between file changes, a unique identifier is used to identify a file. In order to identify documents between file changes, a unique identifier is used to identify a file.
@@ -59,42 +111,46 @@ The included `n2w_add_uuid.py` will add a UUID to a markdown file which does not
already. already.
Combine it with `find` to UUIDify all your markdown files (but make a backup first). Combine it with `find` to UUIDify all your markdown files (but make a backup first).
## Custom Styling
To completely replace the existing styling, set the environment variable `GRONK_CSS_DIR` to another directory with
a file called `styles.css`.
To add additional styling, the default styling will attempt to import `styles.css` from the root of the notes
directory.
To add additional content to the homepage, create a file called `readme.md` at the top level of your notes directory.
To set the HTML `title` tag, set `title` in the frontmatter of `readme.md`:
```markdown
---
title: "alv's notes"
---
# alv's notes
these notes are probably wrong
```
## CLI Usage ## CLI Usage
``` ```
$ notes2web.py notes_directory $ gronk.py notes_directory
``` ```
Output of `notes2web.py --help`: Output of `gronk.py --help`:
``` ```
usage: notes2web.py [-h] [-o OUTPUT_DIR] [-t TEMPLATE] [-H TEMPLATE_TEXT_HEAD] usage: gronk.py [-h] [-o OUTPUT_DIR] [-F] notes
[-f TEMPLATE_TEXT_FOOT] [-i TEMPLATE_INDEX_HEAD]
[-I TEMPLATE_INDEX_FOOT] [-s STYLESHEET]
[--home_index HOME_INDEX] [-e EXTRA_INDEX_CONTENT]
[-n INDEX_ARTICLE_NAMES] [-F] [--fuse FUSE]
[--searchjs SEARCHJS]
notes
positional arguments: positional arguments:
notes notes
optional arguments: options:
-h, --help show this help message and exit -h, --help show this help message and exit
-o OUTPUT_DIR, --output-dir OUTPUT_DIR -o OUTPUT_DIR, --output-dir OUTPUT_DIR
-t TEMPLATE, --template TEMPLATE -F, --force Generate new output html even if source file was modified before output
-H TEMPLATE_TEXT_HEAD, --template-text-head TEMPLATE_TEXT_HEAD html
-f TEMPLATE_TEXT_FOOT, --template-text-foot TEMPLATE_TEXT_FOOT
-i TEMPLATE_INDEX_HEAD, --template-index-head TEMPLATE_INDEX_HEAD
-I TEMPLATE_INDEX_FOOT, --template-index-foot TEMPLATE_INDEX_FOOT
-s STYLESHEET, --stylesheet STYLESHEET
--home_index HOME_INDEX
-e EXTRA_INDEX_CONTENT, --extra-index-content EXTRA_INDEX_CONTENT
-n INDEX_ARTICLE_NAMES, --index-article-names INDEX_ARTICLE_NAMES
-F, --force Generate new output html even if source file was
modified before output html
--fuse FUSE
--searchjs SEARCHJS
``` ```
The command will generate a website in the `output-dir` directory (`./web` by default). The command will generate a website in the `output-dir` directory (`./web` by default).
@@ -113,5 +169,3 @@ Then you just have to point a webserver at `output-dir`.
Default synatx highlighting is based off [Pygments](https://pygments.org/)' default theme and Default synatx highlighting is based off [Pygments](https://pygments.org/)' default theme and
made using Pandoc v2.7.2. made using Pandoc v2.7.2.
I found the theme [here](https://github.com/tajmone/pandoc-goodies/blob/master/skylighting/css/built-in-styles/pygments.css). I found the theme [here](https://github.com/tajmone/pandoc-goodies/blob/master/skylighting/css/built-in-styles/pygments.css).
Pretty sure the link colours are taken from [thebestmotherfucking.website](https://thebestmotherfucking.website/).

14
requirements-dev.txt Normal file
View File

@@ -0,0 +1,14 @@
beautifulsoup4==4.9.3
editfrontmatter==0.0.1
greenlet==2.0.2
Jinja2==3.0.3
MarkupSafe==2.1.0
msgpack==1.0.5
oyaml==1.0
pynvim==0.4.3
pypandoc==1.5
python-frontmatter==1.0.0
python-magic==0.4.24
regex==2021.11.10
soupsieve==2.2.1
PyYAML==6.0.1

View File

@@ -1,11 +1,21 @@
beautifulsoup4==4.9.3 beautifulsoup4==4.9.3
certifi==2023.7.22
charset-normalizer==3.2.0
editfrontmatter==0.0.1 editfrontmatter==0.0.1
gitdb==4.0.10
GitPython==3.1.36
idna==3.4
Jinja2==3.0.3 Jinja2==3.0.3
MarkupSafe==2.1.0 MarkupSafe==2.1.0
oyaml==1.0 oyaml==1.0
pypandoc==1.5 pypandoc==1.5
python-frontmatter==1.0.0 python-frontmatter==1.0.0
python-magic==0.4.24 python-magic==0.4.24
PyYAML==5.4.1 PyYAML==5.3.1
regex==2021.11.10 regex==2021.11.10
soupsieve==2.2.1 soupsieve==2.2.1
regex==2021.11.10
requests==2.31.0
smmap==5.0.1
soupsieve==2.2.1
urllib3==2.0.4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 172 KiB

View File

@@ -0,0 +1,9 @@
{% extends "article.html" %}
{% block body_content %}
<p> This file was not rendered by gronk because it is a plaintext file, not a markdown
file.
You access the raw file <a href="{{ raw_link }}">here</a>.
Below is an unformatted representation of the file:
</p>
<pre>{{ raw_content }}</pre>
{% endblock %}

View File

@@ -1,56 +1,69 @@
<head> {% extends "base.html" %}
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> {% block head %}
<meta charset="utf-8">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap" />
<link rel="stylesheet" type="text/css" href="/styles.css" />
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script> <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> <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<title>$title$</title> {% endblock %}
</head>
<body> {% block content %}
<div id="contentWrapper"> <p class="smallText metadata"> title: {{ title }} </p>
<div id="sidebar"> {% if lecture_slides %}
<div id="header"> <p class="smallText metadata"> lecture_slides: [
<p class="smallText"> {% for slide in lecture_slides %}
<a href="/permalink?uuid=$uuid$">permalink</a> <a href="{{ slide }}">{{ slide }}</a>{% if loop.nextitem %},{% endif %}
</p> {% endfor %}
<p class="smallText"> tags: [ ]</p>
$for(tags)$ {% endif %}
<a href="/.tags/$tags$.html">$tags$</a>$sep$, {% if lecture_notes %}
$endfor$ <p class="smallText metadata"> lecture_notes: [
]</p> {% for note in lecture_notes %}
<p class="smallText"> <a href="{{ note }}">{{ note }}</a>{% if loop.nextitem %},{% endif %}
written by $for(author)$$author$$sep$, $endfor$ {% endfor %}
</p> ]</p>
<p class="smallText"> {% endif %}
syntax highlighting based on <a href="https://pygments.org/">Pygments'</a> default
colors {% if uuid %}
</p> <p class="smallText metadata"> uuid: {{ uuid }} (<a href="/permalink?uuid={{ uuid }}">permalink</a>) </p>
<p class="smallText"> {% endif %}
page generated by <a href="https://git.alv.cx/alvierahman90/notes2web">notes2web</a>
</p> {% if published %}
</div> <p class="smallText metadata"> published: {{ published }} </p>
<h3>Table of Contents</h3> {% endif %}
<input id="search" placeholder="Search table of contents" />
<div id="toc">$toc$</div> <p class="smallText metadata"> tags: [
</div> {% for tag in tags %}
<div id="content"> <a href="/tags/{{ tag }}">{{ tag }}</a>{% if loop.nextitem %},{% endif %}
<details id="commitLog"> {% endfor %}
<summary class="smallText"> ]</p>
Commit log (file history) <p class="smallText metadata">
</summary> {% if author is string %}
$filehistory$ written by: {{ author }}
</details> {% elif author is iterable %}
<details id="license"> written by: [ {% for auth in author %}{{ auth }}{% if loop.nextitem %}, {% endif %}{% endfor %} ]
<summary class="smallText"> {% endif %}
License </p>
</summary> <p class="smallText metadata">
<pre>$licenseFull$</pre> syntax highlighting based on <a href="https://pygments.org/">Pygments'</a> default
</details> colors
$body$ </p>
</div> <p class="smallText metadata">
</div> page generated by <a href="https://git.alv.cx/alvierahman90/gronk">gronk</a>
<script src="/fuse.js"> </script> </p>
<script src="/toc_search.js"> </script> {% if license %}
</body> <details id="license">
<summary class="smallText">
License
</summary>
<pre>{{ license }}</pre>
</details>
{% endif %}
{% block body_content %}
{{ content|safe }}
{% endblock %}
{% endblock %}

18
templates/base.html Normal file
View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta charset="UTF-8">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap" />
<link rel="stylesheet" type="text/css" href="/css/styles.css" />
<title>{{ title }}</title>
{% block head %}
{% endblock %}
</head>
<body>
<div id="content">
{% block content %}
{% 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>
</div>
</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 generated 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,4 +0,0 @@
<p>
These are my personal notes. Correctness is not guaranteed.
Browse by tag <a href="/.tags">here</a>.
</p>

18
templates/home.html Normal file
View File

@@ -0,0 +1,18 @@
{% extends "article.html" %}
{% block content %}
{{ post['content']|safe }}
<p>
Browse <a href="/notes">here</a> or by tag <a href="/tags">here</a>.
</p>
<div id="searchWrapper">
<input autocomplete="off" placeholder="search" id="search" autofocus>
</div>
<p class="smallText" style="margin-top: 0; text-align: center;"> Press <kbd>Enter</kbd> to open first result or <kbd>Shift</kbd>+<kbd>Enter</kbd> to open in new tab</p>
<div id="results">
</div>
<script src="/js/fuse.js"> </script>
<script> const search_data = {{ search_data|tojson }} </script>
<script src="/js/search.js"> </script>
{% endblock %}

View File

@@ -1,29 +0,0 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta charset="utf-8">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap" />
<link rel="stylesheet" type="text/css" href="/styles.css" />
<title>$title$</title>
</head>
<body>
<div id="content">
<h1>$h1title$</h1>
<p>
These are my personal notes. Correctness is not guaranteed.
Browse <a href="/notes">here</a> or by tag <a href="/.tags">here</a>.
</p>
<div id="searchWrapper">
<input placeholder="Search" id="search" autofocus>
<input type="number" id="resultsMax" min="0" value="5">
</div>
<p class="smallText" style="margin-top: 0; text-align: center;"> Press <kbd>Enter</kbd> to open first result or <kbd>Shift</kbd>+<kbd>Enter</kbd> to open in new tab</p>
<div id="results">
</div>
<p class="smallText"> page generated by <a href="https://github.com/alvierahman90/notes2web">notes2web</a></p>
</div>
<script src="/fuse.js"> </script>
<script> const data = $data$ </script>
<script src="/search.js"> </script>
</body>

View File

@@ -1,14 +1,35 @@
<head> {% extends "base.html" %}
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> {% block content %}
<meta charset="utf-8"> {% if not content %}
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap" /> <h1>{{ title }}</h1>
<link rel="stylesheet" type="text/css" href="/styles.css" /> {% endif %}
<title>$title$</title>
</head> {% if not content_after_search %}
<body> {{ content|safe }}
<h1>$h1title$</h1> {% endif %}
<div id="content">
$body$ {% if automatic_index %}
<p style="font-size: 0.7em;"> page generated by <a href="https://github.com/alvierahman90/notes2web">notes2web</a></p> {% if search_bar %}
<div id="searchWrapper">
<input id="search" placeholder="search" autocomplete="off" autofocus>
</div> </div>
</body> <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>
{% endif %}
{% if content_after_search %}
{{ content|safe }}
{% endif %}
<script src="/js/fuse.js"> </script>
<script> const search_data = {{ index_entries|tojson }} </script>
<script src="/js/indexsearch.js"> </script>
{% endblock %}

View File

@@ -1,4 +0,0 @@
</ul>
<p style="font-size: 0.7em;"> page generated by <a href="https://github.com/alvierahman90/notes2web">notes2web</a></p>
</div>
</body>

View File

@@ -1,14 +0,0 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta charset="utf-8">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap" />
<link rel="stylesheet" type="text/css" href="/styles.css" />
<title>$title$</title>
</head>
<body>
<div id="content">
<h1>$title$</h1>
$extra_content$
<ul class="buttonlist">
<li class="article"><a href=".."><p>../</p></a></li>

View File

@@ -1,5 +0,0 @@
author: {{ author }}
date: {{ date }}
title: {{ title }}
tags: {{ tags }}
uuid: {{ uuid }}

11
templates/permalink.html Normal file
View File

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

View File

@@ -1,19 +0,0 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta charset="utf-8">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap" />
<link rel="stylesheet" type="text/css" href="/styles.css" />
<title></title>
</head>
<body>
<div id="content">
<p>
You should be being redirected...
Otherwise, click <a id="manual_redirect">here</a>.
</p>
<p class="smallText"> page generated by <a href="https://github.com/alvierahman90/notes2web">notes2web</a></p>
</div>
<script> const data = $data$ </script>
<script src="/permalink.js"> </script>
</body>

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>

View File

@@ -1,3 +0,0 @@
</pre>
</div>
</body>

View File

@@ -1,34 +0,0 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta charset="utf-8">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap" />
<link rel="stylesheet" type="text/css" href="/styles.css" />
<title>$title$</title>
</head>
<body>
<div id="content">
<div id="header">
<p class="smallText">
page generated by <a href="https://git.alv.cx/alvierahman90/notes2web">notes2web</a>
</p>
<details>
<summary class="smallText">
Commit log (file history)
</summary>
$filehistory$
</details>
<details>
<summary class="smallText">
License
</summary>
<pre>$licenseFull$</pre>
</details>
</div>
<h1>$title$</h1>
<p> This file was not rendered by notes2web because it is a plaintext file, not a markdown
file.
You access the raw file <a href="$raw$">here</a>.
Below is an unformatted representation of the file:
</p>
<pre>