Add blocks backend
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Peter Stuifzand 2020-10-21 20:49:23 +02:00
parent c4bd5107eb
commit 5cc4e65638
18 changed files with 1278 additions and 1031 deletions

View File

@ -16,13 +16,6 @@ type Reference struct {
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
func processBackrefs(dirname string, page Page) error {
@ -131,11 +124,11 @@ func getBackrefs(fp *FilePages, p string) (map[string][]Backref, error) {
}
result[ref.Name] = append(result[ref.Name], Backref{
Name: ref.Name,
Title: title,
LineHTML: template.HTML(pageText),
Name: ref.Name,
Title: title,
LineHTML: template.HTML(pageText),
LineEditHTML: template.HTML(editPageText),
Line: strings.Map(removeBrackets, ref.Link.Line),
Line: strings.Map(removeBrackets, ref.Link.Line),
})
}

434
editor/src/editor.js Normal file
View 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;

View File

@ -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 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 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'
@ -22,13 +9,10 @@ import 'prismjs/components/prism-perl'
import 'prismjs/components/prism-css'
import 'prismjs/components/prism-markup-templating'
import 'prismjs/components/prism-jq'
import menu from './menu.js'
import './styles.scss'
import wikiGraph from './graph'
import { create, all } from 'mathjs'
const math = create(all)
import Editor from './editor'
import MD from './markdown'
import 'katex/dist/katex.min.css'
moment.locale('nl')
mermaid.initialize({startOnLoad: true})
@ -40,122 +24,6 @@ PrismJS.plugins.filterHighlightAll.reject.addSelector('.language-dot')
// 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
*/
@ -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")
.forEach((el, key, parent) => {
fetch('/' + el.dataset.page + '?format=markdown').then(res => res.text()).then(text => {
el.innerHTML = MD.render(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)
}
})
})

25
editor/src/markdown.js Normal file
View 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;

View File

@ -44,6 +44,14 @@ function connectContextMenu(editor) {
})
},
className: 'action-copy-line'
},
zoom: {
name: 'Zoom in',
callback: function (key, opt) {
editor.zoomin(this).then(id => {
location.href = '/edit/'+id;
})
}
}
}
});

View File

@ -13,11 +13,13 @@ function search(element) {
let actualResult = [];
$.each(data.hits, (key, value) => {
actualResult.push({
ref: value.id,
title: value.id.replace(/_/g, ' '),
ref: value.fields.page,
title: value.fields.title,
text: value.fragments.text[0]
})
})
element.classList.remove('is-loading')
console.log(actualResult)
return actualResult
})
}

View File

