Compare commits

...
This repository has been archived on 2024-01-02. You can view files and clone it, but cannot push or open issues or pull requests.

27 Commits
main ... v2

Author SHA1 Message Date
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
31 changed files with 888 additions and 716 deletions

4
.dockerignore Normal file
View File

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

4
.gitignore vendored
View File

@ -1,5 +1,7 @@
*.swp *.swp
.env
env env
n n
web web
__pycache__ 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,16 +1,11 @@
install: install:
cp n2w_add_uuid.py /usr/local/bin cp gronk_add_uuid.py /usr/local/bin
sed "s/N2W_COMMIT = \"\"/N2W_COMMIT = \"$$(git rev-parse --short HEAD)\"/" notes2web.py > /usr/local/bin/notes2web.py sed "s/GRONK_COMMIT = \"dev\"/GRONK_COMMIT = \"$$(git rev-parse --short HEAD)\"/" gronk.py > /usr/local/bin/gronk.py
pip3 install -r requirements.txt --break-system-packages chmod +x /usr/local/bin/gronk.py
mkdir -p /opt/notes2web chmod +x /usr/local/bin/gronk_add_uuid.py
cp -r templates /opt/notes2web mkdir -p /opt/gronk
cp styles.css /opt/notes2web cp -r templates js css /opt/gronk
cp fuse.js /opt/notes2web pip3 install -r requirements.txt
cp search.js /opt/notes2web
cp indexsearch.js /opt/notes2web
cp toc_search.js /opt/notes2web
cp permalink.js /opt/notes2web
chmod +x /usr/local/bin/notes2web.py
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

@ -3,6 +3,7 @@
@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("https://styles.alv.cx/modules/darkmode.css");
@import url("/notes/styles.css");
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;

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'

552
gronk.py Executable file
View File

@ -0,0 +1,552 @@
#!/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
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")
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)
@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,
'content_after_search': False,
'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 '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 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}
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'])
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 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_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"))
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)
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(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)
#pprint.pprint(root_properties)
html = 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'],
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:
# don't render readme.md as index as it is used for directory
if file == "readme.md":
continue
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()

26
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',
)
parser.add_argument('-w', '--write', action='store_true',
help='write to file instead of stdout') 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

View File

