Add graph possibilities
All checks were successful
continuous-integration/drone/push Build is passing

- Add dot language, which renders graphs
- Show search results when no results
- Add graph page
This commit is contained in:
Peter Stuifzand 2020-06-17 17:41:32 +02:00
parent a2cca78018
commit fd755bfb61
5 changed files with 253 additions and 16 deletions

View File

@ -7,6 +7,14 @@
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-3.1.0.tgz",
"integrity": "sha512-GcIY79elgB+azP74j8vqkiXz8xLFfIzbQJdlwOPisgbKT00tviJQuEghOXSMVxJ00HoYJbGswr4kcllUc4xCcg==" "integrity": "sha512-GcIY79elgB+azP74j8vqkiXz8xLFfIzbQJdlwOPisgbKT00tviJQuEghOXSMVxJ00HoYJbGswr4kcllUc4xCcg=="
}, },
"@egjs/hammerjs": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
"integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
"requires": {
"@types/hammerjs": "^2.0.36"
}
},
"@types/anymatch": { "@types/anymatch": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
@ -30,6 +38,11 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/hammerjs": {
"version": "2.0.36",
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.36.tgz",
"integrity": "sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ=="
},
"@types/minimatch": { "@types/minimatch": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -4532,6 +4545,11 @@
"verror": "1.10.0" "verror": "1.10.0"
} }
}, },
"keycharm": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.3.1.tgz",
"integrity": "sha512-zn47Ti4FJT9zdF+YBBLWJsfKF/fYQHkrYlBeB5Ez5e2PjW7SoIxr43yehAne2HruulIoid4NKZZxO0dHBygCtQ=="
},
"killable": { "killable": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@ -6981,6 +6999,13 @@
"tough-cookie": "~2.5.0", "tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0", "tunnel-agent": "^0.6.0",
"uuid": "^3.3.2" "uuid": "^3.3.2"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
}
} }
}, },
"require-directory": { "require-directory": {
@ -7532,6 +7557,14 @@
"faye-websocket": "^0.10.0", "faye-websocket": "^0.10.0",
"uuid": "^3.4.0", "uuid": "^3.4.0",
"websocket-driver": "0.6.5" "websocket-driver": "0.6.5"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
} }
}, },
"sockjs-client": { "sockjs-client": {
@ -8266,6 +8299,11 @@
"setimmediate": "^1.0.4" "setimmediate": "^1.0.4"
} }
}, },
"timsort": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
},
"tiny-emitter": { "tiny-emitter": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
@ -8636,9 +8674,9 @@
"dev": true "dev": true
}, },
"uuid": { "uuid": {
"version": "3.4.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg=="
}, },
"v8-compile-cache": { "v8-compile-cache": {
"version": "2.0.3", "version": "2.0.3",
@ -8671,6 +8709,21 @@
"extsprintf": "^1.2.0" "extsprintf": "^1.2.0"
} }
}, },
"vis-data": {
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-6.6.1.tgz",
"integrity": "sha512-xmujDB2Dzf8T04rGFJ9OP4OA6zRVrz8R9hb0CVKryBrZRCljCga9JjSfgctA8S7wdZu7otDtUIwX4ZOgfV/57w=="
},
"vis-network": {
"version": "7.6.10",
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-7.6.10.tgz",
"integrity": "sha512-wL1dHBWWpzxvUaM0miccDuSLQ2tkw93jCA3j4Zizh4ruph+UXnjkouayaOyJIx43wULUSoKGWkhE6na1q208TA=="
},
"vis-util": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-4.3.2.tgz",
"integrity": "sha512-FIS75hhrzbX1qJwFVwVVm1q2/TEktJWjgWsV0T3E9AYC4PWyQCBKk2LgsSLi+O8NBi7gTe9D4K75MqdPTHrRnA=="
},
"vm-browserify": { "vm-browserify": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
@ -8995,6 +9048,14 @@
"requires": { "requires": {
"ansi-colors": "^3.0.0", "ansi-colors": "^3.0.0",
"uuid": "^3.3.2" "uuid": "^3.3.2"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
} }
}, },
"webpack-sources": { "webpack-sources": {
@ -9076,9 +9137,9 @@
} }
}, },
"wiki-list-editor": { "wiki-list-editor": {
"version": "0.7.8", "version": "0.8.0",
"resolved": "https://registry.npmjs.org/wiki-list-editor/-/wiki-list-editor-0.7.8.tgz", "resolved": "https://registry.npmjs.org/wiki-list-editor/-/wiki-list-editor-0.8.0.tgz",
"integrity": "sha512-t9O3Guey/EI6KdWLn2YVN43I70pBRpOov8iq0kPa9ZukE+eE55iNUWBQW7D61QypUwBegqQWEm4zAQyoo9f8FA==", "integrity": "sha512-mA1JyClhwrvf+sZgZZm7H4TwLQ+xcciJThoZz+Ya7JVrffmWCex8HRByXfxpOA4C0XDNC/Wx1RpCYjf/zBHH3Q==",
"requires": { "requires": {
"dragula": "^3.7.2", "dragula": "^3.7.2",
"he": "^1.2.0", "he": "^1.2.0",

View File

@ -11,6 +11,7 @@
"webpack-dev-server": "^3.11.0" "webpack-dev-server": "^3.11.0"
}, },
"dependencies": { "dependencies": {
"@egjs/hammerjs": "^2.0.17",
"axios": "^0.19.0", "axios": "^0.19.0",
"bulma": "^0.7.5", "bulma": "^0.7.5",
"clipboard": "^2.0.6", "clipboard": "^2.0.6",
@ -18,6 +19,7 @@
"file-loader": "^6.0.0", "file-loader": "^6.0.0",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"jquery-contextmenu": "^2.9.2", "jquery-contextmenu": "^2.9.2",
"keycharm": "^0.3.1",
"lunr": "^2.3.8", "lunr": "^2.3.8",
"markdown-it": "^11.0.0", "markdown-it": "^11.0.0",
"markdown-it-mark": "^3.0.0", "markdown-it-mark": "^3.0.0",
@ -29,7 +31,12 @@
"prismjs": "^1.20.0", "prismjs": "^1.20.0",
"sass-loader": "^7.3.1", "sass-loader": "^7.3.1",
"style-loader": "^1.0.0", "style-loader": "^1.0.0",
"wiki-list-editor": "^0.7.8" "timsort": "^0.3.0",
"uuid": "^8.1.0",
"vis-data": "^6.6.1",
"vis-network": "^7.6.10",
"vis-util": "^4.3.2",
"wiki-list-editor": "^0.8.0"
}, },
"scripts": { "scripts": {
"test": "node_modules/.bin/mocha -r esm", "test": "node_modules/.bin/mocha -r esm",

View File

@ -13,6 +13,7 @@ import 'jquery-contextmenu';
import getCaretCoordinates from './caret-position' import getCaretCoordinates from './caret-position'
import moment from 'moment' import moment from 'moment'
import mermaid from 'mermaid' import mermaid from 'mermaid'
import { parseDOTNetwork, Network } from "vis-network/peer";
import PrismJS from 'prismjs' import PrismJS from 'prismjs'
import 'prismjs/plugins/filter-highlight-all/prism-filter-highlight-all' import 'prismjs/plugins/filter-highlight-all/prism-filter-highlight-all'
import 'prismjs/components/prism-php' import 'prismjs/components/prism-php'
@ -21,12 +22,17 @@ import 'prismjs/components/prism-perl'
import 'prismjs/components/prism-css' import 'prismjs/components/prism-css'
import 'prismjs/components/prism-markup-templating' import 'prismjs/components/prism-markup-templating'
import 'prismjs/components/prism-jq' import 'prismjs/components/prism-jq'
import './styles.scss';
import '../node_modules/jquery-contextmenu/dist/jquery.contextMenu.css'; import './styles.scss'
import '../node_modules/jquery-contextmenu/dist/jquery.contextMenu.css'
import 'vis-network/styles/vis-network.css'
moment.locale('nl') moment.locale('nl')
mermaid.initialize({startOnLoad: true}) mermaid.initialize({startOnLoad: true})
PrismJS.plugins.filterHighlightAll.reject.addSelector('.language-mermaid')
PrismJS.plugins.filterHighlightAll.reject.addSelector('.language-dot')
function isMultiline(input) { function isMultiline(input) {
return input.value.startsWith("```", 0) return input.value.startsWith("```", 0)
} }
@ -83,20 +89,20 @@ function addIndicator(editor, indicator) {
let holder = document.getElementById('editor'); let holder = document.getElementById('editor');
function showSearchResults(searchTool, query, input, value, resultType) { function showSearchResults(searchTool, query, input, value, resultType) {
showSearchResultsExtended('#link-complete', 'link-template', searchTool, query, input, value, resultType, {belowCursor: true}) showSearchResultsExtended('#link-complete', 'link-template', searchTool, query, input, value, resultType, {showOnlyResults:true, belowCursor: true})
} }
function showSearchResultsExtended(element, template, searchTool, query, input, value, resultType, options) { function showSearchResultsExtended(element, template, searchTool, query, input, value, resultType, options) {
const $lc = $(element) const $lc = $(element)
let results = searchTool(query) let results = searchTool(query)
if (query.length === 0 || !results.length) { let opt = options || {};
if (opt.showOnlyResults && (query.length === 0 || !results.length)) {
$lc.fadeOut() $lc.fadeOut()
return return
} }
let opt = options || {};
$lc.data('result-type', resultType) $lc.data('result-type', resultType)
if (opt.belowCursor) { if (opt.belowCursor) {
@ -180,6 +186,24 @@ $(document).on('popup:selected', '#autocomplete', function (event, linkName, res
} }
}) })
function renderGraphs() {
$('code.language-dot').each(function (i, code) {
if (!code.innerText) {
return true
}
let data = parseDOTNetwork(code.innerText)
let network = new Network(code, data, {
layout: {
randomSeed: 1239043
}
});
$(code).on('click', function () {
return false
})
})
}
if (holder) { if (holder) {
const MD = new MarkdownIt({ const MD = new MarkdownIt({
linkify: true, linkify: true,
@ -200,9 +224,9 @@ if (holder) {
})).use(MarkdownItMark) })).use(MarkdownItMark)
const options = { const options = {
transform(text, callback) { transform(text, element) {
let converted = (text.startsWith("```", 0)) ? MD.render(text) : MD.renderInline(text) let converted = (text.startsWith("```", 0)) ? MD.render(text) : MD.renderInline(text)
return callback(converted) element.html(converted)
} }
} }
@ -221,6 +245,12 @@ if (holder) {
).save() ).save()
}) })
editor.on('rendered', function () {
PrismJS.highlightAll()
mermaid.init()
renderGraphs();
})
createPageSearch().then(function ({titleSearch, commandSearch, commands}) { createPageSearch().then(function ({titleSearch, commandSearch, commands}) {
editor.on('start-editing', function (input) { editor.on('start-editing', function (input) {
const $lc = $('#link-complete'); const $lc = $('#link-complete');
@ -370,9 +400,9 @@ if (holder) {
editor.on('stop-editing', function (input) { editor.on('stop-editing', function (input) {
$(input).parents('.list-item').removeClass('active'); $(input).parents('.list-item').removeClass('active');
$('#link-complete').off() $('#link-complete').off()
PrismJS.plugins.filterHighlightAll.reject.addSelector('.language-mermaid')
PrismJS.highlightAll() PrismJS.highlightAll()
mermaid.init() mermaid.init()
renderGraphs();
}) })
}) })
$.contextMenu({ $.contextMenu({

138
main.go
View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
@ -10,6 +11,8 @@ import (
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
"os"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -94,6 +97,26 @@ type indexPage struct {
Backrefs map[string][]Backref Backrefs map[string][]Backref
} }
type Node struct {
Id int `json:"id"`
Label string `json:"label"`
}
type Edge struct {
From int `json:"from"`
To int `json:"to"`
}
type graphPage struct {
pageBaseInfo
Session *Session
Title string
Name string
References Refs
Nodes template.JS
Edges template.JS
}
type editPage struct { type editPage struct {
pageBaseInfo pageBaseInfo
Session *Session Session *Session
@ -121,6 +144,7 @@ type recentPage struct {
} }
type indexHandler struct{} type indexHandler struct{}
type graphHandler struct{}
type saveHandler struct{} type saveHandler struct{}
type editHandler struct{} type editHandler struct{}
type historyHandler struct{} type historyHandler struct{}
@ -428,6 +452,119 @@ func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
func (h *graphHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
sess, err := NewSession(w, r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer func() {
if err := sess.Flush(); err != nil {
log.Println(err)
}
}()
refs := make(Refs)
f, err := os.Open(filepath.Join(mp.(*FilePages).dirname, "backrefs.json"))
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer f.Close()
err = json.NewDecoder(f).Decode(&refs)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var nodesBuf bytes.Buffer
var edgesBuf bytes.Buffer
var nodes []Node
var edges []Edge
nodeCount := 1
nodeMap := make(map[string]int)
for key, references := range refs {
if _, e := nodeMap[key]; !e {
nodeMap[key] = nodeCount
nodeCount += 1
}
for _, item := range references {
if _, e := nodeMap[item.Name]; !e {
nodeMap[item.Name] = nodeCount
nodeCount += 1
}
}
}
for name, id := range nodeMap {
nodes = append(nodes, Node{
Id: id,
Label: name,
})
}
edgeSet := make(map[Edge]bool)
for key, references := range refs {
if toID, e := nodeMap[key]; e {
for _, item := range references {
if fromID, e := nodeMap[item.Name]; e {
edge := Edge{
From: fromID,
To: toID,
}
if _, e := edgeSet[edge]; !e {
edgeSet[edge] = true
edges = append(edges, edge)
}
}
}
}
}
err = json.NewEncoder(&nodesBuf).Encode(&nodes)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = json.NewEncoder(&edgesBuf).Encode(&edges)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
pageBase := getPageBase()
data := graphPage{
pageBaseInfo: pageBase,
Session: sess,
Title: "Graph",
Name: "Graph",
References: refs,
Nodes: template.JS(nodesBuf.String()),
Edges: template.JS(edgesBuf.String()),
}
t, err := template.ParseFiles("templates/layout.html", "templates/graph.html")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = t.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() defer r.Body.Close()
@ -749,6 +886,7 @@ func main() {
http.Handle("/edit/", &editHandler{}) http.Handle("/edit/", &editHandler{})
http.Handle("/history/", &historyHandler{}) http.Handle("/history/", &historyHandler{})
http.Handle("/recent/", &recentHandler{}) http.Handle("/recent/", &recentHandler{})
http.Handle("/graph/", &graphHandler{})
http.Handle("/", &indexHandler{}) http.Handle("/", &indexHandler{})
fmt.Printf("Running on port %d\n", *port) fmt.Printf("Running on port %d\n", *port)

View File

@ -25,6 +25,7 @@
<a href="/edit/{{ .Name }}" class="navbar-item">Edit</a> <a href="/edit/{{ .Name }}" class="navbar-item">Edit</a>
<a href="/history/{{ .Name }}" class="navbar-item">History</a> <a href="/history/{{ .Name }}" class="navbar-item">History</a>
<a href="/recent/" class="navbar-item">Recent Changes</a> <a href="/recent/" class="navbar-item">Recent Changes</a>
<a href="/graph/" class="navbar-item">Graph</a>
<a href="/auth/logout" class="navbar-item">Logout</a> <a href="/auth/logout" class="navbar-item">Logout</a>
<span class="navbar-item"><b>{{ $.Session.Me }}</b></span> <span class="navbar-item"><b>{{ $.Session.Me }}</b></span>
{{ else }} {{ else }}