@ -32,7 +32,9 @@
@import "~bulma/sass/layout/_all";
@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 {
font-family: 'Inter', sans-serif;
@ -237,3 +239,205 @@ mark {
.wiki-list-editor {
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;
}

537
file.go
View File

@ -9,9 +9,11 @@ import (
"html/template"
"io/ioutil"
"log"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
@ -20,8 +22,9 @@ import (
)
const (
DocumentsFile = "_documents.json"
LinksFile = "_links.json"
DocumentsFile = "_documents.json"
LinksFile = "_links.json"
BlocksDirectory = "_blocks"
)
type saveMessage struct {
@ -31,6 +34,75 @@ type saveMessage struct {
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 {
dirname string
saveC chan saveMessage
@ -38,7 +110,13 @@ type FilePages struct {
}
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}
go func() {
for msg := range fp.saveC {
err := fp.save(msg)
@ -50,38 +128,88 @@ func NewFilePages(dirname string, index bleve.Index) PagesRepository {
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 {
name := strings.Replace(title, " ", "_", -1)
title = strings.Replace(title, "_", " ", -1)
// TODO: cleanup loading of pages
// TODO: convert all pages to blocks
name := title
refs, err := getBackrefs(fp, name)
if err != nil {
refs = nil
}
f, err := os.Open(filepath.Join(fp.dirname, name))
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))
if err != nil {
return Page{
Title: title,
Name: name,
Content: "",
Refs: refs,
Blocks: blocks,
}
}
defer f.Close()
body, err := ioutil.ReadAll(f)
if err != nil {
return Page{
Title: title,
Name: name,
Content: "",
Refs: refs,
Blocks: blocks,
}
}
return Page{
Title: title,
Name: name,
Content: "",
Title: title,
Content: string(body),
Refs: refs,
Blocks: blocks,
}
}
defer f.Close()
body, err := ioutil.ReadAll(f)
if err != nil {
return Page{
Title: title,
Name: name,
Content: "",
Refs: refs,
}
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: title,
Content: string(body),
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 {
var sw stopwatch
sw.Start("prepare")
p := msg.p
page := msg.page
summary := msg.summary
@ -101,56 +228,275 @@ func (fp *FilePages) save(msg saveMessage) error {
page.Name = strings.Replace(p, " ", "_", -1)
page.Title = strings.Replace(p, "_", " ", -1)
f, err := os.Create(filepath.Join(fp.dirname, strings.Replace(p, " ", "_", -1)))
if err != nil {
return err
}
defer f.Close()
if page.Content[0] == '{' || page.Content[0] == '[' {
var buf bytes.Buffer
err = json.Indent(&buf, []byte(page.Content), "", " ")
if err != nil {
return err
}
_, err = buf.WriteTo(f)
if err != nil {
return err
}
} else {
f.WriteString(strings.Replace(page.Content, "\r\n", "\n", -1))
}
sw.Stop()
sw.Start("backrefs")
err = processBackrefs(fp.dirname, page)
if err != nil {
return fmt.Errorf("while processing backrefs: %s", err)
}
sw.Stop()
if p[0] != '_' {
sw.Start("prepare")
sw.Start("git")
err = saveWithGit(fp, p, summary, author)
if err != nil {
return fmt.Errorf("while saving to git: %w", err)
}
sw.Stop()
sw.Start("index")
so, err := createSearchObject(page)
if err != nil {
return fmt.Errorf("while creating search object %s: %w", page.Name, err)
}
if fp.index != nil {
err = fp.index.Index(page.Name, so)
f, err := os.Create(filepath.Join(fp.dirname, strings.Replace(p, " ", "_", -1)))
if err != nil {
return fmt.Errorf("while indexing %s: %w", page.Name, err)
return err
}
defer f.Close()
if page.Content[0] == '{' || page.Content[0] == '[' {
var buf bytes.Buffer
err = json.Indent(&buf, []byte(page.Content), "", " ")
if err != nil {
return err
}
_, err = buf.WriteTo(f)
if err != nil {
return err
}
} else {
f.WriteString(strings.Replace(page.Content, "\r\n", "\n", -1))
}
sw.Stop()
sw.Start("backrefs")
err = processBackrefs(fp.dirname, page)
if err != nil {
return fmt.Errorf("while processing backrefs: %s", err)
}
sw.Stop()
sw.Start("git")
err = saveWithGit(fp, p, summary, author)
if err != nil {
log.Printf("Error while saving to git: %w", err)
// return fmt.Errorf("while saving to git: %w", err)
}
sw.Stop()
sw.Start("index")
searchObjects, err := createSearchObjects(page.Name)
if err != nil {
return fmt.Errorf("while creating search object %s: %w", page.Name, err)
}
for _, so := range searchObjects {
if fp.index != nil {
err = fp.index.Index(so.ID, so)
if err != nil {
return fmt.Errorf("while indexing %s: %w", page.Name, err)
}
}
}
sw.Stop()
sw.Start("links")
err = saveLinksIncremental(fp.dirname, page.Title)
sw.Stop()
}
sw.Start("create blocks")
err := saveBlocksFromPage(fp.dirname, page)
if err != nil {
log.Println(err)
}
sw.Stop()
sw.Start("links")
err = saveLinksIncremental(fp.dirname, page.Title)
sw.Stop()
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 {
type Document struct {
Title string `json:"title"`
@ -189,19 +535,19 @@ func saveLinksIncremental(dirname, title string) error {
return nil
}
func saveLinks(fp *FilePages) error {
func saveLinks(mp PagesRepository) error {
type Document struct {
Title string `json:"title"`
}
var results []Document
pages, err := mp.(*FilePages).AllPages()
pages, err := mp.AllPages()
if err != nil {
return err
}
for _, page := range pages {
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 {
return err
}
@ -213,65 +559,6 @@ func saveLinks(fp *FilePages) error {
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 {
cmd := exec.Command("git", "add", ".")
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.Dir = fp.dirname
// cmd.Stderr = os.Stderr
// cmd.Stdout = os.Stderr
err = cmd.Run()
if err != nil {
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) {
log.Println("AllPages", fp.dirname)
files, err := ioutil.ReadDir(fp.dirname)
if err != nil {
return nil, err

1
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/blevesearch/bleve v1.0.9
github.com/blevesearch/cld2 v0.0.0-20200327141045-8b5f551d37f5 // 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/golang/protobuf v1.4.2 // indirect
github.com/ikawaha/kagome.ipadic v1.1.2 // indirect

View File

@ -275,8 +275,6 @@ function editor(root, inputData, options) {
if (store.hasChanged()) {
resolve(store.tree(from))
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) {
events[evt].push(handler)
}
@ -452,8 +459,8 @@ function editor(root, inputData, options) {
return true
})
$(root).on('click', '.list-item', function () {
let currentIndex = $(root).children('div.list-item').index(this)
$(root).on('click', '.content', function (event) {
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) {
return true
}
@ -509,7 +516,8 @@ function editor(root, inputData, options) {
saveTree,
copy,
update,
start
start,
zoomin
};
}

View File

@ -195,7 +195,7 @@ function Store(inputData) {
let index = _.findIndex(idList, (id) => id === currentId)
let oldValue = _.clone(values[currentId])
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
changed = true
}

68
main.go
View File

@ -52,7 +52,9 @@ type Page struct {
Content string
Refs map[string][]Backref
Refs map[string][]Backref
Blocks BlockResponse
Parent string
}
type DiffPage struct {
@ -123,6 +125,11 @@ type graphPage struct {
Edges template.JS
}
type Parent struct {
Text string
ID string
}
type editPage struct {
pageBaseInfo
Session *Session
@ -133,6 +140,8 @@ type editPage struct {
Backrefs map[string][]Backref
ShowGraph bool
TodayPage string
Parent Parent
Parents []Parent
}
type historyPage struct {
@ -445,11 +454,27 @@ func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
pageBase := getPageBase()
title := cleanTitle(page)
title := cleanTitle(mpPage.Title)
if newTitle, err := PageTitle(pageText); err == nil {
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{
pageBaseInfo: pageBase,
Session: sess,
@ -460,6 +485,8 @@ func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Backrefs: mpPage.Refs,
TodayPage: "Today",
ShowGraph: page != "Daily_Notes",
Parent: parent,
Parents: parents,
}
templates := baseTemplate
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)
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)
if err != nil {
log.Println(err)
@ -595,7 +624,7 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
mpPage := mp.Get(page)
pageText := mpPage.Content
if pageText == "" {
if (format == "" || format == "html") && pageText == "" {
http.Redirect(w, r, "/edit/"+page, 302)
return
}
@ -612,6 +641,8 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
jsonPage := pageText != "" && err == nil
if jsonPage {
if format == "json" {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
// Shortcut for json output
_, err := io.WriteString(w, pageText)
if err != nil {
@ -619,11 +650,13 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
return
} else if format == "metakv" {
so, err := createSearchObject(mpPage)
so, err := createStructuredFormat(mpPage)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
err = enc.Encode(so)
@ -701,6 +734,8 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), 500)
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)
if err != nil {
log.Println(err)
@ -708,6 +743,8 @@ func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
} 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, title)
_, err = io.WriteString(w, "\n\n")
@ -907,6 +944,18 @@ func createSearchIndex(dataDir, indexName string) (bleve.Index, error) {
indexDir := filepath.Join(dataDir, indexName)
if _, err := os.Stat(indexDir); os.IsNotExist(err) {
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)
if err != nil {
return nil, err
@ -919,16 +968,17 @@ func createSearchIndex(dataDir, indexName string) (bleve.Index, error) {
}
for _, page := range pages {
so, err := createSearchObject(page)
searchObjects, err := createSearchObjects(page.Name)
if err != nil {
log.Println(err)
continue
}
err = searchIndex.Index(page.Name, so)
if err != nil {
return nil, err
for _, so := range searchObjects {
err = searchIndex.Index(so.ID, so)
if err != nil {
return nil, err
}
}
}
return searchIndex, nil

227
render.go
View File

@ -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
}

View File

@ -94,13 +94,40 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), 500)
return
}
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)
if err != nil {
log.Println("error while processing backrefs: ", err)
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)
if err != nil {
log.Printf("error while saving backrefs %w", err)
@ -115,24 +142,25 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
indexMapping := bleve.NewIndexMapping()
index, err := bleve.New("data/_tmp_index", indexMapping)
index, err := createSearchIndex("data", "_tmp_index")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
for _, page := range pages {
so, err := createSearchObject(page)
searchObjects, err := createSearchObjects(page.Name)
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
}
err = index.Index(page.Name, so)
if err != nil {
log.Printf("error while indexing %s: %w", page.Title, err)
continue
for _, so := range searchObjects {
err = index.Index(so.ID, so)
if err != nil {
log.Printf("error while indexing %s: %w", page.Title, err)
continue
}
}
}
@ -170,6 +198,9 @@ func (s *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
q := bleve.NewQueryStringQuery(r.URL.Query().Get("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)
if err != nil {
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.Title = page.Title
so.Meta = make(map[string]string)

View File

@ -6,6 +6,13 @@
{{define "content"}}
<div class="columns">
<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>
<form action="/save/" method="post">
<input type="hidden" name="p" value="{{ .Name }}"/>

View File

@ -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>

View File

@ -12,201 +12,8 @@
<title>{{ .Title }} - Wiki</title>
{{ block "content_head" . }} {{ end }}
<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>
@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>
</head>
<body data-base-url="{{ .BaseURL }}">
@ -272,7 +79,7 @@
<div id="result-template" class="hide">
<ul>
[[#results]]
<li><a href="/edit/[[ref]]">[[title]]</a></li>
<li><a href="/edit/[[ref]]">[[title]] <div>[[&amp; text]]</div></a></li>
[[/results]]
[[^results]]
<li>No results</li>

View File

@ -1,3 +1,3 @@
{{ 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 }}