@ -1,3 +1,13 @@
/*
* 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"
@ -6,11 +16,11 @@ 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: [ keys: [
{ {
name: HEADERS, name: HEADERS,
weight: 0.2 weight: 1
}, },
{ {
name: PATH, name: PATH,
@ -18,11 +28,11 @@ const fuse = new Fuse(data, {
}, },
{ {
name: TAGS, name: TAGS,
weight: 0.1 weight: 0.5
}, },
{ {
name: TITLE, name: TITLE,
weight: 4 weight: 2
} }
], ],
includeMatches: true, includeMatches: true,

View File

@ -1,428 +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
import yaml
TEXT_ARTICLE_TEMPLATE_FOOT = None
TEXT_ARTICLE_TEMPLATE_HEAD = None
INDEX_TEMPLATE_FOOT = None
INDEX_TEMPLATE_HEAD = None
EXTRA_INDEX_CONTENT = None
N2W_COMMIT = ""
def is_plaintext(filename):
return re.match(r'^text/', magic.from_file(str(filename), mime=True)) is not 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 is_plaintext(name):
plaintext.append(name)
other.append(name)
else:
other.append(name)
return markdown, plaintext, other
def get_inherited_tags(file, base_folder):
tags = []
folder = pathlib.Path(file)
while folder != base_folder.parent:
print(f"get_inherited_tags {folder=}")
folder = pathlib.Path(folder).parent
folder_metadata = folder.joinpath('.n2w.yml')
if not folder_metadata.exists():
continue
with open(folder.joinpath('.n2w.yml')) as fp:
folder_properties = yaml.safe_load(fp)
tags += folder_properties.get('itags')
print(f"get_inherited_tags {tags=}")
return list(set(tags))
def git_head_sha1(working_dir):
git_response = subprocess.run(
[ 'git', f"--git-dir={working_dir.joinpath('.git')}", 'rev-parse', '--short', 'HEAD' ],
stdout=subprocess.PIPE
).stdout.decode('utf-8')
return git_response.strip()
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('--indexsearchjs', type=pathlib.Path, default=pathlib.Path('/opt/notes2web/indexsearch.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 list(set(fm.get('tags') + get_inherited_tags(filename, args.notes))):
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': list(set(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)
with open(filename) as fp:
article = frontmatter.load(fp)
article['tags'] += get_inherited_tags(filename, args.notes)
article['tags'] = sorted(list(set(article['tags'])))
article['filehistory'] = filehistory
article['licenseFull'] = notes_license
html = pypandoc.convert_text(frontmatter.dumps(article), 'html', format='md', extra_args=[
f'--template={args.template}',
'--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': [get_inherited_tags(filename, args.notes)],
'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)
)
)
title = os.path.basename(filename)
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': title,
'tags': [get_inherited_tags(filename, args.notes)],
'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]:
entry['path'] = '/' + entry['path']
html += f"<div class=\"article\"><a href=\"{entry['path']}\">{entry['title']}</a></div>"
html += re.sub('\$data\$', json.dumps(tag_dict[tag]), 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)
title = path.name
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
elif is_plaintext(fullpath):
# 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 += re.sub(r'\$data\$', json.dumps(indexentries), 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.indexsearchjs, args.output_dir.joinpath('indexsearch.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'\$n2w_commit\$', N2W_COMMIT, html)
html = re.sub(r'\$notes_git_head_sha1\$', git_head_sha1(args.notes), 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)

142
readme.md
View File

@ -1,4 +1,4 @@
# notes2web # gronk
View your notes as a static html site. Browse a live sample of it [here](https://notes.alv.cx). View your notes as a static html site. Browse a live sample of it [here](https://notes.alv.cx).
@ -9,43 +9,77 @@ Tested with [pandoc v2.19.2](https://github.com/jgm/pandoc/releases/tag/2.19.2).
## 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
### Docker
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.
```
docker build . -t gronk --build-arg ARCH=amd64
docker run -v ./n:/usr/src/app/notes -v ./web:/usr/src/app/web gronk
```
#### Compose
A [docker compose file](./docker-compose.yml) file has been provided.
Set the following environment variables (or create a .env file) and run `docker compose up`:
- `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 0. Install [Pandoc](https://pandoc.org/index.html) and [Pip](https://github.com/pypa/pip), python3-dev, and a C compiler
1. Run `make install` as root 1. `sudo make install`
## Things to Remember Whilst Writing Notes ## Other Things to Know
- notes2web reads the following YAML [frontmatter](https://jekyllrb.com/docs/front-matter/) variable: - gronk indexes [ATX-style headings](https://pandoc.org/MANUAL.html#atx-style-headings) for
- `author` --- The person(s) who wrote the article
- `tags` --- A YAML list of tags which the article relates to - this is used for browsing and also
searching searching
- `title` --- The title of the article - gronk looks for the plaintext file `LICENSE` in the root directory of your notes
- `uuid` --- A unique identifier used for permalinks. More below.
- `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
- notes2web indexes [ATX-style headings](https://pandoc.org/MANUAL.html#atx-style-headings) for
searching
- notes2web attempts to display file history through the `git log` command
- 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 ## Custom Directory Index and Metadata
[here](https://choosealicense.com).
### Permalinks 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 |
|------------------------|-------------------|--------------------------------------------------------------------------------------------|
| `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` | `false` | 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 |
| `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 |
## 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.
@ -57,58 +91,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).
### Inherited Properties ## Custom Styling
Notes can inherit a some properties from their parent folder(s) by creating a `.n2w.yml` file in a To completely replace the existing styling, set the environment variable `GRONK_CSS_DIR` to another directory with
folder. a file called `styles.css`.
#### Tags To add additional styling, the default styling will attempt to import `styles.css` from the root of the notes
directory.
If you have a folder `uni` with all you university notes, you might want all the files in there to To add additional content to the homepage, create a file called `readme.md` at the top level of your notes directory.
be tagged `uni`: To set the HTML `title` tag, set `title` in the frontmatter of `readme.md`:
`NOTES_PATH/uni/.n2w.yaml`: ```markdown
---
title: "alv's notes"
---
```yaml # alv's notes
itags: [ university ]
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).
@ -127,5 +149,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,5 +1,10 @@
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
@ -9,3 +14,8 @@ python-magic==0.4.24
PyYAML==5.3.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

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,8 +1,5 @@
<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> <script>
MathJax = { MathJax = {
@ -12,63 +9,57 @@
} }
</script> </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="content"> {% if lecture_slides %}
<p class="smallText metadata">
title: $title$
</p>
$if(lecture_slides)$
<p class="smallText metadata"> lecture_slides: [ <p class="smallText metadata"> lecture_slides: [
$for(lecture_slides)$ {% for slide in lecture_slides %}
<a href="$lecture_slides$">$lecture_slides$</a>$sep$, <a href="{{ slide }}">{{ slide }}</a>{% if loop.nextitem %},{% endif %}
$endfor$ {% endfor %}
]</p> ]</p>
$endif$ {% endif %}
$if(lecture_notes)$ {% if lecture_notes %}
<p class="smallText metadata"> lecture_notes: [ <p class="smallText metadata"> lecture_notes: [
$for(lecture_notes)$ {% for note in lecture_notes %}
<a href="$lecture_notes$">$lecture_notes$</a>$sep$, <a href="{{ note }}">{{ note }}</a>{% if loop.nextitem %},{% endif %}
$endfor$ {% endfor %}
]</p> ]</p>
$endif$ {% endif %}
{% if uuid %}
<p class="smallText metadata"> uuid: {{ uuid }} (<a href="/permalink?uuid={{ uuid }}">permalink</a>) </p>
{% endif %}
<p class="smallText metadata">
uuid: $uuid$ (<a href="/permalink?uuid=$uuid$">permalink</a>)
</p>
<p class="smallText metadata"> tags: [ <p class="smallText metadata"> tags: [
$for(tags)$ {% for tag in tags %}
<a href="/.tags/$tags$.html">$tags$</a>$sep$, <a href="/tags/{{ tag }}">{{ tag }}</a>{% if loop.nextitem %},{% endif %}
$endfor$ {% endfor %}
]</p> ]</p>
<p class="smallText metadata"> <p class="smallText metadata">
written by $for(author)$$author$$sep$, $endfor$ {% 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> </p>
<p class="smallText metadata"> <p class="smallText metadata">
syntax highlighting based on <a href="https://pygments.org/">Pygments'</a> default syntax highlighting based on <a href="https://pygments.org/">Pygments'</a> default
colors colors
</p> </p>
<p class="smallText metadata"> <p class="smallText metadata">
page generated by <a href="https://git.alv.cx/alvierahman90/notes2web">notes2web</a> page generated by <a href="https://git.alv.cx/alvierahman90/gronk">gronk</a>
</p> </p>
<details id="commitLog"> {% if license %}
<summary class="smallText">
Commit log (file history)
</summary>
$filehistory$
</details>
<details id="license"> <details id="license">
<summary class="smallText"> <summary class="smallText">
License License
</summary> </summary>
<pre>$licenseFull$</pre> <pre>{{ license }}</pre>
</details> </details>
$body$ {% endif %}
</div> {% block body_content %}
</div> {{ content|safe }}
<script src="/fuse.js"> </script> {% endblock %}
<script src="/toc_search.js"> </script> {% endblock %}
</body>

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>

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,28 +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 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>
<p class="smallText"> page generated by <a href="https://github.com/alvierahman90/notes2web">notes2web</a> (commit $n2w_commit$) notes commit $notes_git_head_sha1$</p>
</div>
<script src="/fuse.js"> </script>
<script> const data = $data$ </script>
<script src="/search.js"> </script>
</body>

View File

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

View File

@ -1,21 +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$
<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>
<ul id="searchResults" 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 }}

10
templates/permalink.html Normal file
View File

@ -0,0 +1,10 @@
{% 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>

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>