Show graph on edit page for neighbors two edges out
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
005be8c733
commit
5bf14bd1d4
|
@ -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
59
editor/src/graph.js
Normal 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
|
|
@ -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)
|
||||
}
|
||||
|
|
6
file.go
6
file.go
|
@ -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
|
||||
|
|
76
graph.go
76
graph.go
|
@ -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
79
main.go
|
@ -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{})
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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="/">
|
||||
|
|
|
@ -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 }}
|
||||
|
|
8
util.go
8
util.go
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user