├── response ├── __init__.py ├── badRequestHandler.py ├── templateHandler.py ├── requestHandler.py ├── serverCSSHandler.py ├── networkVisHandler.py ├── currentBufferHandler.py ├── staticHandler.py ├── defaultFiltersHandler.py ├── filePreviewHandler.py ├── roamBufferHandler.py └── roamDataHandler.py ├── public ├── favicon.ico ├── bootstrap-toggle.min.css ├── bootstrap-toggle.min.js ├── select2.min.css ├── org.css └── bootstrap.min.js ├── .gitignore ├── routes └── main.py ├── variables.py ├── main.py ├── LICENCE ├── server.py ├── README.org ├── org-roam-server-light.el └── templates └── index.html /response/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AloisJanicek/org-roam-server-light/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.log 4 | tmp/ 5 | 6 | *.py[cod] 7 | *.egg 8 | build 9 | htmlcov 10 | .mypy_cache 11 | flycheck_* 12 | -------------------------------------------------------------------------------- /routes/main.py: -------------------------------------------------------------------------------- 1 | routes = { 2 | "/" : { 3 | "template" : "index.html" 4 | }, 5 | 6 | "/goodbye" : { 7 | "template" : "goodbye.html" 8 | } 9 | } -------------------------------------------------------------------------------- /response/badRequestHandler.py: -------------------------------------------------------------------------------- 1 | from response.requestHandler import RequestHandler 2 | 3 | 4 | class BadRequestHandler(RequestHandler): 5 | def __init__(self): 6 | super().__init__() 7 | self.contentType = "text/plain" 8 | self.setStatus(404) 9 | -------------------------------------------------------------------------------- /response/templateHandler.py: -------------------------------------------------------------------------------- 1 | from response.requestHandler import RequestHandler 2 | 3 | 4 | class TemplateHandler(RequestHandler): 5 | def __init__(self): 6 | super().__init__() 7 | self.contentType = "text/html" 8 | 9 | def find(self, routeData): 10 | try: 11 | template_file = open("templates/{}".format(routeData["template"])) 12 | self.contents = template_file 13 | self.setStatus(200) 14 | return True 15 | except: 16 | self.setStatus(404) 17 | return False 18 | -------------------------------------------------------------------------------- /variables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from os import environ 4 | from sys import platform 5 | from pathlib import Path 6 | 7 | if platform == "win32": 8 | tmp_dir = environ['TMP'] 9 | elif platform == "darwin": 10 | tmp_dir = environ['TMPDIR'] 11 | else: 12 | tmp_dir = '/tmp' 13 | 14 | org_roam_server_light_tmp_dir = Path(tmp_dir) / "org-roam-server-light" 15 | 16 | org_roam_directory = (org_roam_server_light_tmp_dir / 17 | "org-roam-directory").read_text() 18 | 19 | org_roam_db = (org_roam_server_light_tmp_dir / 20 | "org-roam-db-location").read_text() 21 | -------------------------------------------------------------------------------- /response/requestHandler.py: -------------------------------------------------------------------------------- 1 | class MockFile: 2 | def read(self): 3 | return False 4 | 5 | 6 | class RequestHandler: 7 | def __init__(self): 8 | self.contentType = "" 9 | self.contents = MockFile() 10 | 11 | def getContents(self): 12 | return self.contents.read() 13 | 14 | def read(self): 15 | return self.contents 16 | 17 | def setStatus(self, status): 18 | self.status = status 19 | 20 | def getStatus(self): 21 | return self.status 22 | 23 | def getContentType(self): 24 | return self.contentType 25 | 26 | def getType(self): 27 | return "static" 28 | -------------------------------------------------------------------------------- /response/serverCSSHandler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os.path 4 | from response.requestHandler import RequestHandler 5 | from variables import org_roam_server_light_tmp_dir 6 | 7 | 8 | server_css_file = ( 9 | org_roam_server_light_tmp_dir 10 | / 11 | "org-roam-server-light-style" 12 | ) 13 | 14 | 15 | class ServerCSSHandler(RequestHandler): 16 | def __init__(self): 17 | super().__init__() 18 | global server_css_file 19 | self.contentType = "text/css" 20 | 21 | if (os.path.isfile(server_css_file) 22 | and os.path.isfile(server_css_file)): 23 | server_css = open( 24 | server_css_file, "r").read() 25 | 26 | self.contents = server_css 27 | else: 28 | self.contents = "" 29 | 30 | self.setStatus(200) 31 | 32 | def getContents(self): 33 | return self.contents 34 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import time 3 | import sys 4 | from http.server import HTTPServer 5 | from server import Server 6 | 7 | HOST_NAME = "localhost" 8 | PORT_NUMBER = 8080 9 | 10 | 11 | class QuietHandler(Server): 12 | def log_message(self, format, *args): 13 | pass 14 | 15 | 16 | if __name__ == "__main__": 17 | args = sys.argv[1:] 18 | if args and args[0] == '-d': 19 | httpd = HTTPServer((HOST_NAME, PORT_NUMBER), Server) 20 | else: 21 | httpd = HTTPServer((HOST_NAME, PORT_NUMBER), QuietHandler) 22 | print( 23 | "If you want to see full request log, pass '-d' debug switch to this program.") 24 | print(time.asctime(), "Server Starts - %s:%s" % (HOST_NAME, PORT_NUMBER)) 25 | try: 26 | httpd.serve_forever() 27 | except KeyboardInterrupt: 28 | pass 29 | httpd.server_close() 30 | print(time.asctime(), "Server Stops - %s:%s" % (HOST_NAME, PORT_NUMBER)) 31 | -------------------------------------------------------------------------------- /response/networkVisHandler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os.path 4 | from response.requestHandler import RequestHandler 5 | from variables import org_roam_server_light_tmp_dir 6 | 7 | 8 | network_vis_options_file = ( 9 | org_roam_server_light_tmp_dir 10 | / 11 | "org-roam-server-light-network-vis-options" 12 | ) 13 | 14 | 15 | class NetworkVisHandler(RequestHandler): 16 | def __init__(self): 17 | super().__init__() 18 | global network_vis_options_file 19 | self.contentType = "application/json" 20 | 21 | if os.path.isfile(network_vis_options_file) and os.path.getsize(network_vis_options_file) > 0: 22 | network_vis_options = open( 23 | network_vis_options_file, "r").read() 24 | self.contents = network_vis_options 25 | else: 26 | self.contents = "{}" 27 | 28 | self.setStatus(200) 29 | 30 | def getContents(self): 31 | return self.contents 32 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Göktuğ Karakaşlı 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /response/currentBufferHandler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os.path 4 | from response.requestHandler import RequestHandler 5 | from variables import org_roam_server_light_tmp_dir 6 | 7 | last_roam_buffer_file = ( 8 | org_roam_server_light_tmp_dir 9 | / 10 | "org-roam-server-light-last-roam-buffer" 11 | ) 12 | 13 | previous_mtime = 1 14 | 15 | 16 | class CurrentBufferHandler(RequestHandler): 17 | def __init__(self): 18 | super().__init__() 19 | global last_roam_buffer_file 20 | global previous_mtime 21 | 22 | self.contentType = "text/event-stream" 23 | 24 | if os.path.isfile(last_roam_buffer_file): 25 | current_mtime = os.path.getmtime(last_roam_buffer_file) 26 | if current_mtime == previous_mtime: 27 | self.contents = "" 28 | else: 29 | last_roam_buffer = open(last_roam_buffer_file, "r").read() 30 | self.contents = "data: " + last_roam_buffer + "\n\n" 31 | previous_mtime = current_mtime 32 | else: 33 | self.contents = "" 34 | self.setStatus(200) 35 | 36 | def getContents(self): 37 | return self.contents 38 | -------------------------------------------------------------------------------- /response/staticHandler.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from response.requestHandler import RequestHandler 4 | 5 | 6 | class StaticHandler(RequestHandler): 7 | def __init__(self): 8 | self.filetypes = { 9 | ".js": "text/javascript", 10 | ".css": "text/css", 11 | ".jpg": "image/jpeg", 12 | ".png": "image/png", 13 | ".ico": "image/x-icon", 14 | "notfound": "text/plain", 15 | } 16 | 17 | def find(self, file_path): 18 | split_path = os.path.splitext(file_path) 19 | extension = split_path[1] 20 | 21 | try: 22 | print("public{}".format(file_path)) 23 | 24 | if extension in (".jpg", ".jpeg", ".png", ".ico"): 25 | self.contents = open("public{}".format(file_path), "rb") 26 | else: 27 | self.contents = open("public{}".format( 28 | file_path), "r", encoding="utf8") 29 | 30 | self.setContentType(extension) 31 | self.setStatus(200) 32 | return True 33 | except: 34 | self.setContentType("notfound") 35 | self.setStatus(404) 36 | return False 37 | 38 | def setContentType(self, ext): 39 | self.contentType = self.filetypes[ext] 40 | -------------------------------------------------------------------------------- /response/defaultFiltersHandler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os.path 4 | from response.requestHandler import RequestHandler 5 | from variables import org_roam_server_light_tmp_dir 6 | 7 | 8 | default_include_filters_file = ( 9 | org_roam_server_light_tmp_dir 10 | / 11 | "org-roam-server-light-default-include-filters" 12 | ) 13 | 14 | default_exclude_filters_file = ( 15 | org_roam_server_light_tmp_dir 16 | / 17 | "org-roam-server-light-default-exclude-filters" 18 | ) 19 | 20 | 21 | class DefaultFiltersHandler(RequestHandler): 22 | def __init__(self): 23 | super().__init__() 24 | global default_include_filters_file 25 | global default_exclude_filters_file 26 | self.contentType = "application/json" 27 | 28 | if (os.path.isfile(default_include_filters_file) 29 | and os.path.isfile(default_exclude_filters_file)): 30 | default_include_filters = open( 31 | default_include_filters_file, "r").read() 32 | 33 | default_exclude_filters = open( 34 | default_exclude_filters_file, "r").read() 35 | 36 | self.contents = "{\"include\": %s, \"exclude\": %s}" % ( 37 | default_include_filters, default_exclude_filters) 38 | else: 39 | self.contents = "{}" 40 | 41 | self.setStatus(200) 42 | 43 | def getContents(self): 44 | return self.contents 45 | -------------------------------------------------------------------------------- /public/bootstrap-toggle.min.css: -------------------------------------------------------------------------------- 1 | /*! ======================================================================== 2 | * Bootstrap Toggle: bootstrap-toggle.css v2.2.0 3 | * http://www.bootstraptoggle.com 4 | * ======================================================================== 5 | * Copyright 2014 Min Hur, The New York Times Company 6 | * Licensed under MIT 7 | * ======================================================================== */ 8 | .checkbox label .toggle,.checkbox-inline .toggle{margin-left:-20px;margin-right:5px} 9 | .toggle{position:relative;overflow:hidden} 10 | .toggle input[type=checkbox]{display:none} 11 | .toggle-group{position:absolute;width:200%;top:0;bottom:0;left:0;transition:left .35s;-webkit-transition:left .35s;-moz-user-select:none;-webkit-user-select:none} 12 | .toggle.off .toggle-group{left:-100%} 13 | .toggle-on{position:absolute;top:0;bottom:0;left:0;right:50%;margin:0;border:0;border-radius:0} 14 | .toggle-off{position:absolute;top:0;bottom:0;left:50%;right:0;margin:0;border:0;border-radius:0} 15 | .toggle-handle{position:relative;margin:0 auto;padding-top:0;padding-bottom:0;height:100%;width:0;border-width:0 1px} 16 | .toggle.btn{min-width:59px;min-height:34px} 17 | .toggle-on.btn{padding-right:24px} 18 | .toggle-off.btn{padding-left:24px} 19 | .toggle.btn-lg{min-width:79px;min-height:45px} 20 | .toggle-on.btn-lg{padding-right:31px} 21 | .toggle-off.btn-lg{padding-left:31px} 22 | .toggle-handle.btn-lg{width:40px} 23 | .toggle.btn-sm{min-width:50px;min-height:30px} 24 | .toggle-on.btn-sm{padding-right:20px} 25 | .toggle-off.btn-sm{padding-left:20px} 26 | .toggle.btn-xs{min-width:35px;min-height:22px} 27 | .toggle-on.btn-xs{padding-right:12px} 28 | .toggle-off.btn-xs{padding-left:12px} -------------------------------------------------------------------------------- /response/filePreviewHandler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | from response.requestHandler import RequestHandler 4 | from variables import org_roam_server_light_tmp_dir 5 | 6 | 7 | class FilePreviewHandler(RequestHandler): 8 | def __init__(self, to_be_exported_file, org_roam_db): 9 | super().__init__() 10 | self.contentType = "text/html" 11 | self.to_be_exported_file = to_be_exported_file 12 | self.export_dir = org_roam_server_light_tmp_dir 13 | self.org_roam_db = org_roam_db 14 | self.filename = os.path.basename(to_be_exported_file).rstrip(".org") 15 | self.exported_file = os.path.join( 16 | self.export_dir, self.filename+".html") 17 | os.system( 18 | "pandoc " 19 | + to_be_exported_file 20 | + " " 21 | + "-f org " 22 | + "-t html " 23 | + "-o " 24 | + self.exported_file 25 | ) 26 | with open(self.exported_file, "r+", encoding="utf8") as f: 27 | body = f.read() 28 | f.seek(0) 29 | f.write( 30 | """ 31 | 32 | 33 | 34 | 35 | 42 | 43 | 44 |
45 | """ 46 | + "

" 47 | + self.get_title() 48 | + "

" 49 | + body 50 | + """ 51 | 52 | 53 | """ 54 | ) 55 | 56 | self.fix_href() 57 | self.contents = open(self.exported_file) 58 | self.setStatus(200) 59 | 60 | def fix_href(self): 61 | f = open(self.exported_file, "r") 62 | d = f.read() 63 | d = d.replace('.org">', '.html">') 64 | f.close() 65 | f = open(self.exported_file, "w") 66 | f.write(d) 67 | f.close() 68 | 69 | def get_title(self): 70 | conn = sqlite3.connect(self.org_roam_db) 71 | c = conn.cursor() 72 | path_quoted = '"' + self.to_be_exported_file + '"' 73 | query = ( 74 | """ 75 | SELECT title 76 | FROM titles 77 | WHERE file = '%s' 78 | """ 79 | % path_quoted 80 | ) 81 | c.execute(query) 82 | results = c.fetchall() 83 | conn.close() 84 | title = results[0][0].strip('"') 85 | 86 | return title 87 | -------------------------------------------------------------------------------- /response/roamBufferHandler.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import sqlite3 4 | 5 | from response.requestHandler import RequestHandler 6 | 7 | 8 | class RoamBufferHandler(RequestHandler): 9 | def __init__(self, org_roam_db, path, label): 10 | super().__init__() 11 | self.org_roam_db = org_roam_db 12 | self.contentType = "text/html" 13 | 14 | self.contents = ( 15 | """ 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 |
27 | """ 28 | + "
" 29 | + "

" 30 | + label[0] 31 | + "" 33 | + self.get_backlinks(path) 34 | + """ 35 | 36 | 37 | """ 38 | ) 39 | self.setStatus(200) 40 | 41 | def getContents(self): 42 | return self.contents 43 | 44 | def get_backlinks(self, path): 45 | conn = sqlite3.connect(self.org_roam_db) 46 | c = conn.cursor() 47 | 48 | path_quoted = '"' + path[0] + '"' 49 | query = ( 50 | """ 51 | SELECT [source], title, [dest], [properties] 52 | FROM links 53 | LEFT OUTER JOIN titles 54 | ON titles.file = [source] 55 | WHERE [dest] = '%s' 56 | """ 57 | % path_quoted 58 | ) 59 | c.execute(query) 60 | results = c.fetchall() 61 | conn.close() 62 | 63 | html = "" 64 | for item in results: 65 | file_title = item[1].strip('"') 66 | file_id = os.path.basename(item[0]) 67 | file_backlinks = item[3].split("[[file:") 68 | backlinks_html = ( 69 | '

' 70 | + "

" 71 | + '' 74 | + file_title 75 | + "" 76 | + "

" 77 | + '

' 78 | ) 79 | for backlink_str in file_backlinks: 80 | 81 | try: 82 | backlink_str[0: backlink_str.index("]]")] 83 | except Exception: 84 | continue 85 | 86 | backlink_str = backlink_str[0: backlink_str.index( 87 | "]]")].split("][") 88 | backlink_id = os.path.basename(backlink_str[0]) 89 | backlink_title = backlink_str[1] 90 | backlinks_html = ( 91 | backlinks_html 92 | + '' 95 | + backlink_title 96 | + "" 97 | + " " 98 | ) 99 | backlinks_html = backlinks_html + "

" 100 | html = html + backlinks_html 101 | 102 | return html 103 | -------------------------------------------------------------------------------- /response/roamDataHandler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import json 5 | import sqlite3 6 | import urllib.parse 7 | from response.requestHandler import RequestHandler 8 | 9 | previous_db_mtime = 0 10 | 11 | 12 | class RoamDataHandler(RequestHandler): 13 | def __init__(self, roam_force, org_roam_db): 14 | super().__init__() 15 | global previous_db_mtime 16 | 17 | self.org_roam_db = org_roam_db 18 | self.contentType = "text/event-stream" 19 | 20 | if len(roam_force) > 0: 21 | is_force = roam_force[0] 22 | else: 23 | is_force = False 24 | 25 | current_db_mtime = os.path.getmtime(org_roam_db) 26 | 27 | if current_db_mtime == previous_db_mtime and not is_force: 28 | self.contents = "" 29 | else: 30 | self.contents = "data: " + self.roam_server_data() + "\n\n" 31 | previous_db_mtime = current_db_mtime 32 | 33 | self.setStatus(200) 34 | 35 | def getContents(self): 36 | return self.contents 37 | 38 | def roam_server_data(self): 39 | 40 | graph = {"nodes": [], "edges": []} 41 | 42 | conn = sqlite3.connect(self.org_roam_db) 43 | 44 | c = conn.cursor() 45 | 46 | node_query = """SELECT titles.file,title,tags 47 | FROM titles 48 | LEFT OUTER JOIN tags 49 | ON titles.file = tags.file""" 50 | c.execute(node_query) 51 | nodes = c.fetchall() 52 | 53 | for node in nodes: 54 | d = {} 55 | path = node[0].strip('"') 56 | d["id"] = os.path.splitext(os.path.basename(path))[0] 57 | title = node[1].strip('"') 58 | d["title"] = title 59 | if node[2]: 60 | tags = node[2].rstrip(')').lstrip('(').split() 61 | sanitized_tags = [] 62 | for tag in tags: 63 | sanitized_tag = tag.strip('"') 64 | sanitized_tags.append(sanitized_tag) 65 | d["tags"] = sanitized_tags 66 | else: 67 | d["tags"] = node[2] 68 | d["label"] = title 69 | d["url"] = "org-protocol://roam-file?file=" + \ 70 | urllib.parse.quote_plus(path) 71 | d["path"] = path 72 | 73 | can_append = True 74 | for item in graph["nodes"]: 75 | if item["id"] == d["id"]: 76 | can_append = False 77 | break 78 | else: 79 | pass 80 | 81 | if can_append: 82 | graph["nodes"].append(d) 83 | 84 | edges_query = """WITH selected AS (SELECT file FROM files) 85 | SELECT DISTINCT [source],[dest] 86 | FROM links 87 | WHERE [dest] IN selected AND [source] IN selected""" 88 | c.execute(edges_query) 89 | edges = c.fetchall() 90 | 91 | for edge in edges: 92 | d = {} 93 | striped_from = edge[0].rstrip(')"').lstrip('("') 94 | striped_to = edge[1].rstrip(')"').lstrip('("') 95 | d["from"] = os.path.splitext(os.path.basename(striped_from))[0] 96 | d["to"] = os.path.splitext(os.path.basename(striped_to))[0] 97 | d["arrows"] = None 98 | graph["edges"].append(d) 99 | 100 | conn.close() 101 | return json.dumps(graph, ensure_ascii=False) 102 | -------------------------------------------------------------------------------- /public/bootstrap-toggle.min.js: -------------------------------------------------------------------------------- 1 | /*! ======================================================================== 2 | * Bootstrap Toggle: bootstrap-toggle.js v2.2.0 3 | * http://www.bootstraptoggle.com 4 | * ======================================================================== 5 | * Copyright 2014 Min Hur, The New York Times Company 6 | * Licensed under MIT 7 | * ======================================================================== */ 8 | +function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.toggle"),f="object"==typeof b&&b;e||d.data("bs.toggle",e=new c(this,f)),"string"==typeof b&&e[b]&&e[b]()})}var c=function(b,c){this.$element=a(b),this.options=a.extend({},this.defaults(),c),this.render()};c.VERSION="2.2.0",c.DEFAULTS={on:"On",off:"Off",onstyle:"primary",offstyle:"default",size:"normal",style:"",width:null,height:null},c.prototype.defaults=function(){return{on:this.$element.attr("data-on")||c.DEFAULTS.on,off:this.$element.attr("data-off")||c.DEFAULTS.off,onstyle:this.$element.attr("data-onstyle")||c.DEFAULTS.onstyle,offstyle:this.$element.attr("data-offstyle")||c.DEFAULTS.offstyle,size:this.$element.attr("data-size")||c.DEFAULTS.size,style:this.$element.attr("data-style")||c.DEFAULTS.style,width:this.$element.attr("data-width")||c.DEFAULTS.width,height:this.$element.attr("data-height")||c.DEFAULTS.height}},c.prototype.render=function(){this._onstyle="btn-"+this.options.onstyle,this._offstyle="btn-"+this.options.offstyle;var b="large"===this.options.size?"btn-lg":"small"===this.options.size?"btn-sm":"mini"===this.options.size?"btn-xs":"",c=a('