This commit is contained in:
parent
c4bd5107eb
commit
5cc4e65638
|
@ -16,13 +16,6 @@ type Reference struct {
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListItem is a simplification of the information that was saved by the editor
|
|
||||||
type ListItem struct {
|
|
||||||
ID string
|
|
||||||
Indented int
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Refs map[string][]Reference
|
type Refs map[string][]Reference
|
||||||
|
|
||||||
func processBackrefs(dirname string, page Page) error {
|
func processBackrefs(dirname string, page Page) error {
|
||||||
|
|
434
editor/src/editor.js
Normal file
434
editor/src/editor.js
Normal file
|
@ -0,0 +1,434 @@
|
||||||
|
import listEditor from "wiki-list-editor";
|
||||||
|
import menu from "./menu";
|
||||||
|
import createPageSearch from "./fuse";
|
||||||
|
import util from "./util";
|
||||||
|
import search from "./search";
|
||||||
|
import axios from 'axios';
|
||||||
|
import qs from 'querystring'
|
||||||
|
import $ from 'jquery';
|
||||||
|
import Mustache from 'mustache';
|
||||||
|
import getCaretCoordinates from './caret-position'
|
||||||
|
import moment from 'moment'
|
||||||
|
import mermaid from 'mermaid'
|
||||||
|
import {Network, parseDOTNetwork} from "vis-network/peer";
|
||||||
|
import PrismJS from 'prismjs'
|
||||||
|
import 'prismjs/plugins/filter-highlight-all/prism-filter-highlight-all'
|
||||||
|
import 'prismjs/components/prism-php'
|
||||||
|
import 'prismjs/components/prism-go'
|
||||||
|
import 'prismjs/components/prism-perl'
|
||||||
|
import 'prismjs/components/prism-css'
|
||||||
|
import 'prismjs/components/prism-markup-templating'
|
||||||
|
import 'prismjs/components/prism-jq'
|
||||||
|
import MD from './markdown'
|
||||||
|
import he from 'he'
|
||||||
|
|
||||||
|
function isMultiline(input) {
|
||||||
|
return input.value.startsWith("```", 0)
|
||||||
|
|| input.value.startsWith("$$", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSaver(editor, saveUrl, page, beforeSave) {
|
||||||
|
return {
|
||||||
|
save() {
|
||||||
|
return editor.save().then(outputData => {
|
||||||
|
beforeSave()
|
||||||
|
let data = {
|
||||||
|
'json': 1,
|
||||||
|
'p': page,
|
||||||
|
'summary': "",
|
||||||
|
'content': JSON.stringify(outputData),
|
||||||
|
};
|
||||||
|
return axios.post(saveUrl, qs.encode(data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Indicator(element, timeout) {
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
done() {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
element.classList.add('hide')
|
||||||
|
}, timeout * 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
setText(text) {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
}
|
||||||
|
element.innerText = text;
|
||||||
|
element.classList.remove('hide')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addIndicator(editor, indicator) {
|
||||||
|
return {
|
||||||
|
save() {
|
||||||
|
editor.save().then(() => {
|
||||||
|
indicator.setText('saved!');
|
||||||
|
indicator.done();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSearchResults(searchTool, query, input, value, resultType) {
|
||||||
|
return showSearchResultsExtended('#link-complete', 'link-template', searchTool, query, input, value, resultType, {
|
||||||
|
showOnlyResults: true,
|
||||||
|
belowCursor: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSearchResultsExtended(element, template, searchTool, query, input, value, resultType, options) {
|
||||||
|
const $lc = $(element)
|
||||||
|
return searchTool(query).then(results => {
|
||||||
|
let opt = options || {};
|
||||||
|
if (opt.showOnlyResults && (query.length === 0 || !results.length)) {
|
||||||
|
$lc.fadeOut()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$lc.data('result-type', resultType)
|
||||||
|
|
||||||
|
if (opt.belowCursor) {
|
||||||
|
let pos = getCaretCoordinates(input, value.selectionEnd, {})
|
||||||
|
let off = $(input).offset()
|
||||||
|
pos.top += off.top + pos.height
|
||||||
|
pos.left += off.left
|
||||||
|
$lc.offset(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
var templateText = he.decode(document.getElementById(template).innerHTML);
|
||||||
|
var rendered = Mustache.render(templateText, {
|
||||||
|
page: value.trim().replace(/\s+/g, '_'),
|
||||||
|
results: results
|
||||||
|
}, {}, ['[[', ']]']);
|
||||||
|
let selected = $lc.find('li.selected');
|
||||||
|
if (selected) {
|
||||||
|
let selectedPos = $lc.find('li').index(selected[0])
|
||||||
|
rendered = $(rendered)
|
||||||
|
const $lis = $lc.find('li')
|
||||||
|
if ($lis.length >= 1) {
|
||||||
|
selectedPos = Math.min(selectedPos, $lis.length - 1)
|
||||||
|
rendered.find('li')[selectedPos].classList.add('selected')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$lc.html(rendered).fadeIn()
|
||||||
|
return results
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGraphs() {
|
||||||
|
$('code.language-dot').each(function (i, code) {
|
||||||
|
if (!code.innerText) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = parseDOTNetwork(code.innerText)
|
||||||
|
let network = new Network(code, data, {
|
||||||
|
layout: {
|
||||||
|
randomSeed: 1239043
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$(code).on('click', function () {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Editor(holder, input) {
|
||||||
|
let scope = {}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
transform(text, element) {
|
||||||
|
if (text === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let converted = text
|
||||||
|
|
||||||
|
if (converted.startsWith("```", 0) || converted.startsWith("$$", 0)) {
|
||||||
|
converted = MD.render(converted)
|
||||||
|
// } else if (converted.startsWith("=", 0)) {
|
||||||
|
// try {
|
||||||
|
// converted = math.evaluate(converted.substring(1), scope).toString()
|
||||||
|
// } catch (e) {
|
||||||
|
// converted = converted + ' <span style="background: red; color: white;">' + e.message + '</span>';
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
if (text.match(/^(\w+):: (.+)$/)) {
|
||||||
|
converted = converted.replace(/^(\w+):: (.*)$/, '**[[$1]]**: $2')
|
||||||
|
} else if (text.match(/#\[\[TODO]]/)) {
|
||||||
|
converted = converted.replace('#[[TODO]]', '<input class="checkbox" type="checkbox" />')
|
||||||
|
} else if (text.match(/#\[\[DONE]]/)) {
|
||||||
|
converted = converted.replace('#[[DONE]]', '<input class="checkbox" type="checkbox" checked />')
|
||||||
|
}
|
||||||
|
MD.options.html = true
|
||||||
|
converted = MD.renderInline(converted)
|
||||||
|
MD.options.html = false
|
||||||
|
}
|
||||||
|
|
||||||
|
element.html(converted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputData = input ? input : JSON.parse(holder.dataset.input)
|
||||||
|
|
||||||
|
let editor = listEditor(holder, inputData, options);
|
||||||
|
holder.$listEditor = editor
|
||||||
|
|
||||||
|
$(holder).find('.content input[type="checkbox"]').on('click', function (event) {
|
||||||
|
let that = this
|
||||||
|
let id = $(this).closest('.list-item').data('id')
|
||||||
|
editor.update(id, function (item, prev, next) {
|
||||||
|
if (that.checked) {
|
||||||
|
item.text = item.text.replace('#[[TODO]]', '#[[DONE]]')
|
||||||
|
} else {
|
||||||
|
item.text = item.text.replace('#[[DONE]]', '#[[TODO]]')
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
});
|
||||||
|
event.stopPropagation()
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.on('change', function () {
|
||||||
|
let element = holder
|
||||||
|
let indicator = Indicator(document.getElementById('save-indicator'), 2);
|
||||||
|
let saveUrl = element.dataset.saveurl;
|
||||||
|
let page = element.dataset.page;
|
||||||
|
|
||||||
|
indicator.setText('has changes...');
|
||||||
|
addIndicator(
|
||||||
|
addSaver(editor, saveUrl, page, () => indicator.setText('saving...')),
|
||||||
|
indicator
|
||||||
|
).save()
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.on('rendered', function () {
|
||||||
|
PrismJS.highlightAll()
|
||||||
|
mermaid.init()
|
||||||
|
renderGraphs();
|
||||||
|
})
|
||||||
|
|
||||||
|
menu.connectContextMenu(editor)
|
||||||
|
|
||||||
|
return createPageSearch().then(function ({titleSearch, commandSearch, commands}) {
|
||||||
|
editor.on('start-editing', function (input) {
|
||||||
|
const $lc = $('#link-complete');
|
||||||
|
|
||||||
|
$(input).parents('.list-item').addClass('active');
|
||||||
|
|
||||||
|
$lc.on('popup:selected', function (event, linkName, resultType, element) {
|
||||||
|
let value = input.value
|
||||||
|
let end = input.selectionEnd
|
||||||
|
if (resultType === 'link') {
|
||||||
|
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()
|
||||||
|
} else if (resultType === 'command') {
|
||||||
|
let start = value.lastIndexOf("/", end)
|
||||||
|
let commandResult = ""
|
||||||
|
let replace = ""
|
||||||
|
let prefix = false
|
||||||
|
let adjustment = 0
|
||||||
|
|
||||||
|
let now = moment()
|
||||||
|
|
||||||
|
if (linkName === "Current Time") {
|
||||||
|
commandResult = now.format('HH:mm')
|
||||||
|
} else if (linkName === "Today") {
|
||||||
|
commandResult = "[[" + now.format('D MMMM YYYY') + "]]"
|
||||||
|
} else if (linkName === "Tomorrow") {
|
||||||
|
commandResult = "[[" + now.add(1, 'day').format('D MMMM YYYY') + "]]"
|
||||||
|
} else if (linkName === "Yesterday") {
|
||||||
|
commandResult = "[[" + now.add(-1, 'day').format('D MMMM YYYY') + "]]"
|
||||||
|
} else if (linkName === "TODO") {
|
||||||
|
commandResult = "#[[TODO]] "
|
||||||
|
replace = "#[[DONE]] "
|
||||||
|
prefix = true
|
||||||
|
} else if (linkName === "DONE") {
|
||||||
|
commandResult = "#[[DONE]] "
|
||||||
|
replace = "#[[TODO]] "
|
||||||
|
prefix = true
|
||||||
|
} else if (linkName === "Page Reference") {
|
||||||
|
commandResult = "[[]]"
|
||||||
|
adjustment = -2
|
||||||
|
} else if (linkName === "Code Block") {
|
||||||
|
commandResult = "```\n\n```"
|
||||||
|
adjustment = -5
|
||||||
|
}
|
||||||
|
|
||||||
|
let startAndLink = prefix
|
||||||
|
? commandResult + value.substring(0, start).replace(replace, "")
|
||||||
|
: value.substring(0, start) + commandResult
|
||||||
|
|
||||||
|
input.value = startAndLink + value.substring(end)
|
||||||
|
|
||||||
|
input.selectionStart = startAndLink.length + adjustment
|
||||||
|
input.selectionEnd = startAndLink.length + adjustment
|
||||||
|
|
||||||
|
input.focus()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
$lc.on('popup:leave', function (event) {
|
||||||
|
input.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
$(input).on('keydown', function (event) {
|
||||||
|
const isVisible = $('#link-complete:visible').length > 0;
|
||||||
|
|
||||||
|
if (event.key === 'Escape' && isVisible) {
|
||||||
|
$lc.fadeOut()
|
||||||
|
return false
|
||||||
|
} else if (event.key === 'Enter' && isVisible) {
|
||||||
|
const element = $lc.find('li.selected')
|
||||||
|
const linkName = element.text()
|
||||||
|
$lc.trigger('popup:selected', [linkName, $lc.data('result-type'), element])
|
||||||
|
$lc.fadeOut()
|
||||||
|
return false
|
||||||
|
} else if (event.key === 'ArrowUp' && isVisible) {
|
||||||
|
const selected = $lc.find('li.selected')
|
||||||
|
const prev = selected.prev('li')
|
||||||
|
if (prev.length) {
|
||||||
|
prev.addClass('selected')
|
||||||
|
selected.removeClass('selected')
|
||||||
|
prev[0].scrollIntoView({block: 'center', inline: 'nearest'})
|
||||||
|
} else {
|
||||||
|
// move back from dropdown to input
|
||||||
|
$lc.trigger('popup:leave')
|
||||||
|
selected.removeClass('selected')
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} else if (event.key === 'ArrowDown' && isVisible) {
|
||||||
|
const selected = $lc.find('li.selected')
|
||||||
|
if (!selected.length) {
|
||||||
|
$lc.find('li:not(.selected):first-child').addClass('selected')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = selected.next('li');
|
||||||
|
if (next.length) {
|
||||||
|
next.addClass('selected')
|
||||||
|
selected.removeClass('selected')
|
||||||
|
next[0].scrollIntoView({block: 'center', inline: 'nearest'})
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let mirror = {
|
||||||
|
'[': ']',
|
||||||
|
'(': ')',
|
||||||
|
'{': '}',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMultiline(input) && mirror.hasOwnProperty(event.key)) {
|
||||||
|
let input = this
|
||||||
|
let val = input.value
|
||||||
|
let prefix = val.substring(0, input.selectionStart)
|
||||||
|
let selection = val.substring(input.selectionStart, input.selectionEnd)
|
||||||
|
let suffix = val.substring(input.selectionEnd)
|
||||||
|
input.value = prefix + event.key + selection + mirror[event.key] + suffix
|
||||||
|
input.selectionStart = prefix.length + event.key.length
|
||||||
|
input.selectionEnd = input.selectionStart + selection.length
|
||||||
|
$(input).trigger('input')
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
let searchEnabled = false
|
||||||
|
|
||||||
|
$(input).on('keyup', function (event) {
|
||||||
|
if (event.key === '/') {
|
||||||
|
searchEnabled = true
|
||||||
|
}
|
||||||
|
if (searchEnabled && event.key === 'Escape') {
|
||||||
|
searchEnabled = false
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const ignoreKeys = {
|
||||||
|
'ArrowUp': true,
|
||||||
|
'ArrowDown': true,
|
||||||
|
'Enter': true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key in ignoreKeys) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = input.value
|
||||||
|
let end = input.selectionEnd
|
||||||
|
|
||||||
|
let [start, insideLink] = util.cursorInsideLink(value, end)
|
||||||
|
let insideSearch = false
|
||||||
|
|
||||||
|
if (searchEnabled && !insideLink) {
|
||||||
|
start = value.lastIndexOf("/", end)
|
||||||
|
insideSearch = start >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insideSearch) {
|
||||||
|
let query = value.substring(start + 1, end);
|
||||||
|
showSearchResults(commandSearch, query, input, value, 'command').then(results => {
|
||||||
|
if (query.length > 0 && result.length === 0) {
|
||||||
|
searchEnabled = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} else if (insideLink) {
|
||||||
|
let query = value.substring(start + 2, end);
|
||||||
|
showSearchResults(titleSearch, query, input, value, 'link');
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
$('#link-complete').fadeOut();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
editor.on('stop-editing', function (input) {
|
||||||
|
$(input).parents('.list-item').removeClass('active');
|
||||||
|
$('#link-complete').off()
|
||||||
|
PrismJS.highlightAll()
|
||||||
|
mermaid.init()
|
||||||
|
renderGraphs();
|
||||||
|
})
|
||||||
|
return editor
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeout = null;
|
||||||
|
let searchInput = document.getElementById('search-input');
|
||||||
|
search(searchInput).then(searcher => {
|
||||||
|
let showSearch = _.debounce(function (searcher) {
|
||||||
|
let query = $(searchInput).val()
|
||||||
|
if (query === '') {
|
||||||
|
let autocomplete = document.getElementById('autocomplete');
|
||||||
|
$(autocomplete).hide()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
showSearchResultsExtended('#autocomplete', 'result-template', query => searcher.search(query), query, searchInput, query, 'search-result')
|
||||||
|
return true;
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
$(searchInput).on('keyup', function (event) {
|
||||||
|
showSearch.cancel()
|
||||||
|
showSearch(searcher)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export default Editor;
|
|
@ -1,19 +1,6 @@
|
||||||
import listEditor from 'wiki-list-editor';
|
|
||||||
import MarkdownIt from 'markdown-it';
|
|
||||||
import MarkdownItWikilinks from './wikilinks';
|
|
||||||
import MarkdownItMark from 'markdown-it-mark';
|
|
||||||
import MarkdownItKatex from 'markdown-it-katex';
|
|
||||||
import axios from 'axios';
|
|
||||||
import qs from 'querystring'
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import search from './search';
|
|
||||||
import createPageSearch from './fuse';
|
|
||||||
import util from './util';
|
|
||||||
import Mustache from 'mustache';
|
|
||||||
import getCaretCoordinates from './caret-position'
|
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import mermaid from 'mermaid'
|
import mermaid from 'mermaid'
|
||||||
import {Network, parseDOTNetwork} from "vis-network/peer";
|
|
||||||
import PrismJS from 'prismjs'
|
import PrismJS from 'prismjs'
|
||||||
import 'prismjs/plugins/filter-highlight-all/prism-filter-highlight-all'
|
import 'prismjs/plugins/filter-highlight-all/prism-filter-highlight-all'
|
||||||
import 'prismjs/components/prism-php'
|
import 'prismjs/components/prism-php'
|
||||||
|
@ -22,13 +9,10 @@ import 'prismjs/components/prism-perl'
|
||||||
import 'prismjs/components/prism-css'
|
import 'prismjs/components/prism-css'
|
||||||
import 'prismjs/components/prism-markup-templating'
|
import 'prismjs/components/prism-markup-templating'
|
||||||
import 'prismjs/components/prism-jq'
|
import 'prismjs/components/prism-jq'
|
||||||
import menu from './menu.js'
|
|
||||||
import './styles.scss'
|
import './styles.scss'
|
||||||
import wikiGraph from './graph'
|
import Editor from './editor'
|
||||||
import { create, all } from 'mathjs'
|
import MD from './markdown'
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
const math = create(all)
|
|
||||||
|
|
||||||
|
|
||||||
moment.locale('nl')
|
moment.locale('nl')
|
||||||
mermaid.initialize({startOnLoad: true})
|
mermaid.initialize({startOnLoad: true})
|
||||||
|
@ -40,122 +24,6 @@ PrismJS.plugins.filterHighlightAll.reject.addSelector('.language-dot')
|
||||||
// wikiGraph('.graph-network')
|
// wikiGraph('.graph-network')
|
||||||
// })
|
// })
|
||||||
|
|
||||||
function isMultiline(input) {
|
|
||||||
return input.value.startsWith("```", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSaver(editor, saveUrl, page, beforeSave) {
|
|
||||||
return {
|
|
||||||
save() {
|
|
||||||
return editor.save().then(outputData => {
|
|
||||||
beforeSave()
|
|
||||||
let data = {
|
|
||||||
'json': 1,
|
|
||||||
'p': page,
|
|
||||||
'summary': "",
|
|
||||||
'content': JSON.stringify(outputData),
|
|
||||||
};
|
|
||||||
return axios.post(saveUrl, qs.encode(data))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Indicator(element, timeout) {
|
|
||||||
let timeoutId;
|
|
||||||
|
|
||||||
return {
|
|
||||||
done() {
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
element.classList.add('hide')
|
|
||||||
}, timeout * 1000);
|
|
||||||
},
|
|
||||||
|
|
||||||
setText(text) {
|
|
||||||
if (timeoutId) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
timeoutId = null;
|
|
||||||
}
|
|
||||||
element.innerText = text;
|
|
||||||
element.classList.remove('hide')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addIndicator(editor, indicator) {
|
|
||||||
return {
|
|
||||||
save() {
|
|
||||||
editor.save().then(() => {
|
|
||||||
indicator.setText('saved!');
|
|
||||||
indicator.done();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSearchResults(searchTool, query, input, value, resultType) {
|
|
||||||
return showSearchResultsExtended('#link-complete', 'link-template', searchTool, query, input, value, resultType, {
|
|
||||||
showOnlyResults: true,
|
|
||||||
belowCursor: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSearchResultsExtended(element, template, searchTool, query, input, value, resultType, options) {
|
|
||||||
const $lc = $(element)
|
|
||||||
return searchTool(query).then(results => {
|
|
||||||
let opt = options || {};
|
|
||||||
if (opt.showOnlyResults && (query.length === 0 || !results.length)) {
|
|
||||||
$lc.fadeOut()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$lc.data('result-type', resultType)
|
|
||||||
|
|
||||||
if (opt.belowCursor) {
|
|
||||||
let pos = getCaretCoordinates(input, value.selectionEnd, {})
|
|
||||||
let off = $(input).offset()
|
|
||||||
pos.top += off.top + pos.height
|
|
||||||
pos.left += off.left
|
|
||||||
$lc.offset(pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
var templateText = document.getElementById(template).innerHTML;
|
|
||||||
var rendered = Mustache.render(templateText, {
|
|
||||||
page: value.trim().replace(/\s+/g, '_'),
|
|
||||||
results: results
|
|
||||||
}, {}, ['[[', ']]']);
|
|
||||||
let selected = $lc.find('li.selected');
|
|
||||||
if (selected) {
|
|
||||||
let selectedPos = $lc.find('li').index(selected[0])
|
|
||||||
rendered = $(rendered)
|
|
||||||
const $lis = $lc.find('li')
|
|
||||||
if ($lis.length >= 1) {
|
|
||||||
selectedPos = Math.min(selectedPos, $lis.length - 1)
|
|
||||||
rendered.find('li')[selectedPos].classList.add('selected')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$lc.html(rendered).fadeIn()
|
|
||||||
return results
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGraphs() {
|
|
||||||
$('code.language-dot').each(function (i, code) {
|
|
||||||
if (!code.innerText) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = parseDOTNetwork(code.innerText)
|
|
||||||
let network = new Network(code, data, {
|
|
||||||
layout: {
|
|
||||||
randomSeed: 1239043
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$(code).on('click', function () {
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* EVENTS
|
* EVENTS
|
||||||
*/
|
*/
|
||||||
|
@ -196,318 +64,17 @@ $(document).on('popup:selected', '#autocomplete', function (event, linkName, res
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const MD = new MarkdownIt({
|
|
||||||
linkify: true,
|
|
||||||
highlight: function (str, lang) {
|
|
||||||
if (lang === 'mermaid') {
|
|
||||||
return '<div class="mermaid">' + str + '</div>';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
MD.use(MarkdownItWikilinks({
|
|
||||||
baseURL: document.querySelector('body').dataset.baseUrl,
|
|
||||||
uriSuffix: '',
|
|
||||||
relativeBaseURL: '/edit/',
|
|
||||||
htmlAttributes: {
|
|
||||||
class: 'wiki-link'
|
|
||||||
},
|
|
||||||
})).use(MarkdownItMark).use(MarkdownItKatex)
|
|
||||||
|
|
||||||
let holders = document.getElementsByClassName('wiki-list-editor');
|
|
||||||
|
|
||||||
_.forEach(holders, async (item, i) => {
|
|
||||||
new Editor(item).then(editor => editor.start());
|
|
||||||
})
|
|
||||||
|
|
||||||
function Editor(holder) {
|
|
||||||
let scope = {}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
transform(text, element) {
|
|
||||||
let converted = text
|
|
||||||
if (converted.startsWith("```", 0) || converted.startsWith("$$", 0)) {
|
|
||||||
converted = MD.render(converted)
|
|
||||||
} else if (converted.startsWith("=", 0)) {
|
|
||||||
try {
|
|
||||||
converted = math.evaluate(converted.substring(1), scope).toString()
|
|
||||||
} catch (e) {
|
|
||||||
converted = converted + ' <span style="background: red; color: white;">' + e.message + '</span>';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (text.match(/^(\w+):: (.+)$/)) {
|
|
||||||
converted = converted.replace(/^(\w+):: (.*)$/, '**[[$1]]**: $2')
|
|
||||||
} else if (text.match(/#\[\[TODO]]/)) {
|
|
||||||
converted = converted.replace('#[[TODO]]', '<input class="checkbox" type="checkbox" />')
|
|
||||||
} else if (text.match(/#\[\[DONE]]/)) {
|
|
||||||
converted = converted.replace('#[[DONE]]', '<input class="checkbox" type="checkbox" checked />')
|
|
||||||
}
|
|
||||||
MD.options.html = true
|
|
||||||
converted = MD.renderInline(converted)
|
|
||||||
MD.options.html = false
|
|
||||||
}
|
|
||||||
|
|
||||||
element.html(converted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let inputData = JSON.parse(holder.dataset.input)
|
|
||||||
|
|
||||||
let editor = listEditor(holder, inputData, options);
|
|
||||||
holder.$listEditor = editor
|
|
||||||
|
|
||||||
$(holder).on('click', '.content input[type="checkbox"]', function (event) {
|
|
||||||
let that = this
|
|
||||||
let id = $(this).closest('.list-item').data('id')
|
|
||||||
editor.update(id, function (item, prev, next) {
|
|
||||||
if (that.checked) {
|
|
||||||
item.text = item.text.replace('#[[TODO]]', '#[[DONE]]')
|
|
||||||
} else {
|
|
||||||
item.text = item.text.replace('#[[DONE]]', '#[[TODO]]')
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
});
|
|
||||||
event.stopPropagation()
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
editor.on('change', function () {
|
|
||||||
let element = holder
|
|
||||||
let indicator = Indicator(document.getElementById('save-indicator'), 2);
|
|
||||||
let saveUrl = element.dataset.saveurl;
|
|
||||||
let page = element.dataset.page;
|
|
||||||
|
|
||||||
indicator.setText('has changes...');
|
|
||||||
addIndicator(
|
|
||||||
addSaver(editor, saveUrl, page, () => indicator.setText('saving...')),
|
|
||||||
indicator
|
|
||||||
).save()
|
|
||||||
})
|
|
||||||
|
|
||||||
editor.on('rendered', function () {
|
|
||||||
PrismJS.highlightAll()
|
|
||||||
mermaid.init()
|
|
||||||
renderGraphs();
|
|
||||||
})
|
|
||||||
|
|
||||||
menu.connectContextMenu(editor)
|
|
||||||
|
|
||||||
return createPageSearch().then(function ({titleSearch, commandSearch, commands}) {
|
|
||||||
editor.on('start-editing', function (input) {
|
|
||||||
const $lc = $('#link-complete');
|
|
||||||
|
|
||||||
$(input).parents('.list-item').addClass('active');
|
|
||||||
|
|
||||||
$lc.on('popup:selected', function (event, linkName, resultType, element) {
|
|
||||||
let value = input.value
|
|
||||||
let end = input.selectionEnd
|
|
||||||
if (resultType === 'link') {
|
|
||||||
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()
|
|
||||||
} else if (resultType === 'command') {
|
|
||||||
let start = value.lastIndexOf("/", end)
|
|
||||||
let commandResult = ""
|
|
||||||
let replace = ""
|
|
||||||
let prefix = false
|
|
||||||
let adjustment = 0
|
|
||||||
|
|
||||||
let now = moment()
|
|
||||||
|
|
||||||
if (linkName === "Current Time") {
|
|
||||||
commandResult = now.format('HH:mm')
|
|
||||||
} else if (linkName === "Today") {
|
|
||||||
commandResult = "[[" + now.format('D MMMM YYYY') + "]]"
|
|
||||||
} else if (linkName === "Tomorrow") {
|
|
||||||
commandResult = "[[" + now.add(1, 'day').format('D MMMM YYYY') + "]]"
|
|
||||||
} else if (linkName === "Yesterday") {
|
|
||||||
commandResult = "[[" + now.add(-1, 'day').format('D MMMM YYYY') + "]]"
|
|
||||||
} else if (linkName === "TODO") {
|
|
||||||
commandResult = "#[[TODO]] "
|
|
||||||
replace = "#[[DONE]] "
|
|
||||||
prefix = true
|
|
||||||
} else if (linkName === "DONE") {
|
|
||||||
commandResult = "#[[DONE]] "
|
|
||||||
replace = "#[[TODO]] "
|
|
||||||
prefix = true
|
|
||||||
} else if (linkName === "Page Reference") {
|
|
||||||
commandResult = "[[]]"
|
|
||||||
adjustment = -2
|
|
||||||
} else if (linkName === "Code Block") {
|
|
||||||
commandResult = "```\n\n```"
|
|
||||||
adjustment = -5
|
|
||||||
}
|
|
||||||
|
|
||||||
let startAndLink = prefix
|
|
||||||
? commandResult + value.substring(0, start).replace(replace, "")
|
|
||||||
: value.substring(0, start) + commandResult
|
|
||||||
|
|
||||||
input.value = startAndLink + value.substring(end)
|
|
||||||
|
|
||||||
input.selectionStart = startAndLink.length + adjustment
|
|
||||||
input.selectionEnd = startAndLink.length + adjustment
|
|
||||||
|
|
||||||
input.focus()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
$lc.on('popup:leave', function (event) {
|
|
||||||
input.focus()
|
|
||||||
})
|
|
||||||
|
|
||||||
$(input).on('keydown', function (event) {
|
|
||||||
const isVisible = $('#link-complete:visible').length > 0;
|
|
||||||
|
|
||||||
if (event.key === 'Escape' && isVisible) {
|
|
||||||
$lc.fadeOut()
|
|
||||||
return false
|
|
||||||
} else if (event.key === 'Enter' && isVisible) {
|
|
||||||
const element = $lc.find('li.selected')
|
|
||||||
const linkName = element.text()
|
|
||||||
$lc.trigger('popup:selected', [linkName, $lc.data('result-type'), element])
|
|
||||||
$lc.fadeOut()
|
|
||||||
return false
|
|
||||||
} else if (event.key === 'ArrowUp' && isVisible) {
|
|
||||||
const selected = $lc.find('li.selected')
|
|
||||||
const prev = selected.prev('li')
|
|
||||||
if (prev.length) {
|
|
||||||
prev.addClass('selected')
|
|
||||||
selected.removeClass('selected')
|
|
||||||
prev[0].scrollIntoView({block: 'center', inline: 'nearest'})
|
|
||||||
} else {
|
|
||||||
// move back from dropdown to input
|
|
||||||
$lc.trigger('popup:leave')
|
|
||||||
selected.removeClass('selected')
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
} else if (event.key === 'ArrowDown' && isVisible) {
|
|
||||||
const selected = $lc.find('li.selected')
|
|
||||||
if (!selected.length) {
|
|
||||||
$lc.find('li:not(.selected):first-child').addClass('selected')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = selected.next('li');
|
|
||||||
if (next.length) {
|
|
||||||
next.addClass('selected')
|
|
||||||
selected.removeClass('selected')
|
|
||||||
next[0].scrollIntoView({block: 'center', inline: 'nearest'})
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let mirror = {
|
|
||||||
'[': ']',
|
|
||||||
'(': ')',
|
|
||||||
'{': '}',
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMultiline(input) && mirror.hasOwnProperty(event.key)) {
|
|
||||||
let input = this
|
|
||||||
let val = input.value
|
|
||||||
let prefix = val.substring(0, input.selectionStart)
|
|
||||||
let selection = val.substring(input.selectionStart, input.selectionEnd)
|
|
||||||
let suffix = val.substring(input.selectionEnd)
|
|
||||||
input.value = prefix + event.key + selection + mirror[event.key] + suffix
|
|
||||||
input.selectionStart = prefix.length + event.key.length
|
|
||||||
input.selectionEnd = input.selectionStart + selection.length
|
|
||||||
$(input).trigger('input')
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
let searchEnabled = false
|
|
||||||
|
|
||||||
$(input).on('keyup', function (event) {
|
|
||||||
if (event.key === '/') {
|
|
||||||
searchEnabled = true
|
|
||||||
}
|
|
||||||
if (searchEnabled && event.key === 'Escape') {
|
|
||||||
searchEnabled = false
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const ignoreKeys = {
|
|
||||||
'ArrowUp': true,
|
|
||||||
'ArrowDown': true,
|
|
||||||
'Enter': true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key in ignoreKeys) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = input.value
|
|
||||||
let end = input.selectionEnd
|
|
||||||
|
|
||||||
let [start, insideLink] = util.cursorInsideLink(value, end)
|
|
||||||
let insideSearch = false
|
|
||||||
|
|
||||||
if (searchEnabled && !insideLink) {
|
|
||||||
start = value.lastIndexOf("/", end)
|
|
||||||
insideSearch = start >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (insideSearch) {
|
|
||||||
let query = value.substring(start + 1, end);
|
|
||||||
showSearchResults(commandSearch, query, input, value, 'command').then(results => {
|
|
||||||
if (query.length > 0 && result.length === 0) {
|
|
||||||
searchEnabled = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
} else if (insideLink) {
|
|
||||||
let query = value.substring(start + 2, end);
|
|
||||||
showSearchResults(titleSearch, query, input, value, 'link');
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
$('#link-complete').fadeOut();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
editor.on('stop-editing', function (input) {
|
|
||||||
$(input).parents('.list-item').removeClass('active');
|
|
||||||
$('#link-complete').off()
|
|
||||||
PrismJS.highlightAll()
|
|
||||||
mermaid.init()
|
|
||||||
renderGraphs();
|
|
||||||
})
|
|
||||||
return editor
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeout = null;
|
|
||||||
let searchInput = document.getElementById('search-input');
|
|
||||||
search(searchInput).then(searcher => {
|
|
||||||
let showSearch = _.debounce(function (searcher) {
|
|
||||||
let query = $(searchInput).val()
|
|
||||||
if (query === '') {
|
|
||||||
let autocomplete = document.getElementById('autocomplete');
|
|
||||||
$(autocomplete).hide()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
showSearchResultsExtended('#autocomplete', 'result-template', query => searcher.search(query), query, searchInput, query, 'search-result')
|
|
||||||
return true;
|
|
||||||
}, 200)
|
|
||||||
|
|
||||||
$(searchInput).on('keyup', function (event) {
|
|
||||||
showSearch.cancel()
|
|
||||||
showSearch(searcher)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
document.querySelectorAll(".page-loader")
|
document.querySelectorAll(".page-loader")
|
||||||
.forEach((el, key, parent) => {
|
.forEach((el, key, parent) => {
|
||||||
fetch('/' + el.dataset.page + '?format=markdown').then(res => res.text()).then(text => {
|
let format = el.dataset.format
|
||||||
|
let edit = el.dataset.edit
|
||||||
|
fetch('/' + el.dataset.page + '?format=' + format)
|
||||||
|
.then(res => edit ? res.json() : res.text())
|
||||||
|
.then(text => {
|
||||||
|
if (edit) {
|
||||||
|
new Editor(el, text).then(editor => editor.start());
|
||||||
|
} else {
|
||||||
el.innerHTML = MD.render(text)
|
el.innerHTML = MD.render(text)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
25
editor/src/markdown.js
Normal file
25
editor/src/markdown.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import MarkdownIt from "markdown-it";
|
||||||
|
import MarkdownItWikilinks from "./wikilinks";
|
||||||
|
import MarkdownItMark from "markdown-it-mark";
|
||||||
|
import MarkdownItKatex from "markdown-it-katex";
|
||||||
|
|
||||||
|
const MD = new MarkdownIt({
|
||||||
|
linkify: true,
|
||||||
|
highlight: function (str, lang) {
|
||||||
|
if (lang === 'mermaid') {
|
||||||
|
return '<div class="mermaid">' + str + '</div>';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
MD.use(MarkdownItWikilinks({
|
||||||
|
baseURL: document.querySelector('body').dataset.baseUrl,
|
||||||
|
uriSuffix: '',
|
||||||
|
relativeBaseURL: '/edit/',
|
||||||
|
htmlAttributes: {
|
||||||
|
class: 'wiki-link'
|
||||||
|
},
|
||||||
|
})).use(MarkdownItMark).use(MarkdownItKatex)
|
||||||
|
|
||||||
|
export default MD;
|
|
@ -44,6 +44,14 @@ function connectContextMenu(editor) {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
className: 'action-copy-line'
|
className: 'action-copy-line'
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
name: 'Zoom in',
|
||||||
|
callback: function (key, opt) {
|
||||||
|
editor.zoomin(this).then(id => {
|
||||||
|
location.href = '/edit/'+id;
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,11 +13,13 @@ function search(element) {
|
||||||
let actualResult = [];
|
let actualResult = [];
|
||||||
$.each(data.hits, (key, value) => {
|
$.each(data.hits, (key, value) => {
|
||||||
actualResult.push({
|
actualResult.push({
|
||||||
ref: value.id,
|
ref: value.fields.page,
|
||||||
title: value.id.replace(/_/g, ' '),
|
title: value.fields.title,
|
||||||
|
text: value.fragments.text[0]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
element.classList.remove('is-loading')
|
element.classList.remove('is-loading')
|
||||||
|
console.log(actualResult)
|
||||||
return actualResult
|
return actualResult
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,9 @@
|
||||||
@import "~bulma/sass/layout/_all";
|
@import "~bulma/sass/layout/_all";
|
||||||
|
|
||||||
@import '~jquery-contextmenu/dist/jquery.contextMenu.css';
|
@import '~jquery-contextmenu/dist/jquery.contextMenu.css';
|
||||||
@import '~vis-network/styles/vis-network.css';
|
//@import '~vis-network/styles/vis-network.css';
|
||||||
|
|
||||||
|
@import url('https://rsms.me/inter/inter.css');
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
|
@ -237,3 +239,205 @@ mark {
|
||||||
.wiki-list-editor {
|
.wiki-list-editor {
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column a + a :before {
|
||||||
|
content: '>';
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
#autocomplete {
|
||||||
|
z-index: 1;
|
||||||
|
right: 0;
|
||||||
|
width: 640px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 600px;
|
||||||
|
position: absolute;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
#autocomplete li > a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#autocomplete li div {
|
||||||
|
font-size: 0.8em;
|
||||||
|
display: block;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
#autocomplete li {
|
||||||
|
padding: 4px 16px;
|
||||||
|
max-height: 5em;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#autocomplete li:hover {
|
||||||
|
background: #fefefe;
|
||||||
|
}
|
||||||
|
#autocomplete li.selected {
|
||||||
|
background: lightblue;
|
||||||
|
}
|
||||||
|
#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;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content input[type="checkbox"] {
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lighter {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
del {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ins {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist--item {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist--item-text {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.input-line {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root .list-item {
|
||||||
|
padding: 3px;
|
||||||
|
padding-left: 12px;
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 32px;
|
||||||
|
flex-direction: column;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
border: none;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item .content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background: lightblue;
|
||||||
|
}
|
||||||
|
.selected .marker {
|
||||||
|
border-color: lightblue;
|
||||||
|
}
|
||||||
|
#editor {
|
||||||
|
width: 750px;
|
||||||
|
}
|
||||||
|
.editor.selected .marker {
|
||||||
|
/*border-color: white;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor.selected {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.input-line, input.input-line:active {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gu-mirror {
|
||||||
|
position: fixed !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
z-index: 9999 !important;
|
||||||
|
opacity: 0.8;
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
|
||||||
|
filter: alpha(opacity=80);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gu-hide {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gu-unselectable {
|
||||||
|
-webkit-user-select: none !important;
|
||||||
|
-moz-user-select: none !important;
|
||||||
|
-ms-user-select: none !important;
|
||||||
|
user-select: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gu-transit {
|
||||||
|
opacity: 0.2;
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
|
||||||
|
filter: alpha(opacity=20);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backrefs {
|
||||||
|
padding: 24px;
|
||||||
|
background: #deeeee;
|
||||||
|
border-top: 3px solid #acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb li {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
.breadcrumb li > a {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
427
file.go
427
file.go
|
@ -9,9 +9,11 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -22,6 +24,7 @@ import (
|
||||||
const (
|
const (
|
||||||
DocumentsFile = "_documents.json"
|
DocumentsFile = "_documents.json"
|
||||||
LinksFile = "_links.json"
|
LinksFile = "_links.json"
|
||||||
|
BlocksDirectory = "_blocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
type saveMessage struct {
|
type saveMessage struct {
|
||||||
|
@ -31,6 +34,75 @@ type saveMessage struct {
|
||||||
author string
|
author string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Block struct {
|
||||||
|
Text string
|
||||||
|
Children []string
|
||||||
|
Parent string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ID struct {
|
||||||
|
StrID string
|
||||||
|
WasInt bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id *ID) UnmarshalJSON(data []byte) error {
|
||||||
|
var intID int
|
||||||
|
err := json.Unmarshal(data, &intID)
|
||||||
|
if err == nil {
|
||||||
|
*id = ID{strconv.FormatInt(int64(intID), 10), true}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var strID string
|
||||||
|
err = json.Unmarshal(data, &strID)
|
||||||
|
if err == nil {
|
||||||
|
*id = ID{strID, false}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("could not unmarshal %q as an int or string", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id *ID) NewID() string {
|
||||||
|
if id.WasInt {
|
||||||
|
l := time.Now().UnixNano()
|
||||||
|
r := rand.Uint64()
|
||||||
|
return fmt.Sprintf("_%d_%d", l, r)
|
||||||
|
} else {
|
||||||
|
return id.StrID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListItemV2 is way to convert from old structure to new structure
|
||||||
|
type ListItemV2 struct {
|
||||||
|
ID ID
|
||||||
|
Indented int
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v2 ListItemV2) ListItem() ListItem {
|
||||||
|
return ListItem{
|
||||||
|
v2.ID.StrID,
|
||||||
|
v2.Indented,
|
||||||
|
v2.Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListItem is a simplification of the information that was saved by the editor
|
||||||
|
type ListItem struct {
|
||||||
|
ID string
|
||||||
|
Indented int
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActualListItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Indented int `json:"indented"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Fold string `json:"fold"`
|
||||||
|
Hidden bool `json:"hidden"`
|
||||||
|
}
|
||||||
|
|
||||||
type FilePages struct {
|
type FilePages struct {
|
||||||
dirname string
|
dirname string
|
||||||
saveC chan saveMessage
|
saveC chan saveMessage
|
||||||
|
@ -38,7 +110,13 @@ type FilePages struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFilePages(dirname string, index bleve.Index) PagesRepository {
|
func NewFilePages(dirname string, index bleve.Index) PagesRepository {
|
||||||
|
err := os.MkdirAll(filepath.Join(dirname, "_blocks"), 0777)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
err := fp.save(msg)
|
err := fp.save(msg)
|
||||||
|
@ -50,14 +128,40 @@ func NewFilePages(dirname string, index bleve.Index) PagesRepository {
|
||||||
return fp
|
return fp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertBlocksToListItems(current string, blocks BlockResponse, indent int) []ActualListItem {
|
||||||
|
var listItems []ActualListItem
|
||||||
|
|
||||||
|
for _, child := range blocks.Children[current] {
|
||||||
|
l := convertBlocksToListItems(child, blocks, indent+1)
|
||||||
|
listItems = append(listItems,
|
||||||
|
ActualListItem{
|
||||||
|
ID: child,
|
||||||
|
Indented: indent,
|
||||||
|
Text: blocks.Texts[child],
|
||||||
|
Fold: "open",
|
||||||
|
Hidden: false,
|
||||||
|
})
|
||||||
|
listItems = append(listItems, l...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return listItems
|
||||||
|
}
|
||||||
|
|
||||||
func (fp *FilePages) Get(title string) Page {
|
func (fp *FilePages) Get(title string) Page {
|
||||||
name := strings.Replace(title, " ", "_", -1)
|
// TODO: cleanup loading of pages
|
||||||
title = strings.Replace(title, "_", " ", -1)
|
// TODO: convert all pages to blocks
|
||||||
|
name := title
|
||||||
|
|
||||||
refs, err := getBackrefs(fp, name)
|
refs, err := getBackrefs(fp, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
refs = nil
|
refs = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blocks, err := loadBlocks(fp.dirname, name)
|
||||||
|
if err != nil {
|
||||||
|
name = strings.Replace(title, " ", "_", -1)
|
||||||
|
title = strings.Replace(title, "_", " ", -1)
|
||||||
|
|
||||||
f, err := os.Open(filepath.Join(fp.dirname, name))
|
f, err := os.Open(filepath.Join(fp.dirname, name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Page{
|
return Page{
|
||||||
|
@ -65,8 +169,10 @@ func (fp *FilePages) Get(title string) Page {
|
||||||
Name: name,
|
Name: name,
|
||||||
Content: "",
|
Content: "",
|
||||||
Refs: refs,
|
Refs: refs,
|
||||||
|
Blocks: blocks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
body, err := ioutil.ReadAll(f)
|
body, err := ioutil.ReadAll(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -75,13 +181,35 @@ func (fp *FilePages) Get(title string) Page {
|
||||||
Name: name,
|
Name: name,
|
||||||
Content: "",
|
Content: "",
|
||||||
Refs: refs,
|
Refs: refs,
|
||||||
|
Blocks: blocks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Page{
|
return Page{
|
||||||
Name: name,
|
Name: name,
|
||||||
Title: title,
|
Title: title,
|
||||||
Content: string(body),
|
Content: string(body),
|
||||||
Refs: refs,
|
Refs: refs,
|
||||||
|
Blocks: blocks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
current := blocks.PageID
|
||||||
|
listItems := convertBlocksToListItems(current, blocks, 0)
|
||||||
|
if listItems == nil {
|
||||||
|
listItems = []ActualListItem{}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(&buf).Encode(&listItems)
|
||||||
|
|
||||||
|
return Page{
|
||||||
|
Name: name,
|
||||||
|
Title: blocks.Texts[name],
|
||||||
|
Content: buf.String(),
|
||||||
|
Refs: refs,
|
||||||
|
Blocks: blocks,
|
||||||
|
Parent: blocks.ParentID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +220,6 @@ func (fp *FilePages) Save(p string, page Page, summary, author string) error {
|
||||||
|
|
||||||
func (fp *FilePages) save(msg saveMessage) error {
|
func (fp *FilePages) save(msg saveMessage) error {
|
||||||
var sw stopwatch
|
var sw stopwatch
|
||||||
sw.Start("prepare")
|
|
||||||
p := msg.p
|
p := msg.p
|
||||||
page := msg.page
|
page := msg.page
|
||||||
summary := msg.summary
|
summary := msg.summary
|
||||||
|
@ -101,6 +228,9 @@ func (fp *FilePages) save(msg saveMessage) error {
|
||||||
page.Name = strings.Replace(p, " ", "_", -1)
|
page.Name = strings.Replace(p, " ", "_", -1)
|
||||||
page.Title = strings.Replace(p, "_", " ", -1)
|
page.Title = strings.Replace(p, "_", " ", -1)
|
||||||
|
|
||||||
|
if p[0] != '_' {
|
||||||
|
sw.Start("prepare")
|
||||||
|
|
||||||
f, err := os.Create(filepath.Join(fp.dirname, strings.Replace(p, " ", "_", -1)))
|
f, err := os.Create(filepath.Join(fp.dirname, strings.Replace(p, " ", "_", -1)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -130,27 +260,243 @@ func (fp *FilePages) save(msg saveMessage) error {
|
||||||
sw.Start("git")
|
sw.Start("git")
|
||||||
err = saveWithGit(fp, p, summary, author)
|
err = saveWithGit(fp, p, summary, author)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("while saving to git: %w", err)
|
log.Printf("Error while saving to git: %w", err)
|
||||||
|
// return fmt.Errorf("while saving to git: %w", err)
|
||||||
}
|
}
|
||||||
sw.Stop()
|
sw.Stop()
|
||||||
|
|
||||||
sw.Start("index")
|
sw.Start("index")
|
||||||
so, err := createSearchObject(page)
|
searchObjects, err := createSearchObjects(page.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("while creating search object %s: %w", page.Name, err)
|
return fmt.Errorf("while creating search object %s: %w", page.Name, err)
|
||||||
}
|
}
|
||||||
|
for _, so := range searchObjects {
|
||||||
if fp.index != nil {
|
if fp.index != nil {
|
||||||
err = fp.index.Index(page.Name, so)
|
err = fp.index.Index(so.ID, so)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("while indexing %s: %w", page.Name, err)
|
return fmt.Errorf("while indexing %s: %w", page.Name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
sw.Stop()
|
sw.Stop()
|
||||||
sw.Start("links")
|
sw.Start("links")
|
||||||
err = saveLinksIncremental(fp.dirname, page.Title)
|
err = saveLinksIncremental(fp.dirname, page.Title)
|
||||||
sw.Stop()
|
sw.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.Start("create blocks")
|
||||||
|
err := saveBlocksFromPage(fp.dirname, page)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
sw.Stop()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func saveWithNewIDs(dirname string, listItems []ListItemV2, pageName string) ([]ListItem, error) {
|
||||||
|
var newListItems []ListItem
|
||||||
|
for _, item := range listItems {
|
||||||
|
newItem := ListItem{
|
||||||
|
ID: item.ID.NewID(),
|
||||||
|
Indented: item.Indented,
|
||||||
|
Text: item.Text,
|
||||||
|
}
|
||||||
|
newListItems = append(newListItems, newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newListItems, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveBlocksFromPage(dirname string, page Page) error {
|
||||||
|
log.Println("Processing: ", page.Name)
|
||||||
|
var listItems []ListItem
|
||||||
|
err := json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItems)
|
||||||
|
if err != nil {
|
||||||
|
var listItemsV2 []ListItemV2
|
||||||
|
err = json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItemsV2)
|
||||||
|
listItems, err = saveWithNewIDs(dirname, listItemsV2, page.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("while rewriting %s to use new ids: %w", page.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks := make(map[string]Block)
|
||||||
|
prevList := make(map[string]ListItem)
|
||||||
|
|
||||||
|
root := "root"
|
||||||
|
parentBlock, err := loadBlock(dirname, page.Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
} else {
|
||||||
|
root = parentBlock.Parent
|
||||||
|
}
|
||||||
|
|
||||||
|
title := page.Title
|
||||||
|
if page.Name[0] == '_' {
|
||||||
|
title = parentBlock.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent = ListItem{
|
||||||
|
Text: title,
|
||||||
|
Indented: -1,
|
||||||
|
ID: page.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
prevList[parent.ID] = parent
|
||||||
|
blocks[parent.ID] = Block{
|
||||||
|
Text: title,
|
||||||
|
Children: nil,
|
||||||
|
Parent: root,
|
||||||
|
}
|
||||||
|
|
||||||
|
var prev = &parent
|
||||||
|
|
||||||
|
for i, item := range listItems {
|
||||||
|
prevList[item.ID] = item
|
||||||
|
if item.Indented > prev.Indented {
|
||||||
|
parent = *prev
|
||||||
|
} else if item.Indented == prev.Indented {
|
||||||
|
// nothing
|
||||||
|
} else if item.Indented <= parent.Indented {
|
||||||
|
for item.Indented <= parent.Indented {
|
||||||
|
if block, e := blocks[parent.ID]; e {
|
||||||
|
parent = prevList[block.Parent]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks[item.ID] = Block{item.Text, []string{}, parent.ID}
|
||||||
|
if block, e := blocks[parent.ID]; e {
|
||||||
|
block.Children = append(block.Children, item.ID)
|
||||||
|
blocks[parent.ID] = block
|
||||||
|
} else {
|
||||||
|
log.Println("Block missing")
|
||||||
|
}
|
||||||
|
prev = &listItems[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loading parent block: %s", parent.ID)
|
||||||
|
f, err := os.Open(filepath.Join(dirname, BlocksDirectory, parent.ID))
|
||||||
|
if err == nil {
|
||||||
|
var parentBlock Block
|
||||||
|
err = json.NewDecoder(f).Decode(&parentBlock)
|
||||||
|
if err == nil {
|
||||||
|
if pb, e := blocks[parent.ID]; e {
|
||||||
|
pb.Text = parentBlock.Text
|
||||||
|
pb.Parent = parentBlock.Parent
|
||||||
|
blocks[parent.ID] = pb
|
||||||
|
log.Printf("Text=%s, Parent=%s", parentBlock.Text, parentBlock.Parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
} else {
|
||||||
|
log.Println(err)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, block := range blocks {
|
||||||
|
log.Println("Writing to ", id)
|
||||||
|
f, err := os.OpenFile(filepath.Join(dirname, BlocksDirectory, id), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(f)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
err = enc.Encode(&block)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockResponse struct {
|
||||||
|
PageID string
|
||||||
|
ParentID string
|
||||||
|
Texts map[string]string
|
||||||
|
Children map[string][]string
|
||||||
|
Parents []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadBlocks(dirname, rootBlockID string) (BlockResponse, error) {
|
||||||
|
resp := BlockResponse{
|
||||||
|
rootBlockID,
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Texts = make(map[string]string)
|
||||||
|
resp.Children = make(map[string][]string)
|
||||||
|
|
||||||
|
queue := []string{rootBlockID}
|
||||||
|
|
||||||
|
block, err := loadBlock(dirname, rootBlockID)
|
||||||
|
if err != nil {
|
||||||
|
return BlockResponse{}, fmt.Errorf("while loading current block (%s): %w", rootBlockID, err)
|
||||||
|
}
|
||||||
|
if rootBlockID[0] != '_' && block.Children == nil {
|
||||||
|
return BlockResponse{}, fmt.Errorf("while loading current block (%s): not a block and no children", rootBlockID)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevID := rootBlockID
|
||||||
|
parentID := block.Parent
|
||||||
|
|
||||||
|
for parentID != "root" {
|
||||||
|
parent, err := loadBlock(dirname, parentID)
|
||||||
|
if err != nil {
|
||||||
|
return BlockResponse{}, fmt.Errorf("while loading current parent block (%s->%s): %w", prevID, parentID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Texts[parentID] = parent.Text
|
||||||
|
resp.Children[parentID] = parent.Children
|
||||||
|
resp.ParentID = parentID
|
||||||
|
|
||||||
|
resp.Parents = append(resp.Parents, parentID)
|
||||||
|
|
||||||
|
prevID = parentID
|
||||||
|
parentID = parent.Parent
|
||||||
|
}
|
||||||
|
if parentID == "root" {
|
||||||
|
resp.ParentID = "root"
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if len(queue) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
current := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
block, err := loadBlock(dirname, current)
|
||||||
|
if err != nil {
|
||||||
|
return BlockResponse{}, fmt.Errorf("while loading block (%s): %w", current, err)
|
||||||
|
}
|
||||||
|
resp.Texts[current] = block.Text
|
||||||
|
resp.Children[current] = block.Children
|
||||||
|
queue = append(queue, block.Children...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadBlock(dirname, blockID string) (Block, error) {
|
||||||
|
f, err := os.Open(filepath.Join(dirname, BlocksDirectory, blockID))
|
||||||
|
if err != nil {
|
||||||
|
return Block{}, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
var block Block
|
||||||
|
err = json.NewDecoder(f).Decode(&block)
|
||||||
|
if err != nil {
|
||||||
|
return Block{}, err
|
||||||
|
}
|
||||||
|
return block, nil
|
||||||
|
}
|
||||||
|
|
||||||
func saveLinksIncremental(dirname, title string) error {
|
func saveLinksIncremental(dirname, title string) error {
|
||||||
type Document struct {
|
type Document struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
|
@ -189,19 +535,19 @@ func saveLinksIncremental(dirname, title string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveLinks(fp *FilePages) error {
|
func saveLinks(mp PagesRepository) error {
|
||||||
type Document struct {
|
type Document struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
}
|
}
|
||||||
var results []Document
|
var results []Document
|
||||||
pages, err := mp.(*FilePages).AllPages()
|
pages, err := mp.AllPages()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, page := range pages {
|
for _, page := range pages {
|
||||||
results = append(results, Document{page.Title})
|
results = append(results, Document{page.Title})
|
||||||
}
|
}
|
||||||
f, err := os.Create(filepath.Join(fp.dirname, LinksFile))
|
f, err := os.Create(filepath.Join(mp.(*FilePages).dirname, LinksFile))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -213,65 +559,6 @@ func saveLinks(fp *FilePages) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveDocuments(fp *FilePages) error {
|
|
||||||
type Document struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Body string `json:"body"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
var results []Document
|
|
||||||
pages, err := mp.(*FilePages).AllPages()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, page := range pages {
|
|
||||||
content := strings.Builder{}
|
|
||||||
|
|
||||||
var listItems []struct {
|
|
||||||
Indented int
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItems)
|
|
||||||
if err == nil {
|
|
||||||
for _, item := range listItems {
|
|
||||||
content.WriteString(item.Text)
|
|
||||||
content.WriteByte(' ')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content.WriteString(page.Content)
|
|
||||||
content.WriteByte(' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
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.String(),
|
|
||||||
URL: page.Name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Create(filepath.Join(fp.dirname, DocumentsFile))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
err = json.NewEncoder(f).Encode(&results)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveWithGit(fp *FilePages, p string, summary, author string) error {
|
func saveWithGit(fp *FilePages, p string, summary, author string) error {
|
||||||
cmd := exec.Command("git", "add", ".")
|
cmd := exec.Command("git", "add", ".")
|
||||||
cmd.Dir = fp.dirname
|
cmd.Dir = fp.dirname
|
||||||
|
@ -282,6 +569,8 @@ func saveWithGit(fp *FilePages, p string, summary, author string) error {
|
||||||
|
|
||||||
cmd = exec.Command("git", "commit", "-m", "Changes to "+p+" by "+author+"\n\n"+summary)
|
cmd = exec.Command("git", "commit", "-m", "Changes to "+p+" by "+author+"\n\n"+summary)
|
||||||
cmd.Dir = fp.dirname
|
cmd.Dir = fp.dirname
|
||||||
|
// cmd.Stderr = os.Stderr
|
||||||
|
// cmd.Stdout = os.Stderr
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("while commiting page %s: %s", p, err)
|
return fmt.Errorf("while commiting page %s: %s", p, err)
|
||||||
|
@ -455,6 +744,8 @@ func (fp *FilePages) RecentChanges() ([]Change, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fp *FilePages) AllPages() ([]Page, error) {
|
func (fp *FilePages) AllPages() ([]Page, error) {
|
||||||
|
log.Println("AllPages", fp.dirname)
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(fp.dirname)
|
files, err := ioutil.ReadDir(fp.dirname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -7,6 +7,7 @@ require (
|
||||||
github.com/blevesearch/bleve v1.0.9
|
github.com/blevesearch/bleve v1.0.9
|
||||||
github.com/blevesearch/cld2 v0.0.0-20200327141045-8b5f551d37f5 // indirect
|
github.com/blevesearch/cld2 v0.0.0-20200327141045-8b5f551d37f5 // indirect
|
||||||
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d // indirect
|
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a // indirect
|
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a // indirect
|
||||||
github.com/golang/protobuf v1.4.2 // indirect
|
github.com/golang/protobuf v1.4.2 // indirect
|
||||||
github.com/ikawaha/kagome.ipadic v1.1.2 // indirect
|
github.com/ikawaha/kagome.ipadic v1.1.2 // indirect
|
||||||
|
|
|
@ -275,8 +275,6 @@ function editor(root, inputData, options) {
|
||||||
if (store.hasChanged()) {
|
if (store.hasChanged()) {
|
||||||
resolve(store.tree(from))
|
resolve(store.tree(from))
|
||||||
store.clearChanged()
|
store.clearChanged()
|
||||||
} else {
|
|
||||||
reject()
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -294,6 +292,15 @@ function editor(root, inputData, options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function zoomin(element, opt) {
|
||||||
|
let item = $(element).parents('.list-item')
|
||||||
|
let id = item.data('id')
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
resolve(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function on(evt, handler) {
|
function on(evt, handler) {
|
||||||
events[evt].push(handler)
|
events[evt].push(handler)
|
||||||
}
|
}
|
||||||
|
@ -452,8 +459,8 @@ function editor(root, inputData, options) {
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
$(root).on('click', '.list-item', function () {
|
$(root).on('click', '.content', function (event) {
|
||||||
let currentIndex = $(root).children('div.list-item').index(this)
|
let currentIndex = $(root).children('div.list-item').index($(this).parents('.list-item')[0])
|
||||||
if (cursor.atPosition(currentIndex) && currentEditor !== null && currentEditor.closest('.list-item')[0] === this) {
|
if (cursor.atPosition(currentIndex) && currentEditor !== null && currentEditor.closest('.list-item')[0] === this) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -509,7 +516,8 @@ function editor(root, inputData, options) {
|
||||||
saveTree,
|
saveTree,
|
||||||
copy,
|
copy,
|
||||||
update,
|
update,
|
||||||
start
|
start,
|
||||||
|
zoomin
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -195,7 +195,7 @@ function Store(inputData) {
|
||||||
let index = _.findIndex(idList, (id) => id === currentId)
|
let index = _.findIndex(idList, (id) => id === currentId)
|
||||||
let oldValue = _.clone(values[currentId])
|
let oldValue = _.clone(values[currentId])
|
||||||
let newValue = callback(values[currentId], values[idList[index - 1]], values[idList[index + 1]]);
|
let newValue = callback(values[currentId], values[idList[index - 1]], values[idList[index + 1]]);
|
||||||
if (oldValue.text !== newValue.text) {
|
if (oldValue && oldValue.text !== newValue.text) {
|
||||||
values[currentId] = newValue
|
values[currentId] = newValue
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
62
main.go
62
main.go
|
@ -53,6 +53,8 @@ type Page struct {
|
||||||
Content string
|
Content string
|
||||||
|
|
||||||
Refs map[string][]Backref
|
Refs map[string][]Backref
|
||||||
|
Blocks BlockResponse
|
||||||
|
Parent string
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiffPage struct {
|
type DiffPage struct {
|
||||||
|
@ -123,6 +125,11 @@ type graphPage struct {
|
||||||
Edges template.JS
|
Edges template.JS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Parent struct {
|
||||||
|
Text string
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
type editPage struct {
|
type editPage struct {
|
||||||
pageBaseInfo
|
pageBaseInfo
|
||||||
Session *Session
|
Session *Session
|
||||||
|
@ -133,6 +140,8 @@ type editPage struct {
|
||||||
Backrefs map[string][]Backref
|
Backrefs map[string][]Backref
|
||||||
ShowGraph bool
|
ShowGraph bool
|
||||||
TodayPage string
|
TodayPage string
|
||||||
|
Parent Parent
|
||||||
|
Parents []Parent
|
||||||
}
|
}
|
||||||
|
|
||||||
type historyPage struct {
|
type historyPage struct {
|
||||||
|
@ -445,11 +454,27 @@ func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pageBase := getPageBase()
|
pageBase := getPageBase()
|
||||||
title := cleanTitle(page)
|
title := cleanTitle(mpPage.Title)
|
||||||
if newTitle, err := PageTitle(pageText); err == nil {
|
if newTitle, err := PageTitle(pageText); err == nil {
|
||||||
title = newTitle
|
title = newTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var parent Parent
|
||||||
|
parent.ID = mpPage.Parent
|
||||||
|
parent.Text = mpPage.Blocks.Texts[parent.ID]
|
||||||
|
|
||||||
|
var parents []Parent
|
||||||
|
for _, p := range mpPage.Blocks.Parents {
|
||||||
|
var parent Parent
|
||||||
|
parent.ID = p
|
||||||
|
parent.Text = mpPage.Blocks.Texts[p]
|
||||||
|
parents = append(parents, parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, j := 0, len(parents)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
parents[i], parents[j] = parents[j], parents[i]
|
||||||
|
}
|
||||||
|
|
||||||
data := editPage{
|
data := editPage{
|
||||||
pageBaseInfo: pageBase,
|
pageBaseInfo: pageBase,
|
||||||
Session: sess,
|
Session: sess,
|
||||||
|
@ -460,6 +485,8 @@ func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
Backrefs: mpPage.Refs,
|
Backrefs: mpPage.Refs,
|
||||||
TodayPage: "Today",
|
TodayPage: "Today",
|
||||||
ShowGraph: page != "Daily_Notes",
|
ShowGraph: page != "Daily_Notes",
|
||||||
|
Parent: parent,
|
||||||
|
Parents: parents,
|
||||||
}
|
}
|
||||||
templates := baseTemplate
|
templates := baseTemplate
|
||||||
templates = append(templates, "templates/edit.html")
|
templates = append(templates, "templates/edit.html")
|
||||||
|
@ -468,6 +495,8 @@ func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=600")
|
||||||
err = t.Execute(w, data)
|
err = t.Execute(w, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
@ -595,7 +624,7 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
mpPage := mp.Get(page)
|
mpPage := mp.Get(page)
|
||||||
pageText := mpPage.Content
|
pageText := mpPage.Content
|
||||||
if pageText == "" {
|
if (format == "" || format == "html") && pageText == "" {
|
||||||
http.Redirect(w, r, "/edit/"+page, 302)
|
http.Redirect(w, r, "/edit/"+page, 302)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -612,6 +641,8 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
jsonPage := pageText != "" && err == nil
|
jsonPage := pageText != "" && err == nil
|
||||||
if jsonPage {
|
if jsonPage {
|
||||||
if format == "json" {
|
if format == "json" {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
// Shortcut for json output
|
// Shortcut for json output
|
||||||
_, err := io.WriteString(w, pageText)
|
_, err := io.WriteString(w, pageText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -619,11 +650,13 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
} else if format == "metakv" {
|
} else if format == "metakv" {
|
||||||
so, err := createSearchObject(mpPage)
|
so, err := createStructuredFormat(mpPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
enc := json.NewEncoder(w)
|
enc := json.NewEncoder(w)
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
err = enc.Encode(so)
|
err = enc.Encode(so)
|
||||||
|
@ -701,6 +734,8 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=600")
|
||||||
err = t.Execute(w, data)
|
err = t.Execute(w, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
@ -708,6 +743,8 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if format == "markdown" {
|
} else if format == "markdown" {
|
||||||
|
w.Header().Set("Content-Type", "text/markdown")
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=600")
|
||||||
_, err = io.WriteString(w, "# ")
|
_, err = io.WriteString(w, "# ")
|
||||||
_, err = io.WriteString(w, title)
|
_, err = io.WriteString(w, title)
|
||||||
_, err = io.WriteString(w, "\n\n")
|
_, err = io.WriteString(w, "\n\n")
|
||||||
|
@ -907,6 +944,18 @@ func createSearchIndex(dataDir, indexName string) (bleve.Index, error) {
|
||||||
indexDir := filepath.Join(dataDir, indexName)
|
indexDir := filepath.Join(dataDir, indexName)
|
||||||
if _, err := os.Stat(indexDir); os.IsNotExist(err) {
|
if _, err := os.Stat(indexDir); os.IsNotExist(err) {
|
||||||
indexMapping := bleve.NewIndexMapping()
|
indexMapping := bleve.NewIndexMapping()
|
||||||
|
documentMapping := bleve.NewDocumentMapping()
|
||||||
|
|
||||||
|
nameFieldMapping := bleve.NewTextFieldMapping()
|
||||||
|
nameFieldMapping.Store = true
|
||||||
|
documentMapping.AddFieldMappingsAt("name", nameFieldMapping)
|
||||||
|
|
||||||
|
titleFieldMapping := bleve.NewTextFieldMapping()
|
||||||
|
titleFieldMapping.Store = true
|
||||||
|
documentMapping.AddFieldMappingsAt("title", titleFieldMapping)
|
||||||
|
|
||||||
|
indexMapping.AddDocumentMapping("block", documentMapping)
|
||||||
|
|
||||||
searchIndex, err := bleve.New(indexDir, indexMapping)
|
searchIndex, err := bleve.New(indexDir, indexMapping)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -919,18 +968,19 @@ func createSearchIndex(dataDir, indexName string) (bleve.Index, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, page := range pages {
|
for _, page := range pages {
|
||||||
so, err := createSearchObject(page)
|
searchObjects, err := createSearchObjects(page.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = searchIndex.Index(page.Name, so)
|
for _, so := range searchObjects {
|
||||||
|
err = searchIndex.Index(so.ID, so)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return searchIndex, nil
|
return searchIndex, nil
|
||||||
} else {
|
} else {
|
||||||
searchIndex, err := bleve.Open(indexDir)
|
searchIndex, err := bleve.Open(indexDir)
|
||||||
|
|
227
render.go
227
render.go
|
@ -1,227 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Renderer interface {
|
|
||||||
Render(w io.Writer) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Block struct {
|
|
||||||
Type string
|
|
||||||
Data json.RawMessage
|
|
||||||
|
|
||||||
renderer Renderer
|
|
||||||
}
|
|
||||||
|
|
||||||
type Paragraph struct {
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Code struct {
|
|
||||||
Code string
|
|
||||||
}
|
|
||||||
|
|
||||||
type List struct {
|
|
||||||
Style string
|
|
||||||
Items []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Header struct {
|
|
||||||
Level int
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChecklistItem struct {
|
|
||||||
Text string
|
|
||||||
Checked bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Checklist struct {
|
|
||||||
Style string
|
|
||||||
Items []ChecklistItem
|
|
||||||
}
|
|
||||||
|
|
||||||
type Link struct {
|
|
||||||
Link string
|
|
||||||
Meta LinkResponseMeta
|
|
||||||
}
|
|
||||||
|
|
||||||
type Table struct {
|
|
||||||
Content [][]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Document struct {
|
|
||||||
Time int64
|
|
||||||
Version string
|
|
||||||
Blocks []Block
|
|
||||||
}
|
|
||||||
|
|
||||||
func (block Block) getType() (Renderer, error) {
|
|
||||||
switch block.Type {
|
|
||||||
case "table":
|
|
||||||
return &Table{}, nil
|
|
||||||
case "link":
|
|
||||||
return &Link{}, nil
|
|
||||||
case "list":
|
|
||||||
return &List{}, nil
|
|
||||||
case "header":
|
|
||||||
return &Header{}, nil
|
|
||||||
case "paragraph":
|
|
||||||
return &Paragraph{}, nil
|
|
||||||
case "code":
|
|
||||||
return &Code{}, nil
|
|
||||||
case "checklist":
|
|
||||||
return &Checklist{}, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown type: %s", block.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (block Block) load() (Renderer, error) {
|
|
||||||
if block.renderer != nil {
|
|
||||||
return block.renderer, nil
|
|
||||||
}
|
|
||||||
renderer, err := block.getType()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(block.Data, renderer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
block.renderer = renderer
|
|
||||||
|
|
||||||
return renderer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (document *Document) Render(w io.Writer) error {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
for _, block := range document.Blocks {
|
|
||||||
renderer, err := block.load()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = renderer.Render(&buf); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, err := buf.WriteTo(w)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderJSON(text string) (string, error) {
|
|
||||||
var document Document
|
|
||||||
err := json.Unmarshal([]byte(text), &document)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err = document.Render(&buf)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return buf.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (checklist *Checklist) Render(w io.Writer) error {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
buf.WriteString(`<div class="checklist">`)
|
|
||||||
for _, item := range checklist.Items {
|
|
||||||
buf.WriteString(`<div class="checklist--item">`)
|
|
||||||
|
|
||||||
buf.WriteString(`<span class="icon is-medium">`)
|
|
||||||
if item.Checked {
|
|
||||||
buf.WriteString(`<i class="fa fa-check-circle has-text-success"></i>`)
|
|
||||||
} else {
|
|
||||||
buf.WriteString(`<i class="fa fa-circle-thin"></i>`)
|
|
||||||
}
|
|
||||||
buf.WriteString(`</span>`)
|
|
||||||
buf.WriteString(`<div class="checklist--item-text">`)
|
|
||||||
buf.WriteString(item.Text)
|
|
||||||
buf.WriteString("</div>")
|
|
||||||
buf.WriteString("</div>")
|
|
||||||
}
|
|
||||||
buf.WriteString("</div>")
|
|
||||||
_, err := buf.WriteTo(w)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (code *Code) Render(w io.Writer) error {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
buf.WriteString("<pre>")
|
|
||||||
buf.WriteString(code.Code)
|
|
||||||
buf.WriteString("</pre>")
|
|
||||||
_, err := buf.WriteTo(w)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (link *Link) Render(w io.Writer) error {
|
|
||||||
// TODO(peter): improve link rendering
|
|
||||||
_, err := fmt.Fprintf(w, `<a href=%q>%s</a>`, link.Link, link.Meta.Title)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (table *Table) Render(w io.Writer) error {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
buf.WriteString("<table class='table'>")
|
|
||||||
for _, row := range table.Content {
|
|
||||||
buf.WriteString("<tr>")
|
|
||||||
for _, col := range row {
|
|
||||||
buf.WriteString("<td>")
|
|
||||||
buf.WriteString(col)
|
|
||||||
buf.WriteString("</td>")
|
|
||||||
}
|
|
||||||
buf.WriteString("</tr>")
|
|
||||||
}
|
|
||||||
buf.WriteString("</table>")
|
|
||||||
_, err := buf.WriteTo(w)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (list *List) Render(w io.Writer) error {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
var tag string
|
|
||||||
if list.Style == "ordered" {
|
|
||||||
tag = "ol"
|
|
||||||
} else {
|
|
||||||
tag = "ul"
|
|
||||||
}
|
|
||||||
buf.WriteString("<")
|
|
||||||
buf.WriteString(tag)
|
|
||||||
buf.WriteString(">")
|
|
||||||
for _, item := range list.Items {
|
|
||||||
buf.WriteString("<li>")
|
|
||||||
buf.WriteString(item)
|
|
||||||
buf.WriteString("</li>")
|
|
||||||
}
|
|
||||||
buf.WriteString("</")
|
|
||||||
buf.WriteString(tag)
|
|
||||||
buf.WriteString(">")
|
|
||||||
|
|
||||||
_, err := buf.WriteTo(w)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (header *Header) Render(w io.Writer) error {
|
|
||||||
_, err := fmt.Fprintf(w, "<h%d>%s</h%d>", header.Level, header.Text, header.Level)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (para Paragraph) Render(w io.Writer) error {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
buf.WriteString("<p>")
|
|
||||||
buf.WriteString(para.Text)
|
|
||||||
buf.WriteString("</p>")
|
|
||||||
_, err := buf.WriteTo(w)
|
|
||||||
return err
|
|
||||||
}
|
|
81
search.go
81
search.go
|
@ -94,13 +94,40 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, page := range pages {
|
for _, page := range pages {
|
||||||
|
err = saveBlocksFromPage("data", page)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error while processing blocks from page %s: %w", page.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload all pages
|
||||||
|
pages, err = mp.AllPages()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, page := range pages {
|
||||||
|
log.Println("processing ", page.Title)
|
||||||
err = processBackrefsForPage(page, refs)
|
err = processBackrefsForPage(page, refs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("error while processing backrefs: ", err)
|
log.Println("error while processing backrefs: ", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Println("saveLinks")
|
||||||
|
err = saveLinks(mp)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error while saving links %w", err)
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("saveBackrefs")
|
||||||
err = saveBackrefs("data/backrefs.json", refs)
|
err = saveBackrefs("data/backrefs.json", refs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error while saving backrefs %w", err)
|
log.Printf("error while saving backrefs %w", err)
|
||||||
|
@ -115,26 +142,27 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
indexMapping := bleve.NewIndexMapping()
|
index, err := createSearchIndex("data", "_tmp_index")
|
||||||
index, err := bleve.New("data/_tmp_index", indexMapping)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, page := range pages {
|
for _, page := range pages {
|
||||||
so, err := createSearchObject(page)
|
searchObjects, err := createSearchObjects(page.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error while createing search object %s: %w", page.Title, err)
|
log.Printf("error while creating search object %s: %w", page.Title, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = index.Index(page.Name, so)
|
for _, so := range searchObjects {
|
||||||
|
err = index.Index(so.ID, so)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error while indexing %s: %w", page.Title, err)
|
log.Printf("error while indexing %s: %w", page.Title, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = os.Rename("data/_page-index", "data/_page-index-old")
|
err = os.Rename("data/_page-index", "data/_page-index-old")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -170,6 +198,9 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
q := bleve.NewQueryStringQuery(r.URL.Query().Get("q"))
|
q := bleve.NewQueryStringQuery(r.URL.Query().Get("q"))
|
||||||
sr := bleve.NewSearchRequest(q)
|
sr := bleve.NewSearchRequest(q)
|
||||||
|
sr.Fields = []string{"page", "title", "text"}
|
||||||
|
sr.Highlight = bleve.NewHighlightWithStyle("html")
|
||||||
|
sr.Highlight.AddField("text")
|
||||||
results, err := s.searchIndex.Search(sr)
|
results, err := s.searchIndex.Search(sr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
|
@ -182,7 +213,45 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSearchObject(page Page) (searchObject, error) {
|
type pageBlock struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Page string `json:"page"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pageBlock) Type() string {
|
||||||
|
return "block"
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSearchObjects(rootBlockID string) ([]pageBlock, error) {
|
||||||
|
blocks, err := loadBlocks("data", rootBlockID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pageBlocks []pageBlock
|
||||||
|
|
||||||
|
queue := []string{blocks.PageID}
|
||||||
|
|
||||||
|
for len(queue) > 0 {
|
||||||
|
current := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
|
||||||
|
pageBlocks = append(pageBlocks, pageBlock{
|
||||||
|
ID: current,
|
||||||
|
Title: blocks.Texts[blocks.PageID],
|
||||||
|
Page: blocks.PageID,
|
||||||
|
Text: blocks.Texts[current],
|
||||||
|
})
|
||||||
|
|
||||||
|
queue = append(queue, blocks.Children[current]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageBlocks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createStructuredFormat(page Page) (searchObject, error) {
|
||||||
so := searchObject{}
|
so := searchObject{}
|
||||||
so.Title = page.Title
|
so.Title = page.Title
|
||||||
so.Meta = make(map[string]string)
|
so.Meta = make(map[string]string)
|
||||||
|
|
|
@ -6,6 +6,13 @@
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
|
<nav class="breadcrumb">
|
||||||
|
<ol>
|
||||||
|
{{ range $p := .Parents }}
|
||||||
|
<li><a href="/edit/{{ $p.ID }}">{{ $p.Text }}</a></li>
|
||||||
|
{{ end }}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
<h1 class="title">{{ .Title }}</h1>
|
<h1 class="title">{{ .Title }}</h1>
|
||||||
<form action="/save/" method="post">
|
<form action="/save/" method="post">
|
||||||
<input type="hidden" name="p" value="{{ .Name }}"/>
|
<input type="hidden" name="p" value="{{ .Name }}"/>
|
||||||
|
|
|
@ -1 +1,9 @@
|
||||||
<div class="wiki-list-editor" data-url="{{ .BaseURL }}{{ .Page }}?format=json" data-input="{{ .Data }}" data-saveurl="/save/" data-base-url="{{ .BaseURL }}" data-page="{{ .Page }}" save-type="{{ .ContentType }}"></div>
|
<div class="wiki-list-editor page-loader"
|
||||||
|
data-base-url="{{ .BaseURL }}"
|
||||||
|
data-edit="true"
|
||||||
|
data-format="json"
|
||||||
|
data-page="{{ .Page }}"
|
||||||
|
data-saveurl="/save/"
|
||||||
|
data-url="{{ .BaseURL }}{{ .Page }}?format=json"
|
||||||
|
save-type="{{ .ContentType }}"
|
||||||
|
></div>
|
||||||
|
|
|
@ -12,201 +12,8 @@
|
||||||
<title>{{ .Title }} - Wiki</title>
|
<title>{{ .Title }} - Wiki</title>
|
||||||
{{ block "content_head" . }} {{ end }}
|
{{ block "content_head" . }} {{ end }}
|
||||||
<style>
|
<style>
|
||||||
.content {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
#autocomplete {
|
|
||||||
z-index: 1;
|
|
||||||
right: 0;
|
|
||||||
width: 400px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
height: 300px;
|
|
||||||
position: absolute;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
#autocomplete li > a {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
#autocomplete li {
|
|
||||||
padding: 4px 16px;
|
|
||||||
}
|
|
||||||
#autocomplete li.selected {
|
|
||||||
background: lightblue;
|
|
||||||
}
|
|
||||||
#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;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content input[type="checkbox"] {
|
|
||||||
vertical-align: text-top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lighter {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
del {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
ins {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checklist {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checklist--item {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checklist--item-text {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<style>
|
<style>
|
||||||
@import url('https://rsms.me/inter/inter.css');
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.input-line {
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
@supports (font-variation-settings: normal) {
|
|
||||||
html {
|
|
||||||
font-family: 'Inter var', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter var', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.input-line {
|
|
||||||
font-family: 'Inter var', sans-serif;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.root .list-item {
|
|
||||||
padding: 3px;
|
|
||||||
padding-left: 12px;
|
|
||||||
display: flex;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 32px;
|
|
||||||
flex-direction: column;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
border: none;
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-item .content {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected {
|
|
||||||
background: lightblue;
|
|
||||||
}
|
|
||||||
.selected .marker {
|
|
||||||
border-color: lightblue;
|
|
||||||
}
|
|
||||||
#editor {
|
|
||||||
width: 750px;
|
|
||||||
}
|
|
||||||
.editor.selected .marker {
|
|
||||||
/*border-color: white;*/
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor.selected {
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line {
|
|
||||||
min-height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.input-line, input.input-line:active {
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gu-mirror {
|
|
||||||
position: fixed !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
z-index: 9999 !important;
|
|
||||||
opacity: 0.8;
|
|
||||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
|
|
||||||
filter: alpha(opacity=80);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gu-hide {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gu-unselectable {
|
|
||||||
-webkit-user-select: none !important;
|
|
||||||
-moz-user-select: none !important;
|
|
||||||
-ms-user-select: none !important;
|
|
||||||
user-select: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gu-transit {
|
|
||||||
opacity: 0.2;
|
|
||||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
|
|
||||||
filter: alpha(opacity=20);
|
|
||||||
}
|
|
||||||
|
|
||||||
.backrefs {
|
|
||||||
padding: 24px;
|
|
||||||
background: #deeeee;
|
|
||||||
border-top: 3px solid #acc;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body data-base-url="{{ .BaseURL }}">
|
<body data-base-url="{{ .BaseURL }}">
|
||||||
|
@ -272,7 +79,7 @@
|
||||||
<div id="result-template" class="hide">
|
<div id="result-template" class="hide">
|
||||||
<ul>
|
<ul>
|
||||||
[[#results]]
|
[[#results]]
|
||||||
<li><a href="/edit/[[ref]]">[[title]]</a></li>
|
<li><a href="/edit/[[ref]]">[[title]] <div>[[& text]]</div></a></li>
|
||||||
[[/results]]
|
[[/results]]
|
||||||
[[^results]]
|
[[^results]]
|
||||||
<li>No results</li>
|
<li>No results</li>
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
{{ define "sidebar" }}
|
{{ define "sidebar" }}
|
||||||
<div class="sidebar page-loader content" data-page="Sidebar"></div>
|
<div class="sidebar page-loader content" data-page="Sidebar" data-format="markdown"></div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user