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")
|
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
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 '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)
|
||||||
}
|
}
|
||||||
|
|
6
file.go
6
file.go
|
@ -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
|
||||||
|
|
64
graph.go
64
graph.go
|
@ -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
57
main.go
|
@ -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{})
|
||||||
|
|
|
@ -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" }}
|
||||||
|
|
|
@ -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="/">
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
8
util.go
8
util.go
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user