diff --git a/.gitignore b/.gitignore index c7ac740..54813d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -_gen \ No newline at end of file +_gen +static/syntax.css \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8ec4397 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +all: static/syntax.css + +static/syntax.css: static/light-syntax.css static/dark-syntax.css + cat static/light-syntax.css > $@ + echo '@media (prefers-color-scheme: dark) { ' >> $@ + cat static/dark-syntax.css >> $@ + echo '}' >> $@ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea46267 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Codigo para llevar's generator + +This is the static site generator used to build [Codigo Para Llevar](https://codigoparallevar.com/) (my personal site). It contains: + +- A markdown blog (with content ported from acrylamid). Saved as `/blog/`. +- A set of org-mode based notes. Saved as `/notes/`. + +It also copies over some static assets (css, js, fonts). + +The scripts are hardcoded with the hostnames and paths for my own site, so you might want to update them. + +General documentation is in progress and might be replaced little by little by the more interactive [org-web-editor](https://code.codigoparallevar.com/kenkeiras/org-web-editor) once that one (1) supports all the features here and (2) has support for building static sites. + +## Instructions + +Generally, what you want to do is to run `make` once to prepare the static files, then run this to generate the notes. + +```bash +mkdir -p _gen +WATCH_AND_REBUILD=0 python3 scripts/generate.py _gen/notes [] +``` + +Use `WATCH_AND_REBUILD=1` (or empty) for automatic rebuilds. + +## Filtering + +This won't render **all** notes, but try to select the PUBLIC ones and skip the PRIVATE ones. + +PUBLIC files are contained on the DEFAULT_SUBPATH, PRIVATE headlines have the `:private:` tag. diff --git a/scripts/autoserve.py b/scripts/autoserve.py new file mode 100644 index 0000000..ab6f149 --- /dev/null +++ b/scripts/autoserve.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 + +import sys +import http.server +import socketserver +import threading +import os +import time +import select + +import urllib.parse + +import inotify.adapters + +PORT = int(os.getenv('PORT', 8000)) + +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) + +MONITORED_EVENT_TYPES = ( + 'IN_CREATE', + # 'IN_MODIFY', + 'IN_CLOSE_WRITE', + 'IN_DELETE', + 'IN_MOVED_FROM', + 'IN_MOVED_TO', + 'IN_DELETE_SELF', + 'IN_MOVE_SELF', +) + +WAITING_RESPONSES = [] +SLEEP_TIME = 0.5 +COUNTER = 0 +MAX_WAITS = 100 + +class Server(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + if self.path.strip('/') == '__wait_for_changes': + WAITING_RESPONSES.append(self) + print(len(WAITING_RESPONSES), "waiting responses") + global COUNTER + ticket, COUNTER = COUNTER, COUNTER + 1 + + while self in WAITING_RESPONSES: + # This is an horribe way to wait! ... but it may work for quick tests 🤷 + + if COUNTER - ticket > MAX_WAITS: + # Connection closed by the other side + print("Manually closed for cleanup") + WAITING_RESPONSES.remove(self) + # send 502 response, timeout + self.send_response(502) + # send response headers + self.end_headers() + + return + + time.sleep(SLEEP_TIME) + return + + path = urllib.parse.unquote(self.path) + if path.strip('/') == '': + path = '/index.html' + if os.path.isdir(path.strip('/')): + if path.endswith('/'): + path = path.strip('/') + '/index.html' + else: + # Redirect to + / + self.send_response(301) + self.send_header('Location', path + '/') + self.end_headers() + return + + if not os.path.exists(path.strip('/')): + self.send_response(404) + self.end_headers() + return + + # send 200 response + self.send_response(200) + # send response headers + self.end_headers() + + with open(path.strip('/'), 'rb') as f: + # send the body of the response + self.wfile.write(f.read()) + + if not path.endswith('.html'): + return + else: + # Append update waiter + with open(os.path.join(THIS_DIR, 'wait_for_update.js'), 'rb') as f: + new_data = b'' + self.wfile.write(new_data) + new_data_len = len(new_data) + + return + +def notify_reloads(): + while len(WAITING_RESPONSES) > 0: + # Close opened connections + res = WAITING_RESPONSES.pop(0) + + try: + # send 200 response + res.send_response(200) + # send response headers + res.end_headers() + except Exception as e: + print("ERROR:", e) + + global COUNTER + COUNTER = 0 + + +def start_notifier(): + notifier = inotify.adapters.InotifyTree(os.getcwd()) + should_reload = False + for event in notifier.event_gen(yield_nones=True): + if event is None: + if should_reload: + print("Reloading!") + should_reload = False + notify_reloads() + continue + + (ev, types, directory, file) = event + if any([type in MONITORED_EVENT_TYPES for type in types]): + print("Detected change!", types, directory, file) + should_reload = True + +def serve(): + Handler = Server + notifier_thread = threading.Thread(target=start_notifier) + + with http.server.ThreadingHTTPServer(("127.0.0.1", PORT), Handler) as httpd: + print("serving at port", PORT) + notifier_thread.start() + httpd.serve_forever() + + +if __name__ == '__main__': + serve() diff --git a/scripts/blog.py b/scripts/blog.py new file mode 100644 index 0000000..d177201 --- /dev/null +++ b/scripts/blog.py @@ -0,0 +1,596 @@ +#!/usr/bin/env python3 + +MARKDOWN_EXTENSION = '.md' +EXTENSIONS = [ + MARKDOWN_EXTENSION, +] + +MARKDOWN_EXTRA_FEATURES = [ + # See more in: https://python-markdown.github.io/extensions/ + 'markdown.extensions.fenced_code', + 'markdown.extensions.codehilite', + 'markdown.extensions.extra', +] + +import copy +import json +import logging +import sys +import os +import datetime +import shutil +import traceback +import time +import re +from typing import List + +from bs4 import BeautifulSoup as bs4 +import bs4 as BeautifulSoup +import jinja2 +import inotify.adapters +import yaml +import markdown +from unidecode import unidecode + +SUMMARIZE_MAX_TOKENS = 1000 +ITEMS_IN_RSS = 50 + +NIKOLA_DATE_RE = re.compile(r'^([0-2]\d|30|31)\.(0\d|1[012])\.(\d{4}), (\d{1,2}):(\d{2})$') + +COMPLETE_DATE_RE = re.compile(r'^(\d{4})-(0\d|1[012])-([0-2]\d|30|31) ' + + r'(\d{2}):(\d{2})(:\d{2})( .+)?$') +SLUG_HYPHENATE_RE = re.compile(r'[\s\-]+') +SLUG_REMOVE_RE = re.compile(r'[^\s\-a-zA-Z0-9]*') + +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +STATIC_PATH = os.path.join(ROOT_DIR, 'static') +ARTICLE_TEMPLATE_NAME = 'article.tmpl.html' +BLOG_INDEX_TEMPLATE_NAME = 'blog_index.tmpl.html' +CATEGORY_LIST_TEMPLATE_NAME = 'category_list.tmpl.html' +ARTICLE_LIST_TEMPLATE_NAME = 'article_list.tmpl.html' +RSS_TEMPLATE_NAME = 'rss.tmpl.xml' +BLOG_INDEX_PAGE_SIZE = 10 + +STATIC_RESOURCES = ( + ('style.css', 'css/style.css'), + ('light-syntax.css', 'css/light-syntax.css'), + ('dark-syntax.css', 'css/dark-syntax.css', ('@media (prefers-color-scheme: dark) {\n', '\n}')), +) + +JINJA_ENV = jinja2.Environment( + loader=jinja2.FileSystemLoader(STATIC_PATH), + autoescape=jinja2.select_autoescape() +) + +WATCH = True +if os.getenv('WATCH_AND_REBUILD', '1') == '0': + WATCH = False + +def update_statics(): + global ARTICLE_TEMPLATE + ARTICLE_TEMPLATE = JINJA_ENV.get_template(ARTICLE_TEMPLATE_NAME) + global BLOG_INDEX_TEMPLATE + BLOG_INDEX_TEMPLATE = JINJA_ENV.get_template(BLOG_INDEX_TEMPLATE_NAME) + global CATEGORY_LIST_TEMPLATE + CATEGORY_LIST_TEMPLATE = JINJA_ENV.get_template(CATEGORY_LIST_TEMPLATE_NAME) + global ARTICLE_LIST_TEMPLATE + ARTICLE_LIST_TEMPLATE = JINJA_ENV.get_template(ARTICLE_LIST_TEMPLATE_NAME) + global RSS_TEMPLATE + RSS_TEMPLATE = JINJA_ENV.get_template(RSS_TEMPLATE_NAME) + +update_statics() + +MONITORED_EVENT_TYPES = ( + 'IN_CREATE', + # 'IN_MODIFY', + 'IN_CLOSE_WRITE', + 'IN_DELETE', + 'IN_MOVED_FROM', + 'IN_MOVED_TO', + 'IN_DELETE_SELF', + 'IN_MOVE_SELF', +) +LANG_PRIORITY = ('en', 'es', 'gl') + + +def parse_nikola_date(match): + return datetime.datetime(year=int(match.group(3)), + month=int(match.group(2)), + day=int(match.group(1)), + hour=int(match.group(4)), + minute=int(match.group(5)), + # Note this final assumption is not good + # and might get you in trouble if trying + # to sort closely-published posts + # when others are in complete-date format + tzinfo=datetime.timezone.utc, + ) + + +def parse_complete_date(match): + return datetime.datetime.strptime(match.group(0), '%Y-%m-%d %H:%M:%S %Z%z') + +def split_tags(tags: str) -> List[str]: + if isinstance(tags, str): + return [tag.strip() for tag in tags.split(',')] + elif isinstance(tags, list): + return tags + else: + raise NotImplementedError("Unknown tag type: {}".format(type(tags))) + +def slugify(title): + """ + Made for compatibility with Nikola's slugify within CodigoParaLlevar blog. + """ + slug = unidecode(title).lower() + slug = SLUG_REMOVE_RE.sub('', slug) + slug = SLUG_HYPHENATE_RE.sub('-', slug) + slug = slug.strip('-') + + return slug.strip() + + +def read_markdown(path): + with open(path, 'rt') as f: + data = f.read() + if data.startswith('---'): + start = data.index('\n') + if '---\n' not in data[start:]: + raise Exception('Front matter not finished on: {}'.format(path)) + front_matter_str, content = data[start:].split('---\n', 1) + front_matter = yaml.load(front_matter_str, Loader=yaml.SafeLoader) + else: + raise Exception('Front matter is needed for proper rendering. Not found on: {}'.format( + path + )) + doc = markdown.markdown(content, extensions=MARKDOWN_EXTRA_FEATURES) + return doc, front_matter + + +def get_out_path(front_matter): + if 'date' in front_matter: + if m := NIKOLA_DATE_RE.match(front_matter['date']): + front_matter['date'] = parse_nikola_date(m) + elif m := COMPLETE_DATE_RE.match(front_matter['date']): + front_matter['date'] = parse_complete_date(m) + else: + raise NotImplementedError('Unknown date format: {}'.format( + front_matter['date'])) + else: + raise Exception('No date found on: {}'.format( + path + )) + + if 'slug' not in front_matter: + if 'title' not in front_matter: + raise Exception('No title found on: {}'.format( + path + )) + + front_matter['slug'] = slugify(front_matter['title']) + + out_path = os.path.join(str(front_matter['date'].year), front_matter['slug']) + if front_matter.get('lang', LANG_PRIORITY[0]) != LANG_PRIORITY[0]: + out_path = os.path.join(front_matter['lang'], str(front_matter['date'].year), front_matter['slug']) + return out_path + + +def load_all(top_dir_relative): + top = os.path.abspath(top_dir_relative) + + docs = {} + + count = 0 + for root, dirs, files in os.walk(top): + for name in files: + if all([not name.endswith(ext) for ext in EXTENSIONS]): + # The logic is negative... but it works + continue + + if name.endswith(MARKDOWN_EXTENSION): + path = os.path.join(root, name) + doc, front_matter = read_markdown(path) + out_path = get_out_path(front_matter) + docs[path] = (doc, front_matter, out_path) + print('\rLoading posts... {}'.format(count), end='', flush=True) + count += 1 + else: + raise NotImplementedError('Unknown filetype: {}'.format(name)) + + print(" [DONE]") + return docs + + +def load_doc(filepath): + doc, front_matter = read_markdown(filepath) + out_path = get_out_path(front_matter) + return (doc, front_matter, out_path) + + +def render_article(doc, front_matter, f, out_path): + extsep = '/' if '/' in out_path else '\\' + subdirs = len(out_path.split(extsep)) + base_path = os.path.join(*(['..'] * subdirs)) + result = ARTICLE_TEMPLATE.render( + content=doc, + title=front_matter['title'], + post_publication_date=front_matter['date'], + post_tags=split_tags(front_matter['tags']), + base_path=base_path, + ) + f.write(result) + +def summarize(doc): + tree = bs4(doc, features='lxml') + + html = list(tree.children)[0] + body = list(html.children)[0] + + comments = tree.find_all(string=lambda text: isinstance(text, BeautifulSoup.Comment)) + + teaser_end = None + for comment in comments: + if 'TEASER_END' in comment: + teaser_end = comment + break + + if 'gnucash' in doc: + assert teaser_end is not None + + def recur_select_to_summarize(source, dest, num_tokens): + for item in source.children: + if num_tokens + len(item.text) < SUMMARIZE_MAX_TOKENS: + # All source fits + num_tokens += len(item.text) + dest.append(item) + + else: + if not isinstance(item, BeautifulSoup.NavigableString): + # Let's take as much source as we can and then stop + subsect = bs4() + recur_select_to_summarize(item, subsect, num_tokens) + + if len(list(subsect.children)) > 0: + dest.append(subsect) + break + + def cut_after_element(reference): + while reference.next_sibling is None: + if reference.parent is None: + logging.warning("Reached root when looking for cutting point for teaser. Doc: {}".format(doc[:100])) + return + reference = reference.parent + + nxt = reference.next_sibling + while nxt is not None: + was = nxt + if reference.next_sibling is not None: + # Move to the "right" + nxt = reference.next_sibling + + else: + # Move "up and right" + nxt = reference.parent + if nxt is not None: + nxt = nxt.next_sibling + was.extract() + + if teaser_end is None: + result = bs4() + + recur_select_to_summarize(body, result, 0) + else: + summary = copy.copy(body) + comments = summary.find_all(string=lambda text: isinstance(text, BeautifulSoup.Comment)) + + teaser_end = None + for comment in comments: + if 'TEASER_END' in comment: + teaser_end = comment + break + assert teaser_end is not None, 'Error finding teaser end on copy' + + cut_after_element(teaser_end) + result = bs4() + for child in summary.children: + result.append(child) + + return result + +def render_index(docs, dest_top): + # Collect all languages accepted for all docs + docs_by_slug = {} + for (doc, front_matter, out_path) in docs.values(): + if front_matter['slug'] not in docs_by_slug: + docs_by_slug[front_matter['slug']] = {} + docs_by_slug[front_matter['slug']][front_matter.get('lang', LANG_PRIORITY[0])] = (doc, front_matter, out_path) + + # Remove duplicated for langs with less priority + selected_docs = [] + for (doc, front_matter, out_path) in docs.values(): + langs = docs_by_slug[front_matter['slug']] + lang_priority = LANG_PRIORITY.index(front_matter.get('lang', LANG_PRIORITY[0])) + min_lang_priority = min([ + LANG_PRIORITY.index(lang) + for lang in langs.keys() + ]) + if lang_priority == min_lang_priority: + selected_docs.append((doc, front_matter, out_path, langs)) + + docs = sorted(selected_docs, key=lambda x: x[1]['date'], reverse=True) + + index_ranges = range(0, len(docs), BLOG_INDEX_PAGE_SIZE) + + for off in index_ranges: + page = docs[off: off + BLOG_INDEX_PAGE_SIZE] + + posts = [ + { + "doc": doc, + "title": front_matter['title'], + "post_publication_date": front_matter['date'], + "post_tags": split_tags(front_matter['tags']), + "summary": summarize(doc), + "link": out_path.rstrip('/') + '/', + } + for (doc, front_matter, out_path, _alternatives) in page + ] + + prev_index_num = None + next_index_num = off // BLOG_INDEX_PAGE_SIZE + 1 + if off > 0: + prev_index_num = off // BLOG_INDEX_PAGE_SIZE - 1 + if next_index_num >= len(index_ranges): + next_index_num = None + + result = BLOG_INDEX_TEMPLATE.render( + posts=posts, + prev_index_num=prev_index_num, + next_index_num=next_index_num, + ) + + if off == 0: + fname = 'index.html' + else: + fname = 'index-{}.html'.format(off // BLOG_INDEX_PAGE_SIZE) + with open(os.path.join(dest_top, fname), 'wt') as f: + f.write(result) + +def render_categories(docs, dest_top): + categories = {} + for (doc, front_matter, out_path) in docs.values(): + for tag in split_tags(front_matter['tags']): + if tag not in categories: + categories[tag] = [] + categories[tag].append((doc, front_matter, out_path)) + + print("Found {} tags".format(len(categories), categories)) + for tag, docs in categories.items(): + docs = sorted(docs, key=lambda x: x[1]['date'], reverse=True) + + posts = [ + { + # "doc": doc, + "title": front_matter['title'], + "post_publication_date": front_matter['date'], + "post_tags": split_tags(front_matter['tags']), + # "summary": summarize(doc), + "link": out_path.rstrip('/') + '/', + } + for (doc, front_matter, out_path) in docs + ] + + result = CATEGORY_LIST_TEMPLATE.render( + posts=posts, + ) + path = os.path.join(dest_top, "tags", tag.replace('/', '_'), "index.html") + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'wt') as f: + f.write(result) + +def render_archive(docs, dest_top): + docs = sorted(docs.values(), key=lambda x: x[1]['date'], reverse=True) + + posts = [ + { + # "doc": doc, + "title": front_matter['title'], + "post_publication_date": front_matter['date'], + "post_tags": split_tags(front_matter['tags']), + # "summary": summarize(doc), + "link": out_path.rstrip('/') + '/', + } + for (doc, front_matter, out_path) in docs + ] + + result = ARTICLE_LIST_TEMPLATE.render( + posts=posts, + ) + path = os.path.join(dest_top, "articles", "index.html") + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'wt') as f: + f.write(result) + +def render_rss(docs, dest_top): + # Collect all languages accepted for all docs + docs_by_slug = {} + for (doc, front_matter, out_path) in docs.values(): + if front_matter['slug'] not in docs_by_slug: + docs_by_slug[front_matter['slug']] = {} + docs_by_slug[front_matter['slug']][front_matter.get('lang', LANG_PRIORITY[0])] = (doc, front_matter, out_path) + + # Remove duplicated for langs with less priority + selected_docs = [] + for (doc, front_matter, out_path) in docs.values(): + langs = docs_by_slug[front_matter['slug']] + lang_priority = LANG_PRIORITY.index(front_matter.get('lang', LANG_PRIORITY[0])) + min_lang_priority = min([ + LANG_PRIORITY.index(lang) + for lang in langs.keys() + ]) + if lang_priority == min_lang_priority: + selected_docs.append((doc, front_matter, out_path, langs)) + + docs = sorted(selected_docs, key=lambda x: x[1]['date'], reverse=True) + + posts = [ + { + # "doc": doc, + "title": front_matter['title'], + "post_publication_date": front_matter['date'], + "post_tags": split_tags(front_matter['tags']), + "summary": summarize(doc), + "link": out_path.rstrip('/') + '/', + } + for (doc, front_matter, out_path, langs) in docs[:ITEMS_IN_RSS] + ] + + result = RSS_TEMPLATE.render( + posts=posts, + last_build_date=datetime.datetime.utcnow(), + ) + path = os.path.join(dest_top, "rss.xml") + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'wt') as f: + f.write(result) + + +def regen_all(source_top, dest_top, docs=None): + if docs is None: + docs = load_all(source_top) + + # Render posts + for (doc, front_matter, out_path) in docs.values(): + doc_full_path = os.path.join(dest_top, out_path) + os.makedirs(os.path.dirname(doc_full_path), exist_ok=True) + # print("==", doc_full_path) + full_out_path = doc_full_path + '/index.html' + os.makedirs(os.path.dirname(full_out_path), exist_ok=True) + with open(full_out_path, 'wt') as f: + try: + render_article(doc, front_matter, f, out_path) + except: + logging.error(traceback.format_exc()) + logging.error("Rendering failed 😿") + continue + + # Render statics + for static in STATIC_RESOURCES: + src_path = static[0] + dest_path = static[1] + + if len(static) > 2: + before, after = static[2] + else: + before, after = '', '' + target_dest = os.path.join(dest_top, dest_path) + os.makedirs(os.path.dirname(target_dest), exist_ok=True) + with open(os.path.join(STATIC_PATH, src_path), 'rt') as src: + data = before + src.read() + after + + with open(target_dest, 'wt') as f: + f.write(data) + + # Render index + render_index(docs, dest_top) + + # Render categories + render_categories(docs, dest_top) + + # Render archive + render_archive(docs, dest_top) + + # Render RSS + render_rss(docs, dest_top) + + return docs + + +def main(source_top, dest_top): + notifier = inotify.adapters.InotifyTrees([source_top, STATIC_PATH]) + + ## Initial load + t0 = time.time() + logging.info("Initial load...") + docs = regen_all(source_top, dest_top) + logging.info("Initial load completed in {:.2f}s".format(time.time() - t0)) + + if not WATCH: + logging.info("Build completed in {:.2f}s".format(time.time() - t0)) + return 0 + + ## Updating + for event in notifier.event_gen(yield_nones=False): + (ev, types, directory, file) = event + if not any([type in MONITORED_EVENT_TYPES for type in types]): + continue + filepath = os.path.join(directory, file) + if filepath.startswith(STATIC_PATH): + t0 = time.time() + try: + update_statics() + except: + logging.error(traceback.format_exc()) + logging.error("Loading new templates failed 😿") + continue + + is_static_resource = False + for static in STATIC_RESOURCES: + src_path = static[0] + dest_path = static[1] + if file == os.path.basename(src_path): + is_static_resource = True + + if len(static) > 2: + before, after = static[2] + else: + before, after = '', '' + target_dest = os.path.join(dest_top, dest_path) + os.makedirs(os.path.dirname(target_dest), exist_ok=True) + with open(os.path.join(STATIC_PATH, src_path), 'rt') as src: + data = before + src.read() + after + + with open(target_dest, 'wt') as f: + f.write(data) + + if is_static_resource: + logging.info("Updated static resources in {:.2f}s".format(time.time() - t0)) + else: + docs = regen_all(source_top, dest_top, docs) + logging.info("Updated all in {:.2f}s".format(time.time() - t0)) + + else: + try: + print("Reloading: {}".format(filepath)) + (doc, front_matter, out_path) = load_doc(filepath) + except: + logging.error(traceback.format_exc()) + logging.error("Skipping update 😿") + continue + + t0 = time.time() + docs[filepath] = (doc, front_matter, out_path) + doc_full_path = os.path.join(dest_top, out_path) + print("Updated: {}.html".format(doc_full_path)) + os.makedirs(os.path.dirname(doc_full_path + '/index.html'), exist_ok=True) + # print("==", doc_full_path) + with open(doc_full_path + '/index.html', 'wt') as f: + try: + render_article(doc, front_matter, f, out_path) + render_archive(docs, dest_top) + except: + logging.error(traceback.format_exc()) + logging.error("Rendering failed 😿") + continue + + logging.info("Updated all in {:.2f}s".format(time.time() - t0)) + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: {} SOURCE_TOP DEST_TOP".format(sys.argv[0])) + exit(0) + + logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s") + main(sys.argv[1], sys.argv[2]) diff --git a/scripts/gen_centered_graph.py b/scripts/gen_centered_graph.py new file mode 100644 index 0000000..05692b1 --- /dev/null +++ b/scripts/gen_centered_graph.py @@ -0,0 +1,165 @@ +import subprocess +import ops_cache +import copy +import tempfile +import os + + +@ops_cache.cache +def gen(headline_id, graph, doc_to_headline_remapping): + reference_node = headline_id + font_name = 'monospace' + + linked_from_internal = set() + g = copy.deepcopy(graph) + + if 'id:' + reference_node in doc_to_headline_remapping: + reference_node = doc_to_headline_remapping['id:' + reference_node].split(':', 1)[1] + + centered_graph = { reference_node: g[reference_node] } + for l in g[reference_node]['links']: + lt = l['target'] + if lt.startswith("id:"): + lt = lt[3:] + linked_from_internal.add(lt) + del g[reference_node] + new_nodes = True + + in_emacs_tree = { + reference_node: set(), + } + + while new_nodes: + new_nodes = False + removed = set() + for k, v in g.items(): + if 'id:' + k in doc_to_headline_remapping: + k = doc_to_headline_remapping['id:' + k].split(':', 1)[1] + + for link in v["links"]: + if link["target"].startswith("id:"): + link["target"] = link["target"][3:] + if link['target'] in centered_graph and link.get('relation') == 'in': + centered_graph[k] = v + + for l in v["links"]: + if l.get('relation') == 'in': + t = l['target'] + if t.startswith("id:"): + t = t[3:] + + if '[' in t: + # Special case, to be handled on org_rw + continue + + if t not in in_emacs_tree: + in_emacs_tree[t] = set() + in_emacs_tree[t].add(k) + + v['links'] = [ + l for l in v["links"] + if l.get('relation') != 'in' + ] + for l in v['links']: + lt = l['target'] + if lt.startswith("id:"): + lt = lt[3:] + linked_from_internal.add(lt) + + removed.add(k) + new_nodes = True + break + for k in removed: + del g[k] + + in_emacs = set(centered_graph.keys()) + + # One more round for the rest, not requiring "in" + for k, v in g.items(): + if 'id:' + k in doc_to_headline_remapping: + k = doc_to_headline_remapping['id:' + k].split(':', 1)[1] + + backlinked = False + for link in v["links"]: + if link["target"].startswith("id:"): + link["target"] = link["target"][3:] + if link['target'] in in_emacs: + centered_graph[k] = v + backlinked = True + removed.add(k) + if not backlinked and (k in linked_from_internal): + centered_graph[k] = v + removed.add(k) + + g = centered_graph + + with tempfile.NamedTemporaryFile(suffix='.dot', mode='wt') as f: + f.write('strict digraph {\n') + f.write('maxiter=10000\n') + f.write('splines=curved\n') + # f.write('splines=spline\n') # Not supported with edges to cluster + f.write('node[shape=rect, width=0.5, height=0.5]\n') + f.write('K=0.3\n') + f.write('edge[len = 1]\n') + def draw_subgraph(node_id, depth): + f.write("subgraph cluster_{} {{\n".format(node_id.replace("-", "_"))) + f.write(' URL="./{}.node.html"\n'.format(node_id)) + f.write(' class="{}"\n'.format('cluster-depth-' + str(depth - 1))) + f.write(" fontname=\"{}\"\n".format(font_name)) + f.write(" label=\"{}\"\n".format(g[node_id]['title'].replace("\"", "'"))) + f.write("\n") + + # print("T: {}".format(in_emacs_tree), file=sys.stderr) + for k in in_emacs_tree[node_id]: + v = g[k] + + if k in in_emacs_tree: + draw_subgraph(k, depth=depth + 1) + else: + print(" _" + k.replace("-", "_") + + "[label=\"" + v["title"].replace("\"", "'") + "\", " + + "URL=\"" + k + ".node.html\", " + + "fontname=\"" + font_name + "\", " + + "class=\"cluster-depth-" + str(depth) + "\"" + + "];", file=f) + + + f.write("\n}\n") + + draw_subgraph(reference_node, 1) + + for k, v in g.items(): + if k not in in_emacs: + print("_" + k.replace("-", "_") + + "[label=\"" + v["title"].replace("\"", "'") + "\", " + + "fontname=\"" + font_name + "\", " + + "URL=\"" + k + ".node.html\"];", file=f) + + for k, v in g.items(): + link_src = '_' + k.replace("-", "_") + if k in in_emacs_tree: + link_src = 'cluster_{}'.format(k.replace("-", "_")) + + for link in v["links"]: + if link["target"].startswith("id:"): + link["target"] = link["target"][3:] + + if '[' in link['target']: + # Special case, to be handled on org_rw + continue + if link['target'] not in g: + # Irrelevant + continue + if link['target'] in in_emacs_tree: + t = 'cluster_{}'.format(link['target'].replace("-", "_")) + else: + t = "_" + link["target"].replace("-", "_") + print(link_src + "->" + t, file=f) + + f.write('}\n') + f.flush() + + with tempfile.NamedTemporaryFile(suffix='.svg') as fsvg: + subprocess.call(['fdp', f.name, '-Tsvg', '-o', fsvg.name]) + fsvg.seek(0) + return fsvg.read().decode() diff --git a/scripts/generate.py b/scripts/generate.py index 05c3f63..12bac97 100644 --- a/scripts/generate.py +++ b/scripts/generate.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +import sqlite3 +import time import json import html import logging @@ -7,6 +9,12 @@ import os import sys import uuid from datetime import datetime +import traceback +import re +from itertools import chain +import shutil + +import inotify.adapters import org_rw from org_rw import OrgTime, dom, Link @@ -14,12 +22,79 @@ from org_rw import dump as dump_org from org_rw import load as load_org from org_rw import token_list_to_raw +import pygments +import pygments.lexers +import pygments.formatters +import gen_centered_graph + +# Set custom states +for state in ("NEXT", "MEETING", "Q", "PAUSED", "SOMETIME", "TRACK", "WAITING"): + org_rw.DEFAULT_TODO_KEYWORDS.append(state) + +for state in ("DISCARDED", "VALIDATING"): + org_rw.DEFAULT_DONE_KEYWORDS.append(state) + EXTENSIONS = [ ".org", ".org.txt", ] +IMG_EXTENSIONS = set([ + "svg", + "png", + "jpg", + "jpeg", + "gif", +]) +SKIPPED_TAGS = set(['attach']) +DEFAULT_SUBPATH = "public" + +WATCH = True +if os.getenv('WATCH_AND_REBUILD', '1') == '0': + WATCH = False MIN_HIDDEN_HEADLINE_LEVEL = 2 +INDEX_ID = os.getenv("INDEX_ID", "ea48ec1d-f9d4-4fb7-b39a-faa7b6e2ba95") +SITE_NAME = "Código para llevar" + +MONITORED_EVENT_TYPES = ( + 'IN_CREATE', + # 'IN_MODIFY', + 'IN_CLOSE_WRITE', + 'IN_DELETE', + 'IN_MOVED_FROM', + 'IN_MOVED_TO', + 'IN_DELETE_SELF', + 'IN_MOVE_SELF', +) + +TEXT_OR_LINK_RE = re.compile(r'([^\s\[\]]+|.)') + +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +STATIC_PATH = os.path.join(ROOT_DIR, 'static') + +class NonExistingLocalNoteError(AssertionError): + def __init__(self, note_id, src_headline): + AssertionError.__init__(self) + self.note_id = note_id + self.src_headline = src_headline + + def get_message(self): + return ("Cannot follow link to '{}' on headline '{}' ({})" + .format(self.note_id, + self.src_headline.id, + self.src_headline.title.get_text().strip())) + +def is_git_path(path): + return any([chunk == ".git" for chunk in path.split(os.sep)]) + +def create_db(path): + if os.path.exists(path): + os.unlink(path) + + db = sqlite3.connect(path) + db.execute('CREATE VIRTUAL TABLE note_search USING fts5(note_id, title, body, top_level_title, is_done, is_todo, tokenize="trigram");') + return db def load_all(top_dir_relative): top = os.path.abspath(top_dir_relative) @@ -34,7 +109,9 @@ def load_all(top_dir_relative): path = os.path.join(root, name) try: - doc = load_org(open(path), extra_cautious=True) + doc = load_org(open(path), + environment={"org-todo-keywords": "TODO(t) NEXT(n) MEETING(m/!) Q(q) PAUSED(p!/!) EVENT(e/!) SOMETIME(s) WAITING(w@/!) TRACK(r/!) | DISCARDED(x@/!) VALIDATING(v!/!) DONE(d!/!)"}, + extra_cautious=True) docs.append(doc) except Exception as err: import traceback @@ -46,26 +123,63 @@ def load_all(top_dir_relative): logging.info("Collected {} files".format(len(docs))) return docs +def remove_non_public_headlines(doc: org_rw.OrgDoc | org_rw.Headline): + if isinstance(doc, org_rw.OrgDoc): + doc.headlines = list(filter_private_headlines(doc.headlines)) + for hl in doc.headlines: + remove_non_public_headlines(hl) + else: + doc.children = list(filter_private_headlines(doc.children)) + for hl in doc.children: + remove_non_public_headlines(hl) -def main(src_top, dest_top): - docs = load_all(src_top) +def filter_private_headlines(headlines): + for hl in headlines: + if 'private' not in hl.tags: + yield hl + +def regen_all(src_top, dest_top, subpath, *, docs=None, db=None): files_generated = 0 + cur = db.cursor() + cleaned_db = False + try: + cur.execute('DELETE FROM note_search;') + cleaned_db = True + except sqlite3.OperationalError as err: + if WATCH: + logging.warning("Error pre-cleaning DB, search won't be updated") + else: + raise + + docs = load_all(src_top) + base_dirs = set() doc_to_headline_remapping = {} os.makedirs(dest_top, exist_ok=True) - graph = {} + + ## Build headline list + # This includes a virtual headline for ID-referenced documents. + all_headlines = [] + main_headlines_by_path = {} + main_headline_to_docid = {} for doc in docs: relpath = os.path.relpath(doc.path, src_top) + + remove_non_public_headlines(doc) changed = False headlines = list(doc.getAllHeadlines()) related = None + if not relpath.startswith(subpath + "/"): + # print("Skip:", relpath) + continue + base_dirs.add(os.path.dirname(relpath)) i = len(headlines) while i > 0: i -= 1 headline = headlines[i] - if headline.title.strip().lower() == "related" and headline.depth == 1: + if headline.title.get_text().strip().lower() == "related" and headline.depth == 1: if related is not None: print( "Found duplicated related: {} vs {}".format( @@ -85,10 +199,7 @@ def main(src_top, dest_top): print("Updated", relpath) save_changes(doc) - if not relpath.startswith("public/"): - # print("Skip:", relpath) - continue - + all_headlines.extend(headlines) main_headline = None topHeadlines = doc.getTopHeadlines() @@ -96,13 +207,11 @@ def main(src_top, dest_top): or (len(topHeadlines) == 2 and related is not None)): main_headline = [h for h in topHeadlines if h != related][0] + main_headlines_by_path[doc.path] = main_headline if doc.id is not None: - endpath = os.path.join(dest_top, doc.id + ".node.html") - with open(endpath, "wt") as f: - doc_to_headline_remapping['id:' + doc.id] = 'id:' + main_headline.id - - f.write(as_document(render(main_headline, doc, headlineLevel=0))) - files_generated += 1 + doc_to_headline_remapping['id:' + doc.id] = 'id:' + main_headline.id + main_headline_to_docid[main_headline.id] = doc.id + files_generated += 1 elif doc.id is not None: logging.error("Cannot render document from id: {}. {} headlines {} related".format( relpath, @@ -110,51 +219,78 @@ def main(src_top, dest_top): 'with' if related is not None else 'without' )) - for headline in headlines: - endpath = os.path.join(dest_top, headline.id + ".node.html") + # Build graph + graph = {} + backlink_graph = {} + for headline in all_headlines: + links = [] + headline_links = list(headline.get_links()) + if headline == main_headline and related is not None: + headline_links.extend(list(related.get_links())) - links = [] - headline_links = list(headline.get_links()) - if headline == main_headline and related is not None: - headline_links.extend(list(related.get_links())) + for l in headline_links: + if l.value.startswith('http://') or l.value.startswith('https://'): + pass # Ignore for now, external URL + elif l.value.startswith('id:'): + links.append({'target': l.value}) + elif l.value.startswith('attachment:'): + pass # Ignore, attachment + elif l.value.startswith('file:'): + pass # Ignore, attachment + elif l.value.startswith('notmuch:'): + pass # Ignore, mail + elif l.value.startswith('orgit-rev:'): + pass # Ignore, mail + elif l.value.startswith('*'): + pass # Ignore, internal + elif not ':' in l.value.split()[0]: + pass # Ignore, internal + elif l.value.startswith('./'): + pass # TODO: Properly handle + else: + logging.warning('On document {}, unknown link to {}'.format(doc.path, l.value)) - for l in headline_links: - if l.value.startswith('http://') or l.value.startswith('https://'): - pass # Ignore for now, external URL - elif l.value.startswith('id:'): - links.append({'target': l.value}) - elif l.value.startswith('attachment:'): - pass # Ignore, attachment - elif l.value.startswith('file:'): - pass # Ignore, attachment - elif l.value.startswith('notmuch:'): - pass # Ignore, mail - elif l.value.startswith('orgit-rev:'): - pass # Ignore, mail - elif l.value.startswith('*'): - pass # Ignore, internal - elif not ':' in l.value.split()[0]: - pass # Ignore, internal - elif l.value.startswith('./'): - pass # TODO: Properly handle - else: - raise NotImplementedError('On document {}, link to {}'.format(doc.path, l.value)) + if headline.parent: + if isinstance(headline.parent, org_rw.Headline): + links.append({ + "target": headline.parent.id, + "relation": "in" + }) + for backlink in links: + if 'relation' in backlink and backlink['relation'] == 'in': + continue - if headline.parent: - if isinstance(headline.parent, org_rw.Headline): - links.append({ - "target": headline.parent.id, - "relation": "in" - }) - graph[headline.id] = { - "title": headline.title.strip(), - "links": links, - "depth": headline.depth, - } + target = backlink['target'] + if target.startswith('id:'): + target = target[len('id:'):] - with open(endpath, "wt") as f: - f.write(as_document(render(headline, doc, headlineLevel=0))) - files_generated += 1 + if target not in backlink_graph: + backlink_graph[target] = set() + + backlink_graph[target].add(headline.id) + + graph[headline.id] = { + "title": org_rw.org_rw.token_list_to_plaintext(headline.title.contents).strip(), + "links": links, + "depth": headline.depth, + } + if headline.id in main_headline_to_docid: + graph[main_headline_to_docid[headline.id]] = graph[headline.id] + + topLevelHeadline = headline + while isinstance(topLevelHeadline.parent, org_rw.Headline): + topLevelHeadline = topLevelHeadline.parent + + # Save for full-text-search + cur.execute('''INSERT INTO note_search(note_id, title, body, top_level_title, is_done, is_todo) VALUES (?, ?, ?, ?, ?, ?);''', + ( + headline.id, + headline.title.get_text(), + '\n'.join(headline.doc.dump_headline(headline, recursive=False)), + topLevelHeadline.title.get_text(), + headline.is_done, + headline.is_todo, + )) # Update graph, replace document ids with headline ids for headline_data in graph.values(): @@ -162,6 +298,17 @@ def main(src_top, dest_top): if link['target'] in doc_to_headline_remapping: link['target'] = doc_to_headline_remapping[link['target']] + # Remap document ids backlinks to main headlines + for doc_id, main_headline_id in doc_to_headline_remapping.items(): + if doc_id.startswith('id:'): + doc_id = doc_id[len('id:'):] + if main_headline_id.startswith('id:'): + main_headline_id = main_headline_id[len('id:'):] + for backlink in backlink_graph.get(doc_id, []): + if main_headline_id not in backlink_graph: + backlink_graph[main_headline_id] = set() + backlink_graph[main_headline_id].add(backlink) + # Output graph files graphpath = os.path.join(dest_top, "graph.json") graph_explorer_path = os.path.join(dest_top, "graph.html") @@ -169,143 +316,491 @@ def main(src_top, dest_top): json.dump(obj=graph, fp=f, indent=2) graph_explorer_path = os.path.join(dest_top, "graph.html") with open(graph_explorer_path, 'wt') as f: - with open(os.path.join(os.path.dirname(os.path.abspath(dest_top)), 'static', 'graph_explorer.html'), 'rt') as template: + with open(os.path.join(os.path.dirname(os.path.abspath(dest_top)), '..', 'static', 'graph_explorer.html'), 'rt') as template: source = template.read() f.write(source.replace('', json.dumps(graph))) logging.info("Generated {} files".format(files_generated)) + # Render docs after we've built the graph + # Render main headlines + full_graph_info = { "nodes": graph, "backlinks": backlink_graph, "main_headlines": main_headlines_by_path } + for _docpath, main_headline in main_headlines_by_path.items(): + if main_headline.doc.id: + endpath = os.path.join(dest_top, main_headline.doc.id + ".node.html") + with open(endpath, "wt") as f: + f.write(render_as_document(main_headline, main_headline.doc, headlineLevel=0, graph=full_graph_info, + doc_to_headline_remapping=doc_to_headline_remapping, + title=org_rw.token_list_to_plaintext(main_headline.title.contents))) -def print_tree(tree, indentation=0): + # Render all headlines + for headline in all_headlines: + endpath = os.path.join(dest_top, headline.id + ".node.html") + + # Render HTML + with open(endpath, "wt") as f: + f.write(render_as_document(headline, headline.doc, headlineLevel=0, graph=full_graph_info, + doc_to_headline_remapping=doc_to_headline_remapping, + title=org_rw.token_list_to_plaintext(headline.title.contents))) + files_generated += 1 + + if headline.id == INDEX_ID: + index_endpath = os.path.join(dest_top, "index.html") + with open(index_endpath, "wt") as f: + f.write(render_as_document(headline, headline.doc, headlineLevel=0, graph=full_graph_info, + doc_to_headline_remapping=doc_to_headline_remapping, + title=org_rw.token_list_to_plaintext(headline.title.contents))) + files_generated += 1 + + cur.close() + db.commit() + + logging.info("Copying attachments") + attachments_dir = os.path.join(dest_top, 'attachments') + os.makedirs(attachments_dir, exist_ok=True) + for base in base_dirs: + data_dir = os.path.join(src_top, base, 'data') + logging.info("Copying attachments from: {}".format(data_dir)) + if not os.path.exists(data_dir): + continue + for subdir in os.listdir(data_dir): + shutil.copytree(os.path.join(data_dir, subdir), + os.path.join(attachments_dir, subdir), + dirs_exist_ok=True) + + +def main(src_top, dest_top, subpath): + notifier = inotify.adapters.InotifyTrees([src_top, STATIC_PATH]) + + ## Initial load + t0 = time.time() + + os.makedirs(dest_top, exist_ok=True) + db = create_db(os.path.join(dest_top, 'db.sqlite3')) + docs = regen_all(src_top, dest_top, subpath=subpath, db=db) + + if not WATCH: + logging.info("Build completed in {:.2f}s".format(time.time() - t0)) + return 0 + + logging.info("Initial load completed in {:.2f}s".format(time.time() - t0)) + ## Updating + for event in notifier.event_gen(yield_nones=False): + (ev, types, directory, file) = event + if not any([type in MONITORED_EVENT_TYPES for type in types]): + continue + if is_git_path(directory): + continue + filepath = os.path.join(directory, file) + print("CHANGED: {}".format(filepath)) + t0 = time.time() + try: + docs = regen_all(src_top, dest_top, subpath=subpath, docs=docs, db=db) + except: + logging.error(traceback.format_exc()) + logging.error("Loading new templates failed 😿") + continue + logging.info("Updated all in {:.2f}s".format(time.time() - t0)) + +def get_headline_with_name(target_name, doc): + target_name = target_name.strip() + for headline in doc.getAllHeadlines(): + if headline.title.get_text().strip() == target_name: + return headline + + return None + +def assert_id_exists(id, src_headline, graph): + if id not in graph["nodes"]: + raise NonExistingLocalNoteError(id, src_headline) + +def print_tree(tree, indentation=0, headline=None): + # if headline and headline.id != INDEX_ID: + # return return for element in tree: - print(" " * indentation + "- " + str(type(element))) if "children" in dir(element): if len(element.children) > 0: - print_tree(element.children, indentation + 1) + print_element(element.children, indentation + 1, headline) print() + elif "content" in dir(element): + for content in element.content: + print_element(content, indentation + 1, headline) -def render_property_drawer(element, acc): +def print_element(element, indentation, headline): + if isinstance(element, org_rw.Link): + print(" " * indentation, "Link:", element.get_raw()) + elif isinstance(element, str): + print(" " * indentation, "{" + element + "}", type(element)) + else: + print_tree(element, indentation, headline) + + +def render_property_drawer(element, acc, headline, graph): pass -def render_logbook_drawer(element, acc): +def render_logbook_drawer(element, acc, headline, graph): pass -def render_property_node(element, acc): +def render_property_node(element, acc, headline, graph): pass -def render_list_group(element, acc): +def render_list_group(element, acc, headline, graph): acc.append("") +def render_table(element, acc, headline, graph): + acc.append("") + render_tree(element.children, acc, headline, graph) + acc.append("
") -def render_list_item(element, acc): +def render_table_row(element, acc, headline, graph): + acc.append("") + for cell in element.cells: + acc.append("") + acc.append(html.escape(cell)) + acc.append("") + acc.append("") + +def render_table_separator_row(element, acc, headline, graph): + acc.append("") + +def render_list_item(element, acc, headline, graph): acc.append("
  • ") if element.tag is not None: acc.append("") - acc.append(html.escape(element.tag)) + render_text_tokens(element.tag, acc, headline, graph) acc.append("") acc.append("") - render_text_tokens(element.content, acc) + render_text_tokens(element.content, acc, headline, graph) acc.append("
  • ") +def render_block(content, acc, _class, is_code): + acc.append('
    '.format(_class))
    +    if is_code:
    +        acc.append('')
     
    -def render_code_block(element, acc):
    -    acc.append('
    ')
    -    acc.append(html.escape(element.lines))
    -    acc.append('
    ') + # Remove indentation common to all lines + acc.append(unindent(content)) + if is_code: + acc.append('
    ') + acc.append('
    ') -def render_results_block(element, acc): - # TODO: - # acc.append('
    ')
    -    # render_tree(element.children, acc)
    -    # acc.append('
    ') - pass +def unindent(content): + base_indentation = min([ + len(l) - len(l.lstrip(' ')) + for l in content.split('\n') + if len(l.strip()) > 0 + ] or [0]) + content_lines = [ + l[base_indentation:] + for l in content.split('\n') + ] + return '\n'.join(content_lines) + +def render_code_block(element, acc, headline, graph): + code = element.lines + + if element.arguments is not None and len(element.arguments) > 0 : + try: + lexer = pygments.lexers.get_lexer_by_name(element.arguments.split()[0], stripall=True) + content = pygments.highlight(unindent(code), + lexer, + pygments.formatters.HtmlFormatter() + ) + acc.append(content) + return + + except pygments.util.ClassNotFound: + pass + logging.error("Cannot find lexer for {}".format(element.subtype.lower())) + content = html.escape(code) + render_block(content, acc, _class='code ' + element.subtype.lower(), is_code=True) -def render_text(element, acc): - acc.append('') - render_text_tokens(element.content, acc) - acc.append('') +def render_results_block(element, acc, headline, graph): + items = [e.get_raw() for e in element.children] + content = '\n'.join(items) + if len(content.strip()) > 0: + render_block(content, acc, _class='results lang-text', is_code=False) -def render_text_tokens(tokens, acc): +def render_generic_drawer_block(element, acc, headline, graph): + items = [e.get_raw() for e in element.children] + content = '\n'.join(items) + if len(content.strip()) > 0: + render_block(content, acc, _class='generic-drawer {}-drawer lang-text'.format(element.drawer_name), is_code=False) + +def render_org_text(element, acc, headline, graph): + as_dom = org_rw.text_to_dom(element.contents, element) + render_text_tokens(as_dom, acc, headline, graph) + +def render_text(element, acc, headline, graph): + acc.append('
    ') + render_text_tokens(element.content, acc, headline, graph) + acc.append('
    ') + +def render_text_tokens(tokens, acc, headline, graph): + acc.append('

    ') + if isinstance(tokens, org_rw.Text): + tokens = tokens.contents for chunk in tokens: if isinstance(chunk, str): - acc.append('{} '.format(chunk)) + lines = chunk.split('\n\n') + contents = [] + for line in lines: + line_chunks = [] + for word in TEXT_OR_LINK_RE.findall(line): + if '://' in word and not (word.startswith('org-protocol://')): + if not (word.startswith('http://') + or word.startswith('https://') + or word.startswith('ftp://') + or word.startswith('ftps://') + ): + logging.warning('Is this a link? {} (on {})\nLine: {}\nChunks: {}'.format(word, headline.doc.path, line, line_chunks)) + line_chunks.append(html.escape(word)) + else: + line_chunks.append('{description}' + .format(url=word, + description=html.escape(word))) + else: + line_chunks.append(html.escape(word)) + contents.append(' '.join(line_chunks)) + + acc.append('{}'.format('

    '.join(contents))) + elif isinstance(chunk, Link): link_target = chunk.value - if link_target.startswith('id:'): - link_target = './' + link_target[3:] + '.node.html' + is_internal_link = True description = chunk.description if description is None: description = chunk.value - acc.append('{}'.format( - html.escape(link_target), - html.escape(description), - )) - # else: - # raise NotImplementedError('TextToken: {}'.format(chunk)) + try: + if link_target.startswith('id:'): + assert_id_exists(link_target[3:], headline, graph) + link_target = './' + link_target[3:] + '.node.html' + elif link_target.startswith('./') or link_target.startswith('../'): + if '::' in link_target: + logging.warning('Not implemented headline links to other files. Used on {}'.format(link_target)) + + else: + target_path = os.path.abspath(os.path.join(os.path.dirname(headline.doc.path), link_target)) + if target_path not in graph['main_headlines']: + logging.warning('Link to doc not in graph: {}'.format(target_path)) + else: + assert_id_exists(graph['main_headlines'][target_path].id, headline, graph) + link_target = './' + graph['main_headlines'][target_path].id + '.node.html' + elif link_target.startswith('attachment:'): + inner_target = link_target.split(':', 1)[1] + link_target = 'attachments/{}/{}/{}'.format(headline.id[:2], headline.id[2:], inner_target) + logging.warning('Not implemented `attachment:` links. Used on {}'.format(link_target)) + elif link_target.startswith('* '): + target_headline = get_headline_with_name(link_target.lstrip('* '), headline.doc) + if target_headline is None: + logging.warning('No headline found corresponding to {}. On file {}'.format(link_target, headline.doc.path)) + else: + assert_id_exists(target_headline.id, headline, graph) + link_target = './' + target_headline.id + '.node.html' + else: + is_internal_link = False + if link_target.startswith('orgit-rev'): + raise NonExistingLocalNoteError(link_target, headline) + elif link_target.startswith('file:'): + raise NonExistingLocalNoteError(link_target, headline) + elif not ( + link_target.startswith('https://') + or link_target.startswith('http://') + or link_target.startswith('/') + ): + raise NotImplementedError('Unknown link type: {}' + .format(link_target)) + + if link_target.rsplit('.', 1)[-1].lower() in IMG_EXTENSIONS: + acc.append(''.format( + html.escape(link_target), + 'internal' if is_internal_link else 'external', + html.escape(link_target), + )) + else: + acc.append('{}'.format( + html.escape(link_target), + 'internal' if is_internal_link else 'external', + html.escape(description), + )) + except NonExistingLocalNoteError as err: + logging.warning(err.get_message()) + acc.append(html.escape(description)) + elif isinstance(chunk, org_rw.MarkerToken): + tag = '<' + if chunk.closing: + tag += '/' + tag += { + org_rw.MarkerType.BOLD_MODE: 'strong', + org_rw.MarkerType.CODE_MODE: 'code', + org_rw.MarkerType.ITALIC_MODE: 'em', + org_rw.MarkerType.STRIKE_MODE: 's', + org_rw.MarkerType.UNDERLINED_MODE: 'span class="underlined"' if not chunk.closing else 'span', + org_rw.MarkerType.VERBATIM_MODE: 'span class="verbatim"' if not chunk.closing else 'span', + }[chunk.tok_type] + tag += '>' + acc.append(tag) + else: + raise NotImplementedError('TextToken: {}'.format(chunk)) + acc.append('

    ') -def render_tag(element, acc): +def render_tag(element, acc, headline, graph): return { dom.PropertyDrawerNode: render_property_drawer, dom.LogbookDrawerNode: render_logbook_drawer, dom.PropertyNode: render_property_node, dom.ListGroupNode: render_list_group, dom.ListItem: render_list_item, + dom.TableNode: render_table, + dom.TableSeparatorRow: render_table_separator_row, + dom.TableRow: render_table_row, dom.CodeBlock: render_code_block, dom.Text: render_text, dom.ResultsDrawerNode: render_results_block, - }[type(element)](element, acc) + dom.GenericDrawerNode: render_generic_drawer_block, + org_rw.Text: render_org_text, + }[type(element)](element, acc, headline, graph) -def render_tree(tree, acc): +def render_tree(tree, acc, headline, graph): for element in tree: - render_tag(element, acc) + render_tag(element, acc, headline, graph) + +def render_inline(tree, f, headline, graph): + acc = [] + f(tree, acc, headline, graph) + return ''.join(acc) -def render(headline, doc, headlineLevel): +def render_as_document(headline, doc, headlineLevel, graph, title, doc_to_headline_remapping): + if isinstance(headline.parent, org_rw.Headline): + topLevelHeadline = headline.parent + while isinstance(topLevelHeadline.parent, org_rw.Headline): + topLevelHeadline = topLevelHeadline.parent + return f""" + + + + {title} @ {SITE_NAME} + + + + + + + Sending you to the main note... [{org_rw.token_list_to_plaintext(topLevelHeadline.title.contents)}] + + + """ + else: + return as_document(render(headline, doc, graph=graph, headlineLevel=headlineLevel, + doc_to_headline_remapping=doc_to_headline_remapping), + title, render_toc(doc)) + +def render_toc(doc): + acc = ['') + + if sum([chunk == '
  • ' for chunk in acc]) < 2: + # If < 2 headlines, ignore it + return None + + return ''.join(acc) + +def render_toc_headline(headline, acc): + acc.append('
  • ') + acc.append(f'{html.escape(headline.title.get_text())}') + children = list(headline.children) + if children: + acc.append('
      ') + for child in children: + render_toc_headline(child, acc) + acc.append('
    ') + acc.append('
  • ') + + + +def render_connections(headline_id, content, graph, doc_to_headline_remapping): + # if headline_id != 'aa29be89-70e7-4465-91ed-361cf0ce62f2': + # return + + logging.info("Generating centered graph for {}".format(headline_id)) + try: + svg = gen_centered_graph.gen(headline_id, graph['nodes'], doc_to_headline_remapping) + content.append("
    {}
    ".format(svg)) + except: + logging.warning("Broken reference on headline ID={}".format(headline_id)) + +def render(headline, doc, graph, headlineLevel, doc_to_headline_remapping): try: dom = headline.as_dom() except: logging.error("Error generating DOM for {}".format(doc.path)) raise - print_tree(dom) + print_tree(dom, indentation=2, headline=headline) content = [] - render_tree(dom, content) - for child in headline.children: - content.append(render(child, doc, headlineLevel=headlineLevel+1)) + if headline.id and headlineLevel == 0: + render_connections(headline.id, content, graph, doc_to_headline_remapping=doc_to_headline_remapping) - if headline.state is None: + render_tree(dom, content, headline, graph) + + for child in headline.children: + content.append(render(child, doc, headlineLevel=headlineLevel+1, graph=graph, + doc_to_headline_remapping=doc_to_headline_remapping)) + + if headline.state is None or headline.state.get('name') is None: state = "" else: - state = f'{headline.state}' + state = f'{headline.state["name"]}' if headline.is_todo: todo_state = "todo" else: todo_state = "done" - display_state = 'collapsed' - if headlineLevel < MIN_HIDDEN_HEADLINE_LEVEL: - display_state = 'expanded' + tag_list = [] + for tag in headline.shallow_tags: + if tag.lower() not in SKIPPED_TAGS: + tag_list.append(f'{html.escape(tag)}') + tags = f'{"".join(tag_list)}' + + display_state = 'expanded' + # # Update display based on document STARTUP config + # visual_level = doc.get_keywords('STARTUP', 'showall') + # if visual_level.startswith('show') and visual_level.endswith('levels'): + # visual_level_num = int(visual_level[len('show'):-len('levels')]) - 1 + # # Note that level is 0 indexed inside this loop + # if headlineLevel >= visual_level_num: + # display_state = 'collapsed' + + title = render_inline(headline.title, render_tag, headline, graph) + + if headlineLevel > 0: + title = f"{title}" return f"""

    {state} - - {html.escape(headline.title)} - + {title} + {tags}

    {''.join(content)} @@ -314,27 +809,40 @@ def render(headline, doc, headlineLevel): """ -def as_document(html): +def as_document(html, title, global_toc): + body_classes = [] + if global_toc is None: + toc_section = "" + body_classes.append('no-toc') + else: + toc_section = f""" +
    +

    Table of contents

    + {global_toc} +
    + """ return f""" + + {title} @ {SITE_NAME} - + + + + + - + + + {toc_section} {html} + + + """ @@ -347,9 +855,13 @@ def save_changes(doc): if __name__ == "__main__": - if len(sys.argv) != 3: - print("Usage: {} SOURCE_TOP DEST_TOP".format(sys.argv[0])) + if len(sys.argv) not in (3, 4): + print("Usage: {} SOURCE_TOP DEST_TOP ".format(sys.argv[0])) exit(0) logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s") - main(sys.argv[1], sys.argv[2]) + subpath = DEFAULT_SUBPATH + + if len(sys.argv) == 4: + subpath = sys.argv[3] + exit(main(sys.argv[1], sys.argv[2], subpath=subpath)) diff --git a/scripts/ops_cache.py b/scripts/ops_cache.py new file mode 100644 index 0000000..45c1431 --- /dev/null +++ b/scripts/ops_cache.py @@ -0,0 +1,75 @@ +import sqlite3 +import json +import logging +from typing import Optional +import xdg +import os +import datetime + +CACHE_DB: Optional[sqlite3.Connection] = None +CACHE_PATH = os.path.join(xdg.xdg_cache_home(), 'codigoparallevar', 'ops.sqlite3') + +def init_db(): + global CACHE_DB + + os.makedirs(os.path.dirname(CACHE_PATH), exist_ok=True) + CACHE_DB = sqlite3.connect(CACHE_PATH) + + cur = CACHE_DB.cursor() + cur.execute('''CREATE TABLE IF NOT EXISTS ops( + in_val TEXT PRIMARY KEY, + code TEXT, + out_val TEXT, + added_at DateTime + ); + ''') + CACHE_DB.commit() + cur.close() + +def query_cache(in_val, code): + if CACHE_DB is None: + init_db() + assert CACHE_DB is not None + cur = CACHE_DB.cursor() + cur.execute('''SELECT out_val FROM ops WHERE in_val = ? AND code = ?''', (in_val, code)) + + # Should return only one result, right? 🤷 + results = cur.fetchall() + assert len(results) < 2 + if len(results) == 0: + return None + else: + return results[0][0] + +def save_cache(in_val, code, out_val): + if CACHE_DB is None: + init_db() + assert CACHE_DB is not None + cur = CACHE_DB.cursor() + cur.execute(''' + INSERT INTO ops(in_val, code, out_val, added_at) + VALUES (?, ?, ?, ?);''', + (in_val, code, out_val, datetime.datetime.now())) + CACHE_DB.commit() + cur.close() + +def cache(fun): + fun_code = fun.__code__.co_code.decode('latin-1') + def wrapped(*kargs, **kwargs): + in_val = json.dumps({ + 'kargs': kargs, + 'kwargs': kwargs, + 'fun_code': fun_code, + }) + + cache_result = query_cache(in_val, fun_code) + found_in_cache = cache_result is not None + if not found_in_cache: + out_val = fun(*kargs, **kwargs) + save_cache(in_val, fun_code, out_val) + else: + out_val = cache_result + + logging.info("{} bytes in, {} bytes out (in_cache: {})".format(len(in_val), len(out_val), found_in_cache)) + return out_val + return wrapped diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000..4de0fcc --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,3 @@ +Markdown +Jinja2 +pygments diff --git a/scripts/search-server.sh b/scripts/search-server.sh new file mode 100644 index 0000000..62abda0 --- /dev/null +++ b/scripts/search-server.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -eu + +PORT=${PORT:-3001} + +cd "$(dirname "$0")/search-server" + +docker build -t search-server . + +cd ../../_gen/notes/ + +set -x + +exec docker run -it --rm -p $PORT:80 -e SNIPPET_SIZE=256 -e PORT=80 -e DB_PATH=/db.sqlite3 -v `pwd`/db.sqlite3:/db.sqlite3:ro search-server diff --git a/scripts/search-server/.gitignore b/scripts/search-server/.gitignore new file mode 100644 index 0000000..59e27c6 --- /dev/null +++ b/scripts/search-server/.gitignore @@ -0,0 +1 @@ +search-server diff --git a/scripts/search-server/Dockerfile b/scripts/search-server/Dockerfile new file mode 100644 index 0000000..727f1d9 --- /dev/null +++ b/scripts/search-server/Dockerfile @@ -0,0 +1,47 @@ +FROM golang:1.19-alpine as builder + +# Install build dependencies +RUN apk add alpine-sdk + +# Create appuser. +ENV USER=appuser +ENV UID=10001 + +# See https://stackoverflow.com/a/55757473/12429735 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + "${USER}" + +# Prepare dependencies +RUN mkdir /build +ADD go.mod go.sum /build/ +WORKDIR /build +RUN go mod download +RUN go mod verify + +# Prepare app +ADD server.go /build/ +# Build as static binary +RUN CGO_ENABLED=1 go build --tags "fts5" -ldflags='-w -s -extldflags "-static"' -o /build/search-server + +# Copy binary to empty image +FROM scratch +# Import the user and group files from the builder. +COPY --from=builder /etc/passwd /etc/passwd +COPY --from=builder /etc/group /etc/group + +# Prepare environment +ENV GIN_MODE=release + +# Copy executable +COPY --from=builder /build/search-server /server + +# Use an unprivileged user. +USER appuser:appuser + +ENTRYPOINT ["/server"] diff --git a/scripts/search-server/go.mod b/scripts/search-server/go.mod new file mode 100644 index 0000000..09793db --- /dev/null +++ b/scripts/search-server/go.mod @@ -0,0 +1,66 @@ +module codigoparallevar/search-server + +go 1.19 + +require github.com/gin-gonic/gin v1.8.1 + +require ( + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/awesome-gocui/gocui v0.6.0 // indirect + github.com/awesome-gocui/keybinding v1.0.0 // indirect + github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/docker/cli v0.0.0-20190906153656-016a3232168d // indirect + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/docker v0.7.3-0.20190309235953-33c3200e0d16 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/fatih/color v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.4.7 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-errors/errors v1.0.1 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.10.0 // indirect + github.com/goccy/go-json v0.9.7 // indirect + github.com/gogo/protobuf v1.3.0 // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b // indirect + github.com/lunixbochs/vtclean v1.0.0 // indirect + github.com/magiconair/properties v1.8.1 // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.4 // indirect + github.com/mattn/go-sqlite3 v1.14.15 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.1.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/opencontainers/go-digest v1.0.0-rc1 // indirect + github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/pelletier/go-toml v1.4.0 // indirect + github.com/pelletier/go-toml/v2 v2.0.1 // indirect + github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee // indirect + github.com/pkg/errors v0.8.1 // indirect + github.com/sirupsen/logrus v1.4.2 // indirect + github.com/spf13/afero v1.2.2 // indirect + github.com/spf13/cast v1.3.0 // indirect + github.com/spf13/cobra v0.0.5 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.4.0 // indirect + github.com/ugorji/go/codec v1.2.7 // indirect + github.com/wagoodman/dive v0.10.0 // indirect + golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect + golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect + golang.org/x/text v0.3.6 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/scripts/search-server/go.sum b/scripts/search-server/go.sum new file mode 100644 index 0000000..a078a11 --- /dev/null +++ b/scripts/search-server/go.sum @@ -0,0 +1,295 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/awesome-gocui/gocui v0.5.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA= +github.com/awesome-gocui/gocui v0.6.0 h1:hhDJiQC12tEsJNJ+iZBBVaSSLFYo9llFuYpQlL5JZVI= +github.com/awesome-gocui/gocui v0.6.0/go.mod h1:1QikxFaPhe2frKeKvEwZEIGia3haiOxOUXKinrv17mA= +github.com/awesome-gocui/keybinding v1.0.0 h1:CrnjCfEhWpjcqIQUan9IllaXeRGELdwfjeUmY7ljbng= +github.com/awesome-gocui/keybinding v1.0.0/go.mod h1:z0TyCwIhaT97yU+becTse8Dqh2CvYT0FLw0R0uTk0ag= +github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc h1:wGNpKcHU8Aadr9yOzsT3GEsFLS7HQu8HxQIomnekqf0= +github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/cli v0.0.0-20190906153656-016a3232168d h1:gwX/88xJZfxZV1yjhhuQpWTmEgJis7/XGCVu3iDIZYU= +github.com/docker/cli v0.0.0-20190906153656-016a3232168d/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v0.7.3-0.20190309235953-33c3200e0d16 h1:dmUn0SuGx7unKFwxyeQ/oLUHhEfZosEDrpmYM+6MTuc= +github.com/docker/docker v0.7.3-0.20190309235953-33c3200e0d16/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.0 h1:G8O7TerXerS4F6sx9OV7/nRfJdnXgHZu/S/7F2SN+UE= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b h1:PMbSa9CgaiQR9NLlUTwKi+7aeLl3GG5JX5ERJxfQ3IE= +github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg= +github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs= +github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/wagoodman/dive v0.10.0 h1:JaitQBVwmfZD5mvLkBHk1LUq6jwsjvnNS6mgIl7YNZQ= +github.com/wagoodman/dive v0.10.0/go.mod h1:8IDxfzmg3+5DQwK6/sGyMpJr95ejuv511+rF9CTNYdQ= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190620144150-6af8c5fc6601/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/scripts/search-server/server.go b/scripts/search-server/server.go new file mode 100644 index 0000000..be6ab0d --- /dev/null +++ b/scripts/search-server/server.go @@ -0,0 +1,183 @@ +package main + +import ( + "github.com/gin-gonic/gin" + "database/sql" + "log" + "os" + "fmt" + "strconv" + _ "github.com/mattn/go-sqlite3" +) + +func main() { + database_path, ok := os.LookupEnv("DB_PATH") + if !ok { + log.Fatal("Environment variable $DB_PATH must point to sqlite3 database with text indices.") + os.Exit(1) + } + + port := 3000 + port_str, ok := os.LookupEnv("PORT") + if ok { + port_num, err := strconv.Atoi(port_str) + + if err != nil { + log.Fatal(err) + os.Exit(1) + } + if (port_num < 1) || (port_num > 65535) { + log.Fatal("Environment variale $PORT must be a number between 1 and 65535.") + os.Exit(1) + } + port = port_num + } + + snippet_size := 128 + snippet_size_str, ok := os.LookupEnv("SNIPPET_SIZE") + if ok { + snippet_size_num, err := strconv.Atoi(snippet_size_str) + + if err != nil { + log.Fatal(err) + os.Exit(1) + } + if (snippet_size_num < 64) { + log.Fatal("Environment variale $SNIPPET_SIZE must be >= 64.") + os.Exit(1) + } + snippet_size = snippet_size_num + } + + db, err := sql.Open("sqlite3", database_path) + if err != nil { + log.Fatal(err) + os.Exit(1) + } + + r := gin.Default() + + api := r.Group("/api") + + api.GET("/ping", func(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "pong", + }) + }) + + api.OPTIONS("/search", func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Accept-Encoding, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + + c.AbortWithStatus(204) + }) + + api.GET("/search", func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Accept-Encoding, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + + query := c.Query("q") + body_type := c.Query("body") + + if ((body_type != "all") && (body_type != "none") && (body_type != "snippet")) { + body_type = "none" + } + + var stm *sql.Stmt + var err error + + if (body_type == "snippet") { + stm, err = db.Prepare("SELECT note_id, highlight(note_search, 1, '', ''), top_level_title, is_done, is_todo, snippet(note_search, 2, '', '', '', ?) FROM note_search(?)") + } else if (body_type == "all") { + stm, err = db.Prepare("SELECT note_id, highlight(note_search, 1, '', ''), top_level_title, is_done, is_todo, highlight(note_search, 2, '', '') FROM note_search(?)") + } else if (body_type == "none") { + stm, err = db.Prepare("SELECT note_id, highlight(note_search, 1, '', ''), top_level_title, is_done, is_todo FROM note_search(?)") + } + + + if err != nil { + log.Fatal(err) + c.JSON(500, gin.H{ + "success": false, + "message": "Error preparing note-search query", + }) + return + } + + results := make([]map[string]string, 0) + var rows *sql.Rows + + if (body_type == "snippet") { + rows, err = stm.Query(snippet_size, query) + } else { + rows, err = stm.Query(query) + } + if err != nil { + log.Fatal(err) + c.JSON(500, gin.H{ + "success": false, + "message": "Error querying note DB", + }) + return + } + + for rows.Next() { + var note_id string + var note_title string + var note_top_level_title string + var note_is_done string + var note_is_todo string + + item := make(map[string]string) + + if (body_type != "none") { + var note_highlight string + + err = rows.Scan( + ¬e_id, + ¬e_title, + ¬e_top_level_title, + ¬e_is_done, + ¬e_is_todo, + ¬e_highlight, + ) + if (body_type != "none") { + item["highlight"] = note_highlight + } + } else { + err = rows.Scan( + ¬e_id, + ¬e_title, + ¬e_top_level_title, + ¬e_is_done, + ¬e_is_todo, + ) + } + if err != nil { + log.Fatal(err) + c.JSON(500, gin.H{ + "success": false, + "message": "Error reading note DB results", + }) + return + } + + item["id"] = note_id + item["title"] = note_title + item["top_level_title"] = note_top_level_title + item["is_done"] = note_is_done + item["is_todo"] = note_is_todo + results = append(results, item) + } + + c.JSON(200, gin.H{ + "results": gin.H{ + "notes": results, + }, + }) + }) + + r.Run(fmt.Sprintf(":%v", port)) +} diff --git a/scripts/test-links.py b/scripts/test-links.py new file mode 100644 index 0000000..1fbd25e --- /dev/null +++ b/scripts/test-links.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +import logging +import os +import sys +import urllib.parse + +from bs4 import BeautifulSoup as bs4 + +from tqdm import tqdm + +def main(files_top): + + print("Listing files...") + found_files = [] + for root, dirs, files in os.walk(files_top): + for name in files: + if name.endswith('.html'): + found_files.append(os.path.join(root, name)) + print("\r{} files".format(len(found_files)), end='', flush=True) + + print() + found_broken = 0 + for fpath in tqdm(found_files): + with open(fpath) as f: + tree = bs4(f.read(), features='lxml', parser='html5') + + for tag, attr in [('a', 'href'), ('img', 'src'), ('audio', 'src'), ('video', 'src')]: + for link in tree.find_all(tag): + if attr not in link.attrs: + continue + link.attrs[attr] = link.attrs[attr].split('#')[0] + if not link.attrs[attr]: + continue + if ':' in link[attr]: + continue + if link[attr].startswith('/'): + target = os.path.join(os.path.abspath(files_top), urllib.parse.unquote(link[attr].lstrip('/'))) + else: + target = os.path.join(os.path.dirname(fpath), urllib.parse.unquote(link[attr])) + if os.path.isdir(target): + pass + elif not os.path.exists(target): + print("[{}] -[ error ]-> {} | {}".format(fpath, target, link[attr])) + found_broken += 1 + + if found_broken: + print(f"Found {found_broken} broken links") + exit(1) + else: + exit(0) + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: {} FILES_TOP".format(sys.argv[0])) + exit(0) + + logging.basicConfig(level=logging.INFO, format="%(levelname)-8s %(message)s") + exit(main(sys.argv[1])) diff --git a/scripts/upload-homepage.sh b/scripts/upload-homepage.sh deleted file mode 100644 index 8d475ef..0000000 --- a/scripts/upload-homepage.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -eu - -cd "$(dirname "$0")/.." - -cd static -scp homepage.html root@codigoparallevar.com:/mnt/vols/misc/codigoparallevar/index.html diff --git a/scripts/upload.sh b/scripts/upload.sh new file mode 100644 index 0000000..2a213b9 --- /dev/null +++ b/scripts/upload.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -eu + +cd "$(dirname "$0")/.." + +# Upload homepage +cd static +scp homepage.html root@codigoparallevar.com:/mnt/vols/misc/codigoparallevar/index.html + +# Build notes +cd ../scripts +rm -Rf ../_gen/notes +WATCH_AND_REBUILD=0 python3 generate.py ~/.logs/brain ../_gen/notes + +rm -Rf ../_gen/blog +WATCH_AND_REBUILD=0 python3 blog.py ~/cloud/nextcloud/blog/posts/ ../_gen/blog + +rm -Rf ../_gen/static +cp -Rv ../static ../_gen/static + +# Upload notes +cd ../_gen +rsync -HPaz static/ --delete-after --exclude='*.html' root@codigoparallevar.com:/mnt/vols/misc/codigoparallevar/static/ +rsync -HPaz notes/ --delete-after --exclude='xapian' --exclude='*.sqlite3' root@codigoparallevar.com:/mnt/vols/misc/codigoparallevar/notes/ +rsync -HPaz notes/db.sqlite3 root@codigoparallevar.com:/mnt/vols/misc/codigoparallevar-api/ +rsync -HPaz blog/ --delete-after --exclude='xapian' --exclude='*.sqlite3' root@codigoparallevar.com:/mnt/vols/misc/codigoparallevar/blog/ + +# Restart API server +ssh root@codigoparallevar.com docker restart notes-api-server diff --git a/scripts/wait_for_update.js b/scripts/wait_for_update.js new file mode 100644 index 0000000..d53b9fd --- /dev/null +++ b/scripts/wait_for_update.js @@ -0,0 +1,19 @@ +(function (){ + var wait_for_update = function() { + console.debug("Waiting for changes..."); + + fetch('/__wait_for_changes').then(r => { + if (r.status !== 200) { + setTimeout( + wait_for_update, + 1000, + ); + } + else { + // Reload + window.location = window.location; + } + }); + }; + wait_for_update(); +})(); diff --git a/static/article.tmpl.html b/static/article.tmpl.html new file mode 100644 index 0000000..6484b84 --- /dev/null +++ b/static/article.tmpl.html @@ -0,0 +1,51 @@ + + + + + {{ title }} @ Código para llevar [blog] + + + + + + + +
    +
    +

    {{ title }}

    + +
    + {{ content | safe }} +
    +
    +
    + + diff --git a/static/article_list.tmpl.html b/static/article_list.tmpl.html new file mode 100644 index 0000000..225ac1c --- /dev/null +++ b/static/article_list.tmpl.html @@ -0,0 +1,54 @@ + + + + + Código para llevar + + + + + + + +
    +
      + {% for post in posts %} +
    • + +

      {{ post.title }}

      + +
    • + {% endfor %} +
    +
    + + diff --git a/static/blog.css b/static/blog.css new file mode 100644 index 0000000..eb9a2ef --- /dev/null +++ b/static/blog.css @@ -0,0 +1,6 @@ +body { + margin: 0 auto; + width: fit-content; + max-width: 100ex; + padding: 0 1ex; +} diff --git a/static/blog_index.tmpl.html b/static/blog_index.tmpl.html new file mode 100644 index 0000000..f4de867 --- /dev/null +++ b/static/blog_index.tmpl.html @@ -0,0 +1,68 @@ + + + + + Código para llevar + + + + + + + +
    + {% for post in posts %} +
    +
    +

    {{ post.title }}

    + +
    + {{ post.summary | safe }} +
    +
    +
    + {% endfor %} +
    +
    + {% if prev_index_num != None %} + {% if prev_index_num == 0 %} + Newer posts + {% else %} + Newer posts + {% endif %} + {% endif %} + + {% if next_index_num %} + Older posts + {% endif %} +
    + + diff --git a/static/category_list.tmpl.html b/static/category_list.tmpl.html new file mode 100644 index 0000000..40b19c9 --- /dev/null +++ b/static/category_list.tmpl.html @@ -0,0 +1,54 @@ + + + + + Código para llevar + + + + + + + +
    +
      + {% for post in posts %} +
    • + +

      {{ post.title }}

      + +
    • + {% endfor %} +
    +
    + + diff --git a/static/dark-syntax.css b/static/dark-syntax.css new file mode 100644 index 0000000..afdbd46 --- /dev/null +++ b/static/dark-syntax.css @@ -0,0 +1,85 @@ +/* Dark mode. */ +@media (prefers-color-scheme: dark) { + pre { line-height: 125%; } + td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } + span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } + td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } + span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } + .hll { background-color: #49483e } + .c { color: #75715e } /* Comment */ + .err { color: #960050; background-color: #1e0010 } /* Error */ + .esc { color: #f8f8f2 } /* Escape */ + .g { color: #f8f8f2 } /* Generic */ + .k { color: #66d9ef } /* Keyword */ + .l { color: #ae81ff } /* Literal */ + .n { color: #f8f8f2 } /* Name */ + .o { color: #f92672 } /* Operator */ + .x { color: #f8f8f2 } /* Other */ + .p { color: #f8f8f2 } /* Punctuation */ + .ch { color: #75715e } /* Comment.Hashbang */ + .cm { color: #75715e } /* Comment.Multiline */ + .cp { color: #75715e } /* Comment.Preproc */ + .cpf { color: #75715e } /* Comment.PreprocFile */ + .c1 { color: #75715e } /* Comment.Single */ + .cs { color: #75715e } /* Comment.Special */ + .gd { color: #f92672 } /* Generic.Deleted */ + .ge { color: #f8f8f2; font-style: italic } /* Generic.Emph */ + .gr { color: #f8f8f2 } /* Generic.Error */ + .gh { color: #f8f8f2 } /* Generic.Heading */ + .gi { color: #a6e22e } /* Generic.Inserted */ + .go { color: #66d9ef } /* Generic.Output */ + .gp { color: #f92672; font-weight: bold } /* Generic.Prompt */ + .gs { color: #f8f8f2; font-weight: bold } /* Generic.Strong */ + .gu { color: #75715e } /* Generic.Subheading */ + .gt { color: #f8f8f2 } /* Generic.Traceback */ + .kc { color: #66d9ef } /* Keyword.Constant */ + .kd { color: #66d9ef } /* Keyword.Declaration */ + .kn { color: #f92672 } /* Keyword.Namespace */ + .kp { color: #66d9ef } /* Keyword.Pseudo */ + .kr { color: #66d9ef } /* Keyword.Reserved */ + .kt { color: #66d9ef } /* Keyword.Type */ + .ld { color: #e6db74 } /* Literal.Date */ + .m { color: #ae81ff } /* Literal.Number */ + .s { color: #e6db74 } /* Literal.String */ + .na { color: #a6e22e } /* Name.Attribute */ + .nb { color: #f8f8f2 } /* Name.Builtin */ + .nc { color: #a6e22e } /* Name.Class */ + .no { color: #66d9ef } /* Name.Constant */ + .nd { color: #a6e22e } /* Name.Decorator */ + .ni { color: #f8f8f2 } /* Name.Entity */ + .ne { color: #a6e22e } /* Name.Exception */ + .nf { color: #a6e22e } /* Name.Function */ + .nl { color: #f8f8f2 } /* Name.Label */ + .nn { color: #f8f8f2 } /* Name.Namespace */ + .nx { color: #a6e22e } /* Name.Other */ + .py { color: #f8f8f2 } /* Name.Property */ + .nt { color: #f92672 } /* Name.Tag */ + .nv { color: #f8f8f2 } /* Name.Variable */ + .ow { color: #f92672 } /* Operator.Word */ + .w { color: #f8f8f2 } /* Text.Whitespace */ + .mb { color: #ae81ff } /* Literal.Number.Bin */ + .mf { color: #ae81ff } /* Literal.Number.Float */ + .mh { color: #ae81ff } /* Literal.Number.Hex */ + .mi { color: #ae81ff } /* Literal.Number.Integer */ + .mo { color: #ae81ff } /* Literal.Number.Oct */ + .sa { color: #e6db74 } /* Literal.String.Affix */ + .sb { color: #e6db74 } /* Literal.String.Backtick */ + .sc { color: #e6db74 } /* Literal.String.Char */ + .dl { color: #e6db74 } /* Literal.String.Delimiter */ + .sd { color: #e6db74 } /* Literal.String.Doc */ + .s2 { color: #e6db74 } /* Literal.String.Double */ + .se { color: #ae81ff } /* Literal.String.Escape */ + .sh { color: #e6db74 } /* Literal.String.Heredoc */ + .si { color: #e6db74 } /* Literal.String.Interpol */ + .sx { color: #e6db74 } /* Literal.String.Other */ + .sr { color: #e6db74 } /* Literal.String.Regex */ + .s1 { color: #e6db74 } /* Literal.String.Single */ + .ss { color: #e6db74 } /* Literal.String.Symbol */ + .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ + .fm { color: #a6e22e } /* Name.Function.Magic */ + .vc { color: #f8f8f2 } /* Name.Variable.Class */ + .vg { color: #f8f8f2 } /* Name.Variable.Global */ + .vi { color: #f8f8f2 } /* Name.Variable.Instance */ + .vm { color: #f8f8f2 } /* Name.Variable.Magic */ + .il { color: #ae81ff } /* Literal.Number.Integer.Long */ +} diff --git a/static/fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-Bold-102.eot b/static/fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-Bold-102.eot new file mode 100644 index 0000000..5e19be4 Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-Bold-102.eot differ diff --git a/static/fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-BoldItalic-102.eot b/static/fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-BoldItalic-102.eot new file mode 100644 index 0000000..702e813 Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-BoldItalic-102.eot differ diff --git a/static/fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-Italic-102.eot b/static/fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-Italic-102.eot new file mode 100644 index 0000000..86a3fb8 Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-Italic-102.eot differ diff --git a/static/fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-Regular-102.eot b/static/fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-Regular-102.eot new file mode 100644 index 0000000..4f636b6 Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/eot/Atkinson-Hyperlegible-Regular-102.eot differ diff --git a/static/fonts/atkinson-hyperlegible/svg/Atkinson-Hyperlegible-Bold-102.svg b/static/fonts/atkinson-hyperlegible/svg/Atkinson-Hyperlegible-Bold-102.svg new file mode 100644 index 0000000..daecf91 --- /dev/null +++ b/static/fonts/atkinson-hyperlegible/svg/Atkinson-Hyperlegible-Bold-102.svg @@ -0,0 +1,2300 @@ + + + + +Release v1.02. (c) 2020 Braille Institute of America, Inc., a 501(c)(3) Charitable Organization. BrailleInstitute.org. +Designed by Elliott Scott, Megan Eiswerth, Linus Boman and Theodore Petrosky at Applied Design Works. + +Braille Institute of America, Inc. provides Atkinson Hyperlegible for use, without derivatives or alteration, to the public free of charge for all non-commercial and commercial work. No attribution required. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/fonts/atkinson-hyperlegible/svg/Atkinson-Hyperlegible-BoldItalic-102.svg b/static/fonts/atkinson-hyperlegible/svg/Atkinson-Hyperlegible-BoldItalic-102.svg new file mode 100644 index 0000000..1c2d2ae --- /dev/null +++ b/static/fonts/atkinson-hyperlegible/svg/Atkinson-Hyperlegible-BoldItalic-102.svg @@ -0,0 +1,2303 @@ + + + + +Release v1.02. (c) 2020 Braille Institute of America, Inc., a 501(c)(3) Charitable Organization. BrailleInstitute.org. +Designed by Elliott Scott, Megan Eiswerth, Linus Boman and Theodore Petrosky at Applied Design Works. + +Braille Institute of America, Inc. provides Atkinson Hyperlegible for use, without derivatives or alteration, to the public free of charge for all non-commercial and commercial work. No attribution required. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/fonts/atkinson-hyperlegible/svg/Atkinson-Hyperlegible-Italic-102.svg b/static/fonts/atkinson-hyperlegible/svg/Atkinson-Hyperlegible-Italic-102.svg new file mode 100644 index 0000000..30beae8 --- /dev/null +++ b/static/fonts/atkinson-hyperlegible/svg/Atkinson-Hyperlegible-Italic-102.svg @@ -0,0 +1,1912 @@ + + + + +Release v1.02. (c) 2020 Braille Institute of America, Inc., a 501(c)(3) Charitable Organization. BrailleInstitute.org. +Designed by Elliott Scott, Megan Eiswerth, Linus Boman and Theodore Petrosky at Applied Design Works. + +Braille Institute of America, Inc. provides Atkinson Hyperlegible for use, without derivatives or alteration, to the public free of charge for all non-commercial and commercial work. No attribution required. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/fonts/atkinson-hyperlegible/svg/Atkinson-Hyperlegible-Regular-102.svg b/static/fonts/atkinson-hyperlegible/svg/Atkinson-Hyperlegible-Regular-102.svg new file mode 100644 index 0000000..5bd4f9f --- /dev/null +++ b/static/fonts/atkinson-hyperlegible/svg/Atkinson-Hyperlegible-Regular-102.svg @@ -0,0 +1,1895 @@ + + + + +Release v1.02. (c) 2020 Braille Institute of America, Inc., a 501(c)(3) Charitable Organization. BrailleInstitute.org. +Designed by Elliott Scott, Megan Eiswerth, Linus Boman and Theodore Petrosky at Applied Design Works. + +Braille Institute of America, Inc. provides Atkinson Hyperlegible for use, without derivatives or alteration, to the public free of charge for all non-commercial and commercial work. No attribution required. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/fonts/atkinson-hyperlegible/ttf/Atkinson-Hyperlegible-Bold-102.ttf b/static/fonts/atkinson-hyperlegible/ttf/Atkinson-Hyperlegible-Bold-102.ttf new file mode 100644 index 0000000..14b7196 Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/ttf/Atkinson-Hyperlegible-Bold-102.ttf differ diff --git a/static/fonts/atkinson-hyperlegible/ttf/Atkinson-Hyperlegible-BoldItalic-102.ttf b/static/fonts/atkinson-hyperlegible/ttf/Atkinson-Hyperlegible-BoldItalic-102.ttf new file mode 100644 index 0000000..4532705 Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/ttf/Atkinson-Hyperlegible-BoldItalic-102.ttf differ diff --git a/static/fonts/atkinson-hyperlegible/ttf/Atkinson-Hyperlegible-Italic-102.ttf b/static/fonts/atkinson-hyperlegible/ttf/Atkinson-Hyperlegible-Italic-102.ttf new file mode 100644 index 0000000..89e5ce4 Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/ttf/Atkinson-Hyperlegible-Italic-102.ttf differ diff --git a/static/fonts/atkinson-hyperlegible/ttf/Atkinson-Hyperlegible-Regular-102.ttf b/static/fonts/atkinson-hyperlegible/ttf/Atkinson-Hyperlegible-Regular-102.ttf new file mode 100644 index 0000000..c4fa6fb Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/ttf/Atkinson-Hyperlegible-Regular-102.ttf differ diff --git a/static/fonts/atkinson-hyperlegible/woff/Atkinson-Hyperlegible-Bold-102.woff b/static/fonts/atkinson-hyperlegible/woff/Atkinson-Hyperlegible-Bold-102.woff new file mode 100644 index 0000000..e7f8977 Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/woff/Atkinson-Hyperlegible-Bold-102.woff differ diff --git a/static/fonts/atkinson-hyperlegible/woff/Atkinson-Hyperlegible-BoldItalic-102.woff b/static/fonts/atkinson-hyperlegible/woff/Atkinson-Hyperlegible-BoldItalic-102.woff new file mode 100644 index 0000000..d6421ac Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/woff/Atkinson-Hyperlegible-BoldItalic-102.woff differ diff --git a/static/fonts/atkinson-hyperlegible/woff/Atkinson-Hyperlegible-Italic-102.woff b/static/fonts/atkinson-hyperlegible/woff/Atkinson-Hyperlegible-Italic-102.woff new file mode 100644 index 0000000..12d2d8c Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/woff/Atkinson-Hyperlegible-Italic-102.woff differ diff --git a/static/fonts/atkinson-hyperlegible/woff/Atkinson-Hyperlegible-Regular-102.woff b/static/fonts/atkinson-hyperlegible/woff/Atkinson-Hyperlegible-Regular-102.woff new file mode 100644 index 0000000..bbe09c5 Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/woff/Atkinson-Hyperlegible-Regular-102.woff differ diff --git a/static/fonts/atkinson-hyperlegible/woff2/Atkinson-Hyperlegible-Bold-102a.woff2 b/static/fonts/atkinson-hyperlegible/woff2/Atkinson-Hyperlegible-Bold-102a.woff2 new file mode 100644 index 0000000..19a58ea Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/woff2/Atkinson-Hyperlegible-Bold-102a.woff2 differ diff --git a/static/fonts/atkinson-hyperlegible/woff2/Atkinson-Hyperlegible-BoldItalic-102a.woff2 b/static/fonts/atkinson-hyperlegible/woff2/Atkinson-Hyperlegible-BoldItalic-102a.woff2 new file mode 100644 index 0000000..43f253e Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/woff2/Atkinson-Hyperlegible-BoldItalic-102a.woff2 differ diff --git a/static/fonts/atkinson-hyperlegible/woff2/Atkinson-Hyperlegible-Italic-102a.woff2 b/static/fonts/atkinson-hyperlegible/woff2/Atkinson-Hyperlegible-Italic-102a.woff2 new file mode 100644 index 0000000..d35d3a7 Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/woff2/Atkinson-Hyperlegible-Italic-102a.woff2 differ diff --git a/static/fonts/atkinson-hyperlegible/woff2/Atkinson-Hyperlegible-Regular-102a.woff2 b/static/fonts/atkinson-hyperlegible/woff2/Atkinson-Hyperlegible-Regular-102a.woff2 new file mode 100644 index 0000000..99b3c6f Binary files /dev/null and b/static/fonts/atkinson-hyperlegible/woff2/Atkinson-Hyperlegible-Regular-102a.woff2 differ diff --git a/static/graph_explorer.html b/static/graph_explorer.html index ade81c3..abb6aee 100644 --- a/static/graph_explorer.html +++ b/static/graph_explorer.html @@ -7,6 +7,12 @@ + diff --git a/static/homepage.html b/static/homepage.html index c4f1ebf..aa566eb 100644 --- a/static/homepage.html +++ b/static/homepage.html @@ -1,16 +1,22 @@ - + Código para llevar @@ -67,11 +74,19 @@