Show graph on edit page for neighbors two edges out
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Peter Stuifzand 2020-07-05 20:34:36 +02:00
parent 005be8c733
commit 5bf14bd1d4
10 changed files with 257 additions and 85 deletions

View File

@ -120,8 +120,8 @@ func getBackrefs(fp *FilePages, p string) (map[string][]Backref, error) {
} }
line = metaKV.ReplaceAllString(line, "**[[$1]]**: $2") line = metaKV.ReplaceAllString(line, "**[[$1]]**: $2")
links := renderLinks(line) pageText := renderMarkdown2(renderLinks(line, false))
pageText := renderMarkdown2(links) editPageText := renderMarkdown2(renderLinks(line, true))
removeBrackets := func(r rune) rune { removeBrackets := func(r rune) rune {
if r == '[' || r == ']' || r == '*' { if r == '[' || r == ']' || r == '*' {
@ -134,6 +134,7 @@ func getBackrefs(fp *FilePages, p string) (map[string][]Backref, error) {
Name: ref.Name, Name: ref.Name,
Title: title, Title: title,
LineHTML: template.HTML(pageText), LineHTML: template.HTML(pageText),
LineEditHTML: template.HTML(editPageText),
Line: strings.Map(removeBrackets, ref.Link.Line), Line: strings.Map(removeBrackets, ref.Link.Line),
}) })
} }

59
editor/src/graph.js Normal file
View File

@ -0,0 +1,59 @@
import {DataSet} from "vis-data/peer";
import {Network} from "vis-network/peer";
import $ from 'jquery';
function wikiGraph(selector, options) {
$(selector).each(function (i, el) {
let $el = $(el)
var nodeName = $el.data('name')
fetch('/api/graph?name=' + nodeName)
.then(res => res.json())
.then(graph => {
var nodes = new DataSet(graph.nodes)
var edges = new DataSet(graph.edges)
var data = {
nodes: nodes,
edges: edges
};
var options = {
edges: {
arrows: 'to',
color: {
highlight: 'green'
},
chosen: {
edge: function (values, id, selected, hovering) {
if (this.from === 1) {
values.color = 'blue';
}
}
}
},
nodes: {
shape: 'dot',
size: 15,
font: {
background: 'white'
}
},
layout: {
improvedLayout: false
}
};
var network = new Network(el, data, options);
network.on('doubleClick', function (props) {
if (props.nodes.length) {
let nodeId = props.nodes[0]
let node = nodes.get(nodeId)
window.location.href = '/edit/' + node.label
}
})
})
})
}
export default wikiGraph

View File

@ -23,6 +23,7 @@ import 'prismjs/components/prism-markup-templating'
import 'prismjs/components/prism-jq' import 'prismjs/components/prism-jq'
import menu from './menu.js' import menu from './menu.js'
import './styles.scss' import './styles.scss'
import wikiGraph from './graph'
moment.locale('nl') moment.locale('nl')
mermaid.initialize({startOnLoad: true}) mermaid.initialize({startOnLoad: true})
@ -30,6 +31,10 @@ mermaid.initialize({startOnLoad: true})
PrismJS.plugins.filterHighlightAll.reject.addSelector('.language-mermaid') PrismJS.plugins.filterHighlightAll.reject.addSelector('.language-mermaid')
PrismJS.plugins.filterHighlightAll.reject.addSelector('.language-dot') PrismJS.plugins.filterHighlightAll.reject.addSelector('.language-dot')
$(function () {
wikiGraph('.graph-network')
})
function isMultiline(input) { function isMultiline(input) {
return input.value.startsWith("```", 0) return input.value.startsWith("```", 0)
} }

View File

