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;