diff --git a/backref.go b/backref.go
index 3645e20..ce8b06a 100644
--- a/backref.go
+++ b/backref.go
@@ -91,10 +91,18 @@ func loadBackrefs(fp *FilePages, p string) (map[string][]Backref, error) {
links := renderLinks(strings.TrimLeft(ref.Link.Line, " *"))
pageText := renderMarkdown2(links)
+ removeBrackets := func(r rune) rune {
+ if r == '[' || r == ']' || r == '*' {
+ return -1
+ }
+ return r
+ }
+
result[ref.Name] = append(result[ref.Name], Backref{
- Name: ref.Name,
- Title: title,
- Line: template.HTML(pageText),
+ Name: ref.Name,
+ Title: title,
+ LineHTML: template.HTML(pageText),
+ Line: strings.Map(removeBrackets, ref.Link.Line),
})
}
diff --git a/editor/package-lock.json b/editor/package-lock.json
index 6e60b87..b3aa1db 100644
--- a/editor/package-lock.json
+++ b/editor/package-lock.json
@@ -1093,6 +1093,14 @@
"del": "^4.1.1"
}
},
+ "cli": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/cli/-/cli-0.4.3.tgz",
+ "integrity": "sha1-5oGcjV+qlX9k+Y9mqFBiaMHR8X0=",
+ "requires": {
+ "glob": ">= 3.1.4"
+ }
+ },
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
@@ -1141,6 +1149,11 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
+ "colors": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
+ "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
+ },
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -2935,6 +2948,24 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
+ "fuse": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/fuse/-/fuse-0.4.0.tgz",
+ "integrity": "sha1-LDjq+IirsKm6eWDP4zOdHz9T9uY=",
+ "requires": {
+ "colors": ">=0.6.x",
+ "fuse.js": "^6.0.0",
+ "jshint": "0.9.x",
+ "optimist": ">=0.3.5",
+ "uglify-js": ">=2.2.x",
+ "underscore": ">=1.4.x"
+ }
+ },
+ "fuse.js": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.0.0.tgz",
+ "integrity": "sha512-e5Ap6mhF/WQ9bKqsMFTTR5/DS9qbYab4VXHtMdxCanH+VZkdUV2LqcgMO31etSQv53NXsguQF1bdqkrrPAM2HQ=="
+ },
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
@@ -3857,6 +3888,30 @@
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
},
+ "jshint": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/jshint/-/jshint-0.9.1.tgz",
+ "integrity": "sha1-/zLsfwn4QAH3SY7q/WPJ5Puy3A4=",
+ "requires": {
+ "cli": "0.4.3",
+ "minimatch": "0.0.x"
+ },
+ "dependencies": {
+ "lru-cache": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-1.0.6.tgz",
+ "integrity": "sha1-qlD5cEdCKsclQ72hd6nJ0BjZhFI="
+ },
+ "minimatch": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.0.5.tgz",
+ "integrity": "sha1-lrtJC707poNrv6wRGt91MBsVhN4=",
+ "requires": {
+ "lru-cache": "~1.0.2"
+ }
+ }
+ }
+ },
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@@ -4744,6 +4799,22 @@
"is-wsl": "^1.1.0"
}
},
+ "optimist": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+ "requires": {
+ "minimist": "~0.0.1",
+ "wordwrap": "~0.0.2"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
+ "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
+ }
+ }
+ },
"original": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz",
@@ -6892,7 +6963,6 @@
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz",
"integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==",
- "dev": true,
"requires": {
"commander": "~2.19.0",
"source-map": "~0.6.1"
@@ -6901,17 +6971,20 @@
"commander": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
- "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==",
- "dev": true
+ "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg=="
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
+ "underscore": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz",
+ "integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg=="
+ },
"union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@@ -7538,15 +7611,20 @@
}
},
"wiki-list-editor": {
- "version": "0.6.4",
- "resolved": "https://registry.npmjs.org/wiki-list-editor/-/wiki-list-editor-0.6.4.tgz",
- "integrity": "sha512-UkCrS62HR6/Slk2jggxQZryoe3YEbBPrceJ76sl6a4YPni/xWCajLGl/qLsUemQMwyoVI7ApAmLE9GvJeLLPyQ==",
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/wiki-list-editor/-/wiki-list-editor-0.6.5.tgz",
+ "integrity": "sha512-1/XQ7JSJoet/TCq5RiF0JAHFZYveI3fEW8drc6gPnAfwz4e2t7vHBZV5sDhFO+z4fD+uISfsBveW4oJzVNteNA==",
"requires": {
"dragula": "^3.7.2",
"jquery": "^3.5.1",
"lodash": "^4.17.15"
}
},
+ "wordwrap": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+ "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc="
+ },
"worker-farm": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz",
diff --git a/editor/package.json b/editor/package.json
index acd707a..fa9e9f0 100644
--- a/editor/package.json
+++ b/editor/package.json
@@ -12,13 +12,15 @@
"bulma": "^0.7.5",
"css-loader": "^3.2.0",
"file-loader": "^6.0.0",
+ "fuse": "^0.4.0",
+ "fuse.js": "^6.0.0",
"jquery-contextmenu": "^2.9.2",
"lunr": "^2.3.8",
"mustache": "^4.0.1",
"node-sass": "^4.14.1",
"sass-loader": "^7.3.1",
"style-loader": "^1.0.0",
- "wiki-list-editor": "^0.6.4"
+ "wiki-list-editor": "^0.6.5"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
diff --git a/editor/src/caret-position.js b/editor/src/caret-position.js
new file mode 100644
index 0000000..e39cfc3
--- /dev/null
+++ b/editor/src/caret-position.js
@@ -0,0 +1,145 @@
+// We'll copy the properties below into the mirror div.
+// Note that some browsers, such as Firefox, do not concatenate properties
+// into their shorthand (e.g. padding-top, padding-bottom etc. -> padding),
+// so we have to list every single property explicitly.
+var properties = [
+ 'direction', // RTL support
+ 'boxSizing',
+ 'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
+ 'height',
+ 'overflowX',
+ 'overflowY', // copy the scrollbar for IE
+
+ 'borderTopWidth',
+ 'borderRightWidth',
+ 'borderBottomWidth',
+ 'borderLeftWidth',
+ 'borderStyle',
+
+ 'paddingTop',
+ 'paddingRight',
+ 'paddingBottom',
+ 'paddingLeft',
+
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/font
+ 'fontStyle',
+ 'fontVariant',
+ 'fontWeight',
+ 'fontStretch',
+ 'fontSize',
+ 'fontSizeAdjust',
+ 'lineHeight',
+ 'fontFamily',
+
+ 'textAlign',
+ 'textTransform',
+ 'textIndent',
+ 'textDecoration', // might not make a difference, but better be safe
+
+ 'letterSpacing',
+ 'wordSpacing',
+
+ 'tabSize',
+ 'MozTabSize'
+
+];
+
+var isBrowser = (typeof window !== 'undefined');
+var isFirefox = (isBrowser && window.mozInnerScreenX != null);
+
+function getCaretCoordinates(element, position, options) {
+ if (!isBrowser) {
+ throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
+ }
+
+ var debug = options && options.debug || false;
+ if (debug) {
+ var el = document.querySelector('#input-textarea-caret-position-mirror-div');
+ if (el) el.parentNode.removeChild(el);
+ }
+
+ // The mirror div will replicate the textarea's style
+ var div = document.createElement('div');
+ div.id = 'input-textarea-caret-position-mirror-div';
+ document.body.appendChild(div);
+
+ var style = div.style;
+ var computed = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9
+ var isInput = element.nodeName === 'INPUT';
+
+ // Default textarea styles
+ style.whiteSpace = 'pre-wrap';
+ if (!isInput)
+ style.wordWrap = 'break-word'; // only for textarea-s
+
+ // Position off-screen
+ style.position = 'absolute'; // required to return coordinates properly
+ if (!debug)
+ style.visibility = 'hidden'; // not 'display: none' because we want rendering
+
+ // Transfer the element's properties to the div
+ properties.forEach(function (prop) {
+ if (isInput && prop === 'lineHeight') {
+ // Special case for s because text is rendered centered and line height may be != height
+ if (computed.boxSizing === "border-box") {
+ var height = parseInt(computed.height);
+ var outerHeight =
+ parseInt(computed.paddingTop) +
+ parseInt(computed.paddingBottom) +
+ parseInt(computed.borderTopWidth) +
+ parseInt(computed.borderBottomWidth);
+ var targetHeight = outerHeight + parseInt(computed.lineHeight);
+ if (height > targetHeight) {
+ style.lineHeight = height - outerHeight + "px";
+ } else if (height === targetHeight) {
+ style.lineHeight = computed.lineHeight;
+ } else {
+ style.lineHeight = 0;
+ }
+ } else {
+ style.lineHeight = computed.height;
+ }
+ } else {
+ style[prop] = computed[prop];
+ }
+ });
+
+ if (isFirefox) {
+ // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
+ if (element.scrollHeight > parseInt(computed.height))
+ style.overflowY = 'scroll';
+ } else {
+ style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
+ }
+
+ div.textContent = element.value.substring(0, position);
+ // The second special handling for input type="text" vs textarea:
+ // spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
+ if (isInput)
+ div.textContent = div.textContent.replace(/\s/g, '\u00a0');
+
+ var span = document.createElement('span');
+ // Wrapping must be replicated *exactly*, including when a long word gets
+ // onto the next line, with whitespace at the end of the line before (#7).
+ // The *only* reliable way to do that is to copy the *entire* rest of the
+ // textarea's content into the created at the caret position.
+ // For inputs, just '.' would be enough, but no need to bother.
+ span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all
+ div.appendChild(span);
+
+ var coordinates = {
+ top: span.offsetTop + parseInt(computed['borderTopWidth']),
+ left: span.offsetLeft + parseInt(computed['borderLeftWidth']),
+ height: parseInt(computed['lineHeight'])
+ };
+
+ if (debug) {
+ span.style.backgroundColor = '#aaa';
+ } else {
+ document.body.removeChild(div);
+ }
+
+ return coordinates;
+}
+
+export default getCaretCoordinates;
diff --git a/editor/src/fuse.js b/editor/src/fuse.js
new file mode 100644
index 0000000..c705b38
--- /dev/null
+++ b/editor/src/fuse.js
@@ -0,0 +1,20 @@
+import Fuse from 'fuse.js'
+import $ from 'jquery'
+
+function createTitleSearch() {
+ return new Promise(function (resolve, reject) {
+ $.get('/links.json', function (documents) {
+ const options = {
+ keys: ['title'],
+ }
+ let fuse = new Fuse(documents, options)
+
+ resolve({
+ documents,
+ search: fuse,
+ })
+ })
+ })
+}
+
+export default createTitleSearch
diff --git a/editor/src/index.js b/editor/src/index.js
index ac2979e..d14354f 100644
--- a/editor/src/index.js
+++ b/editor/src/index.js
@@ -3,8 +3,10 @@ import axios from 'axios';
import qs from 'querystring'
import $ from 'jquery';
import search from './search';
+import createPageSearch from './fuse';
import Mustache from 'mustache';
import 'jquery-contextmenu';
+import getCaretCoordinates from './caret-position'
import './styles.scss';
import '../node_modules/jquery-contextmenu/dist/jquery.contextMenu.css';
@@ -75,6 +77,110 @@ if (holder) {
).save()
})
+ $(document).on('keydown', '#link-complete', function (event) {
+ if (!$('#link-complete:visible')) {
+ return true;
+ }
+ const $popup = $('#link-complete')
+ if (event.key === 'Escape') {
+ $popup.fadeOut()
+ return false;
+ }
+
+ if (event.key === 'Enter') {
+ const linkName = $popup.find('li.selected').text()
+ $popup.trigger('popup:selected', [linkName])
+ $popup.fadeOut()
+ return false
+ }
+
+ if (event.key === 'ArrowUp') {
+ const selected = $popup.find('li.selected')
+ const prev = selected.prev('li')
+ if (prev.length) {
+ prev.addClass('selected')
+ selected.removeClass('selected')
+ }
+ return false;
+ }
+
+ if (event.key === 'ArrowDown') {
+ const selected = $popup.find('li.selected')
+ const next = selected.next('li');
+ if (next.length) {
+ next.addClass('selected')
+ selected.removeClass('selected')
+ }
+ return false
+ }
+
+ return true
+ })
+
+ createPageSearch().then(function ({search}) {
+ editor.on('start-editing', function (input) {
+ const $lc = $('#link-complete')
+
+ $lc.on('popup:selected', function (event, linkName) {
+ let value = input.value
+ let end = input.selectionEnd
+ let start = value.lastIndexOf("[[", end)
+ end += 2
+ let startAndLink = value.substring(0, start) + "[[" + linkName + "]]"
+ input.value = startAndLink + value.substring(end)
+ input.selectionStart = startAndLink.length
+ input.selectionEnd = startAndLink.length
+ input.focus()
+ return true
+ })
+
+ $(input).on('keydown', function (event) {
+ const isVisible = $('#link-complete:visible').length > 0;
+
+ const $lc = $('#link-complete')
+ if (event.key === 'Escape' && isVisible) {
+ $lc.fadeOut()
+ return true
+ }
+
+ if (event.key === 'ArrowDown' && isVisible) {
+ $lc.focus()
+ $lc.find('li:first-child').addClass('selected')
+ return false
+ }
+ return true
+ })
+
+ $(input).on('keyup', function () {
+ let value = input.value
+ let end = input.selectionEnd
+ let start = value.lastIndexOf("[[", end)
+ let linkEnd = value.lastIndexOf("]]", end - 1)
+ if (start < linkEnd) {
+ return
+ }
+ let query = value.substring(start, end);
+ let results = search.search(query)
+
+ let pos = getCaretCoordinates(input, value.selectionEnd, {})
+ let off = $(input).offset()
+ pos.top += off.top + pos.height
+ pos.left += off.left
+
+ const $lc = $('#link-complete')
+ $lc.offset(pos)
+
+ var template = document.getElementById('link-template').innerHTML;
+ var rendered = Mustache.render(template, {results: results}, {}, ['[[', ']]']);
+ $lc.html(rendered).fadeIn()
+ })
+ })
+
+ editor.on('stop-editing', function (input) {
+ $('#link-complete').off()
+ })
+ })
+
$.contextMenu({
selector: '.marker',
items: {
diff --git a/main.go b/main.go
index c6ac062..86b2497 100644
--- a/main.go
+++ b/main.go
@@ -30,9 +30,10 @@ var (
)
type Backref struct {
- Name string
- Title string
- Line template.HTML
+ Name string
+ Title string
+ LineHTML template.HTML
+ Line string
}
// Page
@@ -618,6 +619,27 @@ func main() {
mp = NewFilePages("data")
http.Handle("/auth/", &authHandler{})
+ http.HandleFunc("/links.json", func(w http.ResponseWriter, r *http.Request) {
+ type Document struct {
+ Title string `json:"title"`
+ }
+
+ var results []Document
+ pages, err := mp.(*FilePages).AllPages()
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ for _, page := range pages {
+ results = append(results, Document{page.Title})
+ }
+ w.Header().Set("Content-Type", "application/json")
+ err = json.NewEncoder(w).Encode(&results)
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ })
http.HandleFunc("/documents.json", func(w http.ResponseWriter, r *http.Request) {
type Document struct {
Title string `json:"title"`
@@ -632,26 +654,36 @@ func main() {
return
}
for _, page := range pages {
- content := page.Content
+ content := strings.Builder{}
var listItems []struct {
Indented int
Text string
}
- err = json.NewDecoder(strings.NewReader(content)).Decode(&listItems)
+ err = json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItems)
if err == nil {
- pageText := ""
for _, item := range listItems {
- pageText += strings.Repeat(" ", item.Indented) + "* " + item.Text + "\n"
+ content.WriteString(item.Text)
+ content.WriteByte(' ')
}
+ } else {
+ content.WriteString(page.Content)
+ content.WriteByte(' ')
+ }
- content = pageText
+ for page, refs := range page.Refs {
+ content.WriteString(page)
+ content.WriteByte(' ')
+ for _, ref := range refs {
+ content.WriteString(ref.Line)
+ content.WriteByte(' ')
+ }
}
results = append(results, Document{
Title: page.Title,
- Body: content,
+ Body: content.String(),
URL: page.Name,
})
}
diff --git a/templates/edit.html b/templates/edit.html
index 388bd8b..3592d3b 100644
--- a/templates/edit.html
+++ b/templates/edit.html
@@ -17,7 +17,7 @@
{{ (index $refs 0).Title }}
{{ range $ref := $refs }}
- - {{ $ref.Line }}
+ - {{ $ref.LineHTML }}
{{ end }}
diff --git a/templates/layout.html b/templates/layout.html
index f2bc2a9..92ba687 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -25,6 +25,24 @@
#autocomplete li > a {
white-space: nowrap;
}
+ #link-complete {
+ z-index: 1;
+ width: 217px;
+ overflow-x: hidden;
+ overflow-y: auto;
+ height: 300px;
+ position: absolute;
+ background: white;
+ border: 1px solid #ccc;
+ outline: none;
+ }
+
+ #link-complete li {
+ padding: 4px 16px;
+ }
+ #link-complete li.selected {
+ background: lightblue;
+ }
.monospace {
font-family: "Fira Code Retina", monospace;
@@ -142,6 +160,9 @@
border-bottom-color: #ccc;
}
+ .hide {
+ display: none;
+ }
.selected {
background: lightblue;
@@ -249,12 +270,13 @@
— created by Peter Stuifzand
-
+
+
{{ block "footer_scripts" . }}
{{ end }}
-
+
+
+
+ [[#results]]
+ - [[item.title]]
+ [[/results]]
+
+