@ -8,6 +8,7 @@ import (
"html" "html"
"html/template" "html/template"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -40,7 +41,10 @@ func NewFilePages(dirname string, index bleve.Index) PagesRepository {
fp := &FilePages{dirname, make(chan saveMessage), index} fp := &FilePages{dirname, make(chan saveMessage), index}
go func() { go func() {
for msg := range fp.saveC { for msg := range fp.saveC {
fp.save(msg) err := fp.save(msg)
if err != nil {
log.Println(err)
}
} }
}() }()
return fp return fp

View File

@ -10,6 +10,7 @@ import (
type graphBuilder struct { type graphBuilder struct {
refs Refs refs Refs
nodeMap NodeMap nodeMap NodeMap
nodeCount int
nodes []Node nodes []Node
edges []Edge edges []Edge
} }
@ -31,17 +32,23 @@ func NewGraphBuilder(mp PagesRepository) (*graphBuilder, error) {
return nil, err return nil, err
} }
nodeMap := prepareNodeMap(refs)
return &graphBuilder{ return &graphBuilder{
refs: refs, refs: refs,
nodeMap: nodeMap, nodeMap: make(NodeMap),
nodes: nil, nodes: nil,
edges: nil, edges: nil,
}, nil }, nil
} }
func (gb *graphBuilder) prepareGraph() error { func (gb *graphBuilder) prepareGraph() error {
gb.nodes = prepareNodes(gb.nodeMap) gb.nodes = prepareNodes(gb.nodeMap, func(node Node) Node {
if node.Id == 0 {
var green string
green = "green"
node.Color = &green
}
return node
})
gb.edges = prepareEdges(gb.refs, gb.nodeMap) gb.edges = prepareEdges(gb.refs, gb.nodeMap)
return nil return nil
} }
@ -58,6 +65,26 @@ func (gb *graphBuilder) RemoveNodeWithSuffix(suffix string) {
} }
} }
func (gb *graphBuilder) buildFromCenter(name string) error {
_ = gb.addNode(name)
if ref, e := gb.refs[name]; e {
for _, item := range ref {
gb.addNode(item.Name)
}
}
for key, references := range gb.refs {
for _, item := range references {
if name == item.Name {
gb.addNode(key)
}
}
}
return nil
}
func prepareEdges(refs Refs, nodeMap NodeMap) []Edge { func prepareEdges(refs Refs, nodeMap NodeMap) []Edge {
var edges []Edge var edges []Edge
edgeSet := make(map[Edge]bool) edgeSet := make(map[Edge]bool)
@ -80,33 +107,32 @@ func prepareEdges(refs Refs, nodeMap NodeMap) []Edge {
return edges return edges
} }
func prepareNodes(nodeMap NodeMap) []Node { func prepareNodes(nodeMap NodeMap, apply func(node Node) Node) []Node {
var nodes []Node var nodes []Node
for name, id := range nodeMap { for name, id := range nodeMap {
nodes = append(nodes, Node{ nodes = append(nodes, apply(Node{
Id: id, Id: id,
Label: name, Label: name,
}) Color: nil,
}))
} }
return nodes return nodes
} }
func prepareNodeMap(refs Refs) NodeMap { func (gb *graphBuilder) prepareNodeMap() {
nodeCount := 1 for key, references := range gb.refs {
nodeMap := make(NodeMap) gb.addNode(key)
for key, references := range refs {
if _, e := nodeMap[key]; !e {
nodeMap[key] = nodeCount
nodeCount += 1
}
for _, item := range references { for _, item := range references {
if _, e := nodeMap[item.Name]; !e { gb.addNode(item.Name)
nodeMap[item.Name] = nodeCount
nodeCount += 1
} }
} }
}
func (gb *graphBuilder) addNode(key string) int {
if _, e := gb.nodeMap[key]; !e {
gb.nodeMap[key] = gb.nodeCount
gb.nodeCount += 1
} }
return nodeMap return gb.nodeMap[key]
} }

57
main.go
View File

@ -38,6 +38,7 @@ type Backref struct {
Name string Name string
Title string Title string
LineHTML template.HTML LineHTML template.HTML
LineEditHTML template.HTML
Line string Line string
} }
@ -102,6 +103,7 @@ type indexPage struct {
type Node struct { type Node struct {
Id int `json:"id"` Id int `json:"id"`
Label string `json:"label"` Label string `json:"label"`
Color *string `json:"color"`
} }
type Edge struct { type Edge struct {
@ -483,6 +485,8 @@ func (h *graphHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
gb.prepareNodeMap()
gb.RemoveNode("DONE") gb.RemoveNode("DONE")
gb.RemoveNode("TODO") gb.RemoveNode("TODO")
gb.RemoveNode("Home") gb.RemoveNode("Home")
@ -631,7 +635,7 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
pageText = metaKV.ReplaceAllString(pageText, "**[[$1]]**: $2") pageText = metaKV.ReplaceAllString(pageText, "**[[$1]]**: $2")
pageText = renderLinks(pageText) pageText = renderLinks(pageText, false)
pageText = renderMarkdown2(pageText) pageText = renderMarkdown2(pageText)
pageBase := getPageBase() pageBase := getPageBase()
@ -663,7 +667,7 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
func renderLinks(pageText string) string { func renderLinks(pageText string, edit bool) string {
hrefRE, err := regexp.Compile(`#?\[\[\s*([^\]]+)\s*\]\]`) hrefRE, err := regexp.Compile(`#?\[\[\s*([^\]]+)\s*\]\]`)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -682,7 +686,12 @@ func renderLinks(pageText string) string {
if tag { if tag {
return fmt.Sprintf(`<a href=%q class="tag">%s</a>`, cleanNameURL(s), s) return fmt.Sprintf(`<a href=%q class="tag">%s</a>`, cleanNameURL(s), s)
} }
return fmt.Sprintf("[%s](/%s)", s, cleanNameURL(s)) editPart := ""
if edit {
editPart = "edit/"
}
return fmt.Sprintf("[%s](/%s%s)", s, editPart, cleanNameURL(s))
}) })
return pageText return pageText
} }
@ -791,6 +800,48 @@ func main() {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
http.ServeFile(w, r, filepath.Join(dataDir, LinksFile)) http.ServeFile(w, r, filepath.Join(dataDir, LinksFile))
}) })
http.HandleFunc("/api/graph", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
gb, err := NewGraphBuilder(mp)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = gb.buildFromCenter(name)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// Keep a copy of the nodes, buildFromCenter appends to the nodeMap
var nodes []string
for k, _ := range gb.nodeMap {
nodes = append(nodes, k)
}
for _, node := range nodes {
err = gb.buildFromCenter(node)
}
err = gb.prepareGraph()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
type data struct {
Nodes []Node `json:"nodes"`
Edges []Edge `json:"edges"`
}
err = json.NewEncoder(w).Encode(&data{gb.nodes, gb.edges})
if err != nil {
http.Error(w, err.Error(), 500)
return
}
})
http.Handle("/search/", sh) http.Handle("/search/", sh)
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("./dist")))) http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("./dist"))))
http.Handle("/save/", &saveHandler{}) http.Handle("/save/", &saveHandler{})

View File

@ -3,28 +3,36 @@
{{ end }} {{ end }}
{{define "content"}} {{define "content"}}
<h1 class="title">{{ .Title }}</h1> <div class="columns">
<form action="/save/" method="post"> <div class="column">
<input type="hidden" name="p" value="{{ .Name }}" /> <h1 class="title">{{ .Title }}</h1>
<form action="/save/" method="post">
<input type="hidden" name="p" value="{{ .Name }}"/>
{{ .Editor }} {{ .Editor }}
</form> </form>
{{ if .Backrefs }} {{ if .Backrefs }}
<div class="backrefs content"> <div class="backrefs content">
<h3>Linked references</h3> <h3>Linked references</h3>
<ul> <ul>
{{ range $name, $refs := .Backrefs }} {{ range $name, $refs := .Backrefs }}
<li><a href="/{{ $name }}">{{ (index $refs 0).Title }}</a> <li><a href="/edit/{{ $name }}">{{ (index $refs 0).Title }}</a>
<ul> <ul>
{{ range $ref := $refs }} {{ range $ref := $refs }}
<li>{{ $ref.LineHTML }}</li> <li>{{ $ref.LineEditHTML }}</li>
{{ end }} {{ end }}
</ul> </ul>
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
</div> </div>
{{ end }} {{ end }}
</div>
<div class="column">
<div class="graph-network" data-name="{{ .Name }}" style="height:80vh; top:0; position: sticky"></div>
</div>
</div>
{{ end }} {{ end }}
{{ define "content_head" }} {{ define "content_head" }}

View File

@ -226,7 +226,7 @@
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div>
<nav class="navbar" role="navigation" aria-label="main navigation"> <nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item" href="/"> <a class="navbar-item" href="/">

View File

@ -1,4 +1,7 @@
{{ define "content" }} {{ define "content" }}
<div class="columns">
<div class="column">
<h1 class="title is-1">{{ .Title }}</h1> <h1 class="title is-1">{{ .Title }}</h1>
<div class="content"> <div class="content">
{{ .Content }} {{ .Content }}
@ -18,6 +21,12 @@
{{ end }} {{ end }}
</ul> </ul>
</div> </div>
</div>
<div class="column">
<div class="graph-network" data-name="{{ .Name }}" style="height:80vh; top:0; position: sticky"></div>
</div>
</div>
{{ end }} {{ end }}
{{ define "navbar" }} {{ define "navbar" }}
@ -34,12 +43,13 @@
{{ end }} {{ end }}
{{ define "content_head" }} {{ define "content_head" }}
<style> <style>
.edit { .edit {
color: red; color: red;
} }
.tag { .tag {
color: #444; color: #444;
} }
</style> </style>
{{ end }} {{ end }}

View File

@ -34,6 +34,7 @@ func RandStringBytes(n int) string {
func ParseLinks(blockId string, content string) ([]ParsedLink, error) { func ParseLinks(blockId string, content string) ([]ParsedLink, error) {
hrefRE := regexp.MustCompile(`(#?\[\[\s*([^\]]+)\s*\]\])`) hrefRE := regexp.MustCompile(`(#?\[\[\s*([^\]]+)\s*\]\])`)
keywordsRE := regexp.MustCompile(`(\w+)::`)
scanner := bufio.NewScanner(strings.NewReader(content)) scanner := bufio.NewScanner(strings.NewReader(content))
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
@ -59,6 +60,13 @@ func ParseLinks(blockId string, content string) ([]ParsedLink, error) {
l := cleanNameURL(link) l := cleanNameURL(link)
result = append(result, ParsedLink{blockId, link, l, line}) result = append(result, ParsedLink{blockId, link, l, line})
} }
keywords := keywordsRE.FindAllStringSubmatch(line, -1)
for _, matches := range keywords {
link := matches[1]
l := cleanNameURL(link)
result = append(result, ParsedLink{blockId, link, l, line})
}
} }
return result, nil return result, nil