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")
links := renderLinks(line)
pageText := renderMarkdown2(links)
pageText := renderMarkdown2(renderLinks(line, false))
editPageText := renderMarkdown2(renderLinks(line, true))
removeBrackets := func(r rune) rune {
if r == '[' || r == ']' || r == '*' {
@ -134,6 +134,7 @@ func getBackrefs(fp *FilePages, p string) (map[string][]Backref, error) {
Name: ref.Name,
Title: title,
LineHTML: template.HTML(pageText),
LineEditHTML: template.HTML(editPageText),
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 menu from './menu.js'
import './styles.scss'
import wikiGraph from './graph'
moment.locale('nl')
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-dot')
$(function () {
wikiGraph('.graph-network')
})
function isMultiline(input) {
return input.value.startsWith("```", 0)
}

View File

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

View File

@ -8,10 +8,11 @@ import (
)
type graphBuilder struct {
refs Refs
nodeMap NodeMap
nodes []Node
edges []Edge
refs Refs
nodeMap NodeMap
nodeCount int
nodes []Node
edges []Edge
}
type NodeMap map[string]int
@ -31,17 +32,23 @@ func NewGraphBuilder(mp PagesRepository) (*graphBuilder, error) {
return nil, err
}
nodeMap := prepareNodeMap(refs)
return &graphBuilder{
refs: refs,
nodeMap: nodeMap,
nodeMap: make(NodeMap),
nodes: nil,
edges: nil,
}, nil
}
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)
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 {
var edges []Edge
edgeSet := make(map[Edge]bool)
@ -80,33 +107,32 @@ func prepareEdges(refs Refs, nodeMap NodeMap) []Edge {
return edges
}
func prepareNodes(nodeMap NodeMap) []Node {
func prepareNodes(nodeMap NodeMap, apply func(node Node) Node) []Node {
var nodes []Node
for name, id := range nodeMap {
nodes = append(nodes, Node{
nodes = append(nodes, apply(Node{
Id: id,
Label: name,
})
Color: nil,
}))
}
return nodes
}
func prepareNodeMap(refs Refs) NodeMap {
nodeCount := 1
nodeMap := make(NodeMap)
for key, references := range refs {
if _, e := nodeMap[key]; !e {
nodeMap[key] = nodeCount
nodeCount += 1
}
func (gb *graphBuilder) prepareNodeMap() {
for key, references := range gb.refs {
gb.addNode(key)
for _, item := range references {
if _, e := nodeMap[item.Name]; !e {
nodeMap[item.Name] = nodeCount
nodeCount += 1
}
gb.addNode(item.Name)
}
}
return nodeMap
}
func (gb *graphBuilder) addNode(key string) int {
if _, e := gb.nodeMap[key]; !e {
gb.nodeMap[key] = gb.nodeCount
gb.nodeCount += 1
}
return gb.nodeMap[key]
}

79
main.go
View File

@ -35,10 +35,11 @@ var (
)
type Backref struct {
Name string
Title string
LineHTML template.HTML
Line string
Name string
Title string
LineHTML template.HTML
LineEditHTML template.HTML
Line string
}
// Page
@ -100,8 +101,9 @@ type indexPage struct {
}
type Node struct {
Id int `json:"id"`
Label string `json:"label"`
Id int `json:"id"`
Label string `json:"label"`
Color *string `json:"color"`
}
type Edge struct {
@ -111,11 +113,11 @@ type Edge struct {
type graphPage struct {
pageBaseInfo
Session *Session
Title string
Name string
Nodes template.JS
Edges template.JS
Session *Session
Title string
Name string
Nodes template.JS
Edges template.JS
}
type editPage struct {
@ -483,6 +485,8 @@ func (h *graphHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
gb.prepareNodeMap()
gb.RemoveNode("DONE")
gb.RemoveNode("TODO")
gb.RemoveNode("Home")
@ -631,7 +635,7 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
pageText = metaKV.ReplaceAllString(pageText, "**[[$1]]**: $2")
pageText = renderLinks(pageText)
pageText = renderLinks(pageText, false)
pageText = renderMarkdown2(pageText)
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*\]\]`)
if err != nil {
log.Fatal(err)
@ -682,7 +686,12 @@ func renderLinks(pageText string) string {
if tag {
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
}
@ -791,6 +800,48 @@ func main() {
w.Header().Set("Content-Type", "application/json")
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("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("./dist"))))
http.Handle("/save/", &saveHandler{})

View File

@ -1,31 +1,39 @@
{{ define "navbar" }}
<a href="/{{ .Name }}" class="navbar-item">Back</a>
<a href="/{{ .Name }}" class="navbar-item">Back</a>
{{ end }}
{{define "content"}}
<h1 class="title">{{ .Title }}</h1>
<form action="/save/" method="post">
<input type="hidden" name="p" value="{{ .Name }}" />
{{ .Editor }}
</form>
<div class="columns">
<div class="column">
<h1 class="title">{{ .Title }}</h1>
<form action="/save/" method="post">
<input type="hidden" name="p" value="{{ .Name }}"/>
{{ .Editor }}
</form>
{{ if .Backrefs }}
<div class="backrefs content">
<h3>Linked references</h3>
<ul>
{{ range $name, $refs := .Backrefs }}
<li><a href="/{{ $name }}">{{ (index $refs 0).Title }}</a>
{{ if .Backrefs }}
<div class="backrefs content">
<h3>Linked references</h3>
<ul>
{{ range $ref := $refs }}
<li>{{ $ref.LineHTML }}</li>
{{ range $name, $refs := .Backrefs }}
<li><a href="/edit/{{ $name }}">{{ (index $refs 0).Title }}</a>
<ul>
{{ range $ref := $refs }}
<li>{{ $ref.LineEditHTML }}</li>
{{ end }}
</ul>
</li>
{{ end }}
</ul>
</li>
</div>
{{ end }}
</ul>
</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" }}
{{ end }}

View File

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

View File

@ -1,22 +1,31 @@
{{ define "content" }}
<h1 class="title is-1">{{ .Title }}</h1>
<div class="content">
{{ .Content }}
</div>
<div class="columns">
<div class="column">
<div class="backrefs content">
<h3>Linked references</h3>
<ul>
{{ range $name, $refs := .Backrefs }}
<li><a href="/{{ $name }}">{{ (index $refs 0).Title }}</a>
<ul>
{{ range $ref := $refs }}
<li>{{ $ref.LineHTML }}</li>
{{ end }}
</ul>
</li>
{{ end }}
</ul>
<h1 class="title is-1">{{ .Title }}</h1>
<div class="content">
{{ .Content }}
</div>
<div class="backrefs content">
<h3>Linked references</h3>
<ul>
{{ range $name, $refs := .Backrefs }}
<li><a href="/{{ $name }}">{{ (index $refs 0).Title }}</a>
<ul>
{{ range $ref := $refs }}
<li>{{ $ref.LineHTML }}</li>
{{ end }}
</ul>
</li>
{{ end }}
</ul>
</div>
</div>
<div class="column">
<div class="graph-network" data-name="{{ .Name }}" style="height:80vh; top:0; position: sticky"></div>
</div>
</div>
{{ end }}
@ -34,12 +43,13 @@
{{ end }}
{{ define "content_head" }}
<style>
.edit {
color: red;
}
.tag {
color: #444;
}
</style>
<style>
.edit {
color: red;
}
.tag {
color: #444;
}
</style>
{{ end }}

View File

@ -34,6 +34,7 @@ func RandStringBytes(n int) string {
func ParseLinks(blockId string, content string) ([]ParsedLink, error) {
hrefRE := regexp.MustCompile(`(#?\[\[\s*([^\]]+)\s*\]\])`)
keywordsRE := regexp.MustCompile(`(\w+)::`)
scanner := bufio.NewScanner(strings.NewReader(content))
scanner.Split(bufio.ScanLines)
@ -59,6 +60,13 @@ func ParseLinks(blockId string, content string) ([]ParsedLink, error) {
l := cleanNameURL(link)
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