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'
import {all, create} from 'mathjs'
import formulaFunctions from './formula'
import actions from './actions'
moment.locale('nl')
let math;
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')
element.classList.remove('error')
}, timeout * 1000);
},
addError(message) {
element.classList.add('error')
this.setText(message)
},
setText(text) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
element.classList.remove('hide')
element.innerText = text;
},
}
}
function addIndicator(editor, indicator) {
return {
save() {
return editor.save()
.then(() => {
console.log('success while saving')
indicator.setText('saved!');
indicator.done();
return true
})
.catch(reason => {
console.warn('error while saving: ' + reason)
indicator.addError('error!')
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)
}
let templateText = he.decode(document.getElementById(template).innerHTML);
let 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 formatLineResult(hit) {
return [
{
text: "[[" + hit.title + "]]",
indented: 0,
fold: 'open',
hidden: false,
fleeting: true
},
{
text: hit.line,
indented: 1,
fold: 'open',
hidden: false,
fleeting: true
}
]
}
function formatTitleResult(hit) {
return [
{
text: "[[" + hit.title + "]]",
indented: 0,
fold: 'open',
hidden: false,
fleeting: true
}
]
}
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 el(tag, children = []) {
let el = document.createElement(tag)
_.each(children, item => {
if (item !== undefined) el.appendChild(item)
})
return el
}
function mflatten(rowData) {
return Promise.all(rowData.reduce(function (a, b) {
return a.concat(b);
}, []));
}
function flatten(rowData) {
return rowData.reduce(function (a, b) {
return a.concat(b);
}, []);
}
function Editor(holder, input) {
function renderInline(cellText) {
if (!cellText) return document.createTextNode('')
MD.options.html = true
let rendered = MD.renderInline(cellText)
let span = $("" + rendered + "")
MD.options.html = false
return span[0];
}
function promiseMapAll(p, f) {
return p.then(all => Promise.all(_.map(all, f)))
}
function transformTable(editor, id, element) {
editor.treeForId(id)
.then(tree => {
let header = _.find(tree[0].children, c => c.text === 'headers') || []
let rows = _.find(tree[0].children, c => c.text === 'rows') || []
let collection = _.map(rows.children, child => {
let res = child.text.match(/{{query(!?):\s*([^}]+)}}/)
if (res && res[1] === '!') {
return search.startQuery(res[2])
.then(hits => _.uniqBy(_.flatMap(hits, formatTitleResult), _.property('text')))
}
return Promise.resolve().then(() => [child])
});
Promise.all(collection)
.then(_.flatten)
.then(_.partialRight(_.map, _.property('text')))
.then(_.uniq)
.then(rowTexts => {
return _.map(rowTexts, rowText => {
let page = rowText.substring(2).substring(0, rowText.length - 4)
return fetch('/' + page + '?format=metakv')
.then(res => res.ok ? res.json() : {meta: {}})
.then(res => res.meta)
.then(rowData => {
rowData.title = rowData.title || rowText
return el("tr", [
el("td", [renderInline(rowText)]),
..._.map(header.children, col => {
let td = el("td")
let value = rowData[_.snakeCase(_.trim(col.text))];
if (col.children && col.children.length > 0) {
value = col.children[0].text
}
transform(value ? value.replace(/^:/, '=') : '', $(td), id, editor, rowData)
return td
})
])
})
})
})
.then(mflatten)
.then(trs => {
return el("table", [
el("thead", [
el("tr", [
el("th", [
document.createTextNode("Title")
]),
..._.map(header.children, col => {
return el("th", [
document.createTextNode(col.text)
])
})
]),
]),
el("tbody", trs)
])
})
.then(table => {
table.classList.add('table')
table.classList.add('wiki-table')
let div = el('div', [table])
div.classList.add('table-wrapper')
return element.html(div);
})
})
}
function transformMathExpression(converted, scope) {
try {
if (math === undefined) {
math = create(all)
math.import(formulaFunctions)
}
let expr = converted.substring(1);
let parsedExpr = math.parse(expr)
let compiled = parsedExpr.compile()
let evaluated = compiled.evaluate(scope);
if (parsedExpr.isAssignmentNode) {
converted = parsedExpr.object.name + " = " + evaluated.toString() + ""
} else {
converted = "" + expr + " = " + evaluated.toString() + ""
}
} catch (e) {
converted = converted + ' ' + e.message + '';
}
return converted;
}
function transform(text, element, id, editor, scope) {
if (text === undefined) {
return;
}
if (!scope) {
scope = editor.scope || {}
}
let converted = text
let todo;
if (converted === '{{table}}') {
transformTable.call(this, editor, id, element);
return
} else if (converted.startsWith("```", 0) || converted.startsWith("$$", 0)) {
converted = MD.render(converted)
} else if (converted.startsWith("=", 0)) {
converted = transformMathExpression(converted, scope);
} else {
let re = /^([A-Z0-9 ]+)::\s*(.+)$/i;
let res = text.match(re)
if (res) {
converted = '**[[' + res[1] + ']]**: ' + res[2]
} else if (text.match(/#\[\[TODO]]/)) {
converted = converted.replace('#[[TODO]]', '')
todo = true;
} else if (text.match(/#\[\[DONE]]/)) {
converted = converted.replace('#[[DONE]]', '')
todo = false;
}
MD.options.html = true
converted = MD.renderInline(converted)
MD.options.html = false
}
if (todo !== undefined) {
element.toggleClass('todo--done', todo === false)
element.toggleClass('todo--todo', todo === true)
} else {
element.removeClass(['todo--todo', 'todo--done'])
}
element.html(converted)
}
const options = {
transform
}
let inputData = input ? input : JSON.parse(holder.dataset.input)
let editor = listEditor(holder, inputData, options);
holder.$listEditor = editor
editor.scope = {}
editor.actions = actions
$(holder).on('click', '.content input[type="checkbox"]', function (event) {
let that = this
let li = $(this).parents('.list-item');
let id = li.attr('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
});
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;
addIndicator(
addSaver(editor, saveUrl, page, () => indicator.setText('saving...')),
indicator
).save().then(() => indicator.done())
})
// 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 && results.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, id) {
let $input = $(input);
$input.parents('.list-item').removeClass('active')
$('#link-complete').off()
// PrismJS.highlightAll()
// mermaid.init()
// renderGraphs();
if (!$input.val()) return
let query = $input.val()
Promise.any([
match(query, /{{query(!?):\s*([^}]+)}}/)
])
.then(res => {
if (res[1] === '!') {
return search.startQuery(res[2])
.then(hits => _.uniqBy(_.flatMap(hits, formatTitleResult), _.property('text')))
.then(results => editor.replaceChildren(id, results))
.finally(() => editor.render())
} else {
return search.startQuery(res[2])
.then(hits => _.flatMap(hits, formatLineResult))
.then(results => editor.replaceChildren(id, results))
.finally(() => editor.render())
}
})
.catch(() => console.log('match error'))
});
return editor
})
}
function match(s, re) {
return new Promise((resolve, reject) => {
let res = s.match(re)
if (res) resolve(res)
else reject()
});
}
let searchInput = document.getElementById('search-input');
_.tap(search.search(searchInput), searcher => {
let showSearch = _.debounce(function (searcher) {
let query = $(searchInput).val()
if (query === '') {
$('#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;