Add blocks backend
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
c4bd5107eb
commit
5cc4e65638
@ -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;
|
@ -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;
|