├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── requirements.txt ├── screenshots └── vimwikigraph.png ├── setup.py ├── vimwikigraph.cfg ├── vimwikigraph.sh └── vimwikigraph ├── __init__.py ├── app.py ├── static └── main.css ├── templates └── index.html ├── vimwikigraph.py └── vimwikitags.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.dot 2 | __pycache__ 3 | vimwikigraph.egg-info 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | VimWikiGraph 2 | Copyright © 2022 lambdasonly 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 20 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft flaskapp/static 2 | graft flaskapp/templates 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Introduction 3 | VimWikiGraph is a flask web application for visualising [VimWiki](https://github.com/vimwiki/vimwiki). 4 | It creates an undirected graphs of links between VimWiki files that affords filtering and highlighting. 5 | Highlighting and filtering are performed via regular expressions so multiple keywords can be combined with 6 | the pipe operator `expr1|expr2`. 7 |  8 | 9 | 10 | # Installation 11 | Clone the repository, run `pip install -e .` and copy `vimwikigraph.sh` to a directory included in your PATH. 12 | 13 | 14 | # Configuration 15 | The path to the wiki must be specified in a config file indicated by an environment variable. A template 16 | config file is provided. 17 | ``` 18 | export VIMWIKIGRAPH_CONFIG=~/path/to/vimwikigraph.cfg 19 | ``` 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask-VisJS==0.1.4 2 | Flask<=2.2.2 3 | networkx==2.8.4 4 | numpy==1.23.4 5 | pyvis==0.3.2 6 | Werkzeug<=2.2.2 7 | -------------------------------------------------------------------------------- /screenshots/vimwikigraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdasonly/VimWikiGraph/7ceea069b4c16c137ebb66a2a77d13936f9e279a/screenshots/vimwikigraph.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name='vimwikigraph', 5 | version='0.1.0', 6 | packages=find_packages(), 7 | include_package_data=True, 8 | install_requires=[ 9 | 'flask', 10 | 'flask-visjs', 11 | 'networkx', 12 | 'numpy', 13 | 'pyvis' 14 | ], 15 | ) 16 | -------------------------------------------------------------------------------- /vimwikigraph.cfg: -------------------------------------------------------------------------------- 1 | DEFAULT_FILTER = ['default|filter|regex|expr'] 2 | DEFAULT_INVERT_FILTER = False 3 | DEFAULT_FILE_FILTER = ['^202[0-2]'] 4 | DEFAULT_INVERT_FILE_FILTER = True 5 | DEFAULT_HIGHLIGHT = [':important:'] 6 | EXCLUDE_TAGS = ['private', 'tags'] 7 | SEPARATOR = ';' 8 | -------------------------------------------------------------------------------- /vimwikigraph.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | flask --app vimwikigraph run 3 | -------------------------------------------------------------------------------- /vimwikigraph/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import create_app 2 | 3 | app = create_app() 4 | -------------------------------------------------------------------------------- /vimwikigraph/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | from flask import Flask, render_template, request 5 | from flask_visjs import VisJS4, Network 6 | 7 | from .vimwikigraph import VimwikiGraph 8 | from .vimwikitags import VimwikiTags 9 | 10 | 11 | app = Flask(__name__) 12 | app.config.from_envvar('VIMWIKIGRAPH_CONFIG') 13 | VisJS4().init_app(app) 14 | 15 | 16 | class State: 17 | instance = None 18 | 19 | def __init__(self): 20 | self.vimwikigraphdir = os.environ.get('VIMWIKIDIR', '') 21 | if not self.vimwikigraphdir: 22 | raise ValueError('VIMWIKIDIR environment variable is not set') 23 | self.vimwikigraph = VimwikiGraph(self.vimwikigraphdir) 24 | self.vimwikitags = VimwikiTags(self.vimwikigraphdir) 25 | self.reset_form() 26 | self.exclude_tags = app.config.get('EXCLUDE_TAGS', []) 27 | self.n_tags = app.config.get('N_TAGS', 30) 28 | self.SEP = app.config.get('SEPARATOR', ';') 29 | 30 | def reset_form(self): 31 | self.filter = app.config.get('DEFAULT_FILTER', []) 32 | self.invert_filter = app.config.get('DEFAULT_INVERT_FILTER', False) 33 | self.highlight = app.config.get('DEFAULT_HIGHLIGHT', []) 34 | self.filename_filter = app.config.get('DEFAULT_FILE_FILTER', []) 35 | self.invert_filename_filter = app.config.get('DEFAULT_INVERT_FILE_FILTER', False) 36 | self.collapse = app.config.get('DEFAULT_COLLAPSE', []) 37 | 38 | @staticmethod 39 | def get_instance(): 40 | if State.instance is None: 41 | State.instance = State() 42 | return State.instance 43 | 44 | def set_form(self, filter, invert_filter, filename_filter, invert_file_filter, highlight, collapse): 45 | self.filter = filter.split(self.SEP) 46 | self.invert_filter = invert_filter 47 | self.filename_filter = filename_filter.split(self.SEP) 48 | self.invert_filename_filter = invert_file_filter 49 | self.highlight = highlight.split(self.SEP) 50 | self.collapse = collapse.split(self.SEP) 51 | 52 | def get_graph(self): 53 | return self.vimwikigraph 54 | 55 | def __str__(self): 56 | msg = "Filter" 57 | if self.invert_filter: 58 | msg += "[X]" 59 | msg += f": {self.filter}\nFilename filter" 60 | if self.invert_filename_filter: 61 | msg += "[X]" 62 | msg += f": {self.filename_filter}\nHighlight: {self.highlight}" 63 | return msg 64 | 65 | 66 | @app.route('/', methods=['GET', 'POST']) 67 | def route_index(): 68 | if request.method == 'GET': 69 | state = State.get_instance() 70 | rendered = render_template( 71 | 'index.html', 72 | filter_value=state.SEP.join(state.filter), 73 | invert_filter_value=state.invert_filter, 74 | filename_value=state.SEP.join(state.filename_filter), 75 | invert_filename_value=state.invert_filename_filter, 76 | highlight_value=state.SEP.join(state.highlight), 77 | collapse_value=state.SEP.join(state.collapse), 78 | sep=state.SEP, 79 | ) 80 | return rendered 81 | if request.method == 'POST': 82 | state = State.get_instance() 83 | state.set_form( 84 | request.form['inptFilter'], 85 | 'inptInvertFilter' in request.form, 86 | request.form['inptFileFilter'], 87 | 'inptInvertFileFilter' in request.form, 88 | request.form['inptHighlight'], 89 | request.form['inptCollapse'], 90 | ) 91 | rendered = render_template( 92 | 'index.html', 93 | filter_value=state.SEP.join(state.filter), 94 | invert_filter_value=state.invert_filter, 95 | filename_value=state.SEP.join(state.filename_filter), 96 | invert_filename_value=state.invert_filename_filter, 97 | highlight_value=state.SEP.join(state.highlight), 98 | collapse_value=state.SEP.join(state.collapse), 99 | sep=state.SEP, 100 | ) 101 | return rendered 102 | 103 | 104 | @app.route('/network') 105 | def network_json(): 106 | state = State.get_instance() 107 | graph = state.get_graph().reset_graph() 108 | if state.filename_filter != ['']: 109 | graph = graph.filter_filenames(state.filename_filter, invert=state.invert_filename_filter) 110 | if state.filter != ['']: 111 | graph = graph.filter_nodes(state.filter, invert=state.invert_filter) 112 | if state.collapse != ['']: 113 | graph = graph.collapse_children(state.collapse) 114 | if state.highlight != ['']: 115 | attributes = ['color', 'style'] 116 | values = ['red', 'filled'] 117 | graph = graph.add_attribute_by_regex(state.highlight, attributes, values) 118 | network = Network( 119 | neighborhood_highlight=True, 120 | filter_menu=True, 121 | cdn_resources='remote', 122 | ) 123 | network.from_nx(graph.graph) 124 | return network.to_json(max_depth=3) 125 | 126 | 127 | @app.route('/node', methods=['POST']) 128 | def node_json(): 129 | state = State.get_instance() 130 | if request.json and 'node' in request.json: 131 | node = request.json['node'] 132 | lines = ''.join(state.vimwikigraph.lines[node]) 133 | for highlight in state.highlight: 134 | lines = re.sub(highlight, r'\g<0>', lines, flags=re.IGNORECASE) 135 | else: 136 | lines = [] 137 | return json.dumps({'text': lines}) 138 | 139 | 140 | @app.route('/reload', methods=['POST']) 141 | def reload(): 142 | state = State.get_instance() 143 | state.vimwikigraph.reload_graph() 144 | state.vimwikitags.reload() 145 | state.set_form( 146 | request.form['inptFilter'], 147 | 'inptInvertFilter' in request.form, 148 | request.form['inptFileFilter'], 149 | 'inptInvertFileFilter' in request.form, 150 | request.form['inptHighlight'], 151 | request.form['inptCollapse'], 152 | ) 153 | rendered = render_template( 154 | 'index.html', 155 | filter_value=state.SEP.join(state.filter), 156 | invert_filter_value=state.invert_filter, 157 | filename_value=state.SEP.join(state.filename_filter), 158 | invert_filename_value=state.invert_filename_filter, 159 | highlight_value=state.SEP.join(state.highlight), 160 | collapse_value=state.SEP.join(state.collapse), 161 | sep=state.SEP, 162 | ) 163 | return rendered 164 | 165 | 166 | @app.route('/reset', methods=['GET']) 167 | def reset(): 168 | state = State.get_instance() 169 | state.reset_form() 170 | return json.dumps({ 171 | 'filter_value': state.SEP.join(state.filter), 172 | 'invert_filter_value': state.invert_filter, 173 | 'filename_value': state.SEP.join(state.filename_filter), 174 | 'invert_filename_value': state.invert_filename_filter, 175 | 'highlight_value': state.highlight, 176 | 'collapse_value': state.SEP.join(state.collapse), 177 | }) 178 | 179 | 180 | @app.route('/tags', methods=['GET']) 181 | def tags(): 182 | state = State.get_instance() 183 | count_dict = state.vimwikitags.populate_tags() 184 | tags = [tag for tag in list(count_dict.keys()) if tag not in state.exclude_tags] 185 | return json.dumps({'tags': tags[:state.n_tags]}) 186 | 187 | 188 | def create_app(): 189 | return app 190 | -------------------------------------------------------------------------------- /vimwikigraph/static/main.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | margin: 0; 4 | overflow: hidden; 5 | } 6 | 7 | header { 8 | background-color: #909090; 9 | font-size: 30px; 10 | text-align: center; 11 | padding: 20px; 12 | } 13 | 14 | #content { 15 | display: flex; 16 | } 17 | 18 | nav { 19 | width: min-content; 20 | height: 90vh; 21 | background: #ddd; 22 | padding: 10px; 23 | font-size: 20px; 24 | } 25 | 26 | .filterinputs { 27 | display: flex; 28 | align-items: center; 29 | gap: 5px; 30 | } 31 | 32 | #buttons { 33 | display: flex; 34 | padding: 5px; 35 | } 36 | 37 | #btnApply, #btnReset, #btnReload { 38 | margin: 5px; 39 | padding: 5px; 40 | } 41 | 42 | #graphContainer { 43 | position: relative; 44 | width: 100%; 45 | } 46 | 47 | #graph { 48 | width: 100%; 49 | height: 90vh; 50 | } 51 | 52 | #nodeText { 53 | position: absolute; 54 | left: 0; 55 | top: 0; 56 | width: 90%; 57 | padding: 10px; 58 | margin-left: 50px; 59 | margin-right: 50px; 60 | white-space: pre-line; 61 | overflow-y: auto; 62 | height: 85vh; 63 | visibility: hidden; 64 | } 65 | 66 | .tag { 67 | font-size: 12px; 68 | display: inline-block; 69 | padding: 2px 3px; 70 | margin: 3px; 71 | background-color: #ccc; 72 | border-radius: 5px; 73 | cursor: pointer; 74 | } 75 | 76 | .tag.selected { 77 | background-color: #007bff; 78 | color: #fff; 79 | } 80 | 81 | .switch { 82 | position: relative; 83 | display: inline-block; 84 | width: 16px; 85 | height: 25px; 86 | } 87 | 88 | .switch input { 89 | opacity: 0; 90 | width: 0; 91 | height: 0; 92 | } 93 | 94 | .slider { 95 | position: absolute; 96 | cursor: pointer; 97 | top: 0; 98 | left: 0; 99 | right: 0; 100 | bottom: 0; 101 | background-color: #ccc; 102 | transition: 0.4s; 103 | border-radius: 20px; 104 | } 105 | 106 | .slider:before { 107 | position: absolute; 108 | content: ""; 109 | height: 14px; 110 | width: 14px; 111 | left: 1px; 112 | top: 1.5px; 113 | background-color: white; 114 | transition: 0.2s; 115 | border-radius: 50%; 116 | } 117 | 118 | input:checked + .slider { 119 | background-color: #909090; 120 | } 121 | 122 | input:checked + .slider:before { 123 | transform: translateY(8px); 124 | } 125 | -------------------------------------------------------------------------------- /vimwikigraph/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |